dsh-root-planner-preset 1.1.6 → 1.1.8

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/README.md CHANGED
@@ -19,13 +19,42 @@ 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 the task it was given | Persona rules: named files only, no drive-by fixes, no racing siblings | Prompt-level |
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 agreed, not enforced, because of the shell.** Write tools could be
40
+ fenced structurally — a scoped plugin can shadow `write`/`edit`/`ops_replace` with
41
+ path-checked versions, since scoped registrations shadow globals — but a shell writes
42
+ whatever the sandbox allows, and a child cannot be given its own cwd: `AgentOptions`
43
+ carries no `cwd`, the spawn provider's config has only `providerName`, and the one
44
+ `cwd` override is static per plugin. So the read-only side rests on the persona, and
45
+ the root **checks** it:
46
+
47
+ ```sh
48
+ # before delegating
49
+ touch _agents/.mark-<label>
50
+ # after the child reports
51
+ find . -path ./_agents -prune -o -type f -newer _agents/.mark-<label> -print
52
+ ```
53
+
54
+ No output means the child touched nothing outside its own directory; any output is a
55
+ file to inspect. The command needs no git repository — which matters, because the
56
+ workspace this was developed against is not one.
57
+
29
58
  ### Two limits you will meet on a broad task
30
59
 
31
60
  Both are Host settings, both are structural, and both are documented because they read
package/README.zh.md CHANGED
@@ -19,13 +19,28 @@
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
+ **这条边界是约定,不是围栏,原因是 shell。** 写工具本来可以做成结构性围栏——插件能在 agent 作用域里用**同名工具盖掉全局**的 `write`/`edit`/`ops_replace`,在里面检查路径——但 shell 能写沙箱允许的任何位置,而**子代理没法拥有自己的 cwd**:`AgentOptions` 里没有 `cwd`,spawn 提供方的 config 只有 `providerName`,唯一的 `cwd` 覆盖是**插件加载时定死的静态值**。所以"只读"这一侧靠人设约定,由根**核验**:
34
+
35
+ ```sh
36
+ # 派活前
37
+ touch _agents/.mark-<label>
38
+ # 子代理报告后
39
+ find . -path ./_agents -prune -o -type f -newer _agents/.mark-<label> -print
40
+ ```
41
+
42
+ 零输出 = 子代理没碰过自己目录之外的任何东西;有输出就逐个看。**这条命令不需要 git 仓库**——这一点很要紧,因为本预设开发时对着的那个工作区就不是 git 仓库。
43
+
29
44
  ### 大任务上你会撞到的两个上限
30
45
 
31
46
  两个都是 Host 设置、都是结构性的,也都列在这里——因为它们第一次出现时看着像 bug:
package/cordis.patch.yml CHANGED
@@ -52,19 +52,29 @@
52
52
  9. 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.
53
53
  10. **Leave nothing running.** No servers, watchers, daemons, or detached processes — for you as much as for a subagent. Background execution is your shell's decision to offer, not a convenience to reach for: if a task seems to need a long-lived process, do it in the foreground, bound it with a timeout, or ask the user before starting anything that outlives the work.
54
54
  11. Answer a subagent that reports a problem or an out-of-scope finding. Decide it, then reply with `send_message` so the child can continue: either the change is approved and who makes it, or it is out of scope and why, or what to do instead. An unanswered report leaves the child blocked on a decision only you can make. **A child that reports a clean result needs no reply** — silence is the correct response, so do not invent an acknowledgement.
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.
56
- 13. 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.
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
+ 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
+ 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.
57
62
 
58
63
  ## Subagent rules
59
64
 
60
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.
61
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.
62
- 3. Keep every change inside your task. Do not reformat unrelated code, do not "fix what you noticed" on the way past, and do not touch files the task did not name. If you find another problem, report it — do not fix it.
63
- 4. Do not race the other agents. Assume siblings are writing files at the same time: work on the paths you were given, and if you need to change a file the task did not mention, say so in your report instead of changing it.
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.
64
69
  5. Answer the question you were asked and do not re-plan the parent's task. Bound the work, then return.
