dsh-root-planner-preset 1.4.0 → 1.4.2

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/build-patch.mjs CHANGED
@@ -177,7 +177,12 @@ const patch = `# Generated by build-patch.mjs from persona.txt. Do not hand-edit
177
177
  config:
178
178
  provider: fork
179
179
  toolName: subagent_fork
180
- agentPreset: root-planner-worker
180
+ # No agentPreset, on purpose. The fork provider declares
181
+ # inheritsParentContext true and agentPreset false, and configuring a
182
+ # preset for it makes the row throw at activation — which is exactly how
183
+ # the first attempt at this migration made the whole preset fail to load.
184
+ # A forked child inherits its parent's preset: from a worker that is
185
+ # still the worker preset, from the root it is the root preset.
181
186
  - id: tool-subagent-codex
182
187
  name: '@deepseek-ai/dsh-tool-subagent'
183
188
  disabled: true
@@ -231,10 +236,17 @@ const patch = `# Generated by build-patch.mjs from persona.txt. Do not hand-edit
231
236
  # default persona instead of this preset's.
232
237
  #
233
238
  # The worker preset carries the same persona (it addresses both roles) and only the tool
234
- # plugins a worker needs. It also mounts worker-mask, which restricts the agent to
235
- # CHILD_TOOLS: several newer tools (tool-cordis, plugin-manager, working-directory,
236
- # schedule) are mounted at the app level rather than per preset, so leaving their rows out
237
- # would NOT remove them. The restriction is what makes the working set exactly CHILD_TOOLS.
239
+ # plugins a worker needs. What the worker can call is decided HERE, by which rows exist:
240
+ # in 0.2.1 a preset's rows register their tools per agent scope, and tools.restrict()
241
+ # filters GLOBAL tools only — "restrictions intersect; scoped registrations remain
242
+ # visible". A restriction therefore cannot narrow a preset's own tools, which is why an
243
+ # earlier attempt on this branch mounted a mask row and failed with
244
+ # tools.restrict() names unknown global tools "read", "glob", "write", ...
245
+ # known global tools: ops_glob, ops_grep, ops_inspect_local_file, ops_replace,
246
+ # working_directory
247
+ # That short list is the whole set of globally mounted tools on this machine; every other
248
+ # tool a worker sees comes from a row below. So the curated set is the row list itself,
249
+ # and omitting a row really does remove its tools.
238
250
  - insert:
239
251
  - id: preset-root-planner-worker
240
252
  name: '@deepseek-ai/dsh-agent-preset'
@@ -254,8 +266,6 @@ const patch = `# Generated by build-patch.mjs from persona.txt. Do not hand-edit
254
266
  name: '@deepseek-ai/dsh-agent-instructions'
255
267
  config:
256
268
  maxBytes: 65536
257
- - id: worker-mask
258
- name: 'dsh-root-planner-preset/worker-mask'
259
269
  - id: tool-bash
260
270
  name: '@deepseek-ai/dsh-tool-bash'
261
271
  disabled: !!js process.platform === 'win32'
@@ -319,7 +329,7 @@ const patch = `# Generated by build-patch.mjs from persona.txt. Do not hand-edit
319
329
  config:
320
330
  provider: fork
321
331
  toolName: subagent_fork
322
- agentPreset: root-planner-worker
332
+ # Inherits its parent's preset; see the note on the root preset's fork row.
323
333
  - id: tool-ask-user
324
334
  name: '@deepseek-ai/dsh-tool-ask-user'
325
335
  - id: tool-todo
