Repository navigation
fix(cli): os init and os compile render each refusal once, not once on stdout and again as oclif's Error block (#21542) - #21560
Conversation
…n stdout and again as oclif's Error block Ten sites printed their sentence with printError and then handed it to this.error, which has oclif's entry point render it a second time on stderr. Each now ends in this.exit(2): the status this.error raised, with no second rendering. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…and keeps exit 2 A structural half over every command module (no refusal printer paired with this.error) and a driven half that spawns the CLI through all ten sites. The exit-signal pin's this.error anchor follows the sites to this.exit(2). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…exit-signal pin's comment Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
📓 Docs Drift CheckThis PR changes 1 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 27 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 85b81bd9a0477e469707e2124ecd242debc88f4a && git checkout 85b81bd9a0477e469707e2124ecd242debc88f4a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 10454b3afa94d49e6e424cc16fbbff3a898f3ad8 d83eb007b0e95dbdbb2519742a4ee8523e7ea9fc && git checkout -B drift-repro 10454b3afa94d49e6e424cc16fbbff3a898f3ad8 && git merge --no-ff d83eb007b0e95dbdbb2519742a4ee8523e7ea9fc
node scripts/docs-audit/affected-docs.mjs --json 10454b3afa94d49e6e424cc16fbbff3a898f3ad8
|
…no database yet with empty work (objectstack-ai#21552) (objectstack-ai#21570) Fixes objectstack-ai#21552 Clause-②: no On a project whose database does not exist yet, `os migrate account-issuer`, `os migrate audit-metadata-bodies`, `os migrate meta --stored`, `os secret orphans`, `os secret rewrap` and `os storage orphans` now answer with empty work and exit 0. Before, each exited 1 from its own first read of a table its read-only boot had deferred. This completes the family that PR objectstack-ai#21550 started for `resume`, `recorded-by` and `value-shapes`, under the same ruling (`5965283666`, applied to these six doors by triage's `5966537984`): prefer "not asked"; where a read cannot be avoided, recognise its refusal only with `isMissingTableError` and only for the command's own table. ## Measured at the public door Fixture: one artifact with two app objects, `--database-url file:` a path that does not exist, run as `node packages/cli/bin/run-dev.js ... --json`. Base `f83d0669d7` (origin/main when the branch was cut), head `491087ee0f` (the `src` of the six doors is the same at the current head `69a1689f00`; the patch round touched two test files only). `files-to-references`, `summary-nulls`, `multi-value-columns` and `duplicates` are the controls; `duplicates` has no `--json` flag and always prints its one document. | command (absent database) | base | head | |:--|:--|:--| | `migrate account-issuer` | exit 1, `RESOURCE_CONFLICT`, "Cannot enumerate sys_account" | exit 0, `scanned: 0, ok: true` | | `migrate audit-metadata-bodies` | exit 1, `failures: 3` (`sys_audit_log`, `sys_activity`, `sys_metadata_audit`) | exit 0, `failures: 0` | | `migrate meta --stored` | exit 1, `DATABASE_ERROR` for `sys_metadata` | exit 0, `scanned: 0, clean: true` | | `secret orphans` | exit 1, `scan_failed` for `sys_secret` | exit 0, `counts.total: 0`, all three families `enumerated` | | `secret rewrap` | exit 1, `scan_failed` for `sys_secret` | exit 0, `counts.total: 0`, all three families `enumerated` | | `storage orphans` | exit 1, `DATABASE_ERROR` for `sys_file` | exit 0, `filesScanned: 0, stranded: 0` | | the four controls | exit 0 | exit 0 | | database file left behind | none | none | Each of the six names, on stderr under `--json` and on stdout in human mode, the tables it read as no rows. ## The boot path of each door (what the seam can and cannot say) All six boot through `bootSchemaStack` with `deferSchemaDdl` and `readOnlyProbe`, so `SchemaStack.tableAbsent` is there to ask. Measured per door: - **Five ask it** (`audit-metadata-bodies`, `meta --stored`, `secret orphans`, `secret rewrap`, `storage orphans`) before the first read. A shared helper, `packages/cli/src/utils/absent-table-reads.ts`, is the one place a door asks. It adds no second mechanism: it wraps `tableAbsent`, and a refused read of an ordinary table is still issued and still refused. - **`account-issuer` cannot ask.** Its boot composes no auth plugin, so `sys_account` is not a registered object and the held-back sync never lists it: `tableAbsent('sys_account')` is false on every database. It is the `isMissingTableError` case the ruling names: the probe's read is not avoidable, and the door reads that one refusal, for `sys_account` only, as no rows. Registering the object would not be a fix: the probe selects the retired `issuer` column, which a registered `sys_account` no longer declares. - **`meta --stored`** hands its read to the metadata protocol, which holds a private engine this command cannot wrap. The preview answers an absent `sys_metadata` itself with the report the protocol returns for zero rows, typed as `StoredMigrationReport` so a new field is a compile error here. - **`secret orphans` and `secret rewrap`** read at driver level and through the reference union. The union is given a read-only view of the engine (`secretUnionReadView`): only `find` is carried onto the driver, so `--delete` and `--apply` take their write verbs from the unwrapped driver, and `listDatasourceDefs` stays absent when the engine has none (its absence is a declared gap). ## The table the boot cannot measure: `sys_activity` The control for `audit-metadata-bodies` showed `tableAbsent('sys_activity')` true on a booted database. `sys_activity` is rotation-managed: its rows live in `sys_activity__rYYYYMMDD` shard tables and its base name is a view. The deferred sync asks the driver `hasTable` for the base name, a view is not a table, so on SQLite it lists the base as `create_table` over a database that serves it. A door that believed it would have answered "Nothing to rewrite" over the cleartext credential copies the command exists to reach. So `absentTableReads` takes a schema lookup, and for a rotation-managed object it does not consult the measurement: the read is issued, and only its missing-table refusal (`isMissingTableError`, for that object) reads as no rows. The control pins it: a cleartext copy seeded into the rotation shard is found and counted (`sys_activity.rewritten: 1`). The seam itself is untouched here (see Out of scope). ## Documented exit (A3) - `migrate account-issuer`: its own description, "exits non-zero when the drop must not proceed". A database with no account table has no collision, and a booted database holding two clean accounts answers `ok: true` with exit 0. - `migrate audit-metadata-bodies`, `migrate meta --stored`: the platform checklist's migrate item (`docs/qa/platform-checklist/areas/cli.json`: "EVERY migrate subcommand with --json exits 0 on success"); `meta --stored` also documents "A second pass reporting every row canonical exits 0". - `secret orphans`, `secret rewrap`, `storage orphans`: report-only by their own documentation ("Report-only by default: without `--delete` it writes nothing", "A dry run by default", "Writes nothing"); their report mode exits 0 whenever the read succeeded. - Each is also the exit the same door gives the booted control (below). `content/docs/deployment/cli.mdx` said the opposite in two places: the Data migrations edge paragraph ("Another dry run that reads a missing table can still fail and exit 1") and `os secret orphans` ("it refuses and exits 1 ... `scan_failed`"). Both now describe the empty-work answer, and keep the refusal for any other read that fails. ## Tests - **Integration pin, extended:** `packages/cli/src/commands/migrate/data-commands.absent-database.integration.test.ts` gains a second block. For each of the six, on the absent database: `--json` exit 0 with the empty-work document, no refused read of its own tables, the table named on stderr; human mode exit 0 on the empty-work sentence; no database file created. The control is a booted database holding one row of work per door (two legacy `sys_account` rows, a cleartext audit copy in `sys_audit_log` and in the `sys_activity` shard, a stored metadata row, an unreferenced `sys_secret` row, a stranded `sys_file`): each door READS it, reports the row, and says nothing about absent tables. The four doors that already exited 0 are pinned to still do. Every spawn runs in `beforeAll`. - **Two refusal pins flipped:** `preview-read-only.integration.test.ts` pinned exit 1 for `meta --stored` and `audit-metadata-bodies` on a missing database (the refusal objectstack-ai#21391 declared). That is the behaviour the ruling reverses for these doors; both cases now assert exit 0 and the empty-work document, with the file header updated. - **Unit:** `packages/cli/src/utils/absent-table-reads.test.ts`, 17 cases: the helper's answers, the stderr/stdout split, and the rotation rule. - **Patch round 1: two stale test doubles.** CI Test Core (1/6) was red at `491087ee0f` in `rewrap.guards.test.ts`. That suite, and `orphans.guards.test.ts`, replace `bootSchemaStack` and hand the command a stack of only `kernel` and `shutdown`. The doors' first ask is `stack.tableAbsent`, a member of `SchemaStack` since PR objectstack-ai#21550, so every case fell into the `scan_failed` catch. Root cause read off the case's own output (a throwaway copy of the suite that printed the payload): `{"error":"scan_failed","message":"stack.tableAbsent is not a function"}`, in all five cases of `rewrap.guards.test.ts`. Run locally against the unfixed head, the two suites had 7 red cases (5 in `rewrap.guards`, 2 in `orphans.guards`); CI listed four. The fix is on the double's side: both doubles now carry `tableAbsent` as the real boot returns it (`false` for every table on the plain boot of a writing run; `rewrap.guards` takes the measured-absent set as an option). No consumer-side tolerance was added. `rewrap.guards.test.ts` also gains one case: a dry run over tables the boot measured absent issues no read, reports `counts.total: 0` with every family `enumerated`, and a present-table control still reads. The stack double in `test/exit-signal.pin.test.ts` (`stackWith`) has no `tableAbsent` either, and no path that pin drives reaches it (`recorded-by --apply` and `resume --run` short-circuit before the ask, and `account-issuer` does not use the seam), so it is left as it is. - **Test files that mock `bootSchemaStack` or `schema-migrate.js`** (`git grep` for `vi.mock` of the module over `packages/cli`): `rewrap.guards`, `orphans.guards`, `test/exit-signal.pin`, `files-to-references.column-step-refusal` and `summary-nulls`. The last two reach other commands, not these doors. The test files that reach a door through any other seam (a real boot, the command module, or a source scan) were found by `git grep` for the door names and modules: `data-commands.absent-database`, `preview-read-only`, `meta.stored-flow-resolution`, `meta.report-order`, `meta.stored-flags`, `orphans.driver-contract`, `rewrap.driver-contract`, `platform-migrations-arming`, `schema-migrate.one-shot-family`, `resume.recorded-by`, `sys-secret-rewrap`, `one-shot-settings.pin`, `migrate-meta-engine-guidance`, `migrate-meta-strict-factories`. - At `69a1689f00` (this branch merged with origin/main at `491087ee0f`, which brought PR objectstack-ai#21560's refusal pins; no later merge, the gate derivation does not call the tree stale): - The 13 files above that run in the package's two projects (the five mockers and the door-reaching files, `summary-nulls` and `files-to-references.column-step-refusal` among them), `vitest run --maxWorkers=2`, both projects: 13 files, 202 tests passed (it was 7 failed, 194 passed before the double fix). 7 of the 13 are integration-tier. - The round-1 integration files (`data-commands.absent-database`, `preview-read-only`, `meta.stored-flow-resolution`, `platform-migrations-arming`, `schema-migrate.one-shot-family`, `resume.recorded-by`), `--project integration`: 6 files, 137 passed, 1 skipped (the live PostgreSQL cell, not provisioned here). - `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2`: 253 files, 3702 passed (after `pnpm --filter @objectstack/cli build`, which the two `published-subpath-*` pins need). - `pnpm --filter @objectstack/cli build` and `pnpm --filter @objectstack/cli typecheck` (test layer included): exit 0. - **NOT MEASURED: the other 69 files of the cli integration tier** (82 files listed by `vitest list --project integration`; 13 run here: those 7 and the six round-1 files). Reason: the tier runs past the 10-minute foreground cap here, so the rest is declared to CI. The narrowing is the `git grep` above: none of the 69 names a door, mocks `schema-migrate.js`, or imports the new helper. And the two `*.e2e.test.ts` files that spawn the doors (`test/migrate-meta.e2e.test.ts`, `test/json-stdout-purity.e2e.test.ts`) belong to neither of the package's two vitest projects, so no local run selects them (the runner printed "matches no test file in this package"). ## Reverse verification Each leg mutated the committed tree through `scripts/ablation-replace.mjs`: anchor 1 to 0, blob changed, restored to `HEAD` with an empty `git diff HEAD` and a clean status. The CLI spawns run `packages/cli/src` through tsx, so there is no dist hop. Two first attempts were refused by the tool itself because the replacement text contained the anchor or already occurred in the file; nothing ran, and each leg was redone with a distinct replacement. - **Leg A, `tableAbsent` forced false** (`schema-migrate.ts`). Predicted: every fresh-project pin of the five seam doors red, the booted controls green, `account-issuer` green (it does not use the seam). Observed: 19 failed, 25 passed, 1 skipped. The 19 are the 7 pins PR objectstack-ai#21550 added, the 10 new fresh-project cases (five doors, `--json` and human) and the two flipped `preview-read-only` cases. Every booted control and the four exit-0 controls stayed green, and `account-issuer` stayed green. - **Leg B, the `isMissingTableError` branch of `account-issuer` disabled.** Predicted: the two `account-issuer` fresh-project cases red, nothing else. Observed: 2 failed, 34 passed. - **Leg C, the rotation rule disabled** (`absent-table-reads.ts`). Predicted: the audit control red because `sys_activity` is skipped. Observed: integration 1 failed, 35 passed, with `sys_activity.scanned` 0 where a cleartext copy is stored; the unit file 2 failed, 15 passed. - **Leg D, patch round: the not-asked answer disabled** (`absent-table-reads.ts`, `absent()` always false), over `rewrap.guards.test.ts`. Predicted: only the new absent-tables case red. Observed: 1 failed, 6 passed. Restored to `HEAD`, empty `git diff HEAD`. Legs A to C ran at `491087ee0f`; `src` of the doors and the helper is unchanged since (the round touched two test files). ## Gates At `69a1689f00`, after the final commit: - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 94 commands (the same list as at `491087ee0f`); `--ran`, with an exit code per command, reconciled as "94 run, 0 NOT-MEASURED (a DERIVED zero)". All 94 read exit 0 on this pass, with every package built first (`turbo run build --filter='!@objectstack/docs'`). The derivation says no commit it can see touched what it derives from, so no merge was made. - **`pnpm lint`, as a proven narrowing.** `eslint --no-inline-config --format json` on the 12 changed TS files: 12 files, 0 errors, 0 warnings, none reported as ignored. The population comes from `eslint.config.mjs` (`files: **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`); the config enables no type-aware linting (no `parserOptions.project`, and every block's `parserOptions` is `{ ecmaVersion, sourceType }`), so this diff cannot move the verdict on a file it does not touch. The `.md` and `.mdx` files are outside it. ## Acceptance notes - **Out of the card, kept:** every read-only data-command boot of an absent database still prints `[ObjectQLPlugin] sys_metadata_activation is registered but could not be read`. Triage ruled it out of this card. - **A new file outside the claim's list:** `packages/cli/src/utils/absent-table-reads.ts` (and its unit test). Five doors need the same view, the same notice line and the same rotation rule; five inline copies would drift. `schema-migrate.ts` is untouched. - **A new `this.exit` or `this.error`:** none. The exit-signal pin and PR objectstack-ai#21560's refusal pin both ran green over the whole command table. - **Round 1 of the patch:** `rewrap.guards.test.ts` and `orphans.guards.test.ts` are the two files added to the diff (see Tests). - **`value-shapes` (PR objectstack-ai#21550)** reads through the same seam without the rotation rule. It composes no audit plugin, so none of its scanned objects is rotation-managed today; noted, not changed. ## Out of scope (reported to the seat, not filed here) - `SchemaStack.tableAbsent` answers true for a rotation-managed object's base name on a booted SQLite database, because `previewDeferredSchemaWork` asks `hasTable` and a view is not a table. Measured through `os migrate audit-metadata-bodies` against a booted database whose `sys_activity` is the view over `sys_activity__rYYYYMMDD`. The same list is what `os migrate plan` prints for a host that composes the audit plugin (not measured at `plan`). The fix belongs in the driver's preview, not in a consumer. --- _Generated by [Claude Code](https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21542
Clause-②: no
os initandos compileprinted ten refusals twice: once as the command's own✗line on stdout, and again as oclif'sError:block on stderr, because the command handed the same sentence tothis.error. Each now prints its✗line and the hint under it once, and ends inthis.exit(2), the statusthis.errorraised. Stdout is unchanged; stderr no longer repeats it; every exit status is still 2.The shape, and why it is this one
Triage's ruling asks for one shape at every site, through the split
isReportedErrorguards, with no second mechanism and no per-site variant. The shape the family's text faces already use is: the command renders its own refusal withprintError, and ends inthis.exit(n).validate,info,diff,lint,verify,i18n check,i18n extract,generateandmigrate metaall end that way, andisReportedErroris the guard a catch-all uses for a refusal a helper already wrote to stderr. This PR applies that shape at all ten sites. No helper is added, andutils/format.tsis read, not edited.printErrorToStderr's docblock says a command that has already decided it renders the text face keepsprintErroron stdout, and stderr is for shared paths that cannot see--json(resolveConfigPath).os initdeclares no--jsonat all, andos compile's--jsonbranch returns above every site here. So the surviving copy stays where the✗line and its hint always were. Stdout already held everything stderr carried, so nothing is lost.init.ts's catch-all has noisReportedErrorguard.compile.ts's catch-all already carries it, becauseresolveConfigPath()can reach it. OnlyConfigRefusalError(config.ts) and the SDUI manifest error (sdui-manifest.ts) carry the marker, and nothing ininit.ts'stryreaches either: it imports neither, andvalidateScaffoldhas its own loader. A guard there would be a branch no run can take.this.error(msg)exits 2 andthis.exit(2)is the same status. The dispatch's A4 and the ruling's second pin both say the statuses stay, and the platform checklist already names oclif's 2 onos compile's human path as legitimate.Population: a symbol walk, not a grep
ts-morph over
packages/cli/src/commandsatfd5a1cd597with the type checker: calls resolving to oclif'sCommand.error, and calls resolving toprintErrororprintErrorToStderrinutils/format.ts, grouped by outermost function.this.errorcall sites in 5 files; 57 command files call a refusal printer. Functions holding both: 2,init.ts run()andcompile.ts run().this.errorsites: the card's 8, plus init's scaffold self-test and dependency install refusals inside itstry. The ruling's third bullet makes any pair beyond the 8 ride this landing, and they do.datasourcecommands (introspect,list-tables,validate) hold the other 6this.errorsites and call no refusal printer. Their detail lines go throughthis.logand the one sentence is oclif's, so they are not pairs.this.errorsites in 3 files.Readings at the public door
Spawned
bin/run-dev.js(the source entry; it shares the published entry's oclifhandle()andflush()) from a scratch directory, before and after. Before, the source entry prints oclif's stack beneath the same sentence; the published entry prints› Error: …, as the card measured it.All ten sites, each reached through a real run (fixtures are in the e2e pin's header). Copies of the sentence across both streams, and the exit status:
init demo -t bogusinit Bad_Nameinit demoover a non-emptydemo/initin a directory namedBad Cwdinitbeside anobjectstack.config.tsinit demo -p npm, stubnpmthat installs nothinginit demo -p npm, stubnpmexiting 1init --no-installwith a file namedsrccompile --runtime-bundle, config importing./helperthat onlyhelper.jsxsatisfiescompile, config throwing at loados buildextendsCompileand has no refusal of its own, so it inherits both compile rows. The publishedbin/run.js, built from this tree, was run for the init unknown-template and compile catch-all rows too: exit 2, stdout once, stderr empty.Pins
packages/cli/test/refusal-renders-once.e2e.test.ts(nightly): spawns the CLI through each of the ten sites and asserts the exit status is 2, the refusal's subject occurs once across both streams, exactly one✗line is printed, and the hint under it survives. Every spawn is paid inbeforeAll; no case is clocked.packages/cli/test/refusal-renders-once.test.ts(queue, unit): the structural half over every command module. No function that calls a refusal printer (an identifier bound by an import fromutils/format.js, aliases followed) also callsthis.error. Nine fixtures pin the scan, and three floors keep it from going vacuous (60 command modules, 50 files calling a printer, 3 files raisingthis.error), set under what the symbol walk counted on this tree: 65, 57 and 3. The population is discovered, so the next command that repeats the shape is in it.packages/cli/test/exit-signal.pin.test.ts: itsthis.errorreal-site anchor followed the sites tothis.exit(2)(init's two in-try refusals, compile's and build's bundling refusal).SITE_FLOORstays 131 (both spellings are seeds), and the pin stays green; thethis.errorseed itself is still covered by its fixtures.Reverse verification
Run on the committed fix, through
scripts/ablation-replace.mjsin wrap mode under the lock, restoring one site's pairing at a time. Both halves read the source (bin/run-dev.jsruns fromsrc/, the structural pin reads text), so nodist/is on the path and no rebuild leg applies.init.tsunknown-template site back tothis.error(...): anchor 1 to 0, blob929642fba1fftoedab7c113e65. Structural pin red (1 failed, naminginit.ts run()); e2e red on exactly that case (1 failed, 9 passed: "expected 2 to be 1"); exit-signal pin green. Restore proven: blob equals HEAD (929642fba1ff) andgit diff HEADis empty.compile.tsruntime-bundle site back tothis.error(err.message): anchor 1 to 0, blob25d3ea870831to42e44c372e8e. Structural pin red (2 failed: the pair, and the exit-signal anchor "each ends inthis.exit(2)"); e2e red on exactly the bundling case (1 failed, 9 passed). Restore proven: blob equals HEAD (25d3ea870831),git diff HEADempty,git statusclean.Verification
At
d83eb007b0unless noted; every heavy run went throughos-verify-lock.sh.pnpm --filter @objectstack/cli typecheck: exit 0 (tsc --noEmit, andcheck:test-typecheckOK with its ledger unchanged: 3 files, 28 errors).refusal-renders-once.test.ts,exit-signal.pin.test.tsandvitest-tiers-partition.test.ts: 3 files, 142 tests passed. Driven pin withOS_TEST_TIERS=nightly: 10 of 10 passed.os init,os compileoros build, selected by import of the command module or by spawn argv (run at228259e373; the later commit changes one comment line in a test file): 31 queue-tier files, 485 passed and 6 failed, all 6 inpublished-subpath-console.pin.test.ts. Those six failed because this worktree'spackages/cli/disthad been built withOS_SKIP_DTS=1and carried no declarations; after a declaration build the file passed 14 of 14. 18 nightly e2e files withOS_TEST_TIERS=nightly: 210 passed.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths, atd83eb007b0: 66 derived, all 66 run. Three first answered exit 3 or 124 for environment reasons (check:dual-build-cjs-loadsandcheck:i18n-coveragesaid PREREQUISITE NOT MET because packages outside the cli closure had nodist/;check:type-check-debthit my own 420 s cap). After building the missing packages with declarations and a longer cap, all three exited 0.--ran: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN.isPathIgnoredandcalculateConfigForFile): 5 files in the JSON results, 0 errors, 0 warnings. The.changesetfile is outside ESLint's population. Type-aware linting is off for each file (noparserOptions.project; the config header says it is never enabled), and the diff touches no ESLint config, so it cannot move a verdict on an untouched file. Whole-repopnpm lintis left to CI.fd5a1cd597andorigin/mainis 7 commits ahead. The derivation tool reports one input changed upstream,scripts/engine-double-contract.pinned.json(thecheck:engine-double-contractfamily, which ran green here; this diff adds no fake engine). The upstream commits touchpackages/clionly undermigrate/and two utils, and add noprintErrororthis.errorline undersrc/commands.Acceptance notes
utils/format.tsaboveCliExitCodereads "The only two exit codes this CLI has: 0 success, 1 failure". Ten refusals exit 2, as they always did, and the platform checklist (docs/qa/platform-checklist/areas/cli.json) blesses oclif's 2 onos compile's human path. This PR keeps 2, as the ruling's pin says. Noted, not filed: the docblock's scope is theemitJsonslot's type.compile.ts'sthis.error()in seven strings. The status it blesses is unchanged; only the named mechanism now reads stale. Not edited here (outside this card's file surface, and the file carries a revision history of its own).os compile's catch-all still answers exit 1 for a refusal a helper already wrote (theisReportedErrorbranch) and exit 2 for any other. That asymmetry predates this PR and is kept.Generated by Claude Code