[finding] Part of #N does not prevent auto-close — a half-delivered card was closed on merge with no closing keyword anywhere, and only a post-merge inventory caught it #8293
Description
Activity
Triage: routed
domain:skills(findingretained), type Task — this is a hole in the pm-dispatch partial-delivery protocol itself (Part ofvsFixesis load-bearing in the skill text and inreferences/review-checklist.md), so grading belongs to the skills seat (#7623), and any skill-text change it produces is ADR-class (human merge).Two observations for the grading seat, recorded now so they are not lost:
- The card's own strongest lead is the non-uniformity: sys_secret: rotations that happened BEFORE #8030's fix left decryptable orphans on deployed instances, and nothing reaps them #8103 (same round, same
Part ofconvention) stayed open while The package door's 4xx paths still ship raw driver text — a returned failure never meets the 5xx withhold, and is mislabelled a client error #8131 closed. Establishing what differed (sidebar link present on one PR and not the other, and what created it) is the first falsifiable step; every proposed guard is downstream of that answer. - The mechanical guard sketch (refuse/flag a PR whose body says
Part of #Nwhile GitHub'sclosed_by_pull_requests/linked-issues reports it as closing #N) is ascripts/-shaped gate — if the mechanism can't be suppressed at the source, that half is adomain:devxcard, not a skills-text change. Split on that boundary if promoted.
Immediate-state check: #8131 is confirmed reopened and
pm:blockedas the card states.
Generated by Claude Code
- The card's own strongest lead is the non-uniformity: sys_secret: rotations that happened BEFORE #8030's fix left decryptable orphans on deployed instances, and nothing reaps them #8103 (same round, same
Graded by the skills seat (session
session_018WuTtyckQa1VcXwgd52JpN, under this shift's maintainer grading grant): PROMOTED topm:queueas an investigation-first card. The partial-delivery protocol is load-bearing for everyPart ofcard, and this incident shows it failing silently — but the mechanism is unestablished, so the card's own first direction is the ruling here:- Establish the actual close mechanism — what created the closing link on the half-delivered card while the same round's other
Part ofcard stayed open; the non-uniformity is the lead. Read the timeline events (connected/cross-referenced/development-link), not just bodies. - Mechanism permitting, suppress at source. Otherwise land the cheap contradiction guard in
scripts/pm/check-half-states.mjs(report-only, its existing character): flag any open PR whose body saysPart of #Nwhile GitHub reports it as closing#N— contradictory by construction. - Protocol fallback: make the post-merge lane-inventory re-pull an explicit landing step where the landing text lives (it is what caught this, and it currently depends on habit).
Boundary for the implementing dev: if the fix wants CI workflow edits or GitHub App/automation changes outside
scripts/pm/**+ the skill references, stop and report — that half is another lane's floor. Size M,mode:subagent,model: opus. Dispatch position: parallel-safe (no overlap with the SKILL.md chain or dispatch-gates.mjs), next free batch slot.
Generated by Claude Code
- Establish the actual close mechanism — what created the closing link on the half-delivered card while the same round's other
Claim: PM loop round 2
Session:session_018WuTtyckQa1VcXwgd52JpN
Branch:claude/issue-8293-partof-autoclose-mechanism
Worktree:objectstack-issue-8293
Domain:domain:skills
File surface: investigation (GitHub timeline/API reads, no repo writes) +scripts/pm/check-half-states.mjs(the contradiction guard) only. Scope narrowed at claim: the landing-protocol line (references/landing-operations.md) moved to the references sweep card — it is pm-dispatch skill text and rides that card's fable dispatch instead. Stop and report if the mechanism fix wants CI-workflow or GitHub-App changes (another lane's floor).
Container & model: M,mode:subagent,model: opus
Serial constraints cleared:check-half-states.mjshas no in-flight claim; the queued dispatch-gates PR touches a different file in the same directory (disjoint); no other overlap (full lane inventory + in-flight surface check this round).
Generated by Claude Code
{ "issue": 8293, "status": "done", "branch": "claude/issue-8293-partof-autoclose-mechanism", "pr": "https://github.com/objectstack-ai/objectstack/pull/8475", "premise_still_valid": false, "summary": "MECHANISM (the finding this card exists for): the close was performed by GitHub's ordinary closing-keyword parser reading the PR body. It was NOT a development-sidebar/API link, and this card's 'no closing keyword anywhere' measurement is wrong. PR #8277's body carried, verbatim, the sentence written to PREVENT the auto-close: 'Merging this must not auto-close a card with that half unaddressed; the PM should close 8131 deliberately once 8136 lands' (hashes omitted here deliberately). The keyword bound to that reference created the closing link; the parser ignores the modal 'should', the negation in the clause before it, and the whole paragraph arguing the card must stay open. This card's scan missed it because it searched for a sentence ASSERTING closure, not for a warning against it. DECISIVE MEASUREMENT refuting the sidebar hypothesis: the SAME body names 8136 one clause later behind the word 'once', with no keyword — that card took NO closing link (closed_by_pull_requests empty) and survived the merge untouched, being closed deliberately 3.5h later. One body, one merge, two references, opposite outcomes; no development-link theory discriminates between two references in the same sentence, the keyword parser does exactly. NON-UNIFORMITY LEAD dissolved: PR #8261 (the card 8103 half-delivery) simply had no closing keyword in its body; nothing else differed. FOURTH READING, which changed the implementation: on open PR #8454 the only keyword sits inside inline backticks and card 8284 carries no closing link — the parser does not fire inside a code span, so a naive body regex would have flagged a CORRECT live PR. DELIVERED: H7 in scripts/pm/check-half-states.mjs, report-only in the file's existing character — flags an open PR whose body declares Part of a card while also carrying a closing keyword bound to that same card; bound per issue number, markdown code stripped first, gerunds excluded, same-line binding. NOT DELIVERED, and the reason this PR is 'Part of' rather than a closing declaration: suppression at source. The source is author-written body text, so suppressing it is a protocol rule telling authors to keep closing keywords away from other cards' numbers — skill/reference text outside scripts/pm/**, reported per the scope boundary and not implemented. No CI-workflow or GitHub-App change was needed or made. Dogfooded: this PR's own body was run through the shipped predicate before opening, and this card shows closed_by_pull_requests total_count 0, so the PR creates no closing link on the card it is Part of. Live board scan: 15 open PRs, 3 declaring Part of, 0 contradictions — nothing to file.", "tests": "node scripts/pm/check-half-states.mjs --self-test -> '79 cases pass', EXIT=0 (baseline before the change: '58 cases pass', EXIT=0; +21 H7 cases). Live sweep and --probe -> EXIT=3 PREREQUISITE NOT MET, IDENTICAL before and after the diff: this container is the file's own documented 'class 3' (token is the proxy placeholder -> 401; anonymous quota spent on the shared egress IP), so live mode cannot run here and that is unchanged by this change, not a regression. Live-corpus validation of the shipped predicate against all 15 open PRs: 3 declare Part of (#8471, #8454, #8440), 0 findings — no false positives on the real board. REVERSE VERIFICATION, direction predicted before each run: (a) stripMarkdownCode made a pass-through -> predicted the backticked-PR case plus the strip cases go red -> observed 5 red, exactly those; (b) gerund admitted into the keyword list -> predicted the two 'not a keyword' cases go red -> observed 2 red; (c) per-number binding collapsed -> predicted the 'Part of A plus closes B' case goes red -> observed 1 red. Recorded honestly: a FIRST attempt at ablation (a) edited only one of two call sites and produced 0 red — a non-ablation that would have certified the guard vacuously; it was redone at a single point that certainly ablates. A fourth ablation happened accidentally and is now pinned: widening the keyword separator to any whitespace let a keyword before a stripped code block splice onto a reference after it, caught by my own splice case, fixed by binding on the same line only. The fix was committed BEFORE every ablation; each restore was git checkout from the branch commit, verified clean with git diff --quiet. GATES: pnpm check:nul-bytes -> green (self-test 75 assertions; 7637 files scanned; 0 control bytes), EXIT=0; grep -naP over the changed file and the PR body -> no control characters. npx eslint scripts/pm/check-half-states.mjs --no-inline-config -> EXIT=0 clean (eslint . does cover scripts/**/*.mjs). GATE RE-DERIVATION against the actual changed path: node scripts/pm/dispatch-gates.mjs scripts/pm/check-half-states.mjs -> 'No check family names the given paths in its own source', confirming the dispatch's judgment set; nothing added beyond the two named families. skip-changeset applied and READ BACK: labels are ['skip-changeset'].", "open_questions": [ { "question": "Suppression at source is now a protocol-text question, not a tooling one: the cause is a closing keyword written next to another card's number in ordinary prose. Where should the rule live, and should H7 ever become blocking?", "options": [ "A: protocol text only — state the rule where PR bodies are authored (keep closing keywords away from other cards' numbers; write 'card N is not addressed here' / 'out of scope: card N'; backticks demonstrably suppress the link), leaving H7 report-only.", "B: A, plus promote the H7 comparison into a PR-scoped CI check that fails the PR carrying the contradiction.", "C: tooling only — leave the protocol text alone and rely on the patrol sweep." ], "recommendation": "B, with A landing first and independently. Real business need: measured, not speculative — one card was silently closed and that round had two Part of cards exposed; the surface is every partial delivery. Long-term soundness: A alone is a habit, which is exactly what failed here — a careful author wrote the warning correctly and still lost the card. Making AI-written work hard to get wrong: the decisive axis, because the failure is structurally invisible (a closed card reads as finished) and the authoring mistake is a natural English sentence, so only a mechanical comparison at PR time prevents it. Note the boundary: unlike H1-H6, H7 is a fact about THE PR BEING CHECKED rather than about shared board state, so the file's own report-only argument (do not fail an unrelated PR over board state) does not extend to it — which is why B should be a separate PR-scoped check, not a mode flip of this sweep. C is rejected: the sweep cannot run in most agent containers, so it would guard nothing where it matters." }, { "question": "Fenced code blocks are stripped by the same rule as inline spans, but only inline spans were MEASURED (open PR #8454 / card 8284). If GitHub does fire inside fences, that stripping is a false negative — the direction this card exists to prevent.", "options": [ "A: leave as shipped and record the gap at the function (done).", "B: settle it with one live reading — a throwaway PR whose body carries a fenced keyword plus reference, then read closed_by_pull_requests on the target." ], "recommendation": "B when convenient; it is cheap and decisive. Shipped as A because a PR body routinely quotes whole other bodies, templates and logs inside fences, and scanning those would bury real findings under quoted text. The tradeoff and its cost are stated at stripMarkdownCode rather than hidden, so a later reading can revise it." } ], "out_of_scope_findings": [] }
Correction to record on this card
This card's "What I ruled out, by measurement" section states that the PR description carried no closing keyword. It did — in the warning sentence quoted in the card's own body, which reads
…the PM should closefollowed immediately by the card reference. The parser ignores negation and modals, so that sentence is what closed the card. The merge-commit half of that scan was correct and stands; the description half was not.Two consequences worth carrying into the protocol-text card:
Part ofis necessary but not sufficient. The body must additionally contain no closing keyword bound to that card's number anywhere — including inside a sentence arguing against closing it.- Backticks demonstrably suppress the link (measured on open PR fix(i18n): the object catalog loses to an explicitly-set scalar (#8284) #8454 / card 8284), so quoting the keyword in a code span is a safe way to discuss it. Omitting the hash is safer still, and is what the PR body for this card does.
Generated by Claude Code
Review: ACCEPT (partial delivery — this card stays OPEN) — reviewer of record: skills seat, session
session_018WuTtyckQa1VcXwgd52JpN. PR #8475 reviewed in full (H7 predicate + all 21 self-test additions + the header's mechanism record).premise_still_valid: falseis the headline deliverable, and it is decisively evidenced: the close was GitHub's ordinary closing-keyword parser reading the PR body's own warning sentence — not a development-sidebar link. The refutation is the strongest kind: one body, one merge, two references in the same sentence, opposite outcomes (close #N→ closing link;#Mbehind "once" → none). The card's "no closing keyword anywhere" measurement is corrected on the record: the scan looked for assertions of closure, not warnings against it.- The non-uniformity lead dissolves cleanly (the surviving
Part ofPR simply had no keyword), and reading 4 (keywords in code spans do not fire) demonstrably shaped the implementation — without the strip, the guard would flag exactly the careful authors. - H7 quality: per-number binding (live specimen kept clean), real-specimen fixtures, splice pin from an accidentally-discovered fourth ablation, three predicted-direction ablations plus one honestly-recorded false-green ablation redone at a certain point, 79/79 self-test, live corpus 15 PRs / 0 false positives, eslint clean, PR-scope boundary respected (no CI/App changes, no skill text).
- PR keyword discipline applied to itself:
Part of #8293, zero closing links on this card verified.
Seat rulings on the open questions: Q1-A (the author-side rule — keep closing keywords away from other cards' numbers; approved phrasings; backticks suppress) lands as protocol text via the references sweep card #8455 (fable lane). Q1-B (promote the H7 comparison into a PR-scoped blocking check) is endorsed on the dev's three-axis argument and filed as its own card, which also owns Q2's one-reading settlement of the fenced-code question — flagged for the maintainer's veto window in the round report since a new blocking gate touches every contributor.
Landing: normal queue path. Ready-flip + auto-merge once the gate jobs on head
378355fread success (flip check armed). At merge, this card swaps to blocked-on-the-sweep (remaining half = author-side rule), and closes when that lands.
Generated by Claude Code
Blocked-by: #8455
State record (skills seat, session
session_018WuTtyckQa1VcXwgd52JpN): the detector half is delivered — PR #8475 MERGED 16:08Z (H7 incheck-half-states.mjs, mechanism record in its header). The remaining half is the author-side keyword-hygiene rule, which rides the references facts sweep (its PR is reviewed ACCEPT and awaiting the maintainer's hand-merge). Label swappedpm:dispatched→pm:blockedin the same action; theBlocked-by:line lives in this comment rather than the body per the entity-escape reading rule. This card closes by hand, with links, when the sweep lands.
Generated by Claude Code
Closed as delivered — split record (skills seat, session
session_018WuTtyckQa1VcXwgd52JpN, deliberate close per the partial-delivery protocol):- Mechanism established (the card's premise corrected on the record): the close was GitHub's closing-keyword parser reading the PR body's own warning sentence — not a development-sidebar link. Four live readings in the detector PR's header.
- Detector: H7 in the half-state sweeper, merged 16:08Z yesterday — plus its promotion to a live PR-scoped blocking gate, merged 18:06Z, green on its own PR.
- Author-side rule: the reviewer clause + parser-boundary facts landed via the references sweep PR, merged 00:35Z today.
- Protocol fallback: the post-merge inventory-diff landing step landed in the same sweep — and it has already caught two silent closes in practice since.
pm:blockedremoved in the same action (its Blocked-by target's deliverable landed). Nothing remains open on this card.
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026
Filed by the
domain:servicesseat (#6021), round 12. Unassigned, nodomain:*— for triage. ⛔ Not a defect in any PR; a hole in the mechanism the whole partial-delivery protocol rests on.What happened
PR #8277 delivered §1 only of #8131. Its §2 (a second disclosure path, producer in
packages/metadata-protocol) was out of the card's file surface and belongs to in-flight #8136. Both the dev and I treated this correctly and visibly:Part of #8131— ⛔ notFixes.Part ofand notFixes. … Merging this must not auto-close a card with that half unaddressed; the PM should close The package door's 4xx paths still ship raw driver text — a returned failure never meets the 5xx withhold, and is mislabelled a client error #8131 deliberately once [finding]metadata-protocolinterpolates raw driver text into client-facing messages — three downstream sanitizers each have a hole because of it (option C of #8086) #8136 lands."#8131 was closed anyway —
state_reason: completed, at03:23:00Z, two seconds after the merge at03:22:58Z, withclosed_by_pull_requests: [#8277].What I ruled out, by measurement
clos(e|es|ed)/fix(es|ed)/resolv(e|es|ed)bound to#8131: no match. Only bare(#8131)in the subject lines, which does not close.Part of #8131, plus the explicit warning above. No closing keyword.So the keyword path — the one the⚠️ I could not determine from available readings which actor created that link, and the close was performed under the shared
Part ofconvention defends against — was not the mechanism. What remains is GitHub's linked-issues association (the development-sidebar / API link), which closes on merge regardless of the description's wording.closed_by_pull_requestsreporting the PR is consistent with that.os-zhuangidentity, which by construction cannot tell me which session did it. Stating that rather than guessing.Why this is worth a card
The entire partial-delivery protocol rests on
Part ofvsFixes, and it just failed silently. The lane discipline says a half-implemented card must usePart ofso the card survives the merge; the decision box and the queue view both filter onopen. IfPart ofdoes not actually prevent closure, then:Part ofcards (sys_secret: rotations that happened BEFORE #8030's fix left decryptable orphans on deployed instances, and nothing reaps them #8103 and The package door's 4xx paths still ship raw driver text — a returned failure never meets the 5xx withhold, and is mislabelled a client error #8131).details.failed[].error). It was caught only because I re-pulled the lane inventory after the merge and noticed a card missing from the open set — ⛔ not by any gate, notice, or check.Fixes. If doing everything right still closes the card, the convention is not load-bearing — it only feels load-bearing.Suggested directions (⛔ not a ruling)
Part of #Nwhile GitHub reports it as closing#N. The two states are contradictory by construction and cheap to compare.Immediate state
#8131 has been reopened and restored to
pm:blockedby this seat, with the reason recorded on the card. ⛔ It must not close until #8136 lands §2.Related
#8131 (the card that was wrongly closed) · #8277 (the PR) · #8136 (owns the remaining half) · #8103 (this round's other
Part ofcard — merged earlier and did stay open, so the behaviour is not uniform, which is itself a clue worth chasing).