65
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.
66
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.
67
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.
68
78
 
69
79
  ## When to pass work down
70
80
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-root-planner-preset",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "type": "module",
5
5
  "description": "DSH agent preset that outsources file reading to strictly read-only subagents, while the root agent keeps the plan and owns every file mutation.",
6
6
  "license": "MIT",
package/persona.txt CHANGED
@@ -31,19 +31,29 @@ Reading is not planning. Raw file contents and raw query results belong in the a
31
31
  9. 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.
32
32
  10. **Leave nothing running.** No servers, watchers, daemons, or detached processes — for you as much as for a subagent. Background execution is your shell's decision to offer, not a convenience to reach for: if a task seems to need a long-lived process, do it in the foreground, bound it with a timeout, or ask the user before starting anything that outlives the work.
33
33
  11. Answer a subagent that reports a problem or an out-of-scope finding. Decide it, then reply with `send_message` so the child can continue: either the change is approved and who makes it, or it is out of scope and why, or what to do instead. An unanswered report leaves the child blocked on a decision only you can make. **A child that reports a clean result needs no reply** — silence is the correct response, so do not invent an acknowledgement.
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.
35
- 13. 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.
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
+ 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
+ 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.
36
41
 
37
42
  ## Subagent rules
38
43
 
39
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.
40
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.
41
- 3. Keep every change inside your task. Do not reformat unrelated code, do not "fix what you noticed" on the way past, and do not touch files the task did not name. If you find another problem, report it — do not fix it.
42
- 4. Do not race the other agents. Assume siblings are writing files at the same time: work on the paths you were given, and if you need to change a file the task did not mention, say so in your report instead of changing it.
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.
43
48
  5. Answer the question you were asked and do not re-plan the parent's task. Bound the work, then return.
44
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.
45
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.
46
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.
47
57
 
48
58
  ## When to pass work down
49
59
 