package/child-tools.mjs CHANGED
@@ -1,9 +1,23 @@
1
1
  /**
2
- * The child tool mask — the single source of truth shared by the generator and
3
- * the verifier, so the two cannot disagree about what a subagent may call.
2
+ * The expected worker toolset — the list this bundle wants a subagent to end up with, kept
3
+ * in one place so the intent is stated once.
4
4
  *
5
- * These are the only GLOBAL tools a subagent may call. Everything absent is
6
- * invisible to the child.
5
+ * ## It is no longer a runtime mask, and cannot be one
6
+ *
7
+ * Until DSH 0.2.0 this list was handed to `dsh-tool-subagent` as `toolFilter.allow`.
8
+ * 0.2.1 removed that field: zod strips it silently and the child then runs the platform's
9
+ * default preset. The replacement the error message names is a preset (`agentPreset`), so
10
+ * what a worker can call is decided by which rows the `root-planner-worker` preset mounts.
11
+ *
12
+ * A `tools.restrict()` stand-in was tried and cannot work either: a preset's rows register
13
+ * their tools in the agent scope, and a restriction filters GLOBAL tools only
14
+ * ("restrictions intersect; scoped registrations remain visible"). On this machine the
15
+ * globally mounted set is just `ops_glob`, `ops_grep`, `ops_inspect_local_file`,
16
+ * `ops_replace` and `working_directory`; everything else a worker sees comes from a row.
17
+ *
18
+ * So this file is the documented expectation, verified against a live session rather than
19
+ * asserted against a snapshot: `verify-patch.mjs` proves the worker preset's row list, and
20
+ * `tools/extract-offered-tools.mjs` reads what a real child was actually offered.
7
21
  *
8
22
  * ## What a subagent MAY do
9
23
  *
@@ -12,7 +26,7 @@
12
26
  * project cannot run tests, git, or a verification script, so every executable
13
27
  * step bounces back to the root and the parallelism the tree exists for buys
14
28
  * nothing. `write`, `edit`, `ops_replace`, `ops_bash` and `pwsh` are all in the
15
- * mask for that reason.
29
+ * list for that reason.
16
30
  *
17
31
  * ## What is forbidden, and how it is enforced
18
32
  *
package/cordis.patch.yml CHANGED
@@ -194,7 +194,12 @@
194
194
  config:
195
195
  provider: fork
196
196
  toolName: subagent_fork
197
- agentPreset: root-planner-worker
197
+ # No agentPreset, on purpose. The fork provider declares
198
+ # inheritsParentContext true and agentPreset false, and configuring a
199
+ # preset for it makes the row throw at activation — which is exactly how
200
+ # the first attempt at this migration made the whole preset fail to load.
201
+ # A forked child inherits its parent's preset: from a worker that is
202
+ # still the worker preset, from the root it is the root preset.
198
203
  - id: tool-subagent-codex
199
204
  name: '@deepseek-ai/dsh-tool-subagent'
200
205
  disabled: true
@@ -248,10 +253,17 @@
248
253
  # default persona instead of this preset's.
249
254
  #
250
255
  # The worker preset carries the same persona (it addresses both roles) and only the tool
251
- # plugins a worker needs. It also mounts worker-mask, which restricts the agent to
252
- # CHILD_TOOLS: several newer tools (tool-cordis, plugin-manager, working-directory,
253
- # schedule) are mounted at the app level rather than per preset, so leaving their rows out
254
- # would NOT remove them. The restriction is what makes the working set exactly CHILD_TOOLS.
256
+ # plugins a worker needs. What the worker can call is decided HERE, by which rows exist:
257
+ # in 0.2.1 a preset's rows register their tools per agent scope, and tools.restrict()
258
+ # filters GLOBAL tools only — "restrictions intersect; scoped registrations remain
259
+ # visible". A restriction therefore cannot narrow a preset's own tools, which is why an
260
+ # earlier attempt on this branch mounted a mask row and failed with
261
+ # tools.restrict() names unknown global tools "read", "glob", "write", ...
262
+ # known global tools: ops_glob, ops_grep, ops_inspect_local_file, ops_replace,
263
+ # working_directory
264
+ # That short list is the whole set of globally mounted tools on this machine; every other
265
+ # tool a worker sees comes from a row below. So the curated set is the row list itself,
266
+ # and omitting a row really does remove its tools.
255
267
  - insert:
256
268
  - id: preset-root-planner-worker
257
269
  name: '@deepseek-ai/dsh-agent-preset'
@@ -342,8 +354,6 @@
342
354
  name: '@deepseek-ai/dsh-agent-instructions'
343
355
  config:
344
356
  maxBytes: 65536
345
- - id: worker-mask
346
- name: 'dsh-root-planner-preset/worker-mask'
347
357
  - id: tool-bash
348
358
  name: '@deepseek-ai/dsh-tool-bash'
349
359
  disabled: !!js process.platform === 'win32'
@@ -407,7 +417,7 @@
407
417
  config:
408
418
  provider: fork
409
419
  toolName: subagent_fork
410
- agentPreset: root-planner-worker
420
+ # Inherits its parent's preset; see the note on the root preset's fork row.
411
421
  - id: tool-ask-user
412
422
  name: '@deepseek-ai/dsh-tool-ask-user'
413
423
  - id: tool-todo
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-root-planner-preset",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "type": "module",
5
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",
@@ -8,12 +8,10 @@
8
8
  "exports": {
9
9
  ".": "./guard.js",
10
10
  "./guard": "./guard.js",
11
- "./worker-mask": "./worker-mask.js",
12
11
  "./package.json": "./package.json"
13
12
  },
14
13
  "files": [
15
14
  "guard.js",
16
- "worker-mask.js",
17
15
  "persona.txt",
18
16
  "child-tools.mjs",
19
17
  "build-patch.mjs",
@@ -74,6 +74,19 @@ check(depthPatch?.insert === undefined, 'override is not an insert patch');
74
74
  // whenever the profile re-composes — so its content is the evidence that the
75
75
  // edit was picked up rather than merely written.
76
76
  const composed = yaml.load(readFileSync(COMPOSED, 'utf8'), { schema: loaderSchema });
77
+ // `cordis.yml` is a runtime cache, not a source: its own header calls it "an empty entry
78
+ // list" and says the tree is composed from patches at startup. An empty file therefore
79
+ // means the harness has not rewritten the composition yet on this machine, not that the
80
+ // override is missing. Treating it as failure produced four red checks after a
81
+ // `--dump-config` reset the file — a red that said nothing about the preset.
82
+ if (!Array.isArray(composed) || composed.length === 0) {
83
+ process.stdout.write(
84
+ 'SKIP the composed-profile checks: cordis.yml still holds the empty template that the\n' +
85
+ 'harness rewrites at startup. Start dsh once, then re-run this script.\n',
86
+ );
87
+ process.stdout.write(failures.length === 0 ? '\nALL CHECKS PASSED\n' : `\n${failures.length} CHECK(S) FAILED\n`);
88
+ process.exit(failures.length === 0 ? 0 : 1);
89
+ }
77
90
  const subagentRows = composed.filter((row) => row.id === 'subagent');
78
91
  check(subagentRows.length === 1, `exactly one subagent row in the composition (found ${subagentRows.length})`);
79
92
  check(subagentRows[0]?.name === '@deepseek-ai/dsh-subagent', 'the composition row matches the asserted name');
package/verify-patch.mjs CHANGED
@@ -144,43 +144,99 @@ check(existsSync(join(here, 'guard.js')), 'guard.js ships inside the package');
144
144
  }
145
145
  const delegationRows = delegation?.config ?? [];
146
146
 
147
- // DSH 0.2.1 removed `toolFilter` from the delegation tool. It does not warn: zod strips
148
- // the key and the child then runs the platform's DEFAULT preset, which is how a live child
149
- // ended up with all 39 tools and the wrong persona. The curated child toolset therefore
150
- // moved to two places at once — a worker preset (which decides the mounted tool plugins)
151
- // and `worker-mask` (which subtracts the app-level tools a preset cannot omit).
147
+ // DSH 0.2.1 removed `toolFilter` from the delegation tool, and it does not warn: zod
148
+ // strips the key and the child then runs the platform's DEFAULT preset. The worker's tool
149
+ // set is now decided by which rows the worker preset mounts, because a preset's rows
150
+ // register their tools in the agent scope while `tools.restrict()` filters GLOBAL tools
151
+ // only ("restrictions intersect; scoped registrations remain visible"). A mask row was
152
+ // tried first and failed at activation with:
153
+ // tools.restrict() names unknown global tools "read", "glob", "write", ...
154
+ // known global tools: ops_glob, ops_grep, ops_inspect_local_file, ops_replace,
155
+ // working_directory
156
+ // Those five are the entire global set on this machine; everything else a worker sees
157
+ // comes from a row. So the assertions below are about rows, and they are two-sided:
158
+ // required rows must exist, forbidden rows must not.
152
159
  const workerRow = inserted.find((row) => row.id === 'preset-root-planner-worker');
153
160
  check(workerRow !== undefined, 'the patch declares a worker preset');
154
161
  check(workerRow?.config?.id === 'root-planner-worker', 'the worker preset id is root-planner-worker');
155
162
  check(workerRow?.config?.order === 6, 'the worker preset order is 6');
156
163
  const workerPlugins = workerRow?.config?.plugins ?? [];
164
+ const workerRowIds = new Set(workerPlugins.map((row) => row.id));
165
+
166
+ /** Rows a worker needs: persona, the working tools, and delegation so the tree recurses. */
167
+ const WORKER_REQUIRED_ROWS = [
168
+ 'persona',
169
+ 'agent-instructions',
170
+ 'tool-pwsh',
171
+ 'tool-fs',
172
+ 'tool-fs-search',
173
+ 'tool-skill',
174
+ 'tool-web',
175
+ 'tool-todo',
176
+ 'tool-ask-user',
177
+ 'delegation',
178
+ ];
179
+ for (const id of WORKER_REQUIRED_ROWS) {
180
+ check(workerRowIds.has(id), `the worker preset mounts ${id}`);
181
+ }
182
+
183
+ /**
184
+ * Rows a worker must not have. Each one was in the root preset and not here: workflow
185
+ * orchestration, deliverables, job control, plan mode, plugin management. Anything that
186
+ * lets a worker outlive its task or re-plan the tree belongs to the root.
187
+ */
188
+ const WORKER_FORBIDDEN_ROWS = ['tool-workflow', 'workflow-ptc', 'present', 'tool-jobs', 'tool-plugin-manager', 'planning'];
189
+ for (const id of WORKER_FORBIDDEN_ROWS) {
190
+ check(!workerRowIds.has(id), `the worker preset does not mount ${id}`);
191
+ }
157
192
  check(
158
- workerPlugins.some((row) => row.id === 'worker-mask' && row.name === 'dsh-root-planner-preset/worker-mask'),
159
- 'the worker preset mounts worker-mask (the tool restriction)',
193
+ !workerPlugins.some((row) => /worker-mask/.test(String(row?.id)) || /worker-mask/.test(String(row?.name))),
194
+ 'no mask row is mounted (a restriction cannot narrow a preset\'s own tools)',
160
195
  );
