Skip to content

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

Closed

Description

@zhuangjianguo

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 ERROR lines 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.

⚠️ Worth deciding alongside: whether ✓ 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 banner

On 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

  1. claude commented on Sep 11, 2026

    @claude
    Contributor

    维护者速读 —— 交付是一份空 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 该怎么把这件事告诉父进程」。 知情那一半已经建好了;缺的只是往外那一跳。

    ⚠️ 还有一件比卡片知道的更糟:超预算那条路径上,banner 的 Seeds: 行整行消失(实测 0 行 vs 已结算对照的 1 行),因为喂它的 recordSeedOutcome 只在加载完成时被调用(唯一调用点 app-plugin.ts:1453)。⇒ 一次「seed 还在写」的启动,打印出来的东西和「这个 app 根本没有 seed」逐字一样。读者相信「没有别的了」,不是因为疏忽,是因为眼前的 banner 用省略说了这句话。

    你要做的:选一个形状。 三个是活的:

    • D1(dev 推荐) —— 在 serve 已有的 ipc 通道上加第二条消息(挨着 objectstack:listening)。生产端已存在、已发布;os dev 本来就握着那条通道并且已经在消费一种消息。代价诚实地说:消费者 spawn 的是 os dev 而不是 serve,所以 os dev 必须往它自己的父进程再转发一次 —— 这等于把「有 ipc 通道」写进一个已发布命令的契约。
    • B(次选) —— 让 ✓ Server is ready 等到 seed 真的结算再打印。⛔ 这不是分诊禁止的「猜一个间隔」,它跟的是真实结算。代价:实测 HTTP server 比 banner 早 5.8 秒就在服务了,所以操作员会对着一个已经能用的服务器干等黑屏。
    • A —— 新加 --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 的最小是表面的 —— 它把一个已发布命令的可观察时序改掉,而那正是 ② 里说的「公开契约」。⇒ ⛔ 不建议按「改动行数」排序,按「新增了多少必须永远维护的词汇」排序。


    测量的界限,⛔ 不许当成已证

    • 颠倒顺序在本容器只复现了 1/8 次。 另外 7 次续跑落在 banner 之前,因为本机 sqlite / sqlite-wasm 驱动的续跑阻塞事件循环、饿死 banner。放大语料不能决定胜负(132 → 20132 行;把续跑拖到 9.4 秒仍然输)。⇒ 顺序是结构上未耦合,谁先到取决于续跑让不让出。⭐ 这与卡片自己的证据是同一事实的两面 —— 卡片正是因为两个容器给出不同时钟才被立。dev ⛔ 没有复现卡面那 82 秒,也没有声称复现。
    • 未测量:被抑制的 seed 源分支(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 · session session_01TSf4DV7ziu4V5j73e46b7c · 分叉交付即完整交付


    Generated by Claude Code

  2. os-tesla commented on Sep 12, 2026

    @os-tesla
    Collaborator

    Ruling recorded — D1: a second ipc message on serve's existing channel when seeding settles, forwarded by os dev to 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) / PM2 process.send('ready') / a readiness probe all take. The producer already exists and is published (SEED_SETTLEMENT_SERVICE, emitSeedSettled(true) on the detached promise, objectstack:listening on the ipc channel); what is missing is the one hop outward.

    • ⛔ B (hold the banner until seeding settles) not taken: the HTTP server is serving 5.8 s before the banner, and a shell script still cannot wait on prose.
    • ⛔ A (--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

    1. serve emits a second ipc message beside objectstack:listening when the seed settlement service reports settled (success or failure, with the summary), and os dev forwards 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.
    2. Rider, same PR: on the over-budget path the banner prints a line saying seeding continues in the background (and the 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.
    3. ⛔ Before landing, MEASURE the case the seat flagged: multi-tenant replay and skipSeedData report pending > 0 for 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.
    4. Docs: the os dev page documents the two ipc messages and the pattern for a parent that spawns os dev.

    State

    needs-user-decision → pm:queue; domain:cli / priority:p2 kept.


    Generated by Claude Code

  3. claude commented on Sep 12, 2026

    @claude
    Contributor

    Deferred, not shelved — the ruled implementation is hard-serial-fenced on THREE files across two open PRs

    domain:cli execution seat (#6024), round 22, 2026-09-12T04:52Z. The director seat's ruling at 5642795097 makes this card work again, and it sorts into this lane's selection order ahead of every 2026-09-11 p2. 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>/files at 2026-09-12T04:51Z:

    ruled work site on this tree held by status
    serve emits a second ipc message beside objectstack:listening packages/cli/src/commands/serve.ts (publishBoundPort, :4890) #17725 (serve.ts +102/−8, and serve-bound-port-publication.test.ts +58/−3 — the very test that pins the ipc publication) open, draft
    os dev forwards it to its own parent packages/cli/src/commands/dev.ts (:510, the serve --dev spawn with the ipc channel) #17725 (dev.ts +46/−3) open, draft
    rider 2: the over-budget banner says seeding continues, and the Seeds: row says pending packages/cli/src/utils/format.ts (printServerReady :865, the banner string :887) #17775 (format.ts +82/−20) open, draft

    ⇒ 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, and format.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 live needs:contract-review gate and an open scope question with the maintainer, so its merge is not near.

    What the next dispatcher must carry forward

    • ⭐ The declared file surface in this seat's earlier claim (5634883930) was WRONG and is superseded. It named packages/cli/src/commands/dev.ts; the dev relocated every site from the symbol and found the banner is emitted from packages/cli/src/utils/format.ts and fired from serve.ts — dev.ts only spawns. The relocation reading is 5636189867. ⛔ Do not re-derive the surface from the old claim.
    • The producer half already exists and is published — SEED_SETTLEMENT_SERVICE (packages/spec/src/contracts/seed-settlement.ts:110, in api-surface/contracts.json), registered by the runtime the CLI boots, and emitSeedSettled(true) already hangs off the detached promise at packages/runtime/src/app-plugin.ts:1566-:1571. ⛔ Reading packages/spec is fine; editing it is another seat's card — the ruled work is the hop outward, and nothing in it requires a spec change.
    • Clause-②: yes is already ruled on the card, so needs:contract-review is owed at dispatch and the PR does not enqueue without an at-tier review.
    • ⛔ The two prohibitions on the card still stand: never delay the ready line by a guessed interval (the signal follows the real settlement), and never fold The seed loader logs owner_id stays NULL for 120 rows whose owner_id is not NULL — the summary contradicts the table it just wrote #17177 into this.

    This card stays pm:queue with 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 when main carries both landings.


    Generated by Claude Code

  4. claude commented on Sep 12, 2026

    @claude
    Contributor

    Fence update — one of the three released, the card is still fenced. domain:cli seat (#6024), 2026-09-12T05:41Z.

    ⚠️ Corrected in place at 05:43Z: the first version of this comment lost three backticked fragments to a shell quoting fault on the way out — a lane name, a label name and this comment's own link label. The readings were never wrong, the transport ate them. Restored below.

    #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 that printServerReady moved in that landing: collectMetadataStats above it grew by ~80 lines, so ⛔ do not carry the :865 / :887 line numbers forward — locate from the symbol.

    Still held by #17725 (open, draft, needs:contract-review live, 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 its serve-bound-port-publication.test.ts is 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 5643512620 stands unchanged.


    Generated by Claude Code

  5. claude commented on Sep 12, 2026

    @claude
    Contributor

    Claim: session_01TSf4DV7ziu4V5j73e46b7c
    Branch: claude/issue-17329-seed-settled-ipc-message
    Clause-②: yes

    Dispatch — domain:cli execution seat (#6024), round 22, 2026-09-12T17:26Z, on origin/main at 89a652b8. The assignee and this Claim: 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 as 89a652b8 (serve.ts, dev.ts, and serve-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:

    1. serve emits a second ipc message beside objectstack:listening when the seed settlement service reports settled (success or failure, with the summary), and os dev forwards 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.
    2. Rider, same PR: on the over-budget path the banner prints a line saying seeding continues in the background (and the 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.
    3. ⛔ Before landing, MEASURE the case the seat flagged: multi-tenant replay and skipSeedData report pending > 0 for 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.
    4. Docs: the os dev page documents the two ipc messages and the pattern for a parent that spawns os dev.

    And the two shapes it refused, so you do not re-invent them:

    ⛔ B (hold the banner until seeding settles) not taken: the HTTP server is serving 5.8 s before the banner, and a shell script still cannot wait on prose.
    ⛔ A (--wait-for-seed + exit-code vocabulary) not taken now; it remains the fallback if a no-code consumer turns out to need it.

    ⭐ The producer half already exists and is published. The ruling's own words: 「what is missing is the one hop outward」. SEED_SETTLEMENT_SERVICE is declared in packages/spec/src/contracts/seed-settlement.ts and registered by the runtime the CLI itself boots; packages/runtime/src/app-plugin.ts already 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:

    ⇒ 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

    path what is expected
    packages/cli/src/commands/serve.ts the second ipc message, emitted beside the existing objectstack:listening announcement when the settlement service reports settled — success or failure, carrying the summary.
    packages/cli/src/commands/dev.ts forward it to os dev's own parent when one holds the channel (the existing consumer is at the msg?.type === 'objectstack:listening' branch). ⛔ A parent that does not hold an ipc channel must be unaffected.
    packages/cli/src/utils/format.ts rider 2 — printServerReady's over-budget path says seeding continues, and the Seeds: row reads pending rather than vanishing.
    tests serve-bound-port-publication.test.ts already 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/ the os dev page: both ipc messages and the parent-spawn pattern (ruled item 4). ⛔ Never content/docs/releases/.
    .changeset/ one changeset — see §5 for the level.

    ⛔ 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 and emitSeedSettled) and packages/runtime/src/seed-settlement.ts are to be read, never edited — this card is about the signal, not the seeding. ⚠️ app-plugin.ts is additionally held right now by open draft #17718, which is transferred to the domain:spec seat. Touching it would collide with another seat's PR.

    ⛔ packages/spec is 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_SERVICE lives 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 skipSeedData report pending > 0 for 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 it

    The 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).

    ⚠️ A yes and an all-patch changeset is a red Check Changeset by construction — the level axis requires at least one package whose packages/**/src/** the diff moves to be graded minor or above. A published command's ipc contract gaining a message is an additive widening ⇒ @objectstack/cli at minor. ⛔ Never drop the changeset and ⛔ never re-declare no to 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 is PREREQUISITE NOT MET — ⛔ neither a pass nor a finding.

    ⚠️ This lane's blind spot: dispatch-gates does not name pnpm lint. Always add it, as the full union (eslint . --no-inline-config, ⛔ no narrowing). Heavy runs through scripts/pm/os-verify-lock.sh; quote the wrapper's VERDICT command-exit line. 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-tree git status.

    ⚠️ Commit trailers: use the model-free pair. This repo's pre-push check:commit-card-trailers mechanically refuses a trailer carrying a model identifier — measured on #15484. Use Co-Authored-By: Claude <noreply@anthropic.com> plus Claude-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 needs app-plugin.ts or seed-settlement.ts edited 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

  6. claude commented on Sep 12, 2026

    @claude
    Contributor

    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

  7. claude commented on Sep 12, 2026

    @claude
    Contributor

    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 5648081064 on PR #17892, head 225ad588f7ac6ff238f6e770d8ab5ee59cf2d9de: ## Contract review heading, the judged head as a code span, ①②③ item by item, and the Implemented-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-② stays yes — 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, and minor on @objectstack/cli is 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:

    • ① the review record exists in the measured shape ✅
    • ② node scripts/pm/check-clause2-carriers.mjs --pair 17892 → exit 0, both carriers agree
    • ③ 35 of 35 checks green on this head at 19:29Z (32 success + 3 skipped, zero pending, zero failures; latest generation per name, ⛔ not the required subset). ⚠️ A check_suite.completed notice 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 dev relaying only the settle message is what the ruling asked for and the asymmetry is now stated in the docs (cli.mdx:274-276); and skipSeedData is unreachable from any os serve / os dev boot — outside tests and comments it is set only on os 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.

    ⇒ needs:contract-review off both carriers in this stroke (「PASS ⇒ 同席同笔剥双载体」). Next: ready → merge queue → landing record → dispose. On merge this releases packages/cli/src/commands/dev.ts, which is the last fence on #12271 (fence note 5648100840; ⚠️ that card's shape B turns out to have two call sites in that file, not one).


    Generated by Claude Code

  8. claude commented on Sep 12, 2026

    @claude
    Contributor

    Landed — ed5a1e7f23ab398f898bfa272077023184dbd2ab

    Release: session session_01TSf4DV7ziu4V5j73e46b7c · cause: work landed on main · destination: closed completed (by the PR's own Fixes), assignee cleared and pm:dispatched stripped in the same write. domain:cli execution 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 clear 5648175767.

    Landing verified — two readings plus a control that can fail

    reading result
    git rev-list --parents -n 1 ed5a1e7f 2 fields (ed5a1e7f a61ae59f) ⇒ single-parent squash
    git merge-base --is-ancestor ed5a1e7f origin/main exit 0
    negative control the same test on the pre-merge head 225ad588f7 → exit 1, and git cat-file -t 225ad588f7 = commit ⇒ a real object, and the test can return false
    subject feat(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 controls

    • The keying, which is this delivery's whole argument, at packages/cli/src/commands/serve.ts:617-619: seedingHasSettled returns !snapshot || snapshot.inFlight === 0 — ⛔ not pending === 0, because suppressed sources keep pending above zero for the life of the boot and a pending-keyed message would never fire there, making its absence indistinguishable from a boot still writing.
    • The latch is on main (createSeedSettlementAnnouncer, 4 hits in that file) and the relay at dev.ts:171 gates on msg.type !== 'objectstack:seed-settled'.
    • The docs carry the message 4 times in content/docs/deployment/cli.mdx, including the two-message table and the stated asymmetry.
    • Fabricated control: 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 (or os serve) can now wait for the boot to finish without reading the child's stdout: objectstack:seed-settled arrives on the existing ipc channel once nothing is still writing, carrying { ok, suppressed, sources }, always after objectstack:listening and exactly once. ⚠️ os dev relays only that message — it consumes objectstack:listening itself — and the docs say so.

    ⚠️ Two things about this landing worth carrying forward

    1. The queue ejected it once at 19:42:53Z and it was NOT this PR's failure — Governed Surface Guard refused 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 returned success on the new group. Recorded at 5648258069.
    2. ⛔ A sequencing slip of this seat's, corrected for next time (5648186515): clearing the clause-② carrier re-triggers Check 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 back success, but 「predictable」 is not 「measured」.

    ⭐ Unblocked by this landing

    packages/cli/src/commands/dev.ts is released ⇒ #12271 loses its last fence. ⚠️ Its shape B has two call sites in that file, not the one the card quotes — :307 (no env) and :775 (an explicit env: process.env) — see 5648100840. ⛔ Triage's fences stand: no general paths gate, #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

  9. added a commit that references this issue on Sep 17, 2026
    ed5a1e7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpriority:p2Medium: important, M3

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions