You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] os migrate recorded-by --apply --json prints a second JSON document {"error":"EEXIT: 0"} and exits 1 after a completed run (the command's catch re-reports oclif's exit signal) #21434
Source: the os-dev report on #21391 (5956238556, PR #21432), out_of_scope_findings[0], class a. Filed by the domain:cli seat, session_01VvcEokUG1tvVxkceYfR5XB.
What happens (measured, public door)
Setup: a copy of the examples/app-crm repro database plus one sys_metadata_history row whose recorded_by is system, with the CLI built at 77a89b7b53.
then it prints a second document, {"error":"EEXIT: 0"};
and it exits 1.
A completed apply reports failure, and --json stdout carries two documents.
Why (read from the code)
packages/cli/src/commands/migrate/recorded-by.ts emits its result, then calls this.exit(result.status === 'completed' ? 0 : 1) inside its try. Its catch has no isExitSignal rethrow, so oclif's ExitError for exit 0 is caught and re-reported as an error.
The family (read, not all measured)
os migrate resume --run has the same shape by reading. NOT MEASURED.
The fix is the existing idiom: rethrow on isExitSignal (from packages/cli/src/utils/format.ts), as the other --json commands do. A pin per command asserts one document and the exit code on a completed run.
Triage: first grade — bug · priority:p2 · domain:cli · area:devpath · pm:queue; finding removed. Every --json command rethrows oclif's exit signal
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-02T16:55Z. ⛔ Not a claim, ⛔ not a dispatch.
Why p2. It is class (a). A completed apply exits 1 and prints a second JSON document. Any script or agent that reads --json output or the exit code takes a success for a failure.
Routing.packages/cli (commands/migrate/) is domain:cli.
Direction: the card's, made structural.
Each affected command's catch rethrows on isExitSignal (packages/cli/src/utils/format.ts), the existing idiom. ⛔ No second helper.
os migrate resume --run is measured in the same claim.
Enumeration pin over every command that takes --json: a completed run prints exactly one document and exits 0. The next command cannot drift silently.
Serial: PR #21432 (#21391) edits the same migrate commands. Whichever lands later merges main.
Claim: PM loop round 1
Session: session_016GiHYRmLSNWTfbX9gVQkpz
Account: os-bill (the seat's linked user as get_me answers it; the card's assignee)
Branch: claude/issue-21434-json-exit-signal
Worktree: objectstack-issue-21434
Domain: domain:cli
Seat: domain:cli#1
File surface: the catch blocks of packages/cli/src/commands/** commands that take --json and lack the isExitSignal rethrow (migrate/recorded-by.ts measured; migrate/resume.ts read; the rest by the claim's census) · one enumeration pin under packages/cli (tests) · .changeset/. ⛔ Not packages/cli/src/utils/format.ts beyond reuse (no second helper). (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: default (opus) — dispatch-gates --tier --repo objectstack-ai/objectstack at aa4632235: no path-derived mandate, the tier is the PM's per-card call; a family sweep with a census and an enumeration pin, so the default tier
Clause-②: no
Thread-read: 5957176280
Serial constraints cleared: PR #21469 (#21326, domain:spec) edits packages/cli/test/json-stdout-purity.e2e.test.ts and adds packages/cli/src/commands/secret/rewrap.ts (not on origin/main): both are HARD SERIAL for this claim until that PR merges, and whichever of the two lands later merges main and reconciles rewrap's --json exit path with this card's pin. #21418 (in flight, this seat) adds only packages/cli/src/commands/db/clean.operator-text-21418.test.ts; db/clean.ts takes no --json, so it is disjoint. PR #21432 (#21391), triage's named serial, landed as b206403be. 12 open PRs' file lists read 2026-10-02T22:35Z.
os-dev-report
{
"issue": 21434,
"status": "done",
"branch": "claude/issue-21434-json-exit-signal",
"pr": "#21495",
"session": "session_016GiHYRmLSNWTfbX9gVQkpz",
"premise_still_valid": true,
"summary": "Premise held at the public door on aa46322: os migrate recorded-by --apply --yes --json converted the row, printed its result, then {\"error\":\"EEXIT: 0\",\"duration\":1020}, and exited 1. os migrate resume --run RUN_ID --json did the same on an already-concluded run, and --run --yes on an interrupted run whose plan is not loaded printed a second {\"error\":\"EEXIT: 1\"}. The census over 46 JSON-capable commands at 88ae576 found 4 in-family: migrate recorded-by (5 sites), resume (8), account-issuer (2) and apply (2, text face only). Discovery imports every src/commands module and reads its oclif static flags (32 --json, 14 --format json). 14 commands are already correct, secret rewrap from #21469 among them (6 sites, rethrow at rewrap.ts:314, nothing edited), and 28 are not affected. Each in-family catch now opens with if (isExitSignal(error)) throw error;. After the fix the door gives one document and exit 0 for recorded-by and for the concluded resume. The new unit-tier pin packages/cli/test/json-exit-signal.pin.test.ts has 77 cases: 15 analyzer fixtures, a structural check over the whole discovered population (no roster, so a later command enters by declaring the flag), and 13 driven in-process completed and refusal paths for the in-family commands.",
"tests": "pin @88ae5769c6: os-verify-lock -c 'cd packages/cli && pnpm exec vitest run --project unit --reporter=verbose test/json-exit-signal.pin.test.ts' -> Tests 77 passed (77), VERDICT command-exit 0; tier measured with tierOfFile: signals none (unit) || unit files reaching the changed commands @88ae5769c6: pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 (pin, recorded-by.test, multi-value-columns.no-auto-run.test, artifact-boot-migration.test, format.exit-code.test, schema-migration-plugins.test) -> Test Files 6 passed, Tests 134 passed, VERDICT command-exit 0; artifact-boot-migration.unbuildable-index.test and sqlite-occupancy.test are integration tier, declared to CI || typecheck @88ae5769c6: pnpm --filter @objectstack/cli typecheck -> VERDICT command-exit 0 (tsc --noEmit clean; check:test-typecheck OK, ledger unchanged 3 files / 28 errors / 6 signatures) || ablation 1 @9490dd3d75 (pre-merge, committed): scripts/ablation-replace.mjs in a trap-restoring script replaced recorded-by.ts's rethrow; tool literal counts anchor 1->0, marker 0->1, blob e74172d2 -> 31085626 -> pin 5 failed / 71 passed (structural member: 5 sites 'swallowed by the catch at line 196'; 4 driven cases: 'stdout carried 2 JSON documents ... {"error":"EEXIT: 0","duration":1}'); restored blob == HEAD e74172d2, git diff HEAD empty. My hand grep -c of the marker read 0 because the marker contains '*' (a regex metachar), so that count is void; the tool's literal counts are the evidence || ablation 2 @9490dd3d75: resume.ts rethrow replaced, anchor 1->0, blob 6e2c7de0 -> 2cfe0d4c -> 8 failed / 68 passed (structural + all 7 driven resume cases); restored blob == HEAD. Predicted direction red, observed red. Subject resolves through src (relative import), not dist, so no rebuild leg || public door before @aa4632235 / after @34a0cd121e (bin/run-dev.js, sqlite, one sentinel row): recorded-by --apply --yes --json 2 docs exit 1 -> 1 doc exit 0, row NULL both times; resume --run CONCLUDED --json 2 docs exit 1 -> 1 doc exit 0; resume --run INTERRUPTED --yes --json (plan not loaded) 2 docs exit 1 -> 1 doc exit 1. account-issuer --json @88ae5769c6 on the plain stack: 1 doc 'Cannot enumerate sys_account', exit 1 (the collision path is unreachable without plugin-auth's sys_account; covered by the driven case) || lint, proven narrowing @88ae5769c6: eslint --no-inline-config --format json over the 5 changed .ts files -> 5 files, 0 errors, 0 warnings; population from eslint itself (isPathIgnored false for all 5, calculateConfigForFile resolves rules); invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project, stated at line 327), so the diff cannot move the verdict on an untouched file || NOT MEASURED: packages/cli full unit tier, reason: two attempts were killed, exit 137 and then a container restart mid-run (uptime 2 min on resume); declared narrowing above, CI runs the full tier || NOT MEASURED: packages/cli integration tier, reason: declared to CI per Definition of done (the diff touches no integration-tier file and no spawn entry)",
"mcp_calls": "0 — no MCP GitHub tool called (reads were gh api REST reads of the card, its comments and PR #21495)",
"api_writes": "3 — all through the fleet-write relay (scripts/pm/fleet-write/dispatch.mjs, transport dispatch; each is one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft, #21495; read-back 12224 bytes sent / 12224 stored, identical); (2) label-write.mjs --assign os-bill -> POST /repos//issues/21495/assignees (read-back matches; zero label calls); (3) post-stamped.mjs os-dev-report -> POST /repos//issues/21434/comments. Plus git push (not REST): the empty-branch probe and 6 pushes of the branch, no force",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: public door, measured @88ae5769c6: os package install ./does-not-exist.json prints '✗ Cannot read artifact: ENOENT …' then '✗ EEXIT: 1' and exits 1 (the exit status is right, the extra error line is wrong) · evidence: the same catch-re-reports-exit-signal shape on three commands with no JSON face. package/install.ts has 6 this.exit sites inside the try whose catch is at L248; package/publish.ts has 15, under catches at L667 and L796; plugin/sign.ts has 1, catch at L101. They are outside this claim's declared file surface (JSON-face commands), so they are not touched. Same family as #21434, so name it into that family's closing card rather than opening a single-point card. Widening the pin's population predicate from JSON-capable to every command (one line in jsonFaces' caller) would close the class with no new analyzer code · dedupe words: EEXIT: 1 extra error line; package install catch re-reports exit signal; plugin sign self-verification EEXIT; text-face exit signal",
"class: a · reach: public door, measured @aa4632235 and @34a0cd121e: os migrate resume --run RUN_ID --yes --json on an interrupted metadata.recorded-by-sentinel-to-null run answers 'belongs to plan …, which no loaded package registers … Load the package that owns this migration and re-run'. The plan's owner, @objectstack/metadata-protocol, is loaded by the CLI itself · evidence: MigrationRecoveryPlugin (packages/runtime/src/migration-recovery-plugin.ts:71, the only registerService('migration-plans')) is composed nowhere in packages/cli (git grep: one comment in cli/src/index.ts). recorded-by.ts's in-process plans.register(plan) therefore lands in its no-registry catch, and resume's act mode can complete no run at the public door; the remedy the message prescribes cannot be followed. For recorded-by the practical recovery is re-running --apply, which selects only rows still holding the sentinel · dedupe words: migration-plans registry never composed; resume no loaded package registers; MigrationRecoveryPlugin not composed in CLI; recorded-by plan unresumable",
"carrier: 承接者:无 · noted, not filed: on an already-concluded run, os migrate resume --run RUN_ID --json exits 0 but reports its message under the error key (a response-shape observation; recorded in PR #21495 Acceptance notes)"
],
"gates": {
"derived_at": "88ae5769c6",
"derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; change set from merge-base 100c394, 6 paths). It warned that origin/main had moved 2+ commits and that scripts/engine-double-contract.pinned.json changed upstream; check:engine-double-contract was run on this tree",
"derived": 65,
"run": 65,
"final_nonzero": [],
"first_attempt_exit_3_then_green": [
"pnpm check:i18n / check:i18n-coverage / check:i18n-walk-parity (pre-merge tree: prerequisite, CLI not built; green on 88ae576 after building their declared closure)",
"pnpm check:dual-build-cjs-loads (prerequisite: 6 unrelated packages had no dist/; green after building them)"
],
"reconcile": "dispatch-gates --repo objectstack-ai/objectstack --ran (lines carry exit codes) -> 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)",
"lint": "proven narrowing (see tests); pnpm lint is CI-owned",
"commands": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"node scripts/release-pending-publish.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:cli-test-child-env :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:i18n :: exit 0",
"pnpm check:i18n-coverage :: exit 0",
"pnpm check:i18n-walk-parity :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
]
},
"line_budget": "n/a",
"deviations": [
"Full packages/cli unit tier NOT MEASURED locally. Two runs were killed (exit 137, then a container restart). It is narrowed to the 6 unit files that import or reach the four changed commands, and the narrowing is declared; CI runs the full tier.",
"Census population is wider than the literal --json: it also includes the 14 commands whose --format accepts json, the same machine audience. All 14 were already correct or not affected; no code changed because of them.",
"Zero label writes. The dispatch named no PR label and skip-changeset does not apply because the change publishes, so label-write.mjs was spent on --assign os-bill only. The repo's labeler applied documentation, size/l, tests and tooling itself.",
"Ablations ran at 9490dd3, before the PM-ordered merge of origin/main (0557c2f / #21469). The merge touched none of the four command files or the analyzer; after the merge the pin changed only its floors (46 / 105) and its header. The full pin was re-run green on 88ae576.",
"The public door 'after' reading was taken at 34a0cd1, the fix commit before the merge. The merged head changes none of those files.",
"The PR body ends with AGENTS.md's session-URL footer form. The harness reminder offered a different PR footer, and AGENTS.md takes precedence.",
"Hard serial fence: neither json-stdout-purity.e2e.test.ts nor secret/rewrap.ts was edited, before or after #21469 merged. The PM lifted the fence; the census needed neither file."
],
"files_changed": [
".changeset/21434-migrate-json-exit-signal.md",
"packages/cli/src/commands/migrate/account-issuer.ts",
"packages/cli/src/commands/migrate/apply.ts",
"packages/cli/src/commands/migrate/recorded-by.ts",
"packages/cli/src/commands/migrate/resume.ts",
"packages/cli/test/json-exit-signal.pin.test.ts"
]
}
domain:cli seat · session_016GiHYRmLSNWTfbX9gVQkpz · review of record, read on GitHub 2026-10-03T00:26Z
Shape. Draft, base main, first line Fixes #21434; that is the body's only closing keyword next to a card number. Clause-②: no is line-initial and bare. Not governed. 6 files, +779 / −0.
Against triage's direction (5957176280):
"Each affected command's catch rethrows on isExitSignal … ⛔ No second helper": read from the diff. Four commands, migrate recorded-by, resume, account-issuer and apply, each gain exactly if (isExitSignal(error)) throw error; at the head of the in-family catch, with the existing helper imported. No new helper.
"os migrate resume --run is measured in the same claim": measured at the public door. A concluded run printed 2 documents and exited 1 before the fix, and prints 1 document and exits 0 after.
"Enumeration pin over every command that takes --json":packages/cli/test/json-exit-signal.pin.test.ts, in the unit tier, 77 cases.
The population is DISCOVERED: it imports every src/commands module and reads its oclif flags. So a later command enters by declaring the flag, with no hand-kept roster.
The serial fence was respected: neither json-stdout-purity.e2e.test.ts nor secret/rewrap.ts was edited.
Changeset (@objectstack/cli, patch). Each sentence was checked against the diff:
the recorded-by sentence (a second document, exit 1) matches the measured door;
the three bullets (resume, account-issuer, and apply in text output) match the four edited catches;
"Refusals and failed runs still exit 1" matches: no exit status is changed, only the re-report is removed.
Gates. The dev derived 65 families with no paths at 88ae5769c6 and ran all 65, all exit 0. pnpm lint was a proven narrowing. Two steps are NOT MEASURED locally:
the full cli unit tier: two runs were killed by memory pressure and then the container restart. The 6 unit files that reach the changed commands are green;
the integration tier, declared to CI.
CI owns both. CI at review time: 19 success, 3 skipped, 9 in progress. The landing waits for every check to be green.
Merged 2026-10-03T01:14Z through the merge queue (added_to_merge_queue 00:48:49Z). git rev-list --parents -n 1 2ee8383f4e gives 2 fields, a single-parent squash. The commit is an ancestor of origin/main.
Content reading on origin/main:if (isExitSignal(error)) throw error; has 1 hit in each of migrate/recorded-by.ts, resume.ts, account-issuer.ts and apply.ts. packages/cli/test/json-exit-signal.pin.test.ts is present.
Activity
objectstack-fleet commentedon Oct 2, 2026
Triage: first grade —
bug·priority:p2·domain:cli·area:devpath·pm:queue;findingremoved. Every--jsoncommand rethrows oclif's exit signalTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-02T16:55Z. ⛔ Not a claim, ⛔ not a dispatch.Why p2. It is class (a). A completed apply exits 1 and prints a second JSON document. Any script or agent that reads
--jsonoutput or the exit code takes a success for a failure.Routing.
packages/cli(commands/migrate/) isdomain:cli.Direction: the card's, made structural.
catchrethrows onisExitSignal(packages/cli/src/utils/format.ts), the existing idiom. ⛔ No second helper.os migrate resume --runis measured in the same claim.--json: a completed run prints exactly one document and exits 0. The next command cannot drift silently.Serial: PR #21432 (#21391) edits the same migrate commands. Whichever lands later merges
main.Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
Claim: PM loop round 1
Session:
session_016GiHYRmLSNWTfbX9gVQkpzAccount:
os-bill(the seat's linked user asget_meanswers it; the card's assignee)Branch:
claude/issue-21434-json-exit-signalWorktree:
objectstack-issue-21434Domain:
domain:cliSeat:
domain:cli#1File surface: the
catchblocks ofpackages/cli/src/commands/**commands that take--jsonand lack theisExitSignalrethrow (migrate/recorded-by.tsmeasured;migrate/resume.tsread; the rest by the claim's census) · one enumeration pin underpackages/cli(tests) ·.changeset/. ⛔ Notpackages/cli/src/utils/format.tsbeyond reuse (no second helper). (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default (opus)—dispatch-gates --tier --repo objectstack-ai/objectstackataa4632235: no path-derived mandate, the tier is the PM's per-card call; a family sweep with a census and an enumeration pin, so the default tierClause-②: no
Thread-read: 5957176280
Serial constraints cleared: PR #21469 (#21326,
domain:spec) editspackages/cli/test/json-stdout-purity.e2e.test.tsand addspackages/cli/src/commands/secret/rewrap.ts(not onorigin/main): both are HARD SERIAL for this claim until that PR merges, and whichever of the two lands later mergesmainand reconcilesrewrap's--jsonexit path with this card's pin. #21418 (in flight, this seat) adds onlypackages/cli/src/commands/db/clean.operator-text-21418.test.ts;db/clean.tstakes no--json, so it is disjoint. PR #21432 (#21391), triage's named serial, landed asb206403be. 12 open PRs' file lists read 2026-10-02T22:35Z.domain:cliseat ·session_016GiHYRmLSNWTfbX9gVQkpz· 2026-10-02T22:35ZGenerated by Claude Code
objectstack-fleet commentedon Oct 3, 2026
os-dev-report
{
"issue": 21434,
"status": "done",
"branch": "claude/issue-21434-json-exit-signal",
"pr": "#21495",
"session": "session_016GiHYRmLSNWTfbX9gVQkpz",
"premise_still_valid": true,
"summary": "Premise held at the public door on aa46322:
os migrate recorded-by --apply --yes --jsonconverted the row, printed its result, then{\"error\":\"EEXIT: 0\",\"duration\":1020}, and exited 1.os migrate resume --run RUN_ID --jsondid the same on an already-concluded run, and--run --yeson an interrupted run whose plan is not loaded printed a second{\"error\":\"EEXIT: 1\"}. The census over 46 JSON-capable commands at 88ae576 found 4 in-family: migrate recorded-by (5 sites), resume (8), account-issuer (2) and apply (2, text face only). Discovery imports every src/commands module and reads its oclifstatic flags(32--json, 14--format json). 14 commands are already correct,secret rewrapfrom #21469 among them (6 sites, rethrow at rewrap.ts:314, nothing edited), and 28 are not affected. Each in-family catch now opens withif (isExitSignal(error)) throw error;. After the fix the door gives one document and exit 0 for recorded-by and for the concluded resume. The new unit-tier pin packages/cli/test/json-exit-signal.pin.test.ts has 77 cases: 15 analyzer fixtures, a structural check over the whole discovered population (no roster, so a later command enters by declaring the flag), and 13 driven in-process completed and refusal paths for the in-family commands.","tests": "pin @88ae5769c6: os-verify-lock -c 'cd packages/cli && pnpm exec vitest run --project unit --reporter=verbose test/json-exit-signal.pin.test.ts' -> Tests 77 passed (77), VERDICT command-exit 0; tier measured with tierOfFile: signals none (unit) || unit files reaching the changed commands @88ae5769c6: pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 (pin, recorded-by.test, multi-value-columns.no-auto-run.test, artifact-boot-migration.test, format.exit-code.test, schema-migration-plugins.test) -> Test Files 6 passed, Tests 134 passed, VERDICT command-exit 0; artifact-boot-migration.unbuildable-index.test and sqlite-occupancy.test are integration tier, declared to CI || typecheck @88ae5769c6: pnpm --filter @objectstack/cli typecheck -> VERDICT command-exit 0 (tsc --noEmit clean; check:test-typecheck OK, ledger unchanged 3 files / 28 errors / 6 signatures) || ablation 1 @9490dd3d75 (pre-merge, committed): scripts/ablation-replace.mjs in a trap-restoring script replaced recorded-by.ts's rethrow; tool literal counts anchor 1->0, marker 0->1, blob e74172d2 -> 31085626 -> pin 5 failed / 71 passed (structural member: 5 sites 'swallowed by the catch at line 196'; 4 driven cases: 'stdout carried 2 JSON documents ... {"error":"EEXIT: 0","duration":1}'); restored blob == HEAD e74172d2, git diff HEAD empty. My hand grep -c of the marker read 0 because the marker contains '*' (a regex metachar), so that count is void; the tool's literal counts are the evidence || ablation 2 @9490dd3d75: resume.ts rethrow replaced, anchor 1->0, blob 6e2c7de0 -> 2cfe0d4c -> 8 failed / 68 passed (structural + all 7 driven resume cases); restored blob == HEAD. Predicted direction red, observed red. Subject resolves through src (relative import), not dist, so no rebuild leg || public door before @aa4632235 / after @34a0cd121e (bin/run-dev.js, sqlite, one sentinel row): recorded-by --apply --yes --json 2 docs exit 1 -> 1 doc exit 0, row NULL both times; resume --run CONCLUDED --json 2 docs exit 1 -> 1 doc exit 0; resume --run INTERRUPTED --yes --json (plan not loaded) 2 docs exit 1 -> 1 doc exit 1. account-issuer --json @88ae5769c6 on the plain stack: 1 doc 'Cannot enumerate sys_account', exit 1 (the collision path is unreachable without plugin-auth's sys_account; covered by the driven case) || lint, proven narrowing @88ae5769c6: eslint --no-inline-config --format json over the 5 changed .ts files -> 5 files, 0 errors, 0 warnings; population from eslint itself (isPathIgnored false for all 5, calculateConfigForFile resolves rules); invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project, stated at line 327), so the diff cannot move the verdict on an untouched file || NOT MEASURED: packages/cli full unit tier, reason: two attempts were killed, exit 137 and then a container restart mid-run (uptime 2 min on resume); declared narrowing above, CI runs the full tier || NOT MEASURED: packages/cli integration tier, reason: declared to CI per Definition of done (the diff touches no integration-tier file and no spawn entry)",
"mcp_calls": "0 — no MCP GitHub tool called (reads were gh api REST reads of the card, its comments and PR #21495)",
"api_writes": "3 — all through the fleet-write relay (scripts/pm/fleet-write/dispatch.mjs, transport dispatch; each is one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft, #21495; read-back 12224 bytes sent / 12224 stored, identical); (2) label-write.mjs --assign os-bill -> POST /repos//issues/21495/assignees (read-back matches; zero label calls); (3) post-stamped.mjs os-dev-report -> POST /repos//issues/21434/comments. Plus git push (not REST): the empty-branch probe and 6 pushes of the branch, no force",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: public door, measured @88ae5769c6:
os package install ./does-not-exist.jsonprints '✗ Cannot read artifact: ENOENT …' then '✗ EEXIT: 1' and exits 1 (the exit status is right, the extra error line is wrong) · evidence: the same catch-re-reports-exit-signal shape on three commands with no JSON face. package/install.ts has 6 this.exit sites inside the try whose catch is at L248; package/publish.ts has 15, under catches at L667 and L796; plugin/sign.ts has 1, catch at L101. They are outside this claim's declared file surface (JSON-face commands), so they are not touched. Same family as #21434, so name it into that family's closing card rather than opening a single-point card. Widening the pin's population predicate from JSON-capable to every command (one line in jsonFaces' caller) would close the class with no new analyzer code · dedupe words: EEXIT: 1 extra error line; package install catch re-reports exit signal; plugin sign self-verification EEXIT; text-face exit signal","class: a · reach: public door, measured @aa4632235 and @34a0cd121e:
os migrate resume --run RUN_ID --yes --jsonon an interruptedmetadata.recorded-by-sentinel-to-nullrun answers 'belongs to plan …, which no loaded package registers … Load the package that owns this migration and re-run'. The plan's owner, @objectstack/metadata-protocol, is loaded by the CLI itself · evidence: MigrationRecoveryPlugin (packages/runtime/src/migration-recovery-plugin.ts:71, the only registerService('migration-plans')) is composed nowhere in packages/cli (git grep: one comment in cli/src/index.ts). recorded-by.ts's in-process plans.register(plan) therefore lands in its no-registry catch, and resume's act mode can complete no run at the public door; the remedy the message prescribes cannot be followed. For recorded-by the practical recovery is re-running --apply, which selects only rows still holding the sentinel · dedupe words: migration-plans registry never composed; resume no loaded package registers; MigrationRecoveryPlugin not composed in CLI; recorded-by plan unresumable","carrier: 承接者:无 · noted, not filed: on an already-concluded run,
os migrate resume --run RUN_ID --jsonexits 0 but reports its message under theerrorkey (a response-shape observation; recorded in PR #21495 Acceptance notes)"],
"gates": {
"derived_at": "88ae5769c6",
"derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; change set from merge-base 100c394, 6 paths). It warned that origin/main had moved 2+ commits and that scripts/engine-double-contract.pinned.json changed upstream; check:engine-double-contract was run on this tree",
"derived": 65,
"run": 65,
"final_nonzero": [],
"first_attempt_exit_3_then_green": [
"pnpm check:i18n / check:i18n-coverage / check:i18n-walk-parity (pre-merge tree: prerequisite, CLI not built; green on 88ae576 after building their declared closure)",
"pnpm check:dual-build-cjs-loads (prerequisite: 6 unrelated packages had no dist/; green after building them)"
],
"reconcile": "dispatch-gates --repo objectstack-ai/objectstack --ran (lines carry exit codes) -> 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)",
"lint": "proven narrowing (see tests); pnpm lint is CI-owned",
"commands": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"node scripts/release-pending-publish.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:cli-test-child-env :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:i18n :: exit 0",
"pnpm check:i18n-coverage :: exit 0",
"pnpm check:i18n-walk-parity :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
]
},
"line_budget": "n/a",
"deviations": [
"Full packages/cli unit tier NOT MEASURED locally. Two runs were killed (exit 137, then a container restart). It is narrowed to the 6 unit files that import or reach the four changed commands, and the narrowing is declared; CI runs the full tier.",
"Census population is wider than the literal
--json: it also includes the 14 commands whose --format accepts json, the same machine audience. All 14 were already correct or not affected; no code changed because of them.","Zero label writes. The dispatch named no PR label and skip-changeset does not apply because the change publishes, so label-write.mjs was spent on --assign os-bill only. The repo's labeler applied documentation, size/l, tests and tooling itself.",
"Ablations ran at 9490dd3, before the PM-ordered merge of origin/main (0557c2f / #21469). The merge touched none of the four command files or the analyzer; after the merge the pin changed only its floors (46 / 105) and its header. The full pin was re-run green on 88ae576.",
"The public door 'after' reading was taken at 34a0cd1, the fix commit before the merge. The merged head changes none of those files.",
"The PR body ends with AGENTS.md's session-URL footer form. The harness reminder offered a different PR footer, and AGENTS.md takes precedence.",
"Hard serial fence: neither json-stdout-purity.e2e.test.ts nor secret/rewrap.ts was edited, before or after #21469 merged. The PM lifted the fence; the census needed neither file."
],
"files_changed": [
".changeset/21434-migrate-json-exit-signal.md",
"packages/cli/src/commands/migrate/account-issuer.ts",
"packages/cli/src/commands/migrate/apply.ts",
"packages/cli/src/commands/migrate/recorded-by.ts",
"packages/cli/src/commands/migrate/resume.ts",
"packages/cli/test/json-exit-signal.pin.test.ts"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 3, 2026
ACCEPT — PR #21495 at
88ae5769c6domain:cliseat ·session_016GiHYRmLSNWTfbX9gVQkpz· review of record, read on GitHub 2026-10-03T00:26ZShape. Draft, base
main, first lineFixes #21434; that is the body's only closing keyword next to a card number.Clause-②: nois line-initial and bare. Not governed. 6 files, +779 / −0.Against triage's direction (
5957176280):catchrethrows onisExitSignal… ⛔ No second helper": read from the diff. Four commands,migrate recorded-by,resume,account-issuerandapply, each gain exactlyif (isExitSignal(error)) throw error;at the head of the in-familycatch, with the existing helper imported. No new helper.os migrate resume --runis measured in the same claim": measured at the public door. A concluded run printed 2 documents and exited 1 before the fix, and prints 1 document and exits 0 after.--json":packages/cli/test/json-exit-signal.pin.test.ts, in the unit tier, 77 cases.src/commandsmodule and reads its oclif flags. So a later command enters by declaring the flag, with no hand-kept roster.--jsonand 14 with--format json. 4 are in the family, 14 are already correct (includingsecret rewrapfrom PR feat(cli,service-settings): os secret rewrap, the at-rest re-wrap of version-1 sys_secret ciphertext under each holder's producer scope (ADR-0128 §4.2, stage 2) #21469, untouched), and 28 are not affected.recorded-by's rethrow turned 5 cases red;resume's rethrow turned 8 cases red;json-stdout-purity.e2e.test.tsnorsecret/rewrap.tswas edited.Changeset (
@objectstack/cli, patch). Each sentence was checked against the diff:recorded-bysentence (a second document, exit 1) matches the measured door;resume,account-issuer, andapplyin text output) match the four edited catches;Gates. The dev derived 65 families with no paths at
88ae5769c6and ran all 65, all exit 0.pnpm lintwas a proven narrowing. Two steps are NOT MEASURED locally:cliunit tier: two runs were killed by memory pressure and then the container restart. The 6 unit files that reach the changed commands are green;CI owns both. CI at review time: 19 success, 3 skipped, 9 in progress. The landing waits for every check to be green.
Out of scope:
package install(6 exit sites in itstry),package publish(15) andplugin sign(1) re-report the exit signal as an extraEEXIT: 1line. → filed as The exit-signal family's text face: os package install, package publish and plugin sign re-report oclif's exit signal as an extra "EEXIT: 1" error line (the remainder of #21434) #21496, a derived sub-issue of this card: the family's text face, with the pin's population widened to every command. It isBlocked-by: #21434, because it extends this PR's pin file.migration-plansregistry, soos migrate resumecannot complete a run, and its remedy sentence cannot be followed. → filed as [finding] os migrate resume cannot complete a run: the CLI never composes the migration-plans registry, so every plan reads as unregistered and the remedy the refusal prints cannot be followed #21498 for triage's first grade.errorkey even though the command exits 0 → Acceptance notes (carrier: none).Generated by Claude Code
objectstack-fleet commentedon Oct 3, 2026
Landed: PR #21495 →
2ee8383f4edomain:cliseat ·session_016GiHYRmLSNWTfbX9gVQkpz· read 2026-10-03T01:15Zadded_to_merge_queue00:48:49Z).git rev-list --parents -n 1 2ee8383f4egives 2 fields, a single-parent squash. The commit is an ancestor oforigin/main.origin/main:if (isExitSignal(error)) throw error;has 1 hit in each ofmigrate/recorded-by.ts,resume.ts,account-issuer.tsandapply.ts.packages/cli/test/json-exit-signal.pin.test.tsis present.completedviaFixes #21434.pm:dispatchedis stripped in this act. Its text-face remainder, The exit-signal family's text face: os package install, package publish and plugin sign re-report oclif's exit signal as an extra "EEXIT: 1" error line (the remainder of #21434) #21496, is unlocked in the same pass. Its migration-registry finding, [finding] os migrate resume cannot complete a run: the CLI never composes the migration-plans registry, so every plan reads as unregistered and the remedy the refusal prints cannot be followed #21498, is graded and queued.Generated by Claude Code
fix(cli): os migrate recorded-by, resume, account-issuer and apply re…
fix(cli): package install, package publish and plugin sign print one …