Skip to content

fix(scripts/pm): H52 stands down on a seat ANSWER a held card can actually write - #19436

Merged
os-steve merged 3 commits into
mainfrom
claude/issue-19160-h52-answer-on-thread-clears
Sep 20, 2026
Merged

os-steve merged 3 commits into
mainfrom
claude/issue-19160-h52-answer-on-thread-clears

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #19160

Clause-②: no

The defect

H52 stood down on exactly three acts: close the card, hang needs-user-decision, or post a newer os-dev-report whose open_questions is empty. A card no dev is dispatched to can perform none of the three — closing a defect card is forbidden (「缺陷卡 ⛔ 不藏进 hold 也不自行关闭」), the row's own remedy forbids re-hanging needs-user-decision for a residual raised while executing a ruling, and no newer report can exist because a pm:on-hold card is dispatched to nobody. The one act a seat CAN perform — answering the questions on the thread — was invisible, because latestDevReport reads only report comments.

That is the 「无机制可唤醒的卡 ⛔ 不 hold」 shape the charter refuses for a CARD, one layer up: a patrol row with no act that can ever clear it.

The answer shape — MEASURED from the two live answers, ⛔ not invented

Both live carriers already carry it, unedited. The pin is one ATX heading line that names the array and says ANSWERED:

card answering comment the heading line, verbatim
objectui#8348 5737496254 ### The two open questions, answered
objectui#9868 5736885341 ## Half-state patrol H52 — the two `open_questions` on this card are ANSWERED, and this comment is the record that stands the row down
export const H52_ANSWERED_HEADING =
  /^#{1,6}[ \t]+(?=[^\n]*\bopen[\s_`*]*questions?\b)(?=[^\n]*\banswered\b)/im;

LINE-anchored (m) where OS_DEV_REPORT_MARKER is body-anchored, because objectui#8348 carries its heading 32 lines into a seat review, so a body anchor would have read only one of the two. The ATX heading is what keeps that safe: the same words in prose are refused, and so is a blockquoted heading (a QUOTE of another card's answer). Both refusals are pinned, and both are ablated below.

Before / after, measured on the two REAL threads

Not a fixture — the live comment rows, fetched and handed to both versions of the predicate:

card pre-fix (488f4f5) post-fix (a978f79)
objectui#8348 (pm:on-hold, 14 comments) H52 fires clears
objectui#9868 (pm:on-hold, 7 comments) H52 fires clears

Neither card needs an edit: the answers they already carry are the record. Population unchanged, the needs-user-decision refusal unchanged, latestDevReport unchanged.

The four axes, per design choice

1. A fourth null-exit, ⛔ not a state-based exclusion. Business need: the two live instances are correctly held and correctly answered — what is missing is a reader, exactly as the row's own filing said of open_questions itself. Long-term: excluding pm:on-hold would delete the row's whole population argument (the census measured 14 on-hold carriers and 52 of 63 carriers outside pm:dispatched) and would silence a card whose question really is open. AI-error: an exit keyed on a DECLARED act keeps 「declared = enforced」; a state exclusion makes silence depend on a label nobody wrote for this purpose. Startup focus: no new surface — one reader beside an existing one.

2. The shape is measured from the two live answers, ⛔ not a fourth prose rule. Business need: both answers satisfy it with zero edits, so the fleet pays nothing and the shape is already in use. Long-term: a rule nobody has written yet would have no corpus behind it and would drift. AI-error: the ATX heading is a loud, structural declaration — the seat SAYS the questions are answered; prose that merely reads like an answer is refused, which is the strict-contract direction rather than lenient matching. Startup focus: one regex, no new protocol.

3. Ordering by the same rule the family already uses. Business need: a seat answer OLDER than the report does not clear — the report re-raised the questions after it. Long-term: latestMarkedComment (newest-of) and releaseAnswersClaim (is this the later record) are CALLED, not restated, so "which of two is newer" stays answered in one place for every marker. AI-error: a second hand-rolled comparator is where the next tie-break bug lives. Startup focus: zero new machinery.

