Uh oh!
There was an error while loading. Please reload this page.
fix(pm): ungate the blocker-liveness comment read and pair it with a stale-body-line row (H28) - #11874
Merged
Merged
Conversation
… stale-body-line row (H28) `needsBlockedByComments()` returns false whenever the body carries any `Blocked-by:` line. That is correct for H4 — it asks only whether the line EXISTS, and a body line answers that without the network — but H19/H26 borrowed the same gathering to ask a different question: is what the line names still RUNNING. On a RE-PARK (body upstream closed, real prerequisite carded, new blocker written into a comment) the liveness read therefore resolved ONLY the spent body target, found it closed, and published "every target it names is closed" — a FALSE unlock candidate, on three consecutive sweeps, acted on once. The liveness read is now ungated (`needsBlockerLivenessComments` — the label alone), so every `pm:blocked` card contributes both channels. H4's gate is left exactly as-is; its cheap question stays cheap. Ungating alone only turns the false candidate into a PARTIAL one, so H28 is its pair: a body-named target that is CLOSED while a comment names an OPEN one that is absent from the body ⇒ the body line is stale, migrate the live blocker to the body (the canonical home). That enforces the canonical-home doctrine at the moment the wrong shape is written rather than by prose. Self-test 1058 -> 1102 cases, including both legs of the reverse verification over the measured card's recorded byte shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ
This was referenced Aug 24, 2026
hotlong
marked this pull request as ready for review
August 25, 2026 00:35
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#11747
Ruled option A from the re-charter (comment 5402127035), after the deliverable-3
diagnosis falsified the original charter's premise. The census's headline claim —
"the unlock scan greps bodies only" — is false on
main; the real defect runs theopposite way.
The defect
needsBlockedByComments()returnsfalsewhenever the body carries anyBlocked-by:line. That is correct for H4, which asks only whether the lineexists — a body line answers that without the network. But H19 and H26
borrowed the same gathering to ask a different question: is what the line names
still running?
On a re-park the two questions diverge. A seat that finds the body's upstream
closed, cards the real prerequisite, and writes the new blocker into a comment (the
cheaper, safer write) leaves a card whose body names a closed issue and whose
comment names an open one. The gated liveness read saw only the closed one,
found nothing else, and published "Every target it names is closed: nothing this
card declared a wait on is still running" — a false unlock candidate. It was
published on three consecutive sweeps and acted on once: a card was released to
pm:queuewhile its real blocker was open andpm:dispatched.The fix
The liveness read is ungated —
needsBlockerLivenessComments()is the labelalone, so every
pm:blockedcard contributes both channels unconditionally.H4's gate is left exactly as-is; its cheap question stays cheap. The
gathering is placed in the label loop, beside the H4 fetch, so it rides the same
shared comment cache (a card already fetched costs nothing) and so every fetch is
counted before the total-shortfall rethrow reads those stats. Cost is the gate's
own complement — one comment fetch per blocked card that has a body line, 15 of
33 in the 2026-08-24 census.
H28, the paired patrol row. Ungating alone only turns the false candidate
into a
PARTIALone; H19 then reports that the block is half-expired but saysnothing about which half is documentation, so the stale body line survives and
the next re-park writes the same shape again. H28 fires on the conjunction — a
body-named target that is CLOSED while a comment names one that is OPEN
and absent from the body — and asks for the live blocker to be migrated to the
body. That is what makes the canonical-home doctrine enforced at write time
rather than trusted to prose; prose is precisely what failed here.
Deliberately silent on each half alone: body-closed with no live comment target
is H19's ordinary expired block; two open targets in two channels is untidy, not
wrong; a target named in both channels is already in the canonical home and
is not a migration candidate; an unresolved target on either side stays with
H19's unjudged sentence rather than becoming a migration order built on a guess.
Free — the same resolutions H19/H26 already hold, asked a third question (which
channel each target arrived in).
references/state-machine.mdgains the two doc rows: the canonical marker(line-start
Blocked-by: #N/owner/repo#N, undecorated) with the 2026-08-24measurement recorded, and the re-derive-before-discharging rule. No ceiling
raise — the file sat at its ratchet ceiling (43, headroom 0) and the rows were
paid for by consolidation: three status notes that had gone stale or historical
were compressed, including one that said the
pm:retriageage predicate wasstill to land when H18 has shipped.
Reverse verification
Both legs run over the measured card's recorded byte shape — body
Blocked-by: #9255(closed 2026-08-19T11:28:26Z) plus the re-park comment statingthe new blocker in the backticked-key spelling the census recorded
(
`Blocked-by:`#11501, open). Fixtures, not live cards.#9255only1 of 1 ... CLOSED, "Every target it names is closed", neverPARTIAL#9255+#115011 of 2 ... PARTIAL, naming#11501as still openplus H28 firing alongside — naming the spent body target, the live comment target,
and the migration.
Two ablations confirm the new cases are load-bearing, each anchored on the exact
text changed and each with an
EXIT-trap restore:needsBlockerLivenesstoneedsBlockedByComments,injected 1 / deleted 0 on disk) turns the self-test red: "2 of 1102 case(s)
failed", on the gate cases.
first positive H28 assertion (exit 1). Direction noted honestly: it surfaces as a
TypeErroronnull.includesrather than named case failures — the samepredicate(...).includes(...)shape the neighbouring H26 block already uses.The tree was verified clean (
git status --porcelainempty) after each restore.Gates
Derived at commit
e6a8452ewithnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no path args — the script takes its own change set fromthe merge base). It printed "--repo 'objectstack-ai/objectstack' checked against
this checkout's 'origin' remote — it holds" and matched 17 families, all run,
all green at that commit. Exit codes captured before any pipe. Selected verdict
lines, each the gate's own:
✓ check-half-states self-test: 1102 cases pass.(was 1058)✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/references/state-machine.md is 43 lines (ceiling 43; headroom 0).check-nul-bytes: OK (scanned 6611 text file(s) ... no raw ASCII control bytes).check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords ...).✓ check-partof-closing-keyword self-test: 28 cases pass.✓ check-governed-merges --self-test: 129 assertions ...✓ check:entry-guard: 148 scripts/ file(s) — every entry guard goes through invoked-as.mjs ...✓ check:parse-guard: 147 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.✓ check-skill-id-lint: 22 file(s) clean✓ check-skill-frame-sync: 4 copies of the decision frame are structurally isomorphic across 3 files✓ check-agent-test-spelling: 0 violations — 357 file(s) ...✓ doc authoring guard: 389 files clean✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 421 files / 1448 TS blocks judged clean(needed@objectstack/lint+@objectstack/formulabuilt first)OK: 16 package(s) read outside themselves, all declared(cross-package-test-inputs)✓ check:pnpm-filter-targets: 135/168 --filter occurrence(s) ... resolveTwo gates first reported
PREREQUISITE NOT METin the fresh worktree (a missingyamldep, an unbuilt@objectstack/formula) — not findings; both green afterpnpm installand the dependency-closure build.skip-changesetapplies: the diff is.claude/**+scripts/pm/**only, so itpublishes nothing.
Notes for the reviewer
.claude/**) — draft, human merge. Not flipped ready, noauto-merge armed (PD feat: Comprehensive CRM example demonstrating all ObjectStack protocol features #14).
cases, and the one doc file — so finding(pm): pm:queue 半态「已判未转」——两条可机判的 H 规则,加 #11086 的一条输入 #11179's patrol extension merges cleanly on
top.
blockerTargetsFor's docblock had a "stated boundary" paragraph recordingthe old gate's bound as accepted; it is rewritten, since that bound was the defect.
#11179(unfolded per the re-charter, dispatched standalone) and#8941(the re-charter discharges it on its own measured evidence). Both remainopen as far as this PR is concerned — neither is a closing target here.
Generated by Claude Code