dsh-root-planner-preset 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/CONTRIBUTING.md +7 -3
- package/INSTALL.md +5 -1
- package/README.md +21 -17
- package/README.zh.md +19 -16
- package/build-patch.mjs +18 -6
- package/child-tools.mjs +64 -24
- package/cordis.patch.yml +45 -19
- package/docs/DEVELOPMENT.md +6 -2
- package/package.json +1 -1
- package/persona.txt +19 -13
- package/tools/inspect-sessions.mjs +71 -0
- package/tools/verify-session.mjs +177 -0
- package/verify-patch.mjs +25 -8
package/.github/CONTRIBUTING.md
CHANGED
|
@@ -72,9 +72,13 @@ script prints `SKIP` and explains why. That is a pass, not a failure.
|
|
|
72
72
|
- **One concern per pull request.** A persona change and a mask change are two
|
|
73
73
|
pull requests unless one requires the other.
|
|
74
74
|
- Say which preset you tested with, and on which platform.
|
|
75
|
-
- Do not weaken the
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
- Do not weaken the **process bound**. `enableRunInBackground: false` must stay on
|
|
76
|
+
the preset's `tool-pwsh` row: a change that lets a subagent detach a server drops
|
|
77
|
+
the one property this preset enforces structurally rather than by asking nicely.
|
|
78
|
+
`verify-patch.mjs` fails if it is missing.
|
|
79
|
+
- A subagent keeps `write`, `edit`, `ops_replace` and `pwsh`. They are workers —
|
|
80
|
+
taking their tools away turns every executable step back into the root's own work,
|
|
81
|
+
which is the failure mode this preset exists to avoid.
|
|
78
82
|
- Keep `main` linear — the branch protection refuses merge commits, so rebase
|
|
79
83
|
rather than merge.
|
|
80
84
|
|
package/INSTALL.md
CHANGED
|
@@ -87,6 +87,9 @@ git clone https://github.com/XiaoOu2024/dsh-root-planner-preset
|
|
|
87
87
|
|
|
88
88
|
1. **开一个新会话**(已有会话和它的子代理会保留启动时的插件版本,不会变)。
|
|
89
89
|
2. 在预设选择器里选 **Root Planner**。
|
|
90
|
+
|
|
91
|
+
> ⚠️ **这是最容易踩的坑:根会话必须真的选中 Root Planner。** 有人实测时根会话仍是 `standard`,结果整棵树的行为都对不上——根不受本预设任何规则约束,而它派出的子代理却是 `root-planner`,于是"谁的权限是什么"完全错位,最后据此得出一堆关于预设的错误结论。**怎么确认**:会话文件 `~/.dsh/sessions/<cwd>/<session-id>/session.v4.jsonl.zstd` 的第一行里有 `"agentPreset":"root-planner"` 字段;写着 `standard` 就是没选中。
|
|
92
|
+
|
|
90
93
|
3. 派一个研究任务,确认两件事:
|
|
91
94
|
|
|
92
95
|
- 子代理的工具列表里**没有** `write`、`edit`、`ops_replace`、`ops_bash`、`pwsh`;
|
|
@@ -110,7 +113,8 @@ git clone https://github.com/XiaoOu2024/dsh-root-planner-preset
|
|
|
110
113
|
| 子代理创建失败,报 **`tools.restrict() names unknown global tool …`** | 白名单里有**不是全局工具**的名字。注意有两种情况:①你的环境根本没注册该工具;②它是**agent 作用域**工具(如 `subagent`,它注册在每个被组合 Agent 自身的作用域里),而 `toolFilter` **只接受全局工具名** | 报错里会列出该环境全部的全局工具名,照着它删掉白名单里那几个不存在的名字。白名单在 `cordis.patch.yml` 的两个 `toolFilter.allow` 处(或在 `child-tools.mjs` 里改后重跑生成器),改完必须重开会话 |
|
|
111
114
|
| 委派立刻返回**深度错误** | `maxDepth` 没设或设得太小 | 见第三节,设为 `2` |
|
|
112
115
|
| 子代理**什么都不能读** | 白名单过窄(比如你删过头了) | 至少保留 `read`、`glob`、`grep`、`ops_inspect_local_file`、`ops_glob`、`ops_grep` |
|
|
113
|
-
| 子代理**还是能改文件** |
|
|
116
|
+
| 子代理**还是能改文件** | 根会话没选中 Root Planner(见第四节),或白名单被改坏了 | 先查会话文件的 `agentPreset` 字段是否 `root-planner`;再确认 `cordis.patch.yml` 里两处 `toolFilter.allow` 都**不含** `write`、`edit`、`ops_replace`、`ops_bash`、`pwsh`。改完必须重开会话 |
|
|
117
|
+
| 行为与人设描述**对不上**(比如"子代理能写"、"规则没生效") | 最常见的原因就是**根会话跑在别的预设上**,而它的子代理跑在 root-planner 上——两者的规则和权限本来就不一样 | 查会话文件第一行的 `agentPreset`;也可跑 `node tools/inspect-sessions.mjs <会话目录>` 一次性打印根与全部子会话的预设、深度与工具清单 |
|
|
114
118
|
| 改了 `persona.txt` 但没生效 | 会话与子代理都锁定启动时的插件版本 | 开**新会话**;必要时把 `preset-root-planner` 在插件列表里关掉再打开 |
|
|
115
119
|
| Windows 上校验报**人设不一致** | 换行符被 Git 改写了 | 确认 `.gitattributes` 存在且含 `* text=auto eol=lf`,重新克隆一次 |
|
|
116
120
|
|
package/README.md
CHANGED
|
@@ -1,41 +1,43 @@
|
|
|
1
1
|
# dsh-root-planner-preset
|
|
2
2
|
|
|
3
|
-
An agent preset for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) that **separates
|
|
3
|
+
An agent preset for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) that **separates deciding from doing**.
|
|
4
4
|
|
|
5
|
-
The root agent's context holds
|
|
5
|
+
The root agent's context holds the plan and the decisions; reading, editing and running commands go to subagents that report back summaries. Subagents are full workers — they have `write`, `edit` and a shell — and the one thing none of them may leave behind is **a process still running**.
|
|
6
6
|
|
|
7
|
-
>
|
|
7
|
+
> The root decides, the workers do the work, and nothing outlives the task.
|
|
8
8
|
|
|
9
9
|
[中文说明](README.zh.md) | [Install](INSTALL.md) | [Maintainer notes](docs/DEVELOPMENT.md)
|
|
10
10
|
|
|
11
11
|
## The problem it solves
|
|
12
12
|
|
|
13
|
-
On a long task a coding agent's context fills up with raw file contents, and it forgets the goal, loses the plan, and starts repeating work. The usual answer — "delegate more to subagents" —
|
|
13
|
+
On a long task a coding agent's context fills up with raw file contents and command output, and it forgets the goal, loses the plan, and starts repeating work. The usual answer — "delegate more to subagents" — fails in two opposite directions: a delegate with no hands cannot do anything, so every step bounces back; and a fleet of delegates with no bounds will edit whatever they like and leave servers running.
|
|
14
14
|
|
|
15
|
-
This preset
|
|
15
|
+
This preset takes the middle line: **workers with tools, bounded by their task.**
|
|
16
16
|
|
|
17
17
|
| Requirement | Mechanism | Strength |
|
|
18
18
|
|---|---|---|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
19
|
+
| A subagent can read, **edit files and run commands** | `toolFilter.allow` on both delegation tools includes `write`, `edit`, `ops_replace`, `pwsh` | Grant, not restriction — a worker without tools is not a worker |
|
|
20
|
+
| A subagent cannot leave a process running | `enableRunInBackground: false` on the preset's `pwsh` row: the call is refused **and** `run_in_background` is removed from the tool schema | **Structural** — no model at any depth can reach for a detached process |
|
|
21
|
+
| Nobody runs an unbounded service | Persona rules for the root and for subagents | Prompt-level, covering what a schema cannot (`ops_bash` has no job registry) |
|
|
22
|
+
| A subagent stays inside the task it was given | Persona rules: named files only, no drive-by fixes, no racing siblings | Prompt-level |
|
|
23
|
+
| Delegation depth is the user's setting | The runtime depth cap (`subagent.maxDepth`) | **Structural**: past the cap it throws `SubagentDepthError` |
|
|
24
|
+
| The root decides; workers execute | Persona rules | Prompt-level |
|
|
24
25
|
|
|
25
|
-
The mask is an **allow** list
|
|
26
|
+
The mask is an **allow** list: a tool added by a future plugin is invisible to subagents until it is added here.
|
|
26
27
|
|
|
27
28
|
## How it works
|
|
28
29
|
|
|
29
30
|
```
|
|
30
31
|
root agent ──delegates──▶ subagent ──delegates──▶ … until maxDepth
|
|
31
32
|
│ │
|
|
32
|
-
└──
|
|
33
|
+
└── decides, integrates, and states what each worker may touch
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
Two mechanisms decide how deep the tree goes, and they pull in the same direction:
|
|
36
37
|
|
|
37
|
-
- **The mask**
|
|
38
|
-
|
|
38
|
+
- **The mask** gives *every* subagent, at *every* level, the same working set — read,
|
|
39
|
+
write, shell — and withholds the same things. The grant does not weaken with
|
|
40
|
+
depth, and neither does the bound.
|
|
39
41
|
- **The depth cap** (`subagent.maxDepth`, a Host setting you control) decides how
|
|
40
42
|
many levels exist. `2` means root → child → grandchild; raise it and the tree
|
|
41
43
|
deepens, set it to `1` and children become leaves.
|
|
@@ -52,9 +54,9 @@ subagent creation failed** with
|
|
|
52
54
|
`tools.restrict() names unknown global tool "subagent"` — the mask rejecting a
|
|
53
55
|
name it could never legally contain, not the tool being missing.
|
|
54
56
|
|
|
55
|
-
- **Root agent**: plans first (goal, success criteria, constraints, ordered task list),
|
|
56
|
-
- **Subagent**: reads,
|
|
57
|
-
- **The root
|
|
57
|
+
- **Root agent**: plans first (goal, success criteria, constraints, ordered task list), dispatches work that is heavy enough to be worth the round trip, keeps the integration and the decisions, and states in every delegation which files may be touched.
|
|
58
|
+
- **Subagent**: does the task — reads, edits the files it was given, runs the commands that belong to it — and reports the conclusion with the minimum evidence, plus exactly what it changed.
|
|
59
|
+
- **The root decides** what changes and who makes them; a worker that changes something outside its task is a defect, not initiative.
|
|
58
60
|
|
|
59
61
|
How does a subagent know it is a subagent? DSH force-inserts the sentence `You are a delegated subagent: …` into every subagent's prompt, injected by the runtime and impossible to remove — while a preset persona has **no** `{{role}}`-style variable available. So the persona states a **test** instead: read your own prompt for that sentence. One persona then serves both roles, and parent and child read the same text, which is what prevents the drift where a parent believes a child is constrained and the child never saw it.
|
|
60
62
|
|
|
@@ -76,6 +78,8 @@ Then **one required step**: set **Settings → Subagents → depth limit** to **
|
|
|
76
78
|
|
|
77
79
|
Then **start a new session** and pick **Root Planner** in the preset selector. Full steps and troubleshooting are in [INSTALL.md](INSTALL.md).
|
|
78
80
|
|
|
81
|
+
> ⚠️ **The easiest mistake to make: the root session must actually be composed with Root Planner.** In one real trial the root session was still `standard`, so the root was bound by none of this preset while the subagents it spawned *were* `root-planner` — every rule and permission in the tree was mismatched, and a batch of confident conclusions about the preset followed from it. **To check:** the first line of `~/.dsh/sessions/<cwd>/<session-id>/session.v4.jsonl.zstd` carries `"agentPreset"`, which must read `root-planner`; or run `node tools/inspect-sessions.mjs <session-dir>` to print the preset, depth and tool inventory of the root and every child at once.
|
|
82
|
+
|
|
79
83
|
## When it fits, and when it does not
|
|
80
84
|
|
|
81
85
|
**Fits**: long research-heavy tasks across many files, reverse engineering, code review — anything read-heavy, or where a change must be carefully decided.
|
package/README.zh.md
CHANGED
|
@@ -1,49 +1,50 @@
|
|
|
1
1
|
# dsh-root-planner-preset
|
|
2
2
|
|
|
3
|
-
一个面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)的 agent
|
|
3
|
+
一个面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)的 agent 预设:**把「决策」和「干活」分开**。
|
|
4
4
|
|
|
5
|
-
根 agent
|
|
5
|
+
根 agent 的上下文装计划与决策;阅读、改文件、跑命令交给子代理,子代理回报摘要。**子代理是有工具的工人**——它有 `write`、`edit` 和 shell;唯一不许留下的是**还在跑的进程**。
|
|
6
6
|
|
|
7
|
-
>
|
|
7
|
+
> 根负责决策,工人负责干活,没有任何东西能活过任务本身。
|
|
8
8
|
|
|
9
9
|
[English](README.md) | [安装步骤](INSTALL.md) | [维护者文档](docs/DEVELOPMENT.md)
|
|
10
10
|
|
|
11
11
|
## 它解决什么问题
|
|
12
12
|
|
|
13
|
-
用 coding agent
|
|
13
|
+
用 coding agent 干长任务,上下文很快被原始文件内容和命令输出塞满,于是忘了目标、丢了计划、开始重复劳动。常见对策「让 agent 多派子代理」会朝两个相反方向失败:**没有手的子代理什么也干不了**,每一步都被推回根;而**没有边界的子代理群**会随手改任何文件、还会给你留下一堆跑着的服务。
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
本预设走中间那条线:**有工具的工人,边界是它的任务。**
|
|
16
16
|
|
|
17
17
|
| 要求 | 机制 | 强度 |
|
|
18
18
|
|---|---|---|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
19
|
+
| 子代理能读、**能改文件、能跑命令** | 两个委派工具的白名单**包含** `write`、`edit`、`ops_replace`、`pwsh` | 这是授予而非限制——没有工具的工人不算工人 |
|
|
20
|
+
| 子代理不能留下运行中的进程 | 预设的 `pwsh` 行设 `enableRunInBackground: false`:后台调用被拒,**且 `run_in_background` 参数被从工具 schema 里移除** | **结构性**——任何深度的模型都够不到"脱离进程"这个能力 |
|
|
21
|
+
| 谁都不跑无界服务 | 根与子代理的人设规则 | 提示词级,覆盖 schema 管不到的部分(`ops_bash` 没有任务注册表) |
|
|
22
|
+
| 子代理不越出被交代的任务 | 人设规则:只动点名文件、不顺手修、不与兄弟抢文件 | 提示词级 |
|
|
23
|
+
| 委派层数由用户配置 | 运行时深度上限(`subagent.maxDepth`) | **结构性**:超过上限直接抛 `SubagentDepthError` |
|
|
24
|
+
| 根决策、工人执行 | 人设规则 | 提示词级 |
|
|
24
25
|
|
|
25
|
-
白名单是 **allow 而非 deny
|
|
26
|
+
白名单是 **allow 而非 deny**:将来某个插件新增的工具,对子代理默认不可见。
|
|
26
27
|
|
|
27
28
|
## 工作方式
|
|
28
29
|
|
|
29
30
|
```
|
|
30
31
|
根 agent ──委派──▶ 子代理 ──委派──▶ … 直到 maxDepth
|
|
31
32
|
│ │
|
|
32
|
-
└──
|
|
33
|
+
└── 决策、集成、把关;每一层都拿同一套工具,也受同一条边界
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
有两个机制决定树有多深,方向一致:
|
|
36
37
|
|
|
37
|
-
-
|
|
38
|
+
- **白名单**给**每一层**、**每一个**子代理同一套工作集(读、写、shell),也扣掉同样几样。授予不随深度衰减,边界也不衰减。
|
|
38
39
|
- **深度上限**(`subagent.maxDepth`,由你控制的 Host 设置)决定有几层。`2` 就是根 → 子 → 孙;调大则更深,设成 `1` 则子代理变叶子。
|
|
39
40
|
|
|
40
41
|
`toolFilter` 是**全局**工具的掩码,而 `subagent` 不是全局工具:因为 `modelSelectionSettings: true`,它被注册进每个被组合 Agent **自身的作用域**,掩码永远写不上它的名字。所以它不在 `child-tools.mjs` 里——**不是**因为子代理没有它。作用域注册**不受限制影响**,而每个子代理都继承本预设,于是每一层都拿到自己的 `subagent`,可以一直往下派到深度上限为止。
|
|
41
42
|
|
|
42
43
|
本包早期版本确实把 `subagent` 写进了白名单,结果是**每一次子代理创建都失败**,报 `tools.restrict() names unknown global tool "subagent"`——那是白名单在拒绝一个它永远不可能合法包含的名字,不是工具缺失。
|
|
43
44
|
|
|
44
|
-
- **根 agent
|
|
45
|
-
-
|
|
46
|
-
-
|
|
45
|
+
- **根 agent**:先立计划(目标、成功判据、约束、任务清单),把值得一次往返的活派出去,自己保留集成、把关与决策——并在每次委派里**点名允许改哪些文件**。
|
|
46
|
+
- **子代理**:干活。读、改被点名的文件、跑属于它的命令,然后回报结论与最小证据,外加**确切改了哪些文件**。
|
|
47
|
+
- **根决策**改什么、谁改;工人改了任务之外的东西,那是缺陷,不是主动性。
|
|
47
48
|
|
|
48
49
|
子代理怎么知道自己是子代理?DSH 会在每个子代理的提示词里强制插入一句 `You are a delegated subagent: …`,运行时注入、无法移除,而预设人设**没有** `{{role}}` 这类变量可用。所以人设里写的是一条**判定指令**:读自己的提示词里有没有那句话。这让人设只需要一份,父与子读到的是同一份文本,因此不会出现「父以为子被这么约束、子其实没读到」的漂移。
|
|
49
50
|
|
|
@@ -65,6 +66,8 @@ dsh plugin --profile web add github:XiaoOu2024/dsh-root-planner-preset
|
|
|
65
66
|
|
|
66
67
|
然后**开新会话**,在预设选择器里选 **Root Planner**。完整步骤与故障排查见 [INSTALL.md](INSTALL.md)。
|
|
67
68
|
|
|
69
|
+
> ⚠️ **最容易踩的坑:根会话必须真的选中 Root Planner。** 有人实测时根会话仍是 `standard`,于是根不受本预设约束、而它派出的子代理却是 `root-planner`——权限与规则整棵树错位,最后据此得出一批关于本预设的错误结论。**确认方法**:会话文件 `~/.dsh/sessions/<cwd>/<session-id>/session.v4.jsonl.zstd` 第一行的 `"agentPreset"` 字段应为 `root-planner`;或跑 `node tools/inspect-sessions.mjs <会话目录>` 一次性列出根与全部子会话的预设、深度与工具清单。
|
|
70
|
+
|
|
68
71
|
## 适合 / 不适合
|
|
69
72
|
|
|
70
73
|
**适合**:需要跨很多文件调研的长任务、逆向分析、代码审查,以及任何「读得多、改得少」或「改动必须被仔细决策」的工作。
|
package/build-patch.mjs
CHANGED
|
@@ -65,7 +65,7 @@ const patch = `# Generated by build-patch.mjs from persona.txt. Do not hand-edit
|
|
|
65
65
|
config:
|
|
66
66
|
id: root-planner
|
|
67
67
|
name: Root Planner
|
|
68
|
-
description:
|
|
68
|
+
description: Keeps the plan and the decisions in the root agent, dispatches reading, edits and commands to subagent workers, and lets nothing outlive the task.
|
|
69
69
|
order: 5
|
|
70
70
|
plugins:
|
|
71
71
|
- id: persona
|
|
@@ -84,6 +84,16 @@ const patch = `# Generated by build-patch.mjs from persona.txt. Do not hand-edit
|
|
|
84
84
|
- id: tool-pwsh
|
|
85
85
|
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
86
86
|
disabled: !!js process.platform !== 'win32'
|
|
87
|
+
# Subagents get a shell here (see child-tools.mjs), so background jobs
|
|
88
|
+
# are switched off for the whole preset. This is the one place the
|
|
89
|
+
# "no long-running services" rule is enforceable rather than advisory:
|
|
90
|
+
# with enableRunInBackground false the tool refuses a background call
|
|
91
|
+
# AND omits run_in_background from its schema entirely, so no model at
|
|
92
|
+
# any depth can reach for a detached process. A server a child starts
|
|
93
|
+
# without backgrounding holds its own call until the timeout and dies
|
|
94
|
+
# with it, which is the behaviour this preset wants.
|
|
95
|
+
config:
|
|
96
|
+
enableRunInBackground: false
|
|
87
97
|
- id: tool-fs
|
|
88
98
|
name: '@deepseek-ai/dsh-tool-fs'
|
|
89
99
|
- id: tool-fs-search
|
|
@@ -138,11 +148,13 @@ const patch = `# Generated by build-patch.mjs from persona.txt. Do not hand-edit
|
|
|
138
148
|
thresholdChars: 8192
|
|
139
149
|
headChars: 4096
|
|
140
150
|
tailChars: 1024
|
|
141
|
-
# Delegation. Both tools share ONE
|
|
142
|
-
# tree
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
#
|
|
151
|
+
# Delegation. Both tools share ONE mask (child-tools.mjs), so every child
|
|
152
|
+
# in the tree gets the same working set — read, write, shell — no matter
|
|
153
|
+
# which tool produced it, and every child is bounded the same way.
|
|
154
|
+
# maxDepth is left unset on purpose: it resolves from the Host
|
|
155
|
+
# subagent.maxDepth setting, which is what decides how many levels exist.
|
|
156
|
+
# Raising it deepens the tree; setting it to 0 disables delegation and
|
|
157
|
+
# breaks this preset's design.
|
|
146
158
|
- id: delegation
|
|
147
159
|
name: cordis:group
|
|
148
160
|
group: true
|
package/child-tools.mjs
CHANGED
|
@@ -3,27 +3,48 @@
|
|
|
3
3
|
* the verifier, so the two cannot disagree about what a subagent may call.
|
|
4
4
|
*
|
|
5
5
|
* These are the only GLOBAL tools a subagent may call. Everything absent is
|
|
6
|
-
* invisible to the child
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* `subagent` here rejected a name the mask can never legally contain. Do not add
|
|
23
|
-
* it back; `verify-patch.mjs` asserts its absence on purpose.
|
|
6
|
+
* invisible to the child.
|
|
7
|
+
*
|
|
8
|
+
* ## What a subagent MAY do
|
|
9
|
+
*
|
|
10
|
+
* A subagent reads, **changes files, and runs commands**. It has to: a delegate
|
|
11
|
+
* that cannot create anything is not a worker, and a "read-only worker" in a code
|
|
12
|
+
* project cannot run tests, git, or a verification script, so every executable
|
|
13
|
+
* step bounces back to the root and the parallelism the tree exists for buys
|
|
14
|
+
* nothing. `write`, `edit`, `ops_replace`, `ops_bash` and `pwsh` are all in the
|
|
15
|
+
* mask for that reason.
|
|
16
|
+
*
|
|
17
|
+
* ## What is forbidden, and how it is enforced
|
|
18
|
+
*
|
|
19
|
+
* The line is not "may not change anything" — it is **unbounded processes**. A
|
|
20
|
+
* subagent must not leave a service, watcher or daemon running past its task.
|
|
21
|
+
* That is enforced, not merely advised:
|
|
24
22
|
*
|
|
25
|
-
* `
|
|
26
|
-
*
|
|
23
|
+
* 1. `enableRunInBackground: false` on the preset's `tool-pwsh` row (set in
|
|
24
|
+
* `build-patch.mjs`). The tool then refuses a background call *and omits
|
|
25
|
+
* `run_in_background` from its schema*, so no model at any depth can reach for
|
|
26
|
+
* a detached process at all. Structural.
|
|
27
|
+
* 2. A persona rule covers what a schema cannot: `ops_bash` is a host tool with
|
|
28
|
+
* no per-preset config and no job registry, so a command blocking inside it is
|
|
29
|
+
* bounded only by the call timeout.
|
|
30
|
+
*
|
|
31
|
+
* A subagent that starts a long-running server is therefore self-limiting — the
|
|
32
|
+
* call holds until its timeout and the process dies with it.
|
|
33
|
+
*
|
|
34
|
+
* ## Why `subagent` is absent, and what that does NOT mean
|
|
35
|
+
*
|
|
36
|
+
* With `modelSelectionSettings: true` the spawn tool is mounted inside each composed
|
|
37
|
+
* Agent's own scope (`dsh-tool-subagent` registers it through
|
|
38
|
+
* `installScoped`), and `tools.restrict()` takes a mask of *global* tools
|
|
39
|
+
* only. It throws on a scope-local name exactly as it throws on an unknown
|
|
40
|
+
* one, which is how this bundle broke: every subagent creation failed with
|
|
41
|
+
* `tools.restrict() names unknown global tool "subagent"`.
|
|
42
|
+
*
|
|
43
|
+
* Its absence does **not** make a child a leaf. Restrictions filter globals only —
|
|
44
|
+
* "scoped registrations remain visible" — and a child inherits this preset, so it
|
|
45
|
+
* gets its own scoped `subagent` and delegates onward until the runtime depth cap
|
|
46
|
+
* stops it. Depth is the user's setting; the mask is not a depth limit. Do not add
|
|
47
|
+
* it back; `verify-patch.mjs` asserts its absence on purpose.
|
|
27
48
|
*/
|
|
28
49
|
export const CHILD_TOOLS = [
|
|
29
50
|
'read',
|
|
@@ -36,6 +57,20 @@ export const CHILD_TOOLS = [
|
|
|
36
57
|
'web_fetch',
|
|
37
58
|
'read_image',
|
|
38
59
|
'skill',
|
|
60
|
+
'write',
|
|
61
|
+
'edit',
|
|
62
|
+
'ops_replace',
|
|
63
|
+
// Running commands. `pwsh` is the one shell this profile reports as a global
|
|
64
|
+
// tool, and background jobs are switched off for it in the preset (see above).
|
|
65
|
+
//
|
|
66
|
+
// `ops_bash` is deliberately NOT here even though it looks like the natural
|
|
67
|
+
// choice on Windows: the runtime's own tool inventory — the list
|
|
68
|
+
// `tools.restrict()` prints when it refuses a name — includes
|
|
69
|
+
// ops_glob/ops_grep/ops_inspect_local_file but NOT ops_bash, so naming it breaks
|
|
70
|
+
// every child creation exactly the way `subagent` did. The dsh-ops shell
|
|
71
|
+
// component publishes it through a different path than its file component. Add
|
|
72
|
+
// it only after seeing it in your own environment's inventory line.
|
|
73
|
+
'pwsh',
|
|
39
74
|
'todo_write',
|
|
40
75
|
'ask_user_question',
|
|
41
76
|
'get_goal',
|
|
@@ -55,8 +90,13 @@ export const CHILD_TOOLS = [
|
|
|
55
90
|
export const SCOPE_LOCAL_TOOLS = ['subagent'];
|
|
56
91
|
|
|
57
92
|
/**
|
|
58
|
-
* The
|
|
59
|
-
*
|
|
60
|
-
*
|
|
93
|
+
* The capabilities a subagent must not reach. It is deliberately no longer a list
|
|
94
|
+
* of "things that write" — a subagent is *expected* to write, so forbidding that
|
|
95
|
+
* would contradict the mask above. What it must not reach is an unbounded process,
|
|
96
|
+
* and because the shell tool is present rather than absent, the property to verify
|
|
97
|
+
* is the tool's configuration, not its absence.
|
|
61
98
|
*/
|
|
62
|
-
export const
|
|
99
|
+
export const FORBIDDEN_CAPABILITIES = {
|
|
100
|
+
/** The preset's `tool-pwsh` row must carry this, or a child could detach a server. */
|
|
101
|
+
pwshConfig: { enableRunInBackground: false },
|
|
102
|
+
};
|
package/cordis.patch.yml
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
config:
|
|
12
12
|
id: root-planner
|
|
13
13
|
name: Root Planner
|
|
14
|
-
description:
|
|
14
|
+
description: Keeps the plan and the decisions in the root agent, dispatches reading, edits and commands to subagent workers, and lets nothing outlive the task.
|
|
15
15
|
order: 5
|
|
16
16
|
plugins:
|
|
17
17
|
- id: persona
|
|
@@ -42,24 +42,28 @@
|
|
|
42
42
|
## Root agent rules
|
|
43
43
|
|
|
44
44
|
1. Plan first. Establish the goal, success criteria, constraints, and an ordered task list before changing anything. Revise the plan when evidence contradicts it.
|
|
45
|
-
2.
|
|
46
|
-
3. Delegate research by default. The `subagent` tool (fresh child) and the `subagent_fork` tool (child seeded with this conversation) are the normal way to read, search, list, trace, or look something up — not a last resort. Delegate whenever locating an answer would take more than a couple of reads, whenever it spans several files, and whenever you notice you are only "looking around".
|
|
45
|
+
2. Decompose heavy work; do not delegate light work. Split a task when it exceeds a few reads or has clearly separable parts, and dispatch the parts in parallel where they are independent — rather than grinding through them serially. But delegation has a cost: every child is a fresh context, a round trip, and a lossy summary. A single file, a single count, one known path, or a question one search answers is **yours to do** — dispatching it costs more than it saves, and a tree of agents for a nineteen-file listing is waste, not rigor. When the user explicitly asks for multi-level delegation, that instruction outranks this budget. Delegation **depth** is the environment's business, not yours: delegate until the runtime refuses, then finish what remains yourself.
|
|
46
|
+
3. Delegate research by default — within that budget. The `subagent` tool (fresh child) and the `subagent_fork` tool (child seeded with this conversation) are the normal way to read, search, list, trace, or look something up — not a last resort. Delegate whenever locating an answer would take more than a couple of reads, whenever it spans several files, and whenever you notice you are only "looking around".
|
|
47
47
|
4. Keep delegations independent and concurrent. State every self-contained subtask in one message so they run together, then continue planning while they run. Wait only when your next action needs a specific result.
|
|
48
|
-
5. Read directly only when it is genuinely cheap: one known file, a short targeted range, or checking a specific claim a subagent made. Never bulk-browse a tree, never read a large file "to get oriented", and never re-read what a subagent already summarized.
|
|
48
|
+
5. Read directly only when it is genuinely cheap: one known file, a short targeted range, or checking a specific claim a subagent made. Never bulk-browse a tree, never read a large file "to get oriented", and never re-read what a subagent already summarized. **Verifying a contested or load-bearing claim is reading for a reason, not idling: when a summary's accuracy decides the next step, open the file and check it yourself.** Rule 8 tells you to distrust a summary; this is the clause that lets you act on that.
|
|
49
49
|
6. Ask for the summary shape you need. Tell the subagent which decision its finding must support, what to report (conclusion, evidence with file and line, uncertainty), and what to omit (raw dumps, unrelated context).
|
|
50
|
-
7. You own
|
|
50
|
+
7. You own the decisions about mutation; subagents carry out the ones you hand them. A subagent has `write` and `edit`, so it will change the files its task names — that is the point of giving it tools. What it must never do is decide on its own: **state in every delegation exactly which files it may touch and what it may run**, and treat any change outside that as a defect to correct, not initiative. Nothing about the repository's shape should change because a worker felt like it.
|
|
51
51
|
8. Treat a summary as evidence, not a verdict. Weigh it against the plan, spot-check claims that are contested or load-bearing, and send back a summary that dodges the question, hedges without evidence, or pads with prose.
|
|
52
|
-
9.
|
|
52
|
+
9. **Leave nothing running.** No servers, watchers, daemons, or detached processes — for you as much as for a subagent. Background execution is your shell's decision to offer, not a convenience to reach for: if a task seems to need a long-lived process, do it in the foreground, bound it with a timeout, or ask the user before starting anything that outlives the work.
|
|
53
|
+
10. Answer a subagent that reports a problem or an out-of-scope finding. Decide it, then reply with `send_message` so the child can continue: either the change is approved and who makes it, or it is out of scope and why, or what to do instead. An unanswered report leaves the child blocked on a decision only you can make. **A child that reports a clean result needs no reply** — silence is the correct response, so do not invent an acknowledgement.
|
|
54
|
+
11. Give concurrent subagents disjoint work. Two workers editing the same file produce a merge you have to untangle, and neither of them can see the other. Partition by file or by directory when you dispatch in parallel, and keep the integration — the final read-through of what changed — for yourself.
|
|
55
|
+
12. Decide who runs it, and do not hand a command down out of habit. Subagents do have a shell, so "run the tests and report" is a legitimate delegation — but the same budget as rule 2 applies: a single short command is cheaper run directly, and a delegated command returns you its output through a summary rather than raw. Hand down the commands that belong to a workstream a subagent already owns; run the ones that decide your own next step.
|
|
53
56
|
|
|
54
57
|
## Subagent rules
|
|
55
58
|
|
|
56
|
-
1. You
|
|
57
|
-
2.
|
|
58
|
-
3.
|
|
59
|
-
4.
|
|
60
|
-
5.
|
|
61
|
-
6.
|
|
62
|
-
7.
|
|
59
|
+
1. You can read, change files, and run commands — you are a worker, not an observer. **Read the task you were given for what you may do.** A bounded task means bounded: change the files the task is about, run the commands it needs, and leave everything else alone.
|
|
60
|
+
2. **Never leave something running.** No servers, watchers, daemons, background jobs, or detached processes — background execution is switched off for your shell, and `ops_bash` has no job registry, so a command that blocks there holds your call until it times out. If a task seems to need a service, start it only as a foreground command that finishes, or report that it needs one and let the delegating agent decide. A long-lived process is a decision above your level, not a detail you arrange quietly.
|
|
61
|
+
3. Keep every change inside your task. Do not reformat unrelated code, do not "fix what you noticed" on the way past, and do not touch files the task did not name. If you find another problem, report it — do not fix it.
|
|
62
|
+
4. Do not race the other agents. Assume siblings are writing files at the same time: work on the paths you were given, and if you need to change a file the task did not mention, say so in your report instead of changing it.
|
|
63
|
+
5. Answer the question you were asked and do not re-plan the parent's task. Bound the work, then return.
|
|
64
|
+
6. Report only what you found: state the conclusion, flag what you could not determine, and name the uncertainty. Do not pad, do not speculate past the evidence, and do not restate the request.
|
|
65
|
+
7. Format findings for the delegating agent's context budget. Lead with the conclusion, then the minimum evidence that establishes it (paths, line numbers, short quoted excerpts), then open questions. Omit raw file contents, exhaustive listings, and anything the reader did not ask for. Run commands with their output narrowed — a filtered result beats a full dump you expect the reader to sift.
|
|
66
|
+
8. You may be a leaf or you may still hold one more level of delegation. The runtime enforces the depth limit, not your restraint: if you have no delegation tool, or a delegation attempt is rejected for depth, that is the intended shape of the tree — **do the work yourself and say in your report that you did it directly because the tree ended there.** Do not route work onward to avoid doing it, do not treat a depth rejection as a failure, and do not hand the work back to the delegating agent merely because you cannot pass it further. If you cannot finish, or the request is ambiguous and cannot be resolved by doing the work, say so plainly and stop. Do not guess and do not invent a result.
|
|
63
67
|
|
|
64
68
|
## When to pass work down
|
|
65
69
|
|
|
@@ -73,7 +77,9 @@
|
|
|
73
77
|
|
|
74
78
|
## Reporting upward
|
|
75
79
|
|
|
76
|
-
Send the delegating agent what it needs to decide and nothing else: status, the conclusion, supporting evidence with locations, what remains uncertain, and
|
|
80
|
+
Send the delegating agent what it needs to decide and nothing else: status, the conclusion, supporting evidence with locations, what remains uncertain, and — for anything you changed — exactly which files and what changed in them. Do not open with a preamble and do not close with an offer to do more.
|
|
81
|
+
|
|
82
|
+
If the workspace has its own conventions for recording changes — a change log, a per-directory register, a commit message format — follow them only if your task told you to. Otherwise report what those conventions require alongside your result, so the agent that owns the record can write it: a worker that both changes files and decides how the change is recorded has moved the decision without saying so.
|
|
77
83
|
- id: agent-instructions
|
|
78
84
|
name: '@deepseek-ai/dsh-agent-instructions'
|
|
79
85
|
config:
|
|
@@ -84,6 +90,16 @@
|
|
|
84
90
|
- id: tool-pwsh
|
|
85
91
|
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
86
92
|
disabled: !!js process.platform !== 'win32'
|
|
93
|
+
# Subagents get a shell here (see child-tools.mjs), so background jobs
|
|
94
|
+
# are switched off for the whole preset. This is the one place the
|
|
95
|
+
# "no long-running services" rule is enforceable rather than advisory:
|
|
96
|
+
# with enableRunInBackground false the tool refuses a background call
|
|
97
|
+
# AND omits run_in_background from its schema entirely, so no model at
|
|
98
|
+
# any depth can reach for a detached process. A server a child starts
|
|
99
|
+
# without backgrounding holds its own call until the timeout and dies
|
|
100
|
+
# with it, which is the behaviour this preset wants.
|
|
101
|
+
config:
|
|
102
|
+
enableRunInBackground: false
|
|
87
103
|
- id: tool-fs
|
|
88
104
|
name: '@deepseek-ai/dsh-tool-fs'
|
|
89
105
|
- id: tool-fs-search
|
|
@@ -138,11 +154,13 @@
|
|
|
138
154
|
thresholdChars: 8192
|
|
139
155
|
headChars: 4096
|
|
140
156
|
tailChars: 1024
|
|
141
|
-
# Delegation. Both tools share ONE
|
|
142
|
-
# tree
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
#
|
|
157
|
+
# Delegation. Both tools share ONE mask (child-tools.mjs), so every child
|
|
158
|
+
# in the tree gets the same working set — read, write, shell — no matter
|
|
159
|
+
# which tool produced it, and every child is bounded the same way.
|
|
160
|
+
# maxDepth is left unset on purpose: it resolves from the Host
|
|
161
|
+
# subagent.maxDepth setting, which is what decides how many levels exist.
|
|
162
|
+
# Raising it deepens the tree; setting it to 0 disables delegation and
|
|
163
|
+
# breaks this preset's design.
|
|
146
164
|
- id: delegation
|
|
147
165
|
name: cordis:group
|
|
148
166
|
group: true
|
|
@@ -172,6 +190,10 @@
|
|
|
172
190
|
- web_fetch
|
|
173
191
|
- read_image
|
|
174
192
|
- skill
|
|
193
|
+
- write
|
|
194
|
+
- edit
|
|
195
|
+
- ops_replace
|
|
196
|
+
- pwsh
|
|
175
197
|
- todo_write
|
|
176
198
|
- ask_user_question
|
|
177
199
|
- get_goal
|
|
@@ -199,6 +221,10 @@
|
|
|
199
221
|
- web_fetch
|
|
200
222
|
- read_image
|
|
201
223
|
- skill
|
|
224
|
+
- write
|
|
225
|
+
- edit
|
|
226
|
+
- ops_replace
|
|
227
|
+
- pwsh
|
|
202
228
|
- todo_write
|
|
203
229
|
- ask_user_question
|
|
204
230
|
- get_goal
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -26,6 +26,8 @@ Paths below are relative to the repository root; this file lives in `docs/`.
|
|
|
26
26
|
| `cordis.patch.yml` | Generated. Do not hand-edit — regenerate instead. |
|
|
27
27
|
| `lib-yaml.mjs` | Resolves a YAML parser from the local DSH installation, so the scripts need no dependencies. |
|
|
28
28
|
| `verify-patch.mjs` | Parses the generated patch and checks persona integrity, role wiring, and the child mask. |
|
|
29
|
+
| `child-tools.mjs` | The child mask, defined once so the generator and the verifier cannot disagree. |
|
|
30
|
+
| `tools/inspect-sessions.mjs` | Forensics for a real run: prints each session's `agentPreset`, delegation depth and tool inventory from the `.jsonl.zstd` logs. Use it before trusting any claim about what a run did — an apparent bundle defect turned out twice to be the *root* session composed with a different preset. |
|
|
29
31
|
| `verify-depth-override.mjs` | Checks that the profile patch layer still parses and that the `subagent` depth override landed in the composed profile. |
|
|
30
32
|
| `package.json` | Bundle manifest (`dsh.bundle.patch`). |
|
|
31
33
|
| `.gitattributes` | Pins LF; without it a Windows checkout breaks the byte-identity check. |
|
|
@@ -230,5 +232,7 @@ directory is not read. `CLAUDE.md` is loaded with the same candidates, and ident
|
|
|
230
232
|
sibling files render once.
|
|
231
233
|
|
|
232
234
|
The persona carries one precedence clause instead of the conventions themselves:
|
|
233
|
-
project files apply, but the subagent
|
|
234
|
-
|
|
235
|
+
project files apply, but the subagent's task bound wins where they conflict —
|
|
236
|
+
a worker changes the files its task names and nothing else, and that is enforced by
|
|
237
|
+
the runtime's tool grant plus the persona, neither of which a project file can
|
|
238
|
+
override.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-root-planner-preset",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DSH agent preset that outsources file reading to strictly read-only subagents, while the root agent keeps the plan and owns every file mutation.",
|
|
6
6
|
"license": "MIT",
|
package/persona.txt
CHANGED
|
@@ -21,24 +21,28 @@ Reading is not planning. Raw file contents and raw query results belong in the a
|
|
|
21
21
|
## Root agent rules
|
|
22
22
|
|
|
23
23
|
1. Plan first. Establish the goal, success criteria, constraints, and an ordered task list before changing anything. Revise the plan when evidence contradicts it.
|
|
24
|
-
2.
|
|
25
|
-
3. Delegate research by default. The `subagent` tool (fresh child) and the `subagent_fork` tool (child seeded with this conversation) are the normal way to read, search, list, trace, or look something up — not a last resort. Delegate whenever locating an answer would take more than a couple of reads, whenever it spans several files, and whenever you notice you are only "looking around".
|
|
24
|
+
2. Decompose heavy work; do not delegate light work. Split a task when it exceeds a few reads or has clearly separable parts, and dispatch the parts in parallel where they are independent — rather than grinding through them serially. But delegation has a cost: every child is a fresh context, a round trip, and a lossy summary. A single file, a single count, one known path, or a question one search answers is **yours to do** — dispatching it costs more than it saves, and a tree of agents for a nineteen-file listing is waste, not rigor. When the user explicitly asks for multi-level delegation, that instruction outranks this budget. Delegation **depth** is the environment's business, not yours: delegate until the runtime refuses, then finish what remains yourself.
|
|
25
|
+
3. Delegate research by default — within that budget. The `subagent` tool (fresh child) and the `subagent_fork` tool (child seeded with this conversation) are the normal way to read, search, list, trace, or look something up — not a last resort. Delegate whenever locating an answer would take more than a couple of reads, whenever it spans several files, and whenever you notice you are only "looking around".
|
|
26
26
|
4. Keep delegations independent and concurrent. State every self-contained subtask in one message so they run together, then continue planning while they run. Wait only when your next action needs a specific result.
|
|
27
|
-
5. Read directly only when it is genuinely cheap: one known file, a short targeted range, or checking a specific claim a subagent made. Never bulk-browse a tree, never read a large file "to get oriented", and never re-read what a subagent already summarized.
|
|
27
|
+
5. Read directly only when it is genuinely cheap: one known file, a short targeted range, or checking a specific claim a subagent made. Never bulk-browse a tree, never read a large file "to get oriented", and never re-read what a subagent already summarized. **Verifying a contested or load-bearing claim is reading for a reason, not idling: when a summary's accuracy decides the next step, open the file and check it yourself.** Rule 8 tells you to distrust a summary; this is the clause that lets you act on that.
|
|
28
28
|
6. Ask for the summary shape you need. Tell the subagent which decision its finding must support, what to report (conclusion, evidence with file and line, uncertainty), and what to omit (raw dumps, unrelated context).
|
|
29
|
-
7. You own
|
|
29
|
+
7. You own the decisions about mutation; subagents carry out the ones you hand them. A subagent has `write` and `edit`, so it will change the files its task names — that is the point of giving it tools. What it must never do is decide on its own: **state in every delegation exactly which files it may touch and what it may run**, and treat any change outside that as a defect to correct, not initiative. Nothing about the repository's shape should change because a worker felt like it.
|
|
30
30
|
8. Treat a summary as evidence, not a verdict. Weigh it against the plan, spot-check claims that are contested or load-bearing, and send back a summary that dodges the question, hedges without evidence, or pads with prose.
|
|
31
|
-
9.
|
|
31
|
+
9. **Leave nothing running.** No servers, watchers, daemons, or detached processes — for you as much as for a subagent. Background execution is your shell's decision to offer, not a convenience to reach for: if a task seems to need a long-lived process, do it in the foreground, bound it with a timeout, or ask the user before starting anything that outlives the work.
|
|
32
|
+
10. Answer a subagent that reports a problem or an out-of-scope finding. Decide it, then reply with `send_message` so the child can continue: either the change is approved and who makes it, or it is out of scope and why, or what to do instead. An unanswered report leaves the child blocked on a decision only you can make. **A child that reports a clean result needs no reply** — silence is the correct response, so do not invent an acknowledgement.
|
|
33
|
+
11. Give concurrent subagents disjoint work. Two workers editing the same file produce a merge you have to untangle, and neither of them can see the other. Partition by file or by directory when you dispatch in parallel, and keep the integration — the final read-through of what changed — for yourself.
|
|
34
|
+
12. Decide who runs it, and do not hand a command down out of habit. Subagents do have a shell, so "run the tests and report" is a legitimate delegation — but the same budget as rule 2 applies: a single short command is cheaper run directly, and a delegated command returns you its output through a summary rather than raw. Hand down the commands that belong to a workstream a subagent already owns; run the ones that decide your own next step.
|
|
32
35
|
|
|
33
36
|
## Subagent rules
|
|
34
37
|
|
|
35
|
-
1. You
|
|
36
|
-
2.
|
|
37
|
-
3.
|
|
38
|
-
4.
|
|
39
|
-
5.
|
|
40
|
-
6.
|
|
41
|
-
7.
|
|
38
|
+
1. You can read, change files, and run commands — you are a worker, not an observer. **Read the task you were given for what you may do.** A bounded task means bounded: change the files the task is about, run the commands it needs, and leave everything else alone.
|
|
39
|
+
2. **Never leave something running.** No servers, watchers, daemons, background jobs, or detached processes — background execution is switched off for your shell, and `ops_bash` has no job registry, so a command that blocks there holds your call until it times out. If a task seems to need a service, start it only as a foreground command that finishes, or report that it needs one and let the delegating agent decide. A long-lived process is a decision above your level, not a detail you arrange quietly.
|
|
40
|
+
3. Keep every change inside your task. Do not reformat unrelated code, do not "fix what you noticed" on the way past, and do not touch files the task did not name. If you find another problem, report it — do not fix it.
|
|
41
|
+
4. Do not race the other agents. Assume siblings are writing files at the same time: work on the paths you were given, and if you need to change a file the task did not mention, say so in your report instead of changing it.
|
|
42
|
+
5. Answer the question you were asked and do not re-plan the parent's task. Bound the work, then return.
|
|
43
|
+
6. Report only what you found: state the conclusion, flag what you could not determine, and name the uncertainty. Do not pad, do not speculate past the evidence, and do not restate the request.
|
|
44
|
+
7. Format findings for the delegating agent's context budget. Lead with the conclusion, then the minimum evidence that establishes it (paths, line numbers, short quoted excerpts), then open questions. Omit raw file contents, exhaustive listings, and anything the reader did not ask for. Run commands with their output narrowed — a filtered result beats a full dump you expect the reader to sift.
|
|
45
|
+
8. You may be a leaf or you may still hold one more level of delegation. The runtime enforces the depth limit, not your restraint: if you have no delegation tool, or a delegation attempt is rejected for depth, that is the intended shape of the tree — **do the work yourself and say in your report that you did it directly because the tree ended there.** Do not route work onward to avoid doing it, do not treat a depth rejection as a failure, and do not hand the work back to the delegating agent merely because you cannot pass it further. If you cannot finish, or the request is ambiguous and cannot be resolved by doing the work, say so plainly and stop. Do not guess and do not invent a result.
|
|
42
46
|
|
|
43
47
|
## When to pass work down
|
|
44
48
|
|
|
@@ -52,4 +56,6 @@ A delegated task is not delegated again: hand a given question to at most one ag
|
|
|
52
56
|
|
|
53
57
|
## Reporting upward
|
|
54
58
|
|
|
55
|
-
Send the delegating agent what it needs to decide and nothing else: status, the conclusion, supporting evidence with locations, what remains uncertain, and
|
|
59
|
+
Send the delegating agent what it needs to decide and nothing else: status, the conclusion, supporting evidence with locations, what remains uncertain, and — for anything you changed — exactly which files and what changed in them. Do not open with a preamble and do not close with an offer to do more.
|
|
60
|
+
|
|
61
|
+
If the workspace has its own conventions for recording changes — a change log, a per-directory register, a commit message format — follow them only if your task told you to. Otherwise report what those conventions require alongside your result, so the agent that owns the record can write it: a worker that both changes files and decides how the change is recorded has moved the decision without saying so.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Forensics on the real root-planner test run: which preset did the root session
|
|
2
|
+
// compose with, and what tools did each level actually receive?
|
|
3
|
+
//
|
|
4
|
+
// Usage: node inspect-sessions.mjs <rootSessionDir> [childSessionDir ...]
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { zstdDecompressSync } from 'node:zlib';
|
|
9
|
+
|
|
10
|
+
/** Read one .jsonl.zstd session log into parsed events. */
|
|
11
|
+
function readSession(dir) {
|
|
12
|
+
const raw = zstdDecompressSync(readFileSync(join(dir, 'session.v4.jsonl.zstd')));
|
|
13
|
+
return raw
|
|
14
|
+
.toString('utf8')
|
|
15
|
+
.split('\n')
|
|
16
|
+
.filter((line) => line.trim().length > 0)
|
|
17
|
+
.map((line) => {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(line);
|
|
20
|
+
} catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const dir of process.argv.slice(2)) {
|
|
28
|
+
const events = readSession(dir);
|
|
29
|
+
const types = new Map();
|
|
30
|
+
for (const event of events) types.set(event.type, (types.get(event.type) ?? 0) + 1);
|
|
31
|
+
|
|
32
|
+
process.stdout.write(`\n===== ${dir.split(/[\\/]/).pop()} — ${events.length} events =====\n`);
|
|
33
|
+
process.stdout.write(`event types: ${[...types.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12).map(([t, n]) => `${t}×${n}`).join(', ')}\n`);
|
|
34
|
+
|
|
35
|
+
// Session header: the composition that was actually in force.
|
|
36
|
+
for (const event of events) {
|
|
37
|
+
if (event.type === 'session/header' || event.type === 'session/start') {
|
|
38
|
+
const h = event.data ?? event;
|
|
39
|
+
process.stdout.write(
|
|
40
|
+
`header: origin=${h.header?.origin ?? h.origin} parent=${h.header?.parentSession ?? h.parentSession} ` +
|
|
41
|
+
`depth=${h.header?.delegationDepth ?? h.delegationDepth} preset=${h.preset ?? h.presetId ?? '(not in header)'}\n`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Tool names actually offered to this session, from its request payloads.
|
|
47
|
+
const offered = new Set();
|
|
48
|
+
for (const event of events) {
|
|
49
|
+
const message = event.data?.request?.messages ?? event.request?.messages;
|
|
50
|
+
if (!Array.isArray(message)) continue;
|
|
51
|
+
for (const entry of message) {
|
|
52
|
+
if (entry?.role !== 'system') continue;
|
|
53
|
+
const text = typeof entry.content === 'string' ? entry.content : JSON.stringify(entry.content);
|
|
54
|
+
for (const match of text.matchAll(/"name":\s*"([a-z_0-9]+)"/g)) offered.add(match[1]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (offered.size > 0) {
|
|
58
|
+
process.stdout.write(`tools seen in request payloads (${offered.size}): ${[...offered].sort().join(', ')}\n`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Any tool error mentioning a mask or an executor.
|
|
62
|
+
for (const event of events) {
|
|
63
|
+
const text = JSON.stringify(event);
|
|
64
|
+
for (const needle of ['unknown global tool', 'EXECUTOR_UNAVAILABLE', 'SubagentDepthError', 'exceeds maxDepth']) {
|
|
65
|
+
if (text.includes(needle)) {
|
|
66
|
+
const start = Math.max(0, text.indexOf(needle) - 220);
|
|
67
|
+
process.stdout.write(` [${needle}] …${text.slice(start, text.indexOf(needle) + 220)}…\n`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Verifies a real root-planner run from its own session logs.
|
|
2
|
+
//
|
|
3
|
+
// Why this exists: twice now, a conclusion about this preset was drawn from a run
|
|
4
|
+
// whose ROOT session was composed with a different preset (the children were
|
|
5
|
+
// root-planner while the root was `standard`), so every rule and permission in the
|
|
6
|
+
// tree was mismatched — and the run looked like a bundle defect. Reading the logs
|
|
7
|
+
// is the only way to tell. This script does it mechanically.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// node tools/verify-session.mjs <session-dir> [--expect-depth N] [--quiet]
|
|
11
|
+
//
|
|
12
|
+
// <session-dir> is the ROOT session directory under
|
|
13
|
+
// ~/.dsh/sessions/<cwd-slug>/ ; child directories sitting beside it are found by
|
|
14
|
+
// parentSession, so pass only the root.
|
|
15
|
+
|
|
16
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { createZstdDecompress } from 'node:zlib';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Decompress a concatenated-zstd file. The session logs are appended frame by
|
|
22
|
+
* frame, and a single decompress call stops at the first frame — which yields a
|
|
23
|
+
* 300-byte file out of an 85 KB one and silently hides every event.
|
|
24
|
+
* @param {Buffer} buf - whole file.
|
|
25
|
+
* @returns {Promise<string>} the complete decompressed text.
|
|
26
|
+
*/
|
|
27
|
+
async function decompressAll(buf) {
|
|
28
|
+
const out = [];
|
|
29
|
+
let rest = buf;
|
|
30
|
+
for (;;) {
|
|
31
|
+
const text = await new Promise((resolve, reject) => {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
const dec = createZstdDecompress();
|
|
34
|
+
dec.on('data', (chunk) => chunks.push(chunk));
|
|
35
|
+
dec.on('error', reject);
|
|
36
|
+
dec.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
37
|
+
dec.end(rest);
|
|
38
|
+
});
|
|
39
|
+
if (text.length === 0) break;
|
|
40
|
+
out.push(text);
|
|
41
|
+
// Each frame's decompressed size is not exposed; feed the remainder in and let
|
|
42
|
+
// the next frame fail once the buffer is exhausted.
|
|
43
|
+
const consumed = frameBytes(rest, text.length);
|
|
44
|
+
if (consumed === undefined || consumed >= rest.length) break;
|
|
45
|
+
rest = rest.subarray(consumed);
|
|
46
|
+
}
|
|
47
|
+
return out.join('');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Locate the end of the first zstd frame by magic-number scanning. */
|
|
51
|
+
function frameBytes(buf, ) {
|
|
52
|
+
for (let i = 4; i + 4 <= buf.length; i += 1) {
|
|
53
|
+
if (buf[i] === 0x28 && buf[i + 1] === 0xb5 && buf[i + 2] === 0x2f && buf[i + 3] === 0xfd) return i;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Read one session directory into {header, events}. */
|
|
59
|
+
async function readSession(dir) {
|
|
60
|
+
const text = await decompressAll(readFileSync(join(dir, 'session.v4.jsonl.zstd')));
|
|
61
|
+
const events = text
|
|
62
|
+
.split('\n')
|
|
63
|
+
.filter((line) => line.trim().length > 0)
|
|
64
|
+
.flatMap((line) => {
|
|
65
|
+
try {
|
|
66
|
+
return [JSON.parse(line)];
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return { header: events[0], events };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const argv = process.argv.slice(2);
|
|
75
|
+
const quiet = argv.includes('--quiet');
|
|
76
|
+
const expectDepthIndex = argv.indexOf('--expect-depth');
|
|
77
|
+
const expectDepth = expectDepthIndex === -1 ? undefined : Number(argv[expectDepthIndex + 1]);
|
|
78
|
+
const rootDir = argv.find((arg) => !arg.startsWith('--') && !/^\d+$/.test(arg));
|
|
79
|
+
|
|
80
|
+
if (rootDir === undefined) {
|
|
81
|
+
process.stderr.write('usage: node tools/verify-session.mjs <root-session-dir> [--expect-depth N]\n');
|
|
82
|
+
process.exit(2);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const base = join(rootDir, '..');
|
|
86
|
+
const root = await readSession(rootDir);
|
|
87
|
+
|
|
88
|
+
const sessions = [{ dir: rootDir, ...root }];
|
|
89
|
+
for (const name of readdirSync(base)) {
|
|
90
|
+
if (name === rootDir.split(/[\\/]/).pop()) continue;
|
|
91
|
+
const candidate = join(base, name);
|
|
92
|
+
try {
|
|
93
|
+
if (!statSync(join(candidate, 'session.v4.jsonl.zstd')).isFile()) continue;
|
|
94
|
+
} catch {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const session = await readSession(candidate);
|
|
98
|
+
// Only real children count. Sibling ROOT sessions of other conversations live in
|
|
99
|
+
// the same directory and have no parentSession — including them made the report
|
|
100
|
+
// claim the run failed when it had not.
|
|
101
|
+
if (session.header?.origin !== 'subagent') continue;
|
|
102
|
+
sessions.push({ dir: candidate, ...session });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const failures = [];
|
|
106
|
+
const check = (ok, label) => {
|
|
107
|
+
if (ok) {
|
|
108
|
+
if (!quiet) process.stdout.write(`ok ${label}\n`);
|
|
109
|
+
} else {
|
|
110
|
+
process.stdout.write(`FAIL ${label}\n`);
|
|
111
|
+
failures.push(label);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (!quiet) process.stdout.write(`root session: ${root.header?.id}\n`);
|
|
116
|
+
|
|
117
|
+
check(
|
|
118
|
+
root.header?.agentPreset === 'root-planner',
|
|
119
|
+
`root session is composed with root-planner (found "${root.header?.agentPreset ?? '?'}") — if this fails, the run proves nothing about the preset`,
|
|
120
|
+
);
|
|
121
|
+
check(root.header?.delegationDepth === 0, `root is at depth 0 (found ${root.header?.delegationDepth})`);
|
|
122
|
+
|
|
123
|
+
const children = sessions.filter((session) => session.header?.parentSession === root.header?.id);
|
|
124
|
+
const byId = new Map(sessions.map((session) => [session.header?.id, session]));
|
|
125
|
+
const depths = new Map([[root.header?.id, 0]]);
|
|
126
|
+
const depthOf = (session) => {
|
|
127
|
+
const known = depths.get(session.header?.id);
|
|
128
|
+
if (known !== undefined) return known;
|
|
129
|
+
const parent = byId.get(session.header?.parentSession);
|
|
130
|
+
const value = parent === undefined ? 1 : depthOf(parent) + 1;
|
|
131
|
+
depths.set(session.header?.id, value);
|
|
132
|
+
return value;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (!quiet) {
|
|
136
|
+
process.stdout.write(`\n${sessions.length} session(s) total:\n`);
|
|
137
|
+
for (const session of sessions) {
|
|
138
|
+
process.stdout.write(
|
|
139
|
+
` ${String(session.header?.id).slice(0, 20)} preset=${String(session.header?.agentPreset).padEnd(14)} ` +
|
|
140
|
+
`depth=${depthOf(session)} parent=${String(session.header?.parentSession ?? '(root)').slice(0, 20)}\n`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
process.stdout.write('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const descendants = sessions.filter((session) => session !== sessions[0]);
|
|
147
|
+
check(descendants.length > 0, 'the root actually delegated (at least one child session exists)');
|
|
148
|
+
|
|
149
|
+
for (const session of descendants) {
|
|
150
|
+
const depth = depthOf(session);
|
|
151
|
+
check(
|
|
152
|
+
session.header?.agentPreset === 'root-planner',
|
|
153
|
+
`child ${String(session.header?.id).slice(0, 8)} at depth ${depth} inherits root-planner (found "${session.header?.agentPreset ?? '?'}")`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const maxDepth = Math.max(0, ...[...sessions].map((session) => depthOf(session)));
|
|
158
|
+
if (expectDepth !== undefined) {
|
|
159
|
+
check(maxDepth === expectDepth, `deepest session is depth ${expectDepth} (found ${maxDepth})`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// A delegation attempted from a leaf is the depth cap doing its job, not a defect.
|
|
163
|
+
let capped = 0;
|
|
164
|
+
for (const session of sessions) {
|
|
165
|
+
const text = JSON.stringify(session.events);
|
|
166
|
+
for (const needle of ['exceeds maxDepth', 'SubagentDepthError', 'unknown global tool']) {
|
|
167
|
+
if (text.includes(needle)) capped += 1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!quiet && capped > 0) process.stdout.write(`note: ${capped} depth-cap or mask rejection(s) recorded in the logs\n`);
|
|
171
|
+
|
|
172
|
+
process.stdout.write(
|
|
173
|
+
failures.length === 0
|
|
174
|
+
? `\nVERIFIED: root and all ${descendants.length} descendant session(s) used root-planner; max depth ${maxDepth}.\n`
|
|
175
|
+
: `\n${failures.length} CHECK(S) FAILED — this run does not demonstrate the preset.\n`,
|
|
176
|
+
);
|
|
177
|
+
process.exit(failures.length === 0 ? 0 : 1);
|
package/verify-patch.mjs
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
// 1. every patch file parses as YAML,
|
|
3
3
|
// 2. the preset row and its delegation rows exist,
|
|
4
4
|
// 3. the parsed persona is byte-identical to persona.txt (no YAML mangling),
|
|
5
|
-
// 4. the child tool mask names only global tools,
|
|
6
|
-
//
|
|
5
|
+
// 4. the child tool mask names only global tools, keeps the write and shell tools
|
|
6
|
+
// a worker needs, and excludes agent-scoped names tools.restrict() would reject,
|
|
7
|
+
// 5. the "no unbounded process" property holds: the preset's pwsh row has
|
|
8
|
+
// background jobs switched off.
|
|
7
9
|
//
|
|
8
10
|
// Usage: node verify-patch.mjs
|
|
9
11
|
|
|
10
12
|
import { readFileSync } from 'node:fs';
|
|
11
13
|
import { dirname, join } from 'node:path';
|
|
12
14
|
import { fileURLToPath } from 'node:url';
|
|
13
|
-
import { CHILD_TOOLS,
|
|
15
|
+
import { CHILD_TOOLS, FORBIDDEN_CAPABILITIES, SCOPE_LOCAL_TOOLS } from './child-tools.mjs';
|
|
14
16
|
import { loadYaml, loaderSchema as makeLoaderSchema } from './lib-yaml.mjs';
|
|
15
17
|
|
|
16
18
|
const yaml = loadYaml();
|
|
@@ -54,7 +56,11 @@ check(personaRow?.config?.suffix === 'Your working directory is {{cwd}}.', 'pers
|
|
|
54
56
|
const expected = readFileSync(join(here, 'persona.txt'), 'utf8').replace(/\r\n/g, '\n').trimEnd();
|
|
55
57
|
check(personaRow?.config?.prefix === expected, 'parsed persona is byte-identical to persona.txt');
|
|
56
58
|
check(!personaRow?.config?.prefix?.includes('\t'), 'persona contains no tab characters');
|
|
57
|
-
check(/read-only/.test(personaRow?.config?.prefix ?? ''), 'persona
|
|
59
|
+
check(/read-only/.test(personaRow?.config?.prefix ?? '') === false, 'persona no longer claims subagents are read-only (they are workers with write and shell)');
|
|
60
|
+
check(
|
|
61
|
+
/Never leave something running/.test(personaRow?.config?.prefix ?? ''),
|
|
62
|
+
'persona forbids long-running processes for subagents',
|
|
63
|
+
);
|
|
58
64
|
|
|
59
65
|
const delegation = plugins.find((row) => row.id === 'delegation');
|
|
60
66
|
check(delegation?.group === true, 'delegation is a group row');
|
|
@@ -71,13 +77,14 @@ const childTools = [
|
|
|
71
77
|
return { id, toolName, allow: row?.config?.toolFilter?.allow };
|
|
72
78
|
});
|
|
73
79
|
|
|
74
|
-
//
|
|
80
|
+
// A subagent is a worker: it keeps the write and shell tools, so the mask must
|
|
81
|
+
// only exclude what a mask can name and what would unbounded a child's process.
|
|
75
82
|
for (const { id, allow } of childTools) {
|
|
76
83
|
check(Array.isArray(allow) && allow.length > 0, `${id} declares a non-empty allow list`);
|
|
77
|
-
for (const denied of MUTATION_TOOLS) {
|
|
78
|
-
check(!allow?.includes(denied), `${id} mask excludes ${denied}`);
|
|
79
|
-
}
|
|
80
84
|
check(allow?.includes('read'), `${id} mask keeps read`);
|
|
85
|
+
for (const worker of ['write', 'edit', 'ops_replace', 'pwsh']) {
|
|
86
|
+
check(allow?.includes(worker), `${id} mask keeps ${worker} (a subagent must be able to work)`);
|
|
87
|
+
}
|
|
81
88
|
for (const scoped of SCOPE_LOCAL_TOOLS) {
|
|
82
89
|
check(
|
|
83
90
|
!allow?.includes(scoped),
|
|
@@ -86,6 +93,16 @@ for (const { id, allow } of childTools) {
|
|
|
86
93
|
}
|
|
87
94
|
}
|
|
88
95
|
|
|
96
|
+
// "No unbounded process": with the shell tools present, the property lives in the
|
|
97
|
+
// preset's own rows rather than in the mask.
|
|
98
|
+
const pwshRow = plugins.find((row) => row.id === 'tool-pwsh');
|
|
99
|
+
check(pwshRow !== undefined, 'the preset declares tool-pwsh');
|
|
100
|
+
check(
|
|
101
|
+
pwshRow?.config?.enableRunInBackground === FORBIDDEN_CAPABILITIES.pwshConfig.enableRunInBackground,
|
|
102
|
+
`tool-pwsh sets enableRunInBackground=${FORBIDDEN_CAPABILITIES.pwshConfig.enableRunInBackground} ` +
|
|
103
|
+
`(found ${JSON.stringify(pwshRow?.config?.enableRunInBackground)}) — without it a subagent could detach a server`,
|
|
104
|
+
);
|
|
105
|
+
|
|
89
106
|
/**
|
|
90
107
|
* The global tool inventory of the author's profile, captured from a runtime
|
|
91
108
|
* error rather than hand-kept: `tools.restrict()` prints every known global tool
|