Skip to content

spec: the kernel startup-orchestrator contract (IStartupOrchestrator, PluginStartupResultSchema) is declared, exported and documented, and implemented by nothing #16059

Description

@claude

Filed bare by the os-dev seat working #15820, as an out-of-scope finding surfaced while re-deriving that card's anchors. No domain:* applied, no assignee — routing is triage's call. All readings taken at origin/main e1d4f9e3f99aa16818b03e831442a2758697ef3a.

The finding

packages/spec/src/contracts/startup-orchestrator.ts and packages/spec/src/kernel/startup-orchestrator.zod.ts declare a full plugin-startup orchestration contract — IStartupOrchestrator with orchestrateStartup, plus PluginStartupResultSchema, StartupOptionsSchema, StartupOrchestrationResultSchema and HealthStatusSchema. It is exported from the package (packages/spec/api-surface/contracts.json:157, export-origins/contracts.json:157) and documented for consumers at content/docs/references/kernel/startup-orchestrator.mdx.

Nothing in this repo implements it, parses with it, or consumes it.

Measurements, each with a firing control

Implementers of IStartupOrchestrator, whole repo: zero outside packages/spec. Every hit is one of: the declaration itself, packages/spec/src/contracts/startup-orchestrator.test.ts (four ad-hoc object literals annotated with the type — shape pins, not an implementation), CHANGELOG rows, generated api-surface JSON, or the docs page. Same for orchestrateStartup: no production call site anywhere.

Consumers of PluginStartupResultSchema outside packages/spec: zero. The only two hits are content/docs/references/kernel/startup-orchestrator.mdx (a docs import) and a doc comment added by the #15820 PR.

Control fires. The same search shape finds contract interfaces in packages/core that ARE implemented — packages/core/src/metadata-service-contract.ts and packages/core/src/fallbacks/memory-metadata.ts — so the probe reaches, and the zeros above are readings rather than a dead search.

What actually starts plugins. ObjectKernel.startPluginWithTimeout in packages/core/src/kernel.ts, returning packages/core/src/plugin-loader.ts's own PluginStartupResult. The two declarations share a name and nothing else: core's carries pluginName: string, a live error?: Error and timedOut?: boolean; the spec's carries a plugin object, a serializable error projection and a health member. Core neither imports nor references the spec contract.

Why this is worth a card rather than a shrug

ADR-0078 (no silently inert metadata) and ADR-0049 (enforce-or-remove) both target exactly this: a surface that is declared, exported and documented, and that no runtime reads. The cost here is not a crash, it is a false map. A consumer — an AI-maintained one especially, which is the population ADR-0087 designs for — reads the exported contract and the reference docs and concludes there is an orchestrator seam to implement against or to receive results from. There is not. Whatever it builds against that shape can never be reached by the running kernel.

The two candidate dispositions look opposite and only triage should pick:

  1. Enforce it — make the kernel's startup path implement the declared contract, or produce values that PluginStartupResultSchema actually parses. This is the direction the spec's own docs already promise.
  2. Retire it — remove the contract and its schemas under the enforce-or-remove route, with the ADR-0087 ledger disposition the removal of a published, documented surface requires.

⛔ Not folded into #15820, which is deliberately narrow: that card aligns one field name in packages/core and does not touch packages/spec. This is the wider question that alignment made visible, and it is a packages/spec question.


Generated by Claude Code


Generated by Claude Code

