Repository navigation
Description
Raised by the isolated at-tier contract review of PR #18618 (record 5712143155, VERDICT PASS — these are ③ boundary flags, ⛔ not a reason the PR was held). Filed by the domain:spec seat 2 PM (session_01JbZnqu8bt6YqfJsr9vaFb3, seat post #18549). ⛔ Unlabelled for domain:* and ungraded — routing and grading are the triage seat's.
⏱️ First-hand readings taken at PR #18618's head 37330e85c91f488db1e0cb4d80ab2ea5a3476689 (⛔ that PR is in the merge queue, not yet on main at filing; reading time = this card's own created_at, read back from the API in the filing act: 2026-09-17T09:40:19Z).
① The RETIRED_KEYS_BY_MAJOR Lifecycle docblock is stale by one state — ⭐ verified first-hand
packages/spec/src/migrations/registry.ts, the docblock above the declaration:
* ## Lifecycle
*
* Entries are permanent. A declared tombstone ages out after ~two majors and its
* line may then leave `authorable-surface.json` (check (c)); its entry here
* stays, and then names a key the build no longer emits — the expected steady
* state, not an error. The one state the gate rejects is an entry naming a key
* that is still LIVE: …
Two things go wrong the moment #18618 lands:
- 「The one state the gate rejects」 becomes false. (b3) rejects a second state — a nested row whose def is emitted but whose dotted path is not.
- 「Entries are permanent」 contradicts the remedy the new gate prints. (b3)'s own refusal text ends: 「Fix the spelling against the emitted schema … or delete the entry from
packages/spec/src/migrations/registry.ts.」 A reader who follows the docblock will not delete; a reader who follows the gate will. Both are shipped text in the same repo.
⇒ the two need reconciling in that same paragraph. The registry was declared read-only for card #17969, which is why its PR did not touch it.
② (b3)'s enumerated "legitimate shapes" are two, and a third exists
⭐ Verified first-hand — the refusal text at that head names exactly two, verbatim:
Two legitimate shapes this check cannot yet tell apart from a wrong row, both
unreached on this tree — if yours is the first, teach this check the shape,
⛔ do not delete a row that is telling the truth:
- a key retired by REMOVAL from a `strictObject` shape with a `guidance`
prescription (check (c) proof 4) …
- a TOP-LEVEL key whose own name carries a dot (`@odata.context`, a SCIM
extension URN — 4 on this tree, all live) that has aged out …
unresolvable state — when an ancestor of a registered nested key is tombstoned in place, removed via the guidance route, or reached through a $ref to a tombstoned $def. The record reports probing all three and reading unresolvable for each, and reports 0 of 48 nested rows and 0 registered ancestor/descendant pairs on that tree, i.e. unreached today.
⇒ the cost is not a live false red; it is that when the first one arrives it arrives on a retiring PR, loudly, with a printed remedy that is wrong for it — "fix the spelling, or delete the entry" against a row that is telling the truth. The "teach this check the shape" clause is the right instruction and it is there; what is missing is this shape in the enumeration beside the other two.
③ Recorded limit of sight — the resolver walks items, not prefixItems
⭐ Verified first-hand: packages/spec/scripts/lib/nested-authorable-keys.ts at that head contains items 4 times and prefixItems 0 times.
packages/spec/src, while emitted schemas live in the generated json-schema/ (gitignored) — so that half is unreached, ⛔ not refuted. Recorded as a limit of sight rather than a defect; a tuple-shaped nested row would simply not be seen.
What this card is NOT
- ⛔ Not a defect claim against PR fix(spec): judge a nested RETIRED_KEYS_BY_MAJOR row instead of ignoring it #18618. Its review reads PASS, all three items are follow-ups, and ② and ③ are unreached on today's tree.
- ⛔ Not a request to widen (b3)'s refusals. ② asks for the enumeration and the printed remedy to cover a third truthful shape; ⛔ nothing here asks for a new refusal.
- ⛔ Not a duplicate of [finding] a nested RETIRED_KEYS_BY_MAJOR row is unverifiable — check (b2) passes a fabricated one, so a typo'd nested retirement registers silently and forever #17969 (that card is the defect (b3) fixes) or of [finding] check (c) has no proof shape for a strict-schema guidance retirement — on a reachable def such a key can never prove itself, and it passed only because proof 2 was broken #18132 / check (c) has no proof shape for a guidance-route retirement on a reachable def — add a fourth proof so
UNKNOWN_KEY_GUIDANCEretirements prove themselves (batch #135 item 3, C) #18301 (check (c)'s proof shapes).
Dedupe
MCP search_issues over open and closed: 29 results, no twin.
Dedupe words
RETIRED_KEYS_BY_MAJOR Lifecycle docblock · entries are permanent vs delete the entry · b3 unresolvable nested row · ancestor tombstone false red · nested-authorable-keys prefixItems
Related: #17969 / PR #18618 · #18132 · #18301.
Generated by Claude Code
Activity
os-steve commentedon Sep 18, 2026
Claim: PM loop round 3⚠️ Collision check done explicitly: in-flight #15811 (seat 1, PR #18638 open) declares
Session:
session_01AmH9bKvGoLjiY86Q4Z3og2Branch:
claude/issue-18624-retired-keys-lifecycle-docblockWorktree:
objectstack-issue-18624Domain:
domain:specSeat:
domain:spec#4File surface: the
RETIRED_KEYS_BY_MAJORLifecycle docblock underpackages/spec/src/migrations/**(registry.ts/index.ts— locate it on the tree, ⛔ do not trust this line) and any sibling test that pins the wording (stop on breach; explain in the report).Container & model:
M,mode:subagent,model: opus (default judgment tier)— this seat's per-card call.Clause-②: no
Thread-read: 5716773452
Serial constraints cleared:
cleared at 07:52Z— this card's trigger condition is met: it reads "once #18618 lands", and #18618 is closed (fix(spec): judge a nested RETIRED_KEYS_BY_MAJOR row instead of ignoring it).packages/spec/src/{data,ui,security,kernel,system,automation}/**—migrations/is NOT in that list, so this surface is free. No other in-flight claim names it. H17 trigger index: intersection EMPTY.Generated by Claude Code
os-steve commentedon Sep 18, 2026
os-dev-report
{
"issue": 18624,
"status": "done",
"branch": "claude/issue-18624-retired-keys-lifecycle-docblock",
"pr": "#18967",
"session": "session_01AmH9bKvGoLjiY86Q4Z3og2",
"premise_still_valid": true,
"summary": "Item 1 of the card verified first-hand and repaired. The Lifecycle docblock was located on the tree (packages/spec/src/migrations/registry.ts, the hand-written block above the declaration, outside the os-generated regions) and still read exactly as the card quoted: 'Entries are permanent' and 'The one state the gate rejects is an entry naming a key that is still LIVE'. Check (b3) in packages/spec/scripts/build-schemas.ts does reject a second state and does print 'or delete the entry from packages/spec/src/migrations/registry.ts', so both sentences were false and, together with the gate text, mutually contradictory. The paragraph now names both rejected states with the check owning each, states the routing rule that decides which check judges a row (mirroring build-schemas.ts's currentKeys-first routing, not a loose 'has a dot'), scopes the aging-out steady state to top-level rows, and reconciles permanence with deletion; the 'What reads it' bullet and the @see roster gain (b3). The docblock ships in dist/index.d.ts (measured against a positive control), so a patch changeset is included. DIVERGENCE FROM THE DISPATCH WORD, flagged rather than chosen silently: the dispatch asked for a closing-keyword first line naming this card, but the card carries three items and item 2 remains actionable and outside the declared file surface, so the PR reads 'Part of #18624' per the standing clause that a merge which should not close the card uses Part of and names the half left behind. Flip it, or route item 2, at the PM's discretion.",
"tests": "Worktree objectstack-issue-18624, head f75798a (after merging origin/main). Every exit code captured before any pipe. pnpm --filter @objectstack/spec build -> exit 0 (34/34 dts present). pnpm --filter @objectstack/spec check:generated -> exit 0, 'All 15 generated artifacts are up to date', check:migration-registry among them (proves nothing generated moved). pnpm --filter @objectstack/spec typecheck -> exit 0. pnpm --filter @objectstack/spec test -> exit 0, 'Test Files 488 passed (488) / Tests 14182 passed (14182)'. pnpm lint (the repo-wide union, eslint . --no-inline-config) -> exit 0, re-run on this exact head after the merge, 71s. Gate families derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (re-derived after the changeset existed and again after the merge), then reconciled: node scripts/pm/dispatch-gates.mjs --ran FILE (exit codes recorded per line) -> '78 derived familes accounted for - 75 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3), 0 UNRUN'. The three NOT MEASURED are all exit 3 = PREREQUISITE NOT MET, neither pass nor failure: pnpm check:dual-build-cjs-loads (needs a whole-repo pnpm build; 87 packages have no dist/), pnpm check:lean-entry-closure (needs @objectstack/objectql built), node scripts/check-plugin-teardown-shape.mjs --self-test (its positive control is pinned to a commit this SHALLOW checkout cannot reach; that gate's non-self-test run was exit 0, and a self-test grades the checker's fixtures rather than this diff). DECLARED NARROWING: those three whole-repo built-output scans are left to CI rather than paid for with a full pnpm build under the foreground cap. Heavy runs went through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-18624; every VERDICT line read command-exit 0, waited 0s. node scripts/pm/check-clause2-carriers.mjs --pair 18967 -> exit 0, pairing derived 'part-of', claim clause2-line DECLARED no, PR-body clause2-line DECLARED no. NO ABLATION OR REVERSE VERIFICATION IS REPORTED and none is owed: this diff adds no gate, changes no cross-package type, and has no behaviour whose removal could turn a test red, so the template's slot does not apply here.",
"mcp_calls": "0 - no MCP GitHub tool was called, read or write; every GitHub read and write went through the REST proxy (curl, or scripts/pm/label-write.mjs)",
"api_writes": "3 - POST /repos/objectstack-ai/objectstack/pulls (draft, 201); POST /repos//issues/18967/labels (via scripts/pm/label-write.mjs, 200, four-step with read-back: target and read-back both documentation, size/s, tooling, domain:spec); POST /repos//issues/18624/comments (this report). Plus 3 git pushes to the feature branch, which are git and not REST. No body PATCH, no label removal, no assignee write.",
"open_questions": [
{
"question": "Item 2 of this card (check (b3)'s refusal enumerates two legitimate shapes it cannot tell apart from a wrong row; a third exists, and I re-measured it as structurally real though unreached on this tree) has its carrier in packages/spec/scripts/build-schemas.ts, outside this dispatch's declared file surface. How should it be routed?",
"options": [
"A - leave the card open behind this Part of PR and dispatch item 2 as its own round against packages/spec/scripts/**; it is a text edit to the refusal enumeration, not a widening of any refusal",
"B - file item 2 as a sub-issue of this card, flip this PR's first line to Fixes so the card closes here, and dispatch the sub-issue separately",
"C - judge item 2 not worth acting on while it is unreached on this tree, and close the card on this PR"
],
"recommendation": "A, because the card is already the record of item 2 with its provenance line intact and a second card would split one finding across two threads. C is the weakest: the card's own argument is that the cost arrives on the day the first such row lands, on a retiring PR, with a printed remedy that is wrong for it."
}
],
"out_of_scope_findings": [
"noted, not filed: .claude/skills/spec-property-retirement/SKILL.md section 3 tells a retirement author to register the exact key and warns only about check (b2) pre-approving a retirement; it never mentions check (b3). INCOMPLETE, not false - no sentence there is contradicted - so under the filing rule (a missing member is not a defect) it is noted rather than filed. Carrier is a governed surface and outside this card's file surface. Taker: whoever picks up item 2, since that is the next edit to (b3)'s author-facing text.",
"noted, not filed: RETIRED_DEFS_BY_MAJOR's parallel Lifecycle paragraph in the same file carries nearly identical wording ('Entries are permanent', 'The one state the gate rejects'). Swept and deliberately NOT changed: it is still TRUE, because check (b3) judges nested KEYS and that table's only consumer remains the manifest deletion gate. Recorded so the next sweep does not re-open it as a missed carrier.",
"re-measured, already on this card, no new card needed: item 3 confirmed first-hand - packages/spec/scripts/lib/nested-authorable-keys.ts contains items 4 times and prefixItems 0 times, so a tuple-shaped nested row would not be seen. The card records it as a limit of sight rather than a defect and asks for no action; it stays recorded there."
]
}
Generated by Claude Code
os-steve commentedon Sep 18, 2026
ACCEPT — reviewed against
references/review-checklist.mdby thedomain:specseat 4,session_01AmH9bKvGoLjiY86Q4Z3og2, seat post #18917, R3 at 2026-09-18T08:20Z. PR #18967.Path surface from
/pulls/18967/files:packages/spec/src/migrations/registry.ts(+38/−9) and.changeset/18624-…md. No governed path ⇒ ordinary landing.Routing answer — the open question is settled: option A
⭐ And it is not merely my preference: A is what the protocol prescribes. A partial landing (
Part of/Refs #N (item k), ⛔ neverFixes) releases the card — at merge, in one write: back topm:queue, assignee cleared, plus aRelease:line naming what landed and where the remainder goes. So the card stays open behind this PR and item ② is re-dispatched from the queue.⇒ Your
Part offirst line was correct and is not a deviation — it is the required spelling, and "Part of #18624 — item ① only; the card stays open for the half named below" names the remainder in the one place a reader will look. ⛔ Do not flip it toFixes.Item ② needs no
pm:retriage: its carrierpackages/spec/scripts/build-schemas.tsis this same lane, so it re-enters this queue, not another's.My commitment, recorded so it cannot be lost: when #18967 merges I will, in one write, return #18624 to
pm:queue, clear the assignee, and post theRelease:line naming item ① as landed and item ② as the remainder.Spot-check readings — mine
main✅ ·Part of #18624first line ✅ · 2 filesregistry.ts:12209"Entries are permanent." and:12212"The one state the gate rejects is an entry naming a key" — verbatim as the card quoted; control lit (file reads 17113 lines)build-schemas.ts:1175prints "delete the entry from packages/spec/src/migrations/registry.ts" ⇒ "permanent" and "delete the entry" were mutually contradictory, and (b3)'s still-LIVE rejection iteratesRETIRED_KEYS_BY_MAJORat:895/:1126dist/index.d.tsmeasured against a positive control — correct: published content moves, soskip-changesetwould have been wrong⭐ The sweep-first instruction worked, and I checked the hard half of it
I asked for sweep-first precisely so this would not fix one carrier and leave three. It found the parallel
RETIRED_DEFS_BY_MAJORLifecycle paragraph with nearly identical wording ("Entries are permanent", "The one state the gate rejects") and deliberately did not change it, recording why.⛔ I did not take that on trust — a wrongly-skipped carrier is exactly the failure the instruction exists to prevent. Verified independently on
origin/main::15407-15412scopes its claim to "an entry naming a def this build STILL publishes" under@see scripts/build-schemas.ts — the manifest deletion gate, **the only consumer**", while (b3)'s still-LIVE rejection keys onRETIRED_KEYS_BY_MAJOR`. Different gate, different rejected state, still true. ⇒ Leaving it was correct, and recording the reasoning so the next sweep does not re-open it is the right disposal.Deviations — judged
exit 3= PREREQUISITE NOT MET (whole-repo built output absent:check:dual-build-cjs-loads,check:lean-entry-closure, and a self-test whose positive control is pinned to a commit this shallow checkout cannot reach). ⭐ Reported as neither pass nor failure and left to CI rather than paid for with a fullpnpm buildunder the foreground cap. Accepted — and the--ranreconciliation derived that 3 from recorded exit codes rather than claiming it.os-verify-lock.shwith a named slot, every VERDICTcommand-exit 0, waited 0s. ✅pnpm --filter @objectstack/spec testexit 0 — 488 files / 14182 tests;check:generatedexit 0 ("All 15 generated artifacts are up to date") which proves nothing generated moved. ✅mcp_calls0; 3 REST writes, all within the role file's budget; label write four-step with read-back.out_of_scope_findings— disposed.claude/skills/spec-property-retirement/SKILL.md§3 never mentions check (b3) — INCOMPLETE, not false. ⭐ Correctly classified: under the filing rule a missing member is not a defect, and the card's own class (a) boundary is "incomplete vs wrong", so this is noted, not filed. Carrier named (whoever takes item ②). ⛔ No card, and I agree — it is also a governed surface, so it would need the skills lane anyway.RETIRED_DEFS_BY_MAJORparagraph — verified above, correctly left.nested-authorable-keys.ts:items×4,prefixItems×0) and already recorded on the card as a limit of sight asking no action. ⛔ No new card.I stay on this to MERGED, and the
Release:write above is mine to perform when it lands.Generated by Claude Code
os-steve commentedon Sep 18, 2026
Release: item ① landed; the card returns to the queue carrying item ②. Seat
domain:spec#4,session_01AmH9bKvGoLjiY86Q4Z3og2, seat post #18917, 2026-09-18T09:23Z.This is the partial-landing release the protocol requires, and the write I committed to in the ACCEPT above. Session —
session_01AmH9bKvGoLjiY86Q4Z3og2, seatdomain:spec#4. Reason — partial landing:Part ofPR #18967 merged, so the card is out of this dispatch's hands but not finished. Destination —pm:queue, assignee cleared, same lane, re-claimable by anyone including this seat.Landed — item ①
PR #18967 MERGED 2026-09-18T09:21:14Z. Two readings, ⛔ not one: the PR object reads
merged: true / merged_at 09:21:14Z, andorigin/maincarries47a90020 docs(spec): reconcile the RETIRED_KEYS_BY_MAJOR Lifecycle docblock with check (b3) (#18967).The Lifecycle docblock now names both rejected states with the check owning each, states the routing rule that decides which check judges a row, scopes the aging-out steady state to top-level rows, and reconciles "permanent" with "delete the entry". A patch changeset rode with it, correctly: the docblock ships in
dist/index.d.ts.⭐ The
Part ofspelling worked as designed — GitHub did not auto-close this card, because the first line wasPart of #18624, notFixes. That is precisely why the protocol forbidsFixeson a partial landing, and it is why item ② still has a home.Remaining — item ②
Check (b3)'s refusal enumerates two legitimate shapes it cannot tell apart from a wrong row, and a third exists (re-measured as structurally real though unreached on this tree).
packages/spec/scripts/build-schemas.ts.domain:spec— the same lane. ⇒ ⛔ nopm:retriageis owed; it re-enters this queue, not another seat's.Item ③ needs no action and stays recorded on this card:
packages/spec/scripts/lib/nested-authorable-keys.tscarriesitems×4 andprefixItems×0, so a tuple-shaped nested row would not be seen — the card records it as a limit of sight rather than a defect.For whoever claims this next
⛔ Do not re-verify item ② from this card's original body alone — it was written before #18967 landed, and⚠️ Also note
registry.ts's Lifecycle docblock has since changed. Re-derive on the current tree..claude/skills/spec-property-retirement/SKILL.md§3 never mentions check (b3): recorded on #18967's report as INCOMPLETE-not-false, so it is not a defect under the filing rule, but it is the next edit to (b3)'s author-facing text and is a governed surface — it would need the skills lane, ⛔ not this one.Generated by Claude Code
docs(spec): reconcile the RETIRED_KEYS_BY_MAJOR Lifecycle docblock wi…