dsh-root-planner-preset 1.0.2 → 1.0.4

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.
@@ -0,0 +1,92 @@
1
+ # Contributing
2
+
3
+ Thanks for looking. This repository is meant to be maintained by more than its
4
+ author — the license is [MIT](../LICENSE), and issues and pull requests are open
5
+ to anyone.
6
+ ## The one thing to know first
7
+
8
+ **`cordis.patch.yml` is generated. Do not edit it by hand.**
9
+
10
+ It is produced from `persona.txt` by `build-patch.mjs`, and it has to be indented
11
+ into a YAML block scalar exactly — a single mis-indented line silently truncates
12
+ the ~6 KB persona, which is very hard to spot in review and easy to introduce by
13
+ hand. That is the whole reason the generator exists.
14
+
15
+ So to change what the agent is told:
16
+
17
+ ```sh
18
+ $EDITOR persona.txt
19
+ node build-patch.mjs # regenerates cordis.patch.yml
20
+ node verify-patch.mjs # must print ALL CHECKS PASSED
21
+ ```
22
+
23
+ Commit **both** files. CI re-runs the generator and fails if the committed patch
24
+ does not match `persona.txt` byte for byte, so forgetting the second command is
25
+ caught for you.
26
+
27
+ ## Setup
28
+
29
+ The bundle itself ships no dependencies. The repository's scripts need one YAML
30
+ parser, which `lib-yaml.mjs` resolves from your own DSH installation — or from a
31
+ local install:
32
+
33
+ ```sh
34
+ npm ci # installs js-yaml, the only devDependency
35
+ ```
36
+
37
+ ## What the checks mean
38
+
39
+ `node verify-patch.mjs` asserts, among other things:
40
+
41
+ - the patch parses as loader YAML (including the `!!js` scalars, read as opaque);
42
+ - the persona parsed out of the patch is **byte-identical** to `persona.txt`;
43
+ - both delegation tools publish `subagent` / `subagent_fork`;
44
+ - both `toolFilter.allow` masks exclude every mutation entry point
45
+ (`write`, `edit`, `ops_replace`, `ops_bash`, `pwsh`) and name **only tools that
46
+ exist** — a name that is not a registered global tool makes
47
+ `tools.restrict()` throw the first time a subagent is created.
48
+
49
+ That last point is the most likely thing to need changing on your machine: the
50
+ mask was written against the author's profile. If your profile registers a
51
+ different set of global tools, the mask is what has to adapt — edit the list near
52
+ the bottom of `verify-patch.mjs` in the same commit.
53
+
54
+ `node verify-depth-override.mjs` checks the delegation depth override. Depth is a
55
+ **Host setting, not part of this bundle**, so on a machine that never set it the
56
+ script prints `SKIP` and explains why. That is a pass, not a failure.
57
+
58
+ ## Most valuable contributions, in order
59
+
60
+ 1. **Bug reports with real error text**, plus your DSH version and which global
61
+ tools your profile registers. The `Not verified` section of the
62
+ [README](../README.md) names exactly where this matters: no subagent has been
63
+ spawned under this preset in the author's environment to date.
64
+ 2. **Mask fixes for other environments** — see above.
65
+ 3. **Persona improvements** — the text itself, when you have an observed failure
66
+ it would have prevented.
67
+ 4. **Documentation and translations** — [README.zh.md](../README.zh.md) and
68
+ [INSTALL.md](../INSTALL.md) especially.
69
+
70
+ ## Ground rules
71
+
72
+ - **One concern per pull request.** A persona change and a mask change are two
73
+ pull requests unless one requires the other.
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.
78
+ - Keep `main` linear — the branch protection refuses merge commits, so rebase
79
+ rather than merge.
80
+
81
+ ## How `main` is protected
82
+
83
+ `main` requires the `verify` status check (strict: the branch must be up to date),
84
+ refuses force pushes and deletions, requires linear history, and requires
85
+ conversations to be resolved. The repository owner can bypass these for a direct
86
+ push in an emergency; **collaborators cannot** — send a pull request and let CI
87
+ run.
88
+
89
+ If you are added as a collaborator and want to be able to merge your own pull
90
+ requests without waiting for the owner, say so: review requirements are off
91
+ deliberately, because a single-maintainer repository that requires an approval it
92
+ cannot get would deadlock.
@@ -0,0 +1,51 @@
1
+ # Verifies the bundle on every push and pull request.
2
+ #
3
+ # Two failure modes this exists to catch, both of which are silent locally:
4
+ #
5
+ # 1. `cordis.patch.yml` is generated from `persona.txt`. Someone edits the
6
+ # generated file by hand (or edits persona.txt and forgets to regenerate), and
7
+ # the persona the preset actually ships no longer matches the source. The
8
+ # generator is deterministic, so re-running it and diffing the result catches
9
+ # exactly that drift.
10
+ # 2. A change to the child tool mask names a tool that is not a registered global
11
+ # tool. `tools.restrict()` throws on an unknown name at child-creation time, so
12
+ # this only surfaces the first time a subagent is actually spawned — never in a
13
+ # local syntax check.
14
+ #
15
+ # The bundle itself ships no dependencies; `js-yaml` is a devDependency of this
16
+ # repository only, and `lib-yaml.mjs` picks it up from the checkout.
17
+
18
+ name: verify
19
+
20
+ on:
21
+ push:
22
+ branches: [main]
23
+ pull_request:
24
+ workflow_dispatch:
25
+
26
+ permissions:
27
+ contents: read
28
+
29
+ jobs:
30
+ verify:
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+
35
+ - uses: actions/setup-node@v4
36
+ with:
37
+ node-version: '22'
38
+
39
+ - name: Install the local YAML parser
40
+ run: npm ci --ignore-scripts
41
+
42
+ - name: The committed patch matches persona.txt
43
+ run: |
44
+ node build-patch.mjs
45
+ if ! git diff --exit-code -- cordis.patch.yml; then
46
+ echo "::error file=cordis.patch.yml::cordis.patch.yml is out of date. Run 'node build-patch.mjs' and commit the result instead of editing it by hand."
47
+ exit 1
48
+ fi
49
+
50
+ - name: Patch structure, persona byte-identity and the child tool mask
51
+ run: node verify-patch.mjs
package/INSTALL.md CHANGED
@@ -107,7 +107,7 @@ git clone https://github.com/XiaoOu2024/dsh-root-planner-preset
107
107
  |---|---|---|
108
108
  | 预设列表里**没有 Root Planner** | bundle 没装上,或预设行激活失败(会带诊断留在名册上) | 看 DSH 的插件/加载诊断;多半是第 1 条前置不满足——本包的补丁引用了随包预设里不存在的工具行 |
109
109
  | DSH **启动失败**(进程直接退出) | DSH 启动是全有全无,一个插件加载失败整个进程就退出 | 用插件市场的「**调整插件**」恢复界面:DSH 点名的插件会标红并默认取消勾选,勾好再启动。找不到该界面时,手工编辑 profile 的 `cordis.patch.yml` 把对应行设为 `disabled: true` |
110
- | 子代理创建失败,报 **`tools.restrict() names unknown global tool …`** | 白名单里有你的环境**没注册**的工具名 | 按报错里点出的名字,从 `cordis.patch.yml` 两个 `toolFilter.allow` 里删掉该行,重装或重开会话 |
110
+ | 子代理创建失败,报 **`tools.restrict() names unknown global tool …`** | 白名单里有**不是全局工具**的名字。注意有两种情况:①你的环境根本没注册该工具;②它是**agent 作用域**工具(如 `subagent`,它注册在每个被组合 Agent 自身的作用域里),而 `toolFilter` **只接受全局工具名** | 报错里会列出该环境全部的全局工具名,照着它删掉白名单里那几个不存在的名字。白名单在 `cordis.patch.yml` 的两个 `toolFilter.allow` 处(或在 `child-tools.mjs` 里改后重跑生成器),改完必须重开会话 |
111
111
  | 委派立刻返回**深度错误** | `maxDepth` 没设或设得太小 | 见第三节,设为 `2` |
112
112
  | 子代理**什么都不能读** | 白名单过窄(比如你删过头了) | 至少保留 `read`、`glob`、`grep`、`ops_inspect_local_file`、`ops_glob`、`ops_grep` |
113
113
  | 子代理**还是能改文件** | 白名单被改坏了 | 确认 `cordis.patch.yml` 里两处 `toolFilter.allow` 都**不含** `write`、`edit`、`ops_replace`、`ops_bash`、`pwsh`;改完必须重开会话 |
package/README.md CHANGED
@@ -18,21 +18,40 @@ This preset turns both of those into structural constraints rather than good int
18
18
  |---|---|---|
19
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
20
  | Subagents **have no shell** to work around it | The same list, without `ops_bash`, `pwsh` | **Structural** |
