dsh-root-planner-preset 1.4.1 → 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
@@ -236,10 +236,17 @@ const patch = `# Generated by build-patch.mjs from persona.txt. Do not hand-edit
236
236
  # default persona instead of this preset's.
237
237
  #
238
238
  # The worker preset carries the same persona (it addresses both roles) and only the tool
239
- # plugins a worker needs. It also mounts worker-mask, which restricts the agent to
240
- # CHILD_TOOLS: several newer tools (tool-cordis, plugin-manager, working-directory,
241
- # schedule) are mounted at the app level rather than per preset, so leaving their rows out
242
- # 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.
243
250
  - insert:
244
251
  - id: preset-root-planner-worker
245
252
  name: '@deepseek-ai/dsh-agent-preset'
@@ -259,8 +266,6 @@ const patch = `# Generated by build-patch.mjs from persona.txt. Do not hand-edit
259
266
  name: '@deepseek-ai/dsh-agent-instructions'
260
267
  config:
261
268
  maxBytes: 65536
262
- - id: worker-mask
263
- name: 'dsh-root-planner-preset/worker-mask'
264
269
  - id: tool-bash
265
270
  name: '@deepseek-ai/dsh-tool-bash'
266
271
  disabled: !!js process.platform === 'win32'
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
@@ -253,10 +253,17 @@
253
253
  # default persona instead of this preset's.
254
254
  #
255
255
  # The worker preset carries the same persona (it addresses both roles) and only the tool
256
- # plugins a worker needs. It also mounts worker-mask, which restricts the agent to
257
- # CHILD_TOOLS: several newer tools (tool-cordis, plugin-manager, working-directory,
258
- # schedule) are mounted at the app level rather than per preset, so leaving their rows out
259
- # 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.
260
267
  - insert:
261
268
  - id: preset-root-planner-worker
262
269
  name: '@deepseek-ai/dsh-agent-preset'
@@ -347,8 +354,6 @@
347
354
  name: '@deepseek-ai/dsh-agent-instructions'
348
355
  config:
349
356
  maxBytes: 65536
350
- - id: worker-mask
351
- name: 'dsh-root-planner-preset/worker-mask'
352
357
  - id: tool-bash
353
358
  name: '@deepseek-ai/dsh-tool-bash'
354
359
  disabled: !!js process.platform === 'win32'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-root-planner-preset",
3
- "version": "1.4.1",
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",
package/verify-patch.mjs CHANGED
@@ -144,20 +144,56 @@ 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
 
@@ -227,83 +263,12 @@ for (const [label, rows] of [
227
263
  );
228
264
  }
229
265
 
230
- /**
231
- * The global tool inventory of the author's profile, captured from a runtime
232
- * error rather than hand-kept: `tools.restrict()` prints every known global tool
233
- * when it refuses a name. Replace this list with the equivalent line from your
234
- * own environment — it is what makes the check below meaningful.
235
- *
236
- * Provenance: transcribed on DSH 0.2.1-rc.2. The upgrade from 0.2.0-rc.2 added
237
- * `working_directory`, the four `schedule_*` tools, `cordis_inspect_list`,
238
- * `cordis_inspect_query` and `plugin_manager`; all are registered by profile-level tool
239
- * packages, so they are global. None of them is in the child mask, which is the intended
240
- * behaviour of an allow list — a new tool is invisible to children until it is named.
241
- *
242
- * Regenerate after a DSH upgrade: break the mask on purpose (add a bogus name), let the
243
- * agent-creation error print the list, and transcribe it — `tools/extract-tool-inventory.mjs`
244
- * reads exactly that line out of a session log.
245
- */
246
- const OBSERVED_GLOBAL_TOOLS = new Set([
247
- 'ask_user_question',
248
- 'cordis_inspect_list',
249
- 'cordis_inspect_query',
250
- 'create_goal',
251
- 'edit',
252
- 'exit_plan_mode',
253
- 'get_goal',
254
- 'glob',
255
- 'grep',
256
- 'interrupt_agent',
257
- 'job_kill',
258
- 'job_list',
259
- 'job_output',
260
- 'list_agents',
261
- 'ops_glob',
262
- 'ops_grep',
263
- 'ops_inspect_local_file',
264
- 'ops_replace',
265
- 'plugin_manager',
266
- 'present',
267
- 'pwsh',
268
- 'read',
269
- 'read_image',
270
- 'schedule_create',
271
- 'schedule_delete',
272
- 'schedule_list',
273
- 'schedule_update',
274
- 'send_message',
275
- 'skill',
276
- 'subagent_fork',
277
- 'todo_write',
278
- 'update_goal',
279
- 'web_fetch',
280
- 'web_search',
281
- 'workflow',
282
- 'working_directory',
283
- 'write',
284
- ]);
285
-
286
- /**
287
- * Tools that are agent-scoped rather than global, so `tools.restrict()` can never name
288
- * them. `subagent` is the one this bundle ran into: with `modelSelectionSettings: true`
289
- * it is mounted inside each composed Agent's own scope, and a restriction accepts global
290
- * names only. The curated set is the source of truth now — the old per-call mask is gone.
291
- */
292
- {
293
- const unknown = CHILD_TOOLS.filter((name) => !OBSERVED_GLOBAL_TOOLS.has(name));
294
- const scoped = new Set(SCOPE_LOCAL_TOOLS);
295
- const unexpected = unknown.filter((name) => !scoped.has(name));
296
- check(
297
- unexpected.length === 0,
298
- `the curated set names only tools the runtime reports as global` +
299
- (unexpected.length ? ` (not global: ${unexpected.join(', ')})` : ''),
300
- );
301
- // `subagent` reaches a worker through the preset's own delegation row rather than the
302
- // restriction, so it may legitimately sit in the curated set while being scope-local.
303
- for (const name of unknown) {
304
- check(scoped.has(name), `the only scope-local name in the curated set is declared as such (${name})`);
305
- }
306
- }
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>`.
307
272
 
308
273
  process.stdout.write(failures.length === 0 ? '\nALL CHECKS PASSED\n' : `\n${failures.length} CHECK(S) FAILED\n`);
309
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
- }