Jelajahi Sumber

refactor(ui): extract stdio plugin package

Move the readline front door from stdio-agent into @deepseek-ai/dsh-stdio, keeping the loader shape and the stdio coverage with the new package.
imccyu 2 bulan lalu
induk
melakukan
80fbeefbd6

+ 16 - 0
docs/config-catalog.md

@@ -608,6 +608,22 @@ export interface Config {
 
 Source: [`packages/skill/skill-local/src/index.ts:39`](../packages/skill/skill-local/src/index.ts)
 
+## `@deepseek-ai/dsh-stdio`
+
+Requires: `agents` · `userInteraction`
+
+```ts config-catalog
+/** Serializable plugin configuration (cordis-native, schemastery). */
+export interface Config {
+  /** Banner printed once on start, before the first `> ` prompt. */
+  welcome?: string
+  /** Id of the agent stdin drives (`send`/`steer`) and whose status gates the EOF exit; rendering is global. Defaults to `'main'`. */
+  agent?: string
+}
+```
+
+Source: [`packages/ui/stdio/src/index.ts:30`](../packages/ui/stdio/src/index.ts)
+
 ## `@deepseek-ai/dsh-stdio-agent`
 
 ```ts config-catalog

+ 4 - 4
docs/event-producer-consumer.md

@@ -7,8 +7,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 
 | Event | Mode | Declared in | Dispatchers | Listeners |
 | --- | --- | --- | --- | --- |
-| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:139`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`jsonrpc`](../packages/ui/jsonrpc), [`stdio-agent`](../packages/ui/stdio-agent) |
-| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:148`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`stdio-agent`](../packages/ui/stdio-agent) |
+| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:139`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`jsonrpc`](../packages/ui/jsonrpc), [`stdio`](../packages/ui/stdio) |
+| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:148`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`stdio`](../packages/ui/stdio) |
 | `agent/error` | `emit` | [`packages/core/agent/src/types.ts:283`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - |
 | `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:202`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic), [`user-approval`](../packages/ui/user-approval) |
 | `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:212`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) |
@@ -16,7 +16,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:224`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
 | `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:239`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill) |
 | `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:180`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
-| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:157`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`invariants`](../packages/support/invariants), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`stdio-agent`](../packages/ui/stdio-agent) |
+| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:157`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`invariants`](../packages/support/invariants), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`stdio`](../packages/ui/stdio) |
 | `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:250`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
 | `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:260`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
 | `agent/turn-stop` | `serial` | [`packages/core/agent/src/types.ts:270`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
@@ -27,7 +27,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:39`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`invariants`](../packages/support/invariants), [`llm-replay`](../packages/support/llm-replay) |
 | `session/created` | `emit` | [`packages/core/session/src/index.ts:47`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence) |
 | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:57`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | - |
-| `session/event` | `emit` | [`packages/core/session/src/index.ts:69`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence), [`stdio-agent`](../packages/ui/stdio-agent) |
+| `session/event` | `emit` | [`packages/core/session/src/index.ts:69`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence), [`stdio`](../packages/ui/stdio) |
 | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:79`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence) |
 | `skill/provider-added` | `emit` | [`packages/skill/skill/src/index.ts:131`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`emit`) | - |
 | `skill/provider-removed` | `emit` | [`packages/skill/skill/src/index.ts:137`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`emit`) | - |

+ 8 - 1
docs/module-graph.md

@@ -98,6 +98,7 @@ flowchart TD
     pkg_jsonrpc["jsonrpc"]
     pkg_jsonrpc_agent["jsonrpc-agent"]
     pkg_permission["permission"]
+    pkg_stdio["stdio"]
     pkg_stdio_agent["stdio-agent"]
     pkg_tool_ask_user["tool-ask-user"]
     pkg_user_approval["user-approval"]
@@ -205,6 +206,10 @@ flowchart TD
   pkg_permission --> pkg_sandbox
   pkg_permission --> pkg_session
   pkg_permission --> pkg_user_approval
+  pkg_stdio --> pkg_agent
+  pkg_stdio --> pkg_llm
+  pkg_stdio --> pkg_session
+  pkg_stdio --> pkg_user_interaction
   pkg_agent_loop --> pkg_agent
   pkg_agent_loop --> pkg_llm
   pkg_agent_loop --> pkg_scope
@@ -333,6 +338,7 @@ flowchart TD
   pkg_stdio_agent --> pkg_llm
   pkg_stdio_agent --> pkg_session
   pkg_stdio_agent --> pkg_session_persistence_jsonl
+  pkg_stdio_agent --> pkg_stdio
   pkg_stdio_agent --> pkg_tool_ask_user
   pkg_stdio_agent --> pkg_tools
   pkg_stdio_agent --> pkg_user_interaction
@@ -385,6 +391,7 @@ flowchart TD
 | [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/ui/user-approval) |
 | [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`sandbox`](../packages/sandbox/sandbox) |
 | [`permission`](../packages/ui/permission) | `ui` | [`bash`](../packages/bash/bash), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`user-approval`](../packages/ui/user-approval) |
+| [`stdio`](../packages/ui/stdio) | `ui` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`user-interaction`](../packages/ui/user-interaction) |
 | [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) |
 | [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
@@ -410,4 +417,4 @@ flowchart TD
 | [`subagent-fork`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
 | [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
 | [`acp-agent`](../packages/ui/acp-agent) | `ui` | [`acp`](../packages/ui/acp), [`agent-core`](../packages/core/agent-core), [`app-boot`](../packages/ui/app-boot), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) |
-| [`stdio-agent`](../packages/ui/stdio-agent) | `ui` | [`agent`](../packages/core/agent), [`agent-core`](../packages/core/agent-core), [`app-boot`](../packages/ui/app-boot), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) |
+| [`stdio-agent`](../packages/ui/stdio-agent) | `ui` | [`agent`](../packages/core/agent), [`agent-core`](../packages/core/agent-core), [`app-boot`](../packages/ui/app-boot), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`stdio`](../packages/ui/stdio), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) |

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

@@ -10,7 +10,7 @@ The boundary bought package metadata, workspace and tsconfig references, module-
 
 ## Decision
 
-The `stdio-chat` module now lives inside `dsh-stdio-agent` with its runtime seam. Per-file tests cover EOF, rendering, disposal, and piped-versus-TTY behavior without replacing process globals. It retains the named Cordis plugin export shape consumed by the app; an `unwrapExports` assertion and keyless Loader smokes guard both the package and composed entry paths.
+The helper lives in `@deepseek-ai/dsh-stdio` as the terminal-channel plugin (`packages/ui/stdio/src/index.ts`): `createStdioChat`, its `StdioRuntime` test seam, and its unit tests (`packages/ui/stdio/tests/stdio.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 stdio package's plugin-shape unit suite pins the 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.
 

+ 4 - 0
knip.json

@@ -85,6 +85,10 @@
       "entry": ["tests/**/*.spec.ts", "tests/**/*.e2e.ts"],
       "project": ["src/**/*.ts", "tests/**/*.ts"]
     },
+    "packages/ui/stdio": {
+      "entry": ["tests/**/*.spec.ts"],
+      "project": ["src/**/*.ts", "tests/**/*.ts"]
+    },
     "packages/ui/jsonrpc-agent": {
       "project": ["src/**/*.ts"]
     },

+ 2 - 1
packages/ui/README.md

@@ -9,13 +9,14 @@ Integrations that expose the agent to an external editor or client. These are **
 | `permission/` | User-facing permission presets (`workspace-write`/`danger-full-access`): one product-level select bundling the sandbox-mode and approval-policy knobs, written through to their session events | `ctx.permission` |
 | `user-interaction/` | Abstract human question/answer seam used by UI-backed confirmation tools | `ctx.userInteraction` |
 | `tool-ask-user/` | Model-facing `ask_user_question` tool over `ctx.userInteraction` | (registers on `ctx.tools`) |