package/release.mjs CHANGED
@@ -271,13 +271,33 @@ if (dryRun || skipInstall) {
271
271
  // ── CI ───────────────────────────────────────────────────────────────────────
272
272
  if (!dryRun && !skipGh) {
273
273
  step('Wait for CI on the pushed commit');
274
+ const repo = 'XiaoOu2024/dsh-root-planner-preset';
274
275
  const sha = git('rev-parse', 'HEAD');
275
- const waited = run(
276
- GH,
277
- ['run', 'watch', sha, '--repo', 'XiaoOu2024/dsh-root-planner-preset', '--exit-status'],
278
- { allowFailure: true },
279
- );
280
- process.stdout.write(` ${waited.trim().split('\n').slice(-2).join('\n ') || '(no output)'}\n`);
276
+ // `gh run watch <sha>` is wrong: the bare argument is a run ID, so a commit hash
277
+ // returns HTTP 404. Select the run by commit, then watch it by ID; if no run has
278
+ // registered yet, say so rather than reporting a failure that never happened.
279
+ let runId;
280
+ for (let attempt = 1; attempt <= 12 && runId === undefined; attempt += 1) {
281
+ const listed = run(
282
+ GH,
283
+ ['run', 'list', '--repo', repo, '--commit', sha, '--limit', '1', '--json', 'databaseId'],
284
+ { allowFailure: true },
285
+ );
286
+ try {
287
+ const [first] = JSON.parse(listed.trim() || '[]');
288
+ if (first?.databaseId !== undefined) runId = String(first.databaseId);
289
+ } catch {
290
+ /* the run has not registered yet */
291
+ }
292
+ if (runId === undefined) await new Promise((resolve) => setTimeout(resolve, 10_000));
293
+ }
294
+ if (runId === undefined) {
295
+ process.stdout.write(` no CI run registered for ${sha.slice(0, 8)} within 2 minutes; check it by hand\n`);
296
+ } else {
297
+ const waited = run(GH, ['run', 'watch', runId, '--repo', repo, '--exit-status'], { allowFailure: true });
298
+ const tail = waited.trim().split('\n').filter((line) => line.trim().length > 0).slice(-2);
299
+ process.stdout.write(` ${tail.join('\n ') || `run ${runId} finished`}\n`);
300
+ }
281
301
  }
282
302
 
283
303
  step(dryRun ? 'Dry run complete — nothing was changed' : `Released ${manifest.name}@${version}`);
@@ -0,0 +1,134 @@
1
+ // Measures cross-agent read overlap in a real run: how many files were read by more
2
+ // than one agent, and how much redundant reading that represents.
3
+ //
4
+ // The question this answers: sessions are isolated, so a parent cannot pass "what I
5
+ // already read" to a sibling. Is that redundancy a rounding error or the dominant
6
+ // cost of the fan-out? Only the logs can say.
7
+ //
8
+ // Usage: node tools/measure-read-overlap.mjs <session-dir> [...]
9
+
10
+ import { readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { createZstdDecompress } from 'node:zlib';
13
+
14
+ /** Locate the end of the first zstd frame, where the next one begins. */
15
+ function frameEnd(buf) {
16
+ for (let i = 4; i + 4 <= buf.length; i += 1) {
17
+ if (buf[i] === 0x28 && buf[i + 1] === 0xb5 && buf[i + 2] === 0x2f && buf[i + 3] === 0xfd) return i;
18
+ }
19
+ return buf.length;
20
+ }
21
+
22
+ /** Decompress a concatenated-zstd log frame by frame. */
23
+ async function decompressAll(buf) {
24
+ let rest = buf;
25
+ let text = '';
26
+ while (rest.length > 0) {
27
+ const slice = rest.subarray(0, frameEnd(rest));
28
+ try {
29
+ text += await new Promise((resolve, reject) => {
30
+ const chunks = [];
31
+ const dec = createZstdDecompress();
32
+ dec.on('data', (chunk) => chunks.push(chunk));
33
+ dec.on('error', reject);
34
+ dec.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
35
+ dec.end(slice);
36
+ });
37
+ } catch {
38
+ break;
39
+ }
40
+ if (slice.length >= rest.length) break;
41
+ rest = rest.subarray(slice.length);
42
+ }
43
+ return text;
44
+ }
45
+
46
+ const READ_TOOLS = new Set(['read', 'ops_inspect_local_file']);
47
+
48
+ /** Normalize a path argument so the same file spelled two ways counts once. */
49
+ function normalize(value) {
50
+ if (typeof value !== 'string') return undefined;
51
+ return value.replace(/\\/g, '/').toLowerCase().trim();
52
+ }
53
+
54
+ /**
55
+ * Read one tool call's arguments. The log stores them as a JSON *string*, not as an
56
+ * object — reading `arguments.path` off it silently yields nothing, which made the
57
+ * first version of this tool report zero overlap on a run full of reads.
58
+ * @param data - the `tool/call` event's data.
59
+ * @returns the parsed arguments, or an empty object.
60
+ */
61
+ function parseArguments(data) {
62
+ const raw = data?.arguments ?? data?.message?.arguments;
63
+ if (typeof raw === 'object' && raw !== null) return raw;
64
+ if (typeof raw !== 'string') return {};
65
+ try {
66
+ const parsed = JSON.parse(raw);
67
+ return typeof parsed === 'object' && parsed !== null ? parsed : {};
68
+ } catch {
69
+ return {};
70
+ }
71
+ }
72
+
73
+ /** Pull (agent, path) read pairs out of one session log. */
74
+ async function readsOf(dir) {
75
+ const text = await decompressAll(readFileSync(join(dir, 'session.v4.jsonl.zstd')));
76
+ const events = text
77
+ .split('\n')
78
+ .filter((line) => line.trim().length > 0)
79
+ .flatMap((line) => {
80
+ try {
81
+ return [JSON.parse(line)];
82
+ } catch {
83
+ return [];
84
+ }
85
+ });
86
+ const header = events[0];
87
+ const reads = [];
88
+ for (const event of events) {
89
+ if (event.type !== 'tool/call') continue;
90
+ const name = event.data?.name ?? event.data?.message?.name;
91
+ if (!READ_TOOLS.has(name)) continue;
92
+ const args = parseArguments(event.data);
93
+ const path = args.path ?? args.file_path;
94
+ const norm = normalize(path);
95
+ if (norm !== undefined) reads.push({ path: norm, depth: header?.delegationDepth });
96
+ }
97
+ return { id: dir.split(/[\\/]/).pop(), depth: header?.delegationDepth, reads };
98
+ }
99
+
100
+ const sessions = [];
101
+ for (const dir of process.argv.slice(2)) sessions.push(await readsOf(dir));
102
+
103
+ /** How many distinct agents touched each file. */
104
+ const byPath = new Map();
105
+ for (const session of sessions) {
106
+ for (const path of new Set(session.reads.map((r) => r.path))) {
107
+ const list = byPath.get(path) ?? [];
108
+ list.push(session.id.slice(0, 8));
109
+ byPath.set(path, list);
110
+ }
111
+ }
112
+
113
+ const totalReads = sessions.reduce((sum, s) => sum + s.reads.length, 0);
114
+ const shared = [...byPath.entries()].filter(([, agents]) => agents.length > 1);
115
+ const sharedReads = sessions.reduce(
116
+ (sum, s) => sum + s.reads.filter((r) => (byPath.get(r.path)?.length ?? 0) > 1).length,
117
+ 0,
118
+ );
119
+
120
+ process.stdout.write(`\n${sessions.length} session(s), ${totalReads} read calls, ${byPath.size} distinct paths\n`);
121
+ process.stdout.write(
122
+ `paths read by more than one agent: ${shared.length} (${((shared.length / Math.max(1, byPath.size)) * 100).toFixed(1)}% of paths)\n`,
123
+ );
124
+ process.stdout.write(
125
+ `read calls that hit a path another agent also read: ${sharedReads} (${((sharedReads / Math.max(1, totalReads)) * 100).toFixed(1)}% of reads)\n`,
126
+ );
127
+
128
+ const worst = shared.sort((a, b) => b[1].length - a[1].length).slice(0, 12);
129
+ if (worst.length > 0) {
130
+ process.stdout.write('\nmost shared paths:\n');
131
+ for (const [path, agents] of worst) {
132
+ process.stdout.write(` ${String(agents.length)}× ${path.slice(-96)}\n`);
133
+ }
134
+ }
package/verify-patch.mjs CHANGED
@@ -56,12 +56,62 @@ 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(/read-only/.test(personaRow?.config?.prefix ?? '') === false, 'persona no longer claims subagents are read-only (they are workers with write and shell)');
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
+
79
+ /**
80
+ * Rule counts, asserted on purpose.
81
+ *
82
+ * Byte-identity above only proves the patch matches `persona.txt` in the working
83
+ * tree. It says nothing about what was PUBLISHED: 1.1.6 shipped a package.json bump
84
+ * while the two collaboration rules added in the same session stayed in the working
85
+ * tree, so npm carried a 13-rule persona while the repository had 14. The byte check
86
+ * could not see that, because both files agreed with each other.
87
+ *
88
+ * Update these numbers in the same commit as the rule they count, so the release is
89
+ * the act that carries it.
90
+ */
91
+ const ROOT_RULE_COUNT = 18;
92
+ const SUBAGENT_RULE_COUNT = 13;
93
+ const rootSection = (personaRow?.config?.prefix ?? '').split('## Root agent rules')[1]?.split('## Subagent rules')[0] ?? '';
94
+ const rootRules = [...rootSection.matchAll(/^(\d+)\. /gm)].map((match) => Number(match[1]));
95
+ check(
96
+ rootRules.length === ROOT_RULE_COUNT,
97
+ `persona carries ${ROOT_RULE_COUNT} root rules (found ${rootRules.length})`,
98
+ );
99
+ check(
100
+ rootRules.every((value, index) => value === index + 1),
101
+ `root rule numbering is 1..${rootRules.length} with no gaps or repeats (found ${rootRules.join(',')})`,
102
+ );
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
+
65
115
  const delegation = plugins.find((row) => row.id === 'delegation');
66
116
  check(delegation?.group === true, 'delegation is a group row');
67
117
  const delegationRows = delegation?.config ?? [];