diff --git a/apps/cli/composition.md b/apps/cli/composition.md
index c4deb098c4..54755185ab 100644
--- a/apps/cli/composition.md
+++ b/apps/cli/composition.md
@@ -54,6 +54,8 @@ flowchart LR
cfg --> plugin_tui_approval
plugin_tui_permission["permission
@deepseek-ai/dsh-permission"]
cfg --> plugin_tui_permission
+ plugin_tui_bash_env["bash-env
@deepseek-ai/dsh-bash-env"]
+ cfg --> plugin_tui_bash_env
plugin_tui_tool_bash["tool-bash
@deepseek-ai/dsh-tool-bash"]
cfg --> plugin_tui_tool_bash
plugin_tui_tool_tasks["tool-tasks
@deepseek-ai/dsh-tool-tasks"]
@@ -165,6 +167,7 @@ flowchart LR
| `bash-sandbox` | `@deepseek-ai/dsh-bash-sandbox` |
| `approval` | `@deepseek-ai/dsh-user-approval` |
| `permission` | `@deepseek-ai/dsh-permission` |
+| `bash-env` | `@deepseek-ai/dsh-bash-env` |
| `tool-bash` | `@deepseek-ai/dsh-tool-bash` |
| `tool-tasks` | `@deepseek-ai/dsh-tool-tasks` |
| `fs-policy` | `@deepseek-ai/dsh-fs-policy` |
diff --git a/apps/cli/config/base.cordis.yml b/apps/cli/config/base.cordis.yml
index df7ed94258..a3d0dce332 100644
--- a/apps/cli/config/base.cordis.yml
+++ b/apps/cli/config/base.cordis.yml
@@ -174,6 +174,9 @@
sandbox: danger-full-access
approval: never
+- id: bash-env
+ name: '@deepseek-ai/dsh-bash-env'
+
- id: tool-bash
name: '@deepseek-ai/dsh-tool-bash'
diff --git a/apps/cli/package.json b/apps/cli/package.json
index a6be4e9d90..710ba1f61c 100644
--- a/apps/cli/package.json
+++ b/apps/cli/package.json
@@ -23,6 +23,7 @@
"@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-app-boot": "workspace:^",
"@deepseek-ai/dsh-bash-local": "workspace:^",
+ "@deepseek-ai/dsh-bash-env": "workspace:^",
"@deepseek-ai/dsh-bash-sandbox": "workspace:^",
"@deepseek-ai/dsh-client-connection": "workspace:^",
"@deepseek-ai/dsh-client-hmr": "workspace:^",
diff --git a/apps/cli/src/web.ts b/apps/cli/src/web.ts
index 4fbfba4d8d..4af7a42013 100644
--- a/apps/cli/src/web.ts
+++ b/apps/cli/src/web.ts
@@ -11,7 +11,7 @@ import type { Context } from 'cordis'
import { addHarnessSourceSection, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
import type {} from '@deepseek-ai/dsh-host-webserver'
import type {} from '@deepseek-ai/dsh-system-prompt'
-import type {} from '@deepseek-ai/dsh-tool-bash'
+import type {} from '@deepseek-ai/dsh-bash-env'
import { AppCLIEntry } from './app-cli-entry.ts'
// The shared core every `dsh` surface mounts, plus this surface's overlay over it.
diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json
index 2f995abf87..aafdf5ad9a 100644
--- a/apps/cli/tsconfig.json
+++ b/apps/cli/tsconfig.json
@@ -23,6 +23,9 @@
{
"path": "../../packages/ui/app-boot"
},
+ {
+ "path": "../../packages/bash/bash-env"
+ },
{
"path": "../../packages/bash/tool-bash"
},
diff --git a/examples/acp-agent/tests/acp.snapshot.ts b/examples/acp-agent/tests/acp.snapshot.ts
index 326a7ee0c0..f191b5ce1b 100644
--- a/examples/acp-agent/tests/acp.snapshot.ts
+++ b/examples/acp-agent/tests/acp.snapshot.ts
@@ -154,6 +154,12 @@ const SCENARIOS: Scenario[] = [
configPath: PTY_CONFIG,
},
{ name: 'bash-tool-turn', hasModelTurn: true, recorded: true },
+ // The pwsh-tool-turn scenario is NOT registered yet: its overlay
+ // (pwsh.cordis.yml / pwsh.cordis.snapshot.yml) swaps the bundle's bash tool
+ // for the PowerShell twin, so its header class needs its own prompt/tool
+ // sidecars and a recorded transcript. Both require a keyed environment
+ // (`test:snapshot:record`); the composition ships so the scenario can be
+ // registered and recorded in one keyed pass.
{ name: 'todo-write', hasModelTurn: true, recorded: true },
{
name: 'skill-load',
diff --git a/examples/acp-agent/tests/fixtures/bash/tool-pwsh/cordis.yml b/examples/acp-agent/tests/fixtures/bash/tool-pwsh/cordis.yml
new file mode 100644
index 0000000000..c152b6ca67
--- /dev/null
+++ b/examples/acp-agent/tests/fixtures/bash/tool-pwsh/cordis.yml
@@ -0,0 +1,27 @@
+# Minimal tool-pwsh composition: real app boot path, real pwsh executor, real
+# foreground + background tool calls; driven by the package's loader.spec.ts.
+- id: system-prompt
+ name: '@deepseek-ai/dsh-system-prompt'
+
+- id: tools
+ name: '@deepseek-ai/dsh-tools'
+
+- id: subprocess
+ name: '@deepseek-ai/dsh-subprocess-local'
+
+- id: bash
+ name: '@deepseek-ai/dsh-pwsh-local'
+ config:
+ graceMs: 200
+
+- id: bash-env
+ name: '@deepseek-ai/dsh-bash-env'
+
+- id: tasks
+ name: '@deepseek-ai/dsh-tasks-local'
+
+- id: tool-tasks
+ name: '@deepseek-ai/dsh-tool-tasks'
+
+- id: tool-pwsh
+ name: '@deepseek-ai/dsh-tool-pwsh'
diff --git a/examples/acp-agent/tests/fixtures/bash/tool-pwsh/driver.ts b/examples/acp-agent/tests/fixtures/bash/tool-pwsh/driver.ts
new file mode 100644
index 0000000000..9899bfcd17
--- /dev/null
+++ b/examples/acp-agent/tests/fixtures/bash/tool-pwsh/driver.ts
@@ -0,0 +1,67 @@
+#!/usr/bin/env node
+/**
+ * Test driver: boot the tool-pwsh Loader composition, execute one real
+ * foreground and one real background pwsh command through the tool registry,
+ * and persist the observed model-visible output to `./pwsh-loader-report.json`
+ * for the package spec's inspect step.
+ */
+
+import { writeFile } from 'node:fs/promises'
+import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
+import { CallId } from '@deepseek-ai/dsh-llm'
+
+const configPath = process.argv[2]
+if (configPath === undefined) throw new Error('tool-pwsh driver requires a config path')
+
+const ctx = await boot('tool-pwsh-loader-smoke', resolveConfigPath(configPath, undefined))
+try {
+ const schema = ctx.tools.schemas().find(tool => tool.name === 'pwsh')
+ if (schema === undefined) throw new Error('pwsh tool not registered by the composition')
+ const prompt = (await ctx.systemPrompt.assemble()).sections.find(section => section.name === 'tool:pwsh')
+
+ const foreground = await ctx.tools.execute({
+ signal: new AbortController().signal,
+ callId: CallId('loader-fg'),
+ name: 'pwsh',
+ arguments: { command: 'Write-Output loader-ok', description: 'loader foreground' },
+ })
+ const foregroundText = foreground.content.filter(block => block.type === 'text').map(block => block.text).join('')
+
+ const background = await ctx.tools.execute({
+ signal: new AbortController().signal,
+ callId: CallId('loader-bg'),
+ name: 'pwsh',
+ arguments: {
+ command: 'Start-Sleep -Milliseconds 200; Write-Output loader-bg-ok',
+ description: 'loader background',
+ run_in_background: true,
+ },
+ })
+ const taskId = (background.value as { taskId: string }).taskId
+
+ // The output delta and the terminal status can land in separate reads
+ // (Windows flushes the child pipe at exit), so accumulate both.
+ let backgroundText = ''
+ const deadline = Date.now() + 10_000
+ while (Date.now() < deadline) {
+ const read = await ctx.tools.execute({
+ signal: new AbortController().signal,
+ callId: CallId('loader-bg-read'),
+ name: 'task_output',
+ arguments: { task_id: taskId },
+ })
+ backgroundText += read.content.filter(block => block.type === 'text').map(block => block.text).join('')
+ if (backgroundText.includes('loader-bg-ok') && backgroundText.includes('[status: completed')) break
+ await new Promise(resolve => setTimeout(resolve, 50))
+ }
+
+ await writeFile('./pwsh-loader-report.json', JSON.stringify({
+ schemaHasRunInBackground: Object.hasOwn(schema.parameters.properties as object, 'run_in_background'),
+ promptHasMarkerSection: prompt?.text.includes('Non-zero exits are reported as `[exit code: N]` markers') === true,
+ // Normalize PowerShell's platform line endings (CRLF on Windows, LF elsewhere).
+ foregroundText: foregroundText.replace(/\r\n/g, '\n'),
+ backgroundText: backgroundText.replace(/\r\n/g, '\n'),
+ }))
+} finally {
+ await ctx.fiber.dispose()
+}
diff --git a/examples/acp-agent/tests/pwsh.cordis.snapshot.yml b/examples/acp-agent/tests/pwsh.cordis.snapshot.yml
new file mode 100644
index 0000000000..c52dbbaf57
--- /dev/null
+++ b/examples/acp-agent/tests/pwsh.cordis.snapshot.yml
@@ -0,0 +1,37 @@
+# Minimal keyless composition: real app, pwsh executor, and pwsh tool; replayed model.
+- id: llm-replay
+ name: '@deepseek-ai/dsh-llm-replay'
+ config:
+ providers:
+ - id: deepseek-official
+ name: DeepSeek
+ models:
+ - id: deepseek-v4-pro
+
+- id: subprocess
+ name: '@deepseek-ai/dsh-subprocess-local'
+
+- id: bash
+ name: '@deepseek-ai/dsh-pwsh-local'
+
+- id: bash-env
+ name: '@deepseek-ai/dsh-bash-env'
+
+- id: acp-agent
+ name: '@deepseek-ai/dsh-acp-demo'
+ config:
+ provider: deepseek-official
+ model: deepseek-v4-pro
+ persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
+ persistenceCompression: none
+ workspaceContext: false
+ skills:
+ enabled: false
+ toolTasks: false
+ goals: false
+ # The pwsh tool replaces the bundle's bash tool in this composition.
+ toolBash: false
+ persona: You are a concise snapshot agent working in {{cwd}}.
+
+- id: tool-pwsh
+ name: '@deepseek-ai/dsh-tool-pwsh'
diff --git a/examples/acp-agent/tests/pwsh.cordis.yml b/examples/acp-agent/tests/pwsh.cordis.yml
new file mode 100644
index 0000000000..46a595d7ff
--- /dev/null
+++ b/examples/acp-agent/tests/pwsh.cordis.yml
@@ -0,0 +1,36 @@
+# Minimal live counterpart for the pwsh-tool-turn snapshot composition.
+- id: llm-deepseek
+ name: '@deepseek-ai/dsh-llm-deepseek'
+ config:
+ apiKey: !!js process.env.DEEPSEEK_API_KEY
+ baseURL: !!js process.env.DEEPSEEK_BASE_URL
+ models:
+ - id: deepseek-v4-pro
+
+- id: subprocess
+ name: '@deepseek-ai/dsh-subprocess-local'
+
+- id: bash
+ name: '@deepseek-ai/dsh-pwsh-local'
+
+- id: bash-env
+ name: '@deepseek-ai/dsh-bash-env'
+
+- id: acp-agent
+ name: '@deepseek-ai/dsh-acp-demo'
+ config:
+ provider: deepseek-official
+ model: deepseek-v4-pro
+ persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
+ persistenceCompression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
+ workspaceContext: false
+ skills:
+ enabled: false
+ toolTasks: false
+ goals: false
+ # The pwsh tool replaces the bundle's bash tool in this composition.
+ toolBash: false
+ persona: You are a concise snapshot agent working in {{cwd}}.
+
+- id: tool-pwsh
+ name: '@deepseek-ai/dsh-tool-pwsh'
diff --git a/examples/package.json b/examples/package.json
index 97e7918361..7cbd7d2498 100644
--- a/examples/package.json
+++ b/examples/package.json
@@ -14,6 +14,7 @@
"@deepseek-ai/dsh-agent-spine-demo": "workspace:*",
"@deepseek-ai/dsh-app-boot": "workspace:*",
"@deepseek-ai/dsh-bash": "workspace:*",
+ "@deepseek-ai/dsh-bash-env": "workspace:*",
"@deepseek-ai/dsh-bash-local": "workspace:*",
"@deepseek-ai/dsh-bash-sandbox": "workspace:*",
"@deepseek-ai/dsh-cli-demo": "workspace:*",
@@ -42,6 +43,7 @@
"@deepseek-ai/dsh-plan-mode": "workspace:*",
"@deepseek-ai/dsh-pty": "workspace:*",
"@deepseek-ai/dsh-pty-local": "workspace:*",
+ "@deepseek-ai/dsh-pwsh-local": "workspace:*",
"@deepseek-ai/dsh-repeat-tool-guard": "workspace:*",
"@deepseek-ai/dsh-repository-plugin": "workspace:*",
"@deepseek-ai/dsh-sandbox-local": "workspace:*",
@@ -81,6 +83,7 @@
"@deepseek-ai/dsh-tool-goal": "workspace:*",
"@deepseek-ai/dsh-tool-lsp": "workspace:*",
"@deepseek-ai/dsh-tool-pty": "workspace:*",
+ "@deepseek-ai/dsh-tool-pwsh": "workspace:*",
"@deepseek-ai/dsh-tool-ralph": "workspace:*",
"@deepseek-ai/dsh-tool-session-query": "workspace:*",
"@deepseek-ai/dsh-tool-skill": "workspace:*",
diff --git a/packages/examples/agent-spine-demo/README.i18n.yaml b/packages/examples/agent-spine-demo/README.i18n.yaml
index ba1d18b7e7..8cbf8ce122 100644
--- a/packages/examples/agent-spine-demo/README.i18n.yaml
+++ b/packages/examples/agent-spine-demo/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/examples/agent-spine-demo/README.md
-README.md: 34d68b0791746c28528124853a4d8ea82b68138d
-README.zh.md: 1b0a644595e35d8703d0600812268b0950b79182
+README.md: 7ea2f4afbe5d0bea62cf0fc2c7ecdd1496d20aef
+README.zh.md: cb4202ac0e72db7c85967425671144e743f31b62
diff --git a/packages/examples/agent-spine-demo/README.md b/packages/examples/agent-spine-demo/README.md
index 34d68b0791..7ea2f4afbe 100644
--- a/packages/examples/agent-spine-demo/README.md
+++ b/packages/examples/agent-spine-demo/README.md
@@ -59,7 +59,7 @@ import type { Config } from '@deepseek-ai/dsh-agent-spine-demo'
// workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults.
```
-The bundle FORWARDS each field to the child that owns it: `agents` and `maxParallelToolCalls` to `agent-loop` (`agents` defaults to `[]`; the cap defaults there), so each app supplies its own pre-created agents — TUI and headless apps pre-create `main`, while the ACP app creates agents on demand at `session/new`; `includeHarnessIdentity`, `persona`, and `toolOrder` to `dsh-system-prompt`; `tools` to the tool registry for its presentation mode; `sessionTitle` to the fallback title service; `skills.registry`, `skills.local`, and `skills.tool` to the skill registry, local provider, and model-facing consumer; the required `workspaceContext` choice to `dsh-workspace-context` (`{ maxBytes }` enables loading and `false` disables it); `invariants` to the invariant service; and `toolBash`/`toolTasks` to the two model-facing tool plugins the bundle owns. It always mounts `dsh-llm-retry`, while each leaf adapter owns its nested `retryPolicy`. Omitted `sessionTitle` uses the explicit example policy of 5 words, 40 fallback bytes, and 80 accepted-title bytes. A `goals` object opts into the persisted domain, model tools, and same-session driver while forwarding `goals.domain` and `goals.tool` to their owners; omission or `false` leaves the stack absent so headless callers retain one-turn settlement. Set `skills.enabled: false` to omit both the local provider and model-facing skill tool, set `toolBash: false` when another plugin owns the `bash` tool name, and set `toolTasks: false` to retain the task service for foreground producers without exposing `task_output` / `task_list` / `task_kill`. It resolves `dshHome` once through [`@deepseek-ai/dsh-paths`](../../util/paths/README.md) and forwards that absolute value to tool-bash's managed environment and enabled local skill discovery. An absent top-level `dshHome` adopts `skills.local.dshHome`; supplying both with different resolved paths fails loudly. `toolBash.enableRunInBackground` controls only the bundled bash producer; independently loaded producers keep their own config. Workspace instructions register before the skill catalog so their session-prefix message renders first. App packages use `pickSpineConfig()` to copy only these bundle-owned fields.
+The bundle FORWARDS each field to the child that owns it: `agents` and `maxParallelToolCalls` to `agent-loop` (`agents` defaults to `[]`; the cap defaults there), so each app supplies its own pre-created agents — TUI and headless apps pre-create `main`, while the ACP app creates agents on demand at `session/new`; `includeHarnessIdentity`, `persona`, and `toolOrder` to `dsh-system-prompt`; `tools` to the tool registry for its presentation mode; `sessionTitle` to the fallback title service; `skills.registry`, `skills.local`, and `skills.tool` to the skill registry, local provider, and model-facing consumer; the required `workspaceContext` choice to `dsh-workspace-context` (`{ maxBytes }` enables loading and `false` disables it); `invariants` to the invariant service; and `toolBash`/`toolTasks` to the two model-facing tool plugins the bundle owns. It always mounts `dsh-llm-retry`, while each leaf adapter owns its nested `retryPolicy`. Omitted `sessionTitle` uses the explicit example policy of 5 words, 40 fallback bytes, and 80 accepted-title bytes. A `goals` object opts into the persisted domain, model tools, and same-session driver while forwarding `goals.domain` and `goals.tool` to their owners; omission or `false` leaves the stack absent so headless callers retain one-turn settlement. Set `skills.enabled: false` to omit both the local provider and model-facing skill tool, set `toolBash: false` when another plugin owns the `bash` tool name, and set `toolTasks: false` to retain the task service for foreground producers without exposing `task_output` / `task_list` / `task_kill`. It resolves `dshHome` once through [`@deepseek-ai/dsh-paths`](../../util/paths/README.md) and forwards that absolute value to the shared `bash-env` managed environment and enabled local skill discovery. An absent top-level `dshHome` adopts `skills.local.dshHome`; supplying both with different resolved paths fails loudly. `toolBash.enableRunInBackground` controls only the bundled bash producer; independently loaded producers keep their own config. Workspace instructions register before the skill catalog so their session-prefix message renders first. App packages use `pickSpineConfig()` to copy only these bundle-owned fields.
For example, `{ invariants: { enabled: true, package_allowlist: ['^@deepseek-ai/dsh-'], package_blocklist: ['agent-loop$'] } }` keeps the package-owned companions mounted but suppresses the blocked owner. Blocklist matches override allowlist matches; see [`dsh-invariants`](../../support/invariants/README.md) for regex and lifecycle rules.
diff --git a/packages/examples/agent-spine-demo/README.zh.md b/packages/examples/agent-spine-demo/README.zh.md
index 1b0a644595..cb4202ac0e 100644
--- a/packages/examples/agent-spine-demo/README.zh.md
+++ b/packages/examples/agent-spine-demo/README.zh.md
@@ -59,7 +59,7 @@ import type { Config } from '@deepseek-ai/dsh-agent-spine-demo'
// workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults.
```
-组合包将每个字段转发给拥有它的子节点:`agents` 与 `maxParallelToolCalls` 交给 `agent-loop`(`agents` 默认为 `[]`,上限在该处默认),因此每个应用提供自己的预创建 agent;TUI 和无头应用预创建 `main`,ACP 应用则在 `session/new` 按需创建 agent;`includeHarnessIdentity`、`persona` 与 `toolOrder` 交给 `dsh-system-prompt`;`tools` 交给工具注册表以配置呈现模式;`sessionTitle` 交给后备标题服务;`skills.registry`、`skills.local` 与 `skills.tool` 分别交给 skill 注册表、本地提供方和面向模型的消费方;必填的 `workspaceContext` 选择交给 `dsh-workspace-context`(`{ maxBytes }` 启用加载,`false` 禁用);`invariants` 交给不变式服务;`toolBash`/`toolTasks` 交给组合包拥有的两个面向模型工具插件。组合包始终挂载 `dsh-llm-retry`,而每个叶节点适配器拥有自己的嵌套 `retryPolicy`。省略 `sessionTitle` 时采用显式示例策略:5 个词、40 个后备字节、80 个可接受标题字节。`goals` 对象会选用持久化领域、模型工具和同会话 Goal Round 驱动器,并将 `goals.domain` 与 `goals.tool` 转发给各自拥有者;省略或设为 `false` 会让整个栈缺席,使无头调用方继续以单轮次结算。设置 `skills.enabled: false` 会同时省略本地提供方和面向模型的 skill 工具;当另一个插件拥有 `bash` 工具名时设置 `toolBash: false`;设置 `toolTasks: false` 会保留供前台生产方使用的任务服务,但不公开 `task_output`/`task_list`/`task_kill`。它对 `dshHome` 只解析一次,解析通过 [`@deepseek-ai/dsh-paths`](../../util/paths/README.md) 完成,并将所得绝对值转发给 tool-bash 的托管环境和已启用的本地 skill 发现。顶层 `dshHome` 缺席时采用 `skills.local.dshHome`;两者同时提供但解析后的路径不同会明确失败。`toolBash.enableRunInBackground` 只控制内置 bash 生产方;独立加载的生产方保留各自配置。工作区指令先于 skill 目录注册,因此其会话前缀消息先渲染。应用包使用 `pickSpineConfig()`,只复制这些由组合包拥有的字段。
+组合包将每个字段转发给拥有它的子节点:`agents` 与 `maxParallelToolCalls` 交给 `agent-loop`(`agents` 默认为 `[]`,上限在该处默认),因此每个应用提供自己的预创建 agent;TUI 和无头应用预创建 `main`,ACP 应用则在 `session/new` 按需创建 agent;`includeHarnessIdentity`、`persona` 与 `toolOrder` 交给 `dsh-system-prompt`;`tools` 交给工具注册表以配置呈现模式;`sessionTitle` 交给后备标题服务;`skills.registry`、`skills.local` 与 `skills.tool` 分别交给 skill 注册表、本地提供方和面向模型的消费方;必填的 `workspaceContext` 选择交给 `dsh-workspace-context`(`{ maxBytes }` 启用加载,`false` 禁用);`invariants` 交给不变式服务;`toolBash`/`toolTasks` 交给组合包拥有的两个面向模型工具插件。组合包始终挂载 `dsh-llm-retry`,而每个叶节点适配器拥有自己的嵌套 `retryPolicy`。省略 `sessionTitle` 时采用显式示例策略:5 个词、40 个后备字节、80 个可接受标题字节。`goals` 对象会选用持久化领域、模型工具和同会话 Goal Round 驱动器,并将 `goals.domain` 与 `goals.tool` 转发给各自拥有者;省略或设为 `false` 会让整个栈缺席,使无头调用方继续以单轮次结算。设置 `skills.enabled: false` 会同时省略本地提供方和面向模型的 skill 工具;当另一个插件拥有 `bash` 工具名时设置 `toolBash: false`;设置 `toolTasks: false` 会保留供前台生产方使用的任务服务,但不公开 `task_output`/`task_list`/`task_kill`。它对 `dshHome` 只解析一次,解析通过 [`@deepseek-ai/dsh-paths`](../../util/paths/README.md) 完成,并将所得绝对值转发给共享 `bash-env` 的托管环境和已启用的本地 skill 发现。顶层 `dshHome` 缺席时采用 `skills.local.dshHome`;两者同时提供但解析后的路径不同会明确失败。`toolBash.enableRunInBackground` 只控制内置 bash 生产方;独立加载的生产方保留各自配置。工作区指令先于 skill 目录注册,因此其会话前缀消息先渲染。应用包使用 `pickSpineConfig()`,只复制这些由组合包拥有的字段。
例如,`{ invariants: { enabled: true, package_allowlist: ['^@deepseek-ai/dsh-'], package_blocklist: ['agent-loop$'] } }` 会让包拥有的配套插件保持挂载,但抑制被阻止的拥有者。Blocklist 匹配优先于 allowlist 匹配;正则表达式与生命周期规则见 [`dsh-invariants`](../../support/invariants/README.md)。
diff --git a/packages/examples/agent-spine-demo/package.json b/packages/examples/agent-spine-demo/package.json
index 82178c48de..10c6ce7e55 100644
--- a/packages/examples/agent-spine-demo/package.json
+++ b/packages/examples/agent-spine-demo/package.json
@@ -43,6 +43,7 @@
"@deepseek-ai/dsh-skill-local": "^0.0.1",
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
"@deepseek-ai/dsh-tasks-local": "^0.0.1",
+ "@deepseek-ai/dsh-bash-env": "^0.0.1",
"@deepseek-ai/dsh-tool-bash": "^0.0.1",
"@deepseek-ai/dsh-tool-goal": "^0.0.1",
"@deepseek-ai/dsh-tool-skill": "^0.0.1",
@@ -55,6 +56,7 @@
"@cordisjs/plugin-timer": "workspace:^",
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-loop": "workspace:^",
+ "@deepseek-ai/dsh-bash-env": "workspace:^",
"@deepseek-ai/dsh-bash-local": "workspace:^",
"@deepseek-ai/dsh-bash-sandbox": "workspace:^",
"@deepseek-ai/dsh-fs-local": "workspace:^",
@@ -77,6 +79,7 @@
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tasks": "workspace:^",
"@deepseek-ai/dsh-tasks-local": "workspace:^",
+ "@deepseek-ai/dsh-bash-env": "workspace:^",
"@deepseek-ai/dsh-tool-bash": "workspace:^",
"@deepseek-ai/dsh-tool-fs": "workspace:^",
"@deepseek-ai/dsh-tool-goal": "workspace:^",
diff --git a/packages/examples/agent-spine-demo/src/index.ts b/packages/examples/agent-spine-demo/src/index.ts
index 32e348bf75..30c51fc941 100644
--- a/packages/examples/agent-spine-demo/src/index.ts
+++ b/packages/examples/agent-spine-demo/src/index.ts
@@ -29,6 +29,7 @@ import * as agentInvariant from '@deepseek-ai/dsh-agent/invariant'
import * as scopeInvariant from '@deepseek-ai/dsh-scope/invariant'
import * as agentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
import * as toolBash from '@deepseek-ai/dsh-tool-bash'
+import * as bashEnv from '@deepseek-ai/dsh-bash-env'
import * as workspaceContext from '@deepseek-ai/dsh-workspace-context'
import * as toolSkill from '@deepseek-ai/dsh-tool-skill'
import * as toolTasks from '@deepseek-ai/dsh-tool-tasks'
@@ -234,7 +235,8 @@ export function apply(ctx: Context, config: Config): void {
ctx.plugin(scopeInvariant)
ctx.plugin(agentLoopInvariant)
if (config.toolBash !== false) {
- ctx.plugin(toolBash, Object.assign({}, config.toolBash, { dshHome }))
+ ctx.plugin(bashEnv, { dshHome })
+ ctx.plugin(toolBash, config.toolBash ?? {})
}
if (config.workspaceContext !== false) {
ctx.plugin(workspaceContext, config.workspaceContext)
diff --git a/packages/examples/agent-spine-demo/tsconfig.json b/packages/examples/agent-spine-demo/tsconfig.json
index 670cd9a629..6a0091a6f6 100644
--- a/packages/examples/agent-spine-demo/tsconfig.json
+++ b/packages/examples/agent-spine-demo/tsconfig.json
@@ -68,6 +68,9 @@
{
"path": "../../util/paths"
},
+ {
+ "path": "../../bash/bash-env"
+ },
{
"path": "../../bash/tool-bash"
},