dsh-root-planner-preset 1.0.5 → 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.
@@ -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 read-only guarantee. `write`, `edit`, `ops_replace`,
76
- `ops_bash` and `pwsh` must stay out of the child mask; that property is the
77
- point of the preset, so a change that removes it will be declined.
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/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 reading from deciding**.
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 task analysis, planning and decisions; file reading and information queries go to subagents that return summaries only. Creating, modifying and deleting files stays with the root agent — subagents do not even have the tools.
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
- > One agent in the tree has hands. Every other agent has only eyes.
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" — does not hold on its own: a subagent will still edit files on its way past, and work gets passed further down without limit.
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 turns both of those into structural constraints rather than good intentions:
15
+ This preset takes the middle line: **workers with tools, bounded by their task.**
16
16
 
17
17
  | Requirement | Mechanism | Strength |
18
18
  |---|---|---|
19
- | Subagents **cannot** create, modify or delete files | A `toolFilter.allow` list on both delegation tools, without `write`, `edit`, `ops_replace` | **Structural**: an absent tool cannot be called |
20
- | Subagents **have no shell** to work around it | The same list, without `ops_bash`, `pwsh` | **Structural** |
21
- | Delegation depth is bounded by configuration | The runtime depth cap (`subagent.maxDepth`, a Host setting) | **Structural**: past the cap it throws `SubagentDepthError` |
22
- | The root agent owns every mutation | Persona rules (the mask applies to children only) | Prompt-level |
23
- | A heavy task is decomposed rather than ground through | Persona rule 2 | Prompt-level |
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, not a deny list: a tool added by a future plugin is invisible to subagents until it is added here — **with one exception, and it is what makes recursion work.**
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
- └── the only one with write / edit / shell; every level stays read-only
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** strips every mutation entry point from *every* subagent, at *every*
38
- level — the guarantee does not weaken with depth.
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), delegates reading, searching and locating by default, and reads directly only for one known file, a short targeted range, or to check a claim a subagent made.
56
- - **Subagent**: reads, locates and summarizes. On finding that a file **needs changing** it does not act; it reports the exact change — target path, location (`file:line`), current content, proposed replacement, and why.
57
- - **The root agent decides** on that proposal and then makes the change itself.
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
 
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 的上下文只装任务分析、规划与决策;文件阅读、信息查询交给子代理,子代理只回摘要。而**创建、修改、删除文件只能由根 agent 执行**——子代理连工具都没有。
5
+ 根 agent 的上下文装计划与决策;阅读、改文件、跑命令交给子代理,子代理回报摘要。**子代理是有工具的工人**——它有 `write`、`edit` 和 shell;唯一不许留下的是**还在跑的进程**。
6
6
 
7
- > 一句话:让整棵树里只有一个 agent 有手,其余都只有眼睛。
7
+ > 根负责决策,工人负责干活,没有任何东西能活过任务本身。
8
8
 
9
9
  [English](README.md) | [安装步骤](INSTALL.md) | [维护者文档](docs/DEVELOPMENT.md)
10
10
 
11
11
  ## 它解决什么问题
12
12
 
13
- 用 coding agent 干长任务,上下文很快被原始文件内容塞满,于是忘了目标、丢了计划、开始重复劳动。常见对策是「让 agent 多派子代理」,但光靠提示词约束不住——子代理照样会顺手改文件,任务也会被无限往下转派。
13
+ 用 coding agent 干长任务,上下文很快被原始文件内容和命令输出塞满,于是忘了目标、丢了计划、开始重复劳动。常见对策「让 agent 多派子代理」会朝两个相反方向失败:**没有手的子代理什么也干不了**,每一步都被推回根;而**没有边界的子代理群**会随手改任何文件、还会给你留下一堆跑着的服务。
14
14
 
15
- 这个预设把两件事做成了结构约束,而不是靠自觉:
15
+ 本预设走中间那条线:**有工具的工人,边界是它的任务。**
16
16
 
17
17
  | 要求 | 机制 | 强度 |
18
18
  |---|---|---|
19
- | 子代理**不能**创建/修改/删除文件 | 两个委派工具上的 `toolFilter.allow` 白名单,不含 `write`、`edit`、`ops_replace` | **结构性**:工具不存在,调不到 |
20
- | 子代理**没有** shell 可绕过 | 同一白名单,不含 `ops_bash`、`pwsh` | **结构性** |
21
- | 委派层数由配置决定 | 运行时深度上限(`subagent.maxDepth`,Host 设置) | **结构性**:超过上限直接抛 `SubagentDepthError` |
22
- | 根 agent 独揽所有修改 | 人设规则(白名单只作用于子级) | 提示词级 |
23
- | 任务过重时拆分下发而非硬扛 | 人设规则 2 | 提示词级 |
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
- └── 只有它有 write / edit / shell;每一层都保持只读
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
- - **子代理**:只读、只定位、只总结。发现**需要改文件**时不动手,而是回报精确的改动方案——目标路径、位置(file:line)、当前内容、建议替换、理由。
46
- - **根 agent 收到方案后决策**,然后亲自落盘。
45
+ - **根 agent**:先立计划(目标、成功判据、约束、任务清单),把值得一次往返的活派出去,自己保留集成、把关与决策——并在每次委派里**点名允许改哪些文件**。
46
+ - **子代理**:干活。读、改被点名的文件、跑属于它的命令,然后回报结论与最小证据,外加**确切改了哪些文件**。
47
+ - **根决策**改什么、谁改;工人改了任务之外的东西,那是缺陷,不是主动性。
47
48
 
48
49
  子代理怎么知道自己是子代理?DSH 会在每个子代理的提示词里强制插入一句 `You are a delegated subagent: …`,运行时注入、无法移除,而预设人设**没有** `{{role}}` 这类变量可用。所以人设里写的是一条**判定指令**:读自己的提示词里有没有那句话。这让人设只需要一份,父与子读到的是同一份文本,因此不会出现「父以为子被这么约束、子其实没读到」的漂移。
49
50
 
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: Plans in a lean context, delegates reading to strictly read-only subagents, and owns every file mutation.
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 readonly mask, so every child in the
142
- # tree is read-only no matter which tool produced it. maxDepth is left
143
- # unset on purpose: it resolves from the Host subagent.maxDepth setting
144
- # (default 1), and this preset needs unbounded depth for read-only
145
- # recursion. Do not raise that Host setting to 0 while using this preset.
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, including every mutation entry point.
7
- *
8
- * `subagent` is deliberately NOT here, and cannot be — but that does NOT mean a
9
- * subagent lacks it. Two separate facts:
10
- *
11
- * 1. With `modelSelectionSettings: true` the tool is mounted inside each composed
12
- * Agent's own scope (`dsh-tool-subagent` registers it through
13
- * `installScoped`), and `tools.restrict()` takes a mask of *global* tools
14
- * only. It throws on a scope-local name exactly as it throws on an unknown
15
- * one, which is how this bundle broke: every subagent creation failed with
16
- * `tools.restrict() names unknown global tool "subagent"`.
17
- * 2. Restrictions only filter globals — "scoped registrations remain visible".
18
- * A child inherits this preset, so it gets its own scoped `subagent` and can
19
- * delegate onward until the runtime depth cap stops it.
20
- *
21
- * So the mask is not what makes a child a leaf; `subagent.maxDepth` is. Listing
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
- * `subagent_fork` IS registered in the preset's own scope and is observable as a
26
- * global, so it stays in the list.
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 mutation entry points this bundle promises a subagent cannot reach. The
59
- * verifier asserts each one is absent; `ops_bash` and `bash` are included even
60
- * when a profile registers neither, because their absence is the property.
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 MUTATION_TOOLS = ['write', 'edit', 'ops_replace', 'ops_bash', 'pwsh', 'bash'];
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: Plans in a lean context, delegates reading to strictly read-only subagents, and owns every file mutation.
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
@@ -47,20 +47,23 @@
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
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 every mutation. Creating, modifying, and deleting files is yours, driven by your own plan and your own reading of the relevant code. Never delegate it and never infer it from a summary alone. **No subagent at any depth may write without your explicit authorisation in the task you gave it** — say so when you delegate, rather than assuming the tooling enforces it.
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. Answer a subagent that reports a needed file change. Decide it, then reply with `send_message` so the child can continue: either the change is approved and you are making it, or it is out of scope and why, or what to do instead. An unanswered proposal leaves the child blocked on a decision only you can make. **A child that proposes nothing needs no reply** — silence is the correct response to a clean report, so do not invent an acknowledgement.
53
- 10. Running things is your job, not a child's. Subagents have no shell, so they cannot run tests, git, or verification scripts; delegating "run this and report" fails at the first step. Hand that work to a child only as "read these files and tell me what they imply", and run the command yourself.
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.
54
56
 
55
57
  ## Subagent rules
56
58
 
57
- 1. You are read-only, and this is structural rather than advisory: you have no tool that can create, modify, rename, or delete a file or directory, and no shell. Do not hunt for a workaround and do not retry a denied operation.
58
- 2. When the work needs a file changed, do not change it. Report the change instead: target path, exact location (file and line), current text or state, proposed replacement, and why it is needed. The root agent decides and executes.
59
- 3. A file change is never a silent side effect of research. Scratch artifacts, reformatting, "fixing what you noticed" along the way, and running a generator that rewrites tracked files are all forbidden under this rule; report them instead.
60
- 4. Answer the question you were asked and do not re-plan the parent's task. Bound the work, then return.
61
- 5. 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.
62
- 6. 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.
63
- 7. 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 — **read the material yourself and summarize, and say in your report that you did the reading directly because the tree ended there.** Do not route work onward to avoid reading, 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 reading cannot resolve it, say so plainly and stop. Do not guess and do not invent a result.
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.
64
67
 
65
68
  ## When to pass work down
66
69
 
@@ -74,9 +77,9 @@
74
77
 
75
78
  ## Reporting upward
76
79
 
77
- Send the delegating agent what it needs to decide and nothing else: status, the conclusion, supporting evidence with locations, what remains uncertain, and any proposed file change in the exact form given above. Do not open with a preamble and do not close with an offer to do more.
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.
78
81
 
79
- If the workspace has its own conventions for recording changes — a change log, a per-directory register, a commit message format — a subagent does not perform them: it reports what those conventions require, in the same form as a proposed file change. The agent that can write is the one that records, and it already holds the files that say how.
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.
80
83
  - id: agent-instructions
81
84
  name: '@deepseek-ai/dsh-agent-instructions'
82
85
  config:
@@ -87,6 +90,16 @@
87
90
  - id: tool-pwsh
88
91
  name: '@deepseek-ai/dsh-tool-pwsh'
89
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
90
103
  - id: tool-fs
91
104
  name: '@deepseek-ai/dsh-tool-fs'
92
105
  - id: tool-fs-search
@@ -141,11 +154,13 @@
141
154
  thresholdChars: 8192
142
155
  headChars: 4096
143
156
  tailChars: 1024
144
- # Delegation. Both tools share ONE readonly mask, so every child in the
145
- # tree is read-only no matter which tool produced it. maxDepth is left
146
- # unset on purpose: it resolves from the Host subagent.maxDepth setting
147
- # (default 1), and this preset needs unbounded depth for read-only
148
- # recursion. Do not raise that Host setting to 0 while using this preset.
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.
149
164
  - id: delegation
150
165
  name: cordis:group
151
166
  group: true
@@ -175,6 +190,10 @@
175
190
  - web_fetch
176
191
  - read_image
177
192
  - skill
193
+ - write
194
+ - edit
195
+ - ops_replace
196
+ - pwsh
178
197
  - todo_write
179
198
  - ask_user_question
180
199
  - get_goal
@@ -202,6 +221,10 @@
202
221
  - web_fetch
203
222
  - read_image
204
223
  - skill
224
+ - write
225
+ - edit
226
+ - ops_replace
227
+ - pwsh
205
228
  - todo_write
206
229
  - ask_user_question
207
230
  - get_goal
@@ -232,5 +232,7 @@ directory is not read. `CLAUDE.md` is loaded with the same candidates, and ident
232
232
  sibling files render once.
233
233
 
234
234
  The persona carries one precedence clause instead of the conventions themselves:
235
- project files apply, but the subagent read-only rule wins where they conflict,
236
- because the runtime enforces it and a project file cannot.
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.5",
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
@@ -26,20 +26,23 @@ Reading is not planning. Raw file contents and raw query results belong in the a
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
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 every mutation. Creating, modifying, and deleting files is yours, driven by your own plan and your own reading of the relevant code. Never delegate it and never infer it from a summary alone. **No subagent at any depth may write without your explicit authorisation in the task you gave it** — say so when you delegate, rather than assuming the tooling enforces it.
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. Answer a subagent that reports a needed file change. Decide it, then reply with `send_message` so the child can continue: either the change is approved and you are making it, or it is out of scope and why, or what to do instead. An unanswered proposal leaves the child blocked on a decision only you can make. **A child that proposes nothing needs no reply** — silence is the correct response to a clean report, so do not invent an acknowledgement.
32
- 10. Running things is your job, not a child's. Subagents have no shell, so they cannot run tests, git, or verification scripts; delegating "run this and report" fails at the first step. Hand that work to a child only as "read these files and tell me what they imply", and run the command yourself.
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.
33
35
 
34
36
  ## Subagent rules
35
37
 
36
- 1. You are read-only, and this is structural rather than advisory: you have no tool that can create, modify, rename, or delete a file or directory, and no shell. Do not hunt for a workaround and do not retry a denied operation.
37
- 2. When the work needs a file changed, do not change it. Report the change instead: target path, exact location (file and line), current text or state, proposed replacement, and why it is needed. The root agent decides and executes.
38
- 3. A file change is never a silent side effect of research. Scratch artifacts, reformatting, "fixing what you noticed" along the way, and running a generator that rewrites tracked files are all forbidden under this rule; report them instead.
39
- 4. Answer the question you were asked and do not re-plan the parent's task. Bound the work, then return.
40
- 5. 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.
41
- 6. 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.
42
- 7. 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 — **read the material yourself and summarize, and say in your report that you did the reading directly because the tree ended there.** Do not route work onward to avoid reading, 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 reading cannot resolve it, say so plainly and stop. Do not guess and do not invent a result.
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.
43
46
 
44
47
  ## When to pass work down
45
48
 
@@ -53,6 +56,6 @@ A delegated task is not delegated again: hand a given question to at most one ag
53
56
 
54
57
  ## Reporting upward
55
58
 
56
- Send the delegating agent what it needs to decide and nothing else: status, the conclusion, supporting evidence with locations, what remains uncertain, and any proposed file change in the exact form given above. Do not open with a preamble and do not close with an offer to do more.
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.
57
60
 
58
- If the workspace has its own conventions for recording changes — a change log, a per-directory register, a commit message format — a subagent does not perform them: it reports what those conventions require, in the same form as a proposed file change. The agent that can write is the one that records, and it already holds the files that say how.
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,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, excludes every mutation entry
6
- // point, and excludes agent-scoped names that tools.restrict() would reject.
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, MUTATION_TOOLS, SCOPE_LOCAL_TOOLS } from './child-tools.mjs';
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 states the read-only rule for subagents');
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
- // The mask must deny every mutation entry point, and it can only name global tools.
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