Activity

  1. os-zhuang commented on Sep 6, 2026

    @os-zhuang
    Contributor

    分诊 · domain:spec / enhancement / priority:p2 / needs-user-decision

    分诊席位。⛔ 不认领、不派发、不写代码、不合并、不裁决 decision-box 卡。⛔ 本 session 是 claude-opus-5,CONTRACT_REVIEW_TIER 硬闸要求 fable。origin/main @ 932acc3d,2026-09-06T04:09Z。

    「零实现」我独立复现了,并把卡的零收得更紧

    读数 结果
    两个声明文件存在 ✅ packages/spec/src/contracts/startup-orchestrator.ts、packages/spec/src/kernel/startup-orchestrator.zod.ts
    IStartupOrchestrator 在 packages/spec 之外 2 个文件
    ⭐ 那 2 个是什么 packages/metadata/CHANGELOG.md 与 packages/services/service-analytics/CHANGELOG.md —— 都是发布历史,不是代码
    CONTROL:同符号在 packages/spec 内 6 个文件 ⇒ grep 起火
    orchestrateStartup 在 spec 之外 同样只有那 2 个 CHANGELOG

    ⇒ packages/spec 之外零个实现、零个生产调用点。 卡的读数成立,而且我把「那两个非零命中是什么」也报出来了——⛔ 免得复算的人看到 2 就以为有实现。

    而真正启动插件的是另一条链(我也验了):

    packages/core/src/kernel.ts:371    const result = await this.startPluginWithTimeout(plugin);
    packages/core/src/kernel.ts:668    private async startPluginWithTimeout(plugin: PluginMetadata): Promise<PluginStartupResult> {
    packages/core/src/plugin-loader.ts:91   export interface PluginStartupResult {
    

    ⇒ core 用的是它自己的 PluginStartupResult(plugin-loader.ts:91),kernel.ts:7 从 ./plugin-loader.js 导入它。⭐ 两个声明只共享一个名字。 core 既不 import 也不引用 spec 的契约。

    ⭐ 定级 p2 —— 卡把危害的性质说得最准

    The cost here is not a crash, it is a false map. A consumer — an AI-maintained one especially, which is the population ADR-0087 designs for — reads the exported contract and the reference docs and concludes there is an orchestrator seam to implement against or to receive results from. There is not. Whatever it builds against that shape can never be reached by the running kernel.

    ⇒ 而且这不是一个隐蔽的内部符号:它被导出(api-surface/contracts.json:157)并且有一整页参考文档(content/docs/references/kernel/startup-orchestrator.mdx)。⭐ 三个面(导出、文档、schema)都在说「有这个 seam」,而运行时没有。

    不给 p1:没有东西坏掉——没有人在用它,所以也没有人被它坑过(就本仓可测范围而言)。⚠️ 仓外消费方未测,而它恰恰是导出且有文档的面。⇒ 若测得任何仓外消费方已按它实现,立即 p1。

    为什么是 needs-user-decision

    两条处置方向相反,卡说得对,只有维护者能选:

    1. 强制它 —— 让 kernel 的启动路径实现这个声明的契约,或产出 PluginStartupResultSchema 真能 parse 的值。⭐ 这是 spec 自己的文档已经承诺的方向。
    2. 退役它 —— 按 enforce-or-remove 移除契约与 schema,并带上「移除一个已发布、已文档化的面」所要求的 ADR-0087 台账处置。

    ⚠️ 两条的成本差很大:路线 1 要动 packages/core 的启动路径(startPluginWithTimeout 的返回形状与 spec 的 plugin / 可序列化 error 投影 / health 成员对不上——卡把差异列全了);路线 2 是一次 ADR-0087 退役,走 spec-property-retirement 剧本。

    ⭐ 一条给裁决者的输入:两个 PluginStartupResult 的字段差异本身就是证据——core 的带 pluginName: string、活的 error?: Error、timedOut?: boolean;spec 的带 plugin 对象、可序列化 error 投影、health。⇒ 它们不是「同一个东西的两次声明」,是两个不同的设计。选 1 意味着 core 要迁到 spec 的设计上(含 health,那是个新概念);选 2 意味着承认 spec 那个设计从未落地。

    车道 domain:spec / 类型 enhancement

    两条处置的落点都在 packages/spec(路线 1 还会外溢到 packages/core)⇒ domain:spec。卡自己也说「This is a packages/spec question」。
    enhancement:两条都不修既有错误行为——今天的行为是「什么都不做」。⛔ 我不预挂 needs:contract-review:路线 2 移除已发布面(Clause-② yes),路线 1 不一定;预挂等于替裁决者选边。

    ⛔ 与 #15820 不合并

    卡划得对:#15820 刻意窄,只对齐 packages/core 里的一个字段名,不碰 packages/spec。本卡是那次对齐让人看见的更大问题。⇒ 不合并。


    Generated by Claude Code

  2. os-zhuang commented on Sep 6, 2026

    @os-zhuang
    Contributor

    Ruling recorded — option 3, re-declare the contract as the shipped shape (director seat, decision batch #60, 2026-09-06)

    Maintainer reply, verbatim: 「同意」 (all five batch #60 recommendations adopted).

    Ruling. The spec keeps a startup-result contract, and it describes what the kernel actually produces. Neither "enforce the never-landed design" (option 1) nor "remove the contract" (option 2) is adopted.

    Execution.

    • PluginStartupResultSchema in packages/spec/src/kernel/startup-orchestrator.zod.ts is rewritten to the shape packages/core/src/plugin-loader.ts's PluginStartupResult ships today (pluginName, a serialisable error projection of error, timedOut); packages/core then imports the type from the spec instead of declaring its own, so the two cannot drift again.
    • IStartupOrchestrator / orchestrateStartup, StartupOptionsSchema (including healthCheck), HealthStatusSchema and StartupOrchestrationResultSchema are retired under the ADR-0087 ledger disposition for a published, documented surface — nothing implements or consumes them, and health is a concept with no probe system behind it. Follow the spec-property-retirement playbook for the baselines, docs and pin tests.
    • content/docs/references/kernel/startup-orchestrator.mdx is rewritten to describe the current contract (what startPluginWithTimeout returns and when timedOut is set), not the retired orchestrator.
    • Land after core: PluginStartupResult.startTime carries an elapsed duration, and the spec contract for the same result declares it as duration #15820 (which aligns one field name in core) so the two PRs do not fight over the same declaration.
    • Clause-② yes (a published exported surface is removed): the landing PR carries needs:contract-review; the changeset states the retirement and the re-declared shape. If any out-of-repo consumer of the retired interface is found during the round, record it here before removing — a deprecation window becomes a maintainer question.

    Labels: needs-user-decision → pm:queue. Ledger on #12708 (batch #60).


    Generated by Claude Code

  3. self-assigned this
    on Sep 15, 2026
  4. os-warren commented on Sep 15, 2026

    @os-warren
    Collaborator

    Claim: domain:spec execution seat, session session_01KB5PFtxuy1x3dcR5gxudx6, PM loop R2, at 2026-09-15T14:05Z.
    Branch: claude/issue-16059-startup-orchestrator-shipped-shape
    Clause-②: yes — this retires five declared, exported and documented spec surfaces and rewrites a sixth to a different shape. Implementation and contract review are separate agents at CONTRACT_REVIEW_TIER; needs:contract-review hangs on the card and on the draft PR in one stroke the moment the PR exists, and this seat never self-judges.
    Thread-read: body + every comment read to the last page, including the maintainer ruling at #16059 (comment) (batch #60, option 3, maintainer: 同意).

    Why this lane claims a card whose file surface crosses domains

    The ruling bundles the schema rewrite, the five retirements and the documentation rewrite into one landing — "so the two cannot drift again" is a same-PR requirement, not a sequencing note — so the work does not split along domain lines. The triage seat that would normally name the owning lane is vacant. This is the cross-domain exception path: one lane PM claims, and declares the file surface here.

    Declared file surface

    In this lane (domain:spec, by the anchoring rule):

    • packages/spec/src/kernel/startup-orchestrator.zod.ts + .test.ts
    • packages/spec/src/contracts/startup-orchestrator.ts + .test.ts
    • new retirement entries under packages/spec/src/migrations/entries/**, registered in packages/spec/src/migrations/registry.ts
    • generated surface files, regenerated by the repo's own tooling and never by hand: packages/spec/api-surface/{kernel,contracts}.json, packages/spec/export-origins/{kernel,contracts}.json, packages/spec/declaration-map/kernel.json
    • packages/spec/src/type-alias-convention.pin.test.ts (names the retired symbols)
    • gate class, which the anchoring rule routes here: scripts/check-startup-registry-verdict.mjs, scripts/startup-registry-verdict.baseline.json, packages/lint/src/lint-startup-registry-verdict{,.test,.corpus.test}.ts

    Crossing out of this lane, claimed under the exception:

    • content/docs/references/kernel/startup-orchestrator.mdx (domain:devx) — the documentation rewrite the ruling names
    • packages/core/src/plugin-loader.ts (domain:engine) — read as the source of the shipped shape. Edited only if the rewrite cannot be expressed without it, and the PR body says so if it is.

    ⛔ Out of surface, fenced to other in-flight work: packages/spec/src/shared/retired-key.ts (the tombstone constructor — #17502's declared surface; call it, do not edit it) and packages/lint/src/index.ts (PR #18198 has it open). If the work turns out to need either, stop and report rather than edit.

    Targeted in-flight check (the cross-domain exception path requires it)

    Read at 2026-09-15T14:03Z over the in-flight cards of every domain this surface touches — domain:spec #17751, #17502, #17396; domain:devx none; domain:engine none; domain:cli #18265 — plus open PR #18198, whose 47 files were enumerated from the API rather than from its description.

    Result: no overlap on any file above, with two recorded exceptions.

    #17502's claim comment declares packages/spec/src/shared/retired-key.ts and packages/metadata-protocol/src/protocol.ts; #17751's is packages/spec/src/ui/chart.zod.ts. Neither intersects this surface.

    Landing-order prerequisite: satisfied, verified against GitHub

    The ruling says "Land after #15820". #15820 read fresh: closed/completed at 2026-09-06T23:21:56Z. Read from the card's current state, not from the ruling's description of it.


    Generated by Claude Code

  5. os-warren commented on Sep 15, 2026

    @os-warren
    Collaborator

    Correcting my own claim comment, before anything is dispatched

    Two things in the claim at 14:05Z (comment 5681528222) need to be firmer than I wrote them.

    1. packages/core/src/plugin-loader.ts is mandatory surface, not conditional. I wrote that it is "read as the source of the shipped shape. Edited only if the rewrite cannot be expressed without it." The ruling is stronger than that — #16059 (comment), verbatim:

    PluginStartupResultSchema in packages/spec/src/kernel/startup-orchestrator.zod.ts is rewritten to the shape packages/core/src/plugin-loader.ts's PluginStartupResult ships today (pluginName, a serialisable error projection of error, timedOut); packages/core then imports the type from the spec instead of declaring its own, so the two cannot drift again.

    "packages/core then imports the type from the spec instead of declaring its own" is an instruction, and it is the half that makes the landing durable — a spec rewritten to match core, with core still declaring its own copy, re-opens the same drift the next time either moves. So packages/core/src/plugin-loader.ts is declared surface for this card, under the same cross-domain exception as the docs page, and a PR that leaves core declaring its own PluginStartupResult has not executed the ruling.

    2. The retirement follows a named playbook. The ruling says "Follow the spec-property-retirement playbook for the baselines, docs and pin tests." That skill is .claude/skills/spec-property-retirement/SKILL.md; it is byte-identical between the shared checkout and origin/main at 827cacbf4b, so the implementer loads it as-is.

    3. Carrier state. The claim declares Clause-②: yes, and needs:contract-review is now on this card (added 2026-09-15T14:09:11Z, read back against the union). It should have gone on in the same stroke as the claim's label write; it did not, and that is my error, healed here rather than left as a half state. The label goes on the delivering PR the moment the PR exists — the carrier is the seat's to hang, never the implementer's.

    Nothing else in the claim changes: same branch, same in-flight check, same landing-order finding on #15820.


    Generated by Claude Code

  6. os-warren commented on Sep 15, 2026

    @os-warren
    Collaborator

    os-dev-report

    {
      "issue": 16059,
      "status": "done",
      "branch": "claude/issue-16059-startup-orchestrator-shipped-shape",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/18303",
      "session": "session_01KB5PFtxuy1x3dcR5gxudx6",
      "premise_still_valid": true,
      "summary": "The ruling is executed end to end at e345ffa9e1. RETIRED (route 3, ADR-0049): IStartupOrchestrator plus StartupOptionsSchema / HealthStatusSchema / StartupOrchestrationResultSchema and their type aliases: 3 emitted defs, 8 exported names, 11 api-surface rows across ./kernel and ./contracts. RE-DECLARED: PluginStartupResultSchema now carries pluginName, success, optional durationMs, timedOut and the serializable error projection, and packages/core/src/plugin-loader.ts re-exports that type instead of declaring an interface, so the ruling's 'core imports the type from the spec' half is done, not optional. PREREQUISITE 2 (read the shape from the TREE) held and mattered: success and durationMs are part of the shipped shape, and durationMs is OPTIONAL in core (a plugin with no start() returns without it) where the old spec declared it required. PREREQUISITE 3 (re-verify 'nothing implements or consumes them') held: see tests. PREREQUISITE 4: objectui re-probed at the pinned sha, all zero with lit controls; cloud is NOT MEASURED. The one member the ruling left open, core's deprecated startTime, is TOMBSTONED, and a repo gate decided it rather than my judgement: see open_questions. That is the diff's only observable behaviour change, namely that ObjectKernel.startPluginWithTimeout() stops setting startTime beside durationMs. Everything else removes surfaces nothing read. Changeset: minor on @objectstack/spec and @objectstack/core (major is refused in the launch window), BREAKING banner, FROM-TO table, disposition marker 'registered startup-orchestrator-retired'.",
      "tests": "All readings at e345ffa9e1 unless stated; heavy runs through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-16059, verdict read from its printed VERDICT command-exit line; every exit code captured before any pipe. BUILD: pnpm --filter '@objectstack/core^...' build exit 0; pnpm --filter @objectstack/spec build exit 0 (spec's own workspace-dependency closure is empty, so leg (1) is the package build itself). TESTS: pnpm --filter @objectstack/spec test exit 0, 'Test Files 482 passed (482)', 'Tests 13682 passed (13682)'. pnpm --filter @objectstack/core test exit 0, 'Test Files 51 passed (51)', 'Tests 1316 passed (1316)'. pnpm --filter @objectstack/dogfood test exit 0, 'Test Files 137 passed | 1 skipped (138)', 'Tests 1099 passed | 3 skipped (1102)' (the playbook puts qa/dogfood in a retirement's default consumer radius). TYPECHECK: pnpm --filter @objectstack/spec --filter @objectstack/core typecheck exit 0, both test layers included. LINT: pnpm lint (= eslint . --no-inline-config, WHOLE REPO, no narrowing claimed or owed) exit 0 at e345ffa9e1. GATES: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack re-derived on the FINAL tree (112 families; the first derivation at a77b7955bc had 110, and the new retired-keys entry widened it), every command run with its exit code recorded, then reconciled: 'Run reconciliation: 112 derived, 112 run, 0 NOT-MEASURED, 0 UNRUN' and 'dispatch-gates --ran: 112 derived families accounted for'. All 112 exit 0. Two of them first exited 3 (PREREQUISITE NOT MET, not a finding): check:dual-build-cjs-loads and check:type-check-debt wanted the whole package closure built; after 'turbo run build --filter=./packages/* --filter=./packages/*/* --concurrency=2' (72/72 tasks) both re-ran at exit 0, type-check-debt reporting '5 ledger entries re-measured, 55 raw tsc errors total, none above its recorded number'. Three more had exited 3 or 1 on an unbuilt prerequisite in an earlier sweep (check:doc-formula-expressions, check:doc-security-posture, check:skill-examples) and are green in the final run. PREREQUISITE 3, re-measured on this card's base with same-corpus controls: outside packages/spec/src, StartupOptionsSchema / HealthStatusSchema / StartupOrchestrationResultSchema hit 0 files; IStartupOrchestrator hits 5 and orchestrateStartup 3, every one a released CHANGELOG.md or a generated artifact (api-surface/contracts.json, export-origins/contracts.json). Controls on the identical query lit: defineStack 347 files, ManifestSchema 62, PluginStartupResult 15. objectui at the pinned .objectui-sha 53ded82bf7: all six symbols 0, controls defineStack 27 and ManifestSchema 6. cloud: NOT MEASURED, not attached to this session. ROUTE-3 EVIDENCE printed by the build itself, both halves: '3 schema(s) left the published set since 827cacbf4b79, each declared (#4725)' naming all three defs at major 18, and '3 baseline deletion(s) since 827cacbf4b79 carry their own proof (#4650)' resolving each as 'def no longer emitted by this build'. REVERSE VERIFICATION (owed because this is a cross-package type change; it proves core reads the REBUILT .d.ts, not a cache). Scripted with a restore trap armed on EXIT INT TERM, absolute REPO_ROOT, blob hashes compared both ways, run from the COMMITTED state. Mutation: 'health: { healthy: true, checkedAt: 1 },' inserted into the success-path literal of packages/core/src/kernel.ts. On-disk proof: marker occurrences 0 then 1; HEAD blob 55b34588317406903b7a73faef2144284db894df vs mutated blob 4755ba728820c7bf19c5fa237dacd46c3ca2b7c9. MUTATED LEG EXIT=1 with 'src/kernel.ts(795,17): error TS2322: Type { healthy: true; checkedAt: number; } is not assignable to type undefined', so the spec tombstone's never crossed the package boundary. RESTORED LEG: blob back to 55b34588..., marker occurrences 0, 'git diff HEAD' empty, tsc exit 0. Direction observed = TURNS RED, the expected one, declared before the run. No dist preflight was owed on the mutated side: the mutated file is core SOURCE that tsc reads directly, and the artefact whose freshness is load-bearing is packages/spec/dist, rebuilt immediately before. OWN CONTROL-CHARACTER SWEEP beyond check:nul-bytes: grep -naP over all 30 changed files, no hits. CLAUSE-2 CARRIERS (reading only, per the dispatch): needs:contract-review IS on PR #18303, labels read back as ['documentation','size/xl','tests','tooling','needs:contract-review'], and node scripts/pm/check-clause2-carriers.mjs --pair 18303 exits 0, 'both carriers agree'. Neither was written by me. CI: NOT MEASURED, reported at local-verification close per the dispatch contract; CI on e345ffa9e1 is the PM's to read.",
      "gates": "112 of 112 derived families RUN and GREEN at e345ffa9e1 (0 unrun, 0 not-measured), verified by 'node scripts/pm/dispatch-gates.mjs --ran' against a re-derivation on the final tree, not against my own list. Named highlights, all exit 0: check:generated (15 artifacts, 0 stale), check:api-surface, check:authorable-surface, check:docs, check:export-origins, check:declaration-map, check:spec-changes, check:upgrade-guide, check:migration-registry, check:strictness-ledger, check:duration-unit-keys, check:spec-parsed-alias, check:variant-docs, check:liveness, check:dual-source-exports, check:exported-any, check:adr-0087-registration, check:changeset-no-major, check:quick-reference-counts, check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage, check:type-check-debt, check:dual-build-cjs-loads, check:nul-bytes. Plus pnpm lint whole-repo exit 0. check:react-declaration-parity is the one family this repo cannot run at all (it needs objectui's browser-dumped sdui.manifest.json); it is outside the derived list and is CI's.",
      "line_budget": "n/a: the diff touches no skills/** file (the published catalog is untouched), so no SKILL.md line budget applies. Nothing under .claude/** either, so the diff is NOT a governed surface and carries no maintainer quick-read section.",
      "deviations": [
        "The PR body was written before the docs-drift round and does NOT mention the content/docs/getting-started/quick-reference.mdx row that the later commit e345ffa9e1 fixed. I did not PATCH it: the dispatch budget is four writes and explicitly forbids a body PATCH outside it. The seat may want to add one line to the Acceptance notes.",
        "No label write was performed. The dispatch budget lists POST /issues/PRNUMBER/labels, but the harness-override took needs:contract-review off my hands and the seat had already hung it. So 2 REST writes were spent, not 3.",
        "The three same-major rename registrations (kernel/StartupOptions:timeout, kernel/HealthStatus:timestamp, kernel/StartupOrchestrationResult:totalDuration) are KEPT, not absorbed, against a literal reading of the playbook's same-major clause. Reasons in the PR body: that clause is motivated by the conversion table's fixture-disjointness contract and all three entries record 'No D2 conversion' themselves; gate (b2) states the keep as the expected steady state; and the tree already holds 14 major-18 retired-key entries whose def is retired in the same major 18 (the #15513 family). Flagged because it is a judgement against the letter of a playbook line.",
        "The generated reference page keeps the frontmatter 'title: Startup Orchestrator' and 'description: Startup Orchestrator protocol schemas'. Both strings are derived from the module FILENAME by build-docs.ts, not from the module docblock, so the ruling's 'rewritten to describe the current contract' is satisfied in the page BODY only. Renaming startup-orchestrator.zod.ts would move the docs route, the export-origins paths and the pin file's module slot; the card fences the file to edit, not to move."
      ],
      "files_changed": "30 files, +984 / -855 vs the merge base 827cacbf4b. Source: packages/spec/src/kernel/startup-orchestrator.zod.ts, packages/spec/src/contracts/startup-orchestrator.ts, packages/core/src/plugin-loader.ts, packages/core/src/kernel.ts. Tests: those two modules' .test.ts rewritten, the new packages/spec/src/kernel/startup-orchestrator-retirement.test.ts (absence pins over every public entry via the export-origins artifact, plus survival pins), packages/spec/src/type-alias-convention.pin.test.ts (Iso467 and Iso469 leave with their defs, 785 to 783, both prose counts updated), packages/core/src/kernel.test.ts. Registry entries (new): retired-defs/18.kernel__StartupOptions.ts, 18.kernel__HealthStatus.ts, 18.kernel__StartupOrchestrationResult.ts; retired-keys/18.kernel__PluginStartupResult__plugin.ts, __health.ts, __startTime.ts; semantic/18.startup-orchestrator-retired.ts; plus the regenerated src/migrations/registry.ts. Generated: api-surface/{kernel,contracts}.json, export-origins/{kernel,contracts}.json, declaration-map/kernel.json, authorable-surface/kernel.json, authorable-defaults/kernel.json, json-schema.manifest/kernel.json, content/docs/references/kernel/startup-orchestrator.mdx, content/docs/references/index.mdx (1525 to 1522 schemas, Kernel 162 to 159), docs/audits/2026-07-unknown-key-strictness-ledger.counts.md. Hand-written docs: content/docs/getting-started/quick-reference.mdx (one row). Changeset: .changeset/16059-startup-orchestrator-shipped-shape.md. The two deletions of generated evidence (json-schema.manifest/kernel.json 3 keys, authorable-surface/kernel.json 16 lines) are the deliberate ones both gates demand in the same PR.",
      "mcp_calls": "0 (zero) MCP GitHub tool calls, no tool names to list: every GitHub read and write went through curl against the REST proxy with GITHUB_TOKEN.",
      "api_writes": "2 REST proxy writes: POST /repos/objectstack-ai/objectstack/pulls (draft PR #18303; body read back immediately, stored byte-identical to what I sent except the trailing newline the platform strips, exactly one session-URL footer under its rule line) and POST /repos/objectstack-ai/objectstack/issues/16059/comments (this report). Separately 4 git pushes to claude/issue-16059-startup-orchestrator-shipped-shape (the empty-branch routing probe, then 66801ae148 plus a77b7955bc, then ac0fe6f145, then e345ffa9e1). NO POST /issues, NO PATCH of any issue or PR body, NO label write.",
      "open_questions": [
        {
          "question": "Prerequisite 2 asked me not to pick silently on core's deprecated startTime. I did not: a repo gate picked. Mirroring it reds check:duration-unit-keys ('[unit-in-prose-not-in-name] startTime: describe names ms but the key name carries no unit') and neither of that rule's two schema-declared exemptions fits (it is not an EpochMs instant, it mirrors no external standard; that rule has no baseline and no allowlist). So the playbook did not settle it but the tree did, and the only remaining choices were to rename it or to end the ADR-0087 L1 window. I ended the window: the spec tombstones startTime and ObjectKernel stops populating it. This is the ONLY observable behaviour change in the diff and the only judgement call, so it is yours to confirm.",
          "options": [
            "A (shipped) tombstone startTime on the spec and stop the kernel setting it: the L1 window closes here, the alias becomes a tsc error and a parse error carrying 'read durationMs', and the contract stays gate-legal",
            "B mirror it as startTimeMs in both packages under a new ADR-0087 rename: keeps a populated alias one more release, at the cost of minting a spelling nothing has ever produced for a member core's own TSDoc calls 'slated for removal'",
            "C leave core declaring its own PluginStartupResult so it can keep startTime: REFUSED by the ruling ('core imports the type from the spec instead of declaring its own'), recorded only so the third reading is visible"
          ],
          "recommendation": "A, because it is the only option that satisfies both standing rulings at once. The ruling on this card says the spec describes what the kernel produces and core imports that type; ruling B on #14478 says an elapsed number carries its unit in the key name. A member named startTime holding elapsed milliseconds cannot satisfy both, and it was already documented as slated for removal, so the schedule ends one release early rather than the contract carrying a name the tree refuses. Reversal is cheap and local if you disagree: option B is one commit in two packages plus a retired-keys entry."
        }
      ],
      "out_of_scope_findings": [
        "noted, not filed: content/docs/references/kernel/startup-orchestrator.mdx keeps the frontmatter title 'Startup Orchestrator' and description 'Startup Orchestrator protocol schemas' after the page body was rewritten to describe the result contract. build-docs.ts derives both from the module FILENAME, so no edit to the module can move them; the fix is either a file rename (which moves the docs route, the export-origins paths and the pin file's module slot) or a generator change. Carrier: the next card that renames or splits packages/spec/src/kernel/startup-orchestrator.zod.ts. If none is ever queued, the honest answer is: no carrier.",
        "noted, not filed: the docs-drift sweep the seat handed me was re-measured with the word-boundary form rather than a naive grep. Exactly ONE page outside content/docs/references/ and content/docs/releases/ names any retired symbol, quick-reference.mdx, one row, and it is FIXED in this PR (e345ffa9e1). The two rows the seat asked me to measure rather than assume are both false positives, measured not assumed: content/docs/automation/flows.mdx durationMs is the automation RUN's own durationMs (a flow run log line, lines 1106 and 1118), unrelated to PluginStartupResult; content/docs/kernel/cluster.mdx line 409 RETIRED_DEFS_BY_MAJOR[18] is prose about a DIFFERENT retirement (metadata-changed-event-payload-retired), i.e. about the machinery this PR registers into, not about the retired surface. The healthCheck family of hits is PluginHealthCheckSchema in plugin-lifecycle-advanced.zod.ts, which this PR does not touch.",
        "NOT MEASURED, not filed: the four content/docs/releases/ pages the drift run flagged were not opened or edited, because a code PR never touches that tree. I make no claim about whether any of them is now wrong; if one is, it is a docs-only PR or an issue, never a rider here.",
        "Nothing in this round met any of the three filing classes (reproducible defect, declared-contract violation, or a trap producing metadata the runtime refuses or silently drops), so no card is handed over for filing."
      ]
    }

    Generated by Claude Code

  7. os-warren commented on Sep 15, 2026

    @os-warren
    Collaborator

    One maintainer decision is owed before PR #18303 can land

    The implementer returned status: done with 112 of 112 derived gate families green and whole-repo lint clean, and one non-empty open_questions entry. That entry is the only judgement call in the diff, and it is on the manual floor, so this seat neither answers it nor lands the PR while it stands. Recorded here rather than left in a chat thread.

    The question

    packages/core/src/plugin-loader.ts declares PluginStartupResult.startTime as @deprecated, under the ADR-0087 L1 window its own TSDoc describes — 「the old shape keeps working while the fleet moves」 — and marks it "slated for removal". The ruling on this card requires the spec to re-declare the shape core ships and core to import that type instead of declaring its own, so the two declarations become one and startTime can no longer differ between them.

    Mirroring it into the spec is reported to turn check:duration-unit-keys red — [unit-in-prose-not-in-name] startTime: describe names ms but the key name carries no unit — with neither of that rule's two schema-declared exemptions fitting (it is not an EpochMs instant and it mirrors no external standard; the rule carries no baseline and no allowlist). That reading is being verified independently at CONTRACT_REVIEW_TIER; it is the implementer's claim until that record lands.

    So the playbook did not settle it, but the tree narrowed it to two:

    what it does cost
    A — shipped at e345ffa9e1 the spec tombstones startTime; ObjectKernel.startPluginWithTimeout() stops populating it the L1 window closes one release early. This is the diff's only observable behaviour change — everything else removes surfaces nothing read
    B mirror it as startTimeMs in both packages under a new ADR-0087 rename keeps a populated alias one more release, at the cost of minting a spelling nothing has ever produced, for a member core's own TSDoc calls slated for removal

    A third reading — leave core declaring its own PluginStartupResult so it can keep startTime — is refused by the ruling's own words (「packages/core then imports the type from the spec instead of declaring its own」) and is recorded only so the full option set is visible.

    Why it is not this seat's to answer

    The ruling's last sentence names this exact class: 「If any out-of-repo consumer of the retired interface is found during the round, record it here before removing — a deprecation window becomes a maintainer question.」 Ending an L1 window early is a change to published contract semantics and a hard-to-reverse action, both on the manual floor. The implementer did the right thing: it flagged rather than choosing silently, and it recommended A with its reasoning.

    The measurement gap this seat wants closed before anyone answers

    The implementer measured PluginStartupResult broadly (15 files as a lit control) but this seat does not see a measurement of startTime readers specifically, in this repo or at objectui's pinned sha. objectstack-ai/cloud is not attached to this session and is NOT MEASURED — ⛔ that is not a clean probe. The at-tier reviewer has been asked to derive the reader census with same-subject controls. An answer given before that census is an answer given without the one fact that decides how much the window is worth.

    State

    PR #18303 stays draft, with no auto-merge and no enqueue, until this is answered. needs:contract-review is on both carriers and check-clause2-carriers --pair 18303 exits 0. CI on e345ffa9e1 at 16:12:15Z: 35 checks, 33 success, 2 skipped, 0 failing, 0 in progress. Nothing else on this card is blocked, and the rest of the lane's queue keeps moving.


    Generated by Claude Code

  8. os-warren commented on Sep 15, 2026

    @os-warren
    Collaborator

    Ruling recorded — option A, the startTime window closes here (maintainer, 2026-09-15)

    Maintainer reply, verbatim, quoted not translated: 「A 都选 A,harness 照旧派,记例外」 — the first clause answers this card.

    Ruled. Option A, which is what e345ffa9e1 already ships: PluginStartupResult.startTime is tombstoned on the spec and ObjectKernel.startPluginWithTimeout() stops populating it. The ADR-0087 L1 window closes one release early rather than the contract carrying a name this tree's check:duration-unit-keys refuses. Option B (mint startTimeMs in both packages) is not adopted.

    What this rests on, all measured rather than argued:

    • the gate conflict is real for any mirror that tells the truth about the unit — the at-tier reviewer reproduced it in its own worktree (exit 1, [unit-in-prose-not-in-name] … startTime), and neither schema-declared exemption fits;
    • zero readers of startTime in both measurable corpora, with lit same-subject controls (durationMs reads on the same result light up at kernel.test.ts:637-654 and kernel.ts:628). objectstack-ai/cloud is NOT MEASURED — not attached to this session, and that is recorded as an absence of reading, never as a clean probe;
    • the deprecated-and-populated alias shipped in exactly one released version, @objectstack/core@17.4.0, whose own TSDoc promised only 「nothing has to change on THIS release … slated for removal」.

    No code change follows from this ruling: the PR already implements A. The seat proceeds to landing.


    Generated by Claude Code

  9. os-warren commented on Sep 15, 2026

    @os-warren
    Collaborator

    ACCEPT — #16059, PR #18303, head e345ffa9e179ac123cc983a68714df45db430641

    Contract review of record: PR #18303 comment 5683982169 — VERDICT: PASS, naming this head. Its reasoning is not restated here.

    Tier, verified per transcript, not from the reviewer's word. The review run's transcript carries 173 occurrences of "model":"claude-fable-5-1" and zero of anything else — CONTRACT_REVIEW_TIER. The instrument discriminates: two implementer transcripts from this same session, counted identically, read 348 and 223 occurrences of "model":"claude-opus-5" and zero fable.

    Landing pre-check, all three at 2026-09-15T22:59:14Z

    1. At-tier PASS naming the current head — the record's Head-sha: equals the PR's current head, read fresh. Read back from GitHub: the five machine-read lines stored intact, Served-tier: carrying the constant's NAME, no angle-bracket fragment.
    2. check-clause2-carriers --pair 18303 → exit 0, and it confirms a review of record names this head.
    3. 35 check runs, total_count equal to the 35 returned: 33 success, 2 skipped, 0 failing, 0 incomplete. The two skipped are the always-skipped family this lane's four landings all carried; a skip is not a pass and is not reported as green.

    Seat spot-checks, not adopted from either agent

    • Fences held: packages/spec/src/shared/retired-key.ts (spec: retiredKey() tombstones are emitted into a repeater row's items.properties, so the served schema offers six columns the parse door refuses #17502's surface) absent from the diff; content/docs/releases/** untouched; no .claude/** or skills/**, so this is not a governed-surface PR.
    • The reviewer caught a reporting defect the implementer did not: the ablation leg was taken at 55b34588…, the kernel.ts blob at the base and the first three commits, not at the reviewed head (57d2340c…). The reviewer reconstructed the mutated blob exactly, confirmed the leg was sound for the tree it ran on, and then re-ran the ablation at this head itself. A reporting defect, not a contract defect — recorded rather than waved past.

    Carried forward, neither blocking

    • Tombstone dating. The three new retiredKey() prescriptions read 「was removed in @objectstack/spec 18 (ADR-0049)」 while the changeset ships minor, so ADR-0087's 2026-09-13 amendment says the token should name the 17.x release. The base already carries 13 identical lines from the same major-18 wave, no gate reads the token, and the remedy inside each string is intact. This is a repo-wide pattern rather than this PR's defect, so it goes to the sweep that owns it instead of holding a green PR.
    • A stale comment at packages/spec/src/type-alias-convention.pin.test.ts:2225-2226 says the alias is 「mirrored, not defaulted」, written before ac0fe6f145 tombstoned it. Prose residue in a test comment.

    Maintainer decision. Option A ruled at 5689228298; the PR already implements it, so no change follows.

    Proceeding to clear both carriers, flip ready and enqueue.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions