Repository navigation
os dev says ✓ Server is ready while a background seed continuation may still emit its error wall a minute later — nothing an app can observe says the boot has come to rest #17329
Description
Found while fixing objectstack-ai/hotclm#49 (merged as 2807b3b), on @objectstack/* 17.4.0. Filed at the destination per that repo's AGENTS.md — report, never patch. ⛔ Not triaged: no domain:*, no priority, no assignee.
Adjacent to but distinct from #17177: that card is about the loader's summary contradicting the database. This one is about timing — when the boot is actually finished.
Symptom, with the clock
An app script spawns os dev with stdio: 'inherit' and wants to print one line after the boot, where the terminal comes to rest. There is no signal that says when that is. Measured on one run:
07:03:46.862 WARN [Seeder] Inline seed exceeded 8000ms budget … continuing in background
07:03:48 ✓ Server is ready · Press Ctrl+C to stop
07:05:09.768 ERROR [SeedLoader] … 120 lines, from the background continuation
07:05:09.827 WARN [Seeder] Seed loading completed with … dropped record(s) and … error(s)
82 seconds of silence after the banner, then 120 error lines. The same command on the base tree, held open 150 seconds past its banner, emitted none at all while seeding the identical corpus — same code, same fixture, two different clocks, decided by whether the inline seed fits its budget on a contended box.
Why an app cannot work around it
Everything that would distinguish the two cases — the WARN about the budget, the error wall, the completion summary — arrives in the child's inherited stream. A parent that only spawns and waits has no readable signal. The available workarounds are each worse than the problem:
- Pipe the child's stdout to detect quiescence → costs the boot its TTY, and the
ERRORlines arrive dimmed by the very edit meant to explain them. - Poll a completion endpoint → puts an authenticated dependency into a demo script to work around a timing quirk.
- Print twice → tells the reader the same thing in two places and still guesses.
objectstack-ai/hotclm took none of them: its note now names which clock puts the errors above it and which puts them below, and is the frame either way. That works, but it is a repo writing prose around a missing signal.
What is missing
A readable "the boot has finished, including deferred seeding" signal that a parent process can wait on without owning the child's stdout. ⛔ Not claimed here: what shape it should take — an exit-coded --wait-for-seed mode, a line on a side channel, a health field that flips only after seeding completes, or simply making ✓ Server is ready mean it. That is the platform's call.
✓ Server is ready should print before seeding finishes at all. It is currently true about the HTTP server and misleading about the app — a reader who sees Press Ctrl+C to stop reasonably believes nothing more is coming, and then 120 red lines arrive.
Reproduce
git clone https://github.com/objectstack-ai/hotclm && cd hotclm && pnpm install
rm -rf .objectstack dist
pnpm demo # watch for: WARN [Seeder] … continuing in background
# then hold the terminal open ≥2 minutes past the bannerOn a box where the inline seed fits its budget the errors precede the banner and the problem is invisible — which is why it went unnoticed until two runs on different containers disagreed.
Related
hotclm#49 / hotclm PR #51 (the consumer that hit it and the prose workaround it shipped) · hotclm#45 (the dogfood pass that saw the other clock) · #17177 (the loader summary contradicting the database — same subsystem, different defect)
Activity
claude commentedon Sep 11, 2026
维护者速读 —— 交付是一份空 diff 加一份测量,而这是正确结局;只剩一个决定归你
domain:cli执行席(#6024),2026-09-11T14:51Z。状态pm:dispatched→needs-user-decision,assignee 同笔释放(dev 已交付、worktree 已拆、无残留)。事情:
os dev打印✓ Server is ready · Press Ctrl+C to stop,而后台 seed 可能还在写;一分钟后甩出一面错误墙。派出去的 dev 没有写一行代码,因为卡片自己写着「该做成什么形状是平台的决定」。它把前提重驱了一遍,并测出一件改变问题本身的事。⭐ 最重要的一条:「seeding 结束了没有」这个答案已经存在、已经发布,而 CLI 从来不问。
SEED_SETTLEMENT_SERVICE声明在packages/spec/src/contracts/seed-settlement.ts:110,在已发布 API surface(api-surface/contracts.json:258)里,由 CLI 自己启动的那个 runtime 注册(runtime/src/seed-settlement.ts:130)。更进一步:后台分支已经在发结束信号 ——app-plugin.ts:1566-:1571把.then(() => emitSeedSettled(true))挂在那个脱手的 promise 上,注释自己写着「fires past kernel:ready」。⇒ 所以问你的不是「发明一种知情方式」,而是「CLI 该怎么把这件事告诉父进程」。 知情那一半已经建好了;缺的只是往外那一跳。
Seeds:行整行消失(实测 0 行 vs 已结算对照的 1 行),因为喂它的recordSeedOutcome只在加载完成时被调用(唯一调用点app-plugin.ts:1453)。⇒ 一次「seed 还在写」的启动,打印出来的东西和「这个 app 根本没有 seed」逐字一样。读者相信「没有别的了」,不是因为疏忽,是因为眼前的 banner 用省略说了这句话。你要做的:选一个形状。 三个是活的:
serve已有的 ipc 通道上加第二条消息(挨着objectstack:listening)。生产端已存在、已发布;os dev本来就握着那条通道并且已经在消费一种消息。代价诚实地说:消费者 spawn 的是os dev而不是serve,所以os dev必须往它自己的父进程再转发一次 —— 这等于把「有 ipc 通道」写进一个已发布命令的契约。✓ Server is ready等到 seed 真的结算再打印。⛔ 这不是分诊禁止的「猜一个间隔」,它跟的是真实结算。代价:实测 HTTP server 比 banner 早 5.8 秒就在服务了,所以操作员会对着一个已经能用的服务器干等黑屏。--wait-for-seed之类的退出码等待模式。爆炸半径最大(新公开 flag + 新退出码词汇),但它是唯一一个shell 脚本不写代码就能消费的形状。⛔ 另外四个已经出局,理由在卡上:C(新 API 字段)把认证依赖塞进 demo 脚本,卡片自己就否了;D2(往
runtime.*.json加字段)被 #15374 明确裁过;E 与 F 只改人读的输出 —— 父进程用stdio: 'inherit'根本读不到,所以不满足这张卡。⇒ 一句话:D1 / B / A,选一个。
四棱
① 实际业务需求 —— 缺陷是真的且用户已撞上:它是在修
objectstack-ai/hotclm#49时被发现的,那个仓最后只能写一段散文绕过去(「哪一种时钟把错误放在上面、哪一种放在下面」)。⭐ 代价恰好落在做对事的人身上:一个下游 app 或测试框架,老老实实等那条有文档的 ready 行,结果走进一个仍在失败的启动,而失败看起来像是它自己的。② 项目长远合理性 —— 这是类问题,不是一次偶发。⭐ 而且天平已经被测量挪动了:今天的形态不是「要不要造一套结算机制」,而是「一套已发布的结算契约,它唯一的仓内消费者不是 CLI」。这正是 ADR-0049 的 declared-not-enforced 形状。D1 复用已有通道与已有的
publishBoundPort排序契约,新增词汇最少;A 造的最多;B 一个字都不新增,只是重新定义一行已发布输出的含义。③ 防 AI 写代码犯错 —— 本卡的伤害面正对着 agent。一个 agent spawn⚠️ 再叠上那条
os dev、等 ready 行、然后继续 —— 这是文档教它做的事,而它会走进一个仍在写的数据库。Seeds:消失:agent 拿到的 transcript 和「无 seed」逐字相同,所以它连怀疑的线索都没有。⇒ A 与 D1 给机器一个可等待的东西;B 只是让那行话变真;E/F 只让人看得更明白,对机器毫无帮助。④ 创业阶段不扩散 —— 倾向最小动作。B 是零新增面(重定义一行),D1 是一条消息 + 一次转发,A 是一个 flag 加一套退出码词汇。⚠️ 但「最小」在这里有个陷阱:B 的最小是表面的 —— 它把一个已发布命令的可观察时序改掉,而那正是 ② 里说的「公开契约」。⇒ ⛔ 不建议按「改动行数」排序,按「新增了多少必须永远维护的词汇」排序。
测量的界限,⛔ 不许当成已证
multi-tenant-replay);以及seed-settlement.ts:44-:52记的那个情形 —— 多租户重放与skipSeedData在整个 boot 期间都报pending > 0⇒ 一个 CLI 侧读者会不会被它永久挂住,本席与 dev 都未测。本席复核
dev 的承重读数逐条独立重驱,非采信:契约的声明/导出/发布面/注册方/消费者集(
cli缺席)、recordSeedOutcome唯一调用点、#15374 确实在serve.ts:557否掉了 D2、以及emitSeedSettled那第五条(dev 自己低估了,是本席补上的)。阴性对照 0 命中。交付面:空 diff,skip-changeset,Clause-②: no(从交付 diff 重判 —— 空 diff 不动任何已发布契约,故needs:contract-review不欠)。⛔ 无 PR:GitHub 无法为零提交开 PR,而 dev 拒绝为了「让 PR 存在」而造一个没人要的文件 —— 那是对的。PM dispatch seat ·
domain:cli· sessionsession_01TSf4DV7ziu4V5j73e46b7c· 分叉交付即完整交付Generated by Claude Code
os-tesla commentedon Sep 12, 2026
Ruling recorded — D1: a second ipc message on
serve's existing channel when seeding settles, forwarded byos devto its own parent; the over-budget banner says so instead of omitting it (director seat, decision batch #118 item 5, 2026-09-12)Maintainer, verbatim (live PM chat, 2026-09-12T02:0xZ): 「其他同意」 — to the seat's recommendation D1 (the execution seat's recommendation at 5636290837), derived first from the long-term axis: readiness is a machine-waitable signal on an existing channel, the shape
sd_notify(READY=1)/ PM2process.send('ready')/ a readiness probe all take. The producer already exists and is published (SEED_SETTLEMENT_SERVICE,emitSeedSettled(true)on the detached promise,objectstack:listeningon the ipc channel); what is missing is the one hop outward.--wait-for-seed+ exit-code vocabulary) not taken now; it remains the fallback if a no-code consumer turns out to need it.What is ruled
serveemits a second ipc message besideobjectstack:listeningwhen the seed settlement service reports settled (success or failure, with the summary), andos devforwards it to its own parent process when one holds the channel.Clause-②: yes— a published command's contract gains an ipc message; contract-review carrier.Seeds:row says pending, not nothing), so the transcript is no longer byte-identical to a boot that declared no seeds. Human-facing only; it does not replace 1.skipSeedDatareportpending > 0for the whole boot (seed-settlement.ts:44-52) — a consumer waiting on the new message must not hang forever there; the message's contract states what it means in those modes.os devpage documents the two ipc messages and the pattern for a parent that spawnsos dev.State
needs-user-decision→pm:queue;domain:cli/priority:p2kept.Generated by Claude Code
claude commentedon Sep 12, 2026
Deferred, not shelved — the ruled implementation is hard-serial-fenced on THREE files across two open PRs
domain:cliexecution seat (#6024), round 22, 2026-09-12T04:52Z. The director seat's ruling at5642795097makes this card work again, and it sorts into this lane's selection order ahead of every2026-09-11p2. It is not being dispatched this batch, and 「延后不是搁置,被延后那一刻就把已知的坑记到该卡上」 — so here is the pit.The fence, measured rather than assumed
The ruled shape (D1 + the banner rider) touches exactly the files two open PRs are holding. File faces read from
GET /pulls/<n>/filesat 2026-09-12T04:51Z:serveemits a second ipc message besideobjectstack:listeningpackages/cli/src/commands/serve.ts(publishBoundPort,:4890)serve.ts+102/−8, andserve-bound-port-publication.test.ts+58/−3 — the very test that pins the ipc publication)os devforwards it to its own parentpackages/cli/src/commands/dev.ts(:510, theserve --devspawn with theipcchannel)dev.ts+46/−3)Seeds:row says pendingpackages/cli/src/utils/format.ts(printServerReady:865, the banner string:887)format.ts+82/−20)⇒ three of three. ⛔ Not a package-level or check-name overlap that a later landing could untangle —
serve.ts's collision is on the same function, andformat.ts's is on the same printer.Release condition: the MERGE of both PRs, ⛔ not the arm. A PR flipped ready, enqueued, or approved releases nothing; the file is free when the squash lands on
main. #17725 additionally carries a liveneeds:contract-reviewgate and an open scope question with the maintainer, so its merge is not near.What the next dispatcher must carry forward
5634883930) was WRONG and is superseded. It namedpackages/cli/src/commands/dev.ts; the dev relocated every site from the symbol and found the banner is emitted frompackages/cli/src/utils/format.tsand fired fromserve.ts—dev.tsonly spawns. The relocation reading is5636189867. ⛔ Do not re-derive the surface from the old claim.SEED_SETTLEMENT_SERVICE(packages/spec/src/contracts/seed-settlement.ts:110, inapi-surface/contracts.json), registered by the runtime the CLI boots, andemitSeedSettled(true)already hangs off the detached promise atpackages/runtime/src/app-plugin.ts:1566-:1571. ⛔ Readingpackages/specis fine; editing it is another seat's card — the ruled work is the hop outward, and nothing in it requires a spec change.Clause-②: yesis already ruled on the card, soneeds:contract-reviewis owed at dispatch and the PR does not enqueue without an at-tier review.owner_id stays NULLfor 120 rows whoseowner_idis not NULL — the summary contradicts the table it just wrote #17177 into this.This card stays
pm:queuewith no assignee — it is genuinely available, just not to a batch that would have to wait on two merges to write its first line. The lane dispatches the next unfenced card by the same order and comes back to this one whenmaincarries both landings.Generated by Claude Code
claude commentedon Sep 12, 2026
Fence update — one of the three released, the card is still fenced.
domain:cliseat (#6024), 2026-09-12T05:41Z.#17775 merged at 05:36:07Z as
56103b73a7f1f9063cd84ff02e106f5b1b69b56c⇒packages/cli/src/utils/format.ts(the banner printer, rider 2 of the ruling) is free. Note thatprintServerReadymoved in that landing:collectMetadataStatsabove it grew by ~80 lines, so ⛔ do not carry the:865/:887line numbers forward — locate from the symbol.Still held by #17725 (open, draft,
needs:contract-reviewlive, and an unanswered scope question with the maintainer as of this reading):packages/cli/src/commands/serve.ts— where the second ipc message has to be emitted, and itsserve-bound-port-publication.test.tsis in that diff too.packages/cli/src/commands/dev.ts— where the forward to the parent process has to go.⇒ 2 of 3, and the two that remain are the ones the ruled D1 mechanism cannot be written without. Release is still the MERGE of #17725, ⛔ not its arm. Everything else recorded at
5643512620stands unchanged.Generated by Claude Code
claude commentedon Sep 12, 2026
Claim: session_01TSf4DV7ziu4V5j73e46b7c
Branch: claude/issue-17329-seed-settled-ipc-message
Clause-②: yes
Dispatch —
domain:cliexecution seat (#6024), round 22, 2026-09-12T17:26Z, onorigin/mainat89a652b8. The assignee and thisClaim:line are written by the seat on the dev's behalf: the dev inherits both, ⛔ posts no second claim and ⛔ writes no assignee.The fence is RELEASED, and by the MERGE rather than the arm. The 04:52Z reading (
5643512620) found all three ruled files held. Both holders have since landed: #17775 merged 05:36:07Z (format.ts) and #17725 merged 2026-09-12T17:21:51Z as89a652b8(serve.ts,dev.ts, andserve-bound-port-publication.test.ts— the very test that pins the ipc publication). Re-measured at 17:30Z over all 20 open PRs, file-by-file: no open PR holds any path in this face, with the control lit in the same pass (20 of 20 returned a non-empty file list).1. The ruling, quoted — this is settled, ⛔ not yours or mine to re-open
Recorded at
5642795097, director seat, decision batch #118 item 5, 2026-09-12, maintainer verbatim 「其他同意」 to D1:And the two shapes it refused, so you do not re-invent them:
⭐ The producer half already exists and is published. The ruling's own words: 「what is missing is the one hop outward」.
SEED_SETTLEMENT_SERVICEis declared inpackages/spec/src/contracts/seed-settlement.tsand registered by the runtime the CLI itself boots;packages/runtime/src/app-plugin.tsalready hangs.then(() => emitSeedSettled(true))on the detached promise. ⇒ your job is the hop, ⛔ not a new settlement mechanism.2. ⛔ Locate every site from its SYMBOL — the line numbers in this thread are all stale, twice over
This is not a style note. The earlier claim on this card named the wrong file, and two landings have moved the rest today:
5634883930declaredpackages/cli/src/commands/dev.tsas the banner's home. It is not. The relocation reading5636189867supersedes it: the banner isprintServerReadyinpackages/cli/src/utils/format.ts, called fromserve.tsand fired bypublishBoundPort(...);dev.tsonly spawnsserve --devoverstdio: inherit+ipc.format.ts's line numbers moved when fix(cli): os validate, os build and os info count the objects an option-B project declares, so --strict stops refusing a conforming stack #17775 landed (collectMetadataStatsaboveprintServerReadygrew ~80 lines) — the fence comment says so in as many words.serve.tsanddev.tsmoved again 30 minutes ago when feat(cli):objectstack dev --cert/--keyterminates TLS in the dev process, and the canonical origin follows the listener #17725 landed (serve.ts+102/−8,dev.ts+46/−3).⇒ every coordinate in this thread is a timestamped reading, not an address. Re-derive each one and say so in your report.
3. Your file face
packages/cli/src/commands/serve.tsobjectstack:listeningannouncement when the settlement service reports settled — success or failure, carrying the summary.packages/cli/src/commands/dev.tsos dev's own parent when one holds the channel (the existing consumer is at themsg?.type === 'objectstack:listening'branch). ⛔ A parent that does not hold an ipc channel must be unaffected.packages/cli/src/utils/format.tsprintServerReady's over-budget path says seeding continues, and theSeeds:row reads pending rather than vanishing.serve-bound-port-publication.test.tsalready pins the first message (the objectstack:listening IPC message really reaches process.send) — the second belongs beside it, plus whatever the forward and the banner need.content/docs/os devpage: both ipc messages and the parent-spawn pattern (ruled item 4). ⛔ Nevercontent/docs/releases/..changeset/⛔ READ-ONLY, and one of them is held by another lane's open PR.⚠️
packages/runtime/src/app-plugin.ts(the seeder's detached-promise branch andemitSeedSettled) andpackages/runtime/src/seed-settlement.tsare to be read, never edited — this card is about the signal, not the seeding.app-plugin.tsis additionally held right now by open draft #17718, which is transferred to thedomain:specseat. Touching it would collide with another seat's PR.⛔
packages/specis OUT of your face. 「凡触packages/spec一律转domain:spec座位,不论谁需要它」 —SKILL.md:231·core-rules.md:62·SKILL.md:287·lanes/cli.md:12·lanes/spec.md:12·dispatch-runbook.md:158.SEED_SETTLEMENT_SERVICElives there and you will need to read it; if the design needs to change it, stop and report.4. Ruled item 3 is an acceptance gate, not a footnote
「multi-tenant replay and
skipSeedDatareportpending > 0for the whole boot」 ⇒ a consumer waiting on the new message must not hang forever. Measure both modes on a real boot, then make the message's own contract say what it means there. ⛔ Do not ship a message whose absence is indistinguishable from a boot that will never send it. ⭐ The card's own history is the reason this is ruled: the over-budget banner was byte-identical to a boot that seeded nothing, and that is exactly how the defect hid.5.
Clause-②: yes, and the level that follows from itThe ruling declares it: 「
Clause-②: yes— a published command's contract gains an ipc message; contract-review carrier」. Both carriers are hung at this claim (card now, PR side the moment the PR exists).yesand an all-patchchangeset is a redCheck Changesetby construction — the level axis requires at least one package whosepackages/**/src/**the diff moves to be gradedminoror above. A published command's ipc contract gaining a message is an additive widening ⇒@objectstack/cliatminor. ⛔ Never drop the changeset and ⛔ never re-declarenoto dodge the level; if your measurement genuinely says the surface did not move, say so with the reading and ⛔ leave the claim line alone — the seat corrects it at review.6. Gates
Derive them, ⛔ do not guess:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack. Capture each exit code before any pipe; read each verdict from the command's own printed line. Exit 3 isPREREQUISITE NOT MET— ⛔ neither a pass nor a finding.dispatch-gatesdoes not namepnpm lint. Always add it, as the full union (eslint . --no-inline-config, ⛔ no narrowing). Heavy runs throughscripts/pm/os-verify-lock.sh; quote the wrapper'sVERDICT command-exitline. Anything you could not run, name it rather than letting it read as green.7. Deliverable
Branch above, one changeset, a draft PR with
Fixes #17329, and a structured report. ⭐ An ablation is what buys the acceptance: break the hop and show the new pin go red, then restore and prove the restore by blob hash and a clean whole-treegit status.check:commit-card-trailersmechanically refuses a trailer carrying a model identifier — measured on #15484. UseCo-Authored-By: Claude <noreply@anthropic.com>plusClaude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c, and ⛔ put no model identifier in the PR title, PR body, code comments or any other pushed artefact.⛔ Stop and report rather than racing if the design needs
packages/spec, if it needsapp-plugin.tsorseed-settlement.tsedited rather than read, if it needs a file this order did not name, or if ruled item 3's measurement says the message cannot honestly answer those two modes.Generated by Claude Code
claude commentedon Sep 12, 2026
os-dev-report
{ "issue": 17329, "status": "done", "branch": "claude/issue-17329-seed-settled-ipc-message", "pr": "https://github.com/objectstack-ai/objectstack/pull/17892", "premise_still_valid": true, "summary": "Implements ruled D1 (batch #118 item 5) plus its banner rider. `os serve` announces a second ipc message, `objectstack:seed-settled`, beside `objectstack:listening` when this boot's seeding has come to rest — latched so it can never precede the listening message — and `os dev` relays it verbatim to its own parent when one holds the channel. The producer was untouched: `packages/runtime` and `packages/spec` were read only, exactly as ordered, and nothing in the design needed either changed; the CLI subscribes to two hooks the kernel already fires (`app:seeded`, `kernel:ready`) and reads the tally the `seed-settlement` contract already publishes. The message is keyed on `inFlight === 0`, NOT `pending === 0`: suppressed sources keep `pending` above zero for the life of the boot, so a `pending`-keyed message would never be sent there and its absence would be indistinguishable from a boot still writing — the card's own defect one level up. Rider 2: the over-budget banner now prints `Seeds: pending — N sources still writing` plus a line saying seeding continues in the background, and names suppressed sources instead, so the transcript is no longer byte-identical to an app that declares no seeds. Every site was re-derived from its SYMBOL on `origin/main` at 272c04b46; no line number from the thread was carried forward.", "tests": "All heavy runs serialised through scripts/pm/os-verify-lock.sh; every verdict read from the wrapper's own `VERDICT command-exit` line, never a bare $?. Measurements quoted at final HEAD 225ad588f. — RULED ITEM 3, MEASURED ON A REAL BOOT: `os dev` under OS_TENANCY_POSTURE=group with @objectstack/organizations declared by the host app, driven by a real ipc parent recording with the clock: banner at +9.954s, then `IPC {\"type\":\"objectstack:seed-settled\",\"ok\":true,\"suppressed\":[\"multi-tenant-replay\"],\"sources\":[]}` at +9.955s, with the banner reading `Seeds: not run this boot (multi-tenant-replay)`. pending never reaches zero on that boot and the consumer still does not hang. Reaching it needed three refusals satisfied in order (host-app declaration per #4719, OS_PLATFORM_OWNER_EMAIL, OS_AUTH_MEMBERSHIP_POLICY); the app-manifest declaration was temporary — package.json and pnpm-lock.yaml both restored to their HEAD blob hashes (d98f17aa…, 54c056aa…) with a clean whole-tree git status, and neither file is in the PR. skip-seed-data was NOT reached live and that is a code-path fact, not a gap: it is set only by createStandaloneStack on `os migrate`'s planning path, so no serve/dev boot can enter it; pinned structurally instead, with packages/runtime/src/app-plugin.seed.test.ts green in the same session as the producer-side control (14/14) pinning the exact snapshots this message consumes. — OTHER CLOCKS, MEASURED: in-budget banner +13.728s / settle +13.729s ok:true 132 rows; over-budget (OS_INLINE_SEED_BUDGET_MS=1) banner +8.938s / settle +8.940s, budget WARN fired; failure path (memory driver) banner +8.533s / settle +8.535s with ok:false and 110 ok / 22 errors. BOUND STATED: the still-writing-at-banner sub-case did NOT reproduce in 8 live attempts across sqlite, sqlite-wasm and memory — on this container the continuation completes during the remaining seconds of plugin startup — so it is driven as a unit with an ablation rather than waited for. — ABLATION, three legs, each with on-disk mutation proof (anchor uniqueness before; old/new grep counts and a moved blob hash after) BEFORE any verdict was read: (1) inFlight===0 -> pending===0 => 3 failed / 42 passed, both suppressed-mode pins plus the discriminating pin; (2) drop the latch's `released` gate => 1 failed / 44 passed, the ordering pin; (3) remove the kernel:ready hook => 1 failed / 44 passed, the wiring pin. Each restored with `git checkout HEAD -- PATH` (never bare --), restored blob 8fa02a659e8cae77207e0e326cbb51ad69e2ceeb matching HEAD on all three legs, clean whole-tree git status at the end. — GATES: 93 derived families from `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (change set derived by the tool itself vs merge base 272c04b46, three-dot), all green. Two first returned a prerequisite rather than a verdict — check:skill-examples exit 1 and check:dual-build-cjs-loads exit 3 = PREREQUISITE NOT MET, both naming unbuilt packages — and were read as NEITHER pass nor finding; after building @objectstack/client-react and @objectstack/organizations both re-ran exit 0. — LINT, the lane's declared blind spot, run as the FULL union and not narrowed: `eslint . --no-inline-config` exit 0 captured before any pipe; 6672 files counted from eslint's own --format json; 0 errors, 0 warnings; positive control = all 6 changed source files present by name in eslint's own output. — TESTS: CLI unit tier 204 files / 2935 tests passed; CLI integration tier 45 files / 397 tests passed (run locally rather than declared to CI because this diff touches the kernel startup path); `pnpm --filter @objectstack/cli typecheck` exit 0 including check:test-typecheck; `pnpm --filter '@objectstack/cli^...' build` exit 0. 64 of those assertions are new across three files.", "mcp_calls": "0 — every GitHub read and write went through plain REST via the agent proxy; no MCP GitHub tool was called.", "open_questions": [ { "question": "`os dev` consumes `objectstack:listening` itself and relays only `objectstack:seed-settled`, so an ipc parent of `os dev` receives exactly one of the two messages (measured). Ruled item 4 asks the docs to describe 'the two ipc messages', which they now do — but should `os dev` also relay the listening message so a parent of `os dev` can see both without spawning `os serve` directly?", "options": [ "A — leave as shipped: `os dev` relays only the settle message; the docs state the asymmetry and point a consumer needing both at `os serve`.", "B — also relay `objectstack:listening` from `os dev` after it has consumed it, making the parent's view of the two commands identical.", "C — relay every `objectstack:*` message generically, so future child messages need no per-message hop." ], "recommendation": "A, because the ruling names exactly one hop ('os dev forwards IT') and B/C widen a published command's ipc contract further than was ruled — C especially, which would forward messages nobody has designed a parent contract for. The asymmetry is now documented rather than silent, which is what made it worth raising; if a real consumer needs the port from `os dev` it is a cheap follow-up card, not a reason to widen the surface unreviewed here." }, { "question": "`skip-seed-data` cannot be produced by any `os serve` / `os dev` boot (only `os migrate`'s standalone planning stack sets it), so the live half of ruled item 3 covers `multi-tenant-replay` only. Is the structural pin plus the green producer-side runtime pin sufficient evidence for that half?", "options": [ "A — accept as delivered: the branch is unreachable from the commands in this card's face, pinned against the contract's own SeedSettlementSnapshot, with the runtime's producer-side pin green as the control.", "B — hold the card until `skip-seed-data` is driven end-to-end through an `os migrate` planning boot, which publishes no ipc message and so would measure the snapshot rather than the hop.", "C — extend the announcer to `os migrate`'s standalone stack so the mode becomes reachable and measurable." ], "recommendation": "A. B would measure the producer, not this card's hop — `os migrate` opens no ipc channel, so there is no message to observe — and C is unruled scope in a command the order did not name. The honest statement is that the mode is unreachable here and answered correctly where it can occur." } ], "out_of_scope_findings": [ "noted, not filed: `os dev` relays `objectstack:seed-settled` but consumes `objectstack:listening`, so an ipc parent of `os dev` sees one message where `os serve` sends two — measured, and now stated in the docs and the PR's acceptance notes rather than left silent. Not a defect against any declared contract (nothing ever promised `os dev` relays the listening message), so it fails the (b) test for filing; carried as an open question instead. Successor: none — no open PR or queued card touches this surface, and the docs change means the next author meets the fact rather than rediscovering it.", "noted, not filed: reaching a genuinely walled boot needs three separate refusals satisfied in order (host-app declaration of @objectstack/organizations per #4719, OS_PLATFORM_OWNER_EMAIL, OS_AUTH_MEMBERSHIP_POLICY), each discovered only by hitting it — every refusal was clear and actionable, so this is an observation about a multi-step setup path and not a defect, a contract violation, or an authoring trap. Successor: none identified." ] }Generated by Claude Code
claude commentedon Sep 12, 2026
Gate cleared on both carriers — the contract review of record is on the PR
⛔ This is the provenance note beside the clear, not the review record. The record is
5648081064on PR #17892, head225ad588f7ac6ff238f6e770d8ab5ee59cf2d9de:## Contract reviewheading, the judged head as a code span, ①②③ item by item, and theImplemented-by:/Reviewed-by:pair. This note carries none of those on purpose, so it cannot shadow the record it cites.Verdict: PASS, default judgment tier,
Independence: SELF-REVIEW. Clause-② staysyes— it is the ruling's own declaration and the measurement agrees with it: the act is a new ipc message on a published command's contract, andminoron@objectstack/cliis the level it earns, which is what the changeset already carried. ⛔ No package was raised to quiet a gate and ⛔ no changeset was dropped.落地前检, all three:
node scripts/pm/check-clause2-carriers.mjs --pair 17892→ exit 0, both carriers agreesuccess+ 3skipped, zero pending, zero failures; latest generation per name, ⛔ not the required subset).check_suite.completednotice arrived at 19:28:35Z; ⛔ that is not a green light on its own — this tally is, and it was taken afterwards.The two open questions the dev raised are answered in the record, both from readings, and ⛔ neither goes to the maintainer:
os devrelaying only the settle message is what the ruling asked for and the asymmetry is now stated in the docs (cli.mdx:274-276); andskipSeedDatais unreachable from anyos serve/os devboot — outside tests and comments it is set only onos migrate's planning path — so a live test for it is ⛔ not owed, while the reachable mode (multi-tenant-replay) was driven live with an ipc parent recording against the clock.⇒⚠️ that card's shape B turns out to have two call sites in that file, not one).
needs:contract-reviewoff both carriers in this stroke (「PASS ⇒ 同席同笔剥双载体」). Next: ready → merge queue → landing record → dispose. On merge this releasespackages/cli/src/commands/dev.ts, which is the last fence on #12271 (fence note5648100840;Generated by Claude Code
claude commentedon Sep 12, 2026
Landed —
ed5a1e7f23ab398f898bfa272077023184dbd2abRelease: session
session_01TSf4DV7ziu4V5j73e46b7c· cause: work landed onmain· destination: closedcompleted(by the PR's ownFixes), assignee cleared andpm:dispatchedstripped in the same write.domain:cliexecution seat (#6024), 2026-09-12T20:13Z.PR #17892 merged 2026-09-12T20:12:35Z. Contract review of record
5648081064(PASS, default judgment tier,Independence: SELF-REVIEW); provenance beside the carrier clear5648175767.Landing verified — two readings plus a control that can fail
git rev-list --parents -n 1 ed5a1e7fed5a1e7f a61ae59f) ⇒ single-parent squashgit merge-base --is-ancestor ed5a1e7f origin/main225ad588f7→ exit 1, andgit cat-file -t 225ad588f7=commit⇒ a real object, and the test can return falsefeat(cli): announce seed settlement on serve's ipc channel and forward it from os dev (#17892)Reading 2 — the content is on
main, with both controlspackages/cli/src/commands/serve.ts:617-619:seedingHasSettledreturns!snapshot || snapshot.inFlight === 0— ⛔ notpending === 0, because suppressed sources keeppendingabove zero for the life of the boot and apending-keyed message would never fire there, making its absence indistinguishable from a boot still writing.main(createSeedSettlementAnnouncer, 4 hits in that file) and the relay atdev.ts:171gates onmsg.type !== 'objectstack:seed-settled'.content/docs/deployment/cli.mdx, including the two-message table and the stated asymmetry.objectstack:seed-settled-XYZ→ 0. Live control on the same path:objectstack:listening→ 10.What this card changed, in one line for the next reader
A script that spawns⚠️
os dev(oros serve) can now wait for the boot to finish without reading the child's stdout:objectstack:seed-settledarrives on the existing ipc channel once nothing is still writing, carrying{ ok, suppressed, sources }, always afterobjectstack:listeningand exactly once.os devrelays only that message — it consumesobjectstack:listeningitself — and the docs say so.Governed Surface Guardrefused because 「the label set could NOT be read — fetch failed」, the fail-closed posture this repo chose on purpose. One re-queue (the remedy the gate names) confirmed the transient: the same guard returnedsuccesson the new group. Recorded at5648258069.5648186515): clearing the clause-② carrier re-triggersCheck Changeset, so the 「every check green」 pre-check must be re-taken after the clear. Here the flip happened 16 s after it, with that gate re-running; it came backsuccess, but 「predictable」 is not 「measured」.⭐ Unblocked by this landing
packages/cli/src/commands/dev.tsis released ⇒ #12271 loses its last fence.:307(noenv) and:775(an explicitenv: process.env) — see5648100840. ⛔ Triage's fences stand: no generalpathsgate, #11020 not re-opened, and the existing pin reading AST writes is a boundary of its property rather than a defect.Generated by Claude Code
feat(cli): announce seed settlement on serve's ipc channel and forwar…