Implements docs/rfc/.../2026-06-20-extract-example-app-packages.md. Each
example was thick — a hand-rolled start.ts, an infra preamble, nested
base.yml/base-core.yml/acp-tail.yml includes, and a coupled front-door
cluster enforced only by prose. This moves the composition into packages so
each example is a thin leaf cordis.yml: pick the swappable backends, load one
app package.
New packages:
- @deepseek-ai/dsh-agent-core (packages/core/agent-core): one bundle plugin
that loads the providerless/executor-less/UI-less spine (timer + llm +
sessions + system-prompt + tools + agents + invariants + tool-bash +
agent-loop) via ctx.plugin(...) inside apply(), and forwards agent-loop's
`agents` list as its own Config (export const Config = AgentLoop.Config,
default []).
- @deepseek-ai/dsh-stdio-agent (packages/ui/stdio-agent): terminal chat APP —
agent-core + console logger + readline UI + a pre-created `main` agent, with
a bin. The demo:echo/coding front door.
- @deepseek-ai/dsh-acp-agent (packages/ui/acp-agent): ACP server APP —
agent-core + JSONL persistence + the acp bridge, NO stdout logger, with a
bin. The stdout-purity footgun is structurally unreachable from the leaf.
Amendment to the RFC: hmr stays a LEAF cordis.yml entry, not baked into
dsh-stdio-agent. hmr is a Loader-only dev plugin (throws without
--expose-internals; the in-process test tier can't even import its decorator
form), so a package statically importing it could never carry the per-file
coverage gate. Unlike the console logger, a stray hmr is not a stdout-purity
footgun, so leaving it at the leaf costs no safety. With hmr out, all three new
packages carry in-process unit specs at 100%.
Boot glue (Loader tail, .env load, snapshot-mode selection, stdin-dispose
lifecycle) moves into each app's bin; start.ts and base.yml/base-core.yml/
acp-tail.yml are deleted. Each app package gets a keyless real-load-path test
that boots through its bin + the cordis Loader (guarding the unwrapExports
export-shape bug class, postmortem 0001). ACP snapshot replay stays green
against the existing committed goldens (pure boot restructuring). RFC moved
proposed->implemented with the amendment recorded; package/example/architecture
docs and the module graph updated.
13 KiB
RFCs
One kind of design doc lives here. An RFC records a decision or proposal that shapes this codebase — the why and what we gave up, the parts code and docs can't carry. (Earlier this split into separate "ADR" and "RFC" trees; they were unified, since most ADRs were simply implemented RFCs.)
Layout and naming
Every RFC has two axes, both encoded in its path — {lifecycle}/{class}/yyyy-mm-dd-topic-title.md:
- Lifecycle (the top-level folder) is the RFC's status, and an RFC moves between folders as that status changes:
proposed/— proposals reviewed before implementation; not yet built (or only partly).implemented/— the decision shipped. The file records what was decided and what was rejected, and is kept current with what actually shipped: when the code later moves a file, renames a package, or changes a key/default, the RFC is updated in the same change to match (facts only — paths, names, structure — not the decision itself). See implemented/AGENTS.md.rejected/— the proposal was considered and declined. Kept for the record so the rejection isn't re-litigated.
- Class (the nested folder) is the kind of decision — see Classification below.
The date in the filename is when the topic was first proposed (per git history). Cross-references between RFCs use relative markdown links ([topic](../../implemented/architecture/2026-…-….md)) — never bare prose or numbers — so they are mechanically checkable and survive moves between folders.
Classification
Each RFC is filed under exactly one class — the kind of decision it records. The class is encoded in the path (the folder is the label, so a file's location declares its class) and the set is closed: scripts/verify-rfc-classification.ts rejects any folder outside the set and asserts this index lists every RFC under the heading matching its path. Adding a new class means amending that gate and this section, not just dropping a new folder. See the classification RFC for why the taxonomy is path-encoded and gated.
| Class | What it covers |
|---|---|
feature |
A new user- or model-facing capability. |
bug-fix |
Corrects a defect or closes a gap a postmortem surfaced. |
simplification |
Removes code, behavior, or surface area without adding a capability. |
architecture |
A structural decision about the shipped source — how packages relate, what the runtime vocabulary is. |
process |
Tooling, policy, or workflow around the code — gates, the package manager, vendoring — not runtime behavior. |
testing |
Test infrastructure and strategy. |
The architecture / process line: architecture is about the source we ship; process is the surrounding tooling and workflow. (refactor is deliberately absent — it overlaps simplification, whose discriminator, "does observable behavior change?", already covers it.)
When to write one
Write an RFC when a decision is durable (it shapes the codebase beyond a single function or package), contested (there was a real alternative a reasonable engineer might have chosen), and surprising (a future reader would otherwise ask "why on earth is it done this way?"). A proposal for substantial future work starts in proposed/; a decision already made starts in implemented/. Pick the class folder that matches the decision (see Classification).
Do NOT write one for a mechanical or local choice (a variable name, a one-file refactor), for anything already enforced and explained by a gate or a convention in AGENTS.md, or for a still-provisional decision tagged TODO(...) in the code — record those as TODOs and promote to an RFC only once they settle. An RFC is never edited into a different decision: supersede it with a new one and cross-link. (Editing an implemented/ RFC to track where its already-made decision now lives — a moved file, a renamed package — is not a different decision and is required, not forbidden; see implemented/AGENTS.md.)
Proposed
Feature
| Title | First proposed |
|---|---|
| Agent Client Protocol (ACP) support for external editors | 2026-06-14 |
| Multiplex concurrent ACP sessions over one connection | 2026-06-14 |
| Optional Code Mode — model writes TypeScript against an SDK of all tools | 2026-06-15 |
Simplification
| Title | First proposed |
|---|---|
| Unify the agent id and the session id | 2026-06-20 |
| Stop mirroring durable boundaries as agent events | 2026-06-20 |
Architecture
| Title | First proposed |
|---|---|
| Runtime schemas for the event vocabulary (Zod vs the merge-extensible-map pattern) | 2026-06-16 |
| Extract a generic long-running tool runtime | 2026-06-20 |
Process
| Title | First proposed |
|---|---|
| Architectural conformance — dependency rules and the adapter kit | 2026-06-11 |
| API extractor reports | 2026-06-11 |
| Supply chain checks and vendor drift verification | 2026-06-11 |
| Discover package inventories instead of maintaining static lists | 2026-06-20 |
Testing
| Title | First proposed |
|---|---|
| Mutation testing as the coverage counterweight | 2026-06-11 |
| Deterministic tests, the replay invariant fixture, and race stress | 2026-06-11 |
Implemented
Feature
| Title | First proposed |
|---|---|
Rich ACP bash rendering — the terminal card (_meta) and command classification |
2026-06-18 |
Simplification
| Title | First proposed |
|---|---|
| Drop the mutable session summary | 2026-06-19 |
| Drop unconsumed assembled LLM convenience surfaces | 2026-06-20 |
Drop the unconsumed llm/adapter-change event |
2026-06-20 |
| Prune dead methods from the persistence and bash seams | 2026-06-20 |
| Keep one public stop primitive | 2026-06-20 |
| Fold trace-only session facts into load-bearing events | 2026-06-20 |
Architecture
Process
| Title | First proposed |
|---|---|
| Vendor Cordis as source, not npm dependencies | 2026-06-11 |
| Mechanical quality gates over prose guidelines | 2026-06-11 |
| tsdown for JS bundling instead of dumble | 2026-06-11 |
| Doc-sync enforcement | 2026-06-11 |
| pnpm as the package manager instead of Yarn 4 | 2026-06-16 |
| Markdown cross-link validity linting | 2026-06-18 |
Core-data-structures catalog and the ts type-equiv drift gate |
2026-06-20 |
| Generated cordis events + services catalog | 2026-06-20 |
| Classify RFCs by kind via path-encoded subdirectories | 2026-06-20 |
Testing
| Title | First proposed |
|---|---|
| Property-based testing for protocol-shaped code | 2026-06-11 |
| ACP snapshot tests — record-once / replay-deterministic | 2026-06-19 |
| Real-API e2e in CI against the external DeepSeek API | 2026-06-19 |
Use session.jsonl as the only snapshot session-log artifact |
2026-06-20 |
Rejected
Simplification
| Title | First proposed |
|---|---|
| Persist assembled assistant messages, not stream chunks | 2026-06-20 |
| Drop ACP session/load until resume has a product shape | 2026-06-20 |
Drop ACP terminal _meta rendering |
2026-06-20 |
| Drop bash full-output spill files | 2026-06-20 |
| Drop durable step boundary events | 2026-06-20 |
| Drop unused session lineage metadata | 2026-06-20 |
| Fold the persistence interface into dsh-session | 2026-06-20 |
| Collapse tool-owned UI presentation | 2026-06-20 |
| Retire mid-turn steering | 2026-06-20 |
| Return the ACP bridge to one live session per connection | 2026-06-20 |
| Truncate interrupted final turns on load | 2026-06-20 |
Architecture
| Title | First proposed |
|---|---|
| Deep-readonly public surfaces | 2026-06-11 |
| Make the shared example base providerless | 2026-06-20 |