+| `stdio/` | Terminal readline channel over `ctx.agents`, `session/event`, and `ctx.userInteraction`; agent lifecycle stays with app/developer code | (drives `ctx.agents`) |
 | `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`) |
 | `jsonrpc/` | Stdio JSON-RPC server for out-of-process SDK clients | (drives `ctx.agents`) |
 | `jsonrpc-agent/` | Bin-only SDK runtime app that boots an external `cordis.yml` | (`bin` only) |
 | `app-boot/` | Shared boot glue for the app bins: `.env` loading, fail-loud Loader guards, snapshot-aware config resolution, the settle-the-tree boot sequence | (library for the bins) |
 
-A UI integration is a client-driver plugin, not a loop change or capability seam: it consumes the existing `agent/*` events and `dsh-agent` factory. `jsonrpc` is the SDK-client sibling of the `acp` editor bridge. The readline UI lives inside [`stdio-agent/`](stdio-agent/README.md) because it is scaffolding for that front door, not an independently swappable integration.
+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 `jsonrpc` plugin is the SDK-client sibling of the `acp` bridge (a JSON-RPC server over `ctx.agents` for out-of-process SDK clients rather than editors). The [`stdio`](stdio/README.md) plugin is the unstructured readline analogue of the `acp` bridge; app bundles and SDK projects compose it explicitly with the services and tools their product profile selects.
 
 `user-approval`, `user-interaction`, and `tool-ask-user` live here because asking a human is a UI-backed product affordance, not part of the providerless core spine. `user-approval` owns the one-shot `ctx.approval` decision mechanism and its policy tier; answerers remain with their UI channel owners. `user-interaction` remains provider-neutral (`ctx.userInteraction`), while `tool-ask-user` is its model-facing consumer and the app/bridge packages provide concrete providers.
 

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

@@ -15,7 +15,7 @@ A terminal chat always wants the same cluster, so the package owns it rather tha
 | `@deepseek-ai/dsh-session-persistence-jsonl` | durable JSONL session log under `persistenceRoot` |
 | `@deepseek-ai/dsh-user-interaction` | the human question/answer seam used by confirmation tools |
 | `@deepseek-ai/dsh-tool-ask-user` | the model-facing `ask_user_question` tool |