196
+
161
197
  const workerDelegation = workerPlugins.find((row) => row.id === 'delegation');
162
198
  const workerDelegationRows = workerDelegation?.config ?? [];
163
199
 
164
- // Loader row id -> the model-facing tool name that row publishes.
200
+ // Loader row id -> the model-facing tool name that row publishes, plus the provider whose
201
+ // capability decides whether a preset may be selected for it.
165
202
  const childTools = [
166
- { id: 'tool-subagent', toolName: 'subagent' },
167
- { id: 'tool-subagent-fork', toolName: 'subagent_fork' },
168
- ].map(({ id, toolName }) => {
203
+ { id: 'tool-subagent', toolName: 'subagent', provider: 'spawn' },
204
+ { id: 'tool-subagent-fork', toolName: 'subagent_fork', provider: 'fork' },
205
+ ].map(({ id, toolName, provider }) => {
169
206
  const row = delegationRows.find((entry) => entry.id === id);
170
207
  check(row !== undefined, `delegation declares ${id}`);
171
208
  check(row?.config?.toolName === toolName, `${id} publishes the tool name ${toolName}`);
172
- check(row?.config?.agentPreset === 'root-planner-worker', `${id} spawns into the worker preset`);
209
+ check(row?.config?.provider === provider, `${id} uses the ${provider} provider`);
173
210
  check(
174
211
  row?.config?.toolFilter === undefined,
175
212
  `${id} carries no toolFilter (DSH 0.2.1 removed it and strips it silently)`,
176
213
  );
177
- // A grandchild must land in the worker preset too, or the tree degrades at depth 2:
178
- // the platform default persona and the full tool list come back one level down.
179
- const nested = workerDelegationRows.find((entry) => entry.id === id);
180
- check(nested?.config?.agentPreset === 'root-planner-worker', `worker ${id} spawns into the worker preset too`);
181
- return { id, toolName };
214
+ // Only the spawn provider supports independent preset selection. A grandchild spawned
215
+ // through it must land in the worker preset too, or the tree degrades at depth 2: the
216
+ // platform default persona and the full tool list come back one level down.
217
+ if (provider === 'spawn') {
218
+ check(row?.config?.agentPreset === 'root-planner-worker', `${id} spawns into the worker preset`);
219
+ const nested = workerDelegationRows.find((entry) => entry.id === id);
220
+ check(nested?.config?.agentPreset === 'root-planner-worker', `worker ${id} spawns into the worker preset too`);
221
+ }
222
+ return { id, toolName, provider };
182
223
  });
183
224
 
225
+ // The fork provider declares `agentPreset: false` together with
226
+ // `inheritsParentContext: true`, and its row throws at activation when a preset is
227
+ // configured for it — which is what made the whole preset fail to load on DSH 0.2.1.
228
+ // Assert the absence in both presets so a later edit cannot reintroduce it.
229
+ for (const [label, rows] of [
230
+ ['the root preset', delegationRows],
231
+ ['the worker preset', workerDelegationRows],
232
+ ]) {
233
+ const fork = rows.find((entry) => entry.id === 'tool-subagent-fork');
234
+ check(
235
+ fork?.config?.agentPreset === undefined,
236
+ `${label} sets no agentPreset on the fork row (the fork provider rejects one)`,
237
+ );
238
+ }
239
+
184
240
  // A subagent is a worker: the curated set keeps the write and shell tools.
185
241
  for (const worker of ['read', 'write', 'edit', 'ops_replace', 'pwsh']) {
186
242
  check(CHILD_TOOLS.includes(worker), `the curated set keeps ${worker} (a subagent must be able to work)`);
@@ -207,83 +263,12 @@ for (const [label, rows] of [
207
263
  );
208
264
  }
209
265
 
210
- /**
211
- * The global tool inventory of the author's profile, captured from a runtime
212
- * error rather than hand-kept: `tools.restrict()` prints every known global tool
213
- * when it refuses a name. Replace this list with the equivalent line from your
214
- * own environment — it is what makes the check below meaningful.
215
- *
216
- * Provenance: transcribed on DSH 0.2.1-rc.2. The upgrade from 0.2.0-rc.2 added
217
- * `working_directory`, the four `schedule_*` tools, `cordis_inspect_list`,
218
- * `cordis_inspect_query` and `plugin_manager`; all are registered by profile-level tool
219
- * packages, so they are global. None of them is in the child mask, which is the intended
220
- * behaviour of an allow list — a new tool is invisible to children until it is named.
221
- *
222
- * Regenerate after a DSH upgrade: break the mask on purpose (add a bogus name), let the
223
- * agent-creation error print the list, and transcribe it — `tools/extract-tool-inventory.mjs`
224
- * reads exactly that line out of a session log.
225
- */
226
- const OBSERVED_GLOBAL_TOOLS = new Set([
227
- 'ask_user_question',
228
- 'cordis_inspect_list',
229
- 'cordis_inspect_query',
230
- 'create_goal',
231
- 'edit',
232
- 'exit_plan_mode',
233
- 'get_goal',
234
- 'glob',
235
- 'grep',
236
- 'interrupt_agent',
237
- 'job_kill',
238
- 'job_list',
239
- 'job_output',
240
- 'list_agents',
241
- 'ops_glob',
242
- 'ops_grep',
243
- 'ops_inspect_local_file',
244
- 'ops_replace',
245
- 'plugin_manager',
246
- 'present',
247
- 'pwsh',
248
- 'read',
249
- 'read_image',
250
- 'schedule_create',
251
- 'schedule_delete',
252
- 'schedule_list',
253
- 'schedule_update',
254
- 'send_message',
255
- 'skill',
256
- 'subagent_fork',
257
- 'todo_write',
258
- 'update_goal',
259
- 'web_fetch',
260
- 'web_search',
261
- 'workflow',
262
- 'working_directory',
263
- 'write',
264
- ]);
265
-
266
- /**
267
- * Tools that are agent-scoped rather than global, so `tools.restrict()` can never name
268
- * them. `subagent` is the one this bundle ran into: with `modelSelectionSettings: true`
269
- * it is mounted inside each composed Agent's own scope, and a restriction accepts global
270
- * names only. The curated set is the source of truth now — the old per-call mask is gone.
271
- */
272
- {
273
- const unknown = CHILD_TOOLS.filter((name) => !OBSERVED_GLOBAL_TOOLS.has(name));
274
- const scoped = new Set(SCOPE_LOCAL_TOOLS);
275
- const unexpected = unknown.filter((name) => !scoped.has(name));
276
- check(
277
- unexpected.length === 0,
278
- `the curated set names only tools the runtime reports as global` +
279
- (unexpected.length ? ` (not global: ${unexpected.join(', ')})` : ''),
280
- );
281
- // `subagent` reaches a worker through the preset's own delegation row rather than the
282
- // restriction, so it may legitimately sit in the curated set while being scope-local.
283
- for (const name of unknown) {
284
- check(scoped.has(name), `the only scope-local name in the curated set is declared as such (${name})`);
285
- }
286
- }
266
+ // The curated child toolset is not checkable against a static global inventory, and this
267
+ // file must not pretend otherwise. In DSH 0.2.1 a preset row registers its tool in the
268
+ // agent scope, and `tools.restrict()` filters GLOBAL tools only, so the names in
269
+ // `child-tools.mjs` are not global names. What proves the toolset is the row list —
270
+ // asserted above — plus one live run: spawn from a Root Planner session and read the
271
+ // child's `request/header` with `node tools/extract-offered-tools.mjs <session-dir>`.
287
272
 
288
273
  process.stdout.write(failures.length === 0 ? '\nALL CHECKS PASSED\n' : `\n${failures.length} CHECK(S) FAILED\n`);
289
274
  process.exit(failures.length === 0 ? 0 : 1);
package/worker-mask.js DELETED
@@ -1,30 +0,0 @@
1
- // Restricts a Root Planner worker to the curated child toolset.
2
- //
3
- // DSH 0.2.1 removed `toolFilter` from `dsh-tool-subagent` — its error says to "configure
4
- // persona and tool plugins in an Agent Preset and select it with agentPreset (spawn
5
- // only)". A preset decides which tool plugins are mounted for that agent, but not which
6
- // globally mounted tools it may call: several newer tools (tool-cordis, plugin-manager,
7
- // working-directory, schedule) are registered at the app level and would reach every
8
- // agent regardless of the preset's rows. `tools.restrict()` is the mechanism that still
9
- // subtracts them, and this module is that call, mounted as a row inside the worker
10
- // preset so it applies to the worker's own agent scope.
11
- //
12
- // The allow list lives in `child-tools.mjs` — the same file the previous mask used, so
13
- // the curated set is still declared once.
14
-
15
- import { CHILD_TOOLS } from './child-tools.mjs'
16
-
17
- export const name = 'root-planner-worker-mask'
18
-
19
- export const inject = ['tools']
20
-
21
- export function apply(ctx) {
22
- // `tools.restrict()` requires a scoped context: it refuses to run from a global one
23
- // because that would mask every agent in the process. A preset row is mounted into the
24
- // agent's own scope, which is exactly the scope this needs.
25
- ctx.effect(() => {
26
- const tools = typeof ctx.get === 'function' ? ctx.get('tools') : ctx.tools
27
- if (tools === undefined) return () => {}
28
- return tools.restrict({ allow: CHILD_TOOLS })
29
- })
30
- }