Repository navigation
fix(pm): a SECOND Claim: by one seat is NAMED, not ranked as a supersession - #18859
Merged
os-justin merged 5 commits intoSep 18, 2026
Merged
Conversation
…ition gets its reader The claim protocol forbids a second `Claim:` — the rule sits in this file's own #17366 docblock — and the governing-claim selector READ one as a designed SUPERSESSION: `rejected: 1 … a SUPERSEDED claim`, exit 0. Five cards carried the forbidden second line at 2026-09-18T00:05Z and nothing refused any of them. `claimRepeats` is a sibling pure reader beside `claimRetractions` (one derivation of「retracted」, MEMBERSHIP first as #18719 set it) and names every author holding more than one LIVE claim comment; `c8SecondClaimSameSeat` renders it as row C8 — a VERDICT at EXIT_PAIR_ADVERSE, never a note — and the input record gains `claim.repeat`, the reading the SUPERSEDED sentence used to be the only trace of. `CLAIM_COMMENT_MARKER` unwidened (read through the sibling's one reading), `CLAIM_SELECTION_RULE` and the governing-claim choice unchanged for every other shape, `claimCarrierSelection` still a pure function of the rows it is handed. Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu Co-authored-by: Claude <noreply@anthropic.com>
…ol each A new self-test battery (51 cases) registered in the roster, floor 31 -> 32: same seat twice with no retraction ⇒ named at exit 4 with both ids and both repairs; different authors ⇒ supersession as today; a re-claim after a `Release:` or an id-naming retraction ⇒ RETRACTED as today, wording byte-unchanged; a `Clause-②-correction:` row ⇒ silent; a decorated second claim ⇒ counted through the sibling's one reading; an unparsed `Branch:` ⇒ still named; three claims ⇒ ONE refusal naming three; an unattributable row ⇒ fail closed; a later comment that merely quotes the word ⇒ silent. Every direction carries a non-vacuity control, and the five measured instances (2026-09-18T00:05Z) are replayed from their real rows with #18559's correction as the control. Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu Co-authored-by: Claude <noreply@anthropic.com>
…ull grouping The first ablation leg aborted the whole run on a `[0].ids` read, which names no pin at all; both reads are defensive now. Adds the case that makes the fail-closed guard observable: two unattributable rows are not ONE seat, so `null` never groups with `null`. Battery 51 -> 52 cases. Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu Co-authored-by: Claude <noreply@anthropic.com>
…cond-claim-same-seat-refused
…cond-claim-same-seat-refused
os-justin
marked this pull request as ready for review
September 18, 2026 01:41
os-justin
deleted the
claude/issue-18828-second-claim-same-seat-refused
branch
September 18, 2026 02:30
This was referenced Sep 18, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…d an 'end' node (objectstack-ai#18688) Part of objectstack-ai#15646 Clause-②: yes The flow accept set shrinks for five node types inside region bodies — shapes the runtime never honoured. Ruling D clause 4 states it verbatim.⚠️ **The runtime half is NOT in this PR.** A region-contained node that *durably suspends* must fail the run with a named error — only the run can know that — and ruling D assigns it to a separate `domain:services` card. ⇒ this PR lands with `Part of`, ⛔ not `Fixes`; **objectstack-ai#15646 stays open until the runtime half lands.** --- ##⚠️ SEAT BANNER — ruling **D** superseded the route this body argues for Everything below the horizontal rule was written when the card was ruled **C**, and it argues for **route A** ("Recommendation: A, as implemented"). ⛔ **That is no longer what this PR does.** It is kept unedited as the record of how the decision was reached — ⛔ deleting it would erase the evidence the later ruling was made on. **What this PR does NOW**, per ruling D (batch objectstack-ai#153 item 1, comment `5724940095`, maintainer 「其他同意」): - Inside `loop` / `parallel` branch / `try_catch` (try **and** catch) bodies at any depth, `FlowSchema.superRefine` refuses **five** node types: `screen`, `wait`, `approval`, `approval_revise`, and `end`. - ⛔ **`map` and `subflow` are NOT refused by type.** They pause exactly when the child flow their `config.flowName` names pauses — a *different metadata record*, not in hand at parse. Refusing them by type would also refuse `loop { map(synchronous child) }`, which runs correctly today. - `packages/spec` keeps its published identifier `FLOW_PAUSE_CAPABLE_NODE_TYPES`; only its **contents** narrow. - **`packages/services` is untouched by this round** — measured, zero paths. The 5-tests-in-3-files cost the section below describes **does not occur**: the whole package runs **138 files / 1652 tests, all passing**, with a false-green control proving the test read the rebuilt artifact and not a stale `dist`.⚠️ **Corrected by the seat, and it is a DECLARED DEVIATION from ruling D's letter — ⛔ not a clean pass.** 「zero paths」 is true of **this round's commits** and ⛔ NOT of the PR's cumulative diff: `packages/services/service-automation/src/end-node-refused-outcome.test.ts` (+57/−35) is in the diff, from the earlier round's commit `87973cab8d1`. Ruling D clause 1 says 「`packages/services` untouched; **the 5 tests** and objectstack-ai#15616's suite stand」 — and objectstack-ai#15788's region-`end` case **was one of those 5**. ⇒ the ruling's premise 「B breaks nothing」 was **false for that one case**: clause 1 itself orders `end` refused at parse, and `registerFlow` parses, so the old run-time assertion is unreachable by construction. What was done: the fixture is **byte-identical**, case count **12 → 12**, and the assertion is **strengthened** (region path, message text, and that nothing registered) so it fails again the day the shape becomes declarable. ⛔ No test deleted, skipped or quarantined; ⛔ no engine source moved; objectstack-ai#15616's suite and the other three files are untouched and green. The at-tier review measured all of this and ruled it non-blocking — but it is a deviation and it is stated here rather than buried. **CI on `6de9d662f6df`: 32 success, 3 skipped, 0 failure, 0 pending.** ### ⛔ Two corrections the `domain:spec` seat owes on its own record 1. ⭐ **The seat ruled "keep the published name" on a premise that is FALSE.** It told the round that renaming `FLOW_PAUSE_CAPABLE_NODE_TYPES` removes a *published* export and therefore forces a major. Measured since: main's `packages/spec/api-surface/automation.json` greps **0** for that name (lit controls `FLOW_BUILTIN_NODE_TYPES` and `FLOW_STRUCTURAL_NODE_TYPES` = 1 each; dark control = 0), and the branch greps 1. ⇒ **the constant is introduced by this PR and is on no consumer's import path**; the gate's 「1 breaking (removed)」 was computed against the branch's own earlier snapshot. The *decision* stands and costs nothing — a second name would be cost without benefit — but ⛔ the record must not carry 「a removed published export」 as a fact about consumers. The round measured this and told the seat; the seat re-measured and confirms it. 2. ⛔ **THIS CORRECTION WAS ITSELF WRONG, and the seat withdraws it.** It claimed `--pair 18688` re-measured 「exit 0」 at this head. **That reading came from a STALE INSTRUMENT.** The shared checkout's `check-clause2-carriers.mjs` is blob `ccd5ad7c9a00` and contains **0** occurrences of rule **C8**; `origin/main`'s and this head's is blob `3a270ef2eb5f` and contains **18** (lit control `C1`: 50 vs 51, so the reader works). C8 landed on `main` at 01:41Z via objectstack-ai#18859 and the shared checkout never had it. ⇒ every `--pair` reading this seat took today was taken with a script that cannot see C8. Re-taken with `origin/main`'s script: **objectstack-ai#18688 exit 4 on C8** — this seat held **two live `Claim:` comments** on objectstack-ai#15646 (`5722016855`, `5728277407`), which the protocol forbids. Repaired as C8 prescribes: `Release:` (`5729634742`) then ONE fresh `Claim:` (`5729639847`). **`--pair 18688` now exits 0** — `claim.selected` 1, `claim.rejected` 2. The at-tier review caught this; the seat re-measured and confirms it. --- Route **C**, as ruled. Director seat, summon objectstack-ai#24, batch objectstack-ai#145 item 5 — objectstack-ai#15646 (comment) (maintainer 「同意,其他也同意」), with the batch objectstack-ai#146 scope addition — objectstack-ai#15646 (comment) (maintainer 「146 同意」), which attached objectstack-ai#3267's 禁 ruling and absorbed objectstack-ai#18112 into this card. One PR, one changeset, two refusals in one rule family. ## 🛑 Read this first — this PR is NOT ready to land, and the reason is a measured decision, not a bug `packages/spec` is green end to end. **5 tests in 3 `packages/services/service-automation` files now fail**, and every one of them fails for the same reason: the fixture can no longer be REGISTERED, because `AutomationEngine.registerFlow` parses through `FlowSchema.parse` (`engine.ts:3941`) and this rule refuses the shape.⚠️ **Corrected by the `domain:spec` seat after a classification round — the table below replaces one that named 5 tests in 3 files.** That earlier count was taken by running **three named files**; CI runs `pnpm --filter @objectstack/service-automation test`, the whole package, and a named-file subset cannot see this class of breakage. `os-dev.md:56` reserves this body to the PR-open write, so the round named the wording and the seat writes it. | File | Failing | Card | What it pins | | --- | --- | --- | --- | | `src/builtin/contained-failure-rollup.test.ts` | **7** | objectstack-ai#16314 | the contained-failure rollup fold over `loop { subflow }` —⚠️ **absent from the earlier table entirely**; it predates this branch's base (`git merge-base --is-ancestor` exit 0), so this is a measurement gap, ⛔ not drift | | `src/builtin/map-in-loop-iteration-state.test.ts` | 3 | objectstack-ai#15616 | `loop { body: [ map, probe ] }` over a **non-pausing** child: 5 iterations x 2 items ⇒ 10 child runs, `failed = 0` either way, a fresh result set per iteration | | `src/builtin/contained-failure-visibility.test.ts` | 1 | objectstack-ai#14456 | a parent run's row identity does not leak into a `subflow` child; the region shape is the **vehicle**, not the subject | | `src/end-node-refused-outcome.test.ts` | 0 (was 1) | objectstack-ai#15788 | ⭐ **fixed on this branch** — see below | **Measured with the package suite:** at `e10b395cee`, **12 failed / 1627 passed (1639)** across **4 files**. After the fix below, at `87973cab8d1`: **11 failed / 1628 passed**. ⭐ **One of the twelve was never blocked on the open question, and it is repaired here.** objectstack-ai#15788's region-`end` case sits in **both** candidate populations — this body defines route B as the unconditionally pausing types **plus `end`** — so no answer to the question below moves it. It is re-homed to the registration refusal: the fixture is unchanged byte for byte, and the case now asserts the ZodError's located path, its message and prescription, and that **nothing registered**. ⛔ Not a deletion — it fails again the day the shape becomes declarable. **The 11 are mutually exclusive with route A, and that is measured rather than argued.** Ablating `FLOW_PAUSE_CAPABLE_NODE_TYPES` to route B's definition turns **all 11 green with nothing else moving**; route A on the same four files is 11 red.⚠️ Method note that is load-bearing: `service-automation` resolves `@objectstack/spec` through **`dist`**, so the ablation was rebuilt and verified present in 18 built artifacts before anything was read — an unrebuilt ablation would have gone green and proved nothing. Restored afterwards, verified absent from all 216 artifacts, whole-tree porcelain empty. ⭐ **The 11 are NOT one cost.** 3 of them (objectstack-ai#15616) are free: under route A the shape becomes undeclarable, so the defect is unreachable and the regression suite converts to a refusal pin — mechanically, the same conversion performed above for objectstack-ai#15788; that file's second describe (a TOP-LEVEL pausing map) is untouched and green, so the durable-pause half keeps its coverage. The other 8 (objectstack-ai#16314, objectstack-ai#14456) are a genuine re-home onto a top-level delegating node, and `loop { subflow }` over five rows with one failing is the shape objectstack-ai#15617's ruling **named**, so any re-home must record that the measurement no longer runs on it. ⛔ **Not repaired here.** The dispatch fences `packages/services` ("the engine's runtime refusal stays exactly as it is") —⚠️ and note precisely what that fence claims: it is true of the **diff**, which touches no `packages/services` file. Read as a claim about **effect** it is false, because the parse refusal changes what those suites can register. The changeset carries the same correction, and two of these three are other cards' regression suites: deleting or re-homing objectstack-ai#15616's and objectstack-ai#15788's coverage is a decision, not a fixture edit. **Two of them are also evidence about the rule itself**, which is the open question below. ### The open question: does the narrowing take a shape that WORKS with it? The ruling's population is "a node that **can durably pause** (`map` / `subflow` **with a pausing child**, approval-class nodes)". Measured: `map` and `subflow` pause **exactly when the child flow they NAME pauses** — a different metadata record — so "with a pausing child" is **not decidable at parse**. Only two spellings are: - **A — judge the node TYPE** (what this PR implements). Closes this card's own reproduction, covers all three region kinds, and is the only reading under which C is the *complete* fix the ruling's own reasoning requires. **Cost, measured:** it also refuses `loop { map(synchronous child) }` — a shape that runs correctly today and was deliberately fixed 12 days ago by objectstack-ai#15616 / PR objectstack-ai#15648, whose regression suite is 3 of the 5 failures above. - **B — judge only the UNCONDITIONALLY pausing types** (`screen` / `wait` / `approval` / `approval_revise`) plus `end`. Refuses nothing that works today, and the 3 `map` failures disappear. **Cost:** this card's own reproduction — `loop { try_catch { map(pausing child) } }` — stays declarable and stays silently green, so the card is not closed. There is no third reading available to a parse. **Recommendation: A, as implemented** — objectstack-ai#3267 is ruled 禁 ("structured regions do not support durable pause"), and a shape whose legality lives in a record the author is not editing, revocable by editing that record, is not a contract. Under A the five tests are re-homed (a top-level `map`, a top-level `end`) or retired with a statement, in this PR or a follow-up, once the seat says the coverage may move. ## Step Zero — the ruling's precondition, answered before any code was written > **First step, before writing**: prove the nesting is statically decidable at parse/validate time. **Answer: YES for the nesting and for the node vocabulary this rule judges, with two boundaries that are declared rather than discovered.** What was measured, on this branch's base `7f7b8557df`: 1. **The nesting is decidable, and a refusing layer already exists.** `collectFlowGraphs` (`packages/spec/src/automation/control-flow.zod.ts`) yields the top-level graph plus every region body, depth first, with a `scope` label and a `path` that anchors a Zod issue where the author wrote the node. `FlowSchema`'s `superRefine` already walks exactly that and refuses on it — the objectstack-ai#16134 one-node-id-space rule. The PM seat's clue held: there is no *refusing* layer for this shape, but the walk and the refusal machinery are both live and in the same file. 2. **The pausing vocabulary is statically declared for the built-in set.** Derived by reading the shipped `defineActionDescriptor` literals, not by recall: `supportsPause: true` appears on `screen` / `wait` / `subflow` / `map` (`packages/services/service-automation/src/builtin/`) and `approval` / `approval_revise` (`packages/plugins/plugin-approvals/src/`) — six, the same six the ADR-0044 `resumeAuthority` default-flip migration entry names in its own prose. They are published here as `FLOW_PAUSE_CAPABLE_NODE_TYPES`. 3. **`end` is fully static** — `FLOW_STRUCTURAL_NODE_TYPES`, a node type the engine handles with no executor at all. **What is NOT decidable, and what this rule does about it.** Whether a given node *will* pause is not decidable at parse, in two different ways, and both are stated in the docblock, in the changeset and in the ADR-0087 entry: - **`map` / `subflow` pause exactly when the child flow they NAME pauses** (`map.config.flowName`, an opaque reference to another metadata record). So the rule judges the node **TYPE**, not the run. That is wider than the runs that actually broke — a region-nested `map` over a synchronous child parsed green before and is refused now — and it is deliberate: the old shape's legality lived in a record the author is not editing and could be revoked by editing that record. "Legal until somebody adds a `wait` to the child flow" is not a contract. - **A plugin-registered pausing type is invisible to a parse.** ADR-0018 left the node-type namespace open (`FlowNodeSchema.type` is a validated `string`), and a parse has no registry. Pinned as a boundary test so it moves deliberately. - **`MAX_REGION_DEPTH` (32).** The walk stops there.⚠️ Unlike objectstack-ai#16134's duplicate-id rule, there is **no second spec refusal behind the ceiling** for this rule — `analyzeRegion` says nothing about pausing nodes — so past depth 32 the engine's run-time refusal is the only one. Measured and pinned at nesting 32 (refused) / 33 (not judged), and stated in the changeset rather than left for an author to find. ## What changed `FlowSchema.superRefine` gains one walk over `collectFlowGraphs`, skipping the flow's own graph, that raises a `custom` issue anchored at `[...regionPath, 'nodes', i, 'type']` for: - **a pause-capable node** in a region body — the message names the node, the region scope (`loop 'sweep' body → try_catch 'guard' try`), why a region body cannot host it, and the fix; - **an `end` node** in a region body, whatever its `outcome` — an `end` there was a no-op, and a refusing one was converted into a region error at the same boundary (objectstack-ai#15788). The ruled prescription is the message: a region body cannot end the run; put the `end` on the top-level graph. `FLOW_PAUSE_CAPABLE_NODE_TYPES` is the new export (`api-surface` / `export-origins` regenerated). The two approval entries are the declared constants `APPROVAL_NODE_TYPE` / `APPROVAL_REVISE_NODE_TYPE`, so a rename cannot desynchronise them. ⛔ `packages/services` is untouched — this is authoring-time enforcement only. ⛔ No engine rollback seam (route A, no card filed, per the ruling). ⛔ No runtime detection in `map` (route B, refused). ⛔ objectstack-ai#15617's `failed` fold is not addressed. ## Tests New file `packages/spec/src/automation/flow-region-pause-and-end.test.ts` — every case fails without the rule: - both refusals × all three region kinds: `loop` body, `try_catch` try **and** catch, `parallel` branch. A rule covering `loop` only is route B wearing C's clothes; the `try_catch` catch arm and the `parallel` branch arm are the two route B could never see, and each has its own case. - all six pause-capable types, table-driven off the exported constant. - the card's own reproduction, `loop { try_catch { map } }`, refused with the chained region path. - **negative tests, the over-reach guard**: every pause-capable type and an `end` still parse on the **top-level graph**; every non-pausing type still parses inside a region; a node merely *named* `end` or `wait` in a region still parses (the rule judges `type`, not `id`). - both declared boundaries pinned: the plugin-contributed pausing type, and the depth-32/33 seam. - `defineFlow` and `formatZodError` renderings. Two existing cases pinned the behaviour this rule replaces and were **replaced rather than re-spelled**, each saying so in its own comment: `end-node-outcome.test.ts`'s region-nested `end` (its subject — an `end`-in-region whose *config* is judged one door later — no longer exists) and `flow.test.ts`'s BPMN `waitEventConfig` region case (now asserts the earlier refusal *and* keeps the region-contract half it actually exists to measure). The `requireTypeScopedConfig` docblock that asserted a nested block-less `wait` parses green was corrected in the same edit. ## Verification Measured on `e10b395cee`. Heavy runs go through `scripts/pm/os-verify-lock.sh`; every exit code below is read from the wrapper's own `VERDICT command-exit` line or captured into a variable **before** any pipe — never `$?` after one. | Command | Verdict | | --- | --- | | `pnpm --filter @objectstack/spec build` | `command-exit 0` | | `pnpm --filter @objectstack/spec test` (whole package) | `command-exit 0` — **486 files, 13895 tests, 0 failed** | | `pnpm --filter @objectstack/spec typecheck` (`tsc --noEmit` + `check:scripts-typecheck` + `check:test-typecheck`) | `command-exit 0` | | `pnpm --filter @objectstack/spec check:generated` | `command-exit 0` — all 15 artifacts up to date | | `pnpm lint` (whole repo, `eslint . --no-inline-config`) | `exit 0` — run in full, so nothing here is a narrowing | | `dispatch-gates.mjs --ran` reconciliation | `exit 0` — **85 derived, 81 run, 4 NOT-MEASURED, 0 UNRUN** | | `@objectstack/service-automation` — the 3 files whose fixtures feed this rule | `exit 1` — **5 failed / 24 passed**, see the section at the top | **Reverse verification (one-shot, restored).** The rule's own early-exit was mutated (`graph.path.length === 0` → `>= 0`), and the mutation was proved on disk before anything was read from the run — anchor grep 1 → 0, marker grep 0 → 1, blob `044bbbba` → `56a1c350`: - **ablated** — `flow-region-pause-and-end.test.ts`: **20 failed / 7 passed**. The 20 are exactly the refusal assertions; the 7 that survive are the over-reach guards and the two boundary pins, which must stay green with the rule absent. That split is itself the reading: a rule that also broke the negative cases would be refusing too much. - **restored** — `git checkout HEAD --` the file, blob back to `044bbbba`, `git diff HEAD` clean, `git status` empty, same file **27/27 passed**. No `dist` preflight applies: the test imports `./flow.zod` by relative source path, so the subject never resolves through `packages/spec/dist`. A restore `trap` was armed for the whole window. **The four NOT-MEASURED gates** are `check:doc-formula-expressions`, `check:dual-build-cjs-loads`, `check:lean-entry-closure` and `check:type-check-debt` — each exited **3, PREREQUISITE NOT MET**, printing in its own words that nothing was measured. All four read built output across packages this diff does not touch and need a repo-wide build; CI's `Build Core` and `Lint & Repo Gates` are where they run. ⛔ Not green, not red — unrun. **Not measured, stated:** CI convergence on this PR (the report is filed at the end of local verification); the branch has not been merged forward since `7f7b8557df`, so `main`'s newer commits are tested by CI and the queue rather than here. **Review-gate reading, not an action.** `scripts/pm/check-clause2-carriers.mjs --pair 18688` exits **4** on two rows, both belonging to the claiming seat and ⛔ neither touched here: **C1** — card objectstack-ai#15646 carries `needs:contract-review` while this PR does not (the gate is a dual carrier); **C2** — no comment on the card's thread is a machine-legible claim comment (none has a first line beginning `Claim:` carrying the `Clause-②:` line), so the declaration limb has nothing to read. The declaration itself is at the top of this body and in the changeset. ## Acceptance notes Observations from this card's reading, recorded here and **not** filed — none is a reproducible defect, a contract violation, or an authoring trap: - The ruling's own parenthetical, "`map` / `subflow` **with a pausing child**", describes the defect population rather than a decidable rule population, and its "a shape the runtime never honoured" is exact for `end`, `screen`, `wait` and the approval pair but not for a `map` over a synchronous child, which runs today. The PR takes the capability reading — the only one that makes C the complete fix the ruling's own reasoning requires — and the changeset states the cost in the author's own terms. Noted so a reviewer reads the widening deliberately rather than discovering it. - The card body and the batch objectstack-ai#145 ruling both say objectstack-ai#15617 is open; it is **closed / completed**. Nothing here depends on it. - `engine.ts:9937` in the ruling reads `engine.ts:9970` on this tree — line numbers are clues, and this one was re-read rather than trusted. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18828
Clause-②: no
The claim protocol forbids a second
Claim:— the rule is this file's own, in the #17366 docblock atscripts/pm/check-clause2-carriers.mjs:378— and until this PR nothing READ it. A thread that carried the forbidden second line was RANKED, not refused: the governing-claim selector took the newest live claim that parses a branch and printed the loser asrejected: 1 … a SUPERSEDED claim, clean and green at exit 0, in the register reserved for a transition the protocol DESIGNED. A writer-side prohibition with no enforcing reader.claimRepeatsand the C8 row are that reader.What was VERIFIED and what was FIXED
Every contract item was found already correct and is left byte-unchanged. No code fix was needed; the only commit this run adds is a merge of
origin/main(the derivation was answering about a stale tree — see Gates).EXIT_PAIR_ADVERSE(4), never a NOTE at 0C8is pushed inpairRows(:4430);pairNotesdoes not carry it, pinnedclaimCarrierSelectionstays a pure function of the rows it is handedorigin/main(sha256 of the whole function40f59ba86082c358at both revs)CLAIM_COMMENT_MARKERunwidenedcheck-half-states.mjsand read throughmarkerMatches; that file is not in this diff at allCLAIM_SELECTION_RULEand the governing-claim choice unchangedt(...)cases in the battery block; the ablation shows all nine directions carriedSELF_TEST_BATTERY_FLOORraised by exactly one#18559as the sanctioned-shape controlrowAuthor,laterOnThreadandclaimRetractionsare byte-identical across the two revs as well.The before-reading — the fixture control through the exported reader
The same two rows (one author, two claims each parsing a branch, no retraction between) through the same exported
claimCarrierSelection/pairInputRecord/pairRows, atorigin/main88aa326deband at this branch:origin/main88aa326debclaims/live/pool/rejected71000000027100000002— unmovedrejected[0].reasona SUPERSEDED claim — it is not the newest LIVE claim that parses a branch …claimRepeatsexportedclaim.repeatin the record1 author(s) holding more than one LIVE claim comment …pairRowscodesC8EXIT_PAIR_ADVERSE)That is the defect and the repair in one table: the selector does not move, and the thread stops reading green.
The live count — the five cards and the control, re-taken 2026-09-18T00:44:39Z
Read per card through the gate's own⚠️ Those threads may have left this state since.
markerMatches/CLAIM_COMMENT_MARKERandclaimRetractions, not by eye.Claim:comments (author)Clause-②-correction:Release:os-support-ai(5719079496, 5720020876)os-support-ai(5720104138, 5720190458)os-support-ai(5720212595, 5720888122)os-support-ai(5721424769, 5721997887)os-support-ai(5721425530, 5722028692)os-support-ai(5721425131)The card's table reproduces exactly. All six cards are now closed, and the open-PR column is empty for every one of them: the only open PR cross-referenced from any of these threads is #18857, whose body's closing keyword names
#18780instead —prDeliversCardanswersfalsefor all six andtruefor#18780(the control leg), so it is not paired with any of them. ⛔ The repair of the five isos-support-ai's; this PR only names them, and posts nothing on those cards.The escalation probe — the triage's p1 condition, MEASURED
The triage marked this p2 because all five instances were one seat self-superseding, and named the escalation condition it had not run: a second
Claim:from a DIFFERENT session on one card, which would be a silent ownership transfer printed green. I ran it.Population, read 2026-09-18T00:47:37Z → 00:48:42Z: every open card on both boards this gate reads — 529 open cards in
objectstack-ai/objectstack, 413 inobjectstack-ai/objectui(942 total), of which 880 carry at least one comment and were read; 163 carry at least one claim comment. 9 comment lists sit at the 100-comment cap and are UNJUDGED past it, exactly as #18683 prescribes. 0 parse failures.The answer is not 0 — it is 12. Twelve open cards carry LIVE
Claim:comments from two or more DIFFERENT authors with no retraction between them:#13503claude[bot]+baozhoutao#14026hotlong+claude[bot]#15811os-bill+os-litant#17852os-warren+os-litant#4730yinlianghui+os-sales#7070os-warren+claude[bot]#7696os-justin+os-tesla#7804os-tesla+os-sam+os-justin#7848claude[bot]+baozhoutao#7924os-warren+os-sales#8115claude[bot]+yinlianghui#9370os-tesla+os-justinWho the new exit 4 meets before it lands
The seat needs this before landing, so I swept it rather than assuming. Two facts:
check:pm-clause2-carriers— the only wiring,.github/workflows/lint.yml:1140— runs--self-testand nothing else. No workflow runs--pairor a sweep, so landing this changes no required context. The exit 4 appears only when a seat runs--pairor a sweep by hand.objectstack-ai/objectui#9584(open, not draft; its closing keyword namesobjectui#9499), which delivers a card carrying two live claims byos-try-charles(5663366106 on 2026-09-14, 5690579598 on 2026-09-16). That pair answers exit 4 at its next--pair.DEFAULT_SWEEP_REPOisobjectstack-ai/objectstack, so objectui is only ever read when passed explicitly.⛔ Nothing was posted on #9499, #9584 or any of the twelve.
The reading, and WHERE it is computed
claimRepeats(:1898) is a sibling pure reader besideclaimRetractions, built on it — the same map decides membership here and for governance, so the pool and this row cannot describe two different retractions. It names every author holding more than one LIVE claim comment, orders them by the file's one recency rule (laterOnThread, to ORDER the record, never to pick a winner), and resolves no state, no row and no exit code.c8SecondClaimSameSeat(:4380) renders the verdict;pairRows(:4430) pushes it as rowC8;pairInputRecordaddsclaim.repeat(:5876, declared inINPUT_RECORD_PAIR_FIELDSat :5641) as the READING — one derivation feeding both, so the record and the verdict cannot disagree about how many claims a seat holds or which they are.MEMBERSHIP first, and that is what makes the state repairable. The state is read over LIVE claims only. A re-claim after a
Release:is the protocol working and reads exactly as it did before. And a seat that already wrote a second claim has an act that clears the row:Release:what it holds, then one freshClaim:. A rule written over the writing moment instead ("no retraction strictly BETWEEN the two lines") would have been unrepairable by construction — nothing un-writes a comment — so the row would have been a permanent red with a remedy nobody could execute.The four axes
Claim:re-enters the pool as the newest claim and becomes what every downstream reader is handed — the property the correction key was deliberately designed NOT to have. Rendering that as a NOTE at exit 0 is precisely the tolerant-consumer shape this repo refuses: an adverse fact printed green is how a batch of identical mistakes stays invisible. Declaring the prohibition and not enforcing it is the "声明而未兑现" gap; the repair is to enforce it loudly, atEXIT_PAIR_ADVERSE. The row also ⛔ never prescribes WHICH repair — choosing between a correction and a release would be choosing whether the card is being re-taken, which is the seat's judgement, so it names both and writes nothing.Release:reads as it always did, aClause-②-correction:is not a claim and never was. The cross-seat question, which is a genuine second capability, is explicitly NOT taken here.The pins — per direction, each with a non-vacuity control
Release:or the id-naming retraction⛔ NOT supersededwording byte-unchangedClause-②-correction:as the later rowmarkerMatchesexactly as a bare one; the raw constant refuses both, so the counting is the sibling's ONE readingBranch:parses to zero branchesrowAuthornull)claimRetractionsdoes — andnullnever groups withnullDirection (i) has a control in the wild on this very card: the triage comment 5722477144 contains the word
Claim:mid-line, andmarkerMatchesrefuses it — card #18828 reads one claim comment, so--pairon this PR is silent.Roster line (:792):
'#18828: a SECOND \Claim:` by ONE seat — the writer-side prohibition, finally READ': 52— and the battery block declares exactly 52t(...)cases. **Floor** (:806):SELF_TEST_BATTERY_FLOOR` 31 → 32, raised by exactly one.The ablation
Run from the committed fix, twice, each leg proving its mutation landed on disk before the reading was taken and proving its restore by an empty
git diff HEADand by blob hash — never by an editing command's exit code.HEADblob3a270ef2eb5f33780e04e4732714f8e88d74a017.3a270ef2eb…72115d2796ead200f93aa855c8ba5820a83f5f8f40cfadd4f5740f34210675ceb998fb2977823769Both legs restored:
git diff HEADempty, blob back to3a270ef2eb….Total case count is 941 in all three runs — the rest of the self-test is byte-identical in its case count, and in both legs 0 of the failures fall outside the #18828 battery.
Leg A is the interesting one, because it shows the per-direction controls doing their job. Five of the nine directions assert SILENCE and therefore cannot go red when the detection is removed — their non-vacuity controls go red instead. All nine directions are carried:
Claim:", "give that same row a login", "move that same word to the OPENING of a line"Leg B is the narrower, sharper one: removing only the author test reds 3 cases, and pin (b) is among them. That is the pin which distinguishes this card from the cross-seat question — proof the author test is load-bearing and that (b) is not vacuous.
Self-test count: 889 before → 941 after (+52, exactly the registered battery). The 889 was measured by running
--self-testin a detached worktree atorigin/main88aa326deb.Gates
Derived from the worktree with⚠️ The first derivation printed STALE TREE — the branch was 2 commits behind
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no hand-fed path list).origin/mainand 8 files the derivation reads had changed — soorigin/mainwas merged in first and the list re-derived on the merged tree at7424cf3f44; the--repoassertion holds against this checkout'sorigin.34 derived, 34 run, all exit 0. Each exit code captured redirect-then-
$?, never across a pipe.Reconciled with
--ran, exit codes included: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN — "a DERIVED zero — all 34 recorded an exit code and none of them is 3".Repo-wide
pnpm lint(eslint . --no-inline-config): exit 0. The heavy run took a ticket throughscripts/pm/os-verify-lock.sh(slotissue-18828-dev), queued behind the seat's owndispatch-gates.mjs --self-test.node scripts/pm/check-clause2-carriers.mjs --pairon this PR is reported in the dispatch report — this card carries one claim comment, so the row is silent on it.skip-changeset:scripts/pm/**publishes nothing from any released package — the whole diff is one non-published script.Generated by Claude Code