-| `stdio-chat` (in-package module) | the readline UI, bound to the `main` agent |
+| `@deepseek-ai/dsh-stdio` | 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`.
 

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

@@ -38,9 +38,10 @@
     "@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-tools": "^0.0.1",
     "@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1",
+    "@deepseek-ai/dsh-stdio": "^0.0.1",
     "@deepseek-ai/dsh-tool-ask-user": "^0.0.1",
+    "@deepseek-ai/dsh-tools": "^0.0.1",
     "@deepseek-ai/dsh-user-interaction": "^0.0.1",
     "cordis": "^4.0.0-rc.6",
     "schemastery": "^3.17.0"
@@ -55,9 +56,10 @@
     "@deepseek-ai/dsh-agent-core": "workspace:^",
     "@deepseek-ai/dsh-system-prompt": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
-    "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
+    "@deepseek-ai/dsh-stdio": "workspace:^",
     "@deepseek-ai/dsh-tool-ask-user": "workspace:^",
+    "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/dsh-user-interaction": "workspace:^",
     "cordis": "^4.0.0-rc.6",
     "schemastery": "^3.17.0"

+ 4 - 4
packages/ui/stdio-agent/src/index.ts

@@ -1,8 +1,8 @@
 /**
  * The stdio chat app: the default agent spine ({@link @deepseek-ai/dsh-agent-core}) plus the
- * coupled front-door cluster a terminal 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.
+ * coupled front-door cluster a terminal chat needs — a console logger, the independently
+ * packaged readline UI, JSONL session persistence, the user-interaction seam with its
+ * `ask_user_question` tool, and a pre-created `main` agent the UI drives.
  * Swappable adapters, executors, optional tools, and HMR stay in the leaf. This
  * Loader plugin intentionally exposes named exports only; a default export
  * would hide its `Config` schema (see docs/postmortem/0001).
@@ -19,7 +19,7 @@ import * as agentCore from '@deepseek-ai/dsh-agent-core'
 import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
 import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
 import * as toolAskUser from '@deepseek-ai/dsh-tool-ask-user'
-import * as uiStdio from './stdio-chat.ts'
+import * as uiStdio from '@deepseek-ai/dsh-stdio'
 
 export const name = 'stdio-agent'
 

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

@@ -24,6 +24,7 @@ const dshPackages = [
   'bash/tool-bash', 'support/invariants', 'ui/app-boot',
   'session-persistence/session-persistence',
   'session-persistence/session-persistence-jsonl', 'ui/stdio-agent',
+  'ui/stdio', 'ui/tool-ask-user', 'ui/user-interaction',
 ]
 const vendorPackages = [
   'cordis', 'loader', 'include', 'timer', 'hmr', 'logger-console',

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

@@ -35,6 +35,9 @@
     {
       "path": "../user-interaction"
     },
+    {
+      "path": "../stdio"
+    },
     {
       "path": "../tool-ask-user"
     },

+ 22 - 0
packages/ui/stdio/README.md

@@ -0,0 +1,22 @@
+# @deepseek-ai/dsh-stdio
+
+The terminal readline front door for DeepSeek Harness agents. It reads prompts from stdin, sends or steers them through `ctx.agents`, renders the durable `session/event` transcript to stdout, and answers `ctx.userInteraction` requests in the same terminal.
+
+This package owns the terminal channel only. It injects `agents` and `userInteraction`, then drives an agent created or resumed by app or developer code. The agent spine, agent lifecycle, console logger, and model-facing [`ask_user_question`](../tool-ask-user/README.md) tool remain separate composition entries.
+
+## Config
+
+| Key | Default | Meaning |
+|---|---|---|
+| `welcome` | `ready.` | Banner printed before the first prompt |
+| `agent` | `main` | Agent id driven by stdin and observed for EOF shutdown |
+
+The plugin seeds display labels from the live agent registry, then tracks `agent/created` and `agent/disposed` so HMR and externally managed agents render consistently. Disposal closes readline and unregisters every listener/provider through Cordis effects.
+
+```yaml
+- id: stdio
+  name: '@deepseek-ai/dsh-stdio'
+  config:
+    welcome: 'agent REPL ready. Give it a coding task.'
+    agent: main
+```

+ 42 - 0
packages/ui/stdio/package.json

@@ -0,0 +1,42 @@
+{
+  "name": "@deepseek-ai/dsh-stdio",
+  "description": "Terminal readline front door for driving and rendering DeepSeek Harness agents over stdio",
+  "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",
+    "@deepseek-ai/dsh-user-interaction": "^0.0.1",
+    "cordis": "^4.0.0-rc.6"
+  },
+  "dependencies": {
+    "schemastery": "^3.18.0"
+  },
+  "devDependencies": {
+    "@cordisjs/plugin-loader": "workspace:^",
+    "@deepseek-ai/dsh-agent": "workspace:^",
+    "@deepseek-ai/dsh-llm": "workspace:^",
+    "@deepseek-ai/dsh-session": "workspace:^",
+    "@deepseek-ai/dsh-user-interaction": "workspace:^",
+    "cordis": "^4.0.0-rc.6"
+  }
+}

+ 29 - 5
packages/ui/stdio-agent/src/stdio-chat.ts → packages/ui/stdio/src/index.ts

@@ -2,7 +2,11 @@
  * The stdio app's readline UI: reads lines from stdin into `agent.send()` or
  * `steer()`, renders the durable event stream to stdout, and exits piped input
  * only after submitted work reaches idle.
- * @module @deepseek-ai/dsh-stdio-agent/stdio-chat
+ *
+ * This package is the independently composable stdio front door. It establishes
+ * the terminal channel and drives an agent created or resumed by app or
+ * developer code.
+ * @module @deepseek-ai/dsh-stdio
  */
 
 import { createInterface } from 'node:readline'
@@ -26,8 +30,6 @@ export const inject = ['agents', 'userInteraction']
 export interface Config {
   /** Banner printed once on start, before the first `> ` prompt. */
   welcome?: string
-  // TODO(fixed-stdio-agent): this app-internal plugin is mounted only for the
-  // precreated `main` agent; remove configurability and its config-only test.
   /** Id of the agent stdin drives (`send`/`steer`) and whose status gates the EOF exit; rendering is global. Defaults to `'main'`. */
   agent?: string
 }
@@ -350,16 +352,38 @@ export function createStdioChat(ctx: Context, config: Config, runtime: StdioRunt
   }, 'ui-stdio')
 }
 
+/**
+ * Open the terminal channel once its configured agent exists. Generated stdio
+ * projects boot the Cordis tree first and create or resume the agent from
+ * developer code immediately afterward, so stdin must remain untouched until
+ * the matching `agent/created` notification arrives.
+ * @param ctx - the context supplying the agent registry and event stream.
+ * @param config - presentation and target-agent configuration.
+ * @param runtime - process-I/O seam.
+ */
+export function mountStdio(ctx: Context, config: Config, runtime: StdioRuntime): void {
+  const agentId = AgentId(config.agent ?? 'main')
+  if (ctx.agents.get(agentId) !== undefined) {
+    createStdioChat(ctx, config, runtime)
+    return
+  }
+  const dispose = ctx.on('agent/created', (agent) => {
+    if (agent.id !== agentId) return
+    dispose()
+    createStdioChat(ctx, config, runtime)
+  })
+}
+
 /**
  * Cordis entry point. Binds the real `process` streams and delegates to
- * {@link createStdioChat}; the indirection keeps the side-effecting handles out
+ * {@link mountStdio}; the indirection keeps the side-effecting handles out
  * of the testable core, which is why the unit suite drives `createStdioChat`
  * directly. This thin wrapper is exercised end-to-end by the keyless
  * Loader-path e2e smoke in `examples/echo-agent` (the real product entry).
  */
 /* v8 ignore start -- production stdio wiring; testable core is createStdioChat() (covered), exercised e2e by echo-agent keyless smoke */
 export function apply(ctx: Context, config: Config): void {
-  createStdioChat(ctx, config, {
+  mountStdio(ctx, config, {
     input: process.stdin,
     output: process.stdout,
     exit: code => process.exit(code),

+ 19 - 0
packages/ui/stdio/tests/plugin-shape.spec.ts

@@ -0,0 +1,19 @@
+import { describe, expect, it } from 'vitest'
+import Loader from '@cordisjs/plugin-loader'
+import * as stdio from '../src/index.ts'
+
+/** Real Loader export-path guard for the namespace stdio plugin. */
+describe('dsh-stdio plugin export shape', () => {
+  it('preserves name, inject, Config, and apply through Loader unwrapping', () => {
+    expect('default' in stdio).toBe(false)
+    expect(typeof stdio.apply).toBe('function')
+
+    const loader = Object.create(Loader.prototype) as Loader
+    const unwrapped = loader.unwrapExports(stdio) as Record<string, unknown>
+    expect(unwrapped).toBe(stdio)
+    expect(unwrapped.name).toBe('ui-stdio')
+    expect(unwrapped.inject).toEqual(['agents', 'userInteraction'])
+    expect(unwrapped.Config).toBeDefined()
+    expect(typeof unwrapped.apply).toBe('function')
+  })
+})

+ 2 - 2
packages/ui/stdio-agent/tests/readline.spec.ts → packages/ui/stdio/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/stdio-chat.ts'
+import type { StdioRuntime } from '../src/index.ts'
 
 const createInterface = vi.hoisted(() => vi.fn(() => {
   const reader = new EventEmitter() as EventEmitter & { close(): void }
@@ -33,7 +33,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/stdio-chat.ts')
+    const { createStdioChat } = await import('../src/index.ts')
 
     const tty = fakeRuntime(true, true)
     createStdioChat(fakeContext(), {}, tty)

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

@@ -6,7 +6,7 @@ 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 UserInteractionService from '@deepseek-ai/dsh-user-interaction'
-import { createStdioChat, type Config, type StdioRuntime } from '../src/stdio-chat.ts'
+import { createStdioChat, mountStdio, type Config, type StdioRuntime } from '../src/index.ts'
 
 /**
  * Unit tests for the stdio UI plugin. They drive the REAL plugin body
@@ -93,6 +93,55 @@ function flushExit(): Promise<void> {
   return new Promise(resolve => setTimeout(resolve, 250))
 }
 
+describe('mountStdio readiness', () => {
+  it('leaves stdin untouched until the configured agent is created', async () => {
+    const ctx = new Context()
+    await ctx.plugin(AgentRegistry)
+    await ctx.plugin(UserInteractionService)
+    const { runtime, out } = makeRuntime()
+    const fiber = await ctx.plugin(Object.assign((inner: Context) => {
+      mountStdio(inner, CONFIG, runtime)
+    }, { inject: ['agents', 'userInteraction'] }))
+
+    expect(out.text()).toBe('')
+    ctx.agents.register(makeAgent('other'))
+    expect(out.text()).toBe('')
+    ctx.agents.register(makeAgent('main'))
+    expect(out.text()).toBe('hi there\n> ')
+    await fiber.dispose()
+  })
+
+  it('opens immediately when the configured agent already exists', async () => {
+    const ctx = new Context()
+    await ctx.plugin(AgentRegistry)
+    await ctx.plugin(UserInteractionService)
+    ctx.agents.register(makeAgent('main'))
+    const { runtime, out } = makeRuntime()
+    const fiber = await ctx.plugin(Object.assign((inner: Context) => {
+      mountStdio(inner, CONFIG, runtime)
+    }, { inject: ['agents', 'userInteraction'] }))
+
+    expect(out.text()).toBe('hi there\n> ')
+    await fiber.dispose()
+  })
+
+  it('waits for main when no target agent is configured', async () => {
+    const ctx = new Context()
+    await ctx.plugin(AgentRegistry)
+    await ctx.plugin(UserInteractionService)
+    const { runtime, out } = makeRuntime()
+    const fiber = await ctx.plugin(Object.assign((inner: Context) => {
+      mountStdio(inner, { welcome: 'ready' }, runtime)
+    }, { inject: ['agents', 'userInteraction'] }))
+
+    ctx.agents.register(makeAgent('other'))
+    expect(out.text()).toBe('')
+    ctx.agents.register(makeAgent('main'))
+    expect(out.text()).toBe('ready\n> ')
+    await fiber.dispose()
+  })
+})
+
 describe('createStdioChat rendering', () => {
   it('writes the welcome banner and prompt on start', async () => {
     const { out } = await setup()

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

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

+ 28 - 0
pnpm-lock.yaml

@@ -172,6 +172,9 @@ importers:
       '@deepseek-ai/dsh-session':
         specifier: workspace:^
         version: link:../../core/session
+      '@deepseek-ai/dsh-stdio':
+        specifier: workspace:^
+        version: link:../stdio
       '@deepseek-ai/dsh-system-prompt':
         specifier: workspace:^
         version: link:../../core/system-prompt
@@ -1392,6 +1395,31 @@ 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/ui/stdio:
+    dependencies:
+      schemastery:
+        specifier: ^3.18.0
+        version: 3.18.0
+    devDependencies:
+      '@cordisjs/plugin-loader':
+        specifier: workspace:^
+        version: link:../../../vendor/loader
+      '@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
+      '@deepseek-ai/dsh-user-interaction':
+        specifier: workspace:^
+        version: link:../user-interaction
+      cordis:
+        specifier: ^4.0.0-rc.6
+        version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@vendor+loader)
+
   packages/ui/stdio-agent:
     devDependencies:
       '@cordisjs/plugin-include':

+ 1 - 0
tsconfig.build.json

@@ -61,6 +61,7 @@
     { "path": "./packages/ui/app-boot" },
     { "path": "./packages/ui/jsonrpc" },
     { "path": "./packages/ui/jsonrpc-agent" },
+    { "path": "./packages/ui/stdio" },
     { "path": "./packages/ui/stdio-agent" },
     { "path": "./packages/support/llm-replay" },
     { "path": "./packages/support/acp-snapshot" },

+ 1 - 0
tsconfig.json

@@ -72,6 +72,7 @@
     { "path": "./packages/ui/app-boot" },
     { "path": "./packages/ui/jsonrpc" },
     { "path": "./packages/ui/jsonrpc-agent" },
+    { "path": "./packages/ui/stdio" },
     { "path": "./packages/ui/stdio-agent" },
     { "path": "./packages/support/llm-replay" },
     { "path": "./packages/support/acp-snapshot" },