Repository navigation
[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
Filed by the domain:services seat (#6021), round 12. Unassigned, no domain:* — 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:
- PR body first line:
Part of #8131— ⛔ notFixes. - Second line, verbatim: "
⚠️ DeliberatelyPart 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." - My review endorsed it in writing and pre-registered the post-merge re-grade.
#8131 was closed anyway — state_reason: completed, at 03:23:00Z, two seconds after the merge at 03:22:58Z, with closed_by_pull_requests: [#8277].
What I ruled out, by measurement
- Merge commit message — scanned the full message for
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. - PR description — read in full.
Part of #8131, plus the explicit warning above. No closing keyword.
So the keyword path — the one the Part of convention 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_requests reporting the PR is consistent with that. os-zhuang identity, 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 of vs Fixes, and it just failed silently. The lane discipline says a half-implemented card must use Part of so the card survives the merge; the decision box and the queue view both filter on open. If Part of does not actually prevent closure, then:
- Every future half-delivery is exposed, not just this one. This round alone had two
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). - The failure is silent and looks like success. A closed card reads as finished. 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 still has an open disclosure path (§2, raw driver text in
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. - The dev did everything right. Correct keyword, explicit warning in the body, escalated the conflict when my dispatch order told it to write
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)
- Establish the actual mechanism first. If it is a sidebar/API issue-link, find what creates it — an automation, a bot, or a step in the PR-opening flow. That is the fix's target; everything else is a workaround.
- Mechanical guard, if the link cannot be suppressed at the source: a check that refuses (or flags) a PR whose body says
Part of #Nwhile GitHub reports it as closing#N. The two states are contradictory by construction and cheap to compare. - Protocol fallback: make "re-pull the lane inventory after every merge and diff it against the expected state" an explicit post-merge step rather than something a careful PM happens to do. It is what caught this, and it currently depends on habit.
Immediate state
#8131 has been reopened and restored to pm:blocked by 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 of card — merged earlier and did stay open, so the behaviour is not uniform, which is itself a clue worth chasing).
Activity
hotlong commentedon Aug 13, 2026
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:
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.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
hotlong commentedon Aug 13, 2026
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:Part ofcard stayed open; the non-uniformity is the lead. Read the timeline events (connected/cross-referenced/development-link), not just bodies.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.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
hotlong commentedon Aug 13, 2026
Claim: PM loop round 2
Session:
session_018WuTtyckQa1VcXwgd52JpNBranch:
claude/issue-8293-partof-autoclose-mechanismWorktree:
objectstack-issue-8293Domain:
domain:skillsFile 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: opusSerial 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
hotlong commentedon Aug 13, 2026
{ "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.Generated by Claude Code
hotlong commentedon Aug 13, 2026
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.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.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
hotlong commentedon Aug 13, 2026
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
hotlong commentedon Aug 14, 2026
Closed as delivered — split record (skills seat, session
session_018WuTtyckQa1VcXwgd52JpN, deliberate close per the partial-delivery protocol):pm:blockedremoved in the same action (its Blocked-by target's deliverable landed). Nothing remains open on this card.Generated by Claude Code
feat(pm): flag a PR whose `Part of` is contradicted by a closing keyw…