21
- | Work cannot be passed down without limit | The runtime delegation depth limit (`subagent.maxDepth`) | **Structural**: past the cap it throws `SubagentDepthError` |
21
+ | Delegation depth is bounded by configuration | The runtime depth cap (`subagent.maxDepth`, a Host setting) | **Structural**: past the cap it throws `SubagentDepthError` |
22
22
  | The root agent owns every mutation | Persona rules (the mask applies to children only) | Prompt-level |
23
- | The root agent delegates reading by default | Persona rules | Prompt-level |
23
+ | A heavy task is decomposed rather than ground through | Persona rule 2 | Prompt-level |
24
24
 
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.
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
26
 
27
27
  ## How it works
28
28
 
29
29
  ```
30
- root agent (depth 0) <- the only one with write / edit / shell
31
- ├── subagent (depth 1) <- read-only; may delegate one more level
32
- │ └── grandchild (depth 2) <- read-only; a leaf
33
- └── subagent (depth 1)
30
+ root agent ──delegates──▶ subagent ──delegates──▶ … until maxDepth
31
+ │ │
32
+ └── the only one with write / edit / shell; every level stays read-only
34
33
  ```
35
34
 
35
+ Two mechanisms decide how deep the tree goes, and they pull in the same direction:
36
+
37
+ - **The mask** strips every mutation entry point from *every* subagent, at *every*
38
+ level — the guarantee does not weaken with depth.
39
+ - **The depth cap** (`subagent.maxDepth`, a Host setting you control) decides how
40
+ many levels exist. `2` means root → child → grandchild; raise it and the tree
41
+ deepens, set it to `1` and children become leaves.
42
+
43
+ A `toolFilter` is a mask of **global** tools, and `subagent` is not one: with
44
+ `modelSelectionSettings: true` it is mounted inside each composed Agent's own
45
+ scope, so a mask can never name it. That is why it is absent from
46
+ `child-tools.mjs` — **not** because children lack it. Scoped registrations stay
47
+ visible through a restriction, and every child inherits this preset, so each level
48
+ gets its own `subagent` and can delegate onward until the depth cap stops it.
49
+
50
+ An earlier version of this bundle did list `subagent` in the mask, and **every
51
+ subagent creation failed** with
52
+ `tools.restrict() names unknown global tool "subagent"` — the mask rejecting a
53
+ name it could never legally contain, not the tool being missing.
54
+
36
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.
37
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.
38
57
  - **The root agent decides** on that proposal and then makes the change itself.
@@ -81,13 +100,15 @@ Then **start a new session** and pick **Root Planner** in the preset selector. F
81
100
 
82
101
  ## Verified / not verified
83
102
 
84
- **Verified**: the patch parses as loader YAML; the parsed persona is **byte-identical** to `persona.txt`; both delegation masks exclude every mutation entry point and name only tools this profile actually registers; a clean checkout passes verification (`.gitattributes` pins LF, without which a Windows clone breaks the byte-identity check); the depth override reached the composed profile.
103
+ **Verified**: the patch parses as loader YAML; the parsed persona is **byte-identical** to `persona.txt`; both delegation masks exclude every mutation entry point and name only tools the runtime reports as global; a clean checkout passes verification (`.gitattributes` pins LF, without which a Windows clone breaks the byte-identity check); the depth override reached the composed profile.
104
+
105
+ **Fixed after a real failure**: a subagent spawn on a live `root-planner` session failed with `tools.restrict() names unknown global tool "subagent"`. The cause was a design error in this bundle, not a misconfiguration — `subagent` is registered inside each composed Agent's own scope, and a `toolFilter` takes global names only, so the mask was naming something it could never contain. The mask no longer names it, `verify-patch.mjs` asserts its absence by intent, and the mask is defined once in `child-tools.mjs` so the generator and the verifier cannot drift apart.
85
106
 
86
- **Not verified**: **no subagent has actually been spawned under the `root-planner` preset yet.** The tool names in the mask were cross-checked against the author's environment, not exercised at runtime. If your environment lacks one of those global tool names, child creation fails with `tools.restrict() names unknown global tool …`. See the troubleshooting section of [INSTALL.md](INSTALL.md).
107
+ **Still not verified**: **no subagent has been spawned successfully under this preset yet.** Recursion is expected to work for the reason given above — the child inherits the preset and therefore its own scoped `subagent`, and a restriction does not hide scoped registrations — but that reasoning is exactly the kind that this bundle has already been wrong about once. Treat it as unconfirmed until someone reports a two-level delegation working.
87
108
 
