Define the in-file RFC contract in docs/rfc/README.md § The file format: the header block (`# RFC: <title>` plus a dateless Status enum cross-checked against the lifecycle folder), the per-lifecycle body skeleton (a Problem opener everywhere; Proposal/Alternatives considered/ Acceptance criteria/Risks in proposed/; present-tense Decision/ Consequences with proposal-era headings banned in implemented/; the frozen proposal shape in rejected/), and a mandatory Alternatives considered section with a date-fenced grandfather comment for pre-format RFCs whose alternatives are not reconstructible from the record. Enforce it with a new doc-sync gate, scripts/verify-rfc-format.ts, and normalize all 112 RFCs to it: ~15 Status-line spellings collapse to the enum, 29 Context openers become Problem, the 39 legacy-format XXX debt markers are resolved and banned from reappearing, proposal-era sections in implemented RFCs are rewritten to shipped reality (including the web/fs/subagent seam RFCs' migration plans and test checklists, closing the doc-tiers deferred-work item on the web seam), every RFC gains an Alternatives considered section or the grandfather comment, and the bilingual pair is re-mirrored and re-recorded. Move the generated index tables out of README.md into a fully generated docs/rfc/INDEX.md — gen-rfc-index now writes the whole file, and verify-rfc-classification checks its freshness and rejects index-shaped rows in the curated README — which makes room for the format contract to live in the README front door instead of a separate FORMAT.md. The decision record, and the first RFC written in the new format, is docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.md.
3.6 KiB
RFC: Share the app bins' boot glue instead of maintaining twin copies
Status: implemented
Problem
packages/ui/stdio-agent/src/bin.ts and packages/ui/acp-agent/src/bin.ts carried four near-twin helpers — loadEnv, installFailLoud, assertEntriesLoaded, boot — whose bodies differed essentially in the diagnostic prefix, plus two copies of the hardest-won boot lore in the repo: the Promise.allSettled swallow inside loader.await(), the silent-exit-0 import-failure guard, and the --expose-internals resolution note. The copies had drifted (boot(configPath) resolved the path internally in one bin but required a pre-resolved absolute path in the other, with forked JSDoc prose), and all of it sat outside the per-file 100% gate — vitest.config.ts excludes packages/*/*/src/bin.ts because importing a self-executing bin runs it — which also made the helpers' export keywords decorative: no spec could import them, so the only exercisers were subprocess smokes.
Decision
The helpers live once, in @deepseek-ai/dsh-app-boot (packages/ui/app-boot, in the ui group because the bins are published artifacts whose runtime dependency must itself be published, not support/): resolveConfigPath (snapshot-aware, the single path resolver for both bins), loadEnv, installFailLoud, assertEntriesLoaded, and boot, each parameterized by the bin's diagnostic prefix and injectable at its side-effect seams (the warn sink, the process slice) so the unit suite covers every branch — including boot() driven in-process against the real Loader with relative-specifier configs, both the settled-tree happy path and the fiber-less-entry rejection. The package carries the per-file 100% coverage gate; the loader-failure lore has one home.
Each bin.ts is a thin self-executing composition over the shared helpers plus its app-specific lifecycle (the ACP bin: replay-mode env skipping and the stdin-EOF dispose; the stdio bin: nothing extra). The bins stay coverage-excluded and export nothing; the published-artifact guards are unchanged — the built-bin smokes still run each bin under plain node in a node_modules-shaped temp dir (now symlinking ui/app-boot too) and still assert the missing-config non-zero exit, per the "real entry path means the published artifact" defensive pattern. The extract-example-app-packages RFC's bin-ownership facts are amended accordingly.
Alternatives considered
Why not keep the duplication?
The bins were framed as independently-owned published artifacts, and a new package carries fixed overhead (manifest, README, tsconfig reference, publint surface) comparable to the deduplicated line count. But app-vs-app sharing was never weighed by the RFC that created the bins — it consolidated three example start.ts copies INTO the bins and stopped there; the drift was observed fact; and the coverage-gap argument is independent of the dedup argument: this was the only nontrivial runtime logic in the repo exempt from the per-file 100% gate. The recorded fallback (extracting only the pure logic into per-app modules) would have ended the exemption but kept two homes for the lore.
Consequences
- A boot-glue change (a new guard, a resolution fix) lands once and both published bins inherit it; the bins cannot drift apart again.
dsh-app-bootstays dependency-light (cordis + the loader/include pair) — it is boot machinery, not app surface.- The bins' own files are near-trivial compositions; everything with branches lives under the coverage gate.