fix(pm): tell a completed clause-② review from a gate that never ran - #14174

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-14155-pair-cleared-state
Sep 1, 2026
Merged

fix(pm): tell a completed clause-② review from a gate that never ran#14174
os-sam merged 1 commit into
mainfrom
claude/issue-14155-pair-cleared-state

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#14155

check-clause2-carriers.mjs --pair answered C3 / exit 4 on a legitimately cleared clause-② pair. A Clause-②: yes declaration is history — it stays on the thread forever. The gate label is state — a completed review clears it from both carriers, by rule. So every clause-② pair that completes its review lands in exactly C3's trigger shape: declared yes, gate on neither carrier. Read literally, precondition ② of the landing check could only be satisfied in the window between the review PASS and the label clear — the wrong order — and each landed pair kept its C3 row on every later sweep.

What changed

C3 (and therefore --pair) now reads the needs:contract-reviewlabel event stream on both carriers and answers four distinguishable states:

events sayverdictexit
never hung on either carrierC3 fires — the fail-open, unchanged4
hung then cleared on both, head unmoved sincecompleted state — clean0
hung then cleared on both, head moved sinceadverse: the re-hang-owed state4
bound on one carrier onlyadverse: one removal where a clear leaves two — the strip signature4
stream or head commit unreadableUNJUDGED, never clean2

This mechanizes the recovery rule in references/contract-review.md — 「PASS + 无标 + head 未动 = 已清标非被剥;head 后移或无结论才重挂」 — minus its PASS conjunct, which stays human. Precondition ① is still a person reading the PASS comment on the card.

The three standing refusals are intact

The exit register keeps its shape — 0/4/3(/2), 4 for pair-adverse, never 0-with-a-message — and the docblock's exit table is updated to match. Exit 2 already existed for --pair; it now also covers an unreadable event stream.

Cost

Event streams are fetched only for pairs already in C3's candidate shape (declared yes, bare on both carriers), and the head commit only once both carriers read cleared. needsGateHistory is the single predicate the live reader and the UNJUDGED accounting share, so the set that owes a stream and the set that gets one cannot drift apart. Measured live: full sweep 8s, 9 pairs, 0 UNJUDGED.

Two readings that shaped the implementation:

  • The event endpoint returns rows OLDEST FIRST, so a stream read short loses the removal and reads as a live hang or a never-hung gate. The reader pages to exhaustion and hands null — UNJUDGED — when it cannot.
  • Head motion is read from the head commit's committer date, one exact uncapped request. The unforgeable alternative (head_ref_force_pushed) exists only on the timeline endpoint, which caps at 250 events and truncates silently; a capped read backing a clean verdict is the fail-open shape this family refuses everywhere else. The residual hole — a force-push landing a deliberately backdated commit — is named at the reading site, and the human PASS conjunct is what closes it.

The gate label constant and the label-event vocabulary are imported from check-half-states.mjs rather than restated, so this row cannot disagree with H31/H35 about what a gate event is. The repo-wide, windowed form of the one-carrier question stays H35's; this row is the per-pair form and is unbounded in time, and it points at H35 rather than re-judging it.

Verification

Reverse-verified in two legs, each mutate → prove on disk → run → restore → prove restored (hash equals the HEAD blob, git diff HEAD empty):

Self-test grew 64 → 98 cases, with fixture pairs for all four states replayed from the 2026-09-01 measurement (card #13657 hung 16:55:54Z / cleared 08:54:47Z; PR #13864 hung 15:19:53Z / cleared 08:54:56Z — nine seconds apart; head 9af92aa3 dated 08:16:59Z). lint.yml still runs the self-test only; the network sweep stays out of CI.

Gates at final HEAD 260a96e4d, exit codes captured before any pipe: the full derived union (dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, 15 commands, re-derived at final HEAD and identical) passes, plus repo-wide pnpm lint (eslint . --no-inline-config, exit 0, full scan — no narrowing). check-test-completeness is NOT MEASURED rather than green: it grades a saved turbo run test log that CI tees and no standalone invocation can produce.

scripts/pm/**-only diff — publishes nothing from any package, so skip-changeset rather than a changeset file.

Generated by Claude Code


Generated by Claude Code

`--pair` answered C3 / exit 4 on a legitimately cleared pair: a
`Clause-②: yes` declaration is history and stays on the thread, while the
label is state that a completed review clears from both carriers by rule,
so every pair that completes its review landed in exactly C3's trigger
shape. Read literally, the landing check's precondition (2) could only be
satisfied between the review PASS and the label clear — the wrong order.
C3 now reads the `needs:contract-review` label EVENT STREAM on both
carriers and answers four distinguishable states: never hung (the
fail-open, unchanged), hung-then-cleared with the head unmoved (the
completed state, clean), hung-then-cleared with the head moved since (the
re-hang-owed state), and bound on one carrier only (the strip signature).
An unreadable stream is UNJUDGED, never clean.
No verdict comment is read: the PASS conjunct of the recovery rule stays
human. No spelling is relaxed, and the tool still writes nothing. Event
streams are fetched only for pairs already in C3's candidate shape, so
sweep cost stays bounded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Msg17tAHJ3jVTYFgHydCm2
@os-samClaude

Copy link
Copy Markdown
Collaborator

Ready + auto-merge armed — provenance per the landing discipline.


Generated by Claude Code

@os-sam
os-sam added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit da839baSep 1, 2026
34 checks passed
@os-sam
os-sam deleted the claude/issue-14155-pair-cleared-state branch September 1, 2026 10:40
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(pm): tell a completed clause-② review from a gate that never ran - #14174

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-14155-pair-cleared-state
Sep 1, 2026
Merged

fix(pm): tell a completed clause-② review from a gate that never ran#14174
os-sam merged 1 commit into
mainfrom
claude/issue-14155-pair-cleared-state

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#14155

check-clause2-carriers.mjs --pair answered C3 / exit 4 on a legitimately cleared clause-② pair. A Clause-②: yes declaration is history — it stays on the thread forever. The gate label is state — a completed review clears it from both carriers, by rule. So every clause-② pair that completes its review lands in exactly C3's trigger shape: declared yes, gate on neither carrier. Read literally, precondition ② of the landing check could only be satisfied in the window between the review PASS and the label clear — the wrong order — and each landed pair kept its C3 row on every later sweep.

What changed

C3 (and therefore --pair) now reads the needs:contract-reviewlabel event stream on both carriers and answers four distinguishable states:

events sayverdictexit
never hung on either carrierC3 fires — the fail-open, unchanged4
hung then cleared on both, head unmoved sincecompleted state — clean0
hung then cleared on both, head moved sinceadverse: the re-hang-owed state4
bound on one carrier onlyadverse: one removal where a clear leaves two — the strip signature4
stream or head commit unreadableUNJUDGED, never clean2

This mechanizes the recovery rule in references/contract-review.md — 「PASS + 无标 + head 未动 = 已清标非被剥;head 后移或无结论才重挂」 — minus its PASS conjunct, which stays human. Precondition ① is still a person reading the PASS comment on the card.

The three standing refusals are intact

The exit register keeps its shape — 0/4/3(/2), 4 for pair-adverse, never 0-with-a-message — and the docblock's exit table is updated to match. Exit 2 already existed for --pair; it now also covers an unreadable event stream.

Cost

Event streams are fetched only for pairs already in C3's candidate shape (declared yes, bare on both carriers), and the head commit only once both carriers read cleared. needsGateHistory is the single predicate the live reader and the UNJUDGED accounting share, so the set that owes a stream and the set that gets one cannot drift apart. Measured live: full sweep 8s, 9 pairs, 0 UNJUDGED.

Two readings that shaped the implementation:

  • The event endpoint returns rows OLDEST FIRST, so a stream read short loses the removal and reads as a live hang or a never-hung gate. The reader pages to exhaustion and hands null — UNJUDGED — when it cannot.
  • Head motion is read from the head commit's committer date, one exact uncapped request. The unforgeable alternative (head_ref_force_pushed) exists only on the timeline endpoint, which caps at 250 events and truncates silently; a capped read backing a clean verdict is the fail-open shape this family refuses everywhere else. The residual hole — a force-push landing a deliberately backdated commit — is named at the reading site, and the human PASS conjunct is what closes it.

The gate label constant and the label-event vocabulary are imported from check-half-states.mjs rather than restated, so this row cannot disagree with H31/H35 about what a gate event is. The repo-wide, windowed form of the one-carrier question stays H35's; this row is the per-pair form and is unbounded in time, and it points at H35 rather than re-judging it.

Verification

Reverse-verified in two legs, each mutate → prove on disk → run → restore → prove restored (hash equals the HEAD blob, git diff HEAD empty):

Self-test grew 64 → 98 cases, with fixture pairs for all four states replayed from the 2026-09-01 measurement (card #13657 hung 16:55:54Z / cleared 08:54:47Z; PR #13864 hung 15:19:53Z / cleared 08:54:56Z — nine seconds apart; head 9af92aa3 dated 08:16:59Z). lint.yml still runs the self-test only; the network sweep stays out of CI.

Gates at final HEAD 260a96e4d, exit codes captured before any pipe: the full derived union (dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, 15 commands, re-derived at final HEAD and identical) passes, plus repo-wide pnpm lint (eslint . --no-inline-config, exit 0, full scan — no narrowing). check-test-completeness is NOT MEASURED rather than green: it grades a saved turbo run test log that CI tees and no standalone invocation can produce.

scripts/pm/**-only diff — publishes nothing from any package, so skip-changeset rather than a changeset file.

Generated by Claude Code


Generated by Claude Code

`--pair` answered C3 / exit 4 on a legitimately cleared pair: a
`Clause-②: yes` declaration is history and stays on the thread, while the
label is state that a completed review clears from both carriers by rule,
so every pair that completes its review landed in exactly C3's trigger
shape. Read literally, the landing check's precondition (2) could only be
satisfied between the review PASS and the label clear — the wrong order.
C3 now reads the `needs:contract-review` label EVENT STREAM on both
carriers and answers four distinguishable states: never hung (the
fail-open, unchanged), hung-then-cleared with the head unmoved (the
completed state, clean), hung-then-cleared with the head moved since (the
re-hang-owed state), and bound on one carrier only (the strip signature).
An unreadable stream is UNJUDGED, never clean.
No verdict comment is read: the PASS conjunct of the recovery rule stays
human. No spelling is relaxed, and the tool still writes nothing. Event
streams are fetched only for pairs already in C3's candidate shape, so
sweep cost stays bounded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Msg17tAHJ3jVTYFgHydCm2
@os-samClaude

Copy link
Copy Markdown
Collaborator

Ready + auto-merge armed — provenance per the landing discipline.


Generated by Claude Code

@os-sam
os-sam added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit da839baSep 1, 2026
34 checks passed
@os-sam
os-sam deleted the claude/issue-14155-pair-cleared-state branch September 1, 2026 10:40
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(pm): tell a completed clause-② review from a gate that never ran - #14174

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-14155-pair-cleared-state
Sep 1, 2026
Merged

fix(pm): tell a completed clause-② review from a gate that never ran#14174
os-sam merged 1 commit into
mainfrom
claude/issue-14155-pair-cleared-state

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#14155

check-clause2-carriers.mjs --pair answered C3 / exit 4 on a legitimately cleared clause-② pair. A Clause-②: yes declaration is history — it stays on the thread forever. The gate label is state — a completed review clears it from both carriers, by rule. So every clause-② pair that completes its review lands in exactly C3's trigger shape: declared yes, gate on neither carrier. Read literally, precondition ② of the landing check could only be satisfied in the window between the review PASS and the label clear — the wrong order — and each landed pair kept its C3 row on every later sweep.

What changed

C3 (and therefore --pair) now reads the needs:contract-reviewlabel event stream on both carriers and answers four distinguishable states:

events sayverdictexit
never hung on either carrierC3 fires — the fail-open, unchanged4
hung then cleared on both, head unmoved sincecompleted state — clean0
hung then cleared on both, head moved sinceadverse: the re-hang-owed state4
bound on one carrier onlyadverse: one removal where a clear leaves two — the strip signature4
stream or head commit unreadableUNJUDGED, never clean2

This mechanizes the recovery rule in references/contract-review.md — 「PASS + 无标 + head 未动 = 已清标非被剥;head 后移或无结论才重挂」 — minus its PASS conjunct, which stays human. Precondition ① is still a person reading the PASS comment on the card.

The three standing refusals are intact

The exit register keeps its shape — 0/4/3(/2), 4 for pair-adverse, never 0-with-a-message — and the docblock's exit table is updated to match. Exit 2 already existed for --pair; it now also covers an unreadable event stream.

Cost

Event streams are fetched only for pairs already in C3's candidate shape (declared yes, bare on both carriers), and the head commit only once both carriers read cleared. needsGateHistory is the single predicate the live reader and the UNJUDGED accounting share, so the set that owes a stream and the set that gets one cannot drift apart. Measured live: full sweep 8s, 9 pairs, 0 UNJUDGED.

Two readings that shaped the implementation:

  • The event endpoint returns rows OLDEST FIRST, so a stream read short loses the removal and reads as a live hang or a never-hung gate. The reader pages to exhaustion and hands null — UNJUDGED — when it cannot.
  • Head motion is read from the head commit's committer date, one exact uncapped request. The unforgeable alternative (head_ref_force_pushed) exists only on the timeline endpoint, which caps at 250 events and truncates silently; a capped read backing a clean verdict is the fail-open shape this family refuses everywhere else. The residual hole — a force-push landing a deliberately backdated commit — is named at the reading site, and the human PASS conjunct is what closes it.

The gate label constant and the label-event vocabulary are imported from check-half-states.mjs rather than restated, so this row cannot disagree with H31/H35 about what a gate event is. The repo-wide, windowed form of the one-carrier question stays H35's; this row is the per-pair form and is unbounded in time, and it points at H35 rather than re-judging it.

Verification

Reverse-verified in two legs, each mutate → prove on disk → run → restore → prove restored (hash equals the HEAD blob, git diff HEAD empty):

Self-test grew 64 → 98 cases, with fixture pairs for all four states replayed from the 2026-09-01 measurement (card #13657 hung 16:55:54Z / cleared 08:54:47Z; PR #13864 hung 15:19:53Z / cleared 08:54:56Z — nine seconds apart; head 9af92aa3 dated 08:16:59Z). lint.yml still runs the self-test only; the network sweep stays out of CI.

Gates at final HEAD 260a96e4d, exit codes captured before any pipe: the full derived union (dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, 15 commands, re-derived at final HEAD and identical) passes, plus repo-wide pnpm lint (eslint . --no-inline-config, exit 0, full scan — no narrowing). check-test-completeness is NOT MEASURED rather than green: it grades a saved turbo run test log that CI tees and no standalone invocation can produce.

scripts/pm/**-only diff — publishes nothing from any package, so skip-changeset rather than a changeset file.

Generated by Claude Code


Generated by Claude Code

`--pair` answered C3 / exit 4 on a legitimately cleared pair: a
`Clause-②: yes` declaration is history and stays on the thread, while the
label is state that a completed review clears from both carriers by rule,
so every pair that completes its review landed in exactly C3's trigger
shape. Read literally, the landing check's precondition (2) could only be
satisfied between the review PASS and the label clear — the wrong order.
C3 now reads the `needs:contract-review` label EVENT STREAM on both
carriers and answers four distinguishable states: never hung (the
fail-open, unchanged), hung-then-cleared with the head unmoved (the
completed state, clean), hung-then-cleared with the head moved since (the
re-hang-owed state), and bound on one carrier only (the strip signature).
An unreadable stream is UNJUDGED, never clean.
No verdict comment is read: the PASS conjunct of the recovery rule stays
human. No spelling is relaxed, and the tool still writes nothing. Event
streams are fetched only for pairs already in C3's candidate shape, so
sweep cost stays bounded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Msg17tAHJ3jVTYFgHydCm2
@os-samClaude

Copy link
Copy Markdown
Collaborator

Ready + auto-merge armed — provenance per the landing discipline.


Generated by Claude Code

@os-sam
os-sam added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit da839baSep 1, 2026
34 checks passed
@os-sam
os-sam deleted the claude/issue-14155-pair-cleared-state branch September 1, 2026 10:40
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(pm): tell a completed clause-② review from a gate that never ran - #14174

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-14155-pair-cleared-state
Sep 1, 2026
Merged

fix(pm): tell a completed clause-② review from a gate that never ran#14174
os-sam merged 1 commit into
mainfrom
claude/issue-14155-pair-cleared-state

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#14155

check-clause2-carriers.mjs --pair answered C3 / exit 4 on a legitimately cleared clause-② pair. A Clause-②: yes declaration is history — it stays on the thread forever. The gate label is state — a completed review clears it from both carriers, by rule. So every clause-② pair that completes its review lands in exactly C3's trigger shape: declared yes, gate on neither carrier. Read literally, precondition ② of the landing check could only be satisfied in the window between the review PASS and the label clear — the wrong order — and each landed pair kept its C3 row on every later sweep.

What changed

C3 (and therefore --pair) now reads the needs:contract-reviewlabel event stream on both carriers and answers four distinguishable states:

events sayverdictexit
never hung on either carrierC3 fires — the fail-open, unchanged4
hung then cleared on both, head unmoved sincecompleted state — clean0
hung then cleared on both, head moved sinceadverse: the re-hang-owed state4
bound on one carrier onlyadverse: one removal where a clear leaves two — the strip signature4
stream or head commit unreadableUNJUDGED, never clean2

This mechanizes the recovery rule in references/contract-review.md — 「PASS + 无标 + head 未动 = 已清标非被剥;head 后移或无结论才重挂」 — minus its PASS conjunct, which stays human. Precondition ① is still a person reading the PASS comment on the card.

The three standing refusals are intact

The exit register keeps its shape — 0/4/3(/2), 4 for pair-adverse, never 0-with-a-message — and the docblock's exit table is updated to match. Exit 2 already existed for --pair; it now also covers an unreadable event stream.

Cost

Event streams are fetched only for pairs already in C3's candidate shape (declared yes, bare on both carriers), and the head commit only once both carriers read cleared. needsGateHistory is the single predicate the live reader and the UNJUDGED accounting share, so the set that owes a stream and the set that gets one cannot drift apart. Measured live: full sweep 8s, 9 pairs, 0 UNJUDGED.

Two readings that shaped the implementation:

  • The event endpoint returns rows OLDEST FIRST, so a stream read short loses the removal and reads as a live hang or a never-hung gate. The reader pages to exhaustion and hands null — UNJUDGED — when it cannot.
  • Head motion is read from the head commit's committer date, one exact uncapped request. The unforgeable alternative (head_ref_force_pushed) exists only on the timeline endpoint, which caps at 250 events and truncates silently; a capped read backing a clean verdict is the fail-open shape this family refuses everywhere else. The residual hole — a force-push landing a deliberately backdated commit — is named at the reading site, and the human PASS conjunct is what closes it.

The gate label constant and the label-event vocabulary are imported from check-half-states.mjs rather than restated, so this row cannot disagree with H31/H35 about what a gate event is. The repo-wide, windowed form of the one-carrier question stays H35's; this row is the per-pair form and is unbounded in time, and it points at H35 rather than re-judging it.

Verification

Reverse-verified in two legs, each mutate → prove on disk → run → restore → prove restored (hash equals the HEAD blob, git diff HEAD empty):

Self-test grew 64 → 98 cases, with fixture pairs for all four states replayed from the 2026-09-01 measurement (card #13657 hung 16:55:54Z / cleared 08:54:47Z; PR #13864 hung 15:19:53Z / cleared 08:54:56Z — nine seconds apart; head 9af92aa3 dated 08:16:59Z). lint.yml still runs the self-test only; the network sweep stays out of CI.

Gates at final HEAD 260a96e4d, exit codes captured before any pipe: the full derived union (dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, 15 commands, re-derived at final HEAD and identical) passes, plus repo-wide pnpm lint (eslint . --no-inline-config, exit 0, full scan — no narrowing). check-test-completeness is NOT MEASURED rather than green: it grades a saved turbo run test log that CI tees and no standalone invocation can produce.

scripts/pm/**-only diff — publishes nothing from any package, so skip-changeset rather than a changeset file.

Generated by Claude Code


Generated by Claude Code

`--pair` answered C3 / exit 4 on a legitimately cleared pair: a
`Clause-②: yes` declaration is history and stays on the thread, while the
label is state that a completed review clears from both carriers by rule,
so every pair that completes its review landed in exactly C3's trigger
shape. Read literally, the landing check's precondition (2) could only be
satisfied between the review PASS and the label clear — the wrong order.
C3 now reads the `needs:contract-review` label EVENT STREAM on both
carriers and answers four distinguishable states: never hung (the
fail-open, unchanged), hung-then-cleared with the head unmoved (the
completed state, clean), hung-then-cleared with the head moved since (the
re-hang-owed state), and bound on one carrier only (the strip signature).
An unreadable stream is UNJUDGED, never clean.
No verdict comment is read: the PASS conjunct of the recovery rule stays
human. No spelling is relaxed, and the tool still writes nothing. Event
streams are fetched only for pairs already in C3's candidate shape, so
sweep cost stays bounded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Msg17tAHJ3jVTYFgHydCm2
@os-samClaude

Copy link
Copy Markdown
Collaborator

Ready + auto-merge armed — provenance per the landing discipline.


Generated by Claude Code

@os-sam
os-sam added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit da839baSep 1, 2026
34 checks passed
@os-sam
os-sam deleted the claude/issue-14155-pair-cleared-state branch September 1, 2026 10:40
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(pm): tell a completed clause-② review from a gate that never ran - #14174

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-14155-pair-cleared-state
Sep 1, 2026
Merged

fix(pm): tell a completed clause-② review from a gate that never ran#14174
os-sam merged 1 commit into
mainfrom
claude/issue-14155-pair-cleared-state

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#14155

check-clause2-carriers.mjs --pair answered C3 / exit 4 on a legitimately cleared clause-② pair. A Clause-②: yes declaration is history — it stays on the thread forever. The gate label is state — a completed review clears it from both carriers, by rule. So every clause-② pair that completes its review lands in exactly C3's trigger shape: declared yes, gate on neither carrier. Read literally, precondition ② of the landing check could only be satisfied in the window between the review PASS and the label clear — the wrong order — and each landed pair kept its C3 row on every later sweep.

What changed

C3 (and therefore --pair) now reads the needs:contract-reviewlabel event stream on both carriers and answers four distinguishable states:

events sayverdictexit
never hung on either carrierC3 fires — the fail-open, unchanged4
hung then cleared on both, head unmoved sincecompleted state — clean0
hung then cleared on both, head moved sinceadverse: the re-hang-owed state4
bound on one carrier onlyadverse: one removal where a clear leaves two — the strip signature4
stream or head commit unreadableUNJUDGED, never clean2

This mechanizes the recovery rule in references/contract-review.md — 「PASS + 无标 + head 未动 = 已清标非被剥;head 后移或无结论才重挂」 — minus its PASS conjunct, which stays human. Precondition ① is still a person reading the PASS comment on the card.

The three standing refusals are intact

The exit register keeps its shape — 0/4/3(/2), 4 for pair-adverse, never 0-with-a-message — and the docblock's exit table is updated to match. Exit 2 already existed for --pair; it now also covers an unreadable event stream.

Cost

Event streams are fetched only for pairs already in C3's candidate shape (declared yes, bare on both carriers), and the head commit only once both carriers read cleared. needsGateHistory is the single predicate the live reader and the UNJUDGED accounting share, so the set that owes a stream and the set that gets one cannot drift apart. Measured live: full sweep 8s, 9 pairs, 0 UNJUDGED.

Two readings that shaped the implementation:

  • The event endpoint returns rows OLDEST FIRST, so a stream read short loses the removal and reads as a live hang or a never-hung gate. The reader pages to exhaustion and hands null — UNJUDGED — when it cannot.
  • Head motion is read from the head commit's committer date, one exact uncapped request. The unforgeable alternative (head_ref_force_pushed) exists only on the timeline endpoint, which caps at 250 events and truncates silently; a capped read backing a clean verdict is the fail-open shape this family refuses everywhere else. The residual hole — a force-push landing a deliberately backdated commit — is named at the reading site, and the human PASS conjunct is what closes it.

The gate label constant and the label-event vocabulary are imported from check-half-states.mjs rather than restated, so this row cannot disagree with H31/H35 about what a gate event is. The repo-wide, windowed form of the one-carrier question stays H35's; this row is the per-pair form and is unbounded in time, and it points at H35 rather than re-judging it.

Verification

Reverse-verified in two legs, each mutate → prove on disk → run → restore → prove restored (hash equals the HEAD blob, git diff HEAD empty):

Self-test grew 64 → 98 cases, with fixture pairs for all four states replayed from the 2026-09-01 measurement (card #13657 hung 16:55:54Z / cleared 08:54:47Z; PR #13864 hung 15:19:53Z / cleared 08:54:56Z — nine seconds apart; head 9af92aa3 dated 08:16:59Z). lint.yml still runs the self-test only; the network sweep stays out of CI.

Gates at final HEAD 260a96e4d, exit codes captured before any pipe: the full derived union (dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, 15 commands, re-derived at final HEAD and identical) passes, plus repo-wide pnpm lint (eslint . --no-inline-config, exit 0, full scan — no narrowing). check-test-completeness is NOT MEASURED rather than green: it grades a saved turbo run test log that CI tees and no standalone invocation can produce.

scripts/pm/**-only diff — publishes nothing from any package, so skip-changeset rather than a changeset file.

Generated by Claude Code


Generated by Claude Code

`--pair` answered C3 / exit 4 on a legitimately cleared pair: a
`Clause-②: yes` declaration is history and stays on the thread, while the
label is state that a completed review clears from both carriers by rule,
so every pair that completes its review landed in exactly C3's trigger
shape. Read literally, the landing check's precondition (2) could only be
satisfied between the review PASS and the label clear — the wrong order.
C3 now reads the `needs:contract-review` label EVENT STREAM on both
carriers and answers four distinguishable states: never hung (the
fail-open, unchanged), hung-then-cleared with the head unmoved (the
completed state, clean), hung-then-cleared with the head moved since (the
re-hang-owed state), and bound on one carrier only (the strip signature).
An unreadable stream is UNJUDGED, never clean.
No verdict comment is read: the PASS conjunct of the recovery rule stays
human. No spelling is relaxed, and the tool still writes nothing. Event
streams are fetched only for pairs already in C3's candidate shape, so
sweep cost stays bounded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Msg17tAHJ3jVTYFgHydCm2
@os-samClaude

Copy link
Copy Markdown
Collaborator

Ready + auto-merge armed — provenance per the landing discipline.


Generated by Claude Code

@os-sam
os-sam added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit da839baSep 1, 2026
34 checks passed
@os-sam
os-sam deleted the claude/issue-14155-pair-cleared-state branch September 1, 2026 10:40
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(pm): tell a completed clause-② review from a gate that never ran - #14174

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-14155-pair-cleared-state
Sep 1, 2026
Merged

fix(pm): tell a completed clause-② review from a gate that never ran#14174
os-sam merged 1 commit into
mainfrom
claude/issue-14155-pair-cleared-state

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#14155

check-clause2-carriers.mjs --pair answered C3 / exit 4 on a legitimately cleared clause-② pair. A Clause-②: yes declaration is history — it stays on the thread forever. The gate label is state — a completed review clears it from both carriers, by rule. So every clause-② pair that completes its review lands in exactly C3's trigger shape: declared yes, gate on neither carrier. Read literally, precondition ② of the landing check could only be satisfied in the window between the review PASS and the label clear — the wrong order — and each landed pair kept its C3 row on every later sweep.

What changed

C3 (and therefore --pair) now reads the needs:contract-reviewlabel event stream on both carriers and answers four distinguishable states:

events sayverdictexit
never hung on either carrierC3 fires — the fail-open, unchanged4
hung then cleared on both, head unmoved sincecompleted state — clean0
hung then cleared on both, head moved sinceadverse: the re-hang-owed state4
bound on one carrier onlyadverse: one removal where a clear leaves two — the strip signature4
stream or head commit unreadableUNJUDGED, never clean2

This mechanizes the recovery rule in references/contract-review.md — 「PASS + 无标 + head 未动 = 已清标非被剥;head 后移或无结论才重挂」 — minus its PASS conjunct, which stays human. Precondition ① is still a person reading the PASS comment on the card.

The three standing refusals are intact

The exit register keeps its shape — 0/4/3(/2), 4 for pair-adverse, never 0-with-a-message — and the docblock's exit table is updated to match. Exit 2 already existed for --pair; it now also covers an unreadable event stream.

Cost

Event streams are fetched only for pairs already in C3's candidate shape (declared yes, bare on both carriers), and the head commit only once both carriers read cleared. needsGateHistory is the single predicate the live reader and the UNJUDGED accounting share, so the set that owes a stream and the set that gets one cannot drift apart. Measured live: full sweep 8s, 9 pairs, 0 UNJUDGED.

Two readings that shaped the implementation:

  • The event endpoint returns rows OLDEST FIRST, so a stream read short loses the removal and reads as a live hang or a never-hung gate. The reader pages to exhaustion and hands null — UNJUDGED — when it cannot.
  • Head motion is read from the head commit's committer date, one exact uncapped request. The unforgeable alternative (head_ref_force_pushed) exists only on the timeline endpoint, which caps at 250 events and truncates silently; a capped read backing a clean verdict is the fail-open shape this family refuses everywhere else. The residual hole — a force-push landing a deliberately backdated commit — is named at the reading site, and the human PASS conjunct is what closes it.

The gate label constant and the label-event vocabulary are imported from check-half-states.mjs rather than restated, so this row cannot disagree with H31/H35 about what a gate event is. The repo-wide, windowed form of the one-carrier question stays H35's; this row is the per-pair form and is unbounded in time, and it points at H35 rather than re-judging it.

Verification

Reverse-verified in two legs, each mutate → prove on disk → run → restore → prove restored (hash equals the HEAD blob, git diff HEAD empty):

Self-test grew 64 → 98 cases, with fixture pairs for all four states replayed from the 2026-09-01 measurement (card #13657 hung 16:55:54Z / cleared 08:54:47Z; PR #13864 hung 15:19:53Z / cleared 08:54:56Z — nine seconds apart; head 9af92aa3 dated 08:16:59Z). lint.yml still runs the self-test only; the network sweep stays out of CI.

Gates at final HEAD 260a96e4d, exit codes captured before any pipe: the full derived union (dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, 15 commands, re-derived at final HEAD and identical) passes, plus repo-wide pnpm lint (eslint . --no-inline-config, exit 0, full scan — no narrowing). check-test-completeness is NOT MEASURED rather than green: it grades a saved turbo run test log that CI tees and no standalone invocation can produce.

scripts/pm/**-only diff — publishes nothing from any package, so skip-changeset rather than a changeset file.

Generated by Claude Code


Generated by Claude Code

`--pair` answered C3 / exit 4 on a legitimately cleared pair: a
`Clause-②: yes` declaration is history and stays on the thread, while the
label is state that a completed review clears from both carriers by rule,
so every pair that completes its review landed in exactly C3's trigger
shape. Read literally, the landing check's precondition (2) could only be
satisfied between the review PASS and the label clear — the wrong order.
C3 now reads the `needs:contract-review` label EVENT STREAM on both
carriers and answers four distinguishable states: never hung (the
fail-open, unchanged), hung-then-cleared with the head unmoved (the
completed state, clean), hung-then-cleared with the head moved since (the
re-hang-owed state), and bound on one carrier only (the strip signature).
An unreadable stream is UNJUDGED, never clean.
No verdict comment is read: the PASS conjunct of the recovery rule stays
human. No spelling is relaxed, and the tool still writes nothing. Event
streams are fetched only for pairs already in C3's candidate shape, so
sweep cost stays bounded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Msg17tAHJ3jVTYFgHydCm2
@os-samClaude

Copy link
Copy Markdown
Collaborator

Ready + auto-merge armed — provenance per the landing discipline.


Generated by Claude Code

@os-sam
os-sam added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit da839baSep 1, 2026
34 checks passed
@os-sam
os-sam deleted the claude/issue-14155-pair-cleared-state branch September 1, 2026 10:40
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(pm): tell a completed clause-② review from a gate that never ran - #14174

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-14155-pair-cleared-state
Sep 1, 2026
Merged

fix(pm): tell a completed clause-② review from a gate that never ran#14174
os-sam merged 1 commit into
mainfrom
claude/issue-14155-pair-cleared-state

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#14155

check-clause2-carriers.mjs --pair answered C3 / exit 4 on a legitimately cleared clause-② pair. A Clause-②: yes declaration is history — it stays on the thread forever. The gate label is state — a completed review clears it from both carriers, by rule. So every clause-② pair that completes its review lands in exactly C3's trigger shape: declared yes, gate on neither carrier. Read literally, precondition ② of the landing check could only be satisfied in the window between the review PASS and the label clear — the wrong order — and each landed pair kept its C3 row on every later sweep.

What changed

C3 (and therefore --pair) now reads the needs:contract-reviewlabel event stream on both carriers and answers four distinguishable states:

events sayverdictexit
never hung on either carrierC3 fires — the fail-open, unchanged4
hung then cleared on both, head unmoved sincecompleted state — clean0
hung then cleared on both, head moved sinceadverse: the re-hang-owed state4
bound on one carrier onlyadverse: one removal where a clear leaves two — the strip signature4
stream or head commit unreadableUNJUDGED, never clean2

This mechanizes the recovery rule in references/contract-review.md — 「PASS + 无标 + head 未动 = 已清标非被剥;head 后移或无结论才重挂」 — minus its PASS conjunct, which stays human. Precondition ① is still a person reading the PASS comment on the card.

The three standing refusals are intact

The exit register keeps its shape — 0/4/3(/2), 4 for pair-adverse, never 0-with-a-message — and the docblock's exit table is updated to match. Exit 2 already existed for --pair; it now also covers an unreadable event stream.

Cost

Event streams are fetched only for pairs already in C3's candidate shape (declared yes, bare on both carriers), and the head commit only once both carriers read cleared. needsGateHistory is the single predicate the live reader and the UNJUDGED accounting share, so the set that owes a stream and the set that gets one cannot drift apart. Measured live: full sweep 8s, 9 pairs, 0 UNJUDGED.

Two readings that shaped the implementation:

  • The event endpoint returns rows OLDEST FIRST, so a stream read short loses the removal and reads as a live hang or a never-hung gate. The reader pages to exhaustion and hands null — UNJUDGED — when it cannot.
  • Head motion is read from the head commit's committer date, one exact uncapped request. The unforgeable alternative (head_ref_force_pushed) exists only on the timeline endpoint, which caps at 250 events and truncates silently; a capped read backing a clean verdict is the fail-open shape this family refuses everywhere else. The residual hole — a force-push landing a deliberately backdated commit — is named at the reading site, and the human PASS conjunct is what closes it.

The gate label constant and the label-event vocabulary are imported from check-half-states.mjs rather than restated, so this row cannot disagree with H31/H35 about what a gate event is. The repo-wide, windowed form of the one-carrier question stays H35's; this row is the per-pair form and is unbounded in time, and it points at H35 rather than re-judging it.

Verification

Reverse-verified in two legs, each mutate → prove on disk → run → restore → prove restored (hash equals the HEAD blob, git diff HEAD empty):

Self-test grew 64 → 98 cases, with fixture pairs for all four states replayed from the 2026-09-01 measurement (card #13657 hung 16:55:54Z / cleared 08:54:47Z; PR #13864 hung 15:19:53Z / cleared 08:54:56Z — nine seconds apart; head 9af92aa3 dated 08:16:59Z). lint.yml still runs the self-test only; the network sweep stays out of CI.

Gates at final HEAD 260a96e4d, exit codes captured before any pipe: the full derived union (dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, 15 commands, re-derived at final HEAD and identical) passes, plus repo-wide pnpm lint (eslint . --no-inline-config, exit 0, full scan — no narrowing). check-test-completeness is NOT MEASURED rather than green: it grades a saved turbo run test log that CI tees and no standalone invocation can produce.

scripts/pm/**-only diff — publishes nothing from any package, so skip-changeset rather than a changeset file.

Generated by Claude Code


Generated by Claude Code

`--pair` answered C3 / exit 4 on a legitimately cleared pair: a
`Clause-②: yes` declaration is history and stays on the thread, while the
label is state that a completed review clears from both carriers by rule,
so every pair that completes its review landed in exactly C3's trigger
shape. Read literally, the landing check's precondition (2) could only be
satisfied between the review PASS and the label clear — the wrong order.
C3 now reads the `needs:contract-review` label EVENT STREAM on both
carriers and answers four distinguishable states: never hung (the
fail-open, unchanged), hung-then-cleared with the head unmoved (the
completed state, clean), hung-then-cleared with the head moved since (the
re-hang-owed state), and bound on one carrier only (the strip signature).
An unreadable stream is UNJUDGED, never clean.
No verdict comment is read: the PASS conjunct of the recovery rule stays
human. No spelling is relaxed, and the tool still writes nothing. Event
streams are fetched only for pairs already in C3's candidate shape, so
sweep cost stays bounded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Msg17tAHJ3jVTYFgHydCm2
@os-samClaude

Copy link
Copy Markdown
Collaborator

Ready + auto-merge armed — provenance per the landing discipline.


Generated by Claude Code

@os-sam
os-sam added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit da839baSep 1, 2026
34 checks passed
@os-sam
os-sam deleted the claude/issue-14155-pair-cleared-state branch September 1, 2026 10:40
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(pm): tell a completed clause-② review from a gate that never ran - #14174

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-14155-pair-cleared-state
Sep 1, 2026
Merged

fix(pm): tell a completed clause-② review from a gate that never ran#14174
os-sam merged 1 commit into
mainfrom
claude/issue-14155-pair-cleared-state

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#14155

check-clause2-carriers.mjs --pair answered C3 / exit 4 on a legitimately cleared clause-② pair. A Clause-②: yes declaration is history — it stays on the thread forever. The gate label is state — a completed review clears it from both carriers, by rule. So every clause-② pair that completes its review lands in exactly C3's trigger shape: declared yes, gate on neither carrier. Read literally, precondition ② of the landing check could only be satisfied in the window between the review PASS and the label clear — the wrong order — and each landed pair kept its C3 row on every later sweep.

What changed

C3 (and therefore --pair) now reads the needs:contract-reviewlabel event stream on both carriers and answers four distinguishable states:

events sayverdictexit
never hung on either carrierC3 fires — the fail-open, unchanged4
hung then cleared on both, head unmoved sincecompleted state — clean0
hung then cleared on both, head moved sinceadverse: the re-hang-owed state4
bound on one carrier onlyadverse: one removal where a clear leaves two — the strip signature4
stream or head commit unreadableUNJUDGED, never clean2

This mechanizes the recovery rule in references/contract-review.md — 「PASS + 无标 + head 未动 = 已清标非被剥;head 后移或无结论才重挂」 — minus its PASS conjunct, which stays human. Precondition ① is still a person reading the PASS comment on the card.

The three standing refusals are intact

The exit register keeps its shape — 0/4/3(/2), 4 for pair-adverse, never 0-with-a-message — and the docblock's exit table is updated to match. Exit 2 already existed for --pair; it now also covers an unreadable event stream.

Cost

Event streams are fetched only for pairs already in C3's candidate shape (declared yes, bare on both carriers), and the head commit only once both carriers read cleared. needsGateHistory is the single predicate the live reader and the UNJUDGED accounting share, so the set that owes a stream and the set that gets one cannot drift apart. Measured live: full sweep 8s, 9 pairs, 0 UNJUDGED.

Two readings that shaped the implementation:

  • The event endpoint returns rows OLDEST FIRST, so a stream read short loses the removal and reads as a live hang or a never-hung gate. The reader pages to exhaustion and hands null — UNJUDGED — when it cannot.
  • Head motion is read from the head commit's committer date, one exact uncapped request. The unforgeable alternative (head_ref_force_pushed) exists only on the timeline endpoint, which caps at 250 events and truncates silently; a capped read backing a clean verdict is the fail-open shape this family refuses everywhere else. The residual hole — a force-push landing a deliberately backdated commit — is named at the reading site, and the human PASS conjunct is what closes it.

The gate label constant and the label-event vocabulary are imported from check-half-states.mjs rather than restated, so this row cannot disagree with H31/H35 about what a gate event is. The repo-wide, windowed form of the one-carrier question stays H35's; this row is the per-pair form and is unbounded in time, and it points at H35 rather than re-judging it.

Verification

Reverse-verified in two legs, each mutate → prove on disk → run → restore → prove restored (hash equals the HEAD blob, git diff HEAD empty):

Self-test grew 64 → 98 cases, with fixture pairs for all four states replayed from the 2026-09-01 measurement (card #13657 hung 16:55:54Z / cleared 08:54:47Z; PR #13864 hung 15:19:53Z / cleared 08:54:56Z — nine seconds apart; head 9af92aa3 dated 08:16:59Z). lint.yml still runs the self-test only; the network sweep stays out of CI.

Gates at final HEAD 260a96e4d, exit codes captured before any pipe: the full derived union (dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, 15 commands, re-derived at final HEAD and identical) passes, plus repo-wide pnpm lint (eslint . --no-inline-config, exit 0, full scan — no narrowing). check-test-completeness is NOT MEASURED rather than green: it grades a saved turbo run test log that CI tees and no standalone invocation can produce.

scripts/pm/**-only diff — publishes nothing from any package, so skip-changeset rather than a changeset file.

Generated by Claude Code


Generated by Claude Code

`--pair` answered C3 / exit 4 on a legitimately cleared pair: a
`Clause-②: yes` declaration is history and stays on the thread, while the
label is state that a completed review clears from both carriers by rule,
so every pair that completes its review landed in exactly C3's trigger
shape. Read literally, the landing check's precondition (2) could only be
satisfied between the review PASS and the label clear — the wrong order.
C3 now reads the `needs:contract-review` label EVENT STREAM on both
carriers and answers four distinguishable states: never hung (the
fail-open, unchanged), hung-then-cleared with the head unmoved (the
completed state, clean), hung-then-cleared with the head moved since (the
re-hang-owed state), and bound on one carrier only (the strip signature).
An unreadable stream is UNJUDGED, never clean.
No verdict comment is read: the PASS conjunct of the recovery rule stays
human. No spelling is relaxed, and the tool still writes nothing. Event
streams are fetched only for pairs already in C3's candidate shape, so
sweep cost stays bounded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Msg17tAHJ3jVTYFgHydCm2
@os-samClaude

Copy link
Copy Markdown
Collaborator

Ready + auto-merge armed — provenance per the landing discipline.


Generated by Claude Code

@os-sam
os-sam added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit da839baSep 1, 2026
34 checks passed
@os-sam
os-sam deleted the claude/issue-14155-pair-cleared-state branch September 1, 2026 10:40
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-sam@claude