88
109
  ## Contributing
89
110
 
90
- Issues and pull requests are welcome — this repository is meant to be maintained by more than its author. The license is [MIT](LICENSE), so forking, modifying and redistributing are all permitted.
111
+ Issues and pull requests are welcome — this repository is meant to be maintained by more than its author. The license is [MIT](LICENSE), so forking, modifying and redistributing are all permitted. **[CONTRIBUTING.md](.github/CONTRIBUTING.md) has the workflow, the checks, and the ground rules** — read it before changing the persona.
91
112
 
92
113
  Useful contributions, roughly in order of value:
93
114
 
package/README.zh.md CHANGED
@@ -18,21 +18,29 @@
18
18
  |---|---|---|
19
19
  | 子代理**不能**创建/修改/删除文件 | 两个委派工具上的 `toolFilter.allow` 白名单,不含 `write`、`edit`、`ops_replace` | **结构性**:工具不存在,调不到 |
20
20
  | 子代理**没有** shell 可绕过 | 同一白名单,不含 `ops_bash`、`pwsh` | **结构性** |
21
- | 任务不会被无限转派 | 运行时深度上限(`subagent.maxDepth`) | **结构性**:超过上限直接抛 `SubagentDepthError` |
21
+ | 委派层数由配置决定 | 运行时深度上限(`subagent.maxDepth`,Host 设置) | **结构性**:超过上限直接抛 `SubagentDepthError` |
22
22
  | 根 agent 独揽所有修改 | 人设规则(白名单只作用于子级) | 提示词级 |
23
- | 根 agent 默认委派阅读 | 人设规则 | 提示词级 |
23
+ | 任务过重时拆分下发而非硬扛 | 人设规则 2 | 提示词级 |
24
24
 
25
- 白名单是 **allow 而非 deny**:将来某个插件新增的工具,对子代理默认不可见。
25
+ 白名单是 **allow 而非 deny**:将来某个插件新增的工具,对子代理默认不可见——**但有一个例外,而正是它让递归得以成立**。
26
26
 
27
27
  ## 工作方式
28
28
 
29
29
  ```
30
- 根 agent (depth 0) ← 只有它有 write / edit / shell
31
- ├── 子代理 (depth 1) ← 只读;可以再派一层
32
- │ └── 孙代理 (depth 2) ← 只读;叶子
33
- └── 子代理 (depth 1)
30
+ 根 agent ──委派──▶ 子代理 ──委派──▶ … 直到 maxDepth
31
+ │ │
32
+ └── 只有它有 write / edit / shell;每一层都保持只读
34
33
  ```
35
34
 
35
+ 有两个机制决定树有多深,方向一致:
36
+
37
+ - **白名单**剥掉**每一层**、**每一个**子代理的所有写入入口——这个保证不随深度衰减。
38
+ - **深度上限**(`subagent.maxDepth`,由你控制的 Host 设置)决定有几层。`2` 就是根 → 子 → 孙;调大则更深,设成 `1` 则子代理变叶子。
39
+
40
+ `toolFilter` 是**全局**工具的掩码,而 `subagent` 不是全局工具:因为 `modelSelectionSettings: true`,它被注册进每个被组合 Agent **自身的作用域**,掩码永远写不上它的名字。所以它不在 `child-tools.mjs` 里——**不是**因为子代理没有它。作用域注册**不受限制影响**,而每个子代理都继承本预设,于是每一层都拿到自己的 `subagent`,可以一直往下派到深度上限为止。
41
+
42
+ 本包早期版本确实把 `subagent` 写进了白名单,结果是**每一次子代理创建都失败**,报 `tools.restrict() names unknown global tool "subagent"`——那是白名单在拒绝一个它永远不可能合法包含的名字,不是工具缺失。
43
+
36
44
  - **根 agent**:先立计划(目标、成功判据、约束、任务清单),把阅读/搜索/定位默认派出去,自己只在「单个已知文件、一小段范围、或核实子代理的某个断言」时才亲自读。
37
45
  - **子代理**:只读、只定位、只总结。发现**需要改文件**时不动手,而是回报精确的改动方案——目标路径、位置(file:line)、当前内容、建议替换、理由。
38
46
  - **根 agent 收到方案后决策**,然后亲自落盘。
