Merge commit '5ae8c6487ad9556b8793b890a4fe5ece24c55805' into codex/product-subagent-one-shot-background

# Conflicts:
#	packages/subagent/subagent-codex/README.i18n.yaml
#	packages/subagent/subagent-codex/README.zh.md
#	packages/subagent/tool-subagent/tests/tool-subagent.spec.ts
This commit is contained in:
pku-xht
2026-08-13 15:50:23 +08:00
4174 changed files with 66693 additions and 36015 deletions

View File

@@ -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: 4fbd0692a2df403c6395235e096e193c994ea198
README.zh.md: 7861f5b4f8cb447aff01ec8b64b12fd298485b4b
README.md: 9a8d722b044ed5d8e31e3c27e54f8c9ef0839f82
README.zh.md: 7889b4180b5ce2f65550f98708d498f324ac349d

View File

@@ -9,7 +9,7 @@ The `dsh` command is the product launcher for profiles: ordered stacks of plugin
| Command | Purpose |
|---|---|
| `dsh --profile <name>` | Boot the named profile under `$DSH_HOME/profiles/<name>`. |
| `dsh --profile headless "task"` | Run one fresh persisted session, print the final answer, and exit. |
| `dsh --profile headless "job"` | Run one fresh persisted session, print the final answer, and exit. |
| `dsh web` | Alias of `--profile web`. |
| `dsh plugin --profile <name> <pnpm args>` | Manage a profile's plugins by forwarding to pnpm in the profile directory. |
@@ -21,7 +21,7 @@ The launcher parses only its own flags and hands everything after them to the bo
```sh
dsh --profile web --port 8080 # --port belongs to the web app
dsh --profile tui --resume <id> # --resume belongs to the terminal app
dsh --profile tui --resume <id> # example, assuming the tui profile is installed; --resume belongs to the terminal app
dsh --profile headless "run the tests"
dsh --profile web --help # the web app's flags, not the launcher's
dsh --help # the launcher's own help
@@ -29,7 +29,16 @@ dsh --help # the launcher's own help
## Profiles
A profile directory holds a `package.json` (out-of-tree plugin dependencies plus the profile manifest `dsh.profile` with its ordered `bundles` list) and a `cordis.patch.yml` (the user's own patch layer, hot-reloaded on long-lived surfaces). The tree composes over an empty root: each bundle's patch in `dsh.profile.bundles` order, then the profile's `cordis.patch.yml`, then the home-level `$DSH_HOME/cordis.patch.yml`, then `--patch` overlays. Bundles named in `dsh.profile.bundles` resolve from the dsh installation first (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins. Use `--dump-default-config` and `--dump-config` to inspect the composed tree without booting it.
A profile directory holds a `package.json` (out-of-tree plugin dependencies plus the profile manifest `dsh.profile` with its ordered `bundles` list) and a `cordis.patch.yml` (the user's own patch layer).
The tree composes over an empty root:
- each bundle's patch in `dsh.profile.bundles` order
- then the profile's `cordis.patch.yml`, then the home-level `$DSH_HOME/cordis.patch.yml`
- then `--patch` overlays
Bundles named in `dsh.profile.bundles` resolve from the dsh installation first (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins.
Use `--dump-default-config` and `--dump-config` to inspect the composed tree without booting it.
The [CLI behavior reference](reference/README.md) owns exact layer precedence, flags, shutdown behavior, deployment defaults, and source execution.

View File

@@ -2,26 +2,26 @@
[English](README.md) | 中文
`dsh` 命令是 profile 的产品启动器:profile 是按序叠放的插件组合包 patch 层,之上再叠加用户自己的覆盖。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项、配置错误和启动失败都会以非零状态退出。
`dsh` DeepSeek Harness 中用于启动 profile 的命令;profile 由多个插件组合包 patch 层按顺序叠加而成,其上再应用用户自己的覆盖配置。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项、配置错误和启动失败都会以非零状态退出。
## 入口模式
| 命令 | 用途 |
|---|---|
| `dsh --profile <name>` | 启动位于 `$DSH_HOME/profiles/<name>` 的指定 profile。 |
| `dsh --profile headless "task"` | 运行一个新的持久化会话,打印最终答案并退出。 |
| `dsh --profile headless "job"` | 运行一个新的持久化会话,打印最终答案并退出。 |
| `dsh web` | `--profile web` 的别名。 |
| `dsh plugin --profile <name> <pnpm args>` | 通过在 profile 目录中转发给 pnpm 来管理该 profile 的插件。 |
调用目录是默认 workspace 根目录。`web``headless` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。
运行命令时所在的目录将作为默认 workspace 根目录。`web``headless` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。
## 应用参数
启动器只解析属于自己的 flag,并把其后的一切交给启动起来的 profile,任何注入它的应用插件都可以解析这份共享的不可变快照([`dsh-cmdline`](../../packages/boot/cmdline/README.md))。因此启动器的 flag 必须写在前面,而启动器不认识的第一个 token 就是应用参数的起点:
启动器只解析自身的 flag,并将其后的所有内容交给启动的 profile;注入该 profile 的任意应用插件都可以解析这份共享的不可变快照[`dsh-cmdline`](../../packages/boot/cmdline/README.md)。因此启动器的 flag 必须写在前面启动器无法识别的第一个 token 标志着应用参数的开始:
```sh
dsh --profile web --port 8080 # --port belongs to the web app
dsh --profile tui --resume <id> # --resume belongs to the terminal app
dsh --profile tui --resume <id> # example, assuming the tui profile is installed; --resume belongs to the terminal app
dsh --profile headless "run the tests"
dsh --profile web --help # the web app's flags, not the launcher's
dsh --help # the launcher's own help
@@ -29,10 +29,19 @@ dsh --help # the launcher's own help
## Profile
profile 目录包含一个 `package.json`树外插件依赖,加上 profile manifest元数据清单`dsh.profile` 及其有序`bundles` 列表)和一个 `cordis.patch.yml`用户自己的 patch 层,在长期运行的 surface 上热重载)。配置树在空根之上组合:先按 `dsh.profile.bundles` 顺序应用各组合包的 patch然后是 profile 的 `cordis.patch.yml`,然后是 home 级的 `$DSH_HOME/cordis.patch.yml`,然后是 `--patch` overlay。`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base``@deepseek-ai/dsh-web-app``@deepseek-ai/dsh-headless`),再从 profile 自己的 `node_modules` 解析pnpm 把树外插件安装在后者。使用 `--dump-default-config``--dump-config` 可在不启动的情况下检查组合后的配置树
profile 目录包含一个 `package.json`,其中记录树外插件依赖,以及 profile manifest元数据清单`dsh.profile` 和其中按顺序排列`bundles` 列表;还包含一个 `cordis.patch.yml`,其中保存用户自己的 patch 层。
[CLI命令行界面行为参考](reference/README.md)负责确切的层优先级、flag、关闭行为、部署默认值和源码执行。
配置树以空根为起点,依次叠加以下配置层:
- `dsh.profile.bundles` 中各组合包的 patch
- profile 自身的 `cordis.patch.yml`,然后是 home 级的 `$DSH_HOME/cordis.patch.yml`
- `--patch` 指定的覆盖层
`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base``@deepseek-ai/dsh-web-app``@deepseek-ai/dsh-headless`),再从 profile 自身的 `node_modules` 解析pnpm 会将树外插件安装到该目录。
使用 `--dump-default-config``--dump-config` 可在不启动的情况下检查组合后的配置树。
层的确切优先级、flag、关闭行为、部署默认值和源码执行方式以 [CLI命令行界面行为参考](reference/README.md)为准。
## 开发
生产运行需要已构建的包与前端产物。请在仓库根目录单独运行 `pnpm run build`,然后使用 `pnpm dsh <args...>` 运行 TypeScript 入口并转发所有参数;模块解析约定[源码执行参考](reference/README.md#source-execution)负责
生产运行需要已构建的包与前端产物。请在仓库根目录单独运行 `pnpm run build`,然后使用 `pnpm dsh <args...>` 运行 TypeScript 入口并转发所有参数;模块解析约定[源码执行参考](reference/README.md#source-execution)为准

View File

@@ -24,19 +24,19 @@ flowchart LR
cfg --> plugin_dsh_base_typert_gateway
plugin_dsh_base_session_title["session-title<br/>@deepseek-ai/dsh-session-title"]
cfg --> plugin_dsh_base_session_title
plugin_dsh_base_session_title_llm["session-title-llm<br/>@deepseek-ai/dsh-session-title-first-message-llm"]
plugin_dsh_base_session_title_llm["session-title-llm<br/>@deepseek-ai/dsh-session-title-first-prompt-llm"]
cfg --> plugin_dsh_base_session_title_llm
plugin_dsh_base_user_interaction["user-interaction<br/>@deepseek-ai/dsh-user-interaction"]
cfg --> plugin_dsh_base_user_interaction
plugin_dsh_base_user_questions["user-questions<br/>@deepseek-ai/dsh-user-questions"]
cfg --> plugin_dsh_base_user_questions
plugin_dsh_base_agent["agent<br/>@deepseek-ai/dsh-agent"]
cfg --> plugin_dsh_base_agent
plugin_dsh_base_agent_default_model["agent-default-model<br/>@deepseek-ai/dsh-agent-default-model"]
cfg --> plugin_dsh_base_agent_default_model
plugin_dsh_base_tasks["tasks<br/>@deepseek-ai/dsh-tasks-local"]
cfg --> plugin_dsh_base_tasks
plugin_dsh_base_jobs["jobs<br/>@deepseek-ai/dsh-jobs-local"]
cfg --> plugin_dsh_base_jobs
plugin_dsh_base_llm_retry["llm-retry<br/>@deepseek-ai/dsh-llm-retry"]
cfg --> plugin_dsh_base_llm_retry
plugin_dsh_base_settings["settings<br/>@deepseek-ai/dsh-settings-local"]
plugin_dsh_base_settings["settings<br/>@deepseek-ai/dsh-settings-file"]
cfg --> plugin_dsh_base_settings
plugin_dsh_base_credentials["credentials<br/>@deepseek-ai/dsh-credentials-local"]
cfg --> plugin_dsh_base_credentials
@@ -50,8 +50,8 @@ flowchart LR
cfg --> plugin_dsh_base_session_query_sqlite
plugin_dsh_base_session_projection["session-projection<br/>@deepseek-ai/dsh-session-projection"]
cfg --> plugin_dsh_base_session_projection
plugin_dsh_base_telemetry_otel["telemetry-otel<br/>@deepseek-ai/dsh-session-telemetry-otel"]
cfg --> plugin_dsh_base_telemetry_otel
plugin_dsh_base_session_telemetry_otel["session-telemetry-otel<br/>@deepseek-ai/dsh-session-telemetry-otel"]
cfg --> plugin_dsh_base_session_telemetry_otel
plugin_dsh_base_subprocess["subprocess<br/>@deepseek-ai/dsh-subprocess-local"]
cfg --> plugin_dsh_base_subprocess
plugin_dsh_base_sandbox["sandbox<br/>@deepseek-ai/dsh-sandbox-local"]
@@ -64,28 +64,28 @@ flowchart LR
cfg --> plugin_dsh_base_pwsh_sandbox
plugin_dsh_base_approval["approval<br/>@deepseek-ai/dsh-user-approval"]
cfg --> plugin_dsh_base_approval
plugin_dsh_base_permission["permission<br/>@deepseek-ai/dsh-permission"]
plugin_dsh_base_permission["permission<br/>@deepseek-ai/dsh-permission-presets"]
cfg --> plugin_dsh_base_permission
plugin_dsh_base_bash_env["bash-env<br/>@deepseek-ai/dsh-bash-env"]
cfg --> plugin_dsh_base_bash_env
plugin_dsh_base_shell_env["shell-env<br/>@deepseek-ai/dsh-shell-env"]
cfg --> plugin_dsh_base_shell_env
plugin_dsh_base_tool_bash["tool-bash<br/>@deepseek-ai/dsh-tool-bash"]
cfg --> plugin_dsh_base_tool_bash
plugin_dsh_base_tool_pwsh["tool-pwsh<br/>@deepseek-ai/dsh-tool-pwsh"]
cfg --> plugin_dsh_base_tool_pwsh
plugin_dsh_base_tool_tasks["tool-tasks<br/>@deepseek-ai/dsh-tool-tasks"]
cfg --> plugin_dsh_base_tool_tasks
plugin_dsh_base_fs_policy["fs-policy<br/>@deepseek-ai/dsh-fs-policy"]
cfg --> plugin_dsh_base_fs_policy
plugin_dsh_base_tool_jobs["tool-jobs<br/>@deepseek-ai/dsh-tool-jobs"]
cfg --> plugin_dsh_base_tool_jobs
plugin_dsh_base_fs_observation_policy["fs-observation-policy<br/>@deepseek-ai/dsh-fs-observation-policy"]
cfg --> plugin_dsh_base_fs_observation_policy
plugin_dsh_base_tool_fs["tool-fs<br/>@deepseek-ai/dsh-tool-fs"]
cfg --> plugin_dsh_base_tool_fs
plugin_dsh_base_tool_fs_search["tool-fs-search<br/>@deepseek-ai/dsh-tool-fs-search"]
cfg --> plugin_dsh_base_tool_fs_search
plugin_dsh_base_workspace_context["workspace-context<br/>@deepseek-ai/dsh-workspace-context"]
cfg --> plugin_dsh_base_workspace_context
plugin_dsh_base_agent_instructions["agent-instructions<br/>@deepseek-ai/dsh-agent-instructions"]
cfg --> plugin_dsh_base_agent_instructions
plugin_dsh_base_skill["skill<br/>@deepseek-ai/dsh-skill"]
cfg --> plugin_dsh_base_skill
plugin_dsh_base_skill_local["skill-local<br/>@deepseek-ai/dsh-skill-local"]
cfg --> plugin_dsh_base_skill_local
plugin_dsh_base_skill_filesystem["skill-filesystem<br/>@deepseek-ai/dsh-skill-filesystem"]
cfg --> plugin_dsh_base_skill_filesystem
plugin_dsh_base_skill_badge["skill-badge<br/>@deepseek-ai/dsh-skill-badge"]
cfg --> plugin_dsh_base_skill_badge
plugin_dsh_base_tool_skill["tool-skill<br/>@deepseek-ai/dsh-tool-skill"]
@@ -96,28 +96,24 @@ flowchart LR
cfg --> plugin_dsh_base_command_feedback
plugin_dsh_base_goal["goal<br/>@deepseek-ai/dsh-goal"]
cfg --> plugin_dsh_base_goal
plugin_dsh_base_goal_session["goal-session<br/>@deepseek-ai/dsh-goal-session"]
cfg --> plugin_dsh_base_goal_session
plugin_dsh_base_goal_round_driver["goal-round-driver<br/>@deepseek-ai/dsh-goal-round-driver"]
cfg --> plugin_dsh_base_goal_round_driver
plugin_dsh_base_command_goal["command-goal<br/>@deepseek-ai/dsh-command-goal"]
cfg --> plugin_dsh_base_command_goal
plugin_dsh_base_plan_mode["plan-mode<br/>@deepseek-ai/dsh-plan-mode"]
cfg --> plugin_dsh_base_plan_mode
plugin_dsh_base_token_meter["token-meter<br/>@deepseek-ai/dsh-token-meter"]
cfg --> plugin_dsh_base_token_meter
plugin_dsh_base_compact_basic["compact-basic<br/>@deepseek-ai/dsh-compact-basic"]
cfg --> plugin_dsh_base_compact_basic
plugin_dsh_base_compaction_basic["compaction-basic<br/>@deepseek-ai/dsh-compaction-basic"]
cfg --> plugin_dsh_base_compaction_basic
plugin_dsh_base_command_compact["command-compact<br/>@deepseek-ai/dsh-command-compact"]
cfg --> plugin_dsh_base_command_compact
plugin_dsh_base_subagent["subagent<br/>@deepseek-ai/dsh-subagent"]
cfg --> plugin_dsh_base_subagent
plugin_dsh_base_subagent_spawn["subagent-spawn<br/>@deepseek-ai/dsh-subagent-spawn"]
cfg --> plugin_dsh_base_subagent_spawn
plugin_dsh_base_subagent_fork["subagent-fork<br/>@deepseek-ai/dsh-subagent-fork"]
cfg --> plugin_dsh_base_subagent_fork
plugin_dsh_base_subagent_codex["subagent-codex<br/>@deepseek-ai/dsh-subagent-codex"]
cfg --> plugin_dsh_base_subagent_codex
plugin_dsh_base_subagent_claude_code["subagent-claude-code<br/>@deepseek-ai/dsh-subagent-claude-code"]
cfg --> plugin_dsh_base_subagent_claude_code
plugin_dsh_base_subagent_spawn_in_process["subagent-spawn-in-process<br/>@deepseek-ai/dsh-subagent-spawn-in-process"]
cfg --> plugin_dsh_base_subagent_spawn_in_process
plugin_dsh_base_subagent_fork_in_process["subagent-fork-in-process<br/>@deepseek-ai/dsh-subagent-fork-in-process"]
cfg --> plugin_dsh_base_subagent_fork_in_process
plugin_dsh_base_tool_subagent_control["tool-subagent-control<br/>@deepseek-ai/dsh-tool-subagent-control"]
cfg --> plugin_dsh_base_tool_subagent_control
plugin_dsh_base_tool_subagent_list_agents["tool-subagent-list-agents<br/>@deepseek-ai/dsh-tool-subagent-control/list-agents"]
@@ -128,11 +124,11 @@ flowchart LR
cfg --> plugin_dsh_base_tool_subagent_fork
plugin_dsh_base_tool_subagent_report["tool-subagent-report<br/>@deepseek-ai/dsh-tool-subagent-report"]
cfg --> plugin_dsh_base_tool_subagent_report
plugin_dsh_base_workflow_workerthread["workflow-workerthread<br/>@deepseek-ai/dsh-workflow-workerthread"]
cfg --> plugin_dsh_base_workflow_workerthread
plugin_dsh_base_workflow_worker_thread["workflow-worker-thread<br/>@deepseek-ai/dsh-workflow-worker-thread"]
cfg --> plugin_dsh_base_workflow_worker_thread
plugin_dsh_base_tool_workflow["tool-workflow<br/>@deepseek-ai/dsh-tool-workflow"]
cfg --> plugin_dsh_base_tool_workflow
plugin_dsh_base_timeout_policy["timeout-policy<br/>@deepseek-ai/dsh-timeout-policy"]
plugin_dsh_base_timeout_policy["timeout-policy<br/>@deepseek-ai/dsh-tool-call-timeout-policy"]
cfg --> plugin_dsh_base_timeout_policy
plugin_dsh_base_spill_local["spill-local<br/>@deepseek-ai/dsh-spill-local"]
cfg --> plugin_dsh_base_spill_local
@@ -140,8 +136,8 @@ flowchart LR
cfg --> plugin_dsh_base_spill_policy
plugin_dsh_base_session_checkpoint_policy["session-checkpoint-policy<br/>@deepseek-ai/dsh-session-checkpoint-policy"]
cfg --> plugin_dsh_base_session_checkpoint_policy
plugin_dsh_base_tool_result_prune["tool-result-prune<br/>@deepseek-ai/dsh-compact-tool-result-prune"]
cfg --> plugin_dsh_base_tool_result_prune
plugin_dsh_base_tool_result_pruner["tool-result-pruner<br/>@deepseek-ai/dsh-compaction-tool-result-pruner"]
cfg --> plugin_dsh_base_tool_result_pruner
plugin_dsh_base_tool_todo["tool-todo<br/>@deepseek-ai/dsh-tool-todo"]
cfg --> plugin_dsh_base_tool_todo
plugin_dsh_base_tool_goal["tool-goal<br/>@deepseek-ai/dsh-tool-goal"]
@@ -150,8 +146,8 @@ flowchart LR
cfg --> plugin_dsh_base_tool_ralph
plugin_dsh_base_tool_str_replace_editor["tool-str-replace-editor<br/>@deepseek-ai/dsh-tool-str-replace-editor"]
cfg --> plugin_dsh_base_tool_str_replace_editor
plugin_dsh_base_repeat_tool_guard["repeat-tool-guard<br/>@deepseek-ai/dsh-repeat-tool-guard"]
cfg --> plugin_dsh_base_repeat_tool_guard
plugin_dsh_base_repeat_tool_reminder["repeat-tool-reminder<br/>@deepseek-ai/dsh-repeat-tool-reminder"]
cfg --> plugin_dsh_base_repeat_tool_reminder
plugin_dsh_base_web["web<br/>@deepseek-ai/dsh-web"]
cfg --> plugin_dsh_base_web
plugin_dsh_base_web_search_deepseek["web-search-deepseek<br/>@deepseek-ai/dsh-web-search-deepseek"]
@@ -180,70 +176,68 @@ flowchart LR
| `typert-loader` | `@deepseek-ai/dsh-typert-loader` |
| `typert-gateway` | `@deepseek-ai/dsh-api-gateway` |
| `session-title` | `@deepseek-ai/dsh-session-title` |
| `session-title-llm` | `@deepseek-ai/dsh-session-title-first-message-llm` |
| `user-interaction` | `@deepseek-ai/dsh-user-interaction` |
| `session-title-llm` | `@deepseek-ai/dsh-session-title-first-prompt-llm` |
| `user-questions` | `@deepseek-ai/dsh-user-questions` |
| `agent` | `@deepseek-ai/dsh-agent` |
| `agent-default-model` | `@deepseek-ai/dsh-agent-default-model` |
| `tasks` | `@deepseek-ai/dsh-tasks-local` |
| `jobs` | `@deepseek-ai/dsh-jobs-local` |
| `llm-retry` | `@deepseek-ai/dsh-llm-retry` |
| `settings` | `@deepseek-ai/dsh-settings-local` |
| `settings` | `@deepseek-ai/dsh-settings-file` |
| `credentials` | `@deepseek-ai/dsh-credentials-local` |
| `llm-pi-ai` | `@deepseek-ai/dsh-llm-pi-ai` |
| `session-persistence-jsonl` | `@deepseek-ai/dsh-session-persistence-jsonl` |
| `attachment-local` | `@deepseek-ai/dsh-attachment-local` |
| `session-query-sqlite` | `@deepseek-ai/dsh-session-query-sqlite` |
| `session-projection` | `@deepseek-ai/dsh-session-projection` |
| `telemetry-otel` | `@deepseek-ai/dsh-session-telemetry-otel` |
| `session-telemetry-otel` | `@deepseek-ai/dsh-session-telemetry-otel` |
| `subprocess` | `@deepseek-ai/dsh-subprocess-local` |
| `sandbox` | `@deepseek-ai/dsh-sandbox-local` |
| `sandbox-policy` | `@deepseek-ai/dsh-sandbox-policy` |
| `bash-sandbox` | `@deepseek-ai/dsh-bash-sandbox` |
| `pwsh-sandbox` | `@deepseek-ai/dsh-pwsh-sandbox` |
| `approval` | `@deepseek-ai/dsh-user-approval` |
| `permission` | `@deepseek-ai/dsh-permission` |
| `bash-env` | `@deepseek-ai/dsh-bash-env` |
| `permission` | `@deepseek-ai/dsh-permission-presets` |
| `shell-env` | `@deepseek-ai/dsh-shell-env` |
| `tool-bash` | `@deepseek-ai/dsh-tool-bash` |
| `tool-pwsh` | `@deepseek-ai/dsh-tool-pwsh` |
| `tool-tasks` | `@deepseek-ai/dsh-tool-tasks` |
| `fs-policy` | `@deepseek-ai/dsh-fs-policy` |
| `tool-jobs` | `@deepseek-ai/dsh-tool-jobs` |
| `fs-observation-policy` | `@deepseek-ai/dsh-fs-observation-policy` |
| `tool-fs` | `@deepseek-ai/dsh-tool-fs` |
| `tool-fs-search` | `@deepseek-ai/dsh-tool-fs-search` |
| `workspace-context` | `@deepseek-ai/dsh-workspace-context` |
| `agent-instructions` | `@deepseek-ai/dsh-agent-instructions` |
| `skill` | `@deepseek-ai/dsh-skill` |
| `skill-local` | `@deepseek-ai/dsh-skill-local` |
| `skill-filesystem` | `@deepseek-ai/dsh-skill-filesystem` |
| `skill-badge` | `@deepseek-ai/dsh-skill-badge` |
| `tool-skill` | `@deepseek-ai/dsh-tool-skill` |
| `commands` | `@deepseek-ai/dsh-commands` |
| `command-feedback` | `@deepseek-ai/dsh-command-feedback` |
| `goal` | `@deepseek-ai/dsh-goal` |
| `goal-session` | `@deepseek-ai/dsh-goal-session` |
| `goal-round-driver` | `@deepseek-ai/dsh-goal-round-driver` |
| `command-goal` | `@deepseek-ai/dsh-command-goal` |
| `plan-mode` | `@deepseek-ai/dsh-plan-mode` |
| `token-meter` | `@deepseek-ai/dsh-token-meter` |
| `compact-basic` | `@deepseek-ai/dsh-compact-basic` |
| `compaction-basic` | `@deepseek-ai/dsh-compaction-basic` |
| `command-compact` | `@deepseek-ai/dsh-command-compact` |
| `subagent` | `@deepseek-ai/dsh-subagent` |
| `subagent-spawn` | `@deepseek-ai/dsh-subagent-spawn` |
| `subagent-fork` | `@deepseek-ai/dsh-subagent-fork` |
| `subagent-codex` | `@deepseek-ai/dsh-subagent-codex` |
| `subagent-claude-code` | `@deepseek-ai/dsh-subagent-claude-code` |
| `subagent-spawn-in-process` | `@deepseek-ai/dsh-subagent-spawn-in-process` |
| `subagent-fork-in-process` | `@deepseek-ai/dsh-subagent-fork-in-process` |
| `tool-subagent-control` | `@deepseek-ai/dsh-tool-subagent-control` |
| `tool-subagent-list-agents` | `@deepseek-ai/dsh-tool-subagent-control/list-agents` |
| `tool-subagent` | `@deepseek-ai/dsh-tool-subagent` |
| `tool-subagent-fork` | `@deepseek-ai/dsh-tool-subagent` |
| `tool-subagent-report` | `@deepseek-ai/dsh-tool-subagent-report` |
| `workflow-workerthread` | `@deepseek-ai/dsh-workflow-workerthread` |
| `workflow-worker-thread` | `@deepseek-ai/dsh-workflow-worker-thread` |
| `tool-workflow` | `@deepseek-ai/dsh-tool-workflow` |
| `timeout-policy` | `@deepseek-ai/dsh-timeout-policy` |
| `timeout-policy` | `@deepseek-ai/dsh-tool-call-timeout-policy` |
| `spill-local` | `@deepseek-ai/dsh-spill-local` |
| `spill-policy` | `@deepseek-ai/dsh-spill-policy` |
| `session-checkpoint-policy` | `@deepseek-ai/dsh-session-checkpoint-policy` |
| `tool-result-prune` | `@deepseek-ai/dsh-compact-tool-result-prune` |
| `tool-result-pruner` | `@deepseek-ai/dsh-compaction-tool-result-pruner` |
| `tool-todo` | `@deepseek-ai/dsh-tool-todo` |
| `tool-goal` | `@deepseek-ai/dsh-tool-goal` |
| `tool-ralph` | `@deepseek-ai/dsh-tool-ralph` |
| `tool-str-replace-editor` | `@deepseek-ai/dsh-tool-str-replace-editor` |
| `repeat-tool-guard` | `@deepseek-ai/dsh-repeat-tool-guard` |
| `repeat-tool-reminder` | `@deepseek-ai/dsh-repeat-tool-reminder` |
| `web` | `@deepseek-ai/dsh-web` |
| `web-search-deepseek` | `@deepseek-ai/dsh-web-search-deepseek` |
| `tool-web` | `@deepseek-ai/dsh-tool-web` |

View File

@@ -1,6 +1,6 @@
# The `code` agent preset: the standard coding agent, presented as Code Mode.
#
# Everything in `standard` is here unchanged. What is added is the `tool-mode`
# Everything in `standard` is here unchanged. What is added is the `tool-presentation`
# row: instead of one tool call per action, the model writes a TypeScript
# program against a generated SDK and `run_code` executes it, so a sequence
# that would be five round trips becomes one.
@@ -34,14 +34,14 @@
text: >-
You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
- id: workspace-context
name: '@deepseek-ai/dsh-workspace-context'
- id: agent-instructions
name: '@deepseek-ai/dsh-agent-instructions'
config:
maxBytes: 65536
# ── shell ───────────────────────────────────────────────────────────────────
# `bash-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
# `shell-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
# the criterion for host-plane ownership — injection resolves before any session
# exists, so there is no agent to key by. Behind a preset realm those variables
@@ -68,27 +68,27 @@
config:
sampleOverCapGlobResults: false
# ── background tasks ────────────────────────────────────────────────────────
# ── background jobs ────────────────────────────────────────────────────────
# Only the model-facing controls. The task REGISTRY stays on the host plane:
# its producers sit outside any realm this file could put it in — `tool-bash`
# above resolves it with `ctx.get`, and an entry-local realm here is invisible
# to every sibling row, so `run_in_background` would answer "background tasks
# to every sibling row, so `run_in_background` would answer "background jobs
# unavailable" while these controls sat in the catalog. The registry is keyed by
# owning agent anyway, so one host instance serves every session. What a preset
# chooses is whether its agent can collect and stop background work at all.
- id: tool-tasks
name: '@deepseek-ai/dsh-tool-tasks'
- id: tool-jobs
name: '@deepseek-ai/dsh-tool-jobs'
# ── skills ──────────────────────────────────────────────────────────────────
# The skill REGISTRY lives in the host composition and is layered per scope:
# these rows register into THIS preset's layer of it, so they need no realm.
# `skill-local` contributes local-root discovery for agents on this preset, and
# `skill-filesystem` contributes local-root discovery for agents on this preset, and
# `tool-skill` gives them the catalog and loader; the merged catalog also
# carries whatever the deployment registered globally (repository plugins).
- id: skill-local
name: '@deepseek-ai/dsh-skill-local'
- id: skill-filesystem
name: '@deepseek-ai/dsh-skill-filesystem'
- id: tool-skill
name: '@deepseek-ai/dsh-tool-skill'
@@ -132,7 +132,7 @@
# ── compaction ──────────────────────────────────────────────────────────────
# `compact-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
# `compaction-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
# share this realm rather than sit outside it.
#
# `tokenMeter` is deliberately NOT in this realm: the meter stays on the HOST
@@ -140,22 +140,22 @@
# keys every fold by Session, and owns the context-meter projection units the
# browser reads for every session — behind a realm those units would come and go
# with whichever presets happen to be mounted. What a preset chooses is whether
# its agent compacts at all, which is `compact-basic` below.
# its agent compacts at all, which is `compaction-basic` below.
- id: compaction
name: cordis:group
group: true
isolate:
compact: true
toolResultPrune: true
compaction: true
toolResultPruner: true
config:
- id: compact-basic
name: '@deepseek-ai/dsh-compact-basic'
- id: compaction-basic
name: '@deepseek-ai/dsh-compaction-basic'
- id: command-compact
name: '@deepseek-ai/dsh-command-compact'
- id: tool-result-prune
name: '@deepseek-ai/dsh-compact-tool-result-prune'
- id: tool-result-pruner
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
config:
thresholdChars: 8192
headChars: 4096
@@ -176,7 +176,7 @@
name: cordis:group
group: true
isolate:
workflows: true
workflowEngine: true
config:
- id: tool-subagent-control
name: '@deepseek-ai/dsh-tool-subagent-control'
@@ -219,8 +219,8 @@
backgroundMode: one-shot
maxDepth: provider-managed
- id: workflow-workerthread
name: '@deepseek-ai/dsh-workflow-workerthread'
- id: workflow-worker-thread
name: '@deepseek-ai/dsh-workflow-worker-thread'
config:
provider: spawn
@@ -256,7 +256,7 @@
# Code Mode for this agent alone. The row waits for the host's `codeRuntime`
# rather than assuming it: a deployment that composes no TypeScript runtime
# fails this preset at mount, naming this id, instead of at the first request.
- id: tool-mode
name: '@deepseek-ai/dsh-agent-tool-mode'
- id: tool-presentation
name: '@deepseek-ai/dsh-agent-tool-presentation'
config:
mode: code

View File

@@ -1,3 +1,3 @@
name: 代码模式
name: PTC 模式
description: 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
order: 2

View File

@@ -28,14 +28,14 @@
Load the `editing-cordis-compositions` skill before writing or changing a composition.
- id: workspace-context
name: '@deepseek-ai/dsh-workspace-context'
- id: agent-instructions
name: '@deepseek-ai/dsh-agent-instructions'
config:
maxBytes: 65536
# ── shell ───────────────────────────────────────────────────────────────────
# `bash-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
# `shell-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
# the criterion for host-plane ownership — injection resolves before any session
# exists, so there is no agent to key by. Behind a preset realm those variables
@@ -62,17 +62,17 @@
config:
sampleOverCapGlobResults: false
# ── background tasks ────────────────────────────────────────────────────────
# ── background jobs ────────────────────────────────────────────────────────
# Only the model-facing controls. The task REGISTRY stays on the host plane:
# its producers sit outside any realm this file could put it in — `tool-bash`
# above resolves it with `ctx.get`, and an entry-local realm here is invisible
# to every sibling row, so `run_in_background` would answer "background tasks
# to every sibling row, so `run_in_background` would answer "background jobs
# unavailable" while these controls sat in the catalog. The registry is keyed by
# owning agent anyway, so one host instance serves every session. What a preset
# chooses is whether its agent can collect and stop background work at all.
- id: tool-tasks
name: '@deepseek-ai/dsh-tool-tasks'
- id: tool-jobs
name: '@deepseek-ai/dsh-tool-jobs'
# ── goals ───────────────────────────────────────────────────────────────────
@@ -113,7 +113,7 @@
# ── compaction ──────────────────────────────────────────────────────────────
# `compact-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
# `compaction-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
# share this realm rather than sit outside it.
#
# `tokenMeter` is deliberately NOT in this realm: the meter stays on the HOST
@@ -121,22 +121,22 @@
# keys every fold by Session, and owns the context-meter projection units the
# browser reads for every session — behind a realm those units would come and go
# with whichever presets happen to be mounted. What a preset chooses is whether
# its agent compacts at all, which is `compact-basic` below.
# its agent compacts at all, which is `compaction-basic` below.
- id: compaction
name: cordis:group
group: true
isolate:
compact: true
toolResultPrune: true
compaction: true
toolResultPruner: true
config:
- id: compact-basic
name: '@deepseek-ai/dsh-compact-basic'
- id: compaction-basic
name: '@deepseek-ai/dsh-compaction-basic'
- id: command-compact
name: '@deepseek-ai/dsh-command-compact'
- id: tool-result-prune
name: '@deepseek-ai/dsh-compact-tool-result-prune'
- id: tool-result-pruner
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
config:
thresholdChars: 8192
headChars: 4096
@@ -163,7 +163,7 @@
name: cordis:group
group: true
isolate:
workflows: true
workflowEngine: true
config:
- id: tool-subagent-control
name: '@deepseek-ai/dsh-tool-subagent-control'
@@ -206,8 +206,8 @@
backgroundMode: one-shot
maxDepth: provider-managed
- id: workflow-workerthread
name: '@deepseek-ai/dsh-workflow-workerthread'
- id: workflow-worker-thread
name: '@deepseek-ai/dsh-workflow-worker-thread'
config:
provider: spawn
@@ -252,8 +252,8 @@
# Both rows register into THIS preset's layer of the host skill registry, so
# they need no realm; the agent's merged catalog also carries whatever the
# deployment registered globally (repository plugins).
- id: skill-local
name: '@deepseek-ai/dsh-skill-local'
- id: skill-filesystem
name: '@deepseek-ai/dsh-skill-filesystem'
config:
customSkillDirs:
- !!js "process.getBuiltinModule('node:url').fileURLToPath(new URL('skills/', baseUrl))"

View File

@@ -0,0 +1,420 @@
---
name: cordis-plugin-development
description: Create, modify, debug, or extend dynamic Cordis Plugins, including Host Services and Events, Client Slot and theme UI, Package-private Client-to-Host calls, dynamic Tools, version updates, approval failures, and runtime diagnostics. Use this Skill to route a user request to the correct platform and Inspect Provider, then define, run, repair, or roll back the Plugin.
---
# Develop Dynamic Cordis Plugins
First determine whether a capability belongs on Host or Client, then query the real interface before writing code. Never infer a complete API from a Service name, Event payload, Slot props, theme token, or example.
## Standard workflow
1. Call `cordis_inspect_list` to obtain the Providers, methods, and schemas currently registered on Host and Client.
2. Select the smallest set of `cordis_inspect_query` calls needed to read the exact Services, Events, Builtins, Slots, Theme tokens, or Tools that the implementation will use.
3. For a new Plugin, design its first Package. To modify an existing Plugin, first use `cordis_inspect_self(pluginId, packageId)` to read the base source and diagnostics.
4. Write plain JavaScript in `code.host`, `code.client`, or both, then call `cordis_define`.
5. Call `cordis_run` with the final `pluginId` and `packageId` returned by define.
6. Handle approval, waiting, Client loading, and render failures from the Run card, steering messages, or `cordis_inspect_self`.
7. Use `cordis_stop` to disable the Plugin temporarily. Use `cordis_undefine` only when it is no longer needed.
Do not wait in the same turn for user approval or asynchronous browser results. After `cordis_run` returns `awaiting-approval` or `starting`, end the current Tool flow and wait for the system to report the final outcome through state updates and steering.
## Tool usage guidance
| Tool | Use it when | Do not |
| --- | --- | --- |
| `cordis_inspect_list` | Discover current Host/Client Providers and method schemas in one call; refresh after the runtime capability directory changes | Hard-code Provider names and skip list; treat a manifest as business data |
| `cordis_inspect_query` | Confirm exact Service methods, Event modes, Builtins, Slots, tokens, or Tool schemas before writing code | Use it instead of calling a real Service from the Plugin; assume a Client query will finish without a responding page |
| `cordis_inspect_self` | List current Plugins, inspect version pointers, or read exact Package source and runtime diagnostics | Fetch all source just to build a list; use it to modify or start a Plugin |
| `cordis_define` | Create a Plugin's first version or append an immutable Package to an existing Plugin; let the user preview the code first | Expect define to execute `apply`, request approval, or update current |
| `cordis_run` | Activate an exact Package; use `run` for first activation, restart, or rollback, and `update` to switch versions | Use `run` to switch versions implicitly; treat pending or starting as success |
| `cordis_stop` | Pause current effects while preserving Packages, grants, and version pointers for later use | Use stop to mean permanent deletion |
| `cordis_undefine` | Permanently remove a Plugin and all of its Packages and clear historical business views | Call it while rollback, inspection, or restart is still needed |
## Choose a platform
| Requirement | Preferred platform | Inspect first |
| --- | --- | --- |
| Files, commands, processes, or networking | Host | `fs`, `bash`, `subprocess`, `pty`, and `web` in `Service.listService` |
| Agents, durable Session data, or Host lifecycle | Host | The relevant Service and `Event.listEvents` |
| Register a dynamic Tool callable in the next model step | Host | `harness` in `Builtin.listBuiltins`, plus `Tool.listTools` |
| Page theme, layout, or current page state | Client | `Theme.listTokens` and Client `Service.listService` |
| Conversation Snapshot or session/workspace lists | Client | The target Slot's standard props and owner props |
| Settings pages, sidebars, input areas, overlays, or Tool cards | Client | `Slots.listSubTree` |
| Fetch on Host and display on Client | Both | Host Service + `harness.handle`; Client Slot + `host.call` |
Prefer the capability closest to the data owner. If Slot props already provide the Conversation Snapshot, do not fetch it again through Host. If only the Package's own styles need to change, do not override the global theme. If only a small entry point is needed, do not replace an entire product UI region.
## Provider navigation
Select methods from the actual `cordis_inspect_list` result. Common initial methods include:
- `Service.listService`: without `service`, returns every callable Service with its purpose and exact method signatures. Query the selected `service` again for access rules, structured method descriptions/parameters/returns, and only its referenced types.
- `Event.listEvents`: without `event`, returns every Event with its purpose, dispatch mode, and exact listener signature. Query the selected `event` again for its structured listener contract and only its referenced types; a Waterfall listener must call `next()`.
- `Builtin.listBuiltins`: returns evaluator-provided symbols and signatures that cannot be obtained through `ctx.get()`.
- `Slots.listSubTree`: without `root`, returns compact live trees with each Slot's purpose, kind, scope, registration keys, replacement risk, and children. With an exact `root`, it also returns that selected Slot's full contract, props, and current occupants while keeping descendants compact.
- `Theme.listTokens`: returns theme tokens that may currently be queried and overridden; it does not modify the theme.
- `Tool.listTools`: returns Tool schemas actually visible to the current Agent, including dynamically registered Tools.
Provider names, methods, and inputs must come from the current list result. The Service/Event Catalog describes which interfaces this version permits; it does not guarantee that a Service is currently mounted. At runtime, use real Services and Events rather than caching or displaying Catalog query results.
## Execution environment
Both `code.host` and `code.client` are plain JavaScript function bodies that return a Cordis Plugin. They are not compiled by TypeScript, JSX, or a bundler.
Do not use:
- `import`, `require`, TypeScript types, `as`, decorators, or JSX;
- globals not confirmed by `Builtin.listBuiltins`;
- guessed access to `window`, `document`, `process`, `Buffer`, `fetch`, or native timers.
Client React code must use `React.createElement(...)`.
Correct:
```js
return {
apply(ctx) {
const slots = ctx.get('slots')
if (slots === undefined) return
slots.inject('tool.view.cordis', () => slots.register(
{ name: 'tool.view.cordis', key: 'self' },
() => React.createElement('div', null, 'Hello'),
))
},
}
```
Incorrect:
```jsx
return {
apply(ctx) {
return <div>Hello</div>
},
}
```
JSX is not the only problem in this example. `apply()` registers lifecycle contributions and cannot return a React Element as the Plugin result. UI must be registered in a queried Slot.
## Access Services
Read optional capabilities with `ctx.get(name)` by default and handle their absence:
```js
return {
apply(ctx) {
const service = ctx.get('serviceName')
if (service === undefined) return
service.someMethod()
},
}
```
Declare `inject` only when a Service is a hard dependency and the Plugin must enter waiting until Cordis reactivates it after the Service appears:
```js
return {
inject: ['requiredService'],
apply(ctx) {
ctx.requiredService.someMethod()
},
}
```
Do not overuse `inject` merely to avoid an `undefined` check. Do not access `ctx.requiredService` without declaring the injection; the Guard rejects undeclared dependencies.
## Manage side effects
Every contribution must be removed after the Plugin is stopped, updated, or removed. Prefer Cordis lifecycle APIs:
- Use `ctx.on()` to register Event listeners.
- Use `ctx.effect()` to own an external subscription that returns a disposer.
- Retain disposers returned by Cordis Service, Tool, Slot, timer, and theme APIs.
- Do not create process-wide or page-wide side effects at module scope or outside `apply()`.
Recommended:
```js
return {
apply(ctx) {
const service = ctx.get('serviceName')
if (service === undefined) return
ctx.effect(() => service.subscribe((value) => {
console.log(value)
}))
},
}
```
If `subscribe()` does not return a disposer, first query whether the Service provides a supported cleanup mechanism. Do not assume unload automatically removes arbitrary third-party callbacks.
## Host and Client timers
On both platforms, the timer is a Service named `timer` with the same interface; it is not a Builtin. Query `{ "service": "timer" }` through the corresponding platform's `Service.listService` before using it. Declare `inject: ['timer']` before using the timer mixin.
One-shot delay:
```js
return {
inject: ['timer'],
apply(ctx) {
const onClick = () => {
ctx.timeout(() => console.log('done'), 300)
}
// Pass onClick to a queried Slot UI.
},
}
```
Periodic work in a React component:
```js
return {
inject: ['timer'],
apply(ctx) {
function Clock() {
React.useEffect(() => ctx.interval(() => console.log('tick'), 1000), [])
return React.createElement('div', null, 'Running')
}
// Register Clock in a queried Slot.
},
}
```
Incorrect:
```js
return {
apply(ctx) {
ctx.timeout(() => console.log('invalid'), 300)
},
}
```
```js
setTimeout(() => console.log('invalid'), 300)
```
The first example does not declare the timer hard dependency. The second uses a global timer that does not exist.
## Listen to Events
Query the Event Provider first to confirm the platform, parameter order, return value, and `mode`.
Ordinary emit Event:
```js
return {
apply(ctx) {
ctx.on('some/event', (payload) => {
console.log(payload)
})
},
}
```
The last parameter of a Waterfall Event is `next`. Unless the listener intentionally stops downstream processing, it must call and return it:
```js
return {
apply(ctx) {
ctx.on('some/waterfall', (payload, next) => {
console.log(payload)
return next()
})
},
}
```
## Register Client UI
Query `Slots.listSubTree` without `root` to choose a target from the compact purpose and topology tree, then query the exact Slot with `root` before writing its registration. The exact result determines:
- the Slot's purpose in the layout;
- whether its registration protocol is `single`, `list`, `keyed`, or `chain`;
- registration options;
- scope standard props and business owner props;
- current occupants, replacement risks, and descendant Slots.
Use `ctx.get('slots')` and handle its absence. Then use `slots.inject` to wait for the Slot declaration and call `slots.register` inside the callback:
```js
return {
apply(ctx) {
const slots = ctx.get('slots')
if (slots === undefined) return
slots.inject('target.slot', () => slots.register(
{ name: 'target.slot', id: 'my-view' },
(props) => React.createElement('div', null, String(props.someValue)),
))
},
}
```
`ctx.get('slots')` does not require an injection. Do not rewrite it as `ctx.slots` unless `inject: ['slots']` is declared:
```js
return {
apply(ctx) {
ctx.slots.register({ name: 'target.slot' }, () => null)
},
}
```
Do not guess an `id`, `key`, selector, or props before querying the Slot protocol. Do not default to root-level `root`, `sidebar`, `conversation`, or `details` Slots; replacing an entire occupant also removes the descendant Slots it declares.
### Settings pages
A full settings UI should usually register its own section through `settings.section` to obtain a complete content area. `settings.general.item` is only appropriate for one compact, general-purpose preference. Query the actual subtree, options, and props for both, then select the narrowest entry point that is still sufficient.
Dynamic Plugins are temporary and process-local, so their settings UI does not need persistent storage. Do not add durable settings or another persistence mechanism for it. Register the UI in the appropriate settings Slot and keep any transient interaction state in memory for the lifetime of the Plugin.
### Session and page data
A session-scoped Slot may provide `useSession`, `useSessions`, `useWorkspaces`, `useProjection`, input state, or actions through standard props. Follow the query result and prefer owner or standard props directly; do not add a Host RPC for data already present there.
Select only the fields that the UI actually needs. Do not copy or render an entire Conversation Snapshot, Session, Tool call, or Slot props object.
### Cordis Run-specific panel
To place interactive UI in the latest `cordis_run` card, register `tool.view.cordis` with `key: 'self'`:
When the feature needs user interaction tied to this Package's result, this region is often a good fit because it keeps the controls in the conversation flow beside the Run card. It is not the default target for every Client UI: settings, sidebars, message actions, and overlays should use their own queried Slots when those locations better match the feature.
```js
return {
apply(ctx) {
const slots = ctx.get('slots')
if (slots === undefined) return
slots.inject('tool.view.cordis', () => slots.register(
{ name: 'tool.view.cordis', key: 'self' },
(props) => React.createElement('div', null, `Package ${props.packageId}`),
))
},
}
```
At runtime, `self` binds to `pluginId + packageId`. Do not include `pluginRunId` in the key. When the same Package runs multiple times, the latest Run card hosts the UI and older cards automatically degrade.
### Ordinary Tool cards
To customize the call card for an ordinary model Tool, query `tool.call.toolview`. Its key is the Tool name; registering an existing key may replace the product's default card. When customizing only a newly added Tool, first verify its schema with `Tool.listTools`, then query the complete `ToolCallOwnerProps`.
### Overlays and local entry points
- For toasts, status notices, and frame-wide overlays, query `shell.overlay` first; observe its pointer-events and ordering rules.
- When the selected target is a global overlay Slot, decide whether the UI should be draggable, how the user shows and hides it, and which existing layers it must cover or remain below.
- For small sidebar actions, prefer additive inner Slots such as `sidebar.footer.action`; do not replace the entire sidebar.
- For supplementary content after a conversation turn, query `conversation.chat.turnTail` and register according to its returned chain selector and fallback rules.
## Themes and styles
Determine the scope of the change first:
1. Global theme: first query `Theme.listTokens`, then query `{ "service": "theme" }` through Client `Service.listService`. Supply light and dark values for each override as required by the query, and retain the returned disposer.
2. The Package's own components: use `styles.insert(css)` and prefer theme CSS variables for colors.
3. New visible content: choose a Slot first, then decide between local CSS and global tokens.
Do not manipulate `document.body`, `window`, or hard-coded product DOM selectors. The theme Service changes tokens but does not create UI. Slots create UI but do not replace the theme system.
## Call Host from Client
Host registers a Package-private method with `harness.handle(method, handler)`, and Client invokes it with `host.call(method, args)`. This is Client→Host JSON RPC.
Host:
```js
return {
apply(ctx) {
harness.handle('read-state', async (args) => {
return { value: args.key }
})
},
}
```
Client:
```js
return {
async apply(ctx) {
const result = await host.call('read-state', { key: 'demo' })
console.log(result.value)
},
}
```
Arguments and return values must be lossless JSON. Do not pass functions, React elements, class instances, Contexts, Services, or other runtime objects; return `null` when there is no response data. Do not register a public Remote Service or use `ctx.remote` for Package-private communication.
## Register a dynamic model Tool
Host can use `harness` to register a Tool callable in the next model step. First query the current `harness` signature with Host `Builtin.listBuiltins`, then inspect existing Tool names and schemas with `Tool.listTools` to avoid conflicts.
Tool arguments and return values must be JSON-compatible. `execute` owns the business result; render and presentation own only what the model and native UI see. Tool registration must belong to the current Plugin Fiber so it is automatically removed after stop or update.
## Handle internal live data
Service instances, Event payloads, Slot props, Session and Conversation Snapshots, Tool state, and other DSH/Cordis objects are internal live data.
Do not:
- call `JSON.stringify` or `structuredClone` on these objects or their descendants;
- recursively enumerate, fully copy, or display them as a whole;
- place Host objects in the Package's long-lived state or RPC return values.
Read only the leaf fields required by the current feature. Extract the minimum strings, numbers, booleans, and other scalar values before constructing owned JSON.
## Versions, approval, and repair
- A Plugin is the stable instance identified by `pluginId`.
- A Package is an immutable code version identified by `packageId`.
- Every activation attempt has its own `pluginRunId`.
- `currentPackageId` is the latest successful version; it does not imply that the Plugin is currently running.
- `nextPackageId` is the target awaiting approval, activating, awaiting Client activation, or most recently failed.
Choose the `cordis_run` mode as follows:
| Current state | Target | mode |
| --- | --- | --- |
| No current | Any Package under the Plugin | `run` |
| Has current | The same Package | `run` |
| Has current | A different Package | `update` |
| Update failed | `nextPackageId` | `update` to retry |
| Update failed | `currentPackageId` | `run` to roll back |
An unauthorized Client Package returns `awaiting-approval`. A single check mark authorizes only the current Package; double check marks authorize future versions of the same Plugin. A grant remains after a technical runtime failure. An authorized Package returns `starting` and completes asynchronously in the browser.
After a technical failure:
1. Use `cordis_inspect_self(pluginId, packageId)` to read the failed version's source and exact diagnostics.
2. If the error involves an unknown capability, list and query the corresponding Provider again.
3. Define a new Package under the same Plugin; do not overwrite the failed Package.
4. Run again with the new `packageId` and the correct mode.
Do not retry automatically after the user rejects approval. A failed update does not automatically restore the old physical Run; explicitly run current when recovery is required.
## Modify @pluginId
When the user identifies a target with `@pluginId`, do not create another Plugin. The injected context contains only identity, version pointers, and the default base Package, not source code.
Modify it as follows:
1. Read the base Package with `cordis_inspect_self(pluginId, packageId)`.
2. Preserve the Host or Client half that does not need to change and modify only the target code.
3. Call `cordis_define` with `plugin.kind: 'existing'` and the original `pluginId`.
4. Use the returned `packageId`; when current exists, activate the new version with `update` in the usual case.
If the reference is unavailable, explain that the Plugin was removed, belongs to another Session, or was lost on process restart. Do not create a same-named replacement.
## Common failure checks
| Failure | Check first |
| --- | --- |
| `service "x" is not declared` | Whether code uses `ctx.x` without declaring `inject: ['x']` on the Plugin object; switch to `ctx.get('x')` with an absence check or declare a true hard dependency |
| `cannot get property "timer" without inject` | Query the timer Service and declare `inject: ['timer']` |
| Client parse failure | Whether the code uses JSX, TypeScript, import, or an unavailable global |
| Slot registration failure | Whether the live subtree was queried, the Slot exists, and options, key, or selector satisfy the returned protocol |
| UI loads but the page reports an error | Inspect the `client-render` diagnostic and stack; the error belongs to an exact Run, so define a new Package to repair it |
| `host.call` failure | The Host handler name, current `pluginRunId`, JSON arguments, and real Service dependencies inside the handler |
| Update failure | Preserve current/next semantics; repair next and update, or run current to roll back |

View File

@@ -88,8 +88,8 @@ When a preset genuinely owns a service, wrap the provider **and every consumer t
isolate:
workflows: true
config:
- id: workflow-workerthread
name: '@deepseek-ai/dsh-workflow-workerthread'
- id: workflow-worker-thread
name: '@deepseek-ai/dsh-workflow-worker-thread'
config:
provider: spawn
- id: tool-workflow
@@ -100,7 +100,7 @@ When a preset genuinely owns a service, wrap the provider **and every consumer t
A consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. Mount-validation catches that as a row that never activated.
Realms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-tasks`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm.
Realms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-jobs`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm.
## Verifying a change
@@ -147,7 +147,7 @@ Copy these disabled templates from a shipped full preset and remove `disabled` o
maxDepth: provider-managed
```
The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only that product tool, and enabling both exposes both. `backgroundMode: one-shot` keeps omitted or `false` calls in the foreground and lets explicit `run_in_background: true` return a generic Task id. Full presets already carry `tool-tasks`, while the host carries the task registry; retain both when making a custom composition so `task_output`, `task_list`, `task_kill`, cancellation, and completion notices stay available. The host must provide `codex` or `claude` on `PATH`; the preset does not install, authenticate, select a model for, or probe either product.
The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only that product tool, and enabling both exposes both. `backgroundMode: one-shot` keeps omitted or `false` calls in the foreground and lets explicit `run_in_background: true` return a generic Job id. Full presets already carry `tool-jobs`, while the host carries the job registry; retain both when making a custom composition so `job_output`, `job_list`, `job_kill`, cancellation, and completion notices stay available. The host must provide `codex` or `claude` on `PATH`; the preset does not install, authenticate, select a model for, or probe either product.
## What not to move into a preset

View File

@@ -1,15 +1,16 @@
# The `minimal` agent preset: a fixed-prompt, two-tool coding-agent composition.
#
# The persona is the complete system prompt, so global identity, Web orientation,
# tool guidance, and later assembly listeners cannot add prompt text. The model
# composes only the persistent `bash` and `str_replace_editor` tools. Context
# compaction is deliberately absent.
# tool guidance, and later assembly listeners cannot add prompt text. Runtime
# context snapshots are suppressed for this preset, and the model composes only
# persistent `bash` and `str_replace_editor`. Context compaction is absent.
- id: persona
name: '@deepseek-ai/dsh-persona'
config:
text: You are a helpful software engineer assistant.
complete: true
includeRuntimeContext: false
# The PTY registry is an agent-owned service, so it lives in an entry-local
# realm. The backend still consumes the host sandbox policy and subprocess
@@ -18,13 +19,13 @@
name: cordis:group
group: true
isolate:
pty: true
terminals: true
config:
- id: pty
name: '@deepseek-ai/dsh-pty'
name: '@deepseek-ai/dsh-terminal'
- id: pty-local
name: '@deepseek-ai/dsh-pty-local'
- id: terminal-bash
name: '@deepseek-ai/dsh-terminal-bash'
config:
timeoutMs: 300000

View File

@@ -27,14 +27,14 @@
text: >-
You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
- id: workspace-context
name: '@deepseek-ai/dsh-workspace-context'
- id: agent-instructions
name: '@deepseek-ai/dsh-agent-instructions'
config:
maxBytes: 65536
# ── shell ───────────────────────────────────────────────────────────────────
# `bash-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
# `shell-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
# the criterion for host-plane ownership — injection resolves before any session
# exists, so there is no agent to key by. Behind a preset realm those variables
@@ -61,27 +61,27 @@
config:
sampleOverCapGlobResults: false
# ── background tasks ────────────────────────────────────────────────────────
# ── background jobs ────────────────────────────────────────────────────────
# Only the model-facing controls. The task REGISTRY stays on the host plane:
# its producers sit outside any realm this file could put it in — `tool-bash`
# above resolves it with `ctx.get`, and an entry-local realm here is invisible
# to every sibling row, so `run_in_background` would answer "background tasks
# to every sibling row, so `run_in_background` would answer "background jobs
# unavailable" while these controls sat in the catalog. The registry is keyed by
# owning agent anyway, so one host instance serves every session. What a preset
# chooses is whether its agent can collect and stop background work at all.
- id: tool-tasks
name: '@deepseek-ai/dsh-tool-tasks'
- id: tool-jobs
name: '@deepseek-ai/dsh-tool-jobs'
# ── skills ──────────────────────────────────────────────────────────────────
# The skill REGISTRY lives in the host composition and is layered per scope:
# these rows register into THIS preset's layer of it, so they need no realm.
# `skill-local` contributes local-root discovery for agents on this preset, and
# `skill-filesystem` contributes local-root discovery for agents on this preset, and
# `tool-skill` gives them the catalog and loader; the merged catalog also
# carries whatever the deployment registered globally (repository plugins).
- id: skill-local
name: '@deepseek-ai/dsh-skill-local'
- id: skill-filesystem
name: '@deepseek-ai/dsh-skill-filesystem'
- id: tool-skill
name: '@deepseek-ai/dsh-tool-skill'
@@ -125,7 +125,7 @@
# ── compaction ──────────────────────────────────────────────────────────────
# `compact-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
# `compaction-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
# share this realm rather than sit outside it.
#
# `tokenMeter` is deliberately NOT in this realm: the meter stays on the HOST
@@ -133,22 +133,22 @@
# keys every fold by Session, and owns the context-meter projection units the
# browser reads for every session — behind a realm those units would come and go
# with whichever presets happen to be mounted. What a preset chooses is whether
# its agent compacts at all, which is `compact-basic` below.
# its agent compacts at all, which is `compaction-basic` below.
- id: compaction
name: cordis:group
group: true
isolate:
compact: true
toolResultPrune: true
compaction: true
toolResultPruner: true
config:
- id: compact-basic
name: '@deepseek-ai/dsh-compact-basic'
- id: compaction-basic
name: '@deepseek-ai/dsh-compaction-basic'
- id: command-compact
name: '@deepseek-ai/dsh-command-compact'
- id: tool-result-prune
name: '@deepseek-ai/dsh-compact-tool-result-prune'
- id: tool-result-pruner
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
config:
thresholdChars: 8192
headChars: 4096
@@ -175,7 +175,7 @@
name: cordis:group
group: true
isolate:
workflows: true
workflowEngine: true
config:
- id: tool-subagent-control
name: '@deepseek-ai/dsh-tool-subagent-control'
@@ -218,8 +218,8 @@
backgroundMode: one-shot
maxDepth: provider-managed
- id: workflow-workerthread
name: '@deepseek-ai/dsh-workflow-workerthread'
- id: workflow-worker-thread
name: '@deepseek-ai/dsh-workflow-worker-thread'
config:
provider: spawn

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh",
"description": "dsh CLI: profile boot, plugin management, and the browser UI alias",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.5",
"publishConfig": {
"access": "restricted"
},
@@ -18,39 +18,42 @@
"lib/*.js",
"config"
],
"license": "BSD-3-Clause",
"license": "MIT",
"dependencies": {
"@deepseek-ai/cordis-plugin-hmr": "workspace:^",
"@deepseek-ai/cordis-plugin-include": "workspace:^",
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
"@deepseek-ai/cordis-plugin-timer": "workspace:^",
"@deepseek-ai/dsh-agent-tool-mode": "workspace:^",
"@deepseek-ai/dsh-agent-tool-presentation": "workspace:^",
"@deepseek-ai/dsh-app-boot": "workspace:^",
"@deepseek-ai/dsh-base": "workspace:^",
"@deepseek-ai/dsh-cordis-client-runner": "workspace:^",
"@deepseek-ai/dsh-client-ui-agent-preset": "workspace:^",
"@deepseek-ai/dsh-client-ui-cordis": "workspace:^",
"@deepseek-ai/dsh-command-compact": "workspace:^",
"@deepseek-ai/dsh-command-goal": "workspace:^",
"@deepseek-ai/dsh-compact-basic": "workspace:^",
"@deepseek-ai/dsh-compact-tool-result-prune": "workspace:^",
"@deepseek-ai/dsh-compaction-basic": "workspace:^",
"@deepseek-ai/dsh-compaction-tool-result-pruner": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-goal-session": "workspace:^",
"@deepseek-ai/dsh-goal-round-driver": "workspace:^",
"@deepseek-ai/dsh-cmdline": "workspace:^",
"@deepseek-ai/dsh-environment": "workspace:^",
"@deepseek-ai/dsh-launch-environment": "workspace:^",
"@deepseek-ai/dsh-fs-local": "workspace:^",
"@deepseek-ai/dsh-headless": "workspace:^",
"@deepseek-ai/dsh-mcp-client": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
"@deepseek-ai/dsh-home-paths": "workspace:^",
"@deepseek-ai/dsh-persona": "workspace:^",
"@deepseek-ai/dsh-plan-mode": "workspace:^",
"@deepseek-ai/dsh-pty": "workspace:^",
"@deepseek-ai/dsh-pty-local": "workspace:^",
"@deepseek-ai/dsh-terminal": "workspace:^",
"@deepseek-ai/dsh-terminal-bash": "workspace:^",
"@deepseek-ai/dsh-pwsh-local": "workspace:^",
"@deepseek-ai/dsh-pwsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-session-reference": "workspace:^",
"@deepseek-ai/dsh-time-context": "workspace:^",
"@deepseek-ai/dsh-skill": "workspace:^",
"@deepseek-ai/dsh-skill-local": "workspace:^",
"@deepseek-ai/dsh-tasks-local": "workspace:^",
"@deepseek-ai/dsh-skill-filesystem": "workspace:^",
"@deepseek-ai/dsh-jobs-local": "workspace:^",
"@deepseek-ai/dsh-tmux-context": "workspace:^",
"@deepseek-ai/dsh-token-meter": "workspace:^",
"@deepseek-ai/dsh-tool-ask-user": "workspace:^",
@@ -62,18 +65,18 @@
"@deepseek-ai/dsh-tool-goal": "workspace:^",
"@deepseek-ai/dsh-tool-pwsh": "workspace:^",
"@deepseek-ai/dsh-tool-ralph": "workspace:^",
"@deepseek-ai/dsh-tool-schedule": "workspace:^",
"@deepseek-ai/dsh-schedule": "workspace:^",
"@deepseek-ai/dsh-tool-skill": "workspace:^",
"@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^",
"@deepseek-ai/dsh-tool-subagent": "workspace:^",
"@deepseek-ai/dsh-tool-subagent-control": "workspace:^",
"@deepseek-ai/dsh-tool-tasks": "workspace:^",
"@deepseek-ai/dsh-tool-jobs": "workspace:^",
"@deepseek-ai/dsh-tool-todo": "workspace:^",
"@deepseek-ai/dsh-tool-web": "workspace:^",
"@deepseek-ai/dsh-tool-workflow": "workspace:^",
"@deepseek-ai/dsh-web-app": "workspace:^",
"@deepseek-ai/dsh-workflow-workerthread": "workspace:^",
"@deepseek-ai/dsh-workspace-context": "workspace:^",
"@deepseek-ai/dsh-workflow-worker-thread": "workspace:^",
"@deepseek-ai/dsh-agent-instructions": "workspace:^",
"commander": "^15.0.0",
"@deepseek-ai/cordis": "workspace:^",
"js-yaml": "^4.2.0",
@@ -81,7 +84,7 @@
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-frontend-static": "workspace:^",
"@deepseek-ai/dsh-host-frontend-static": "workspace:^",
"@deepseek-ai/dsh-host-apiproxy": "workspace:^",
"@deepseek-ai/dsh-host-webserver": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",

View File

@@ -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/reference/README.md
README.md: e8bc99f7b2b15f0679ec268a8e778e815758c00f
README.zh.md: c75c487caa7e2ffd7ca5c4b16f3c6243f0a86f3e
README.md: 0b23db293c304807b5959f5eae9a2faf868caa62
README.zh.md: d812c7cd0d1efaa69b135925f3e922668539781b

View File

@@ -73,12 +73,12 @@ New sessions default to the `workspace-write` permission preset. Bash and filesy
## Shared deployment behavior
The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search`, and session telemetry. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`; `web_fetch` is disabled unless a patch layer inserts a provider and enables it.
The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search`, and disabled session telemetry. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`; `web_fetch` is disabled unless a patch layer inserts a provider and enables it.
Session events stream as OTLP/HTTP logs by default. `DSH_TELEMETRY_OTLP_URL` selects another collector. Any non-empty `DSH_TELEMETRY_DISABLED` disables the telemetry row before boot. The shipped base has no telemetry redaction rule, so exported records can contain message text, tool arguments and results, and workspace paths; the [telemetry Agent Note](../../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md) owns that deployment decision.
Session telemetry stays local by default. `DSH_TELEMETRY_MODE=FULL` streams every projected session event as OTLP/HTTP logs, while `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` uploads a session-log suffix only when feedback is recorded. `DSH_TELEMETRY_OTLP_URL` selects another collector, and any non-empty `DSH_TELEMETRY_DISABLED` remains an authoritative hard opt-out. The shipped base has no telemetry redaction rule, so explicitly enabled exports can contain message text, tool arguments and results, and workspace paths; the [default-off Agent Note](../../../.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md) owns that deployment decision.
Install external plugin bundles through `dsh plugin --profile <name> add <package-or-git-spec>`. The installed package owns its dependencies and contributes its declared `cordis.patch.yml` layer. The CLI also ships `@deepseek-ai/dsh-mcp-client` as a dependency for patch layers, but no MCP server is enabled by default because each server command is trusted executable code outside the agent sandbox.
## Source execution
From the repository root, run `pnpm run build` separately after a fresh checkout and whenever artifacts need updating, then use `pnpm dsh <args...>`. The `package.json` script launches `apps/cli/src/bin.ts` with `node --import tsx/esm` without building and forwards every argument. Missing TypeRT host artifacts fail profile boot through module-resolution errors without a build instruction. Once those host artifacts exist, missing frontend or client-plugin bundles fail at startup with an instruction to run `pnpm run build`. The launcher does not check freshness, so existing stale bundles can run older browser code until rebuilt. The process inherits the launch environment; set `NODE_USE_ENV_PROXY=1` when a supporting Node version must honor `HTTP_PROXY` and `HTTPS_PROXY`. The installed form launches the built `apps/cli/lib/bin.js` without rebuilding the repository.
From the repository root, run `pnpm run build` separately after a fresh checkout and whenever artifacts need updating, then use `pnpm dsh <args...>`. The `package.json` script launches `apps/cli/src/bin.ts` with `node --import tsx/esm` without building and forwards every argument. Missing Typert host artifacts fail profile boot through module-resolution errors without a build instruction. Once those host artifacts exist, missing frontend or client-plugin bundles fail at startup with an instruction to run `pnpm run build`. The launcher does not check freshness, so existing stale bundles can run older browser code until rebuilt. The process inherits the launch environment; set `NODE_USE_ENV_PROXY=1` when a supporting Node version must honor `HTTP_PROXY` and `HTTPS_PROXY`. The installed form launches the built `apps/cli/lib/bin.js` without rebuilding the repository.

View File

@@ -2,32 +2,32 @@
[English](README.md) | 中文
本参考定义 profile、web 别名、插件管理和配置 dump 命令模式。参数由 [`src/args.ts`](../src/args.ts) 统一解析,[`src/bin.ts`](../src/bin.ts) 只动态导入选中的运行器。
本参考定义 profile 启动、web 别名、插件管理和配置 dump 命令模式。argv 由 [`src/args.ts`](../src/args.ts) 统一解析一次[`src/bin.ts`](../src/bin.ts) 只动态导入选中的运行器。
## Profile 启动
`dsh --profile <name>` 启动位于 `$DSH_HOME/profiles/<name>` 的 profile。生效配置树空根节点之上按以下顺序逐层组合:profile manifest元数据清单`dsh.profile.bundles` 列表所列的各组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(各 profile 共享的机器本地偏好,因此优先级高于逐 profile 层)以及按 argv 顺序的各个 `--patch <path>` overlay。后应用的层按行胜出patch 替换目标行完整`config` 值,而不是深度合并各键,并且可以插入新行。配置解析、schema 校验、模块解析或插件启动失败会得到报告并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose资源释放再退出。
`dsh --profile <name>` 启动位于 `$DSH_HOME/profiles/<name>` 的 profile。生效配置树空根节点为起点,依次叠加 profile manifest元数据清单`dsh.profile.bundles` 列表中指定的各组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`这是各 profile 共享的机器本地偏好,因此优先于逐 profile 配置层)以及按 argv 顺序指定的各个 `--patch <path>` 覆盖层。对同一配置行,后应用的层优先。patch 替换目标行的整个 `config` 值,而不是深度合并其中的键patch 也可以插入新行。配置解析、schema 校验、模块解析或插件启动失败时,系统会报告错误并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose资源释放再退出。
组合包名称先从 dsh 安装解析,再从 profile 目录解析。因此内置组合包(`@deepseek-ai/dsh-base``@deepseek-ai/dsh-web-app``@deepseek-ai/dsh-headless`总是来自与正在运行的 `dsh` 相同的安装;树外组合包来自 profile 由 pnpm 管理的 `node_modules`任何 patch 行中的裸插件 `name` 通过 profile 目录 Node 父目录逐级查找解析,该查找可达到持续维护的安装后备目录 `$DSH_HOME/profiles/node_modules`安装的应用和组合包所依赖的每个包对应一个符号链接,每次启动时修复
组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此内置组合包(`@deepseek-ai/dsh-base``@deepseek-ai/dsh-web-app``@deepseek-ai/dsh-headless`始终来自当前运行的 `dsh` 所属的安装;树外组合包来自 profile 由 pnpm 管理的 `node_modules`。patch 行中的裸插件 `name` 会从 profile 目录开始,按照 Node 的模块解析规则逐级向父目录查找,直至由 dsh 维护的安装后备目录 `$DSH_HOME/profiles/node_modules`。该目录为 dsh 安装的应用和组合包所依赖的每个包各维护一个符号链接,并在每次启动时修复这些链接
`web``headless` profile 首次使用时会从随附模板自动初始化(`web`base + web-app`headless`base + headless。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile <name> add <package>`
### 应用参数
启动器自的 flag 写在最前面,并在它不认识的第一个 token 结束;从那里开始的一切都通过 `ctx.cmdlineArgs` 原样交给启动起来的 profile,任何注入它的应用插件都可以解析([`dsh-cmdline`](../../../packages/boot/cmdline/README.md))。因此 `dsh --profile web --port 8080` 到达的是 web 应用的 `--port`,`dsh --profile web --help` 打印的是该应用的 help 且什么也不启动,而 `dsh --help`(没有可交付的 profile)打印的是启动器自己的 help`-V`/`--version` 写在应用参数边界之前时会打印启动器的版本。
启动器自的 flag 必须写在最前面并在遇到第一个无法识别的 token 结束;从该 token 开始的所有内容都会通过 `ctx.cmdlineArgs` 原样交给启动的 profile,注入该 profile 的任意应用插件都可以解析这些内容([`dsh-cmdline`](../../../packages/boot/cmdline/README.md)。因此`dsh --profile web --port 8080` 会将 `--port` 交给 web 应用;`dsh --profile web --help` 打印该应用的帮助信息,不启动应用;`dsh --help` 没有可交付参数的 profile,因此会打印启动器自身的帮助信息`-V`/`--version` 位于应用参数边界之前时会打印启动器的版本。
套组合只挂载一次。普通插件注入 `cmdlineArgs`解析应用参数,并结果作为服务提供出去;由 flag 配置的每一行都会注入该服务Loader 会等服务激活后再求值该行配置(`port: !!js ctx.webStartup.port ?? 3080`),因此 flag 胜过写在它旁边的值。该优先级要求配置行保留这一表达式;用户 patch 用字面量替换整 `config`运行时读取也会随之消失。help 和被拒绝的参数会请求退出——拒绝时以非零状态help 时以 0——且不会激活依赖提供方服务的。在线编辑 `cordis.patch.yml` 会针对仍然在线的服务重新求值表达式,因此不会重置已在服务的端口。
套组合只挂载一次。普通插件注入 `cmdlineArgs`解析所属应用参数,并将解析结果作为服务提供。每个从 flag 取值的配置行都会注入该服务Loader 会等服务激活后,再对该行配置求值`port: !!js ctx.webStartup.port ?? 3080`),因此 flag 的优先级高于配置行中写明的值。要维持这一优先级配置行必须保留该表达式;如果用户 patch 用字面量替换整 `config`也会随之移除运行时读取。帮助参数和被拒绝的参数会请求退出:参数被拒绝时以非零状态退出,显示帮助时以 0 退出;依赖提供方服务的配置行不会激活。在线编辑 `cordis.patch.yml` 时,系统会根据仍在运行的服务重新计算表达式,因此不会重置当前正在使用的端口。
启动器的 flag 必须写在应用参数之前,且启动器的解析器会消耗掉一个 `--`:必须以字面量 `--` 送达应用的参数需要写成 `-- --`。如果应用的第一个参数恰好等于 `web``plugin`,会选择对应的子命令。`ctx.cmdlineArgs.get()` 是共享的不可变读取:多个插件可以解析同一份快照,没有读取方的 profile 则会忽略自己的应用参数。
随附的应用持有这些命令行:
随附的应用接受以下命令行参数
| Profile | 参数 |
|---|---|
| `web` | `--host``--port`、可重复的 `--trusted-host` |
| `headless` | 任务文本,作为位置参数 |
一次性任务(`dsh --profile headless "run the tests"`)通过核心注册表创建一个全新的持久化 Agent智能体提交任务、等待完全停稳并对 Session 执行 flush再从其持久化事件区间中推导最后一个非空 assistant 文本与最终 `turn/end` 原因。它在 stdout 打印文本,并在原因为 `completed` 时以 0 退出,否则以 1 退出。没有任务的调用是该应用的用法错误。随附 headless profile 不挂载 ApiProxy、Host、HTTP 服务器、Web 运行时或浏览器客户端;成功运行不会向 stderr 写入任何内容,也不会打开监听端口。
一次性任务(`dsh --profile headless "run the tests"`)通过核心注册表创建一个全新的持久化 Agent智能体提交任务、等待完全停稳并对会话执行 flush再从其持久化事件区间中推导最后一个非空 assistant 文本与最终 `turn/end` 原因。它在 stdout 打印文本,并在原因为 `completed` 时以 0 退出,否则以 1 退出。没有任务的调用是该应用的用法错误。随附 headless profile 不挂载 ApiProxy、Host、HTTP 服务器、Web 运行时或浏览器客户端;成功运行不会向 stderr 写入任何内容,也不会打开监听端口。
可在不启动的情况下检查组合出的配置树:
@@ -36,11 +36,11 @@ dsh --profile web --dump-default-config
dsh --profile web --patch ./extra.yml --dump-config
```
`--dump-default-config` 只打印组合包各层;`--dump-config` 额外加上 profile 的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml``--patch` overlay。两者都会打印注释标明每行由哪个文件提供以及哪些 overlay 修改过它;`!!js` 表达式保持未求值,找不到目标的 patch 会报告到 stderr。dump 从不运行应用命令行提供方,因此展示的是任何应用参数被解析之前的组合配置树,并拒绝携带应用参数的调用。
`--dump-default-config` 只打印组合包各层;`--dump-config` 额外加上 profile 的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml``--patch` overlay。两者都会打印注释标明每行由哪个文件提供以及哪些 overlay 修改过它;`!!js` 表达式保持未求值,找不到目标的 patch 会报告到 stderr。dump 操作不会运行应用命令行参数提供方,因此展示的是解析任何应用参数之前的组合配置树如果调用中包含应用参数dump 会拒绝该调用。
## 插件管理
`dsh plugin --profile <name> <args...>` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `<args...>` 转发给 `pnpm``add``remove``why``update` 及其他所有 pnpm 子命令都照常可用pnpm 必须在 PATH 上。相对路径 spec`.``../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout而不是 profile。每次成功运行后`dsh.profile.bundles` 都会与已安装状态对齐:每个解析到 manifest 中声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的包的依赖加入层栈(因此让包获得该声明的 `update` 会将其激活),没有组合包声明的依赖保持为普通依赖并给出一次性警告已移除的依赖则退出层栈
`dsh plugin --profile <name> <args...>` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `<args...>` 转发给 `pnpm``add``remove``why``update` 及其他所有 pnpm 子命令都照常可用pnpm 必须在 PATH 上。相对路径 spec`.``../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout而不是 profile。每次成功运行后系统都会根据当前安装状态更新 `dsh.profile.bundles`:如果某项依赖解析到的包在 manifest 中声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`,该依赖就会加入配置层栈;如果某项依赖在 `update` 后获得该声明,也会随即激活。没有组合包声明的依赖仍作为普通依赖保留,并显示一次性警告已移除的依赖则从配置层栈中删除
```sh
dsh plugin --profile tui add github:deepseek-harness/turtle-ui
@@ -48,7 +48,7 @@ dsh plugin --profile tui remove turtle-ui
dsh --profile tui
```
Git 托管、随附源码的插件在安装期间通过 `prepare` 脚本构建,而 pnpm ≥10 在消费方允许之前会阻止该脚本:首次 `add` 会失败并给出 pnpm 的 `allowBuilds` 提示(以及 dsh 指向该 profile 的 `pnpm-workspace.yaml` 的指引);把打印出的键复制到那里并重新运行即可。安装已构建的 tarball 或本地 checkout 不需要任何允许
随源码发布的 Git 托管插件在安装期间通过 `prepare` 脚本构建,而 pnpm ≥10 默认会阻止该脚本,直到使用方明确允许。首次运行 `add` 会失败,并显示 pnpm 的 `allowBuilds` 提示dsh 还会提示应修改该 profile 的 `pnpm-workspace.yaml`。将输出的键复制到该文件后,重新运行命令即可。安装已构建的 tarball 或本地 checkout 时,无需加入 `allowBuilds`
## Web 别名
@@ -61,11 +61,11 @@ dsh web --dump-config
dsh web --help
```
生产 Web 运行器需要已构建的包和前端产物(`pnpm run build`)。默认服务地址是 `http://127.0.0.1:3080`。绑定所有接口时,还会信任机器自动发现的 LAN IP 字面量;`--trusted-host` 可添加 `/api` 浏览器信任围栏接受的具名 authority。
生产 Web 运行器需要已构建的包和前端产物(`pnpm run build`)。默认服务地址是 `http://127.0.0.1:3080`。绑定所有网络接口时,还会信任机器自动发现的 LAN IP 字面量;`--trusted-host` 可添加 `/api` 浏览器信任围栏接受的具名 authority。
进程关闭时会给插件树最多 5 秒完成 dispose。第一次 `SIGINT`/`SIGTERM` 启动该优雅排空——`SIGTERM` 是监督进程的普通停止请求,在所有 surface 上以 0 退出`SIGINT` 报告 130第二次信号强制立即退出。如果一次性运行正常结束时已经卡在 dispose ,第一次 `Ctrl+C` 就会升格并立即退出,而不会被吞掉
进程关闭时插件树最多 5 秒完成 dispose。首次收到 `SIGINT``SIGTERM` 时会开始优雅排空`SIGTERM` 是监督进程发出的常规停止请求,在所有运行模式下都以 0 退出`SIGINT` 报告 130第二次收到信号时会立即强制退出。如果一次性运行正常结束时已经卡在 dispose 阶段,第一次按下 `Ctrl+C` 就会直接升级为强制退出,而不会被忽略
所有模式都将调用目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md``CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。每次 profile 启动都监视两个 `cordis.patch.yml` profile 与 home的有效编辑并以事务方式重新应用;一次性 surface 经由有界关闭退出,关闭会先 dispose 监视器。
所有模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md``CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。每次启动 profile 时,系统都会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;一次性运行模式通过有界关闭流程退出,该流程会先 dispose 监视器。
新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取、网络访问和进程可见性不受限制。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。
@@ -73,12 +73,12 @@ dsh web --help
## 共享部署行为
基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search` 和会话遥测。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env``$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`;只有 patch 层插入提供方并启用 `web_fetch` 后,该工具才可用。
基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search`已禁用的会话遥测。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env``$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`;只有 patch 层插入提供方并启用 `web_fetch` 后,该工具才可用。
会话事件默认作为 OTLP/HTTP 日志流式发送`DSH_TELEMETRY_OTLP_URL` 选择其他 collector。任何非空 `DSH_TELEMETRY_DISABLED`会在启动前禁用遥测配置行。随附基础配置没有遥测脱敏规则,因此导出的记录可能包含消息文本、工具参数结果以及 workspace 路径;部署决策[遥测 Agent Note](../../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md)负责
会话遥测默认留在本地。`DSH_TELEMETRY_MODE=FULL` 将每条已投影会话事件作为 OTLP/HTTP 日志流式发送,`DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 则仅在记录反馈时上传会话日志后缀`DSH_TELEMETRY_OTLP_URL` 选择其他 collector。任何非空 `DSH_TELEMETRY_DISABLED`是具有最终效力的遥测强制关闭开关。随附基础配置没有遥测脱敏规则,因此显式启用的导出可能包含消息文本、工具参数结果以及 workspace 路径;相关部署决策[默认关闭 Agent Note](../../../.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md)。
通过 `dsh plugin --profile <name> add <package-or-git-spec>` 安装外部插件组合包。安装的包拥有其依赖,并贡献其声明的 `cordis.patch.yml` 层。CLI 还随附 `@deepseek-ai/dsh-mcp-client` 作为供 patch 层使用的依赖,但默认不启用 MCP 服务器,因为每条服务器命令都是 agent智能体沙箱之外的受信任可执行代码。
## 源码执行
请在仓库根目录中,于全新 checkout 之后及产物需要更新时单独运行 `pnpm run build`,然后使用 `pnpm dsh <args...>``package.json` 中的脚本不会构建,而是通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`并转发所有参数。TypeRT Host 产物缺失时profile 启动会因不含构建指引的模块解析错误而失败。这些 Host 产物存在后,如果前端或 Client plugin 组合包缺失,启动会失败并提示运行 `pnpm run build`。启动器不会检查产物是否为最新,因此已有的陈旧组合包可能继续运行旧版浏览器代码,直至重新构建。该进程会继承启动环境;当支持环境代理的 Node 版本必须遵循 `HTTP_PROXY``HTTPS_PROXY` 时,请设置 `NODE_USE_ENV_PROXY=1`。安装形式会直接启动构建后的 `apps/cli/lib/bin.js`,不会重新构建仓库。
请在仓库根目录中,于全新 checkout 之后及产物需要更新时单独运行 `pnpm run build`,然后使用 `pnpm dsh <args...>``package.json` 中的脚本不会构建,而是通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`并转发所有参数。Typert Host 产物缺失时profile 启动会因不含构建指引的模块解析错误而失败。这些 Host 产物存在后,如果前端或 Client plugin 组合包缺失,启动会失败并提示运行 `pnpm run build`。启动器不会检查产物是否为最新,因此已有的陈旧组合包可能继续运行旧版浏览器代码,直至重新构建。该进程会继承启动环境;当支持环境代理的 Node 版本必须遵循 `HTTP_PROXY``HTTPS_PROXY` 时,请设置 `NODE_USE_ENV_PROXY=1`。安装形式会直接启动构建后的 `apps/cli/lib/bin.js`,不会重新构建仓库。

View File

@@ -29,12 +29,12 @@ import {
watchUserPatches,
type Profile,
} from '@deepseek-ai/dsh-app-boot'
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
/** Shipped agent-preset root: beside this app's own config, in both source and built layouts. */
const SHIPPED_PRESET_ROOT = fileURLToPath(new URL('../config/agent-presets/', import.meta.url))
import { DSH_ENVIRONMENT_KEY, type EnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
import { DSH_LAUNCH_ENVIRONMENT_KEY, type LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
import { createProcessShutdown, type ProcessShutdown } from './process-shutdown.ts'
@@ -54,7 +54,7 @@ export function homePatchPath(): string {
export const INSTALL_ANCHOR = fileURLToPath(new URL('../package.json', import.meta.url))
/** The session-telemetry row id the DSH_TELEMETRY_DISABLED switch targets. */
const TELEMETRY_ROW_ID = 'telemetry-otel'
const TELEMETRY_ROW_ID = 'session-telemetry-otel'
/** The empty root entry list every profile tree patches over. */
const PROFILE_ROOT_CONFIG = `# dsh profile root — an empty entry list. The tree is composed as patches:
@@ -75,7 +75,7 @@ export const PROFILE_ROOT_FILENAME = 'cordis.yml'
* switch set.
* @param disabledEnv - the raw `DSH_TELEMETRY_DISABLED` value (`undefined` when unset).
* @param hasRow - whether the composition carries the telemetry row.
* @returns the disable patch, or `undefined` when telemetry stays enabled or is not mounted.
* @returns the disable patch, or `undefined` when no hard-disable patch is required.
*/
export function resolveTelemetryPatch(disabledEnv: string | undefined, hasRow: boolean): PatchOptions | undefined {
if ((disabledEnv ?? '') === '' || !hasRow) return undefined
@@ -173,7 +173,7 @@ function composeProfile(
/** Options for {@link runProfile}. */
export interface RunProfileOptions {
/** This run's frozen environment snapshot, provided before any entry mounts. */
environment: EnvironmentSnapshot
environment: LaunchEnvironmentSnapshot
/** The profile name to boot. */
profile: string
/** `--patch` overlay paths, in argv order. */
@@ -249,7 +249,7 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
app.current = hostCtx
// Before any config-tree entry mounts, so plugins resolve all launch-time
// environment values from the same immutable provenance snapshot.
hostCtx.provide(DSH_ENVIRONMENT_KEY, options.environment)
hostCtx.provide(DSH_LAUNCH_ENVIRONMENT_KEY, options.environment)
// The command line and bounded exit request are launcher facts available
// to every app plugin that injects the argument snapshot.
provideCmdline(hostCtx, {

View File

@@ -1,9 +1,9 @@
- id: skill-badge
disabled: false
- id: skill-local
- id: skill-filesystem
config:
watch: false
- id: telemetry-otel
- id: session-telemetry-otel
disabled: true

View File

@@ -1,6 +1,6 @@
- id: skill-local
- id: skill-filesystem
config:
watch: false
- id: telemetry-otel
- id: session-telemetry-otel
disabled: true

View File

@@ -31,7 +31,7 @@ try {
steer: () => {},
inject: () => { throw new Error('dsh-badge snapshot must receive the catalog at the step boundary') },
cancel: () => {},
runMaintenance: task => task(new AbortController().signal),
runMaintenance: job => job(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
const decision = await agentEvents(ctx, agent).waterfall(

View File

@@ -21,15 +21,22 @@ import { describe, expect, it } from 'vitest'
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
const builtBin = join(repoRoot, 'apps/cli/lib/bin.js')
const webDist = join(repoRoot, 'apps/web/dist/index.html')
// The web bundle's patch owns the session-query-sqlite lazy-open row.
const configPath = join(repoRoot, 'packages/bundle/web-app/cordis.patch.yml')
// Full-text session search ships off (`openAt: never` on both layers): the
// base patch carries the default, and the web restatement must not re-enable it.
const baseConfigPath = join(repoRoot, 'packages/bundle/base/cordis.patch.yml')
const webConfigPath = join(repoRoot, 'packages/bundle/web-app/cordis.patch.yml')
const requireBuiltArtifacts = process.env.DSH_REQUIRE_BUILT_CLI_SMOKE === '1'
interface ConfigRow {
id?: string
disabled?: unknown
config?: { openAt?: unknown }
}
interface PatchEntry extends ConfigRow {
insert?: ConfigRow[]
}
const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
kind: 'scalar',
construct: value => String(value),
@@ -92,12 +99,20 @@ function runBuiltWeb(cwd: string): Promise<{ stdout: string; stderr: string; cod
}
describe.skipIf(!requireBuiltArtifacts)('built CLI lazy-search startup', () => {
it('boots and disposes the shipped composition without a SQLite startup warning', async () => {
it('boots and disposes the shipped composition with full-text search off by default', async () => {
expect(existsSync(builtBin), `missing built CLI ${resolve(builtBin)}; run pnpm build`).toBe(true)
expect(existsSync(webDist), `missing Web dist ${resolve(webDist)}; run pnpm run build:web`).toBe(true)
const rows = yaml.load(await readFile(configPath, 'utf8'), { schema: configSchema }) as ConfigRow[]
const searchRow = rows.find(row => row.id === 'session-query-sqlite')
expect(searchRow?.config?.openAt).toBe('first-search')
const baseRows = (yaml.load(await readFile(baseConfigPath, 'utf8'), { schema: configSchema }) as PatchEntry[])
.flatMap(entry => entry.insert ?? [entry])
const webRows = (yaml.load(await readFile(webConfigPath, 'utf8'), { schema: configSchema }) as PatchEntry[])
.flatMap(entry => entry.insert ?? [entry])
const baseRow = baseRows.find(row => row.id === 'session-query-sqlite')
const webRow = webRows.find(row => row.id === 'session-query-sqlite')
expect(baseRow?.config?.openAt).toBe('never')
expect(baseRow?.disabled).toBeUndefined()
// The web restatement keeps the shipped default; opting in is a later layer's override.
expect(webRow?.config?.openAt).toBe('never')
expect(webRow?.disabled).toBeUndefined()
const cwd = await mkdtemp(join(tmpdir(), 'dsh-cli-lazy-search-'))
try {

View File

@@ -12,7 +12,7 @@ import type { Context } from '@deepseek-ai/cordis'
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import { boot, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import * as McpClient from '@deepseek-ai/dsh-mcp-client/src/index.ts'
interface ExampleContract {
@@ -123,7 +123,7 @@ describe('third-party memory MCP example overlays', () => {
(ctx) => {
liveContexts.add(ctx)
ctx.loader.builtins['memory-test-system-prompt'] = SystemPrompt
ctx.loader.builtins['memory-test-tools'] = ToolRegistry
ctx.loader.builtins['memory-test-tools'] = ToolRuntime
ctx.loader.builtins['memory-test-mcp-client'] = McpClient
},
)

View File

@@ -2,14 +2,14 @@ import { describe, expect, it } from 'vitest'
import { resolveTelemetryPatch } from '../src/profile-boot.ts'
describe('resolveTelemetryPatch', () => {
it('keeps telemetry enabled when the switch is unset or empty', () => {
it('preserves the configured telemetry mode when the hard-disable switch is unset or empty', () => {
expect(resolveTelemetryPatch(undefined, true)).toBeUndefined()
expect(resolveTelemetryPatch('', true)).toBeUndefined()
})
it('disables on ANY non-empty value, including falsy-looking ones', () => {
for (const value of ['1', '0', 'false', 'no']) {
expect(resolveTelemetryPatch(value, true)).toEqual({ id: 'telemetry-otel', disabled: true })
expect(resolveTelemetryPatch(value, true)).toEqual({ id: 'session-telemetry-otel', disabled: true })
}
})

View File

@@ -14,7 +14,7 @@ import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import { resolveSessionPreset, SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-presets'
import { applyChildComposition, childSessionMeta } from '@deepseek-ai/dsh-subagent'
import { CallId } from '@deepseek-ai/dsh-llm'
import type {} from '@deepseek-ai/dsh-compact-basic'
import type {} from '@deepseek-ai/dsh-compaction-basic'
import type {} from '@deepseek-ai/dsh-skill'
import type {} from '@deepseek-ai/dsh-tools'
// Type-only: resolves `ctx.get('sessionProjections')` and `ctx.get('tokenMeter')`.
@@ -28,6 +28,7 @@ const BASE_PATCH = join(REPO_ROOT, 'packages/bundle/base/cordis.patch.yml')
const WEB_PATCH = join(REPO_ROOT, 'packages/bundle/web-app/cordis.patch.yml')
/** The installation anchor whose dependency surface the preset module fallback mirrors. */
const INSTALL_ANCHOR = join(REPO_ROOT, 'apps/cli/package.json')
const EXAMPLES_INSTALL_ANCHOR = join(REPO_ROOT, 'examples/package.json')
const MINIMAL_PROMPT = 'You are a helpful software engineer assistant.'
const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell
* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
@@ -43,7 +44,11 @@ const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell
* touch the network, or write outside the test. Everything that decides an
* agent's capabilities is the real thing, including both shipped presets.
*/
async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promise<Context> {
async function bootWeb(
settingsFile: string,
extra: PatchOptions[] = [],
extraInstallAnchor?: string,
): Promise<Context> {
const storageRoot = join(dirname(settingsFile), 'storages')
const patches: PatchOptions[] = [
...loadOverlayPatches('dsh-test', BASE_PATCH),
@@ -67,12 +72,12 @@ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promis
// moved into the presets that a host row still waits for. The boot audit
// is that assertion.
{ id: 'webserver', disabled: true },
// The web bundle's runtime row injects `httpServer`, so it cannot
// The web bundle's runtime row injects `webServer`, so it cannot
// activate without the bound port disabled above. It owns dist serving
// and the URL prompt line — surface glue, not anything that decides an
// agent's capabilities, which is all this file asserts.
{ id: 'web-runtime', disabled: true },
{ id: 'telemetry-otel', disabled: true },
{ id: 'session-telemetry-otel', disabled: true },
// A deployment-level skill on the host registry's GLOBAL layer — the same
// registration shape a repository plugin's skill root uses. The layered
// skills test below proves it reaches preset-composed agents.
@@ -88,7 +93,7 @@ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promis
{ id: 'directory-picker', disabled: true },
{ insert: [
{ id: 'directory-picker-browse', name: '@deepseek-ai/dsh-host-directory-picker-browse' },
{ id: 'ui-directory-picker', name: '@deepseek-ai/dsh-client-ui-directory-picker' },
{ id: 'ui-directory-picker-browse', name: '@deepseek-ai/dsh-client-ui-directory-picker-browse' },
] },
// The roster AppCLIEntry would patch in; only the shipped root, so a
// developer's own `~/.dsh/.preset` cannot change this test's outcome.
@@ -110,6 +115,7 @@ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promis
// them resolvable — the same mechanism, not a test-only shim.
const home = dirname(settingsFile)
healProfilesModuleFallback(INSTALL_ANCHOR, home)
if (extraInstallAnchor !== undefined) healProfilesModuleFallback(extraInstallAnchor, home)
const profileDir = join(home, 'profiles', 'spec')
await mkdir(profileDir, { recursive: true })
const rootConfig = join(profileDir, 'cordis.yml')
@@ -209,9 +215,8 @@ describe('the shipped Web composition', () => {
// depend on ripgrep being present on the machine.
expect(toolNames(ctx, handle.agent).filter(name => name !== 'glob' && name !== 'grep')).toEqual([
'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode',
'get_goal', 'interrupt_agent', 'list_agents', 'ralph', 'read', 'read_image', 'send_message', 'skill',
'subagent', 'subagent_fork', 'task_kill',
'task_list', 'task_output', 'todo_write', 'update_goal', 'web_search',
'get_goal', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'ralph', 'read', 'read_image', 'send_message', 'skill',
'subagent', 'subagent_fork', 'todo_write', 'update_goal', 'web_search',
'workflow', 'write',
])
} finally {
@@ -233,8 +238,8 @@ describe('the shipped Web composition', () => {
expect(assembly.tools.find(tool => tool.name === 'bash')?.description).toBe(MINIMAL_BASH_DESCRIPTION)
expect(JSON.stringify(assembly.tools.find(tool => tool.name === 'str_replace_editor')?.parameters))
.toContain('Absolute path')
expect(ctx.agentPresets.serviceFor(handle.agent, 'compact')).toBeUndefined()
expect(handle.agent.ctx.get('compact')).toBeUndefined()
expect(ctx.agentPresets.serviceFor(handle.agent, 'compaction')).toBeUndefined()
expect(handle.agent.ctx.get('compaction')).toBeUndefined()
} finally {
await handle.dispose()
}
@@ -271,7 +276,10 @@ describe('the shipped Web composition', () => {
try {
const tools = toolNames(ctx, handle.agent)
// The self-referential toolset is what distinguishes this preset.
expect(tools).toEqual(expect.arrayContaining(['cordis_inspect', 'cordis_mount', 'cordis_unmount']))
expect(tools).toEqual(expect.arrayContaining([
'cordis_inspect_list', 'cordis_inspect_query', 'cordis_inspect_self',
'cordis_define', 'cordis_run', 'cordis_stop', 'cordis_undefine',
]))
// And it keeps the standard agent's own tools rather than replacing them.
expect(tools).toEqual(expect.arrayContaining(['bash', 'read', 'edit', 'skill']))
expect(tools).not.toContain('str_replace_editor')
@@ -325,7 +333,7 @@ describe('the shipped Web composition', () => {
})
try {
// Editing the live runtime is opt-in per session, not ambient.
expect(toolNames(ctx, handle.agent)).not.toContain('cordis_mount')
expect(toolNames(ctx, handle.agent)).not.toContain('cordis_define')
} finally {
await handle.dispose()
}
@@ -362,7 +370,7 @@ describe('the shipped Web composition', () => {
})
try {
// The host (global) view carries the deployment-level provider alone:
// local discovery moved behind the presets with `skill-local`.
// local discovery moved behind the presets with `skill-filesystem`.
expect((await ctx.skills.list({ cwd: proj })).map(skill => skill.name)).toEqual(['dsh-badge'])
// The standard agent's view merges the global layer with its preset's
@@ -448,17 +456,23 @@ describe('product subagent rows in user presets', () => {
await mkdir(directory, { recursive: true })
await writeFile(join(directory, 'agent.cordis.yml'), composition)
}
productCtx = await bootWeb(settingsFile, [{
id: 'agent-presets',
config: {
default: 'standard',
roots: [
{ path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' },
{ path: userRoot, trust: 'user' },
],
includeUserRoot: false,
productCtx = await bootWeb(settingsFile, [
{ insert: [
{ id: 'subagent-codex', name: '@deepseek-ai/dsh-subagent-codex' },
{ id: 'subagent-claude-code', name: '@deepseek-ai/dsh-subagent-claude-code' },
] },
{
id: 'agent-presets',
config: {
default: 'standard',
roots: [
{ path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' },
{ path: userRoot, trust: 'user' },
],
includeUserRoot: false,
},
},
}])
], EXAMPLES_INSTALL_ANCHOR)
}, 120_000)
afterAll(async () => {
@@ -485,7 +499,7 @@ describe('product subagent rows in user presets', () => {
const tools = toolNames(productCtx, handle.agent)
expect(tools.filter(name => name === 'subagent_codex' || name === 'subagent_claude_code'))
.toEqual(productTools)
expect(tools).toEqual(expect.arrayContaining(['task_kill', 'task_list', 'task_output']))
expect(tools).toEqual(expect.arrayContaining(['job_kill', 'job_list', 'job_output']))
for (const productTool of productTools) {
expect(toolParameterNames(productCtx, handle.agent, productTool)).toEqual([
'description', 'prompt', 'run_in_background',

View File

@@ -51,25 +51,25 @@
"path": "../../packages/core/tools"
},
{
"path": "../../packages/util/environment"
"path": "../../packages/util/launch-environment"
},
{
"path": "../../packages/util/paths"
"path": "../../packages/util/home-paths"
},
{
"path": "../../packages/mcp/mcp-client"
},
{
"path": "../../packages/support/loader-smoke"
"path": "../../packages/test-support/loader-smoke"
},
{
"path": "../../packages/session-query/session-query-sqlite"
},
{
"path": "../../packages/bash/bash-env"
"path": "../../packages/shell/shell-env"
},
{
"path": "../../packages/bash/tool-bash"
"path": "../../packages/shell/tool-bash"
}
]
}