Skip to content

fix(pm): read decorated Restart-when: / Blocked-by: directive lines, and stop prescribing a close for an unparsed one - #10204

Merged
qq9340100 merged 2 commits into
mainfrom
claude/issue-10102-decorated-directives
Aug 20, 2026
Merged

fix(pm): read decorated Restart-when: / Blocked-by: directive lines, and stop prescribing a close for an unparsed one#10204
qq9340100 merged 2 commits into
mainfrom
claude/issue-10102-decorated-directives

Conversation

@qq9340100

Copy link
Copy Markdown
Collaborator

Fixes#10102

Ruled scope: option C (tolerate decoration AND soften the remedy text), plus the channel-asymmetry documentation from the round-30 comment.

The defect

Restart-when: and Blocked-by: are machine-readable lines written by hand in markdown, so authors code-format them. The old anchors (^\s* for H9, ^[ \t]* for H4/H14) accept only whitespace before the key, and a backtick is not whitespace — so a decorated directive read as an absent one. Two confirmed instances, on both predicates, failing in opposite directions.

I measured both against the live, unedited card bodies (fetched from the API, run through the predicate at origin/main33024d9 and at this branch):

cardline in the body, verbatimbeforeafter
#9591 (pm:on-hold)`Restart-when: the v18 major development cycle opens …`H9 FINDING — "no Restart-when: body line"clean, value parsed as the v18 major development cycle opens (…) with no trailing backtick
#10063 (pm:blocked)`Blocked-by: #9612`hasBlockedByLinefalse, index targets []true, index edge {repo: null, number: 9612}

Neither card was edited — the ruling forbids appeasing the scanner, and the point is that both were correctly authored all along.

The two harms want different urgencies, which is why B alone was insufficient: the first is loud and destructive (H9's own remedy text told the reading seat to close a maintainer-commissioned card), the second is silent and permanent (the unlock sweep greps that same literal, so nothing could ever have returned the card — no row, no alarm, no expiry, and no detection channel of its own).

What changed

1. One shared decorated-directive reader (directiveValues), used by H9, blockedByTargets (the H14 index) and hasBlockedByLine (H4) — so a decoration tolerated for one is tolerated for all. This deliberately includes the comment-channel read H4 gained from PR #10075: it goes through the same two functions, so fixing only the body read would have propagated the blindness into the newer surface.

Tolerated: indentation, one -/* list bullet, and any run of `/** markers — including **Blocked-by**:, **Blocked-by:** and `Blocked-by:` (the marker may sit either side of the colon). A trailing marker matching one that opened the line is stripped from the captured value.

Deliberately not tolerated, and pinned as negatives: the key's spelling and case (the unlock scan greps the literal, so restart-when: must still flag); a bullet without its space (-Blocked-by: is a word, not a list item); anything mid-sentence (the prefix stays line-anchored, so 「seats park the `Blocked-by: #1` line in comments」 is still prose); and a line whose "value" is only its own closing marker — the extractor drops empties after cleaning, because stripping can empty a value.

2. H9's remedy text is re-ordered: verify → unwrap → add → and only then close. Tolerating decoration removes the measured cause but cannot remove the class — any future unparsed spelling produces the same "no line" row — so the row now names the possibility it cannot rule out ("a line that IS there but which this scan cannot parse looks exactly like an absent one") instead of offering a close as a co-equal branch. The manual row deliberately carries no such hedge: a line was read there, and hedging the one row that really does mean "this hold has no exit" would make it read as uncertain.

3. The channel asymmetry is documented rather than closed — my call, per ruling item 3. H4/H14 read body OR comment; H9 reads the body only. Documenting, because (a) H9's body-only read is correct on its own terms: the unlock scan greps issue bodies, so a condition parked in a comment genuinely does not exist to the machinery, and that population is exactly what H9 exists to surface — widening it would hide it; (b) H4's widening rests on a measurement H9 has no counterpart for (26 of 40 blocked cards body-clean, 2026-08-19) plus a sweep taught to read both; (c) the LOCATION half of this family belongs to #8941, which is explicitly not closed by this card and is not closed by it here either. It is stated in three places: the H4/H9 header rows, both predicates' section docs, and — because a code comment does not reach the seat reading a row — in H9's finding sentence itself. The stale claim that H9 used "the same single body channel" as Blocked-by: is corrected; it had been false since PR #10075 landed.

Tests

scripts/pm/check-half-states.mjs self-test: 489 → 558 cases (+69), green at 781690e.

Every decoration has a positive and a negative case, on each reader it reaches (directiveValues, H9, H4 body, H4 comment, blockedByTargets, commentBlockedByTargets). #9591's line is pinned by its measured shape, and #10063's likewise.

Three separate ablations, each mutated and restored with the change confirmed on disk by grep before and after (no build step — this is a plain .mjs the self-test runs directly):

ablationresultreads as
decoration markers → never-match27 of 558 failevery backtick/bold positive; all negatives stay green
bullet branch → never-match13 of 558 failevery bullet positive, and nothing else
trailing-marker strip → no-op12 of 558 failvalues keep `/**, and a valueless `Restart-when:` returns null — a false clear on a hold with no exit

The third is the one worth reading: it shows the strip is not cosmetic. Without it, decoration tolerance would have traded a false positive for a false negative on the same predicate.

One pre-existing test was made non-throwing ([0].repo.map(…).join(',')): an assertion that throws when the reader returns nothing aborts the whole self-test and hides every case after it, which is exactly what a regression in this reader produces. The first ablation run hit that and reported one crash instead of 27 failures.

Gates

Re-derived on the actual diff with node scripts/pm/dispatch-gates.mjs (no paths passed — it takes its own change set from the merge base), run at 781690e:

  • pnpm check:pm-half-states✓ check-half-states self-test: 558 cases pass.
  • pnpm check:cross-package-test-inputsOK: 12 package(s) read outside themselves, all declared…
  • pnpm check:partof-closing-keyword✓ check-partof-closing-keyword self-test: 28 cases pass.
  • pnpm check:nul-bytescheck-nul-bytes: OK (scanned 6074 text file(s)…; no raw ASCII control bytes).

⚠️Declared narrowing: the sixth derived family, node scripts/pm/check-half-states.mjs (half-state-patrol.yml), is the live sweep and cannot run in this container — it exits 3 with PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential. That is an environment limit, not a result. Its substance is covered another way above: I fetched #9591 and #10063 anonymously and ran the changed predicates against those real bodies directly.

No changeset — scripts/pm/ is internal agent tooling and publishes nothing user-visible (skip-changeset).

Not in this PR


Generated by Claude Code

@qq9340100qq9340100 added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 20, 2026 — with Claude
@qq9340100
qq9340100 marked this pull request as ready for review August 20, 2026 13:57
@qq9340100
qq9340100 enabled auto-merge August 20, 2026 13:57
@qq9340100
qq9340100 added this pull request to the merge queueAug 20, 2026
Merged via the queue into main with commit a815ee4Aug 20, 2026
26 checks passed
@qq9340100
qq9340100 deleted the claude/issue-10102-decorated-directives branch August 20, 2026 14:23
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

@qq9340100@claude