@@ -81,13 +89,15 @@ dsh plugin --profile web add github:XiaoOu2024/dsh-root-planner-preset
81
89
 
82
90
  ## 已验证 / 未验证
83
91
 
84
- **已验证**:补丁可被 loader 解析;解析出的人设与 `persona.txt` **逐字节相同**;两个委派工具的白名单排除了所有写入入口、且只包含本机实际注册的工具名;干净检出后校验通过(`.gitattributes` 已钉死 LF,否则 Windows 克隆会破坏逐字节校验);深度覆盖已落到合成配置。
92
+ **已验证**:补丁可被 loader 解析;解析出的人设与 `persona.txt` **逐字节相同**;两个委派工具的白名单排除了所有写入入口、且只包含**运行时报告的全局工具**;干净检出后校验通过(`.gitattributes` 已钉死 LF,否则 Windows 克隆会破坏逐字节校验);深度覆盖已落到合成配置。
93
+
94
+ **因一次真实故障而修复**:在跑着 `root-planner` 的会话里创建子代理失败,报 `tools.restrict() names unknown global tool "subagent"`。根因是本包**设计上的错误**,不是配置错误——`subagent` 注册在每个被组合 Agent 自身的作用域里,而 `toolFilter` 只接受全局工具名,白名单写了一个它永远不可能合法包含的名字。现在白名单不再写它,`verify-patch.mjs` 会**按意图断言它必须缺席**,白名单也收敛到 `child-tools.mjs` 单一真源,生成器与校验脚本不可能再各说各话。
85
95
 
86
- **未验证**:**尚未在 `root-planner` 预设下真正派出过一个子代理。** 白名单里的工具名是按作者环境的实际工具清单交叉核对出来的,不是运行时实测。若你的环境缺少其中某个全局工具名,子代理创建会失败并报 `tools.restrict() names unknown global tool …`。详见 [INSTALL.md](INSTALL.md) 的故障排查。
96
+ **仍未验证**:**还没有一次成功的子代理派出。** 递归按上面的理由**应当**可用——子代理继承预设、因此拥有自己的作用域 `subagent`,而限制不会隐藏作用域注册——但这个推理恰好就是本包已经错过一次的那类推理。在有人报告"两层委派成功"之前,请当作未确认。
87
97
 
88
98
  ## 参与维护
89
99
 
90
- **欢迎提 issue 和 PR——这个仓库本来就打算由作者之外的人一起维护。** 许可证是 [MIT](LICENSE),fork、修改、再分发都是允许的。
100
+ **欢迎提 issue 和 PR——这个仓库本来就打算由作者之外的人一起维护。** 许可证是 [MIT](LICENSE),fork、修改、再分发都是允许的。**[CONTRIBUTING.md](.github/CONTRIBUTING.md) 写清了流程、校验项和底线要求**,改人设之前请先读它。
91
101
 
92
102
  按价值排序,有用的贡献大概是这样:
93
103
 
package/build-patch.mjs CHANGED
@@ -10,6 +10,7 @@
10
10
  import { readFileSync, writeFileSync } from 'node:fs';
11
11
  import { dirname, join } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
+ import { CHILD_TOOLS } from './child-tools.mjs';
13
14
 
14
15
  const here = dirname(fileURLToPath(import.meta.url));
15
16
 
@@ -44,32 +45,10 @@ function allowList(names, keyDepth) {
44
45
  }
45
46
 
46
47
  /**
47
- * The child mask: the only global tools a subagent may call. Everything absent
48
- * is invisible to the child, including every mutation entry point.
48
+ * The child mask lives in `child-tools.mjs` so this generator and
49
+ * `verify-patch.mjs` cannot disagree about it. See that file for why `subagent`
50
+ * cannot appear in the list.
49
51
  */
50
- const CHILD_TOOLS = [
51
- 'read',
52
- 'glob',
53
- 'grep',
54
- 'ops_inspect_local_file',
55
- 'ops_glob',
56
- 'ops_grep',
57
- 'web_search',
58
- 'web_fetch',
59
- 'read_image',
60
- 'skill',
61
- 'todo_write',
62
- 'ask_user_question',
63
- 'get_goal',
64
- 'create_goal',
65
- 'update_goal',
66
- 'list_agents',
67
- 'send_message',
68
- 'interrupt_agent',
69
- 'subagent',
70
- 'subagent_fork',
71
- ];
72
-
73
52
  const persona = readPersona();
74
53
 
