Kaynağa Gözat

Merge remote-tracking branch 'origin/master' into simpl-c-web-observation

# Conflicts:
#	docs/rfc/README.md
Tianyi Cui 2 ay önce
ebeveyn
işleme
80609cca11

+ 1 - 1
AGENTS.md

@@ -22,7 +22,7 @@ packages/    Harness packages at packages/<group>/<pkg>/, all named @deepseek-ai
   hooks/       Claude Code / Codex hook bridges + shared wire-protocol library
   session-persistence/  persistence seam + JSONL/SQLite backends
   ui/          ACP bridge + the stdio/ACP app packages (each with a bin)
-  support/     dev/test infrastructure: invariants, ui-stdio, llm-replay, subagent-mock
+  support/     dev/test infrastructure: invariants, llm-replay, subagent-mock
   util/        zero-dependency utilities (Branded<B>)
 examples/    Runnable demos: thin cordis.yml leaves over the app packages (see examples/AGENTS.md)
 docs/        architecture, generated catalogs, RFCs, postmortems, cookbook (see docs/AGENTS.md)

+ 2 - 6
docs/module-graph.md

@@ -48,9 +48,6 @@ graph TD
   tools --> agent
   tools --> llm
   tools --> system-prompt
-  ui-stdio --> agent
-  ui-stdio --> llm
-  ui-stdio --> session
   acp --> agent
   acp --> llm
   acp --> session
@@ -119,9 +116,9 @@ graph TD
   acp-agent --> session-persistence-jsonl
   stdio-agent --> agent
   stdio-agent --> agent-core
+  stdio-agent --> llm
   stdio-agent --> session
   stdio-agent --> session-persistence-jsonl
-  stdio-agent --> ui-stdio
   subagent-fork --> agent
   subagent-fork --> session
   subagent-fork --> subagent
@@ -158,7 +155,6 @@ graph TD
 | `session-persistence-jsonl` | `session`, `session-persistence` |
 | `session-persistence-sqlite` | `session`, `session-persistence` |
 | `tools` | `agent`, `llm`, `system-prompt` |
-| `ui-stdio` | `agent`, `llm`, `session` |
 | `acp` | `agent`, `llm`, `session`, `session-persistence`, `tools` |
 | `agent-loop` | `agent`, `llm`, `session`, `session-persistence`, `system-prompt`, `tools` |
 | `hooks-codex` | `agent`, `hook-protocol`, `llm`, `session`, `tools` |
@@ -174,6 +170,6 @@ graph TD
 | `subagent-mock` | `agent`, `llm`, `subagent` |
 | `tool-subagent` | `agent`, `llm`, `subagent`, `tools` |
 | `acp-agent` | `acp`, `agent-core`, `session-persistence-jsonl` |
-| `stdio-agent` | `agent`, `agent-core`, `session`, `session-persistence-jsonl`, `ui-stdio` |
+| `stdio-agent` | `agent`, `agent-core`, `llm`, `session`, `session-persistence-jsonl` |
 | `subagent-fork` | `agent`, `session`, `subagent`, `subagent-inprocess` |
 | `subagent-spawn` | `subagent`, `subagent-inprocess` |

+ 1 - 1
docs/rfc/README.md

@@ -54,7 +54,6 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
 | [Unify the agent id and the session id](proposed/simplification/2026-06-20-unify-agent-and-session-id.md) | 2026-06-20 |
 | [Drop the `image` content block until a path can honor it](proposed/simplification/2026-07-04-drop-image-content-block.md) | 2026-07-04 |
 | [Drop `GenerateOptions.prefill` and `ToolSchema.strict` — request knobs with no working end-to-end path](proposed/simplification/2026-07-04-drop-inert-request-knobs.md) | 2026-07-04 |
-| [Fold the stdio UI helper into the stdio app](proposed/simplification/2026-07-04-fold-stdio-ui-helper.md) | 2026-07-04 |
 | [Prune dead core-spine surface — `SurfaceManager.invalidate()`, the loop-internal exports, `ToolExecutionResult.callId`](proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md) | 2026-07-04 |
 | [Prune producer-less vocabulary variants (block cache hints, the `agent` message source, the `continuation` turn trigger)](proposed/simplification/2026-07-04-prune-producerless-vocabulary-variants.md) | 2026-07-04 |
 | [Prune write-only fields and a dead routing knob from the fs seam](proposed/simplification/2026-07-04-prune-write-only-fs-surface.md) | 2026-07-04 |
@@ -120,6 +119,7 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
 | [Split the filesystem seam — provider text mutations plus the `dsh-fs-policy` plugin](implemented/simplification/2026-06-26-fsspec-style-fs-seam.md) | 2026-06-26 |
 | [Stop mirroring the token stream as an agent event](implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md) | 2026-07-02 |
 | [Drop the unconsumed web observation surface — the `providers-change` event and the status methods](implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) | 2026-07-04 |
+| [Fold the stdio UI helper into the stdio app](implemented/simplification/2026-07-04-fold-stdio-ui-helper.md) | 2026-07-04 |
 
 ### Architecture
 

+ 24 - 0
docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md

@@ -0,0 +1,24 @@
+# RFC: Fold the stdio UI helper into the stdio app
+
+Status: implemented
+
+## Problem
+
+The readline UI was a whole package (`@deepseek-ai/dsh-ui-stdio` under `packages/support/`) whose only runtime importer was the app package `@deepseek-ai/dsh-stdio-agent`. The examples reach the readline UI by loading the app, never by composing the helper themselves; every other repo reference was mechanical or descriptive surface that existed BECAUSE the package boundary existed — manifest and tsconfig entries, generated module-graph rows, dependency-graph and README rows, and doc comments naming the package. The ui group README recorded the support placement rationale ("exists chiefly for the examples and the coverage gate — `ui/` is reserved for surfaces shipped as product"), which left a standing tension: a shipped product app depending on a support package documented as NOT product surface.
+
+The boundary bought package metadata, workspace and tsconfig references, module-graph rows, README entries, and publint surface for a helper that is not independently swappable: the stdio app's front-door cluster always includes the readline UI, and nothing else can meaningfully consume it.
+
+## Decision
+
+The helper lives inside `@deepseek-ai/dsh-stdio-agent` as the in-package `stdio-chat` module (`packages/ui/stdio-agent/src/stdio-chat.ts`): `createStdioChat`, its `StdioRuntime` test seam, and its unit tests (`packages/ui/stdio-agent/tests/stdio-chat.spec.ts`, `readline.spec.ts`) moved with it, so EOF handling, rendering, disposal, and piped-vs-TTY behavior stay unit-covered under the per-file coverage gate without hijacking process globals. The module keeps the named `name`/`inject`/`Config`/`apply` export shape — the contract the app's `ctx.plugin(uiStdio, …)` mount consumes — and the keyless Loader-path smokes in `examples/echo-agent` and `examples/coding-agent` keep proving the composed tree boots through the real Loader (the app's export SHAPE is pinned by the stdio-agent unit suite's explicit `unwrapExports` assertion, since a bundle without `inject` would boot past a stray default rather than crash).
+
+The `packages/support/ui-stdio` package is gone: manifest, tsconfig references, module-graph rows, and README rows deleted; the doc comments that named the package (the example e2e module docs, `packages/README.md`, the support and todo READMEs, [the ui group README](../../../../packages/ui/README.md)) describe the in-package module.
+
+## Why not promote it to `ui/` instead?
+
+Promotion would have resolved the support-vs-product mismatch while keeping the boundary — the right call only if the readline UI were an independently swappable integration or had a second composer, and the consumer census said neither. The structured ACP bridge stays its own package because it is the product protocol surface with its own contract and snapshot tiers; the readline helper is scaffolding for one app's front door. Re-extraction stays cheap pre-release: if a second product app wants the readline UI, split it back out then, with that consumer shaping the package contract.
+
+## Consequences
+
+- The stdio app owns its whole front door; a leaf `cordis.yml` still loads one app package and nothing changed shape for the demos.
+- A future standalone terminal UI that wants the helper as a package reintroduces it with that second consumer, rather than the repo keeping a boundary for hypothetical reuse.

+ 0 - 27
docs/rfc/proposed/simplification/2026-07-04-fold-stdio-ui-helper.md

@@ -1,27 +0,0 @@
-# RFC: Fold the stdio UI helper into the stdio app
-
-Status: proposed
-
-## Problem
-
-`@deepseek-ai/dsh-ui-stdio` is a whole package whose only runtime importer is the app package `@deepseek-ai/dsh-stdio-agent` (`packages/ui/stdio-agent/src/index.ts`). The examples reach the readline UI by loading the app, never by composing the helper themselves; every other repo reference is mechanical or descriptive surface that exists BECAUSE the package boundary exists — manifest and tsconfig entries, generated module-graph rows, dependency-graph and README rows, and doc comments naming the package. [The ui group README](../../../../packages/ui/README.md) records the placement rationale — the helper "exists chiefly for the examples and the coverage gate — `ui/` is reserved for surfaces shipped as product" — which leaves a standing tension: a shipped product app depends on a support package documented as NOT product surface.
-
-The boundary buys package metadata, workspace and tsconfig references, module-graph rows, README entries, and publint surface for a helper that is not independently swappable: the stdio app's front-door cluster always includes the readline UI, and nothing else can meaningfully consume it.
-
-## Proposal
-
-Fold the helper into `@deepseek-ai/dsh-stdio-agent`: move `createStdioChat`, its `StdioRuntime` test seam, and its unit tests into `packages/ui/stdio-agent`; delete the `packages/support/ui-stdio` package with its manifest, references, module-graph rows, and README rows; update every reference that names the package (the example e2e module docs, `packages/README.md`, the support and todo README rows, the stdio-agent README, the ui group README, tsconfig references, the generated module graph). Keep the runtime seam so EOF handling, rendering, disposal, and piped-vs-TTY behavior stay unit-covered without hijacking process globals; the keyless Loader-path smokes keep guarding the export shape end-to-end.
-
-## Why not promote it to `ui/` instead?
-
-Promotion would resolve the support-vs-product mismatch while keeping the boundary — the right call only if the readline UI were an independently swappable integration or had a second composer, and the consumer census says neither. The structured ACP bridge stays its own package because it is the product protocol surface with its own contract and snapshot tiers; the readline helper is scaffolding for one app's front door. Re-extraction stays cheap pre-release: if a second product app wants the readline UI, split it back out then, with that consumer shaping the package contract.
-
-## Acceptance criteria
-
-- `packages/support/ui-stdio` no longer exists; the helper and its tests live in `packages/ui/stdio-agent`; no reference to the deleted package remains outside RFC history.
-- The stdio app still renders transcript events, handles stdin lines and EOF, renders todo checklists, and disposes readline listeners under HMR; the echo/coding keyless smokes still boot through the real Loader path and guard the export shape.
-- Manifests, tsconfig references, the generated module graph, and docs are updated; `pnpm run test:coverage`, `pnpm run test:snapshot`, `pnpm run doc-sync`, `pnpm run build`, and `pnpm run hygiene` pass.
-
-## Risks
-
-A future standalone terminal UI may want the helper as a package again — reintroduce it with that second consumer rather than keeping the boundary for hypothetical reuse. Moving tests risks blurring app-composition tests with UI-rendering tests; keeping the runtime seam and the colocated unit tests avoids that.

+ 6 - 5
examples/coding-agent/tests/keyless-smoke.e2e.ts

@@ -9,8 +9,8 @@ import { afterEach, describe, expect, it } from 'vitest'
  * Keyless Loader-path smoke for examples/coding-agent: boot the REAL example
  * through the `@deepseek-ai/dsh-stdio-agent` bin against its `cordis.yml` (the
  * cordis Loader, `unwrapExports`, the full plugin tree incl. the
- * `@deepseek-ai/dsh-agent-core` bundle and the extracted
- * `@deepseek-ai/dsh-ui-stdio`), then close stdin with no prompt and assert the
+ * `@deepseek-ai/dsh-agent-core` bundle and the app's in-package readline UI
+ * module), then close stdin with no prompt and assert the
  * ready banner + a clean exit.
  *
  * No prompt is ever sent, so the model is NEVER called — this is why it runs
@@ -18,9 +18,10 @@ import { afterEach, describe, expect, it } from 'vitest'
  * `apply()` only requires a key to be PRESENT (it does not validate it and only
  * uses it when a stream actually starts), so a dummy key lets the tree boot
  * while the absence of any prompt guarantees no network call. The value is the
- * real-Loader-path guard for the app + bundle + UI plugin export shapes (a broken
- * `export default` that drops `inject`/`Config` would crash here — see postmortem
- * 0001), complementing coding-agent's with-key e2e suites which prove the real
+ * real-Loader-path guard that the composed tree boots (see postmortem 0001;
+ * the app carries no `inject`, so its export SHAPE is pinned by the stdio-agent
+ * unit suite's unwrap assertion, not by a crash here),
+ * complementing coding-agent's with-key e2e suites which prove the real
  * product.
  */
 

+ 6 - 5
examples/echo-agent/tests/echo.e2e.ts

@@ -13,11 +13,12 @@ import { afterEach, describe, expect, it } from 'vitest'
  *
  * This is the guard the per-file unit suite structurally cannot be: it drives
  * the `@deepseek-ai/dsh-stdio-agent` app plugin, the `@deepseek-ai/dsh-agent-core`
- * bundle it loads, the extracted `@deepseek-ai/dsh-ui-stdio` plugin, AND the
- * example-local `mock-llm.ts` / `echo-tool.ts` through their REAL load path, so
- * a broken plugin export shape (a stray `export default` that `unwrapExports`
- * would collapse, dropping `inject`/`Config`) fails here even though hand-mounted
- * unit tests stay green (see docs/postmortem/0001). It needs no API key — the
+ * bundle it loads, the app's in-package readline UI module, AND the
+ * example-local `mock-llm.ts` / `echo-tool.ts` through their REAL load path
+ * (see docs/postmortem/0001). The app itself carries no `inject`, so a stray
+ * `export default` would boot rather than crash here — the export SHAPE is
+ * pinned by the explicit unwrap assertion in the stdio-agent unit suite; this
+ * smoke proves the composed tree actually runs. It needs no API key — the
  * `mock-echo` adapter never touches the network — so it runs in the default e2e
  * gate.
  *

+ 1 - 1
packages/README.md

@@ -19,7 +19,7 @@ Packages are grouped by modular role at `packages/<group>/<pkg>/`. The group dir
 | [`hooks/`](hooks/README.md) | Hook bridges + the shared Claude Code / Codex wire-protocol library | Product — stable surface |
 | [`session-persistence/`](session-persistence/README.md) | Persistence capability family: the seam + JSONL/SQLite backends | Product — stable surface |
 | [`ui/`](ui/README.md) | Editor/client integration surfaces (the ACP bridge) + the app packages | Product — stable surface |
-| [`support/`](support/README.md) | Dev/test/example infrastructure (invariants, stdio UI, replay adapter) | Support — lower compatibility expectations |
+| [`support/`](support/README.md) | Dev/test/example infrastructure (invariants, replay adapter, subagent mock) | Support — lower compatibility expectations |
 | [`util/`](util/README.md) | Low-level zero-dependency utilities shared across groups (the `Branded<B>` primitive) | Support — small, stable, harness-dep-free |
 
 The split is the point: a package's group says whether it is part of the product API or support/test/example infrastructure, so release and removal decisions do not treat every package as an equal public contract. New packages join an existing group; adding a new top-level group is a deliberate act (extend the group READMEs and this table).

+ 1 - 2
packages/support/README.md

@@ -5,8 +5,7 @@ Packages that exist to serve development, testing, and the examples rather than
 | Package | Role | ctx key |
 |---|---|---|
 | `invariants/` | Dev-mode event-contract invariants + session-log freeze | (listens on `session/*`, `agent/*`) |
-| `ui-stdio/` | Minimal stdio (readline) UI plugin: renders `agent/*` events, feeds stdin lines to the agent | (drives `ctx.agents`) |
 | `llm-replay/` | Record/replay adapter: short-circuits `llm/stream` from a recorded session JSONL (keyless snapshot tests) | (listens on `llm/stream`) |
 | `subagent-mock/` | Scripted `SubagentProvider` for deterministic seam/tool tests | (registers on `ctx.subagents`) |
 
-`invariants` runs only in dev mode (contract checks, not runtime behavior). `ui-stdio` and `llm-replay` were extracted from the examples for reuse and to bring them under the per-file coverage gate; they back the demos and the snapshot test tier. `subagent-mock` exercises the real `ctx.subagents` load path without a model or child agent. A package graduates OUT of `support/` into a product group only when it gains documented product consumers.
+`invariants` runs only in dev mode (contract checks, not runtime behavior). `llm-replay` backs the demos and the snapshot test tier under the per-file coverage gate. `subagent-mock` exercises the real `ctx.subagents` load path without a model or child agent. A package graduates OUT of `support/` into a product group only when it gains documented product consumers.

+ 0 - 44
packages/support/ui-stdio/README.md

@@ -1,44 +0,0 @@
-# @deepseek-ai/dsh-ui-stdio
-
-A minimal stdio (readline) UI, as a plugin. It reads lines from stdin and feeds them to an agent (`send` when idle, `steer` while a turn is running), and renders that agent's streamed output and tool activity to stdout. A UI is "just a plugin" here — it consumes the `session/event` transcript feed plus a few `agent/*` control events (`agent/status`, `agent/created`/`agent/disposed`) and the `agents` service (`inject: ['agents']`), so the same plugin drives any example or product surface.
-
-This is a **convenience REPL for local testing and the demos, not a product surface** — its observable behavior is free to change. It is deliberately NOT treated as a load-bearing consumer when weighing whether a live event/API must exist: the boundary mirror events were removed precisely because "ui-stdio renders from them" is not a product constraint (it was migrated to `session/event`). The real product surfaces are the ACP bridge (`dsh-acp`) and the app packages.
-
-This package consolidates what were two near-identical copies under `examples/echo-agent` and `examples/coding-agent`. The coding copy was a superset; this package IS that superset — dimmed chain-of-thought rendering plus robust piped-stdin EOF handling — with the per-consumer differences moved into `Config`.
-
-## Config
-
-| Key | Type | Default | Notes |
-|---|---|---|---|
-| `welcome` | string | `'ready.'` | Banner printed once on start, before the first `> ` prompt. |
-| `agent` | string | `'main'` | Id of the agent that stdin **drives** (`send`/`steer`) and whose `agent/status` gates the EOF exit. Rendering is **not** scoped by it — see below. |
-
-```yaml
-- id: ui-stdio
-  name: '@deepseek-ai/dsh-ui-stdio'
-  config:
-    welcome: 'agent REPL ready. Give it a coding task.'
-```
-
-## Rendering
-
-Rendering is **global** — every agent's events are written to stdout, not just `config.agent`'s. `config.agent` scopes only *input* (which agent stdin drives) and the EOF-exit gate; the single-agent demos this serves have just one agent, so the distinction is moot for them. (A multi-agent UI that needs per-agent panes would filter these handlers by the agent argument — deliberately out of scope here.)
-
-- `session/event` — the durable transcript feed drives ALL rendering, from a single listener so `inReasoning` transitions stay deterministic in append order: `assistant/chunk` writes the model's `text-delta` verbatim and wraps `reasoning-delta` in the dim SGR (`\x1B[2m … \x1B[0m`) so the chain-of-thought is visually subordinate to the answer (inert when no `reasoning-delta` chunks arrive, e.g. a mock model); `turn/start` prints a `[<agent> turn N]` header (the short agent label comes from a session-id→agent-id map seeded from `ctx.agents.list()` at install and kept live via `agent/created`/`agent/disposed`, since the turn event carries only the turn number); `turn/end` prints the trailing `> ` prompt; `tool/call` renders `[tool call] name(args)`; `tool/result` renders the joined text blocks as `[tool result] …`; and `todo/write` renders a glyphed checklist.
-
-## The I/O seam
-
-The production entry point `apply(ctx, config)` binds the real `process` streams. The testable core is `createStdioChat(ctx, config, runtime)`, where `runtime: StdioRuntime` supplies `input` / `output` / `exit`. This seam is deliberately **not** part of the serializable `Config` (streams and functions do not belong in YAML config); it exists so the render, EOF, and disposal branches can be exercised with fakes instead of hijacking globals.
-
-## Piped-stdin exit
-
-On stdin EOF the plugin exits the process, but carefully:
-
-- **No work submitted** (empty stdin, blank-only lines): exit immediately — no turn will ever start, so there is nothing to wait for. Gating on an observed `running` here would hang forever.
-- **Work submitted**: exit the next time the agent settles to `idle` *after* having been observed `running`. `agent.send()` does not synchronously flip status to `running`, so requiring an observed `running` first (`sawRunning`) avoids exiting in the gap before the turn starts and dropping work; and the loop batches several queued messages into one turn, so the exit keys off the idle transition rather than counting sends.
-
-Disposal (HMR or fiber teardown) closes the readline interface, which also fires `close` — a `disposed` guard ensures teardown never calls `process.exit`.
-
-## Plugin export shape
-
-Named `name` / `inject` / `Config` / `apply`, with **no default export**: the cordis Loader's `unwrapExports` does `exports.default ?? exports`, so a stray default would collapse the module to the bare function and drop the `inject` namespace (see [docs/postmortem/0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.md)). The keyless Loader-path e2e smokes in `examples/{echo,coding}-agent` guard this end-to-end.

+ 0 - 39
packages/support/ui-stdio/package.json

@@ -1,39 +0,0 @@
-{
-  "name": "@deepseek-ai/dsh-ui-stdio",
-  "description": "Minimal stdio (readline) UI plugin: renders agent/* events to stdout and feeds stdin lines to the agent",
-  "version": "0.0.1",
-  "private": true,
-  "type": "module",
-  "main": "lib/index.js",
-  "types": "lib/types/index.d.ts",
-  "exports": {
-    ".": {
-      "types": "./lib/types/index.d.ts",
-      "default": "./lib/index.js"
-    },
-    "./src/*": "./src/*",
-    "./package.json": "./package.json"
-  },
-  "files": [
-    "lib/index.js",
-    "lib/types/**/*.d.ts",
-    "lib/types/**/*.d.ts.map",
-    "src"
-  ],
-  "license": "BSD-3-Clause",
-  "peerDependencies": {
-    "@deepseek-ai/dsh-agent": "^0.0.1",
-    "@deepseek-ai/dsh-llm": "^0.0.1",
-    "@deepseek-ai/dsh-session": "^0.0.1",
-    "cordis": "^4.0.0-rc.6"
-  },
-  "dependencies": {
-    "schemastery": "^3.18.0"
-  },
-  "devDependencies": {
-    "@deepseek-ai/dsh-agent": "workspace:^",
-    "@deepseek-ai/dsh-llm": "workspace:^",
-    "@deepseek-ai/dsh-session": "workspace:^",
-    "cordis": "^4.0.0-rc.6"
-  }
-}

+ 0 - 30
packages/support/ui-stdio/tsconfig.json

@@ -1,30 +0,0 @@
-{
-  "extends": "../../../tsconfig.base.json",
-  "compilerOptions": {
-    "rootDir": "src",
-    "outDir": "lib/types"
-  },
-  "include": [
-    "src"
-  ],
-  "references": [
-    {
-      "path": "../../../vendor/cosmokit"
-    },
-    {
-      "path": "../../../vendor/cordis"
-    },
-    {
-      "path": "../../../vendor/schemastery"
-    },
-    {
-      "path": "../../core/agent"
-    },
-    {
-      "path": "../../llm/llm"
-    },
-    {
-      "path": "../../core/session"
-    }
-  ]
-}

+ 1 - 1
packages/todo/README.md

@@ -6,4 +6,4 @@ The model-facing todo tool. A single **product** package — there is no interfa
 |---|---|---|
 | `tool-todo/` | Model-facing `todo_write` tool; writes the whole list to the session log (`todo/write`) | (registers on `ctx.tools`) |
 
-The list lives on the event-sourced session log (`SessionEventMap['todo/write']`, owned by [`dsh-session`](../core/session)); this package is the thin consumer that appends the snapshot. UIs render off `session/event`: the [stdio UI](../support/ui-stdio) prints the list, the [ACP bridge](../ui/acp) maps it to a `plan` sessionUpdate.
+The list lives on the event-sourced session log (`SessionEventMap['todo/write']`, owned by [`dsh-session`](../core/session)); this package is the thin consumer that appends the snapshot. UIs render off `session/event`: the [stdio app's readline UI](../ui/stdio-agent) prints the list, the [ACP bridge](../ui/acp) maps it to a `plan` sessionUpdate.

+ 1 - 1
packages/todo/tool-todo/README.md

@@ -18,7 +18,7 @@ Beyond the schema's type/required/enum checks, `execute` rejects an empty or dup
 
 ## Rendering
 
-The tool writes only the session event; it does not render. UIs subscribe to `session/event` and render the `todo/write` data themselves: the [stdio UI](../../support/ui-stdio) prints a glyphed checklist, and the [ACP bridge](../../ui/acp) maps the list to a `plan` sessionUpdate (synthesizing the `priority` ACP requires).
+The tool writes only the session event; it does not render. UIs subscribe to `session/event` and render the `todo/write` data themselves: the [stdio app's readline UI](../../ui/stdio-agent) prints a glyphed checklist, and the [ACP bridge](../../ui/acp) maps the list to a `plan` sessionUpdate (synthesizing the `priority` ACP requires).
 
 ## Export shape
 

+ 1 - 1
packages/ui/README.md

@@ -8,6 +8,6 @@ Integrations that expose the agent to an external editor or client. These are **
 | `stdio-agent/` | Terminal stdio chat APP: the agent-core spine + console logger + readline UI + a pre-created `main` agent, with a `bin` | (composition + `bin`) |
 | `acp-agent/` | ACP server APP: the agent-core spine + JSONL persistence + the `acp` bridge (no stdout logger), with a `bin` | (composition + `bin`) |
 
-A UI integration is a client-driver plugin, not a loop change and not a capability seam: it consumes the existing `agent/*` event taxonomy and the `dsh-agent` factory. The readline `ui-stdio` plugin is the unstructured analogue but lives in `support/` because it exists chiefly for the examples and the coverage gate — `ui/` is reserved for surfaces shipped as product.
+A UI integration is a client-driver plugin, not a loop change and not a capability seam: it consumes the existing `agent/*` event taxonomy and the `dsh-agent` factory. The readline UI is the unstructured analogue of the `acp` bridge and lives INSIDE the stdio app (the `stdio-chat` module of [`stdio-agent/`](stdio-agent/README.md)): it is scaffolding for that one front door, not an independently swappable integration, so it carries no package boundary of its own.
 
 `stdio-agent` and `acp-agent` are the two **app packages**: each composes the [`core/agent-core`](../core/agent-core/README.md) spine with its coupled front-door cluster (and owns the boot `bin`), so a leaf `cordis.yml` is the swappable backends plus one app entry plus any optional product tools. They live in `ui/` because each IS a user-facing front door; the stdout-purity coupling (logger vs. no logger) becomes a property of the artifact rather than a leaf convention.

+ 1 - 1
packages/ui/stdio-agent/README.md

@@ -13,7 +13,7 @@ A terminal chat always wants the same cluster, so the package owns it rather tha
 | `@cordisjs/plugin-logger-console` | the console logger — stdout is just the terminal here, so logging to it is correct (the ACP app must NOT have this) |
 | `@deepseek-ai/dsh-agent-core` | the spine, pre-creating a `main` agent from this app's `model`/`systemPrompt` |
 | `@deepseek-ai/dsh-session-persistence-jsonl` | durable JSONL session log under `persistenceRoot` |
-| `@deepseek-ai/dsh-ui-stdio` | the readline UI, bound to the `main` agent |
+| `stdio-chat` (in-package module) | the readline UI, bound to the `main` agent |
 
 `@cordisjs/plugin-hmr` (the dev/demo edit-reload loop) is deliberately a **leaf** entry, NOT baked in here: it is a Loader-only, subprocess-only dev plugin — its constructor throws without `node --expose-internals` + a live `loader`, and the in-process test tier cannot even import it (so a package whose `apply` statically pulled it in 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. The `demo:echo` / `demo:repl` leaves load it and pass `--expose-internals`.
 

+ 2 - 2
packages/ui/stdio-agent/package.json

@@ -34,10 +34,10 @@
     "@cordisjs/plugin-loader": "^1.0.0-rc.4",
     "@cordisjs/plugin-logger-console": "^1.0.0",
     "@deepseek-ai/dsh-agent": "^0.0.1",
+    "@deepseek-ai/dsh-llm": "^0.0.1",
     "@deepseek-ai/dsh-agent-core": "^0.0.1",
     "@deepseek-ai/dsh-session": "^0.0.1",
     "@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1",
-    "@deepseek-ai/dsh-ui-stdio": "^0.0.1",
     "cordis": "^4.0.0-rc.6",
     "schemastery": "^3.17.0"
   },
@@ -46,10 +46,10 @@
     "@cordisjs/plugin-loader": "workspace:^",
     "@cordisjs/plugin-logger-console": "workspace:^",
     "@deepseek-ai/dsh-agent": "workspace:^",
+    "@deepseek-ai/dsh-llm": "workspace:^",
     "@deepseek-ai/dsh-agent-core": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
-    "@deepseek-ai/dsh-ui-stdio": "workspace:^",
     "cordis": "^4.0.0-rc.6",
     "schemastery": "^3.17.0"
   }

+ 9 - 6
packages/ui/stdio-agent/src/index.ts

@@ -1,12 +1,13 @@
 /**
  * The stdio chat app: the providerless agent spine ({@link
  * @deepseek-ai/dsh-agent-core}) plus the coupled front-door cluster a terminal
- * chat needs — a console logger, the readline `ui-stdio` UI, JSONL session
+ * chat needs — a console logger, the readline UI (the in-package `stdio-chat`
+ * module), JSONL session
  * persistence, and a pre-created `main` agent the UI drives.
  *
  * The cluster is BAKED IN, not left to the leaf: a stdio app always logs to the
  * console (stdout is just the terminal) and always pre-creates the `main` agent
- * `ui-stdio` sends to. The leaf supplies the swappable backends (the LLM
+ * the readline UI sends to. The leaf supplies the swappable backends (the LLM
  * adapter, the bash executor), optional product tools, the optional `hmr`
  * dev-reload plugin, and this app's {@link Config} (model, prompt, persistence
  * root, welcome banner).
@@ -29,8 +30,10 @@
  * Plugin export shape: named `name`/`Config`/`apply`, NO default export — the
  * cordis Loader's `unwrapExports` does `exports.default ?? exports`, so a stray
  * default would collapse the module to the bare `apply` and drop the `Config`
- * namespace (see docs/postmortem/0001). The keyless Loader-path smoke in the
- * echo example guards this end-to-end.
+ * namespace (see docs/postmortem/0001). This app carries no `inject`, so a
+ * collapsed shape would BOOT rather than crash a smoke — the shape is pinned by
+ * the explicit `unwrapExports` assertion in this package's unit suite, and the
+ * keyless echo smoke proves the composed tree runs through the real Loader.
  *
  * @module @deepseek-ai/dsh-stdio-agent
  */
@@ -42,7 +45,7 @@ import { AgentId } from '@deepseek-ai/dsh-agent'
 import { SessionId } from '@deepseek-ai/dsh-session'
 import * as agentCore from '@deepseek-ai/dsh-agent-core'
 import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
-import * as uiStdio from '@deepseek-ai/dsh-ui-stdio'
+import * as uiStdio from './stdio-chat.ts'
 
 export const name = 'stdio-agent'
 
@@ -81,7 +84,7 @@ export const Config: z<Config> = z.object({
  * Compose the spine with the stdio front door. The console logger comes first
  * (infra), then the agent-core bundle pre-creating the `main` agent from this
  * app's `model`/`systemPrompt`/`resumeSessionId`, then the JSONL backend, then
- * the `ui-stdio` UI bound to `main`. The `hmr` dev-reload plugin is a leaf
+ * the readline UI bound to `main`. The `hmr` dev-reload plugin is a leaf
  * concern (see the module doc), so it is not mounted here.
  */
 export function apply(ctx: Context, config: Config): void {

+ 12 - 17
packages/support/ui-stdio/src/index.ts → packages/ui/stdio-agent/src/stdio-chat.ts

@@ -1,23 +1,18 @@
 /**
- * Minimal stdio UI plugin: reads lines from stdin → `agent.send()`/`steer()`,
- * and renders the durable transcript to stdout. A UI is "just a plugin" — it
- * consumes the `session/event` feed (the assistant token stream, turn/step
- * boundaries, tool activity, todos) plus a few `agent/*` control events
- * (`agent/status`, `agent/created`/`agent/disposed`) and the `agents` service,
- * so the same plugin drives any example or product surface.
+ * The stdio app's readline UI: reads lines from stdin → `agent.send()`/
+ * `steer()`, and renders the durable transcript to stdout. A UI is "just a
+ * plugin" — it consumes the `session/event` feed (the assistant token stream,
+ * turn/step boundaries, tool activity, todos) plus a few `agent/*` control
+ * events (`agent/status`, `agent/created`/`agent/disposed`) and the `agents`
+ * service. Dimmed chain-of-thought rendering plus robust piped-stdin EOF→idle
+ * exit handling, configured via {@link Config}.
  *
- * Consolidates what were two near-identical copies under `examples/echo-agent`
- * and `examples/coding-agent` (the latter a superset). This package IS that
- * superset: dimmed chain-of-thought rendering plus the robust piped-stdin
- * EOF→idle exit handling, configured per consumer via {@link Config}.
+ * An internal module of the stdio app, not a package of its own: the app's
+ * front-door cluster always includes this UI, and nothing else composes it.
+ * The export shape stays named `name`/`inject`/`Config`/`apply` — the plugin
+ * contract the app's `ctx.plugin(uiStdio, …)` mount consumes.
  *
- * Plugin export shape: named `name`/`inject`/`Config`/`apply`, NO default
- * export — the cordis Loader's `unwrapExports` does `exports.default ?? exports`,
- * so a stray default would collapse the module to the bare function and drop
- * the `inject` namespace (see docs/postmortem/0001). The keyless Loader-path
- * e2e smokes in `examples/{echo,coding}-agent` guard this end-to-end.
- *
- * @module @deepseek-ai/dsh-ui-stdio
+ * @module @deepseek-ai/dsh-stdio-agent/stdio-chat
  */
 
 import { createInterface } from 'node:readline'

+ 1 - 1
packages/ui/stdio-agent/tests/built-bin.e2e.ts

@@ -35,7 +35,7 @@ const stdioBin = join(repoRoot, 'packages/ui/stdio-agent/lib/bin.js')
 const dshPackages = [
   'core/agent-core', 'core/agent', 'core/session', 'core/system-prompt',
   'core/tools', 'core/agent-loop', 'llm/llm', 'bash/bash', 'bash/bash-local',
-  'bash/tool-bash', 'support/invariants', 'support/ui-stdio',
+  'bash/tool-bash', 'support/invariants',
   'session-persistence/session-persistence',
   'session-persistence/session-persistence-jsonl', 'ui/stdio-agent',
 ]

+ 2 - 2
packages/support/ui-stdio/tests/readline.spec.ts → packages/ui/stdio-agent/tests/readline.spec.ts

@@ -2,7 +2,7 @@ import { EventEmitter } from 'node:events'
 import type { Readable, Writable } from 'node:stream'
 import { describe, expect, it, vi } from 'vitest'
 import type { Context } from 'cordis'
-import type { StdioRuntime } from '../src/index.ts'
+import type { StdioRuntime } from '../src/stdio-chat.ts'
 
 const createInterface = vi.hoisted(() => vi.fn(() => {
   const reader = new EventEmitter() as EventEmitter & { close(): void }
@@ -32,7 +32,7 @@ function fakeRuntime(inputIsTTY: boolean, outputIsTTY: boolean): StdioRuntime {
 
 describe('createStdioChat readline mode', () => {
   it('enables terminal editing only when both stdio streams are TTYs', async () => {
-    const { createStdioChat } = await import('../src/index.ts')
+    const { createStdioChat } = await import('../src/stdio-chat.ts')
 
     const tty = fakeRuntime(true, true)
     createStdioChat(fakeContext(), {}, tty)

+ 5 - 3
packages/ui/stdio-agent/tests/stdio-agent.spec.ts

@@ -12,9 +12,11 @@ import * as stdioAgent from '../src/index.ts'
  * agent; `persistenceRoot`/`welcome`/`resumeSessionId` route to their backends.
  *
  * `hmr` is NOT part of this plugin (it is a leaf entry — a Loader-only dev
- * plugin the in-process tier cannot import); the REAL Loader-path guard (export
- * shape, `unwrapExports`, the whole subprocess tree incl. `hmr`) is the keyless
- * echo smoke in `examples/echo-agent`. Here we assert the composition + config
+ * plugin the in-process tier cannot import); the keyless echo smoke in
+ * `examples/echo-agent` proves the whole subprocess tree (incl. `hmr`) boots
+ * through the real Loader, while the export SHAPE is pinned by this suite's
+ * explicit `unwrapExports` assertion (an inject-less app would boot past a
+ * stray default rather than crash). Here we assert the composition + config
  * forwarding the unit tier can reach.
  */
 async function mount(config: stdioAgent.Config): Promise<Context> {

+ 1 - 1
packages/support/ui-stdio/tests/ui-stdio.spec.ts → packages/ui/stdio-agent/tests/stdio-chat.spec.ts

@@ -5,7 +5,7 @@ import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent'
 import AgentRegistry from '@deepseek-ai/dsh-agent'
 import type { ContentBlock, StreamChunk } from '@deepseek-ai/dsh-llm'
 import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
-import { createStdioChat, type Config, type StdioRuntime } from '../src/index.ts'
+import { createStdioChat, type Config, type StdioRuntime } from '../src/stdio-chat.ts'
 
 /**
  * Unit tests for the stdio UI plugin. They drive the REAL plugin body

+ 0 - 3
packages/ui/stdio-agent/tsconfig.json

@@ -31,9 +31,6 @@
     },
     {
       "path": "../../session-persistence/session-persistence-jsonl"
-    },
-    {
-      "path": "../../support/ui-stdio"
     }
   ]
 }

+ 3 - 22
pnpm-lock.yaml

@@ -777,25 +777,6 @@ importers:
         specifier: ^4.0.0-rc.6
         version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
 
-  packages/support/ui-stdio:
-    dependencies:
-      schemastery:
-        specifier: ^3.18.0
-        version: 3.18.0
-    devDependencies:
-      '@deepseek-ai/dsh-agent':
-        specifier: workspace:^
-        version: link:../../core/agent
-      '@deepseek-ai/dsh-llm':
-        specifier: workspace:^
-        version: link:../../llm/llm
-      '@deepseek-ai/dsh-session':
-        specifier: workspace:^
-        version: link:../../core/session
-      cordis:
-        specifier: ^4.0.0-rc.6
-        version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
-
   packages/todo/tool-todo:
     devDependencies:
       '@deepseek-ai/dsh-agent':
@@ -919,15 +900,15 @@ importers:
       '@deepseek-ai/dsh-agent-core':
         specifier: workspace:^
         version: link:../../core/agent-core
+      '@deepseek-ai/dsh-llm':
+        specifier: workspace:^
+        version: link:../../llm/llm
       '@deepseek-ai/dsh-session':
         specifier: workspace:^
         version: link:../../core/session
       '@deepseek-ai/dsh-session-persistence-jsonl':
         specifier: workspace:^
         version: link:../../session-persistence/session-persistence-jsonl
-      '@deepseek-ai/dsh-ui-stdio':
-        specifier: workspace:^
-        version: link:../../support/ui-stdio
       cordis:
         specifier: ^4.0.0-rc.6
         version: 4.0.0-rc.6(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader)

+ 0 - 1
tsconfig.build.json

@@ -42,7 +42,6 @@
     { "path": "./packages/ui/acp" },
     { "path": "./packages/ui/acp-agent" },
     { "path": "./packages/ui/stdio-agent" },
-    { "path": "./packages/support/ui-stdio" },
     { "path": "./packages/support/llm-replay" },
     { "path": "./packages/subagent/subagent" },
     { "path": "./packages/support/subagent-mock" },

+ 0 - 1
tsconfig.json

@@ -53,7 +53,6 @@
     { "path": "./packages/ui/acp" },
     { "path": "./packages/ui/acp-agent" },
     { "path": "./packages/ui/stdio-agent" },
-    { "path": "./packages/support/ui-stdio" },
     { "path": "./packages/support/llm-replay" },
     { "path": "./packages/subagent/subagent" },
     { "path": "./packages/support/subagent-mock" },