Forráskód Böngészése

refactor(cli): exclude live-session registry surface

Turtle 1 hónapja
szülő
commit
8f2f6ef0ac

+ 2 - 2
apps/cli/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write apps/cli/README.md
-README.md: e5a64f5a7f17d0084e6d030e7faf819d38a1df1d
-README.zh.md: 7459f356c3c7d494b4e6e8825a9b4751c3d84d1d
+README.md: 5c2b3eb77f2606928520c937d96d8f728afb062b
+README.zh.md: 927934d7265b8a81518d770e57f2b21249b67e56

+ 1 - 3
apps/cli/README.md

@@ -2,9 +2,8 @@
 
 English | [中文](README.zh.md)
 
-The `dsh` command-line entry follows the `apps/` assembly tier: `apps/*` are product assemblies over `packages/*` libraries. Plain `dsh` boots the interactive TUI coding agent, `dsh -p "task"` runs one headless turn, `dsh meta` boots that same TUI over this harness checkout, `dsh migrate` and `dsh upgrade` boot a fresh guided TUI session whose first turn invokes a bundled skill, `dsh list-sessions` lists the sessions running right now, and `dsh web` serves the browser UI.
 
-Argv is parsed once through a [Commander](https://github.com/tj/commander.js) adapter ([`src/args.ts`](src/args.ts)): one program whose default (no subcommand) is the TUI/headless surface (`--config`, `-p`/`--prompt`, `--resume`), whose `meta` subcommand is the same TUI over this checkout, whose `migrate`/`upgrade` subcommands are option-less guided-session entries, whose `list-sessions` subcommand (alias `ps`) lists live sessions, and whose `web` subcommand is the browser UI. `src/bin.ts` switches on the resolved mode and dynamic-imports only that mode's module. `dsh --help` lists every mode and `dsh web --help` renders the web usage, `dsh --version` prints this app's version, and an unknown option or a mistyped `--resume` fails loud (stderr, exit 1) instead of misrouting. Every subcommand that shares no option with the default surface — `migrate`, `upgrade`, `list-sessions`, `web` — rejects a leaked `--config`/`-p`/`--resume` rather than running and dropping it. `dsh web`'s `--host`/`--port` are unvalidated pass-through overrides: the `dsh-host-webserver` schema is the single source of both the default (the shipped `cordis.yml` value when a flag is absent) and validity, and rejects a bad value at boot. `--trusted-host` appends named authorities for the /api browser-trust fence; an all-interfaces bind additionally derives the machine's LAN IP literals itself ([`src/app-cli-entry.ts`](src/app-cli-entry.ts)), so the printed LAN URL works without flags.
+Argv is parsed once through a [Commander](https://github.com/tj/commander.js) adapter ([`src/args.ts`](src/args.ts)): one program whose default (no subcommand) is the TUI/headless surface (`--config`, `-p`/`--prompt`, `--resume`), whose `meta` subcommand is the same TUI over this checkout, whose `migrate`/`upgrade` subcommands are option-less guided-session entries, and whose `web` subcommand is the browser UI. `src/bin.ts` switches on the resolved mode and dynamic-imports only that mode's module. `dsh --help` lists every mode and `dsh web --help` renders the web usage, `dsh --version` prints this app's version, and an unknown option or a mistyped `--resume` fails loud (stderr, exit 1) instead of misrouting. Every subcommand that shares no option with the default surface — `migrate`, `upgrade`, `web` — rejects a leaked `--config`/`-p`/`--resume` rather than running and dropping it. `dsh web`'s `--host`/`--port` are unvalidated pass-through overrides: the `dsh-host-webserver` schema is the single source of both the default (the shipped `cordis.yml` value when a flag is absent) and validity, and rejects a bad value at boot. `--trusted-host` appends named authorities for the /api browser-trust fence; an all-interfaces bind additionally derives the machine's LAN IP literals itself ([`src/app-cli-entry.ts`](src/app-cli-entry.ts)), so the printed LAN URL works without flags.
 
 The TUI surface:
 
@@ -18,7 +17,6 @@ The TUI surface:
 
 `dsh migrate` and `dsh upgrade` are guided fresh-session entries over the default TUI surface: each mints a fresh session in the invoking directory and seeds its first turn with a bundled skill (`dsh-migrate` for migrating from another coding agent — opencode, pi, Claude Code, Codex; `dsh-upgrade` for upgrading this checkout), exactly as if the user typed `/skill:<name>`. The launcher passes the skill name on the boot context ([`INITIAL_SKILL_KEY`](../../packages/ui/tui/README.md)), which the TUI auto-invokes once the chat is live. Both take no options — `--config`, `-p`, and `--resume` fail loud — and seed only on this first launch, so a later `dsh --resume <id>` of the session is an ordinary TUI session with no re-injection.
 
-`dsh list-sessions` lists the sessions running right now: session id, pid, uptime, workspace, and title, newest first. It is read-only and boots no agent tree — it mounts the [session registry](../../packages/session-registry/session-registry/README.md) alone, so listing is fast and cannot start model work as a side effect. Every surface publishes its sessions into that registry through [`dsh-session-registry-live`](../../packages/session-registry/session-registry-live/README.md), and records whose process is gone are pruned on read, so a crashed session disappears without cleanup. `--json` emits the same records as a machine-readable array; an empty listing prints one line and exits 0. There is no workspace filter: the listing is always every live session, whatever directory it runs in. Only top-level surfaces appear — subagents share or spawn other processes and are deliberately invisible.
 
 The Web and headless surfaces boot one shared composition (`cordis.yml`): both treat the invoking directory as the default project and Workspace root, create named Workspaces beneath that root unless `--workspace-root <path>` overrides it, load applicable `AGENTS.md`/`CLAUDE.md` instructions into each agent-loop request prefix with a 65,536-byte render budget, and opt into first-message model titles. Headless differs only in listening on an OS-assigned port (parallel `dsh -p` runs never collide; the stderr-printed URL opens the live session in a browser). Both need the frontend dist and client bundles built (`pnpm run build && pnpm run build:web`).
 

+ 1 - 3
apps/cli/README.zh.md

@@ -2,9 +2,8 @@
 
 [English](README.md) | 中文
 
-`dsh` 命令行入口遵循 `apps/` 组装层:`apps/*` 是位于 `packages/*` 库之上的产品组装。直接运行 `dsh` 会启动交互式 TUI 编码 agent(智能体),`dsh -p "task"` 运行一个无头轮次,`dsh meta` 以本 harness checkout 为 workspace 启动同一个 TUI,`dsh migrate` 和 `dsh upgrade` 启动一个全新的引导式 TUI 会话并在首轮调用内置 skill,`dsh list-sessions` 列出此刻正在运行的会话,`dsh web` 则提供浏览器 UI。
 
-Argv 只会通过 [Commander](https://github.com/tj/commander.js) 适配器([`src/args.ts`](src/args.ts))解析一次:同一个程序的默认形式(无子命令)是 TUI/无头界面(`--config`、`-p`/`--prompt`、`--resume`),`meta` 子命令是以本 checkout 为 workspace 的同一个 TUI,`migrate`/`upgrade` 子命令是无选项的引导会话入口,`list-sessions` 子命令(别名 `ps`)列出存活会话,`web` 子命令则是浏览器 UI。`src/bin.ts` 按解析后的 mode 分支,仅动态导入该 mode 的模块。`dsh --help` 列出所有 mode,`dsh web --help` 渲染 Web 用法,`dsh --version` 打印此应用的版本;未知选项或拼错的 `--resume` 会明确报错(stderr,退出码 1),而不会被错路由。凡与默认界面不共享任何选项的子命令(`migrate`、`upgrade`、`list-sessions`、`web`)都会拒绝泄漏进来的 `--config`/`-p`/`--resume`,而不会照常运行并丢弃它。`dsh web` 的 `--host`/`--port` 是未验证的直通覆盖:`dsh-host-webserver` schema 是默认值(标志缺失时使用已交付的 `cordis.yml` 值)和有效性的唯一真源,并在启动时拒绝错误值。`--trusted-host` 为 /api 浏览器信任栅栏追加具名权威;全接口绑定还会自行推导本机的 LAN IP 字面量([`src/app-cli-entry.ts`](src/app-cli-entry.ts)),因此打印出的 LAN URL 无需任何标志即可使用。
+Argv 只会通过 [Commander](https://github.com/tj/commander.js) 适配器([`src/args.ts`](src/args.ts))解析一次:同一个程序的默认形式(无子命令)是 TUI/无头界面(`--config`、`-p`/`--prompt`、`--resume`),`meta` 子命令是以本 checkout 为 workspace 的同一个 TUI,`migrate`/`upgrade` 子命令是无选项的引导会话入口,`web` 子命令则是浏览器 UI。`src/bin.ts` 按解析后的 mode 分支,仅动态导入该 mode 的模块。`dsh --help` 列出所有 mode,`dsh web --help` 渲染 Web 用法,`dsh --version` 打印此应用的版本;未知选项或拼错的 `--resume` 会明确报错(stderr,退出码 1),而不会被错路由。凡与默认界面不共享任何选项的子命令(`migrate`、`upgrade`、`web`)都会拒绝泄漏进来的 `--config`/`-p`/`--resume`,而不会照常运行并丢弃它。`dsh web` 的 `--host`/`--port` 是未验证的直通覆盖:`dsh-host-webserver` schema 是默认值(标志缺失时使用已交付的 `cordis.yml` 值)和有效性的唯一真源,并在启动时拒绝错误值。`--trusted-host` 为 /api 浏览器信任栅栏追加具名权威;全接口绑定还会自行推导本机的 LAN IP 字面量([`src/app-cli-entry.ts`](src/app-cli-entry.ts)),因此打印出的 LAN URL 无需任何标志即可使用。
 
 TUI 界面:
 
@@ -18,7 +17,6 @@ TUI 界面:
 
 `dsh migrate` 与 `dsh upgrade` 是默认 TUI 界面之上的引导式全新会话入口:各自在调用目录中创建一个全新会话,并以一个内置 skill 播种其首轮(`dsh-migrate` 用于从其他编码 agent 迁移——opencode、pi、Claude Code、Codex;`dsh-upgrade` 用于升级本 checkout),效果等同于用户手动键入 `/skill:<name>`。启动器将 skill 名称提供到启动上下文([`INITIAL_SKILL_KEY`](../../packages/ui/tui/README.md)),TUI 在聊天就绪后自动调用它。两者都不接受任何选项——`--config`、`-p`、`--resume` 都会明确报错——且仅在首次启动时播种,因此之后 `dsh --resume <id>` 恢复该会话时是普通 TUI 会话,不会重复注入。
 
-`dsh list-sessions` 列出此刻正在运行的会话:会话 id、pid、运行时长、工作区和标题,最新的在前。它是只读的,不启动任何 agent 树——它只挂载[会话注册表](../../packages/session-registry/session-registry/README.md),因此列表既快,也不会作为副作用启动模型工作。每个界面都通过 [`dsh-session-registry-live`](../../packages/session-registry/session-registry-live/README.md) 把自己的会话发布到该注册表,进程已不存在的记录会在读取时被剪除,因此崩溃的会话无需清理便会消失。`--json` 以机器可读的数组形式输出同样的记录;空列表打印一行并以 0 退出。没有工作区过滤:列表始终是全部存活会话,无论它们运行在哪个目录下。只有顶层界面会出现——subagent 共用别的进程,或 spawn 出别的进程,因此被刻意排除在列表之外。
 
 Web 和无头界面启动同一个共享组合(`cordis.yml`):两者都将调用目录视为默认项目和 Workspace 根目录,除非通过 `--workspace-root <path>` 覆盖,否则会在该根目录下创建具名 Workspace;它们会把适用的 `AGENTS.md`/`CLAUDE.md` 指令加载到每个 agent-loop 请求前缀中,渲染预算为 65,536 字节,并选用首条消息模型标题。无头界面唯一的差异是监听操作系统分配的端口(并行 `dsh -p` 运行绝不冲突;stderr 打印的 URL 会在浏览器中打开实时会话)。两者都需要先构建前端 dist 和客户端 bundle(`pnpm run build && pnpm run build:web`)。
 

+ 0 - 3
apps/cli/package.json

@@ -72,9 +72,6 @@
     "@deepseek-ai/dsh-session-projection-cache": "workspace:^",
     "@deepseek-ai/dsh-session-query-sqlite": "workspace:^",
     "@deepseek-ai/dsh-session-reference": "workspace:^",
-    "@deepseek-ai/dsh-session-registry": "workspace:^",
-    "@deepseek-ai/dsh-session-registry-file": "workspace:^",
-    "@deepseek-ai/dsh-session-registry-live": "workspace:^",
     "@deepseek-ai/dsh-session-title": "workspace:^",
     "@deepseek-ai/dsh-session-title-first-message-llm": "workspace:^",
     "@deepseek-ai/dsh-skill": "workspace:^",

+ 23 - 148
apps/cli/src/args.ts

@@ -2,24 +2,19 @@
  * Commander adapter for the `dsh` command-line entry: the one place argv is
  * parsed and routed to a mode. `bin.ts` switches on the returned discriminant
  * and dynamic-imports that mode's module. One program: the default (no
- * subcommand) is the TUI/headless surface with option-only flags; `meta` and
- * `web` are real subcommands. Commander owns `--help`/`--version` and parse
- * errors — it prints and exits at the point of failure (a domain failure routes through
+ * subcommand) is the TUI/headless surface with option-only flags; `web` is a
+ * real subcommand. Commander owns `--help`/`--version` and parse errors — it
+ * prints and exits at the point of failure (a domain failure routes through
  * `command.error`), so this returns only a resolved mode.
  * @module @deepseek-ai/dsh/args
  */
 
 import { Command, CommanderError } from 'commander'
 
-/**
- * Interactive TUI: the default mode. `--config` applies an overlay over the
- * shipped composition in place of the personal one, `--config-replace` boots a
- * file as the whole tree instead, and `--resume <id>` rehydrates a session.
- */
+/** Interactive TUI: the default mode. `--config` swaps the tree; `--resume <id>` rehydrates a session. */
 interface TuiInvocation {
   mode: 'tui'
   config?: string
-  configReplace?: string
   resume?: string
 }
 
@@ -29,39 +24,6 @@ interface HeadlessInvocation {
   prompt: string
 }
 
-/**
- * Interactive TUI over this harness checkout: `dsh meta`. Identical to
- * {@link TuiInvocation} except the workspace is the launcher's own source tree
- * rather than the invoking directory. No `--config`: booting a foreign tree
- * against the harness workspace is the `--config` case, not this one.
- */
-interface MetaInvocation {
-  mode: 'meta'
-  resume?: string
-}
-
-/**
- * Guided fresh-session entries: `dsh migrate` seeds the first turn with the
- * `dsh-migrate` skill, `dsh upgrade` with `dsh-upgrade`. Each always mints a
- * fresh session in the invoking directory and takes no options — `--resume`,
- * `--config`, and `-p` are rejected as mistyped, so there is nothing to carry.
- */
-interface SkillSessionInvocation {
-  mode: 'migrate' | 'upgrade'
-}
-
-/**
- * List live sessions: `dsh list-sessions` (alias `dsh ps`). A read-only surface
- * that boots no agent tree — it reads the cross-process session registry and
- * exits. `json` selects the machine-readable form over the human table. There
- * is no workspace filter: the listing is always every live session, whatever
- * directory it runs in.
- */
-interface ListSessionsInvocation {
-  mode: 'list-sessions'
-  json: boolean
-}
-
 /**
  * Browser UI: `dsh web`. `host`/`port` are present only when the flag was
  * passed — pass-through overrides with no CLI default and no CLI validation:
@@ -74,8 +36,6 @@ interface ListSessionsInvocation {
  */
 interface WebInvocation {
   mode: 'web'
-  /** Overlay of loader patches applied over the shipped web composition. */
-  config?: string
   host?: string
   port?: number
   dev: boolean
@@ -85,17 +45,10 @@ interface WebInvocation {
 }
 
 /** The resolved `dsh` invocation: exactly one mode. `--help`/`--version`/errors exit inside {@link parseDshArgs}. */
-export type DshInvocation =
-  | TuiInvocation
-  | HeadlessInvocation
-  | MetaInvocation
-  | SkillSessionInvocation
-  | ListSessionsInvocation
-  | WebInvocation
+export type DshInvocation = TuiInvocation | HeadlessInvocation | WebInvocation
 
 /** Raw web-subcommand options straight from Commander. */
 interface WebOptions {
-  config?: string
   host?: string
   port?: string
   dev?: boolean
@@ -112,7 +65,6 @@ interface WebOptions {
 function resolveWeb(options: WebOptions): WebInvocation {
   return {
     mode: 'web',
-    ...options.config !== undefined && { config: options.config },
     ...options.host !== undefined && { host: options.host },
     ...options.port !== undefined && { port: Number(options.port) },
     dev: options.dev === true,
@@ -134,31 +86,21 @@ export function parseDshArgs(argv: readonly string[], version: string): DshInvoc
   const program = new Command()
     .name('dsh')
     .version(version, '-V, --version', 'output the version number')
-    .description('dsh: DeepSeek Harness — an interactive coding agent for your terminal.\nRun `dsh` with no arguments to start a session in the current directory.')
-    // The default surface takes no positional task, so `dsh "task"` fails
-    // commander's arity check with no hint; these examples are where a first
-    // reader learns the entry points and that a one-shot task rides `-p`.
-    .addHelpText('after', `
-Examples:
-  dsh                     start an interactive session in this directory
-  dsh -p "run the tests"  answer one task, print the result, and exit
-  dsh --resume <id>       continue a past session (list ids with \`dsh ps\`)
-`)
+    .description('dsh: interactive TUI (default), headless task, and browser UI')
     .exitOverride()
     // Default surface: option-only (no positional), so `web` can be a real
     // subcommand without a positional collision.
-    .option('-p, --prompt <task>', 'answer this task without the interactive UI, then exit')
-    .option('--resume <id>', 'continue a past session by id (list ids with `dsh ps`)')
-    .option('--config <path>', 'apply this overlay of loader patches instead of the personal one')
-    .option('--config-replace <path>', 'boot this file as the entire tree, ignoring the shipped and personal configuration')
-    .action((options: { config?: string; configReplace?: string; prompt?: string; resume?: string }) => {
+    .option('--config <path>', 'boot an alternate cordis.yml instead of the shipped tree (TUI mode)')
+    .option('-p, --prompt <task>', 'run one headless turn for this task, print the result, and exit')
+    .option('--resume <id>', 'resume the persisted session with this id (TUI mode)')
+    .action((options: { config?: string; prompt?: string; resume?: string }) => {
       if (options.prompt !== undefined) {
         // A headless prompt owns the invocation; an empty task has nothing to
         // run, and --config/--resume are TUI inputs that must not silently
         // vanish from a headless run.
         if (options.prompt === '') program.error('error: --prompt needs a task')
-        if (options.config !== undefined || options.configReplace !== undefined || options.resume !== undefined) {
-          program.error('error: --prompt takes no --config, --config-replace, or --resume')
+        if (options.config !== undefined || options.resume !== undefined) {
+          program.error('error: --prompt takes no --config or --resume')
         }
         resolved = { mode: 'headless', prompt: options.prompt }
         return
@@ -166,97 +108,30 @@ Examples:
       // An empty --resume= id would silently start a fresh session downstream
       // (agent-loop treats '' as no-resume), so a mistyped resume must fail loud.
       if (options.resume === '') program.error('error: --resume needs a session id')
-      // The two config flags are mutually exclusive: one layers over the shipped
-      // tree, the other discards it, so accepting both would silently drop one.
-      if (options.config !== undefined && options.configReplace !== undefined) {
-        program.error('error: --config and --config-replace are mutually exclusive')
-      }
       resolved = {
         mode: 'tui',
         ...options.config !== undefined && { config: options.config },
-        ...options.configReplace !== undefined && { configReplace: options.configReplace },
         ...options.resume !== undefined && { resume: options.resume },
       }
     })
 
-  // Commander parses the parent (default-surface) options on either side of a
-  // subcommand into `program.opts()`. For a subcommand that shares none of them,
-  // a leaked `--config`/`-p`/`--resume` is a mistyped invocation that must fail
-  // loud rather than silently run and drop the input.
-  const rejectParentOptions = (command: string): void => {
-    const parent = program.opts<{ config?: string; configReplace?: string; prompt?: string; resume?: string }>()
-    if (parent.config !== undefined || parent.configReplace !== undefined
-      || parent.prompt !== undefined || parent.resume !== undefined) {
-      program.error(`error: ${command} takes none of --config, -p/--prompt, or --resume`)
-    }
-  }
-
-  // Registration order is the rendered help order, so daily use comes first
-  // and the harness-development surfaces (`web --dev`, `meta`) come last.
-  // `migrate` and `upgrade` are guided fresh-session entries: they take no
-  // options and always mint a fresh session, so nothing is left to carry. Each
-  // description names the outcome, not the skill the first turn invokes.
-  const guided = {
-    migrate: 'import settings from another coding agent (Claude Code, Codex, opencode)',
-    upgrade: 'update this dsh installation to the latest version',
-  } as const
-  for (const mode of ['migrate', 'upgrade'] as const) {
-    program
-      .command(mode)
-      .description(guided[mode])
-      .action(() => {
-        rejectParentOptions(mode)
-        resolved = { mode }
-      })
-  }
-
-  program
-    .command('list-sessions')
-    .alias('ps')
-    .description('list sessions running right now')
-    .option('--json', 'print the records as a JSON array instead of a table')
-    .action((options: { json?: boolean }) => {
-      rejectParentOptions('list-sessions')
-      resolved = { mode: 'list-sessions', json: options.json === true }
-    })
-
-  // Host and port name no default: the CLI passes neither through when the flag
-  // is absent, so the shipped `cordis.yml` value stands and restating it here
-  // would duplicate a fact this file does not own.
-  const web = program.command('web').description('serve the browser UI on the configured host and port')
+  const web = program.command('web').description('serve the browser UI (host/port default to the shipped config)')
   web
-    .option('--config <path>', 'apply this overlay of loader patches over the shipped configuration')
-    .option('--host <host>', 'bind host; pass 0.0.0.0 to reach it from another machine')
-    .option('--port <port>', 'listen port; pass 0 to let the OS pick a free one')
-    .option('--dev', 'developer mode: hot-reload the browser client')
-    .option('--workspace-root <path>', 'parent directory for workspaces created from the browser UI')
+    .option('--host <host>', 'override the config bind host (127.0.0.1 or 0.0.0.0)')
+    .option('--port <port>', 'override the config listen port (0 requests an OS-assigned port)')
+    .option('--dev', 'mount the client HMR driver and watch plugin bundles for rebuilds')
+    .option('--workspace-root <path>', 'parent directory for name-created workspaces')
     .option('--trusted-host <authority...>', 'extra authority the /api browser-trust fence accepts (host or host:port; repeatable)')
     .action((options: WebOptions) => {
-      rejectParentOptions('web')
-      resolved = resolveWeb(options)
-    })
-
-  // `--resume` is NOT redeclared here: an option a subcommand shares with its
-  // parent parses into `program.opts()` and leaves the subcommand's own options
-  // empty, so redeclaring it would silently drop the id. Commander therefore
-  // omits it from this subcommand's option list, hence the trailing help text.
-  program
-    .command('meta')
-    .description('work on the dsh source that runs this command, from any directory')
-    .addHelpText('after', '\nAccepts --resume <id> to resume a persisted session from this checkout.\n')
-    .action(() => {
       // Commander parses the parent (default-surface) options on either side of
-      // the subcommand into `program.opts()`. `meta` accepts only `--resume`, so
-      // a leaked `--config`/`-p` is a mistyped invocation that must fail loud
-      // rather than silently be dropped.
+      // the subcommand into `program.opts()`. `web` shares none of them, so a
+      // leaked `--config`/`-p`/`--resume` is a mistyped invocation that must
+      // fail loud rather than silently start the web server and drop it.
       const parent = program.opts<{ config?: string; prompt?: string; resume?: string }>()
-      if (parent.config !== undefined || parent.prompt !== undefined) {
-        program.error('error: meta takes neither --config nor -p/--prompt')
+      if (parent.config !== undefined || parent.prompt !== undefined || parent.resume !== undefined) {
+        program.error('error: web takes none of --config, -p/--prompt, or --resume')
       }
-      // Same reason as the default surface: an empty id would start a fresh
-      // session downstream instead of failing the mistyped resume.
-      if (parent.resume === '') program.error('error: --resume needs a session id')
-      resolved = { mode: 'meta', ...parent.resume !== undefined && { resume: parent.resume } }
+      resolved = resolveWeb(options)
     })
 
   try {

+ 2 - 18
apps/cli/src/bin.ts

@@ -30,7 +30,7 @@ const invocation = parseDshArgs(process.argv.slice(2), readVersion())
 switch (invocation.mode) {
   case 'web': {
     const { runWeb } = await import('./web.ts')
-    await runWeb(invocation.host, invocation.port, invocation.dev, invocation.workspaceRoot, invocation.trustedHosts, invocation.config)
+    await runWeb(invocation.host, invocation.port, invocation.dev, invocation.workspaceRoot, invocation.trustedHosts)
     break
   }
   case 'headless': {
@@ -40,23 +40,7 @@ switch (invocation.mode) {
   }
   case 'tui': {
     const { runTui } = await import('./tui.ts')
-    await runTui(invocation.config, invocation.resume, undefined, undefined, invocation.configReplace)
-    break
-  }
-  case 'meta': {
-    const { runMeta } = await import('./tui.ts')
-    await runMeta(invocation.resume)
-    break
-  }
-  case 'list-sessions': {
-    const { runListSessions } = await import('./list-sessions.ts')
-    await runListSessions(invocation.json)
-    break
-  }
-  case 'migrate':
-  case 'upgrade': {
-    const { runSkillSession } = await import('./tui.ts')
-    await runSkillSession(`dsh-${invocation.mode}`)
+    await runTui(invocation.config, invocation.resume)
     break
   }
   default:

+ 1 - 4
apps/cli/src/headless.ts

@@ -14,7 +14,6 @@ import type { MuxFrame } from '@deepseek-ai/dsh-host-apiproxy/api'
 import type { RpcRequest, RpcResponse } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
 import type { SessionId } from '@deepseek-ai/dsh-session'
 import { AppCLIEntry } from './app-cli-entry.ts'
-import { registerLiveSessions } from './register-session.ts'
 
 /** Outcome of one headless turn: aggregated final text plus the turn-end reason kind. */
 interface TurnOutcome {
@@ -76,13 +75,11 @@ async function consumeUntilTurnEnd(frames: AsyncIterable<RpcRequest<MuxFrame>>,
 export async function runHeadless(task: string): Promise<void> {
   // A missing DEEPSEEK_API_KEY throws here (plugin load is fail-loud, uncaught by design).
   const entry = new AppCLIEntry({
-    configPath: fileURLToPath(new URL('../base.cordis.yml', import.meta.url)),
-    overlayPath: fileURLToPath(new URL('../web.cordis.yml', import.meta.url)),
+    configPath: fileURLToPath(new URL('../cordis.yml', import.meta.url)),
     dev: false,
     port: 0,
   })
   const { ctx, port } = await entry.run()
-  await registerLiveSessions(ctx)
   const dispose = async (): Promise<void> => { await ctx.fiber.dispose() }
   // The headless session is web-observable while it runs (same composition).
   process.stderr.write(`dsh: observing at http://127.0.0.1:${String(port)}\n`)

+ 0 - 101
apps/cli/src/list-sessions.ts

@@ -1,101 +0,0 @@
-/**
- * `dsh list-sessions` (alias `dsh ps`) — list the sessions running right now.
- *
- * A read-only surface: it mounts the session registry alone and never boots an
- * agent tree, so listing stays fast and cannot start model work as a side
- * effect. Liveness comes from the registry, which prunes records whose process
- * is gone, and every displayed field including the title comes from the record,
- * so no session log is opened and no backend format is assumed.
- * @module @deepseek-ai/dsh/list-sessions
- */
-
-import { Context } from 'cordis'
-import { type SessionRegistryRecord } from '@deepseek-ai/dsh-session-registry'
-import SessionRegistryFile from '@deepseek-ai/dsh-session-registry-file'
-import { registryRoot } from './register-session.ts'
-
-/** Column header text, also the minimum width of each column. */
-const HEADERS = ['SESSION', 'PID', 'UPTIME', 'WORKSPACE', 'TITLE'] as const
-
-/** Shown when a session has no title yet. */
-const NO_TITLE = '—'
-
-/**
- * Render milliseconds of uptime as a compact human duration.
- * @param ms - elapsed milliseconds since the session registered.
- * @returns a short duration such as `12s`, `4m`, or `2h14m`.
- */
-export function formatUptime(ms: number): string {
-  const seconds = Math.max(0, Math.floor(ms / 1000))
-  if (seconds < 60) return `${String(seconds)}s`
-  const minutes = Math.floor(seconds / 60)
-  if (minutes < 60) return `${String(minutes)}m`
-  const hours = Math.floor(minutes / 60)
-  const remainder = minutes % 60
-  if (hours < 24) return remainder === 0 ? `${String(hours)}h` : `${String(hours)}h${String(remainder)}m`
-  const days = Math.floor(hours / 24)
-  const leftoverHours = hours % 24
-  return leftoverHours === 0 ? `${String(days)}d` : `${String(days)}d${String(leftoverHours)}h`
-}
-
-/** One fully-resolved listing row, in column order. */
-type Row = readonly [string, string, string, string, string]
-
-/**
- * Build the display rows for a listing, newest session first.
- * @param records - the live records to render.
- * @param now - the current epoch milliseconds uptime is measured against.
- * @returns one row per record, each already stringified per column.
- */
-export function buildRows(records: readonly SessionRegistryRecord[], now: number): Row[] {
-  return [...records]
-    .sort((left, right) => right.startedAt - left.startedAt)
-    .map(record => [
-      record.sessionId,
-      String(record.pid),
-      formatUptime(now - record.startedAt),
-      record.cwd,
-      record.title ?? NO_TITLE,
-    ] as const)
-}
-
-/**
- * Render rows as a left-aligned table with a header line.
- *
- * The last column is never padded, so a long title cannot add trailing
- * whitespace to every line.
- * @param rows - the rows to render, already stringified.
- * @returns the complete table text, newline-terminated.
- */
-export function renderTable(rows: readonly Row[]): string {
-  const widths = HEADERS.map((header, column) =>
-    Math.max(header.length, ...rows.map(row => row[column]?.length ?? 0)))
-  const line = (cells: readonly string[]): string =>
-    cells.map((cell, column) => column === cells.length - 1 ? cell : cell.padEnd(widths[column] ?? 0)).join('  ').trimEnd()
-  return [line(HEADERS), ...rows.map(row => line(row))].join('\n') + '\n'
-}
-
-/**
- * List live sessions and exit. Prints a table by default, or a JSON array with
- * `--json`; an empty listing is a success, not an error.
- * @param json - emit the machine-readable JSON array instead of the table.
- */
-export async function runListSessions(json: boolean): Promise<void> {
-  const ctx = new Context()
-  await ctx.plugin(SessionRegistryFile, { root: registryRoot() })
-  const records = await ctx.sessionRegistry.list()
-  await ctx.fiber.dispose()
-
-  if (json) {
-    const rows = [...records]
-      .sort((left, right) => right.startedAt - left.startedAt)
-      .map(record => ({ ...record, uptimeMs: Date.now() - record.startedAt, title: record.title ?? null }))
-    process.stdout.write(`${JSON.stringify(rows, undefined, 2)}\n`)
-    return
-  }
-  if (records.length === 0) {
-    process.stdout.write('no dsh sessions running\n')
-    return
-  }
-  process.stdout.write(renderTable(buildRows(records, Date.now())))
-}

+ 0 - 44
apps/cli/src/register-session.ts

@@ -1,44 +0,0 @@
-/**
- * Mounts the cross-process live-session registry that `dsh list-sessions` reads, plus the
- * publisher that keeps it in step with this process's sessions.
- *
- * Both plugins mount on the booted app's own context, so records share that
- * fiber's lifetime: an ordinary exit disposes the fiber and deregisters, while a
- * killed process leaves records the next reader prunes by pid. Only top-level
- * surfaces a user launches mount this — in-process subagents have no process of
- * their own, and out-of-process subagent backends spawn `dsh-jsonrpc-agent`
- * rather than this CLI, so neither reaches this path.
- * @module @deepseek-ai/dsh/register-session
- */
-
-import { join } from 'node:path'
-import type { Context } from 'cordis'
-import { resolveDshHome } from '@deepseek-ai/dsh-paths'
-import SessionRegistryFile from '@deepseek-ai/dsh-session-registry-file'
-import * as sessionRegistryLive from '@deepseek-ai/dsh-session-registry-live'
-
-/** Registry root under the Harness home, shared by every surface and by `dsh list-sessions`. */
-export const registryRoot = (): string => join(resolveDshHome(), 'run')
-
-/**
- * Publish this process's sessions for the lifetime of `ctx`.
- *
- * Publication follows session lifecycle rather than a launcher-known id, so one
- * path serves every surface identically — the TUI's single session and a
- * server's on-demand ones alike — and titles reach the listing as they are
- * logged.
- *
- * Mounting is best-effort: a registry failure must not take down a working agent
- * session, because the registry is an observability aid rather than part of the
- * agent's contract. Failures warn through the context logger.
- * @param ctx - the booted app context whose lifetime the records share.
- */
-export async function registerLiveSessions(ctx: Context): Promise<void> {
-  try {
-    const scope = ctx.isolate('sessionRegistry')
-    await scope.plugin(SessionRegistryFile, { root: registryRoot() })
-    await scope.plugin(sessionRegistryLive)
-  } catch (error) {
-    ctx.logger('dsh').warn('session registry unavailable; `dsh list-sessions` will not list these sessions: %s', String(error))
-  }
-}

+ 3 - 8
apps/cli/src/tui.ts

@@ -33,7 +33,6 @@ import { resolveDshHome, resolveSessionsRoot } from '@deepseek-ai/dsh-paths'
 import { SessionId } from '@deepseek-ai/dsh-session'
 import { CONFIGURED_AGENT_IDENTITIES_KEY } from '@deepseek-ai/dsh-agent-loop'
 import type { Context } from 'cordis'
-import { registerLiveSessions } from './register-session.ts'
 import {
   INITIAL_SKILL_KEY,
   MAIN_SESSION_ID_KEY,
@@ -71,8 +70,7 @@ const SOURCE_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
  * shared session-store root, `sessions` under the Harness home. Shared-store
  * policy is the launcher's alone — the app bundle treats the slot as opaque and
  * keeps a project-local fallback, so only `dsh` decides that sessions are
- * shared across working directories (making `/resume` and `list-sessions` span
- * every workspace).
+ * shared across working directories (making `/resume` span every workspace).
  * @returns the absolute session-store root this launcher shares.
  */
 export function launcherSessionsRoot(): string {
@@ -238,8 +236,8 @@ export async function runTui(
       hostCtx.provide(MAIN_SESSION_ID_KEY, identity)
       hostCtx.provide(TUI_GOODBYE_MESSAGE_KEY, goodbye)
       // Shared-store policy is the launcher's: sessions live in one root under
-      // the Harness home across every cwd, so /resume and list-sessions see
-      // every workspace. The bundle treats the slot as opaque.
+      // the Harness home across every cwd, so /resume sees every workspace.
+      // The bundle treats the slot as opaque.
       hostCtx.provide(SESSIONS_ROOT_KEY, launcherSessionsRoot())
       // The agent-loop row reads this to bind `main`, and the tui row reads the
       // same id, so a personal overlay repointing the model route cannot drop
@@ -258,8 +256,5 @@ export async function runTui(
   )
   app.current = ctx
   addHarnessSourceSection(ctx, SOURCE_ROOT)
-  // Publication follows the store; meta mode already chdir'd, so each session
-  // reports its own cwd.
-  await registerLiveSessions(ctx)
 }
 /* v8 ignore stop */

+ 2 - 12
apps/cli/src/web.ts

@@ -7,13 +7,9 @@
  */
 
 import { fileURLToPath } from 'node:url'
-import { resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
 import { AppCLIEntry } from './app-cli-entry.ts'
-import { registerLiveSessions } from './register-session.ts'
 
-// The shared core every `dsh` surface mounts, plus this surface's overlay over it.
-const BASE_CONFIG = fileURLToPath(new URL('../base.cordis.yml', import.meta.url))
-const WEB_OVERLAY = fileURLToPath(new URL('../web.cordis.yml', import.meta.url))
+const CONFIG_PATH = fileURLToPath(new URL('../cordis.yml', import.meta.url))
 
 // Display-only mirror of the webserver schema's loopback host: the address the
 // local URL always prints. Not a source of truth — the schema is.
@@ -27,8 +23,6 @@ const LOOPBACK_HOST = '127.0.0.1'
  * @param dev - mount the client HMR driver and watch plugin bundles for rebuilds.
  * @param workspaceRoot - parent directory for name-created workspaces, or `undefined` for the gateway's cwd fallback.
  * @param trustedHosts - extra authorities for the /api browser-trust fence, or `undefined` for the derived LAN literals alone.
- * @param config - an overlay of loader patches applied over the shipped web
- * composition, or `undefined` for none; already parsed from `--config`.
  */
 export async function runWeb(
   host: string | undefined,
@@ -36,12 +30,9 @@ export async function runWeb(
   dev: boolean,
   workspaceRoot: string | undefined,
   trustedHosts: string[] | undefined,
-  config?: string,
 ): Promise<void> {
   const entry = new AppCLIEntry({
-    configPath: BASE_CONFIG,
-    overlayPath: WEB_OVERLAY,
-    ...config !== undefined && { extraOverlayPath: resolveConfigPath(config, undefined) },
+    configPath: CONFIG_PATH,
     dev,
     ...host !== undefined && { host },
     ...port !== undefined && { port },
@@ -49,7 +40,6 @@ export async function runWeb(
     ...trustedHosts !== undefined && { trustedHosts },
   })
   const { ctx, port: boundPort } = await entry.run()
-  await registerLiveSessions(ctx)
 
   let exiting = false
   const shutdown = (code: number): void => {

+ 1 - 38
apps/cli/tests/args.spec.ts

@@ -24,33 +24,17 @@ function exitCode(argv: string[]): number {
 afterEach(() => { vi.restoreAllMocks() })
 
 describe('parseDshArgs', () => {
-  it('routes each mode by its shape: default TUI, -p headless, meta and web subcommands', () => {
+  it('routes each mode by its shape: default TUI, -p headless, web subcommand', () => {
     expect(parse([])).toEqual({ mode: 'tui' })
     expect(parse(['--config', 'custom.yml'])).toEqual({ mode: 'tui', config: 'custom.yml' })
     expect(parse(['--resume', 'sess', '--config', 'app.yml'])).toEqual({ mode: 'tui', config: 'app.yml', resume: 'sess' })
     expect(parse(['-p', 'do the thing'])).toEqual({ mode: 'headless', prompt: 'do the thing' })
-    // `meta` accepts `--resume` but does not redeclare it: a shared option parses
-    // into program.opts() on either side of the subcommand, and redeclaring it
-    // would leave the subcommand's own options empty and drop the id.
-    expect(parse(['meta'])).toEqual({ mode: 'meta' })
-    expect(parse(['meta', '--resume', 'sess'])).toEqual({ mode: 'meta', resume: 'sess' })
-    expect(parse(['--resume', 'sess', 'meta'])).toEqual({ mode: 'meta', resume: 'sess' })
-    // Credential setup is option-free: it writes the Harness-home .env, so
-    // there is nothing for a flag to select.
     // Bare `web` carries no host/port: the shipped cordis.yml owns the default.
     expect(parse(['web'])).toEqual({ mode: 'web', dev: false })
     // Host/port are unvalidated pass-throughs (the webserver schema gates them
     // at boot); the adapter only coerces the port string to a number.
     expect(parse(['web', '--host', '0.0.0.0', '--port', '8080', '--dev', '--workspace-root', '/w']))
       .toEqual({ mode: 'web', host: '0.0.0.0', port: 8080, dev: true, workspaceRoot: '/w' })
-    // Guided fresh-session entries carry nothing: bare mode discriminant only.
-    expect(parse(['migrate'])).toEqual({ mode: 'migrate' })
-    expect(parse(['upgrade'])).toEqual({ mode: 'upgrade' })
-    // `list-sessions` has one option and no workspace filter: the listing is always
-    // global. `ps` is its alias and resolves to the same mode.
-    expect(parse(['list-sessions'])).toEqual({ mode: 'list-sessions', json: false })
-    expect(parse(['list-sessions', '--json'])).toEqual({ mode: 'list-sessions', json: true })
-    expect(parse(['ps', '--json'])).toEqual({ mode: 'list-sessions', json: true })
     // --trusted-host is variadic and repeatable; authorities pass through unvalidated.
     expect(parse(['web', '--trusted-host', 'harness.internal:3080', 'lab.internal', '--trusted-host', '10.0.0.9']))
       .toEqual({ mode: 'web', dev: false, trustedHosts: ['harness.internal:3080', 'lab.internal', '10.0.0.9'] })
@@ -71,27 +55,6 @@ describe('parseDshArgs', () => {
     expect(exitCode(['web', '-p', 'task'])).toBe(1)
     expect(exitCode(['web', '--resume', 's'])).toBe(1)
     expect(exitCode(['--config', 'c.yml', 'web'])).toBe(1)
-    // Same rule for credential setup: it shares no option with the default
-    // surface, so a leaked flag is a typo, not something to ignore.
-    // `meta` fixes its own config tree and is interactive, so --config/-p are
-    // rejected; an empty id is swallowed downstream exactly as above.
-    expect(exitCode(['meta', '--resume='])).toBe(1)
-    expect(exitCode(['meta', '--config', 'c.yml'])).toBe(1)
-    expect(exitCode(['meta', '-p', 'task'])).toBe(1)
-    // `migrate`/`upgrade` take no options: any leaked default-surface flag is a
-    // mistyped invocation, not a silently-dropped input.
-    expect(exitCode(['migrate', '--resume', 's'])).toBe(1)
-    expect(exitCode(['migrate', '--config', 'c.yml'])).toBe(1)
-    expect(exitCode(['migrate', '-p', 'task'])).toBe(1)
-    expect(exitCode(['upgrade', '--resume', 's'])).toBe(1)
-    expect(exitCode(['upgrade', '--config', 'c.yml'])).toBe(1)
-    expect(exitCode(['-p', 'task', 'upgrade'])).toBe(1)
-    // `list-sessions`/`ps` is read-only and shares no default-surface option: a leaked flag is a
-    // mistyped invocation, not a listing with a silently dropped input.
-    expect(exitCode(['ps', '--resume', 's'])).toBe(1)
-    expect(exitCode(['list-sessions', '--config', 'c.yml'])).toBe(1)
-    expect(exitCode(['list-sessions', '-p', 'task'])).toBe(1)
-    expect(exitCode(['--resume', 's', 'ps'])).toBe(1)
   })
 
   it('exits 0 for --help (disclosing web) and --version', () => {

+ 6 - 49
apps/cli/tests/built-bin.e2e.ts

@@ -1,9 +1,8 @@
-import { existsSync, mkdtempSync, rmSync } from 'node:fs'
-import { tmpdir } from 'node:os'
+import { existsSync } from 'node:fs'
 import { join } from 'node:path'
 import { fileURLToPath } from 'node:url'
 import { execa } from 'execa'
-import { afterEach, beforeEach, describe, expect, it } from 'vitest'
+import { describe, expect, it } from 'vitest'
 
 /**
  * Published-entry smoke for the `dsh` bin: run the built `lib/bin.js` under
@@ -17,28 +16,19 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
  * node_modules, so no external consumer is assembled; missing-config fail-loud
  * and full-boot coverage for the shared dsh-app-boot glue live in cli-demo's
  * built-bin suite, and interactive TTY behavior is PTY-covered by
- * examples/tui-agent. `dsh list-sessions` is covered here too: it is the one surface that
- * boots no agent tree, so the built bin is the whole product path.
- * Skips before the bin is built.
+ * examples/tui-agent. Skips before the bin is built.
  */
 
 const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
 const dshBin = join(repoRoot, 'apps/cli/lib/bin.js')
 
-/**
- * Run the built bin with PIPED stdio (stdin closed at EOF); resolve with output
- * + exit code. `env` isolates the Harness home for surfaces that read it.
- */
-async function runBuiltBin(
-  args: readonly string[] = [],
-  env: Record<string, string> = {},
-): Promise<{ stdout: string; code: number; stderr: string }> {
-  const result = await execa(process.execPath, [dshBin, ...args], {
+/** Run the built bin with PIPED stdio (stdin closed at EOF); resolve with output + exit code. */
+async function runBuiltBin(): Promise<{ stdout: string; code: number; stderr: string }> {
+  const result = await execa(process.execPath, [dshBin], {
     input: '',
     timeout: 25_000,
     killSignal: 'SIGKILL',
     reject: false,
-    env,
   })
   if (result.timedOut) {
     throw new Error(`dsh built bin did not exit within 25s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
@@ -55,37 +45,4 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
     // The refusal happens before any plugin mounts: stdout stays silent.
     expect(stdout).toBe('')
   }, 30_000)
-
-  describe('dsh list-sessions', () => {
-    let home: string
-    beforeEach(() => { home = mkdtempSync(join(tmpdir(), 'dsh-ls-bin-')) })
-    afterEach(() => { rmSync(home, { recursive: true, force: true }) })
-
-    it('reports an empty listing as success, not an error', async () => {
-      const { stdout, code, stderr } = await runBuiltBin(['list-sessions'], { DSH_HOME: home })
-      expect(code).toBe(0)
-      expect(stdout.trim()).toBe('no dsh sessions running')
-      expect(stderr).toBe('')
-    }, 30_000)
-
-    it('emits an empty JSON array for machines', async () => {
-      const { stdout, code } = await runBuiltBin(['ps', '--json'], { DSH_HOME: home })
-      expect(code).toBe(0)
-      expect(JSON.parse(stdout)).toEqual([])
-    }, 30_000)
-
-    it('runs without a TTY, unlike the TUI surface', async () => {
-      // The listing is read-only and boots no agent tree, so piped stdio — the
-      // launch the TUI refuses — is a supported way to run it.
-      const { code, stderr } = await runBuiltBin(['ps'], { DSH_HOME: home })
-      expect(code).toBe(0)
-      expect(stderr).not.toContain('interactive TTYs')
-    }, 30_000)
-
-    it('rejects a leaked default-surface flag instead of listing', async () => {
-      const { code, stderr } = await runBuiltBin(['list-sessions', '--resume', 'sess'], { DSH_HOME: home })
-      expect(code).not.toBe(0)
-      expect(stderr).toContain('list-sessions takes none of')
-    }, 30_000)
-  })
 })

+ 0 - 74
apps/cli/tests/list-sessions.spec.ts

@@ -1,74 +0,0 @@
-/**
- * Tests for the `dsh list-sessions` presentation layer: uptime formatting, row building
- * (newest first, absent-title placeholder) and table alignment without trailing
- * padding. Every displayed field comes from the record, so there is no log
- * reading to cover here.
- */
-
-import { describe, expect, it } from 'vitest'
-import { SessionId } from '@deepseek-ai/dsh-session'
-import { BootId, type SessionRegistryRecord } from '@deepseek-ai/dsh-session-registry'
-import { buildRows, formatUptime, renderTable } from '../src/list-sessions.ts'
-
-function record(overrides: Partial<SessionRegistryRecord> = {}): SessionRegistryRecord {
-  return {
-    sessionId: SessionId('sess-1'),
-    pid: 4242,
-    cwd: '/work/project',
-    startedAt: 1_000,
-    bootId: BootId('boot-1'),
-    ...overrides,
-  }
-}
-
-describe('formatUptime', () => {
-  it.each([
-    [0, '0s'],
-    [999, '0s'],
-    [12_000, '12s'],
-    [59_999, '59s'],
-    [60_000, '1m'],
-    [3_540_000, '59m'],
-    [3_600_000, '1h'],
-    [8_040_000, '2h14m'],
-    [86_400_000, '1d'],
-    [90_000_000, '1d1h'],
-  ])('renders %ims as %s', (ms, expected) => {
-    expect(formatUptime(ms)).toBe(expected)
-  })
-
-  it('never renders a negative duration for a clock that moved backwards', () => {
-    expect(formatUptime(-5_000)).toBe('0s')
-  })
-})
-
-describe('buildRows', () => {
-  it('orders newest first and marks a missing title', () => {
-    const rows = buildRows([
-      record({ sessionId: SessionId('older'), startedAt: 1_000 }),
-      record({ sessionId: SessionId('newer'), startedAt: 5_000 }),
-    ], 65_000)
-    expect(rows.map(row => row[0])).toEqual(['newer', 'older'])
-    expect(rows[0]).toEqual(['newer', '4242', '1m', '/work/project', '—'])
-  })
-})
-
-describe('renderTable', () => {
-  it('aligns columns and leaves no trailing whitespace', () => {
-    const table = renderTable(buildRows([
-      record({ sessionId: SessionId('short'), startedAt: 0, title: 'a title' }),
-      record({ sessionId: SessionId('a-much-longer-session-id'), startedAt: 1, title: 'a title' }),
-    ], 1_000))
-    const lines = table.split('\n')
-    expect(lines[0]).toMatch(/^SESSION {18}\s+PID/)
-    for (const line of lines) expect(line).toBe(line.trimEnd())
-    // The header and every row align on the same column starts.
-    const pidColumn = (line: string): number => line.includes('4242') ? line.indexOf('4242') : line.indexOf('PID')
-    expect(pidColumn(lines[1] ?? '')).toBe(pidColumn(lines[0] ?? ''))
-    expect(pidColumn(lines[2] ?? '')).toBe(pidColumn(lines[0] ?? ''))
-  })
-
-  it('renders a header even with no rows, so the columns stay discoverable', () => {
-    expect(renderTable([])).toBe('SESSION  PID  UPTIME  WORKSPACE  TITLE\n')
-  })
-})

+ 0 - 9
apps/cli/tsconfig.json

@@ -29,15 +29,6 @@
     {
       "path": "../../packages/util/paths"
     },
-    {
-      "path": "../../packages/session-registry/session-registry"
-    },
-    {
-      "path": "../../packages/session-registry/session-registry-file"
-    },
-    {
-      "path": "../../packages/session-registry/session-registry-live"
-    },
     {
       "path": "../../packages/client/connection"
     },

+ 0 - 9
pnpm-lock.yaml

@@ -284,15 +284,6 @@ importers:
       '@deepseek-ai/dsh-session-reference':
         specifier: workspace:^
         version: link:../../packages/context/session-reference
-      '@deepseek-ai/dsh-session-registry':
-        specifier: workspace:^
-        version: link:../../packages/session-registry/session-registry
-      '@deepseek-ai/dsh-session-registry-file':
-        specifier: workspace:^
-        version: link:../../packages/session-registry/session-registry-file
-      '@deepseek-ai/dsh-session-registry-live':
-        specifier: workspace:^
-        version: link:../../packages/session-registry/session-registry-live
       '@deepseek-ai/dsh-session-title':
         specifier: workspace:^
         version: link:../../packages/session-title/session-title