75
54
  /** The delegation group is shared by both shapes of child, so the mask is declared once. */
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The child tool mask — the single source of truth shared by the generator and
3
+ * the verifier, so the two cannot disagree about what a subagent may call.
4
+ *
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.
24
+ *
25
+ * `subagent_fork` IS registered in the preset's own scope and is observable as a
26
+ * global, so it stays in the list.
27
+ */
28
+ export const CHILD_TOOLS = [
29
+ 'read',
30
+ 'glob',
31
+ 'grep',
32
+ 'ops_inspect_local_file',
33
+ 'ops_glob',
34
+ 'ops_grep',
35
+ 'web_search',
36
+ 'web_fetch',
37
+ 'read_image',
38
+ 'skill',
39
+ 'todo_write',
40
+ 'ask_user_question',
41
+ 'get_goal',
42
+ 'create_goal',
43
+ 'update_goal',
44
+ 'list_agents',
45
+ 'send_message',
46
+ 'interrupt_agent',
47
+ 'subagent_fork',
48
+ ];
49
+
50
+ /**
51
+ * Tools that are agent-scoped rather than global, so a mask can never name them.
52
+ * Kept separate so the verifier can assert their absence by intent rather than
53
+ * by accident.
54
+ */
55
+ export const SCOPE_LOCAL_TOOLS = ['subagent'];
56
+
57
+ /**
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.
61
+ */
62
+ export const MUTATION_TOOLS = ['write', 'edit', 'ops_replace', 'ops_bash', 'pwsh', 'bash'];
package/cordis.patch.yml CHANGED
@@ -42,13 +42,14 @@
42
42
  ## Root agent rules
43
43
 
44
44
  1. Plan first. Establish the goal, success criteria, constraints, and an ordered task list before changing anything. Revise the plan when evidence contradicts it.
45
- 2. Delegate research by default. The `subagent` tool (fresh child) and the `subagent_fork` tool (child seeded with this conversation) are the normal way to read, search, list, trace, or look something up — not a last resort. Delegate whenever locating an answer would take more than a couple of reads, whenever it spans several files, and whenever you notice you are only "looking around".
46
- 3. 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.
47
- 4. Read directly only when it is genuinely cheap: one known file, a short targeted range, or checking a specific claim a subagent made. Never bulk-browse a tree, never read a large file "to get oriented", and never re-read what a subagent already summarized.
48
- 5. 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).
49
- 6. 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.
50
- 7. 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.
51
- 8. 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.
45
+ 2. When a task feels too heavy, decompose it — do not grind through it serially. Weight is a signal, not a reason to push harder: split the work into independent subtasks and dispatch them, in parallel where they are independent, rather than reading everything yourself in sequence. A task that exceeds a few reads, or that has clearly separable parts, should already have been split. Delegation depth is the environment's business, not yours: delegate until the runtime refuses, then read what remains yourself.
46
+ 3. Delegate research by default. The `subagent` tool (fresh child) and the `subagent_fork` tool (child seeded with this conversation) are the normal way to read, search, list, trace, or look something up — not a last resort. Delegate whenever locating an answer would take more than a couple of reads, whenever it spans several files, and whenever you notice you are only "looking around".
47
+ 4. Keep delegations independent and concurrent. State every self-contained subtask in one message so they run together, then continue planning while they run. Wait only when your next action needs a specific result.
48
+ 5. Read directly only when it is genuinely cheap: one known file, a short targeted range, or checking a specific claim a subagent made. Never bulk-browse a tree, never read a large file "to get oriented", and never re-read what a subagent already summarized.
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.
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.
52
53
 
53
54
  ## Subagent rules
54
55
 
@@ -179,7 +180,6 @@
179
180
  - list_agents
180
181
  - send_message
181
182
  - interrupt_agent
182
- - subagent
183
183
  - subagent_fork
184
184
  - id: tool-subagent-fork
185
185
  name: '@deepseek-ai/dsh-tool-subagent'
@@ -207,7 +207,6 @@
207
207
  - list_agents
208
208
  - send_message
209
209
  - interrupt_agent
210
- - subagent
211
210
  - subagent_fork
212
211
  - id: tool-subagent-codex
213
212
  name: '@deepseek-ai/dsh-tool-subagent'
@@ -31,6 +31,8 @@ Paths below are relative to the repository root; this file lives in `docs/`.
31
31
  | `.gitattributes` | Pins LF; without it a Windows checkout breaks the byte-identity check. |
