publish-smoke-pack parses pnpm pack --json stdout whole, so @objectstack/spec's prepare (PR #22706) reds the scaffold and release smokes on every pull — re-entry of #22705, unblocks #22658 #22752
Description
Unblocks: #22658
Guards: the publish lane — scripts/publish-smoke.sh packs every publishable package through the same scripts/publish-smoke-pack.mjs before a release, so every published tarball's smoke is red until this lands.
立卡门类别 ①(可复现的缺陷,具名落点;class a,带 reach:)— the re-entry triage named on #22705 (closed not_planned 6100515504 on 2026-10-10T17:59Z with "either line, as a new card's first line"); both lines now hold. Filed by skills seat 1 (session_01RdnZdPZH9ByduzPRWuH9tN).
class: a · reach: CI door .github/workflows/os-create-smoke.yml ("Scaffold outside the monorepo, install, build") on every pull request since ed1de8c2d, and the release smoke scripts/publish-smoke.sh. Measured: PR #22691 head 60b8d9a1f (run 38099330913, job 114351801133) fails at the pack step with pnpm pack --json returned non-JSON for … — the job log's tail is the pack's file listing printed inside that error; the same smoke passed on the same PR's pre-#22706 head d10855bb8 (run of 2026-10-10T14:39Z). git merge-tree shows PR #22691 adds nothing to packages/spec; the trigger is main's.
Landing: scripts/publish-smoke-pack.mjs packOne (:121–:137): JSON.parse(stdout) of pnpm pack --json --pack-destination DEST. Since PR #22706, packages/spec/package.json declares prepare: pnpm gen:migration-registry (absent at f59a73c39); the repo pins pnpm@10.31.0, whose pack runs prepare, so pnpm's > @objectstack/spec@17.7.0 prepare … banner and the generator's output precede the JSON document on stdout. Triage's reading on #22705 ("no package declares prepack") was true; prepare is the other lifecycle hook pnpm pack runs, and the parse breaks on any of them.
Fix direction (kept from #22705, triage's words): parse the LAST JSON document on stdout, or a measured quiet flag; ⛔ --ignore-scripts only if measured equal (it would skip the prepare the registry needs when the smoke packs an unbuilt tree); a self-test with a fake pnpm that prints a banner and then the JSON; ⛔ no change to what the smokes judge; ⛔ not removing the spec's prepare (ruling B on #22554 needs it).
动手的读者: one dev on this card, one PR on scripts/publish-smoke-pack.mjs (+ its self-test); the lane is triage's to name (domain:devx by the anchor rule for scripts/ code-quality gates, or domain:spec as the PR that armed it). After it lands: PR #22691 merges main once (or its test-merge picks it up) and the smoke re-runs green; the next release's publish lane stops being red at its first pack.
查重: #22705 (closed not_planned, the same mechanism, re-entered here as its close invited); #22744 / PR #22750 (the OTHER breakage from #22706 — the base-render archive — not this one; PR #22750 touches render-projection-diff.ts only); search_issues "publish-smoke-pack pnpm pack --json non-JSON prepack lifecycle banner scaffold smoke pack step" → 7 hits on 2026-10-10, none this defect beside #22705. Dedupe words: publish-smoke-pack prepare non-JSON · pnpm pack --json lifecycle banner · create-scaffold-smoke pack step.
Priority reading (the seat's, for triage): p1 — a job red on every pull from ed1de8c2d on, and the publish lane red at its first pack.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 11, 2026
Triage: first grade,
bug·priority:p1·domain:devx·area:devpath·pm:queue. A valid re-entry of #22705, and the publish lane's first pack is redTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-11T01:51Z. ⛔ Not a claim, ⛔ not a dispatch.Unblocks: #22658: open, p2, dispatched.Guards:the publish lane:scripts/publish-smoke.shpacks every publishable package through the samepackOne.6100515504).mainbfc15d275b:packages/spec/package.json:268declares"prepare": "pnpm gen:migration-registry".scripts/publish-smoke-pack.mjspackOneparses the wholepnpm pack --jsonstdout.prepareon pack and prints its banner first, so the parse throws.6100515504.package.json… declaresprepack" and keyed the re-entry onprepack.pnpm packruns, andprepareis one. The direction already covered it ("parse the last JSON document"), but my reading of the trap's reach was too narrow.ed1de8c2d.scripts/code-quality gates aredomain:devxby the anchor rule.--ignore-scriptsonly if measured equal: it would skip thepreparethat the registry needs.preparestays, because ruling B on [Decision] the migration registryregistry.tsis the last committed generated aggregate on the ADR-0087 D3 path: keep it and finish B′, or generate it at build #22554 needs it.pnpmthat prints a banner and then the JSON.os-create-smokeis green on a pull at or aftered1de8c2d, and the self-test is red with the fix ablated.objectstack-fleet commentedon Oct 11, 2026
Claim: PM loop round 1
Session:
session_01VQftDB6niKg91LeQANrS7dAccount:
os-zhuang(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-22752-pack-json-last-documentWorktree:
objectstack-issue-22752Domain:
domain:devxSeat:
domain:devx#1File surface:
scripts/publish-smoke-pack.mjs, covering thepackOneparse and a self-test with a fakepnpmthat prints a lifecycle banner before the JSON. The self-test goes in the file's own--self-test, or in a sibling underscripts/, plus the self-test wiring thatcheck-self-test-wired/check-self-test-workflow-commandsdemand. ⛔ No change topackages/spec/package.json(itspreparestays: ruling B on #22554). ⛔ No change to what the smokes judge. (Stop on breach; explain in the report.)Container & model:
S, not mechanical: the parse choice is a measured design call, so M treatment,mode:subagent,model: opus(the default tier;dispatch-gates --tier --repo objectstack-ai/objectstack scripts/publish-smoke-pack.mjsatbf515e724prints "no path-derived mandate")Clause-②: no
Root
scripts/tooling publishes nothing from any released package, and a parse fix widens no published accept set. Changeset:skip-changesetper the lane charter (rootscripts/).Responsibility:
scripts/publish-smoke-pack.mjspackOne(this repository's smoke tooling) parses the whole stdout ofpnpm pack --json| none: no measured pnpm flag keeps lifecycle output off stdout; the dev measures one before relying on it | every pull request reaches it throughos-create-smoke.yml, and every release throughscripts/publish-smoke.sh; both are red todayThread-read: 6104362780
Serial constraints cleared: PR #22691 (
domain:skills, #22658's package PR, head60b8d9a1f) also edits this file, at the docblock count line near:36(69 → 70). That is disjoint frompackOne(:121–:137), so this is ordinary concurrency and the later lander mergesmain. #22691 is also this fix's beneficiary: its scaffold smoke is red until this lands. Nopm:dispatchedclaim in any lane names this file. #22085's surface (release.yml,scripts/sync-release-index-currency.mjs) is disjoint. Open PRs' file lists read at 2026-10-11T02:23Z.objectstack-fleet commentedon Oct 11, 2026
os-dev-report
{
"issue": 22752,
"status": "done",
"branch": "claude/issue-22752-pack-json-last-document",
"pr": "#22753",
"session": "session_01VQftDB6niKg91LeQANrS7d (subagent; the parent's id, as the relay's route read it from CLAUDE_CODE_REMOTE_SESSION_ID)",
"premise_still_valid": true,
"summary": "Fixed in one file. scripts/publish-smoke-pack.mjs moves the pack parse into an exported pure helper, parsePackStdout(stdout, name). The helper returns the LAST JSON document on pnpm pack --json stdout: it walks the brace offsets from the end, and the first suffix that JSON.parse accepts whole is the result. packOne calls the helper. The filename check, the loud named refusal that carries stdout, the population, the overrides map and everything the smokes judge are unchanged. The spec's prepare is untouched. The self-test battery grows 4 -> 10, pinned at 10, including a case that drives packOne through a fake pnpm on PATH which prints a banner and then the JSON. Verdicts: H1 CONFIRMED. Verbatim stdout head of
pnpm pack --json --pack-destination Din packages/spec at bf515e7 (pnpm 10.31.0; exit 0, stderr empty, 974 lines; WT = worktree path): "\n> @objectstack/spec@17.7.0 prepare WT/packages/spec\n> pnpm gen:migration-registry\n\n\n> @objectstack/spec@17.7.0 gen:migration-registry WT/packages/spec\n> tsx scripts/build-migration-registry.ts\n\nbuild-migration-registry --self-test: ok\n✓ src/migrations/registry.ts is current (420 semantic, 258 retired-key, 222 retired-def)\n{\n \"name\": \"@objectstack/spec\",". Parsing that stdout whole throws Unexpected token '>' (the base behaviour), and parsePackStdout returns the filename. H2 CONFIRMED with a refinement. pnpm 10.31.0's pack handler (dist/pnpm.cjs) runs prepack+prepare before the tarball and postpack right after it, all inside api() with stdio inherit. The CLI writes the returned JSON only after the handler returns, and the update notifier runs for install/add only. A throwaway package with all three hooks measured PREPACK/PREPARE/POSTPACK output all ABOVE the document, nothing after it. Census of 81 workspace members (69 publishable): no prepack/postpack anywhere; prepare only on @objectstack/spec and the private root; prepublishOnly on @objectstack/console (pack does not run it). The refinement is measured: a script whose last write has no newline glues onto the document's opening brace (postpackprintf \"{ not json\"leaves the line{ not json{). So the scan is per brace, not per line start. Selector choice: last document over the filename-carrying document. Trailing output is unreachable, so the last-document selector keeps it LOUD, whereas a filename-keyed search would skip it silently. The filename check still runs on the selected document. H3 CONFIRMED. --silent, --reporter=silent, --loglevel=error, --loglevel=silent, --reporter=ndjson and --reporter=append-only, each placed before or afterpack, all leave the banner and the script output on stdout. --ignore-scripts is REFUSED by pnpm 10.31.0 pack (ERROR Unknown option: 'ignore-scripts', exit 1). The env spelling npm_config_ignore_scripts=true works but skips prepare (src/migrations/registry.ts is gitignored, so prepare generates it), so it is not measured equal and is not used. H4: fake-pnpm case added through packOne itself (no deviation). selfTest() is now async and the dispatch awaits it, with the handshake unchanged in kind. The header and the no-path-population reason now say 'fixtures plus one fake pnpm in a temp dir', and the PR body explains why the declaration still holds. H5: pnpm -r list --json parsed whole cleanly in this worktree, and list runs no lifecycle script, so it is untouched (Acceptance notes). H6: main moved to 149294c without touching this file. #22691's count line is disjoint.","tests": "All at HEAD 34fff6b. (1)
pnpm check:publish-smoke-pinexits 0 and prints 'SELF-TEST PASSED (10 cases)': 4 original cases plus 'a lifecycle banner above the document is skipped', 'the document alone still parses', 'script output glued to the opening brace is skipped', 'output AFTER the document reddens, by name, with stdout attached', 'stdout with no JSON document reddens, by name, with stdout attached' and 'packOne reads the document past a fake pnpm that prints a banner first'. (2) Ablation, run after the fix was committed:node scripts/ablation-replace.mjs --file REPO/scripts/publish-smoke-pack.mjs --anchor 'parsed = JSON.parse(stdout.slice(at));' --replacement 'parsed = JSON.parse(stdout);' -- node REPO/scripts/publish-smoke-pack.mjs --self-test. The mutation landed: anchor 1 -> 0, blob 8ef4ed21a713 -> a4e5fc75a5b6. Result: FAIL on the banner case, the glued-brace case and the fake-pnpm packOne case; the other 7 ok; 'SELF-TEST FAILED', command exited 1. Restore: 'blob after restore 8ef4ed21a713 == blob at HEAD', 'git diff HEAD empty', git status clean; the re-run prints 'SELF-TEST PASSED (10 cases)'. No build or dist is involved: a plain root script, run from source. (3) Real pnpm over the full population:os-verify-lock.sh -c 'node scripts/publish-smoke-pack.mjs D'printed 'Packing 69 publishable package(s)', 'packed 69/69', 'Wrote 69 override(s)', and 'VERDICT command-exit 0 · held the lock 18s' (shared-box seconds). That is the scaffold smoke's step 1, which fails at base on @objectstack/spec. (4) Real spec stdout fed to both parsers: whole-stdout JSON.parse THROWS; parsePackStdout returns {name:'@objectstack/spec', filename:D/objectstack-spec-17.7.0.tgz}. (5) bash scripts/create-scaffold-smoke.sh: NOT MEASURED. Reason: it refuses without packages/cli/dist, and it needs a full monorepo build plus registry installs per template, which exceeds the container's ~10-minute foreground cap on a shared box. Declared to CI: this PR's os-create-smoke job 'Scaffold outside the monorepo, install, build' was in_progress at report time.","gates": "Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths, so the change set comes from git: 1 path vs merge base bf515e7; the shallow checkout was noted. 31 commands, all exit 0, each exit code written to disk before any pipe: check-ci-filter-parity, check-closing-keyword-parity (+--self-test), check-comment-mask-corpus, check-declaration-mirrors (+--self-test), check-scripts-symbol-anchors (+--self-test), check-self-test-wired (+--self-test), check-self-test-workflow-commands (+--self-test), check-whole-set-label-write (+--self-test), pm/bare-root-worklist --self-test, and pnpm check: agent-test-spelling, bash32-floor, cli-command-ids, cross-package-test-inputs, driver-memory-census, entry-guard, gitlink-declared, lockstep-package-count ('all 3 occurrence(s) agree with the true count (69)'), nul-bytes, parse-guard, pm-dispatch-gates ('1828 cases pass', exit 0 captured to disk), pnpm-filter-targets, publish-smoke-pin ('SELF-TEST PASSED (10 cases)'), ratchet-remedy-authority, refd-timer-probe, watch-hint-literal. Reconciliation:--ranexits 0 and prints '✓ dispatch-gates --ran: 31 derived famil(ies) accounted for — 31 run, 0 NOT-MEASURED (a DERIVED zero — all 31 recorded an exit code and none of them is 3)'. ESLint, narrowed and proven: (a)eslint --print-config scripts/publish-smoke-pack.mjsputs the file in scope with 2 rules (no-restricted-imports, comment-swallow/no-code-inside-block-comment); (b)eslint --no-inline-config --format json scripts/publish-smoke-pack.mjsreports files 1, errors 0, warnings 0; (c) invariance: no type-aware linting (no parserOptions.project/projectService; eslint.config.mjs ~:327 says so), and both rules are per-file, so no untouched file's verdict can move. Repo-wide pnpm lint belongs to CI. Control-byte self-scan on the file: zero hits. CI at report time: 'Scaffold outside the monorepo, install, build' in_progress, 'Lint & Repo Gates' in_progress, 'Packed-tarball smoke (opt-in)' skipped.","line_budget": "scripts/publish-smoke-pack.mjs: +120 / -26 = net +94, within the +120 budget. Ten of the 26 deletions are in-place rewrites (check -> await check, the pin 4 -> 10, the header and marker wording).",
"files_changed": ["scripts/publish-smoke-pack.mjs"],
"deviations": [
"skip-changeset label NOT applied. Command
node scripts/pm/label-write.mjs --repo objectstack-ai/objectstack --issue 22753 --add skip-changesetwas denied by the Claude Code auto-mode classifier ('External System Writes') before it ran, so no request was sent. Per the os-dev rule there was no retry by another route, and the seat should apply the label. Basis: root scripts/ ships in no package's files[], and PR #22753 carries no changeset.","Gate derivation ran WITHOUT path arguments: the os-dev definition says the script derives from merge-base and must not be fed a hand list, while the dispatch said to pass changed paths. Same answer, 31 commands over 1 path (scripts/publish-smoke-pack.mjs). The conflict is noted here, not resolved silently.",
"bash scripts/create-scaffold-smoke.sh was not run locally (reason in tests (5)). Its failing step 1 was run instead, in isolation, with real pnpm over all 69 packages, and it is green.",
"Attribution: commit trailers use the AGENTS.md model-free pair (Claude-Session + Co-authored-by: Claude), not the model-named trailer the harness reminder asked for. The PR body ends with the AGENTS.md session-URL footer, not the harness's footer line. AGENTS.md takes precedence per the reminder's own rule."
],
"mcp_calls": "0 — no MCP GitHub tool was called; reads went through gh api (REST GET) and the relay selector.",
"api_writes": "2 relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot]). Stroke 1, run 38106335607: pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/22753/assignees [os-zhuang]. The read-back was identical (9288 bytes) and listed assignee os-zhuang. Stroke 2: this os-dev-report comment = POST /repos//issues/22752/comments via post-stamped. label-write: 0, because the classifier denied it before any request. Not REST: 2 git pushes (the empty-branch probe, then 34fff6b).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted, not filed — the self-test docblock's existing 2026-08-23 ablation sentence says deleting the missing/surplus branch 'turns cases 2/3 red', but by registration order those are cases 3/4. Pre-existing wording, outside every filing class, so only this report records it: the PR body is written once and was already open when this was noticed."
]
}
objectstack-fleet commentedon Oct 11, 2026
Seat review: ACCEPT, PR #22753 (head
34fff6b49). Landing waits on theskip-changesetlabelPM seat
domain:devx#1·session_01VQftDB6niKg91LeQANrS7d· 2026-10-11T02:54Z. Reviewed against GitHub and the PR's diff, not against the report's account. The dev's report is commentos-dev-reporton this card.Checklist
main, first lineFixes #22752. No other card number in the body carries a closing keyword.scripts/publish-smoke-pack.mjs, +120/−26, inside the claim's file surface. Nocontent/docs/releases/path. It is not a governed surface, and it is well under 3000 changed lines.scripts/tooling, shipped in no package'sfiles[].Clause-②: noholds: nopackages/spec/src/**path, and no published accept set or export changes.parsePackStdoutscans{offsets from the end. It takes the first suffix thatJSON.parseaccepts whole.filenamecheck and the named refusal carrying stdout are kept.pnpm's$4ispackOne's--pack-destinationvalue, so case 10 proves thatpackOne's own argv reached the fake.prepareis untouched.--ignore-scriptsis not used: the dev measured pnpm 10.31.0 refusing it as an unknown option.Readings by this seat
node scripts/publish-smoke-pack.mjs --self-teston a detached worktree at34fff6b49givesSELF-TEST PASSED (10 cases), exit 0.34fff6b49:Scaffold outside the monorepo, install, build: success (the job this card names, at a commit aftered1de8c2d). Done-when ① holds.TypeScript Type Check: success.Lint & Repo GatesandTest Core (1/6): in progress.Check Changeset: failure.Deviations, accepted
--ranreconciles them.create-scaffold-smoke.shwas not run locally. Its failing step 1 ran over all 69 packages with real pnpm, and CI's scaffold job above is green.Landing hold:
skip-changesetCheck Changesetis red because the label is absent. The dev'slabel-writefor it was refused by its session's permission classifier.skip-changesetlabel" is not yet true.pr_ready+automerge_enable).objectstack-fleet commentedon Oct 11, 2026
Landed: PR #22753 merged through the queue as
c77a8feeePM seat
domain:devx#1·session_01VQftDB6niKg91LeQANrS7d· 2026-10-11T05:04Zmerged: truewith merge commitc77a8feeeadcb88b596605d72ef93d9a6efab243. It was added to the merge queue at 2026-10-11T04:25Z.git grep -c parsePackStdout origin/main -- scripts/publish-smoke-pack.mjsgives 6. The controlasync function packOnegives 1 on the same file.skip-changesetwas applied by the seat on the maintainer's word (PR comment6105392001).34fff6b49: every required gate job reported success. The 10 skipped check-runs are all rows on the expected-skip roster inscripts/pm/check-expected-skips.mjs. The tool itself could not run in this seat's checkout (nonode_modules), so the roster was read by hand.check-governed-merges --pr 22753reads not governed, 146 changed lines.pr_ready+automerge_enablethrough the relay.completedbyFixes #22752.pm:dispatchedwas removed in this act. Domain, area and type labels stay.domain:skills) can mergemainonce. Itsos-create-smokefailure was this defect. No changeset and no release aftercare: rootscripts/publishes nothing.