dsh-pstack 0.2.0-beta.3 → 0.2.1

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.
Files changed (39) hide show
  1. package/README.md +35 -16
  2. package/README.zh.md +36 -17
  3. package/assets/pstack-config-entry.png +0 -0
  4. package/cordis.patch.yml +12 -0
  5. package/dist/client.mjs +2448 -0
  6. package/dist/client.mjs.map +1 -0
  7. package/dist/index.mjs +1 -1
  8. package/dist/{paths-BT77khwm.mjs → paths-yji9SNak.mjs} +0 -1
  9. package/dist/pstack/docs/harness/dsh.md +7 -7
  10. package/dist/remote.d.mts +27 -0
  11. package/dist/remote.mjs +235 -0
  12. package/dist/role-catalog.d.mts +35 -0
  13. package/dist/role-catalog.mjs +122 -0
  14. package/dist/role-config-a4JpVdc-.mjs +379 -0
  15. package/dist/role-manifest-79cnFfDM.mjs +112 -0
  16. package/dist/role-manifest-8OERX9-s.d.mts +90 -0
  17. package/dist/role-wire-CAJsVA6e.d.mts +54 -0
  18. package/dist/roles.d.mts +18 -0
  19. package/dist/roles.mjs +293 -0
  20. package/dist/script.mjs +1 -1
  21. package/dist/skills/authored/decision-audit/SKILL.md +21 -17
  22. package/dist/skills/authored/decision-audit/SKILL.zh.md +21 -17
  23. package/dist/skills/authored/decision-audit/references/recover-commit.md +3 -3
  24. package/dist/skills/authored/decision-audit/references/recover-commit.zh.md +3 -3
  25. package/dist/skills/authored/decision-write/SKILL.md +11 -9
  26. package/dist/skills/authored/decision-write/SKILL.zh.md +11 -9
  27. package/dist/skills/authored/decision-write/references/land.md +3 -3
  28. package/dist/skills/authored/decision-write/references/land.zh.md +3 -3
  29. package/dist/skills/authored/decision-write/references/scope.md +16 -0
  30. package/dist/skills/authored/decision-write/references/scope.zh.md +16 -0
  31. package/dist/skills/authored/docs-drift/SKILL.md +120 -0
  32. package/dist/skills/authored/docs-drift/SKILL.zh.md +120 -0
  33. package/dist/skills/authored/setup-project-docs/SKILL.md +14 -9
  34. package/dist/skills/authored/setup-project-docs/SKILL.zh.md +14 -9
  35. package/dist/skills/authored/setup-project-docs/references/decision-conventions.md +3 -1
  36. package/dist/skills/authored/setup-project-docs/references/decision-conventions.zh.md +3 -1
  37. package/dist/subagent.d.mts +27 -20
  38. package/dist/subagent.mjs +71 -448
  39. package/package.json +34 -3
package/dist/subagent.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { a as POTETO_MODE_SKILL_PATH } from "./paths-BT77khwm.mjs";
1
+ import { a as POTETO_MODE_SKILL_PATH } from "./paths-yji9SNak.mjs";
2
+ import { a as selectRoleRoutes, i as resolveDshHome, n as readRoleConfiguration, r as requireRole, t as modelsFilePath } from "./role-config-a4JpVdc-.mjs";
2
3
  import { isAbsolute, join, relative, resolve } from "node:path";
3
- import { existsSync, readFileSync, statSync } from "node:fs";
4
- import { homedir } from "node:os";
4
+ import { existsSync, statSync } from "node:fs";
5
5
  // src/internal/effort.ts
6
6
  /** Model-facing name of the child's own control. */
7
7
  const SET_EFFORT_TOOL = "set_reasoning_effort";
