Skip to content

[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

@os-zhuang

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:

#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. ⚠️ I could not determine from available readings which actor created that link, and the close was performed under the shared 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:

  1. Every future half-delivery is exposed, not just this one. This round alone had two Part of cards (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).
  2. 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.
  3. 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 #N while 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

  1. added theissue type on Aug 13, 2026
  2. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Triage: routed domain:skills (finding retained), type Task — this is a hole in the pm-dispatch partial-delivery protocol itself (Part of vs Fixes is load-bearing in the skill text and in references/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:

    1. 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 of convention) 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.
    2. The mechanical guard sketch (refuse/flag a PR whose body says Part of #N while GitHub's closed_by_pull_requests/linked-issues reports it as closing #N) is a scripts/-shaped gate — if the mechanism can't be suppressed at the source, that half is a domain:devx card, not a skills-text change. Split on that boundary if promoted.

    Immediate-state check: #8131 is confirmed reopened and pm:blocked as the card states.


    Generated by Claude Code

  3. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Graded by the skills seat (session session_018WuTtyckQa1VcXwgd52JpN, under this shift's maintainer grading grant): PROMOTED to pm:queue as an investigation-first card. The partial-delivery protocol is load-bearing for every Part of card, and this incident shows it failing silently — but the mechanism is unestablished, so the card's own first direction is the ruling here:

    1. Establish the actual close mechanism — what created the closing link on the half-delivered card while the same round's other Part of card stayed open; the non-uniformity is the lead. Read the timeline events (connected/cross-referenced/development-link), not just bodies.
    2. 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 says Part of #N while GitHub reports it as closing #N — contradictory by construction.
    3. 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

  4. self-assigned this
    on Aug 13, 2026
  5. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    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.mjs has 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

  6. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor
    {
      "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 close followed 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:

    1. Part of is 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.
    2. 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

  7. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    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: false is 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; #M behind "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 of PR 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 378355f read 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

  8. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Blocked-by: #8455

    State record (skills seat, session session_018WuTtyckQa1VcXwgd52JpN): the detector half is delivered — PR #8475 MERGED 16:08Z (H7 in check-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 swapped pm:dispatched → pm:blocked in the same action; the Blocked-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

  9. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    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:blocked removed in the same action (its Blocked-by target's deliverable landed). Nothing remains open on this card.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions