dsh-root-planner-preset 1.1.7 → 1.3.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/INSTALL.md +20 -0
- package/README.md +44 -1
- package/README.zh.md +26 -1
- package/build-patch.mjs +11 -0
- package/cordis.patch.yml +22 -2
- package/guard.js +190 -0
- package/package.json +26 -3
- package/persona.txt +11 -2
- package/release.mjs +17 -3
- package/verify-patch.mjs +51 -6
- package/.gitattributes +0 -10
- package/.github/CONTRIBUTING.md +0 -126
- package/.github/workflows/verify.yml +0 -51
package/INSTALL.md
CHANGED
|
@@ -44,6 +44,26 @@ dsh plugin --profile web add dsh-root-planner-preset
|
|
|
44
44
|
|
|
45
45
|
> 也可以钉版本:`dsh plugin --profile web add dsh-root-planner-preset@1.0.0`
|
|
46
46
|
|
|
47
|
+
### 已内置:子代理工作区围栏
|
|
48
|
+
|
|
49
|
+
预设自带一个守卫行,**不需要另装**。它把"子代理只写自己的 `_agents/` 目录"从人设规定变成**调用级拒绝**:
|
|
50
|
+
|
|
51
|
+
- 子代理 `write` / `edit` / `ops_replace` 的目标路径落在 `_agents/` 之外 → **调用被拒,代码根本没跑**;
|
|
52
|
+
- 子代理的 shell 调用 `workdir` 不在工作区内、或命令里写着指向你文件的绝对路径 → **被拒**;
|
|
53
|
+
- 根 agent **不受限制**(它必须能在自己工作区任意位置写,才能把产物并入)。
|
|
54
|
+
|
|
55
|
+
被告拒时它收到的是一句说明边界的错误,例如:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
write: outside your workspace. You may write only inside "...\_agents"; everything else
|
|
59
|
+
in the parent's directory is read-only to you. Send your parent a change request instead
|
|
60
|
+
of writing "...\report.md" directly.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**一个仍然存在的限制**:shell 命令能在运行时拼出路径(`$env:X` 拼 `'..'`、脚本内部写、Here-String),参数扫描看不见。要真正关住子代理的 shell 需要给它更窄的沙箱写域,而 DSH 给不了(`sandboxPolicy.resolve()` 只接受 `{session, mode}`,`workspaceRoot` 恒等于不可变的会话 cwd)。所以父 agent 的时间戳核验仍是兜底——人设里已经写了。
|
|
64
|
+
|
|
65
|
+
> 守卫以**预设行**形式挂载,所以只有用 `root-planner` 预设的 agent 才受约束:`_agents/` 这套约定只有这个预设的人设定义过。
|
|
66
|
+
|
|
47
67
|
### 方式 2:GitHub 源码
|
|
48
68
|
|
|
49
69
|
```sh
|
package/README.md
CHANGED
|
@@ -19,13 +19,56 @@ This preset takes the middle line: **workers with tools, bounded by their task.*
|
|
|
19
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
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
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
|
|
22
|
+
| A subagent stays inside its own workspace | Persona rules: write only under `_agents/<label>/`, the parent's files read-only to it, outside changes sent as a request | Prompt-level, **verified** by the parent (see below) |
|
|
23
23
|
| Delegation depth is the user's setting | The runtime depth cap (`subagent.maxDepth`) | **Structural**: past the cap it throws `SubagentDepthError` |
|
|
24
24
|
| Live children are capped as well | `subagent.maxActiveSubagents` | **Structural**: past the cap a delegation fails with `subagent limit reached`, and it does not queue |
|
|
25
25
|
| The root decides; workers execute | Persona rules | Prompt-level |
|
|
26
26
|
|
|
27
27
|
The mask is an **allow** list: a tool added by a future plugin is invisible to subagents until it is added here.
|
|
28
28
|
|
|
29
|
+
### One workspace per worker
|
|
30
|
+
|
|
31
|
+
Concurrency is handled by separation rather than by coordination. Before delegating,
|
|
32
|
+
the root creates `_agents/<label>/` and puts that absolute path in the task; the child
|
|
33
|
+
writes **only** there, reads the parent's files freely, and sends a change request
|
|
34
|
+
instead of editing anything outside. On completion the child reports its artifacts and
|
|
35
|
+
the root decides which to copy in — merging into an existing file where that is what the
|
|
36
|
+
change means, keeping them separate where it is not. `_agents/` stays (and belongs in
|
|
37
|
+
`.gitignore`) so the same agent can be continued later.
|
|
38
|
+
|
|
39
|
+
**The boundary is enforced, not just agreed.** This preset ships the fence as its own
|
|
40
|
+
plugin row (`dsh-root-planner-preset/guard`), so one install carries the protocol and its
|
|
41
|
+
enforcement together. It refuses a `write`, `edit` or `ops_replace` whose target resolves
|
|
42
|
+
outside `<workspace>/_agents/`, and refuses a shell call whose `workdir` is outside it or
|
|
43
|
+
whose command names an absolute path into your files:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
write: outside your workspace. You may write only inside "...\_agents"; everything else
|
|
47
|
+
in the parent's directory is read-only to you. Send your parent a change request instead
|
|
48
|
+
of writing "...\report.md" directly.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The root session is never fenced: it owns the workspace and has to be able to write
|
|
52
|
+
anywhere in it to integrate a child's artifacts.
|
|
53
|
+
|
|
54
|
+
One limitation survives, and it is worth knowing before you rely on this: a shell command
|
|
55
|
+
can build a path at runtime (`$env:X` + `'..'`, a script that writes, a Here-String), and
|
|
56
|
+
no argument scan can see it. Confining a child's shell properly would need a narrower
|
|
57
|
+
sandbox write root, which DSH cannot give a child — `sandboxPolicy.resolve()` accepts only
|
|
58
|
+
`{ session, mode }` and derives `workspaceRoot` from the immutable, inherited
|
|
59
|
+
`session.header.cwd`. So keep the parent's check as the backstop:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
# before delegating
|
|
63
|
+
touch _agents/.mark-<label>
|
|
64
|
+
# after the child reports
|
|
65
|
+
find . -path ./_agents -prune -o -type f -newer _agents/.mark-<label> -print
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
No output means the child touched nothing outside its own directory; any output is a
|
|
69
|
+
file to inspect. The command needs no git repository — which matters, because the
|
|
70
|
+
workspace this was developed against is not one.
|
|
71
|
+
|
|
29
72
|
### Two limits you will meet on a broad task
|
|
30
73
|
|
|
31
74
|
Both are Host settings, both are structural, and both are documented because they read
|
package/README.zh.md
CHANGED
|
@@ -19,13 +19,38 @@
|
|
|
19
19
|
| 子代理能读、**能改文件、能跑命令** | 两个委派工具的白名单**包含** `write`、`edit`、`ops_replace`、`pwsh` | 这是授予而非限制——没有工具的工人不算工人 |
|
|
20
20
|
| 子代理不能留下运行中的进程 | 预设的 `pwsh` 行设 `enableRunInBackground: false`:后台调用被拒,**且 `run_in_background` 参数被从工具 schema 里移除** | **结构性**——任何深度的模型都够不到"脱离进程"这个能力 |
|
|
21
21
|
| 谁都不跑无界服务 | 根与子代理的人设规则 | 提示词级,覆盖 schema 管不到的部分(`ops_bash` 没有任务注册表) |
|
|
22
|
-
|
|
|
22
|
+
| 子代理只在自己的工作区里写 | 人设规则:只写 `_agents/<label>/` 之下,父目录文件对它只读,越界改动以申请形式提交 | 提示词级,由父 agent **核验**(见下) |
|
|
23
23
|
| 委派层数由用户配置 | 运行时深度上限(`subagent.maxDepth`) | **结构性**:超过上限直接抛 `SubagentDepthError` |
|
|
24
24
|
| 在世子代理数也有上限 | `subagent.maxActiveSubagents` | **结构性**:超过上限时委派直接失败并报 `subagent limit reached`,**不会排队** |
|
|
25
25
|
| 根决策、工人执行 | 人设规则 | 提示词级 |
|
|
26
26
|
|
|
27
27
|
白名单是 **allow 而非 deny**:将来某个插件新增的工具,对子代理默认不可见。
|
|
28
28
|
|
|
29
|
+
### 一个工人一个工作区
|
|
30
|
+
|
|
31
|
+
并发问题是用**隔离**解决的,不是用协调。派活之前,根创建 `_agents/<label>/` 并把**绝对路径**写进任务;子代理**只往那里写**,父目录的文件随便读,要改就发申请。完成后它报告产物清单,由根决定复制哪些进自己的工作区——该合并进现有文件的就合并,该保持独立的就独立。`_agents/` 会保留(并应进 `.gitignore`),以便日后继续使用同一个子代理。
|
|
32
|
+
|
|
33
|
+
**这条边界是强制执行的,不只是约定。** 预设自带围栏(自己的插件行 `dsh-root-planner-preset/guard`),一次安装同时得到协议和它的执行。它拒绝目标落在 `<工作区>/_agents/` 之外的 `write`、`edit`、`ops_replace`;也拒绝 `workdir` 在工作区之外的 shell 调用,以及命令里写着你文件的绝对路径:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
write: outside your workspace. You may write only inside "...\_agents"; everything else
|
|
37
|
+
in the parent's directory is read-only to you. Send your parent a change request instead
|
|
38
|
+
of writing "...\report.md" directly.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
根会话**不受围栏限制**——它必须能在自己工作区任意位置写,才能把子代理的产物并入。
|
|
42
|
+
|
|
43
|
+
**有一个限制必须知道**:shell 命令能在运行时拼出路径(`$env:X` 拼 `'..'`、脚本内部写、Here-String),参数扫描看不见。要真正关住子代理的 shell,需要给它一个更窄的沙箱写域,而 DSH 给不了——`sandboxPolicy.resolve()` 只接受 `{ session, mode }`,`workspaceRoot` 恒等于不可变、从父会话继承的 `session.header.cwd`。所以父 agent 的核验仍然是兜底:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
# 派活前
|
|
47
|
+
touch _agents/.mark-<label>
|
|
48
|
+
# 子代理报告后
|
|
49
|
+
find . -path ./_agents -prune -o -type f -newer _agents/.mark-<label> -print
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
零输出 = 子代理没碰过自己目录之外的任何东西;有输出就逐个看。**这条命令不需要 git 仓库**——这一点很要紧,因为本预设开发时对着的那个工作区就不是 git 仓库。
|
|
53
|
+
|
|
29
54
|
### 大任务上你会撞到的两个上限
|
|
30
55
|
|
|
31
56
|
两个都是 Host 设置、都是结构性的,也都列在这里——因为它们第一次出现时看着像 bug:
|
package/build-patch.mjs
CHANGED
|
@@ -226,6 +226,17 @@ const patch = `# Generated by build-patch.mjs from persona.txt. Do not hand-edit
|
|
|
226
226
|
- id: tool-plugin-manager
|
|
227
227
|
name: '@deepseek-ai/dsh-plugin-manager/tools'
|
|
228
228
|
disabled: true
|
|
229
|
+
# The workspace fence. Mounted as a preset row so one install carries the
|
|
230
|
+
# protocol and its enforcement together: the persona tells a child to write
|
|
231
|
+
# under its own _agents/ directory, and this refuses anything else before
|
|
232
|
+
# the call runs. It is preset-scoped rather than a separate profile bundle
|
|
233
|
+
# because the _agents/ convention only exists here — a guard without this
|
|
234
|
+
# persona would fence agents that were never told where they may write.
|
|
235
|
+
- id: root-planner-guard
|
|
236
|
+
name: 'dsh-root-planner-preset/guard'
|
|
237
|
+
config:
|
|
238
|
+
directoryName: _agents
|
|
239
|
+
guardShell: true
|
|
229
240
|
`;
|
|
230
241
|
|
|
231
242
|
// Substitution happens after the template so the emitted indentation is owned by
|
package/cordis.patch.yml
CHANGED
|
@@ -55,17 +55,26 @@
|
|
|
55
55
|
12. 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. **One writer per path, always:** if two subtasks genuinely need to modify the same file, run them one after the other rather than together, and never let two agents own the same output directory.
|
|
56
56
|
13. Reuse a child before spawning a new one. A finished child still holds everything it read, and `send_message` starts another turn in that same context — so a follow-up about the same material belongs there, not in a fresh agent that must read it all again. Sessions are isolated and a sibling cannot be told what another one already read, so overlapping reads across agents are a real cost you pay twice: once in tokens and once in the chance that two agents report the same file differently. When several subtasks need the same document, give them to one agent, or read it yourself and pass the conclusion down.
|
|
57
57
|
14. 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.
|
|
58
|
+
15. **Name only tools this environment actually has.** Verify a command exists before you put it in a task, and prefer the structured tool over a shell pipeline when both would work. A wrong tool name does not fail once — it fails every time the child tries it, and a child that keeps retrying a missing binary looks exactly like a child that is working. Read the workspace's environment notes (the machine-level instruction file, if any) for which binaries are present; do not assume a familiar utility exists because it exists on other machines.
|
|
59
|
+
16. **Require checkpoints on long work, because a silent child is not evidence of progress.** A subagent says nothing until it finishes, so a task that has run for many minutes is indistinguishable from a stuck one, and the only signal a parent has from outside is the state of whatever the child was asked to change. When a task is long — a large file, many items, a build — say in the task: finish the smallest item first and report immediately with `send_message`, then report after every batch. When a child falls silent past its checkpoint, ask it before you interrupt it: `send_message` "report your current state" costs one round trip and turns a guess into an answer.
|
|
60
|
+
17. **Give every child its own workspace, and name it in the task.** Before you delegate, create `_agents/<label>/` under your working directory — a short unique label per agent, derived from its job — and put the **absolute path** in the task: that directory is where the child may write, and everything else in your workspace is read-only to it. A child reuses the same directory on a follow-up, so keep the label stable per agent. If the workspace is a git repository, make sure `_agents/` is in `.gitignore` before the first child writes there.
|
|
61
|
+
18. **Check the boundary, then integrate.** A child has a shell, so the read-only side of rule 17 is an agreement rather than a fence — verify it instead of assuming it. Before delegating, `touch _agents/.mark-<label>`; after the child reports, run `find . -path ./_agents -prune -o -type f -newer _agents/.mark-<label> -print`. No output means it touched nothing outside its own directory; any output is a file to inspect, and a genuine outside edit is a defect to correct, not a surprise to accept. Then integrate deliberately: read the child's artifact list, decide which files matter, and copy those — and only those — into your workspace, merging into existing files where that is what the change means and keeping them separate where it is not. Leave `_agents/<label>/` in place so the same agent can be continued later.
|
|
58
62
|
|
|
59
63
|
## Subagent rules
|
|
60
64
|
|
|
61
65
|
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.
|
|
62
66
|
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.
|
|
63
|
-
3.
|
|
64
|
-
4.
|
|
67
|
+
3. **Write only inside your own workspace.** Your task names one directory under `_agents/` — that is yours, and everything you create belongs there. Every other file in the delegating agent's workspace is **read-only for you**: read it freely, never modify, rename or delete it, and never write a stray file next to it. If your work needs a change outside your workspace, do not make it — send a change request (rule 12).
|
|
68
|
+
4. Know what the shared files are for. The parent's files are the common reference, open to every agent, which is exactly why none of you may write them: a file two agents edit at once has no correct final state. Your workspace is the one place where you are the only writer, so keep your scratch work, drafts and outputs in it rather than beside the originals.
|
|
65
69
|
5. Answer the question you were asked and do not re-plan the parent's task. Bound the work, then return.
|
|
66
70
|
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.
|
|
67
71
|
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.
|
|
68
72
|
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.
|
|
73
|
+
9. **A write that reports no change is a failure, not progress.** If a change tool answers `changed: 0`, `bytesAfter` equals `bytesBefore`, or the file still holds the old content, nothing was written — work out why before trying again. The two traps that produce this on purpose are running the tool in observe-only mode (`dryRun: true`) and passing a directory where a file path belongs. Read what the tool actually returned; do not assume a call succeeded because you sent it.
|
|
74
|
+
10. **Two identical failures are enough.** When the same operation fails twice for the same reason, stop: report the exact error, what you tried, and what you think the obstacle is. Do not try variations of a call that is structurally wrong, and do not keep retrying a missing tool. A silent retry loop burns the whole task while looking like work.
|
|
75
|
+
11. **Report progress before you finish when the work is long.** Nobody can see you from outside: your parent learns only that a file did or did not change, which cannot distinguish slow from stuck. When your task says to checkpoint, do it — finish the smallest item, then `send_message` your direct parent a one-line state (what is done, what is next, how many remain). Do the same unprompted if you judge the work will take many steps. Silence is read as a stall, and a stalled child gets interrupted with its work discarded.
|
|
76
|
+
12. **A change outside your workspace is a request, not an action.** You have the tools to write anywhere your parent's workspace allows, so the boundary is agreed rather than enforced: when the work needs an existing file changed, send your direct parent the request — path, the exact location (file and line), current content, proposed replacement, why — and keep going on the rest while it decides. Do the same for anything you would delete or rename. Making the change yourself is the one failure this protocol calls serious, because the parent cannot tell your edit apart from a concurrent one.
|
|
77
|
+
13. **Report artifacts, not activity.** Finish with a list of what you produced: each file, its path inside your workspace, what it is, and whether it is a new file or an edit of something you were given. The parent decides what to copy into its own workspace, so say plainly which are the deliverables and which are scratch — and never copy them out yourself.
|
|
69
78
|
|
|
70
79
|
## When to pass work down
|
|
71
80
|
|
|
@@ -280,3 +289,14 @@
|
|
|
280
289
|
- id: tool-plugin-manager
|
|
281
290
|
name: '@deepseek-ai/dsh-plugin-manager/tools'
|
|
282
291
|
disabled: true
|
|
292
|
+
# The workspace fence. Mounted as a preset row so one install carries the
|
|
293
|
+
# protocol and its enforcement together: the persona tells a child to write
|
|
294
|
+
# under its own _agents/ directory, and this refuses anything else before
|
|
295
|
+
# the call runs. It is preset-scoped rather than a separate profile bundle
|
|
296
|
+
# because the _agents/ convention only exists here — a guard without this
|
|
297
|
+
# persona would fence agents that were never told where they may write.
|
|
298
|
+
- id: root-planner-guard
|
|
299
|
+
name: 'dsh-root-planner-preset/guard'
|
|
300
|
+
config:
|
|
301
|
+
directoryName: _agents
|
|
302
|
+
guardShell: true
|
package/guard.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Keeps a delegated subagent inside the workspace its parent gave it.
|
|
2
|
+
//
|
|
3
|
+
// This module ships inside `dsh-root-planner-preset` and is mounted as one of the
|
|
4
|
+
// preset's own plugin rows (`dsh-root-planner-preset/guard`), so installing the preset
|
|
5
|
+
// brings the protocol and its enforcement together. That placement is deliberate: the
|
|
6
|
+
// `_agents/` convention is defined by this preset's persona, and a guard installed
|
|
7
|
+
// without it would fence agents that were never told where they may write.
|
|
8
|
+
//
|
|
9
|
+
// One guard, registered once on the plugin's own context, inspects every tool call
|
|
10
|
+
// and decides from the calling agent's session header. That is deliberately simpler
|
|
11
|
+
// than wiring per-agent listeners: `ToolExecution` already carries `agent`, so the
|
|
12
|
+
// guard needs neither the `agent/created` payload shape nor a per-agent context, and
|
|
13
|
+
// nothing has to be registered or torn down when a child appears or disappears.
|
|
14
|
+
//
|
|
15
|
+
// Why a guard rather than a prompt rule: `tools.guard()` is synchronous and monotonic.
|
|
16
|
+
// It refuses the call instead of asking the model not to make it, and no later guard
|
|
17
|
+
// can force-allow a call this one denied.
|
|
18
|
+
//
|
|
19
|
+
// Two strengths are in play and the difference is not cosmetic:
|
|
20
|
+
//
|
|
21
|
+
// * File tools carry the target path in their arguments, so a write, edit or
|
|
22
|
+
// replace outside `_agents/` is refused structurally — the call never runs.
|
|
23
|
+
// * Shell tools carry a command string, and a command can name a path no argument
|
|
24
|
+
// scan will see (`$env:HOME` + '..', a script that writes, a Here-String). For
|
|
25
|
+
// those the guard enforces the two things it can prove: the working directory
|
|
26
|
+
// must be inside the workspace, and an absolute path written into the command
|
|
27
|
+
// must not point at the parent's files. Anything assembled at runtime still gets
|
|
28
|
+
// past — the parent's timestamp check is the backstop for that, and this file
|
|
29
|
+
// says so rather than pretending the shell is confined.
|
|
30
|
+
|
|
31
|
+
import { appendFileSync } from 'node:fs'
|
|
32
|
+
import { isAbsolute, join, resolve, sep } from 'node:path'
|
|
33
|
+
|
|
34
|
+
/** Tools that take a target file path as an argument. */
|
|
35
|
+
const FILE_TOOLS = new Set(['write', 'edit', 'ops_replace'])
|
|
36
|
+
|
|
37
|
+
/** Tools that take a command string, where the guard is best-effort. */
|
|
38
|
+
const SHELL_TOOLS = new Set(['pwsh', 'ops_bash'])
|
|
39
|
+
|
|
40
|
+
/** Argument keys the file tools use for their target. */
|
|
41
|
+
const PATH_KEYS = ['path', 'file_path', 'filePath', 'target', 'targetPath']
|
|
42
|
+
|
|
43
|
+
/** Argument keys the shell tools use for the working directory. */
|
|
44
|
+
const WORKDIR_KEYS = ['workdir', 'cwd']
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Compare two absolute paths for containment, case-insensitively because this runs
|
|
48
|
+
* on Windows where `D:\X` and `d:\x` are the same directory.
|
|
49
|
+
* @param parent - the directory that must contain `child`.
|
|
50
|
+
* @param child - the candidate path.
|
|
51
|
+
* @returns whether `child` is `parent` or below it.
|
|
52
|
+
*/
|
|
53
|
+
function contains(parent, child) {
|
|
54
|
+
const p = parent.replace(/[\\/]+$/, '').toLowerCase()
|
|
55
|
+
const c = child.replace(/[\\/]+$/, '').toLowerCase()
|
|
56
|
+
if (c === p) return true
|
|
57
|
+
return c.startsWith(`${p}${sep}`) || c.startsWith(`${p}/`) || c.startsWith(`${p}\\`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Pull the first string argument under any of the given keys.
|
|
62
|
+
* @param args - the parsed call arguments.
|
|
63
|
+
* @param keys - candidate keys in priority order.
|
|
64
|
+
* @returns the value, or `undefined` when none is a string.
|
|
65
|
+
*/
|
|
66
|
+
function firstString(args, keys) {
|
|
67
|
+
if (typeof args !== 'object' || args === null) return undefined
|
|
68
|
+
for (const key of keys) {
|
|
69
|
+
const value = args[key]
|
|
70
|
+
if (typeof value === 'string' && value.length > 0) return value
|
|
71
|
+
}
|
|
72
|
+
return undefined
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve one guard for a single agent.
|
|
77
|
+
* @param cwd - the session working directory, used to resolve relative paths.
|
|
78
|
+
* @param workspace - the absolute directory the agent may write in.
|
|
79
|
+
* @returns a synchronous guard.
|
|
80
|
+
*/
|
|
81
|
+
function makeGuard(cwd, workspace) {
|
|
82
|
+
const describe = (target) =>
|
|
83
|
+
`outside your workspace. You may write only inside ${JSON.stringify(workspace)}; ` +
|
|
84
|
+
`everything else in the parent's directory is read-only to you. ` +
|
|
85
|
+
`Send your parent a change request instead of writing ${JSON.stringify(target)} directly.`
|
|
86
|
+
|
|
87
|
+
return (exec) => {
|
|
88
|
+
const name = exec?.name
|
|
89
|
+
const args = exec?.arguments
|
|
90
|
+
if (typeof name !== 'string') return undefined
|
|
91
|
+
|
|
92
|
+
if (FILE_TOOLS.has(name)) {
|
|
93
|
+
const raw = firstString(args, PATH_KEYS)
|
|
94
|
+
// No path argument at all is the tool's own validation problem, not a boundary
|
|
95
|
+
// violation — refusing here would only hide the real error message.
|
|
96
|
+
if (raw === undefined) return undefined
|
|
97
|
+
const abs = isAbsolute(raw) ? resolve(raw) : resolve(cwd, raw)
|
|
98
|
+
if (!contains(workspace, abs)) return `${name}: ${describe(abs)}`
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (SHELL_TOOLS.has(name)) {
|
|
103
|
+
// A shell with no explicit workdir runs in the session cwd, which is the
|
|
104
|
+
// parent's directory — precisely the place the child may not write. Requiring
|
|
105
|
+
// the workspace as workdir makes every relative path in the command land where
|
|
106
|
+
// it belongs.
|
|
107
|
+
const workdir = firstString(args, WORKDIR_KEYS)
|
|
108
|
+
const effective = workdir === undefined ? cwd : isAbsolute(workdir) ? resolve(workdir) : resolve(cwd, workdir)
|
|
109
|
+
if (!contains(workspace, effective)) {
|
|
110
|
+
return (
|
|
111
|
+
`${name}: the working directory ${JSON.stringify(effective)} is outside your workspace. ` +
|
|
112
|
+
`Pass workdir: ${JSON.stringify(workspace)} (or a directory inside it) and use relative paths ` +
|
|
113
|
+
`so everything you create stays in your workspace.`
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
// Best effort: refuse an absolute path in the command that points at the
|
|
117
|
+
// parent's files. A path assembled at runtime defeats this; the parent's
|
|
118
|
+
// timestamp check is the backstop.
|
|
119
|
+
const command = typeof args?.command === 'string' ? args.command : ''
|
|
120
|
+
const escaped = [...command.matchAll(/[A-Za-z]:[\\/][^\s"'`;|&)]*/g)].map((match) => match[0])
|
|
121
|
+
const outside = escaped.find((candidate) => {
|
|
122
|
+
const abs = resolve(candidate)
|
|
123
|
+
return !contains(workspace, abs) && contains(cwd, abs)
|
|
124
|
+
})
|
|
125
|
+
if (outside !== undefined) return `${name}: the command writes ${JSON.stringify(outside)}, ${describe(outside)}`
|
|
126
|
+
return undefined
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return undefined
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const name = 'root-planner-guard'
|
|
134
|
+
|
|
135
|
+
export const inject = ['tools']
|
|
136
|
+
|
|
137
|
+
// No `Config` export on purpose: Cordis validates a declared Config with a zod schema
|
|
138
|
+
// (`schema.validate`), and a plain JSON Schema object throws at activation with
|
|
139
|
+
// "Cannot read properties of undefined (reading 'validate')". Shipping no schema and
|
|
140
|
+
// coercing the optional fields in `apply` keeps the row usable without pulling a
|
|
141
|
+
// dependency into a plugin that is installed by directory link.
|
|
142
|
+
const DEFAULTS = { enabled: true, directoryName: '_agents', guardShell: true }
|
|
143
|
+
|
|
144
|
+
/** Exported for the unit test: the boundary logic is worth testing without a session. */
|
|
145
|
+
export { contains, makeGuard }
|
|
146
|
+
|
|
147
|
+
export function apply(ctx, config = {}) {
|
|
148
|
+
const settings = { ...DEFAULTS, ...(typeof config === 'object' && config !== null ? config : {}) }
|
|
149
|
+
if (settings.enabled === false) return
|
|
150
|
+
const directoryName =
|
|
151
|
+
typeof settings.directoryName === 'string' && settings.directoryName.length > 0
|
|
152
|
+
? settings.directoryName
|
|
153
|
+
: DEFAULTS.directoryName
|
|
154
|
+
const guardShell = settings.guardShell !== false
|
|
155
|
+
|
|
156
|
+
const trace = (line) => {
|
|
157
|
+
if (settings.debug !== true) return
|
|
158
|
+
try {
|
|
159
|
+
appendFileSync(
|
|
160
|
+
settings.debugFile ?? `${process.cwd()}\\guard-debug.log`,
|
|
161
|
+
`${new Date().toISOString()} ${line}\n`,
|
|
162
|
+
)
|
|
163
|
+
} catch {
|
|
164
|
+
/* a probe must never break the plugin */
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
trace(`apply: listening on the plugin context (directoryName=${directoryName})`)
|
|
169
|
+
|
|
170
|
+
// Registered on the plugin's own context: a plain-context guard applies globally,
|
|
171
|
+
// and the decision is made per call from `exec.agent`, so no per-agent wiring is
|
|
172
|
+
// needed and no registration leaks when an agent is disposed.
|
|
173
|
+
ctx.effect(() => {
|
|
174
|
+
const dispose = ctx.tools.guard((exec) => {
|
|
175
|
+
const header = exec?.agent?.session?.header
|
|
176
|
+
// Only delegated sessions are fenced: the root owns the workspace and must be
|
|
177
|
+
// able to write anywhere in it, or it could not integrate a child's artifacts.
|
|
178
|
+
if (header?.origin !== 'subagent') return undefined
|
|
179
|
+
const cwd = header.cwd ?? process.cwd()
|
|
180
|
+
if (typeof cwd !== 'string' || cwd.length === 0) return undefined
|
|
181
|
+
const workspace = join(cwd, directoryName)
|
|
182
|
+
const guard = makeGuard(cwd, workspace)
|
|
183
|
+
const verdict = guardShell ? guard(exec) : SHELL_TOOLS.has(exec?.name) ? undefined : guard(exec)
|
|
184
|
+
trace(`call ${exec?.name} from ${String(header.id)} -> ${verdict === undefined ? 'allow' : 'DENY'}`)
|
|
185
|
+
return verdict
|
|
186
|
+
})
|
|
187
|
+
trace('guard registered')
|
|
188
|
+
return dispose
|
|
189
|
+
})
|
|
190
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-root-planner-preset",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "DSH agent preset
|
|
5
|
+
"description": "DSH agent preset: the root agent plans and decides, subagent workers read, write and run commands inside their own _agents/ workspace, and nothing outlives its task.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "XiaoOu2024",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./guard.js",
|
|
10
|
+
"./guard": "./guard.js",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"guard.js",
|
|
15
|
+
"persona.txt",
|
|
16
|
+
"child-tools.mjs",
|
|
17
|
+
"build-patch.mjs",
|
|
18
|
+
"verify-patch.mjs",
|
|
19
|
+
"verify-depth-override.mjs",
|
|
20
|
+
"lib-yaml.mjs",
|
|
21
|
+
"release.mjs",
|
|
22
|
+
"cordis.patch.yml",
|
|
23
|
+
"tools/",
|
|
24
|
+
"docs/",
|
|
25
|
+
"README.md",
|
|
26
|
+
"README.zh.md",
|
|
27
|
+
"INSTALL.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
8
30
|
"homepage": "https://github.com/XiaoOu2024/dsh-root-planner-preset#readme",
|
|
9
31
|
"repository": {
|
|
10
32
|
"type": "git",
|
|
@@ -20,7 +42,8 @@
|
|
|
20
42
|
"cordis",
|
|
21
43
|
"subagent",
|
|
22
44
|
"delegation",
|
|
23
|
-
"
|
|
45
|
+
"workspace-isolation",
|
|
46
|
+
"guard"
|
|
24
47
|
],
|
|
25
48
|
"dsh": {
|
|
26
49
|
"bundle": {
|
package/persona.txt
CHANGED
|
@@ -34,17 +34,26 @@ Reading is not planning. Raw file contents and raw query results belong in the a
|
|
|
34
34
|
12. 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. **One writer per path, always:** if two subtasks genuinely need to modify the same file, run them one after the other rather than together, and never let two agents own the same output directory.
|
|
35
35
|
13. Reuse a child before spawning a new one. A finished child still holds everything it read, and `send_message` starts another turn in that same context — so a follow-up about the same material belongs there, not in a fresh agent that must read it all again. Sessions are isolated and a sibling cannot be told what another one already read, so overlapping reads across agents are a real cost you pay twice: once in tokens and once in the chance that two agents report the same file differently. When several subtasks need the same document, give them to one agent, or read it yourself and pass the conclusion down.
|
|
36
36
|
14. 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.
|
|
37
|
+
15. **Name only tools this environment actually has.** Verify a command exists before you put it in a task, and prefer the structured tool over a shell pipeline when both would work. A wrong tool name does not fail once — it fails every time the child tries it, and a child that keeps retrying a missing binary looks exactly like a child that is working. Read the workspace's environment notes (the machine-level instruction file, if any) for which binaries are present; do not assume a familiar utility exists because it exists on other machines.
|
|
38
|
+
16. **Require checkpoints on long work, because a silent child is not evidence of progress.** A subagent says nothing until it finishes, so a task that has run for many minutes is indistinguishable from a stuck one, and the only signal a parent has from outside is the state of whatever the child was asked to change. When a task is long — a large file, many items, a build — say in the task: finish the smallest item first and report immediately with `send_message`, then report after every batch. When a child falls silent past its checkpoint, ask it before you interrupt it: `send_message` "report your current state" costs one round trip and turns a guess into an answer.
|
|
39
|
+
17. **Give every child its own workspace, and name it in the task.** Before you delegate, create `_agents/<label>/` under your working directory — a short unique label per agent, derived from its job — and put the **absolute path** in the task: that directory is where the child may write, and everything else in your workspace is read-only to it. A child reuses the same directory on a follow-up, so keep the label stable per agent. If the workspace is a git repository, make sure `_agents/` is in `.gitignore` before the first child writes there.
|
|
40
|
+
18. **Check the boundary, then integrate.** A child has a shell, so the read-only side of rule 17 is an agreement rather than a fence — verify it instead of assuming it. Before delegating, `touch _agents/.mark-<label>`; after the child reports, run `find . -path ./_agents -prune -o -type f -newer _agents/.mark-<label> -print`. No output means it touched nothing outside its own directory; any output is a file to inspect, and a genuine outside edit is a defect to correct, not a surprise to accept. Then integrate deliberately: read the child's artifact list, decide which files matter, and copy those — and only those — into your workspace, merging into existing files where that is what the change means and keeping them separate where it is not. Leave `_agents/<label>/` in place so the same agent can be continued later.
|
|
37
41
|
|
|
38
42
|
## Subagent rules
|
|
39
43
|
|
|
40
44
|
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.
|
|
41
45
|
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.
|
|
42
|
-
3.
|
|
43
|
-
4.
|
|
46
|
+
3. **Write only inside your own workspace.** Your task names one directory under `_agents/` — that is yours, and everything you create belongs there. Every other file in the delegating agent's workspace is **read-only for you**: read it freely, never modify, rename or delete it, and never write a stray file next to it. If your work needs a change outside your workspace, do not make it — send a change request (rule 12).
|
|
47
|
+
4. Know what the shared files are for. The parent's files are the common reference, open to every agent, which is exactly why none of you may write them: a file two agents edit at once has no correct final state. Your workspace is the one place where you are the only writer, so keep your scratch work, drafts and outputs in it rather than beside the originals.
|
|
44
48
|
5. Answer the question you were asked and do not re-plan the parent's task. Bound the work, then return.
|
|
45
49
|
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.
|
|
46
50
|
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.
|
|
47
51
|
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.
|
|
52
|
+
9. **A write that reports no change is a failure, not progress.** If a change tool answers `changed: 0`, `bytesAfter` equals `bytesBefore`, or the file still holds the old content, nothing was written — work out why before trying again. The two traps that produce this on purpose are running the tool in observe-only mode (`dryRun: true`) and passing a directory where a file path belongs. Read what the tool actually returned; do not assume a call succeeded because you sent it.
|
|
53
|
+
10. **Two identical failures are enough.** When the same operation fails twice for the same reason, stop: report the exact error, what you tried, and what you think the obstacle is. Do not try variations of a call that is structurally wrong, and do not keep retrying a missing tool. A silent retry loop burns the whole task while looking like work.
|
|
54
|
+
11. **Report progress before you finish when the work is long.** Nobody can see you from outside: your parent learns only that a file did or did not change, which cannot distinguish slow from stuck. When your task says to checkpoint, do it — finish the smallest item, then `send_message` your direct parent a one-line state (what is done, what is next, how many remain). Do the same unprompted if you judge the work will take many steps. Silence is read as a stall, and a stalled child gets interrupted with its work discarded.
|
|
55
|
+
12. **A change outside your workspace is a request, not an action.** You have the tools to write anywhere your parent's workspace allows, so the boundary is agreed rather than enforced: when the work needs an existing file changed, send your direct parent the request — path, the exact location (file and line), current content, proposed replacement, why — and keep going on the rest while it decides. Do the same for anything you would delete or rename. Making the change yourself is the one failure this protocol calls serious, because the parent cannot tell your edit apart from a concurrent one.
|
|
56
|
+
13. **Report artifacts, not activity.** Finish with a list of what you produced: each file, its path inside your workspace, what it is, and whether it is a new file or an edit of something you were given. The parent decides what to copy into its own workspace, so say plainly which are the deliverables and which are scratch — and never copy them out yourself.
|
|
48
57
|
|
|
49
58
|
## When to pass work down
|
|
50
59
|
|
package/release.mjs
CHANGED
|
@@ -166,15 +166,29 @@ if (dryRun) {
|
|
|
166
166
|
// * The output must be shown even on failure. The first version filtered it
|
|
167
167
|
// down to matching lines and printed a blank line when the command never
|
|
168
168
|
// ran, hiding the real problem behind a successful-looking step.
|
|
169
|
-
const
|
|
170
|
-
|
|
169
|
+
const publishRun = spawnSync(PUBLISHER, ['publish', '--registry', OFFICIAL_REGISTRY, '--access', 'public'], {
|
|
170
|
+
cwd: here,
|
|
171
|
+
env,
|
|
172
|
+
encoding: 'utf8',
|
|
171
173
|
shell: true,
|
|
172
174
|
});
|
|
175
|
+
const published = `${publishRun.stdout ?? ''}${publishRun.stderr ?? ''}`;
|
|
173
176
|
const summary = published
|
|
174
177
|
.split('\n')
|
|
175
178
|
.map((line) => line.trim())
|
|
176
179
|
.filter((line) => line.length > 0 && !/^Progress:|^Packages:|^\+?$/.test(line));
|
|
177
|
-
process.stdout.write(`${summary.map((line) => ` ${line}`).join('\n') || ' (the publisher printed nothing
|
|
180
|
+
process.stdout.write(`${summary.map((line) => ` ${line}`).join('\n') || ' (the publisher printed nothing)'}\n`);
|
|
181
|
+
// Stop here on a failed publish. Continuing to the registry poll turned a network
|
|
182
|
+
// blip into ten minutes of waiting and then a confident but wrong diagnosis
|
|
183
|
+
// ("staged publishing?"), when the publish command had simply not run.
|
|
184
|
+
if (publishRun.status !== 0) {
|
|
185
|
+
const reason = publishRun.error ? `${publishRun.error.code}: ${publishRun.error.message}` : `exit ${publishRun.status}`;
|
|
186
|
+
throw new Error(
|
|
187
|
+
`the publisher failed (${reason}), so nothing was published and the registry check would only time out.\n` +
|
|
188
|
+
`Retry the publish by hand once the cause is clear:\n` +
|
|
189
|
+
` ${PUBLISHER} publish --registry ${OFFICIAL_REGISTRY} --access public`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
178
192
|
}
|
|
179
193
|
|
|
180
194
|
// ── Confirm on the registry ──────────────────────────────────────────────────
|
package/verify-patch.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Usage: node verify-patch.mjs
|
|
11
11
|
|
|
12
|
-
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
13
|
import { dirname, join } from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { CHILD_TOOLS, FORBIDDEN_CAPABILITIES, SCOPE_LOCAL_TOOLS } from './child-tools.mjs';
|
|
@@ -56,14 +56,28 @@ check(personaRow?.config?.suffix === 'Your working directory is {{cwd}}.', 'pers
|
|
|
56
56
|
const expected = readFileSync(join(here, 'persona.txt'), 'utf8').replace(/\r\n/g, '\n').trimEnd();
|
|
57
57
|
check(personaRow?.config?.prefix === expected, 'parsed persona is byte-identical to persona.txt');
|
|
58
58
|
check(!personaRow?.config?.prefix?.includes('\t'), 'persona contains no tab characters');
|
|
59
|
-
check(
|
|
59
|
+
check(
|
|
60
|
+
/You can read, change files, and run commands/.test(personaRow?.config?.prefix ?? ''),
|
|
61
|
+
'persona states that a subagent is a worker with write and shell',
|
|
62
|
+
);
|
|
63
|
+
// The exact wrong sentence this bundle once shipped. A loose /read-only/ test is no
|
|
64
|
+
// good now that the persona legitimately calls the PARENT's files read-only.
|
|
65
|
+
check(
|
|
66
|
+
!/You are read-only, and this is structural/.test(personaRow?.config?.prefix ?? ''),
|
|
67
|
+
'persona does not claim subagents themselves are read-only',
|
|
68
|
+
);
|
|
69
|
+
check(
|
|
70
|
+
/Write only inside your own workspace/.test(personaRow?.config?.prefix ?? ''),
|
|
71
|
+
'persona confines a subagent to its own workspace',
|
|
72
|
+
);
|
|
60
73
|
check(
|
|
61
74
|
/Never leave something running/.test(personaRow?.config?.prefix ?? ''),
|
|
62
75
|
'persona forbids long-running processes for subagents',
|
|
63
76
|
);
|
|
64
77
|
|
|
78
|
+
|
|
65
79
|
/**
|
|
66
|
-
* Rule
|
|
80
|
+
* Rule counts, asserted on purpose.
|
|
67
81
|
*
|
|
68
82
|
* Byte-identity above only proves the patch matches `persona.txt` in the working
|
|
69
83
|
* tree. It says nothing about what was PUBLISHED: 1.1.6 shipped a package.json bump
|
|
@@ -71,10 +85,11 @@ check(
|
|
|
71
85
|
* tree, so npm carried a 13-rule persona while the repository had 14. The byte check
|
|
72
86
|
* could not see that, because both files agreed with each other.
|
|
73
87
|
*
|
|
74
|
-
* Update
|
|
75
|
-
* act that carries it.
|
|
88
|
+
* Update these numbers in the same commit as the rule they count, so the release is
|
|
89
|
+
* the act that carries it.
|
|
76
90
|
*/
|
|
77
|
-
const ROOT_RULE_COUNT =
|
|
91
|
+
const ROOT_RULE_COUNT = 18;
|
|
92
|
+
const SUBAGENT_RULE_COUNT = 13;
|
|
78
93
|
const rootSection = (personaRow?.config?.prefix ?? '').split('## Root agent rules')[1]?.split('## Subagent rules')[0] ?? '';
|
|
79
94
|
const rootRules = [...rootSection.matchAll(/^(\d+)\. /gm)].map((match) => Number(match[1]));
|
|
80
95
|
check(
|
|
@@ -86,8 +101,38 @@ check(
|
|
|
86
101
|
`root rule numbering is 1..${rootRules.length} with no gaps or repeats (found ${rootRules.join(',')})`,
|
|
87
102
|
);
|
|
88
103
|
|
|
104
|
+
const subagentSection = (personaRow?.config?.prefix ?? '').split('## Subagent rules')[1]?.split('## When to pass work down')[0] ?? '';
|
|
105
|
+
const subagentRules = [...subagentSection.matchAll(/^(\d+)\. /gm)].map((match) => Number(match[1]));
|
|
106
|
+
check(
|
|
107
|
+
subagentRules.length === SUBAGENT_RULE_COUNT,
|
|
108
|
+
`persona carries ${SUBAGENT_RULE_COUNT} subagent rules (found ${subagentRules.length})`,
|
|
109
|
+
);
|
|
110
|
+
check(
|
|
111
|
+
subagentRules.every((value, index) => value === index + 1),
|
|
112
|
+
`subagent rule numbering is 1..${subagentRules.length} with no gaps or repeats (found ${subagentRules.join(',')})`,
|
|
113
|
+
);
|
|
114
|
+
|
|
89
115
|
const delegation = plugins.find((row) => row.id === 'delegation');
|
|
90
116
|
check(delegation?.group === true, 'delegation is a group row');
|
|
117
|
+
|
|
118
|
+
// The workspace fence travels with the preset, so one install carries the protocol and
|
|
119
|
+
// its enforcement. These assertions keep that promise honest: the row must exist, must
|
|
120
|
+
// resolve to this package's own module, and must ship that module.
|
|
121
|
+
const guardRow = plugins.find((row) => row.id === 'root-planner-guard');
|
|
122
|
+
check(guardRow !== undefined, 'the preset mounts the workspace guard as its own row');
|
|
123
|
+
check(
|
|
124
|
+
guardRow?.name === 'dsh-root-planner-preset/guard',
|
|
125
|
+
`the guard row resolves to this package's subpath (found ${JSON.stringify(guardRow?.name)})`,
|
|
126
|
+
);
|
|
127
|
+
check(guardRow?.disabled !== true, 'the guard row is enabled');
|
|
128
|
+
check(guardRow?.config?.directoryName === '_agents', 'the guard fences the _agents/ directory');
|
|
129
|
+
check(guardRow?.config?.guardShell === true, 'the guard also applies the shell rules');
|
|
130
|
+
check(existsSync(join(here, 'guard.js')), 'guard.js ships inside the package');
|
|
131
|
+
{
|
|
132
|
+
const manifest = JSON.parse(readFileSync(join(here, 'package.json'), 'utf8'));
|
|
133
|
+
check(manifest.exports?.['./guard'] === './guard.js', 'package.json exports ./guard');
|
|
134
|
+
check((manifest.files ?? []).includes('guard.js'), 'guard.js is in the published files list');
|
|
135
|
+
}
|
|
91
136
|
const delegationRows = delegation?.config ?? [];
|
|
92
137
|
|
|
93
138
|
// Loader row id -> the model-facing tool name that row publishes.
|
package/.gitattributes
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
# This bundle is verified by BYTE identity: verify-patch.mjs asserts that the
|
|
2
|
-
# persona parsed out of cordis.patch.yml equals persona.txt exactly. Git's
|
|
3
|
-
# autocrlf would rewrite both files on checkout and silently break that check on
|
|
4
|
-
# every Windows clone, so line endings are pinned to LF for the whole repository.
|
|
5
|
-
* text=auto eol=lf
|
|
6
|
-
|
|
7
|
-
# Binary if anyone ever adds artwork.
|
|
8
|
-
*.png binary
|
|
9
|
-
*.jpg binary
|
|
10
|
-
*.webp binary
|
package/.github/CONTRIBUTING.md
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
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 **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.
|
|
82
|
-
- Keep `main` linear — the branch protection refuses merge commits, so rebase
|
|
83
|
-
rather than merge.
|
|
84
|
-
|
|
85
|
-
## Releasing
|
|
86
|
-
|
|
87
|
-
`node release.mjs <patch|minor|major|X.Y.Z>` does the whole sequence. Every step in
|
|
88
|
-
it exists because the manual version went wrong at least once:
|
|
89
|
-
|
|
90
|
-
1. regenerates `cordis.patch.yml` and refuses to continue unless
|
|
91
|
-
`verify-patch.mjs` passes;
|
|
92
|
-
2. bumps `package.json` and commits with a local-time stamp;
|
|
93
|
-
3. pushes (the owner bypasses branch protection; a collaborator's push is refused,
|
|
94
|
-
so open a pull request instead);
|
|
95
|
-
4. publishes to `https://registry.npmjs.org/` **explicitly** — the configured
|
|
96
|
-
registry is a read-only mirror that also lags behind, which is how an install
|
|
97
|
-
right after a release fails with *No matching version found*;
|
|
98
|
-
5. **polls the registry for the exact version** rather than trusting the exit code,
|
|
99
|
-
because publishing on this account is staged and `npm publish` can print success
|
|
100
|
-
without the version ever appearing;
|
|
101
|
-
6. upgrades the local profile so the machine runs what was published.
|
|
102
|
-
|
|
103
|
-
`--dry-run` walks the whole thing without changing anything; `--skip-install` and
|
|
104
|
-
`--skip-gh` drop the last two steps.
|
|
105
|
-
|
|
106
|
-
Run it from a terminal. Inside DSH's own sandbox its `spawn` calls fail with
|
|
107
|
-
`EPERM` on Windows — that is the sandbox's piped-stdio boundary, not a defect in
|
|
108
|
-
the script.
|
|
109
|
-
|
|
110
|
-
**A release is not a verification.** The script can put a version on npm while the
|
|
111
|
-
preset stays unproven. Restart `dsh web`, open a new session on Root Planner, and
|
|
112
|
-
confirm the *root* actually composed with it — `node tools/verify-session.mjs
|
|
113
|
-
<session-dir>` — before believing anything about behaviour.
|
|
114
|
-
|
|
115
|
-
## How `main` is protected
|
|
116
|
-
|
|
117
|
-
`main` requires the `verify` status check (strict: the branch must be up to date),
|
|
118
|
-
refuses force pushes and deletions, requires linear history, and requires
|
|
119
|
-
conversations to be resolved. The repository owner can bypass these for a direct
|
|
120
|
-
push in an emergency; **collaborators cannot** — send a pull request and let CI
|
|
121
|
-
run.
|
|
122
|
-
|
|
123
|
-
If you are added as a collaborator and want to be able to merge your own pull
|
|
124
|
-
requests without waiting for the owner, say so: review requirements are off
|
|
125
|
-
deliberately, because a single-maintainer repository that requires an approval it
|
|
126
|
-
cannot get would deadlock.
|
|
@@ -1,51 +0,0 @@
|
|
|
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
|