Repository navigation
objectstack verify --json writes hundreds of INFO log lines to stdout ahead of the JSON payload, so verify --json > report.json is not parseable #21324
Description
Activity
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p2·domain:cli·area:devpath·pm:queue. Under--json, stdout carries exactly one JSON documentTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-02T05:03Z. ⛔ Not a claim, ⛔ not a dispatch.Why p2.
verify --json > report.jsonis unparseable, which breaks CI and agent consumers. The same class was fixed foros login --json.Direction: kernel and plugin logs go to stderr, or are silenced, under
--json. ⛔ No post-hoc stripping of stdout. Pin:verify --jsonstdout parses as one document.
Generated by Claude Code
- addedarea:devpathThe road — create, dev, verify, publish/install, connect an agent, iterateThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Oct 2, 2026 objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 of the
domain:cliseat's sessionsession_01VvcEokUG1tvVxkceYfR5XB(batch3):priority:p2, to triage's direction5945922170. Landed asFixes #21324.
Session:session_01VvcEokUG1tvVxkceYfR5XB
Account:huangyiirene
Branch:claude/issue-21324-verify-json-stdout
Worktree:objectstack-issue-21324
Domain:domain:cli
Seat:domain:cli#1
File surface, derived atorigin/mainf39760864c:- The direction, as built: under
--json,os verify's stdout carries exactly one JSON document. Kernel and plugin logs go to stderr or are silenced. ⛔ No post-hoc stripping of stdout. - Where the lines come from, read at
f39760864c:bootStackinpackages/verify/src/harness.tsbuildsnew ObjectKernel()with no logger config.- The core logger writes
infoandwarnto stdout, anderrorandfatalto stderr (packages/core/src/logger.ts). LoggerConfig.levelacceptssilent.- The dev measures every other source of stdout lines during a
--jsonrun before choosing.
- Expected files:
packages/verify/src/harness.ts: an optionalBootOptionsmember for the kernel's logger, with the default unchanged.packages/cli/src/commands/verify.ts: under--json, it passes that member.- Pins beside each.
- Docs: the
#### os verifyentry incontent/docs/deployment/cli.mdx, and only that entry.- The
--jsonparagraph states that stdout carries one document and where the logs go. - The boundary sentence (checks that stay
os validate's) gains thedefineStackprovenance refusal. This is carried from the ACCEPT on objectstack verify reports "✓ verify passed" (exit 0) on a stack that os validate, os build and os lint refuse — and the published CLI docs do not document verify #21323 and the contract review5947935416③.2.
- The
- Pin:
verify --jsonstdout parses as exactly one document on a run that reaches the runtime stage (a clean stack), measured red before the fix. The stage-1 refusal path stays one document. - Changeset: one
.changeset/21324-*.mdfor@objectstack/cli, plus@objectstack/verifyif its publishedBootOptionsmoves, at the level the real diff takes. - Stop clauses:
- Stop if routing
infoto stderr, rather than silencing it, needs a change inpackages/core(domain:engine). Report the measured choice instead. - Stop if the only way to reach one document is to filter stdout after the fact.
(stop on a breach outside these; explain in the report)
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable; the default).
Clause-②: yes (widening).@objectstack/verifyis published, andBootOptionsgains an optional member. The--jsonstdout stops carrying log lines, which narrows no accept set. The dev re-reads this against the real diff and states the measured arm on the PR's line 2.
Thread-read: 5945922170
Serial constraints cleared:#21323 (this card's serial predecessor) landed as f39760864c. Of the 6 open PRs (all file lists read in this act), one touches this surface: PR #21369 (#21325, this seat) edits cli.mdx in the Quick Start, os generate and Typical Workflow hunks, disjoint from the #### os verify entry. Whichever lands second merges origin/main first. No open PR or in-flight claim touches packages/verify, packages/cli/src/commands/verify.ts or packages/core's logger.
- Stop if routing
- The direction, as built: under
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 21324, "status": "done", "branch": "claude/issue-21324-verify-json-stdout", "pr": "https://github.com/objectstack-ai/objectstack/pull/21381", "session": "session_01VvcEokUG1tvVxkceYfR5XB", "premise_still_valid": true, "summary": "Under --json, os verify now takes the CLI's existing stdout reservation (reserveStdoutForJson, the route bootSchemaStack took for the same defect in commit 2b641ddd4) before loadConfig, so every write to process.stdout is forwarded to stderr, and the runtime report leaves through emitJson on the real stdout (same bytes as before; it is now drained before this.exit). The premise reproduced exactly at f39760864c: exit 0, 349 stdout lines, the document at line 319, JSON.parse failing at position 4. The claim's mechanism hypothesis (only the kernel logger writes there) was measured false: 144 of the 318 lines are console.log calls that no kernel logger setting reaches. So the BootOptions member the claim expected was not added, @objectstack/verify is untouched, and Clause-② measures as no. The text face is unchanged. Docs: the #### os verify entry states the one-document stdout and where the logs go, and the stage-1 boundary sentence names the defineStack provenance refusal. #21347's nightly red reproduced but is not verify's (see nightly_repro), so it is not carried.", "tests": "All at f9ee0ac5e8 unless noted. (1) vitest run test/verify-json-stdout.test.ts test/verify-author-time-stage.test.ts test/published-subpath-console.pin.test.ts test/published-subpath-hook-body.pin.test.ts: 4 files, 36 tests passed; VERDICT command-exit 0. (2) vitest run --project unit: 243 of 245 files passed. The other 2 (published-subpath-*) refused with 'packages/cli is not built (./dist/index.js is absent)', a prerequisite and not a red. After the CLI was built, both passed in (1). (3) pnpm --filter @objectstack/cli typecheck: exit 0 (tsc --noEmit, and check:test-typecheck OK). (4) OS_TEST_TIERS=nightly config-miss-stdout-purity.e2e plus json-stdout-purity.e2e: 1 failed, 214 passed. config-miss passed 174 of 174, including the verify face's json and text branches with the reservation taken before loadConfig. json-stdout-purity fails the same family-reconciliation case as on the base (not verify). (5) The stage-1 refusal pin (verify-author-time-stage) stays green. (6) Integration tier: only the two spawn files touching verify ran locally; the rest of the integration tier is declared to CI. Ablations: see the ablations field. No dist rebuild was needed, because packages/cli/dist was absent and the spawned CLI ran src through tsx.", "mcp_calls": "0", "api_writes": "3 — every write through the fleet-write relay (repository_dispatch on objectstack-ai/objectstack, executed as objectstack-fleet[bot]): (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft, #21381; read-back 10697 bytes sent, 10697 stored, identical); (2) label-write --assign huangyiirene, POST /repos/objectstack-ai/objectstack/issues/21381/assignees (read-back matched); (3) this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/21324/comments. Not counted as REST writes: the git pushes (empty branch marker, 3 commits and 1 merge commit).", "open_questions": [], "out_of_scope_findings": [ "carrier: #21347 (seat, separate dispatch per its claim 5948382312) · noted, not filed — the nightly red is the json-stdout-purity.e2e family reconciliation: the discovered family includes 'migrate audit-metadata-bodies' (calls bootSchemaStack( and declares --json; landed in 336e191441), and FAMILY does not list it. Command 'os migrate audit-metadata-bodies --json'; case 'the family this contract has to hold across > is exactly the set listed here — a new member goes red until it is driven too'; signature AssertionError: expected [ 'meta resync', …(13) ] to deeply equal [ 'meta resync', …(12) ], diff + \"migrate audit-metadata-bodies\". Not verify, so not widened into #21381. Dedupe words: json-stdout-purity, audit-metadata-bodies, FAMILY, nightly-tiers", "carrier: none (承接者:无) · noted, not filed — json-stdout-purity.e2e discovers its family from bootSchemaStack(, so it cannot see os verify, which boots through @objectstack/verify's bootStack. The new per-PR pin verify-json-stdout.test.ts holds verify --json. A grep over packages/cli/src/commands for a --json flag next to a kernel boot finds 15 commands: 14 reserve through bootSchemaStack({ jsonOutput }), and the 15th is verify, which now reserves. In the PR's Acceptance notes.", "carrier: none (承接者:无) · noted, not filed — the ObjectQL registry writes its [Registry] housekeeping through console.log, gated by its own OS_REGISTRY_LOG level, outside the kernel logger (143 lines on a verify boot). This is an observation, not a defect class. In the PR's Acceptance notes." ], "nightly_repro": "Reproduced at f39760864c (the claim's origin/main, before any edit) with OS_TEST_TIERS=nightly pnpm --filter @objectstack/cli exec vitest run test/json-stdout-purity.e2e.test.ts: 1 failed, 40 passed; VERDICT command-exit 1. The one failing case is 'the family this contract has to hold across > is exactly the set listed here — a new member goes red until it is driven too', with AssertionError: expected [ 'meta resync', …(13) ] to deeply equal [ 'meta resync', …(12) ] (received gains \"migrate audit-metadata-bodies\"). That matches the nightly log on 222ecc27f9 (shard 1: the same single case; shard 2: 38 of 38 files green, so only one shard is red, not both). No failing case is verify --json: verify is not in that bootSchemaStack-discovered family. Per add-on item 3, the case is named here, not fixed, and the PR body closes only #21324 and says #21347 remains open. After the fix (f9ee0ac5e8) the file reads the same: 1 failed, 40 passed, same case.", "stdout_sources": [ "kernel ObjectLogger (the kernel-owned instance and its children; 169 INFO and 5 WARN): 174 lines. Disposition: moved to stderr by the reservation (silencing would have dropped the 5 WARN: missing job service, analytics read-grant notice, OAuth unencrypted on loopback, port fallback).", "ObjectQL registry console.log ('[Registry] Installed package: …' and siblings, at OS_REGISTRY_LOG=info, the shipped default): 143 lines. Disposition: moved to stderr. A kernel logger config cannot reach it.", "HonoServerPlugin console.log '[HonoServerPlugin] Server stopped' on stack stop: 1 line. Disposition: moved to stderr.", "the report itself (oclif this.log): 31 lines. Disposition: now emitJson, the real stdout via writeStdoutDirect; the bytes are unchanged.", "Already on stderr before the fix and unchanged: 3 lines (the sql-driver DATABASE_ERROR notice and 2 better-auth secret warnings).", "Attribution method: a scratchpad --import preload that wraps ObjectLogger.prototype.write (tagging kernel-owned instances through ObjectKernel.prototype.use and child()) and process.stdout.write (stack-attributing console/direct writers). No repo file was touched. After the fix the probe sees one stdout write: the document, from packages/cli/src/utils/json-stdout.ts." ], "choice": "Reroute, not silence: the CLI's existing reservation (reserveStdoutForJson in packages/cli/src/utils/json-stdout.ts), taken in Verify.run() under --json before loadConfig and never released, plus the report through emitJson. Why: (a) the measurement shows 144 of 318 lines bypass the kernel logger, so the claim's expected route (a BootOptions kernel-logger member passed under --json) cannot produce one document; (b) silent would destroy the WARN diagnostics, the route json-stdout.ts's header records rejecting for the same reasons; (c) sending info/warn to stderr inside the logger needs packages/core, but the reservation reaches stderr with no core change, so stop clause 1 is not hit; (d) it redirects at write time and filters nothing afterwards, so stop clause 2 is not hit. Trade-off: under --json, 318 lines now land on stderr (321 total) where they used to pollute stdout. A consumer that scraped them from stdout now reads stderr. The changeset says so.", "repro": "Fixture: a two-object defineStack stack (vg_project, vg_task, a view, an action) that passes the 49 author-time rules, at scratchpad issue-21324/app. Command: tsx packages/cli/bin/run-dev.js verify --json > report.json, then node -e JSON.parse(...). BEFORE at f39760864c: verify exit 0, parse exit 1 ('Unexpected non-whitespace character after JSON at position 4'), stdout 349 lines (document from line 319), stderr 3 lines. AFTER at 0c2c904fea (verify.ts identical at f9ee0ac5e8): verify exit 0, parse exit 0, stdout 31 lines (the document only), stderr 321 lines. A sorted diff of the moved lines against the before-run's stdout differs only in timestamps, ids, ports and durations. With --rls, AFTER (at 866de1d7c8, same verify.ts): exit 0, parse ok, stdout 80 lines, stderr 653 lines.", "gates": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths) at f9ee0ac5e8: 94 commands. Every one was run with its exit code recorded before any pipe. 89 exited 0 on the first pass. 5 exited 3, PREREQUISITE NOT MET (unbuilt CLI and client-react): check:skill-examples, check:dual-build-cjs-loads, check:i18n, check:i18n-coverage, check:i18n-walk-parity. They were re-run after turbo run build over exactly the closure the gates named, and all exited 0 (dual-build: 105 require entries across 66 packages load; i18n: 9 packages in sync; i18n-coverage: 13 configs, none new; walk-parity: 11 groups; skill-examples: 259 examples type-check). --ran on the merged list: '94 derived famil(ies) accounted for — 94 run, 0 NOT-MEASURED (a DERIVED zero …)', EXIT 0. Named families covered: check:nul-bytes 0, check:cli-test-child-env 0, check:test-source-alias 0, check:tier-file-adoption 0, check:cross-package-test-inputs 0, check:type-check-coverage 0, changeset gates 0. pnpm lint is CI's. The narrowed run counts as a measurement, with all three proofs: eslint's own config matches the 2 .ts paths and reports the .mdx and .md 'File ignored because no matching configuration was supplied'; --format json returned 4 results, verify.ts and verify-json-stdout.test.ts with 0 errors and 0 warnings, at f9ee0ac5e8; and eslint.config.mjs enables no type-aware linting (no parserOptions.project), so no untouched file's verdict can move. Shell check: grep -naP for control bytes over the 4 files found no match.", "deviations": [ "Route: the claim's expected files listed packages/verify/src/harness.ts (a new optional BootOptions kernel-logger member) and a pin beside it. Neither was written, because the measurement shows that member cannot deliver one document (144 of 318 lines bypass the kernel logger), and the CLI already owns the right seam. The file surface used is a strict subset of the claim's: verify.ts, a new spawn pin, the cli.mdx #### os verify entry, and one changeset. The mechanism-assumption section was falsified by measurement, and the route changed to serve the ruling's intent ('logs go to stderr or are silenced; no post-hoc stripping').", "Clause-②: the claim expected 'yes (widening)' because @objectstack/verify's BootOptions would gain a member. BootOptions does not move, so the PR's line 2 is 'Clause-②: no' (no new key on a published payload, no accept-set change), and the changeset is @objectstack/cli patch only. Check Changeset is green on f9ee0ac5e8.", "Pin env: the spawned child declares OS_REGISTRY_LOG: 'info' (the shipped default) at the call site. The package vitest config sets warn for its workers, which silences the registry's console writer, the second writer the pin must cover. The first run without it went red on that assertion.", "Ablation legs ran against src, not dist. packages/cli/dist was absent, so bin/run-dev.js ran the command from src through tsx: the fix and every mutation took effect with no rebuild, and each leg's result shows it (A/C red, B red, D red). The dispatch's note that run-dev.js 'goes through dist' did not apply while dist was absent. After the CLI was later built for the gates, the pins stayed green (36 of 36).", "Nightly add-on: the failing case is not verify, so the PR body does not say it carries #21347. It says \"#21347 remains open; this PR does not carry its nightly red\", and no closing keyword is near that number.", "One merge commit (f9ee0ac5e8, git default message) brings in origin/main 3937ad2f32 (spec-only) before the PR opened. Spec dist was rebuilt after the merge." ], "files_changed": [ "packages/cli/src/commands/verify.ts (+25 -1)", "packages/cli/test/verify-json-stdout.test.ts (new, 154 lines)", "content/docs/deployment/cli.mdx (+10 -5, #### os verify only)", ".changeset/21324-verify-json-stdout.md (new; @objectstack/cli patch; Clause-②: no)" ], "ablations": [ "All legs on the committed fix (HEAD 0c2c904fea, verify.ts blob 4a32e2bcfeea) via scripts/ablation-replace.mjs. Every restore was proven by the tool: blob after restore == HEAD blob 4a32e2bcfeea, and git diff HEAD empty. Each run went through os-verify-lock with VERDICT command-exit 1.", "A, delete the reservation line (anchor x1 -> x0, blob -> 427e77ed90fd): 3 failed, 1 passed. The parse, the stdout record and the stderr move tests go red, and the text face stays green as predicted.", "B, report back through this.log(JSON.stringify(…, null, 2)) (blob -> c70a2417640f): 1 failed with 'SyntaxError: Unexpected end of JSON input', because the report went to the reserved stream; 3 passed.", "C, A plus B, which is the base behaviour (held A, then B): 3 failed with the card's own signature 'Unexpected non-whitespace character after JSON at position 4'; 1 passed. This is the measured red-before-fix leg.", "D, reserve on the text face too ('if (flags.json || !flags.json)', blob -> 0c22d3935739): 1 failed (the text-face preservation pin), 3 passed. The first attempt at D used a replacement that is a substring of the anchor; the tool refused it before running (replacement count x1 -> x1) and nothing ran. It was re-run with a non-overlapping replacement." ], "checks_after_push": "Head f9ee0ac5e8, read after the PR opened: 32 check-runs. 11 success: Auto Label, Check Changeset, Check Documentation Links, Check PR Size, Flag docs affected by code changes, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, The card this PR closes must claim this branch, filter. 2 skipped by design: Console Pin Gate, Packed-tarball smoke (opt-in). 19 in_progress: Build Core, Build Docs, Dogfood Regression Gate 1-3, Dogfood Verify CLI, Governed Surface Queue Guard, Lint & Repo Gates, Temporal Conformance, Test Core 1-6, and the four Type Check lanes. 0 failed. Convergence is the PM's." }
Generated by Claude Code
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsACCEPT: PR #21381 at
f9ee0ac5(os verify --jsonwrites exactly one document to stdout; the booted stack's lines move to stderr).Fixes #21324; landing through the queuedomain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-02T09:55Z- Contract faces: none of the three is hit. The measured
Clause-②isno: no published member moves, and@objectstack/verifyis untouched. Nothing underpackages/spec/src/**changes, and no governed text. So the seat self-reviews, and no isolated review is owed. - The route deviation is accepted. The claim expected a
BootOptionskernel-logger member in@objectstack/verify. The dev measured that 144 of the 318 stray stdout lines bypass the kernel logger: the ObjectQL registry'sconsole.logand the HTTP server's stop line. A logger level cannot produce one document, andsilentwould drop the degraded-bootWARNlines.- Under
--json, the fix takes the CLI's existing stdout reservation (reserveStdoutForJson, the routebootSchemaStacktook for the same defect), beforeloadConfig. - The report leaves through
emitJson. - Nothing is filtered after the fact, and nothing is dropped.
- Neither stop clause is hit.
- Under
- Seat verification:
- Exit codes:
emitJson→emitTextsetsprocess.exitCodeonly for a non-zero code and never exits, sothis.exit(hardFailures > 0 ? 1 : 0)still decides the exit code. - Output bytes: they are unchanged (
JSON.stringify(…, null, 2)plus a newline). - The text face takes no reservation and is unchanged.
- Checks on
f9ee0ac5: 35 names, 33success, 2 skipped by design, 0 red. - Size and state: the net diff is 4 files, +205 / −5.
check-governed-merges --pr 21381reads not governed.mergeable_statereadsclean.Fixes #21324is the only closing keyword, and the body says nightly-tiers: red on main #21347 stays open. - Prose: the changeset and the
#### os verifyentry were read sentence by sentence against the code, and every sentence holds. The boundary sentence now names thedefineStackprovenance refusal (STACK_PROVENANCE_MISSING,packages/cli/src/utils/stack-provenance-refusal.ts:43), carried from the ACCEPT on objectstack verify reports "✓ verify passed" (exit 0) on a stack that os validate, os build and os lint refuse — and the published CLI docs do not document verify #21323.
- Exit codes:
- The pin:
verify-json-stdout.test.tsspawnsverify --jsonon a stack that reaches the runtime stage. Its four ablation legs went red, including the base behaviour with the card's own signature. The stage-1 refusal pin stays green. - Out-of-scope notes, one line each:
- The nightly red is not
verify. It isjson-stdout-purity.e2e'sFAMILYlackingmigrate audit-metadata-bodies. Carrier: nightly-tiers: red on main #21347, dispatched separately on its own branch (claim5949465273). json-stdout-purity.e2ediscovers its family frombootSchemaStack(, so it cannot seeverify, which boots through@objectstack/verify. This PR's per-PR pin holdsverify; Acceptance notes.- The registry's
console.loghousekeeping sits outside the kernel logger: an observation, Acceptance notes.
- The nightly red is not
- Landing: not governed, so
pr_readyandautomerge_enablefollow in this act, and the queue lands it. The landed note follows the merge.
Generated by Claude Code
- Contract faces: none of the three is hit. The measured
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsLanded: PR #21381 →
51550933db(os verify --jsonwrites exactly one document to stdout).Fixes #21324: the card is completedomain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-02T10:15Z- Landing reading:
51550933dbis onorigin/mainas a single-parent squash: 4 files, +205 / −5. Itsgit patch-id --stableisc65c38643b84…, equal to the reviewed net diff atf9ee0ac5. - The card: closed as completed by this merge.
pm:dispatchedis removed in the same act as this note. - Carried forward: nightly-tiers: red on main #21347 (the nightly red) stays open. It is not
verify's. It is dispatched separately (claim5949465273).
Generated by Claude Code
- Landing reading:
- added 3 commits that reference this issue
on Oct 7, 2026
QA-source: #21318 · cli.verify-verdict-exit-mapping · clause 0 (step 0)
Reproduction (
@objectstack/cli@17.6.0, any scaffolded app)report.jsonholds 365 lines: lines 1–329 are kernel log lines (2026-10-02T04:13:35.431Z INFO Loading plugin: com.objectstack.engine.objectql,… Plugin registered …,HTTP server started successfully,Graceful shutdown complete, …) and the JSON document ({ app, config, multiTenant, crud, hardFailures }) starts at line 330.Expected
Under
--json, stdout carries exactly one JSON document; logs go to stderr (or are silenced). This is whatcli.verify-verdict-exit-mappingstep 0 does (objectstack verify --json > report.json), what CI and agents consuming the report do, and what other--jsoncommands were brought to (cf. theos login --jsontwo-documents fix).Generated by Claude Code