@@ -30,7 +30,7 @@ async function validateEffortFor(llm, route, effort, parent, signal) {
30
30
  try {
31
31
  efforts = await advertisedEfforts(llm, target, signal);
32
32
  } catch (error) {
33
- throw new Error(`route ${target.provider}/${target.model} could not be resolved for reasoningEffort: ${describeError$2(error)}`, { cause: error });
33
+ throw new Error(`route ${target.provider}/${target.model} could not be resolved for reasoningEffort: ${describeError$1(error)}`, { cause: error });
34
34
  }
35
35
  if (efforts.length === 0) throw new Error(`${target.provider}/${target.model} advertises no reasoning efforts, so it cannot take reasoningEffort "${effort}". Omit the argument and let the adapter use its own default.`);
36
36
  if (!efforts.includes(effort)) throw new Error(`${target.provider}/${target.model} does not support reasoning effort "${effort}". It supports: ${efforts.join(", ")}.`);
@@ -83,7 +83,7 @@ function defineSetEffortTool(child, llm) {
83
83
  try {
84
84
  efforts = await advertisedEfforts(llm, target, exec.signal);
85
85
  } catch (error) {
86
- throw new Error(`${target.provider}/${target.model} could not be resolved for reasoning effort: ${describeError$2(error)}`, { cause: error });
86
+ throw new Error(`${target.provider}/${target.model} could not be resolved for reasoning effort: ${describeError$1(error)}`, { cause: error });
87
87
  }
88
88
  if (efforts.length === 0) throw new Error(`${target.provider}/${target.model} advertises no reasoning efforts, so this child has none to change. Your current effort stays as it is.`);
89
89
  if (!efforts.includes(value)) throw new Error(`${target.provider}/${target.model} does not support reasoning effort "${value}", so nothing changed. It supports: ${efforts.join(", ")}.`);
@@ -110,417 +110,10 @@ function agentOptionsFor(route, effortOverride) {
110
110
  function effectiveEffort(route, effortOverride) {
111
111
  return effortOverride ?? (route.kind === "route" ? route.effort : void 0);
112
112
  }
113
- function describeError$2(error) {
114
- if (error instanceof Error) {
115
- const code = error.code;
116
- return typeof code === "string" ? `${error.message} (${code})` : error.message;
117
- }
118
- return String(error);
119
- }
120
- // src/internal/role-manifest.ts
121
- /**
122
- * File name the role configuration lives under inside `$DSH_HOME`.
123
- *
124
- * It keeps the upstream name `pstack-models.mdc`, so a reader moving from the Cursor docs
125
- * recognizes it, but stores it directly under `$DSH_HOME`; DSH does not load `.mdc` files
126
- * on its own, and `pstack_subagent` is the only reader.
127
- */
128
- const PSTACK_MODELS_FILENAME = "pstack-models.mdc";
129
- /** Values that resolve to "send no `agentOptions` and inherit the parent route". */
130
- const INHERIT_PARENT_ALIASES = ["inherit-parent", "auto"];
131
- /** The 17 roles, in manifest order. */
132
- const ROLE_MANIFEST = [
133
- {
134
- key: "feature, refactoring",
135
- shape: "single",
136
- autoPanel: false
137
- },
138
- {
139
- key: "bug-fix",
140
- shape: "single",
141
- autoPanel: false
142
- },
143
- {
144
- key: "perf-issue",
145
- shape: "single",
146
- autoPanel: false
147
- },
148
- {
149
- key: "hillclimb",
150
- shape: "single",
151
- autoPanel: false
152
- },
153
- {
154
- key: "judgment and prose",
155
- shape: "single",
156
- autoPanel: false
157
- },
158
- {
159
- key: "hardest tasks",
160
- shape: "single",
161
- autoPanel: false
162
- },
163
- {
164
- key: "how explorer",
165
- shape: "single",
166
- autoPanel: false
167
- },
168
- {
169
- key: "how explainer",
170
- shape: "single",
171
- autoPanel: false
172
- },
173
- {
174
- key: "why investigators",
175
- shape: "single",
176
- autoPanel: false
177
- },
178
- {
179
- key: "why synthesizer",
180
- shape: "single",
181
- autoPanel: false
182
- },
183
- {
184
- key: "reflect tooling",
185
- shape: "single",
186
- autoPanel: false
187
- },
188
- {
189
- key: "reflect judgment, divergent, synthesizer",
190
- shape: "single",
191
- autoPanel: false
192
- },
193
- {
194
- key: "arena runners",
195
- shape: "list",
196
- autoPanel: true
197
- },
198
- {
199
- key: "arena cross-judge pool",
200
- shape: "list",
201
- autoPanel: false
202
- },
203
- {
204
- key: "swarm workers",
205
- shape: "list",
206
- autoPanel: false
207
- },
208
- {
209
- key: "architect runners",
210
- shape: "list",
211
- autoPanel: true
212
- },
213
- {
214
- key: "interrogate reviewers",
215
- shape: "list",
216
- autoPanel: true
217
- }
218
- ];
219
- /** Exact manifest lookup. Callers normalize the key first. */
220
- function findRoleSpec(key) {
221
- return ROLE_MANIFEST.find((role) => role.key === key);
222
- }
223
- // src/internal/role-config.ts
224
- /**
225
- * The pstack role configuration: `$DSH_HOME/pstack-models.mdc`.
226
- *
227
- * `pstack_subagent` reads this file again on every delegation, so a change reaches the
228
- * next child and never a child that already started.
229
- *
230
- * Three states stay apart because each needs a different fix: the file is missing (no
231
- * policy, so every role inherits the parent route), unreadable (a broken install, never
232
- * treated as "no configuration"), or loaded with per-line issues reported by line.
233
- */
234
- /** Absolute path of the role file for one DSH home. */
235
- function modelsFilePath(dshHome) {
236
- return join(dshHome, PSTACK_MODELS_FILENAME);
237
- }
238
- /** Resolve `$DSH_HOME` the way the Host does: the environment when set, else `~/.dsh`. */
239
- function resolveDshHome(env = process.env, home = homedir()) {
240
- const configured = env["DSH_HOME"];
241
- const path = configured !== void 0 && configured.trim() !== "" ? configured : join(home, ".dsh");
242
- const expanded = path === "~" ? home : path.startsWith("~/") || path.startsWith("~\\") ? join(home, path.slice(2)) : path;
243
- return resolve(expanded);
244
- }
245
- /** Read the role file. Never throws for a missing or unreadable file. */
246
- function readRoleConfiguration(path) {
247
- let text;
248
- try {
249
- text = readFileSync(path, "utf8");
250
- } catch (error) {
251
- if (isMissingFile(error)) return {
252
- status: "missing",
253
- path
254
- };
255
- return {
256
- status: "unreadable",
257
- path,
258
- reason: describeError$1(error)
259
- };
260
- }
261
- return parseRoleConfiguration(text, path);
262
- }
263
- /** Parse role-file text. Pure: no file system access. */
264
- function parseRoleConfiguration(text, path) {
265
- const issues = [];
266
- const roles = /* @__PURE__ */ new Map();
267
- let budget;
268
- stripFrontmatter(text).split("\n").forEach((rawLine, index) => {
269
- const line = index + 1;
270
- const trimmed = rawLine.trim();
271
- if (trimmed === "") return;
272
- if (trimmed.startsWith("#")) {
273
- const match = /^#\s*budget:\s*(.+)$/i.exec(trimmed);
274
- if (match !== null) budget = match[1].trim();
275
- return;
276
- }
277
- const separator = trimmed.indexOf(":");
278
- if (separator === -1) {
279
- issues.push({
280
- line,
281
- message: `expected \`<role>: <value>\`, found ${JSON.stringify(trimmed)}`
282
- });
283
- return;
284
- }
285
- const rawKey = trimmed.slice(0, separator).trim();
286
- const rawValue = trimmed.slice(separator + 1).trim();
287
- const key = normalizeRoleName(rawKey);
288
- if (key === "") {
289
- issues.push({
290
- line,
291
- message: "the role key is empty"
292
- });
293
- return;
294
- }
295
- const spec = findRoleSpec(key);
296
- if (spec === void 0) {
297
- issues.push({
298
- line,
299
- message: unknownRoleMessage(rawKey, key)
300
- });
301
- return;
302
- }
303
- const parsed = parseRouteValue(rawValue);
304
- const entryIssues = [...parsed.issues];
305
- if (spec.shape === "single" && parsed.routes.length > 1) entryIssues.push("a single-route role cannot hold a comma-separated list");
306
- for (const issue of entryIssues) issues.push({
307
- line,
308
- message: `${rawKey}: ${issue}`
309
- });
310
- if (parsed.routes.length === 0) return;
311
- const previous = roles.get(spec.key);
312
- if (previous !== void 0) issues.push({
313
- line,
314
- message: `role "${spec.key}" is already configured on line ${previous.line}; line ${line} wins`
315
- });
316
- roles.set(spec.key, {
317
- line,
318
- rawKey,
319
- key: spec.key,
320
- spec,
321
- routes: parsed.routes,
322
- issues: entryIssues
323
- });
324
- });
325
- return budget === void 0 ? {
326
- status: "loaded",
327
- path,
328
- roles,
329
- issues
330
- } : {
331
- status: "loaded",
332
- path,
333
- budget,
334
- roles,
335
- issues
336
- };
337
- }
338
- /** Parse a role value: one route, or a comma-separated list of routes. */
339
- function parseRouteValue(raw) {
340
- const routes = [];
341
- const issues = [];
342
- const segments = raw.split(",");
343
- segments.forEach((segment, index) => {
344
- const trimmed = segment.trim();
345
- if (trimmed === "") {
346
- if (index !== segments.length - 1) issues.push(`entry ${index + 1} of the list is empty`);
347
- return;
348
- }
349
- const parsed = parseRoute(trimmed);
350
- if (parsed.route === void 0) {
351
- issues.push(parsed.issue);
352
- return;
353
- }
354
- routes.push(parsed.route);
355
- });
356
- if (routes.length === 0 && issues.length === 0) issues.push("the value is empty; use `inherit-parent`, `auto`, or `<provider>/<model> [effort]`");
357
- return {
358
- routes,
359
- issues
360
- };
361
- }
362
- /** Parse one entry of a role value. */
363
- function parseRoute(raw) {
364
- const value = raw.trim();
365
- if (value === "") return { issue: "the value is empty" };
366
- if (INHERIT_PARENT_ALIASES.includes(value.toLowerCase())) return { route: { kind: "inherit" } };
367
- const match = /^([^\s/,[]+)\/([^\s/,[\]]+(?:\/[^\s/,[\]]+)*)(?:\s+\[([^[\]]*)\])?$/.exec(value);
368
- if (match === null) return { issue: `${JSON.stringify(value)} is not a route; use \`inherit-parent\`, \`auto\`, or \`<provider>/<model> [effort]\`` };
369
- const provider = match[1];
370
- const model = match[2];
371
- const effort = match[3];
372
- if (effort === void 0) return { route: {
373
- kind: "route",
374
- provider,
375
- model
376
- } };
377
- const trimmedEffort = effort.trim();
378
- if (trimmedEffort === "") return { issue: `${JSON.stringify(value)} has empty brackets; write \`<provider>/<model> [effort]\` or drop the brackets` };
379
- if (/\s/.test(trimmedEffort)) return { issue: `${JSON.stringify(value)} has a multi-word effort; an effort is one adapter-owned token` };
380
- return { route: {
381
- kind: "route",
382
- provider,
383
- model,
384
- effort: trimmedEffort
385
- } };
386
- }
387
- /** Raised when a role's configured list cannot be selected from. */
388
- var RouteSelectionError = class extends Error {
389
- constructor(message) {
390
- super(message);
391
- this.name = "RouteSelectionError";
392
- }
393
- };
394
- /**
395
- * Expand one role into the routes this call starts.
396
- *
397
- * Automatic panels start one child per entry. Another list role selects one entry, the
398
- * first by default; `roleIndex` chooses which. A single role holds exactly one route and
399
- * rejects `roleIndex`.
400
- */
401
- function selectRoleRoutes(spec, entry, roleIndex) {
402
- if (spec.shape === "single" && roleIndex !== void 0) throw new RouteSelectionError(`role "${spec.key}" holds a single route, so \`roleIndex\` does not apply; drop \`roleIndex\` or use a list-valued role (${listRoleKeys().join(", ")})`);
403
- if (spec.autoPanel && roleIndex !== void 0) throw new RouteSelectionError(`role "${spec.key}" is an automatic panel: it starts one child per entry, so \`roleIndex\` does not apply`);
404
- if (entry === void 0) return {
405
- role: spec,
406
- mode: spec.autoPanel ? "panel" : "single",
407
- configured: false,
408
- routes: [{ kind: "inherit" }],
409
- total: 1
410
- };
411
- if (entry.issues.length > 0) throw new RouteSelectionError(`role "${spec.key}" has invalid configuration on line ${entry.line}: ${entry.issues.join("; ")}. Fix the role file before delegating.`);
412
- if (spec.autoPanel) return {
413
- role: spec,
414
- mode: "panel",
415
- configured: true,
416
- routes: entry.routes,
417
- total: entry.routes.length
418
- };
419
- if (spec.shape === "list") {
420
- const index = roleIndex ?? 0;
421
- if (index >= entry.routes.length) throw new RouteSelectionError(`role "${spec.key}" has ${entry.routes.length} entr${entry.routes.length === 1 ? "y" : "ies"}, so \`roleIndex\` ${index} is out of range; valid indexes are ${entry.routes.map((_route, position) => position).join(", ")}`);
422
- return {
423
- role: spec,
424
- mode: "selected",
425
- configured: true,
426
- routes: [entry.routes[index]],
427
- total: entry.routes.length,
428
- roleIndex: index
429
- };
430
- }
431
- return {
432
- role: spec,
433
- mode: "single",
434
- configured: true,
435
- routes: [entry.routes[0]],
436
- total: entry.routes.length
437
- };
438
- }
439
- /** Keys of the list-valued roles, including the automatic panels. */
440
- function listRoleKeys() {
441
- return ROLE_MANIFEST.filter((role) => role.shape === "list").map((role) => role.key);
442
- }
443
- /** Raised when a role name does not match the manifest. */
444
- var RoleNameError = class extends Error {
445
- constructor(message) {
446
- super(message);
447
- this.name = "RoleNameError";
448
- }
449
- };
450
- /** Normalize a role name for lookup. Case, spacing, spacing around a comma, and a trailing colon are formatting. */
451
- function normalizeRoleName(raw) {
452
- return raw.normalize("NFC").trim().toLowerCase().replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").replace(/:+$/, "").trim();
453
- }
454
- /** Resolve a role name, or throw a diagnostic that names the near misses. */
455
- function requireRole(raw) {
456
- const spec = findRoleSpec(normalizeRoleName(raw));
457
- if (spec !== void 0) return spec;
458
- const suggestions = suggestRoles(raw);
459
- const hint = suggestions.length === 0 ? "No role key is close to it." : `Closest role keys: ${suggestions.map((key) => `"${key}"`).join(", ")}.`;
460
- throw new RoleNameError(`unknown pstack role ${JSON.stringify(raw)}. ${hint} Role names come from the role manifest, not from free text: pass the exact key, or omit \`role\` to inherit the parent route.`);
461
- }
462
- function suggestRoles(raw, limit = 3) {
463
- const target = normalizeRoleName(raw);
464
- if (target === "") return [];
465
- return ROLE_MANIFEST.map((role) => {
466
- const shared = sharedTokenCount(target, role.key);
467
- const distance = editDistance(target, role.key);
468
- const contains = role.key.includes(target) || target.includes(role.key);
469
- const score = (contains ? 1e3 : 0) + shared * 100 - distance + Math.min(role.key.length, target.length);
470
- return {
471
- key: role.key,
472
- score,
473
- distance,
474
- shared,
475
- contains
476
- };
477
- }).filter((candidate) => candidate.contains || candidate.shared > 0 || candidate.distance <= 4).sort((left, right) => right.score - left.score).slice(0, limit).map((candidate) => candidate.key);
478
- }
479
- function unknownRoleMessage(rawKey, key) {
480
- const suggestions = suggestRoles(key);
481
- const hint = suggestions.length === 0 ? "no role key is close to it" : `closest role keys: ${suggestions.map((candidate) => `"${candidate}"`).join(", ")}`;
482
- return `unknown role ${JSON.stringify(rawKey)} (${hint}). A line for a retired or misspelled role is reported rather than ignored.`;
483
- }
484
- function sharedTokenCount(left, right) {
485
- const rightTokens = new Set(tokenize(right));
486
- return [...new Set(tokenize(left))].filter((token) => rightTokens.has(token)).length;
487
- }
488
- function tokenize(value) {
489
- return value.split(/[^a-z0-9]+/).filter((token) => token !== "");
490
- }
491
- /** Levenshtein distance, iterative with one row of state. */
492
- function editDistance(left, right) {
493
- if (left === right) return 0;
494
- if (left === "") return right.length;
495
- if (right === "") return left.length;
496
- let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
497
- for (let row = 1; row <= left.length; row += 1) {
498
- const current = [row];
499
- for (let column = 1; column <= right.length; column += 1) {
500
- const substitution = previous[column - 1] + (left[row - 1] === right[column - 1] ? 0 : 1);
501
- const deletion = previous[column] + 1;
502
- const insertion = current[column - 1] + 1;
503
- current.push(Math.min(substitution, deletion, insertion));
504
- }
505
- previous = current;
506
- }
507
- return previous[right.length];
508
- }
509
- function stripFrontmatter(text) {
510
- const normalized = text.replaceAll("\r\n", "\n");
511
- if (!normalized.startsWith("---\n")) return normalized;
512
- const end = normalized.indexOf("\n---", 3);
513
- if (end === -1) return normalized;
514
- const after = normalized.indexOf("\n", end + 1);
515
- return after === -1 ? "" : normalized.slice(after + 1);
516
- }
517
- function isMissingFile(error) {
518
- return typeof error === "object" && error !== null && error.code === "ENOENT";
519
- }
520
113
  function describeError$1(error) {
521
114
  if (error instanceof Error) {
522
115
  const code = error.code;
523
- return typeof code === "string" ? `${code}: ${error.message}` : error.message;
116
+ return typeof code === "string" ? `${error.message} (${code})` : error.message;
524
117
  }
525
118
  return String(error);
526
119
  }
@@ -556,6 +149,13 @@ const AGENT_KINDS = [
556
149
  "general"
557
150
  ];
558
151
  const DEFAULT_CONFIG = { provider: DEFAULT_SUBAGENT_PROVIDER };
152
+ /**
153
+ * Depth a child runs at when a call omits `maxDepth` or passes an unusable value.
154
+ *
155
+ * A child is always at least depth 1 (it is one level below its parent), so this is the
156
+ * smallest value that can start a child while forbidding it from delegating further.
157
+ */
158
+ const DEFAULT_CHILD_MAX_DEPTH = 1;
559
159
  /** Raised for a configuration or argument problem, before any child starts. */
560
160
  var DelegationError = class extends Error {
561
161
  constructor(message) {
@@ -621,22 +221,36 @@ function composeChildPrompt(input) {
621
221
  sections.push(`Task:\n\n${input.task}`);
622
222
  return sections.join("\n\n---\n\n");
623
223
  }
224
+ const DELEGATION_ARGUMENT_KEYS = [
225
+ "description",
226
+ "prompt",
227
+ "agent",
228
+ "role",
229
+ "roleIndex",
230
+ "reasoningEffort",
231
+ "cwd",
232
+ "maxDepth"
233
+ ];
234
+ /**
235
+ * Keys a call must pass. `maxDepth` is accepted but not required: a missing or unusable
236
+ * value resolves to `DEFAULT_CHILD_MAX_DEPTH` instead of forcing the caller to guess an
237
+ * integer, which is where the reflexive `0` came from.
238
+ */
239
+ const REQUIRED_DELEGATION_ARGUMENT_KEYS = [
240
+ "description",
241
+ "prompt",
242
+ "agent",
243
+ "role",
244
+ "roleIndex",
245
+ "reasoningEffort",
246
+ "cwd"
247
+ ];
624
248
  /** Validate every required model-facing argument. Null declines an optional setting. */
625
249
  function parseDelegationArgs(raw) {
626
250
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new DelegationError("arguments must be an object");
627
251
  const record = raw;
628
- const known = [
629
- "description",
630
- "prompt",
631
- "agent",
632
- "role",
633
- "roleIndex",
634
- "reasoningEffort",
635
- "cwd",
636
- "maxDepth"
637
- ];
638
- for (const key of known) if (!Object.hasOwn(record, key)) throw new DelegationError(`missing required argument "${key}"; every argument must be passed explicitly`);
639
- for (const key of Object.keys(record)) if (!known.includes(key)) throw new DelegationError(`unknown argument "${key}"; accepted arguments are ${known.join(", ")}`);
252
+ for (const key of REQUIRED_DELEGATION_ARGUMENT_KEYS) if (!Object.hasOwn(record, key)) throw new DelegationError(`missing required argument "${key}"; every argument except maxDepth must be passed explicitly`);
253
+ for (const key of Object.keys(record)) if (!DELEGATION_ARGUMENT_KEYS.includes(key)) throw new DelegationError(`unknown argument "${key}"; accepted arguments are ${DELEGATION_ARGUMENT_KEYS.join(", ")}`);
640
254
  const description = record["description"];
641
255
  if (!isNonEmptyString(description)) throw new DelegationError("description must be a non-empty string: it becomes the child label");
642
256
  const prompt = record["prompt"];
@@ -647,8 +261,9 @@ function parseDelegationArgs(raw) {
647
261
  if (role !== null && !isNonEmptyString(role)) throw new DelegationError("role must be a non-empty string or null");
648
262
  const cwd = record["cwd"];
649
263
  if (cwd !== null && !isNonEmptyString(cwd)) throw new DelegationError("cwd must be a non-empty string or null");
650
- const maxDepth = record["maxDepth"];
651
- if (!isNonNegativeSafeInteger(maxDepth)) throw new DelegationError("maxDepth must be a non-negative safe integer");
264
+ const rawMaxDepth = record["maxDepth"];
265
+ const maxDepth = rawMaxDepth === void 0 ? null : rawMaxDepth;
266
+ if (maxDepth !== null && !isNonNegativeSafeInteger(maxDepth)) throw new DelegationError("maxDepth must be a non-negative safe integer, or null/omitted");
652
267
  const roleIndex = record["roleIndex"];
653
268
  if (roleIndex !== null && !isNonNegativeSafeInteger(roleIndex)) throw new DelegationError("roleIndex must be a non-negative safe integer or null");
654
269
  if (roleIndex !== null && role === null) throw new DelegationError("roleIndex requires a non-null role");
@@ -674,15 +289,30 @@ function routeLabel(route, effortOverride) {
674
289
  }
675
290
  /** Take the smallest of the requested depth, this row's cap, and the Host setting. */
676
291
  function resolveDelegationDepth(configured, requested, hostLimit) {
677
- if (requested !== void 0 && !isNonNegativeSafeInteger(requested)) throw new DelegationError("maxDepth must be a non-negative safe integer");
678
- if (configured !== void 0 && requested !== void 0 && requested > configured) throw new DelegationError(`maxDepth ${requested} exceeds this row's configured cap of ${configured}. Lower the request or raise \`pstack-subagent\` config.maxDepth.`);
292
+ if (requested !== void 0 && requested !== null && !isNonNegativeSafeInteger(requested)) throw new DelegationError("maxDepth must be a non-negative safe integer or null");
293
+ if (configured !== void 0 && requested !== void 0 && requested !== null && requested > configured) throw new DelegationError(`maxDepth ${requested} exceeds this row's configured cap of ${configured}. Lower the request or raise \`pstack-subagent\` config.maxDepth.`);
679
294
  const candidates = [
680
295
  configured,
681
296
  requested,
682
297
  hostLimit
683
- ].filter((value) => value !== void 0);
298
+ ].filter((value) => value !== void 0 && value !== null);
684
299
  return candidates.length === 0 ? void 0 : Math.min(...candidates);
685
300
  }
301
+ /**
302
+ * Turn the per-call `maxDepth` request into an effective depth request.
303
+ *
304
+ * A child is always at least depth 1, so `0` can never start one: it is reported and
305
+ * replaced with `DEFAULT_CHILD_MAX_DEPTH` rather than failing the whole call. Omitting
306
+ * `maxDepth` (null) also uses the default, so a caller never has to guess an integer.
307
+ */
308
+ function requestedChildDepth(requested, diagnostics) {
309
+ if (requested === null) return 1;
310
+ if (requested === 0) {
311
+ diagnostics.push(`maxDepth 0 cannot start a child (the child itself is depth 1), so this call used 1. Omit maxDepth for that default, or pass 1 or more.`);
312
+ return 1;
313
+ }
314
+ return requested;
315
+ }
686
316
  /** Resolve the role file, routes, prompt, and depth into one plan. Pure of service calls. */
687
317
  function planDelegation(args, runtime, config = DEFAULT_CONFIG) {
688
318
  const rolePath = modelsFilePath(runtime.dshHome);
@@ -712,7 +342,7 @@ function planDelegation(args, runtime, config = DEFAULT_CONFIG) {
712
342
  task: args.prompt
713
343
  });
714
344
  const agentPreset = agentPresetFor(args.agent);
715
- const depth = resolveDelegationDepth(config.maxDepth, args.maxDepth, void 0);
345
+ const depth = resolveDelegationDepth(config.maxDepth, requestedChildDepth(args.maxDepth, diagnostics), void 0);
716
346
  const total = routes.length;
717
347
  const children = routes.map((route, index) => {
718
348
  const agentOptions = agentOptionsFor(route, args.reasoningEffort ?? void 0);
@@ -902,9 +532,11 @@ async function delegate(parent, args, runtime, config = DEFAULT_CONFIG, signal =
902
532
  const TOOL_DESCRIPTION = [
903
533
  "Delegate self-contained work to one or more fresh child agents.",
904
534
  "",
905
- "Every argument is required. Pass null for role, roleIndex, reasoningEffort, or cwd when that",
906
- "setting is not used. Choose agent explicitly. maxDepth is a non-negative integer. No argument",
907
- "has an implicit default.",
535
+ "Every argument is required except maxDepth. Pass null for role, roleIndex, reasoningEffort,",
536
+ "or cwd when that setting is not used. Choose agent explicitly. maxDepth is optional: omit it",
537
+ "(or pass null) for the safe default of 1, where the child exists but cannot start its own",
538
+ "children; pass 2 or more, within this row's configured cap, to let it delegate further. A 0 is",
539
+ "treated as 1, because a child is already depth 1 and 0 can never start one.",
908
540
  "",
909
541
  "Returns the id of every child it started plus the route each one resolved. The children run",
910
542
  "in the background: their results arrive later as completion notices in this conversation.",
@@ -953,28 +585,19 @@ function buildParameters() {
953
585
  description: "Required initial working directory, inside this session's directory or a separate Git worktree. Relative paths resolve against this session's directory. Null inherits the session directory."
954
586
  },
955
587
  maxDepth: {
956
- type: "integer",
957
- description: "Required non-negative delegation-depth limit. It can only lower this row's configured cap."
588
+ type: ["integer", "null"],
589
+ description: "Optional delegation-depth cap for the child. Omit or pass null for the safe default of 1 (the child exists but cannot start its own children). Pass 2 or more, within this row's configured cap, to let it delegate further. 0 is treated as 1: a child is already depth 1, so 0 can never start one."
958
590
  },
959
591
  roleIndex: {
960
592
  type: ["integer", "null"],
961
- description: "Required entry index for a list-valued role that is not an automatic panel, 0 or greater. Requires a non-null role. Null selects the first entry."
593
+ description: "Required entry index for a list-valued role that is not an automatic panel, 0 or greater. Requires a non-null role. Null selects the first entry. Call `pstack_roles` to see which roles accept an index."
962
594
  },
963
595
  reasoningEffort: {
964
596
  type: ["string", "null"],
965
597
  description: "Required reasoning-effort override for every child this call starts. Use one token the target model advertises, or null to keep the role line's starting effort. An unknown token fails the whole call before any child starts."
966
598
  }
967
599
  },
968
- required: [
969
- "description",
970
- "prompt",
971
- "agent",
972
- "role",
973
- "roleIndex",
974
- "reasoningEffort",
975
- "cwd",
976
- "maxDepth"
977
- ]
600
+ required: [...REQUIRED_DELEGATION_ARGUMENT_KEYS]
978
601
  };
979
602
  }
980
603
  const OUTPUT_SCHEMA = {
@@ -1101,4 +724,4 @@ function describeError(error) {
1101
724
  }
1102
725
  return String(error);
1103
726
  }
1104
- export { AGENT_KINDS, AGENT_PRESETS, DEFAULT_SUBAGENT_PROVIDER, DelegationError, PartialDelegationError, TOOL_NAME, agentPresetFor, apply, assertProviderCapabilities, composeChildPrompt, createPstackSubagentTool, delegate, hostRuntime, inject, name, normalizeConfig, parseDelegationArgs, planDelegation, renderDelegationResult, resolveChildCwd, resolveDelegationDepth, routeLabel, startPlannedChildren };
727
+ export { AGENT_KINDS, AGENT_PRESETS, DEFAULT_CHILD_MAX_DEPTH, DEFAULT_SUBAGENT_PROVIDER, DELEGATION_ARGUMENT_KEYS, DelegationError, PartialDelegationError, REQUIRED_DELEGATION_ARGUMENT_KEYS, TOOL_NAME, agentPresetFor, apply, assertProviderCapabilities, composeChildPrompt, createPstackSubagentTool, delegate, hostRuntime, inject, name, normalizeConfig, parseDelegationArgs, planDelegation, renderDelegationResult, requestedChildDepth, resolveChildCwd, resolveDelegationDepth, routeLabel, startPlannedChildren };