32
32
  | `README.md` / `README.zh.md` | The English and Chinese front pages, cross-linked. npm renders the repository root `README.md`, which is why the English one holds that name. |
33
33
  | `INSTALL.md` | Recipient-facing setup steps and troubleshooting (Chinese). |
34
+ | `.github/CONTRIBUTING.md` | The contributor workflow: regenerate, verify, and the rules a change must not break. |
35
+ | `.github/workflows/verify.yml` | CI: fails when `cordis.patch.yml` drifts from `persona.txt`, and runs the mask check. |
34
36
 
35
37
  ```sh
36
38
  node build-patch.mjs # after every persona.txt edit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-root-planner-preset",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
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",
@@ -26,5 +26,8 @@
26
26
  "bundle": {
27
27
  "patch": "./cordis.patch.yml"
28
28
  }
29
+ },
30
+ "devDependencies": {
31
+ "js-yaml": "^4.1.0"
29
32
  }
30
33
  }
package/persona.txt CHANGED
@@ -21,13 +21,14 @@ Reading is not planning. Raw file contents and raw query results belong in the a
21
21
  ## Root agent rules
22
22
 
23
23
  1. Plan first. Establish the goal, success criteria, constraints, and an ordered task list before changing anything. Revise the plan when evidence contradicts it.
24
- 2. Delegate research by default. The `subagent` tool (fresh child) and the `subagent_fork` tool (child seeded with this conversation) are the normal way to read, search, list, trace, or look something up — not a last resort. Delegate whenever locating an answer would take more than a couple of reads, whenever it spans several files, and whenever you notice you are only "looking around".
25
- 3. 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.
26
- 4. Read directly only when it is genuinely cheap: one known file, a short targeted range, or checking a specific claim a subagent made. Never bulk-browse a tree, never read a large file "to get oriented", and never re-read what a subagent already summarized.
27
- 5. 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).
28
- 6. 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.
29
- 7. 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.
30
- 8. 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.
24
+ 2. When a task feels too heavy, decompose it — do not grind through it serially. Weight is a signal, not a reason to push harder: split the work into independent subtasks and dispatch them, in parallel where they are independent, rather than reading everything yourself in sequence. A task that exceeds a few reads, or that has clearly separable parts, should already have been split. Delegation depth is the environment's business, not yours: delegate until the runtime refuses, then read what remains yourself.
25
+ 3. Delegate research by default. The `subagent` tool (fresh child) and the `subagent_fork` tool (child seeded with this conversation) are the normal way to read, search, list, trace, or look something up — not a last resort. Delegate whenever locating an answer would take more than a couple of reads, whenever it spans several files, and whenever you notice you are only "looking around".
26
+ 4. Keep delegations independent and concurrent. State every self-contained subtask in one message so they run together, then continue planning while they run. Wait only when your next action needs a specific result.
27
+ 5. Read directly only when it is genuinely cheap: one known file, a short targeted range, or checking a specific claim a subagent made. Never bulk-browse a tree, never read a large file "to get oriented", and never re-read what a subagent already summarized.
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.
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.
31
32
 
32
33
  ## Subagent rules
33
34
 
package/verify-patch.mjs CHANGED
@@ -2,13 +2,15 @@
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 contains only names that exist in this profile.
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.
6
7
  //
7
8
  // Usage: node verify-patch.mjs
8
9
 
9
10
  import { readFileSync } from 'node:fs';
10
11
  import { dirname, join } from 'node:path';
11
12
  import { fileURLToPath } from 'node:url';
13
+ import { CHILD_TOOLS, MUTATION_TOOLS, SCOPE_LOCAL_TOOLS } from './child-tools.mjs';
12
14
  import { loadYaml, loaderSchema as makeLoaderSchema } from './lib-yaml.mjs';
13
15
 
14
16
  const yaml = loadYaml();
@@ -69,55 +71,80 @@ const childTools = [
69
71
  return { id, toolName, allow: row?.config?.toolFilter?.allow };
70
72
  });
71
73
 
72
- // The mask must deny every mutation entry point this profile registers.
73
- const mutations = ['write', 'edit', 'ops_replace', 'ops_bash', 'pwsh', 'bash', 'ops_run_background'];
74
+ // The mask must deny every mutation entry point, and it can only name global tools.
74
75
  for (const { id, allow } of childTools) {
75
76
  check(Array.isArray(allow) && allow.length > 0, `${id} declares a non-empty allow list`);
76
- for (const denied of mutations) {
77
+ for (const denied of MUTATION_TOOLS) {
77
78
  check(!allow?.includes(denied), `${id} mask excludes ${denied}`);
78
79
  }
79
80
  check(allow?.includes('read'), `${id} mask keeps read`);
80
- check(allow?.includes('subagent'), `${id} mask keeps subagent (read-only recursion stays possible)`);
81
+ for (const scoped of SCOPE_LOCAL_TOOLS) {
82
+ check(
83
+ !allow?.includes(scoped),
84
+ `${id} mask excludes the agent-scoped tool "${scoped}" (tools.restrict() would refuse it)`,
85
+ );
86
+ }
81
87
  }
82
88
 
83
- // Every masked name must be a tool this profile actually registers, because
84
- // tools.restrict() rejects unknown global names when a child is created.
85
- const liveTools = new Set([
86
- 'read',
89
+ /**
90
+ * The global tool inventory of the author's profile, captured from a runtime
91
+ * error rather than hand-kept: `tools.restrict()` prints every known global tool
92
+ * when it refuses a name. Replace this list with the equivalent line from your
93
+ * own environment — it is what makes the check below meaningful.
94
+ */
95
+ const OBSERVED_GLOBAL_TOOLS = new Set([
96
+ 'ask_user_question',
97
+ 'create_goal',
98
+ 'edit',
99
+ 'exit_plan_mode',
100
+ 'get_goal',
87
101
  'glob',
88
102
  'grep',
89
- 'ops_inspect_local_file',
103
+ 'interrupt_agent',
104
+ 'job_kill',
105
+ 'job_list',
106
+ 'job_output',
107
+ 'list_agents',
90
108
  'ops_glob',
91
109
  'ops_grep',
110
+ 'ops_inspect_local_file',
92
111
  'ops_replace',
93
- 'web_search',
94
- 'web_fetch',
112
+ 'present',
113
+ 'pwsh',
114
+ 'read',
95
115
  'read_image',
116
+ 'send_message',
96
117
  'skill',
118
+ 'subagent_fork',
97
119
  'todo_write',
98
- 'ask_user_question',
99
- 'get_goal',
100
- 'create_goal',
101
120
  'update_goal',
102
- 'list_agents',
103
- 'send_message',
104
- 'interrupt_agent',
105
- 'subagent',
106
- 'subagent_fork',
107
- 'present',
108
- 'write',
109
- 'edit',
110
- 'pwsh',
111
- 'ops_bash',
112
- 'job_output',
113
- 'job_list',
114
- 'job_kill',
121
+ 'web_fetch',
122
+ 'web_search',
115
123
  'workflow',
116
- 'exit_plan_mode',
124
+ 'write',
117
125
  ]);
126
+
127
+ /**
128
+ * Tools that are agent-scoped rather than global, so the mask can never name
129
+ * them. `subagent` is the one this bundle ran into: with
130
+ * `modelSelectionSettings: true` it is mounted inside each composed Agent's own
131
+ * scope, and `tools.restrict()` accepts global names only.
132
+ */
118
133
  for (const { id, allow } of childTools) {
119
- const unknown = (allow ?? []).filter((name) => !liveTools.has(name));
120
- check(unknown.length === 0, `${id} mask names only live tools${unknown.length ? ` (unknown: ${unknown.join(', ')})` : ''}`);
134
+ const unknown = (allow ?? []).filter((name) => !OBSERVED_GLOBAL_TOOLS.has(name));
135
+ check(
136
+ unknown.length === 0,
137
+ `${id} mask names only tools the runtime reports as global` +
138
+ (unknown.length ? ` (not global: ${unknown.join(', ')})` : ''),
139
+ );
140
+ const missing = CHILD_TOOLS.filter((name) => !(allow ?? []).includes(name));
141
+ const extra = (allow ?? []).filter((name) => !CHILD_TOOLS.includes(name));
142
+ check(
143
+ missing.length === 0 && extra.length === 0,
144
+ `${id} mask matches child-tools.mjs (${CHILD_TOOLS.length} entries)` +
145
+ (missing.length ? ` — missing: ${missing.join(', ')}` : '') +
146
+ (extra.length ? ` — unexpected: ${extra.join(', ')}` : ''),
147
+ );
121
148
  }
122
149
 
123
150
  process.stdout.write(failures.length === 0 ? '\nALL CHECKS PASSED\n' : `\n${failures.length} CHECK(S) FAILED\n`);