4. A SIBLING reader, ⛔ never a change to latestDevReport. That reader is shared by other rows and returns exactly what it returned; the new exit is additive.

5. ⭐ A guard I wrote, ablated, and REMOVED as the phantom it was. The first draft excluded a report from being the answer with an explicit test. Ablating it left the suite green: the newest os-dev-report is BY CONSTRUCTION at least as new as any report carrying the heading, so the ordering rule already answers false and the guard could never change an answer. AI-error axis, decisively: a guard that can never fire teaches the next reader that the exclusion is enforced there, and the pin behind it was vacuous. Removed, with the proof recorded in the docblock; the pin now rides the ordering rule and the ordering ablation reds it.

Ablation — every behavioural pin proved non-vacuous

Each leg via scripts/ablation-replace.mjs (anchor must HIT; on-disk blob before/after; restore verified blob == HEAD and git diff HEAD empty).

leg mutation result
1 delete the fourth exit from the predicate RED — both live-heading pins fail
2 delete the report-exclusion guard GREEN ⇒ phantom; guard removed from the diff
3 releaseAnswersClaim(answer, report) to true RED — the OLDER pin and the REPORT pin fail
4 drop the ATX anchor from the shape RED — the PROSE pin and the QUOTE pin fail

Direction observed: turned red, on the legs that own each pin — leg 2 is the one that came back green, and that green is what removed a line of code rather than weakening a gate.

Verification

node scripts/pm/check-half-states.mjs --self-test exit 0 — 5142 cases on the merged tree (5124 at the branch point, +9 mine, +9 from the H59 row merged in). SELF_TEST_BATTERY_FLOOR untouched at 8; all eight batteries unchanged.

The derived union — node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at a978f79, no paths — listed 41 commands. Every one was run with its exit captured before any pipe; the union run and this reading are taken at that head.

