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.
- package/.github/CONTRIBUTING.md +7 -3
- package/README.md +19 -17
- package/README.zh.md +17 -16
- package/build-patch.mjs +18 -6
- package/child-tools.mjs +64 -24
- package/cordis.patch.yml +41 -18
- package/docs/DEVELOPMENT.md +4 -2
- package/package.json +1 -1
- package/persona.txt +15 -12
- 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/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
|
|
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
|
|
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
|
|
@@ -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
|
|
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.
|
|
53
|
-
10.
|
|
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
|
|
58
|
-
2.
|
|
59
|
-
3.
|
|
60
|
-
4.
|
|
61
|
-
5.
|
|
62
|
-
6.
|
|
63
|
-
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.
|
|
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
|
|
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 —
|
|
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
|
|
145
|
-
# tree
|
|
146
|
-
#
|
|
147
|
-
#
|
|
148
|
-
#
|
|
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
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -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
|
|
236
|
-
|
|
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
|
@@ -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
|
|
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.
|
|
32
|
-
10.
|
|
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
|
|
37
|
-
2.
|
|
38
|
-
3.
|
|
39
|
-
4.
|
|
40
|
-
5.
|
|
41
|
-
6.
|
|
42
|
-
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.
|
|
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
|
|
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 —
|
|
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,
|
|
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
|