Acceptance notes

  • Net +60 lines in scripts/pm/check-half-states.mjs (72 added / 12 deleted vs origin/main), inside the dispatch's +60 budget, self-test cases included.
  • skip-changeset: scripts/pm/** publishes nothing from any released package — the fast-track path, no measurement owed.
  • origin/main was merged once, after the H59 row (PR fix(scripts/pm): H59 reads state_reason so a deliberately REOPENED card is not called a platform failure #19427) landed on it; the two H-row regions are disjoint and the merge auto-resolved with no conflict.
  • noted, not filed: markerMatches undecorates backticks and asterisks but not a leading blockquote marker, which is exactly why the QUOTE refusal holds. That is load-bearing for this row and is now pinned here; 承接者: the next author of an undecoration spelling, who will find the pin.

Generated by Claude Code

…form

H52 stood down on exactly three acts: close the card, hang
`needs-user-decision`, or post a newer `os-dev-report` with an empty
`open_questions`. A card no dev is dispatched to can perform none of the
three, so the row re-fires forever on a correctly held card. Two live
carriers on the objectui board each answered the questions on the thread,
NEWER than the report, and re-fired on every sweep.

Add a fourth exit beside the other three: `H52_ANSWERED_HEADING` on a
NON-report comment newer than the latest report. The shape is MEASURED
from those two live answers, which carry it unedited — one ATX heading
line naming `open_questions` and saying ANSWERED. `latestDevReport` is
untouched; the new reader is a SIBLING that calls the same
`latestMarkedComment` / `releaseAnswersClaim` rules for ordering, so a
seat answer OLDER than the report still fires and a report carrying the
heading still governs by its own array.

Claude-Session: https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi
Co-authored-by: Claude <noreply@anthropic.com>
…ntom it was

Ablating the guard left the suite green: the newest `os-dev-report` is BY
CONSTRUCTION at least as new as any report carrying the answered heading,
so `releaseAnswersClaim` already answers false and the guard could never
change an answer. Removed, with the proof recorded in the docblock; the
pin that asserts a report never clears now rides the ordering rule, which
the ordering ablation reds.

Claude-Session: https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi
Co-authored-by: Claude <noreply@anthropic.com>
…anch

Region-disjoint on scripts/pm/check-half-states.mjs — H59 :14208–:14500,
H52 :11906–:12260 plus the H52 self-test band.

Claude-Session: https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi
Co-authored-by: Claude <noreply@anthropic.com>
@os-steve os-steve added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 20, 2026 — with Claude

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a978f79c39c32077e2af6ee6661fdc507c41832a

① Derived judgments

  • Published accept set / public surface: none moves — one report-only PM patrol reader (scripts/pm/check-half-states.mjs, +72 / −12, net +60 against the ≤ +60 budget); no published package, no schema, no label, zero new requests (the answering comment is in the rows the H52 loop already holds — the sweep call site verified); SELF_TEST_BATTERY_FLOOR untouched at 8. check-governed-merges --pr 19436: NOT governed — ordinary queue landing. check-widening-tells --declaration no: 1 file NOT MEASURED — the no stands on the seat's reading. Judged correct.
  • The fix, read line by line: H52 gains a FOURTH null-exit, h52AnswerStandsDown(commentRows) — the newest comment matching H52_ANSWERED_HEADING (an ATX heading line naming open_questions and saying ANSWERED, /^#{1,6}[ \t]+(?=…open[\s_*]*questions?…)(?=…answered…)/im) is newer than the newest os-dev-report, judged by the file's own ordering pair latestMarkedComment+releaseAnswersClaim(imported rules, not restated);latestDevReportuntouched — a sibling reader beside it; the remedy sentence now names the thread route beside the report route. The shape is MEASURED, not invented: the two live answers (objectui#83485737496254, objectui#9868 5736885341) already carry it unedited — seat-run on this head, both live headings match, while the same words in prose and a blockquoted copy of another card's answer do not (pinned). An answer OLDER than the report still fires (pinned); a report carrying the heading never clears (ordering refuses it — a guard the dev first wrote for this was proven a PHANTOM by ablation and removed, the right consequence); the needs-user-decision` refusal and the population are unchanged. Judged correct.
  • Evidence, seat-run on this head in a detached worktree: check-half-states.mjs --self-test exit 0 — 5142 cases (+9 here, +9 from finding(scripts/pm): H59 calls a deliberately REOPENED card a FALSE OPEN — state_reason: 'reopened' is on the payload it already reads, and H22 in the same file reads it #19177's row merged in); merge-tree vs origin/main 24162f9 (which now carries PR fix(pm): H26 claims no SCHEDULED or DECLARED release, not "no mechanism" — counted, not asserted #19426's H26 rewrite): 0 conflicts; CI on a978f79: 37 check runs, 1 still running, 0 red. The dev's before/after on the REAL live threads (rows handed to both versions of the predicate): both objectui carriers fire at the branch point and stand down at this head without an edit to either card; four ablation legs blob-hash-proven (three red on their own pins, one green that removed a line of code); the 41-family derived union 41 / 41 exit 0 with --ran reconciled (a DERIVED zero), check:pm-dispatch-gates re-run with its exit captured after a first uncaptured attempt.

② Semver level

None — nothing published; skip-changeset on the PR is the correct declaration. Judged correct.

③ Boundary flags

open_questions empty. All four mechanism assumptions confirmed by measurement (the sweep call site, the report marker's start-of-body anchor, created_at ordering, the H52 fixture helpers). Deviations read and accepted: the dispatch's case-count figure was the seat's slip (5124 at the branch point, measured); the first detached slow-gate run was NOT MEASURED by the file's own rule and re-run; origin/main was not merged a second time after PR #19426 landed (region-disjoint, the derivation unaffected) — the seat's merge check on the current main is clean, so no merge is owed. Two noted, not filed items with carriers (markerMatches does not undecorate a leading blockquote marker — which is what makes the quote refusal hold, now pinned; the phantom guard removed inside scope).

Implemented-by: claude/issue-19160-h52-answer-on-thread-clears
Reviewed-by: session_017ETYWqMQD4qMtZzAGovWNi

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants