pi-subagents 0.42.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +3 -5
  3. package/package.json +1 -1
  4. package/skills/pi-subagents/SKILL.md +2 -2
  5. package/skills/pi-subagents/references/constraints-and-recipes.md +35 -32
  6. package/skills/pi-subagents/references/execution-controls.md +27 -43
  7. package/skills/pi-subagents/references/management-authoring-rpc.md +20 -3
  8. package/skills/pi-subagents/references/prompting-and-roles.md +16 -49
  9. package/src/agents/agent-refinements.ts +624 -0
  10. package/src/agents/agents.ts +0 -2
  11. package/src/agents/proactive-skills.ts +1 -1
  12. package/src/api/delegation.ts +1 -2
  13. package/src/extension/control-notices.ts +2 -2
  14. package/src/extension/fanout-child.ts +46 -22
  15. package/src/extension/index.ts +29 -12
  16. package/src/extension/public-execution.ts +71 -0
  17. package/src/extension/rpc.ts +7 -8
  18. package/src/extension/schemas.ts +17 -17
  19. package/src/extension/tool-description.ts +14 -14
  20. package/src/missions/actions.ts +47 -11
  21. package/src/missions/goal-driver.ts +162 -0
  22. package/src/missions/lifecycle.ts +44 -12
  23. package/src/missions/store.ts +68 -3
  24. package/src/missions/types.ts +25 -3
  25. package/src/missions/workflow-state.ts +77 -0
  26. package/src/profiles/profiles.ts +1 -3
  27. package/src/runs/background/async-execution.ts +3 -0
  28. package/src/runs/background/async-job-tracker.ts +2 -17
  29. package/src/runs/background/control-channel.ts +50 -6
  30. package/src/runs/background/retained-children.ts +68 -0
  31. package/src/runs/background/scheduled-runs.ts +17 -13
  32. package/src/runs/background/steering.ts +7 -5
  33. package/src/runs/background/subagent-runner.ts +10 -4
  34. package/src/runs/foreground/async-steering-action.ts +36 -10
  35. package/src/runs/foreground/chain-clarify.ts +13 -16
  36. package/src/runs/foreground/execution.ts +4 -0
  37. package/src/runs/foreground/subagent-executor.ts +200 -59
  38. package/src/runs/shared/acceptance.ts +154 -9
  39. package/src/runs/shared/subagent-prompt-runtime.ts +94 -25
  40. package/src/shared/types.ts +17 -3
  41. package/src/slash/delegation-adapters.ts +0 -13
  42. package/src/slash/prompt-template-bridge.ts +14 -12
  43. package/src/slash/prompt-workflows.ts +10 -4
  44. package/src/slash/slash-bridge.ts +8 -6
  45. package/src/slash/slash-commands.ts +27 -12
  46. package/src/slash/slash-live-state.ts +4 -2
  47. package/src/tui/fleet-status.ts +8 -4
  48. package/src/tui/fleet.ts +15 -5
  49. package/src/tui/render.ts +13 -31
  50. package/src/workflows/chat-progress.ts +2 -2
  51. package/src/workflows/scripted-workflow.ts +97 -10
  52. package/agents/context-builder.md +0 -46
  53. package/agents/planner.md +0 -56
  54. package/prompts/parallel-context-build.md +0 -55
  55. package/prompts/parallel-handoff-plan.md +0 -61
@@ -7,7 +7,7 @@ import { getArtifactsDir } from "../shared/artifacts.ts";
7
7
  import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
8
8
  import { resolveWaitToolConfig } from "../runs/background/wait-config.ts";
9
9
  import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
10
- import { readNestedControlRequests, resolveNestedRouteFromEnv, writeNestedControlResult } from "../runs/shared/nested-events.ts";
10
+ import { readNestedControlRequests, resolveNestedRouteFromEnv, type NestedRoute, writeNestedControlResult } from "../runs/shared/nested-events.ts";
11
11
  import { deliverSubagentIntercomMessageEvent } from "../intercom/result-intercom.ts";
12
12
  import { resolveSubagentIntercomTarget } from "../intercom/intercom-bridge.ts";
13
13
  import { SubagentParams } from "./schemas.ts";
@@ -50,25 +50,42 @@ function createChildSafeState(): SubagentState {
50
50
  };
51
51
  }
52
52
 
53
- function startNestedControlInboxListener(pi: ExtensionAPI, state: SubagentState): NodeJS.Timeout | undefined {
54
- let route;
53
+ function resolveNestedControlRoute(): NestedRoute | undefined {
55
54
  try {
56
- route = resolveNestedRouteFromEnv();
55
+ return resolveNestedRouteFromEnv();
57
56
  } catch {
58
57
  return undefined;
59
58
  }
60
- if (!route) return undefined;
61
- const seen = new Set<string>();
62
- const inFlight = new Set<string>();
63
- const pendingResults = new Map<string, Parameters<typeof writeNestedControlResult>[1]>();
59
+ }
60
+
61
+ function nestedControlRouteKey(route: NestedRoute): string {
62
+ return route.controlInbox;
63
+ }
64
+
65
+ interface NestedControlInboxState {
66
+ seen: Set<string>;
67
+ inFlight: Set<string>;
68
+ pendingResults: Map<string, Parameters<typeof writeNestedControlResult>[1]>;
69
+ }
70
+
71
+ interface NestedControlListenerEntry {
72
+ cleanup: () => void;
73
+ state: NestedControlInboxState;
74
+ }
75
+
76
+ function createNestedControlInboxState(): NestedControlInboxState {
77
+ return { seen: new Set(), inFlight: new Set(), pendingResults: new Map() };
78
+ }
79
+
80
+ function startNestedControlInboxListener(pi: ExtensionAPI, state: SubagentState, route: NestedRoute, inboxState: NestedControlInboxState): () => void {
64
81
  const timer = setInterval(() => {
65
82
  try {
66
83
  for (const request of readNestedControlRequests(route)) {
67
- if (seen.has(request.requestId) || inFlight.has(request.requestId)) continue;
68
- inFlight.add(request.requestId);
84
+ if (inboxState.seen.has(request.requestId) || inboxState.inFlight.has(request.requestId)) continue;
85
+ inboxState.inFlight.add(request.requestId);
69
86
  void (async () => {
70
87
  try {
71
- let result = pendingResults.get(request.requestId);
88
+ let result = inboxState.pendingResults.get(request.requestId);
72
89
  if (!result) {
73
90
  let ok = false;
74
91
  let message = "Control request failed.";
@@ -107,15 +124,15 @@ function startNestedControlInboxListener(pi: ExtensionAPI, state: SubagentState)
107
124
  try {
108
125
  writeNestedControlResult(route, result);
109
126
  } catch (error) {
110
- pendingResults.set(request.requestId, result);
127
+ inboxState.pendingResults.set(request.requestId, result);
111
128
  console.error(`Failed to write nested control result for request '${request.requestId}' targeting '${request.targetRunId}' via inbox '${route.controlInbox}'; keeping request for retry:`, error);
112
129
  return;
113
130
  }
114
- pendingResults.delete(request.requestId);
115
- seen.add(request.requestId);
131
+ inboxState.pendingResults.delete(request.requestId);
132
+ inboxState.seen.add(request.requestId);
116
133
  try { fs.unlinkSync(request.filePath); } catch {}
117
134
  } finally {
118
- inFlight.delete(request.requestId);
135
+ inboxState.inFlight.delete(request.requestId);
119
136
  }
120
137
  })();
121
138
  }
@@ -124,7 +141,7 @@ function startNestedControlInboxListener(pi: ExtensionAPI, state: SubagentState)
124
141
  }
125
142
  }, 200);
126
143
  timer.unref?.();
127
- return timer;
144
+ return () => clearInterval(timer);
128
145
  }
129
146
 
130
147
  export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI): void {
@@ -164,14 +181,21 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
164
181
  ].join("\n"),
165
182
  parameters: SubagentParams,
166
183
  execute(id, params, signal, onUpdate, ctx) {
167
- const input = params as SubagentParamsLike;
168
- if (input.tasks !== undefined || input.chain !== undefined || input.concurrency !== undefined || input.chainDir !== undefined || (input.worktree !== undefined && !(input.worktree === true && input.agent))) {
169
- return Promise.resolve({ content: [{ type: "text", text: "Legacy top-level chain and parallel inputs were removed; use workflowScript." }], isError: true, details: { mode: "management", results: [] } });
170
- }
171
- return executor.execute(id, input, signal ?? new AbortController().signal, onUpdate, ctx);
184
+ return executor.executePublic(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx);
172
185
  },
173
186
  };
174
187
 
175
188
  pi.registerTool(tool);
176
- startNestedControlInboxListener(pi, state);
189
+ const route = resolveNestedControlRoute();
190
+ if (!route) return;
191
+ const listenerCleanupKey = "__piSubagentFanoutChildNestedControlInboxCleanups";
192
+ const listenerCleanups = globalStore[listenerCleanupKey] instanceof Map
193
+ ? globalStore[listenerCleanupKey] as Map<string, NestedControlListenerEntry>
194
+ : new Map<string, NestedControlListenerEntry>();
195
+ globalStore[listenerCleanupKey] = listenerCleanups;
196
+ const routeKey = nestedControlRouteKey(route);
197
+ const previous = listenerCleanups.get(routeKey);
198
+ previous?.cleanup();
199
+ const inboxState = previous?.state ?? createNestedControlInboxState();
200
+ listenerCleanups.set(routeKey, { state: inboxState, cleanup: startNestedControlInboxListener(pi, state, route, inboxState) });
177
201
  }
@@ -5,7 +5,7 @@
5
5
  * - Sync (default): Streams output, renders markdown, tracks usage
6
6
  * - Async: Background execution, emits events when done
7
7
  *
8
- * Public execution modes: single (agent + task) and workflow (workflowScript)
8
+ * Public execution mode: workflow (workflowScript)
9
9
  * Toggle: async parameter (default: true; set asyncByDefault:false in config.json to opt out)
10
10
  *
11
11
  * Config file: ~/.pi/agent/extensions/subagent/config.json
@@ -52,7 +52,10 @@ import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capabili
52
52
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
53
53
  import { loadConfig, resolveAsyncByDefault } from "./config.ts";
54
54
  import { buildSubagentToolDescription } from "./tool-description.ts";
55
+ import { collectGoalContinuationNotices } from "../missions/goal-driver.ts";
55
56
  import { syncMissionFromAsyncCompletion } from "../missions/lifecycle.ts";
57
+ import { resolveMissionStoreLocation } from "../missions/store.ts";
58
+ import { listRetainedChildren } from "../runs/background/retained-children.ts";
56
59
  import {
57
60
  type Details,
58
61
  type SubagentState,
@@ -379,6 +382,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
379
382
  }, { placement: fleetViewPlacement })
380
383
  : undefined;
381
384
  let executorScheduled: ((id: string, params: SubagentParamsLike, signal: AbortSignal, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>) | undefined;
385
+ let goalTurnId = 0;
382
386
  const scheduledRunManager = createScheduledRunManager({
383
387
  config,
384
388
  launch: (params, ctx, signal) => {
@@ -501,7 +505,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
501
505
 
502
506
  const executeSubagentCollapsed = (id: string, params: SubagentParamsLike, signal: AbortSignal, onUpdate: ((result: AgentToolResult<Details>) => void) | undefined, ctx: ExtensionContext) => {
503
507
  if (ctx.hasUI) ctx.ui.setToolsExpanded(false);
504
- return executor.execute(id, params, signal, onUpdate, ctx);
508
+ return executor.executePublic(id, params, signal, onUpdate, ctx);
505
509
  };
506
510
 
507
511
  const slashBridge = registerSlashSubagentBridge({
@@ -525,7 +529,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
525
529
  const rpcBridge = registerSubagentRpcBridge({
526
530
  events: pi.events,
527
531
  getContext: () => state.lastUiContext,
528
- execute: (id, params, signal, onUpdate, ctx) => executor.execute(id, params, signal, onUpdate, ctx),
532
+ execute: (id, params, signal, onUpdate, ctx) => executor.executePublic(id, params, signal, onUpdate, ctx),
529
533
  state,
530
534
  });
531
535
 
@@ -537,11 +541,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
537
541
  parameters: SubagentParams,
538
542
 
539
543
  execute(id, params, signal, onUpdate, ctx) {
540
- const input = params as SubagentParamsLike;
541
- if (input.tasks !== undefined || input.chain !== undefined || input.concurrency !== undefined || input.chainDir !== undefined || (input.worktree !== undefined && !(input.worktree === true && input.agent))) {
542
- return Promise.resolve({ content: [{ type: "text", text: "Legacy top-level chain and parallel inputs were removed; use workflowScript." }], isError: true, details: { mode: "management", results: [] } });
543
- }
544
- return executeSubagentCollapsed(id, input, signal ?? new AbortController().signal, onUpdate, ctx);
544
+ return executeSubagentCollapsed(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx);
545
545
  },
546
546
 
547
547
  renderCall(args, theme) {
@@ -554,11 +554,11 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
554
554
  }
555
555
  if (args.workflowScript)
556
556
  return new Text(
557
- `${theme.fg("toolTitle", theme.bold("subagent "))}${formatWorkflowManifest(args.workflowScript, args.async, args.clarify)}`,
557
+ `${theme.fg("toolTitle", theme.bold("subagent "))}${formatWorkflowManifest(args.workflowScript, args.async, false)}`,
558
558
  0,
559
559
  0,
560
560
  );
561
- const asyncLabel = args.async === true && args.clarify !== true ? theme.fg("warning", " [async]") : "";
561
+ const asyncLabel = args.async === true ? theme.fg("warning", " [async]") : "";
562
562
  return new Text(
563
563
  `${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent || "?")}${asyncLabel}`,
564
564
  0,
@@ -581,8 +581,24 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
581
581
  registerWaitTool(pi, state, waitToolConfig.enabled, waitSubscriptionManager);
582
582
 
583
583
  pi.on("agent_end", async (_event, ctx) => {
584
- if (ctx.hasUI) return;
585
- await drainOutstandingWork({ state, events: pi.events });
584
+ if (!ctx.hasUI) await drainOutstandingWork({ state, events: pi.events });
585
+ const ownerSessionId = state.currentSessionId;
586
+ if (!ownerSessionId) return;
587
+ goalTurnId += 1;
588
+ try {
589
+ const location = resolveMissionStoreLocation({ projectRoot: state.baseCwd, ...(config.missions ? { config: config.missions } : {}) });
590
+ const retainedChildren = listRetainedChildren(DIRS.async, ownerSessionId);
591
+ for (const notice of collectGoalContinuationNotices({ location, ownerSessionId, retainedChildren, turnId: goalTurnId })) {
592
+ handleSubagentControlNotice({
593
+ pi,
594
+ state,
595
+ visibleControlNotices: new Set(),
596
+ details: { source: "goal", event: notice.event, noticeText: notice.message },
597
+ });
598
+ }
599
+ } catch (error) {
600
+ console.error("Failed to evaluate goal missions:", error);
601
+ }
586
602
  });
587
603
 
588
604
  registerSlashCommands(pi, state);
@@ -678,6 +694,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
678
694
 
679
695
  const resetSessionState = (ctx: ExtensionContext, recovering: boolean) => {
680
696
  state.baseCwd = ctx.cwd;
697
+ goalTurnId = 0;
681
698
  state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
682
699
  state.parentSessionFile = ctx.sessionManager.getSessionFile();
683
700
  state.subagentSpawns = {
@@ -0,0 +1,71 @@
1
+ export interface PublicSubagentExecutionParams {
2
+ action?: unknown;
3
+ agent?: unknown;
4
+ task?: unknown;
5
+ step?: unknown;
6
+ tasks?: unknown;
7
+ chain?: unknown;
8
+ parallel?: unknown;
9
+ concurrency?: unknown;
10
+ chainDir?: unknown;
11
+ workflowScript?: unknown;
12
+ resume?: unknown;
13
+ clarify?: unknown;
14
+ }
15
+
16
+ export type PublicSubagentExecutionMode = "workflow" | "management";
17
+
18
+ export type PublicSubagentExecutionNormalization<T> =
19
+ | { ok: true; params: T }
20
+ | { ok: false; error: string; mode: PublicSubagentExecutionMode };
21
+
22
+ /**
23
+ * Enforce the public execution cutover before requests reach the executor.
24
+ * Internal runs.run children and structured owned delegation bypass this boundary.
25
+ */
26
+ export function normalizePublicSubagentExecution<T extends PublicSubagentExecutionParams>(params: T): PublicSubagentExecutionNormalization<T> {
27
+ const action = params.action;
28
+ if (action !== undefined && (typeof action !== "string" || !action.trim())) {
29
+ return { ok: false, error: "action must be a non-empty management/control action, or omit action and use workflowScript.", mode: "management" };
30
+ }
31
+ const normalizedAction = typeof action === "string" ? action.trim() : undefined;
32
+ if (params.clarify !== undefined) {
33
+ return { ok: false, error: "Public workflowScript execution does not support clarify UI.", mode: "workflow" };
34
+ }
35
+ if (params.resume !== undefined) {
36
+ return { ok: false, error: "Top-level resume execution is not available. Put resume on a workflowScript runs.run/runs.all item.", mode: "workflow" };
37
+ }
38
+ const hasLegacyOrchestration = params.tasks !== undefined || params.chain !== undefined || params.parallel !== undefined || params.concurrency !== undefined || params.chainDir !== undefined;
39
+ if (hasLegacyOrchestration) {
40
+ return { ok: false, error: "Legacy top-level chain and parallel inputs were removed; use workflowScript.", mode: normalizedAction ? "management" : "workflow" };
41
+ }
42
+ if (normalizedAction !== undefined) {
43
+ const legacyAction = normalizedAction.toLowerCase();
44
+ if (legacyAction === "single") {
45
+ return { ok: false, error: "Direct execution was removed. Use workflowScript: \"return runs.run('main', { agent, task })\".", mode: "workflow" };
46
+ }
47
+ if (legacyAction === "parallel" || legacyAction === "tasks" || legacyAction === "chain") {
48
+ return { ok: false, error: "Legacy top-level chain and parallel inputs were removed; use workflowScript.", mode: "workflow" };
49
+ }
50
+ if (normalizedAction === "schedule.create") {
51
+ if (params.agent !== undefined || params.task !== undefined || params.step !== undefined) {
52
+ return { ok: false, error: "schedule.create requires workflowScript and does not accept direct agent, task, or step execution fields.", mode: "management" };
53
+ }
54
+ if (typeof params.workflowScript !== "string" || !params.workflowScript.trim()) {
55
+ return { ok: false, error: "schedule.create requires a non-empty workflowScript.", mode: "management" };
56
+ }
57
+ return { ok: true, params: { ...params, action: normalizedAction } };
58
+ }
59
+ if (params.workflowScript !== undefined) {
60
+ return { ok: false, error: "workflowScript execution must omit action; only schedule.create accepts action with workflowScript.", mode: "management" };
61
+ }
62
+ return { ok: true, params: { ...params, action: normalizedAction } };
63
+ }
64
+ if (params.agent !== undefined || params.task !== undefined || params.step !== undefined) {
65
+ return { ok: false, error: "Direct execution was removed. Use workflowScript: \"return runs.run('main', { agent, task })\".", mode: "workflow" };
66
+ }
67
+ if (typeof params.workflowScript !== "string" || !params.workflowScript.trim()) {
68
+ return { ok: false, error: "Execution requires a non-empty workflowScript. Direct execution was removed; use workflowScript: \"return runs.run('main', { agent, task })\".", mode: "workflow" };
69
+ }
70
+ return { ok: true, params };
71
+ }
@@ -19,6 +19,7 @@ import {
19
19
  import { readStatus } from "../shared/utils.ts";
20
20
  import { SubagentParams } from "./schemas.ts";
21
21
  import { formatWorkflowJsonPreview } from "../workflows/scripted-workflow.ts";
22
+ import { normalizePublicSubagentExecution } from "./public-execution.ts";
22
23
 
23
24
  export const SUBAGENT_RPC_PROTOCOL_VERSION = 1;
24
25
  export const SUBAGENT_RPC_REQUEST_EVENT = "subagents:rpc:v1:request";
@@ -420,19 +421,15 @@ async function executeChecked(
420
421
 
421
422
  function spawnParams(params: unknown): SubagentParamsLike {
422
423
  const input = assertRecordParams(params, "spawn");
423
- if (input.tasks !== undefined || input.chain !== undefined || input.concurrency !== undefined || input.chainDir !== undefined || (input.worktree !== undefined && !(input.worktree === true && input.agent))) {
424
- throw new SubagentRpcError("invalid_params", "RPC spawn no longer accepts top-level chain or parallel inputs; use workflowScript.");
425
- }
426
- if (input.action !== undefined) {
424
+ const normalized = normalizePublicSubagentExecution(input);
425
+ if (!normalized.ok) throw new SubagentRpcError("invalid_params", normalized.error);
426
+ if (normalized.params.action !== undefined) {
427
427
  throw new SubagentRpcError("invalid_params", "RPC spawn does not accept management/control actions. Use status or interrupt RPC methods instead.");
428
428
  }
429
429
  if (input.async === false) {
430
430
  throw new SubagentRpcError("invalid_params", "RPC spawn only supports detached async launches; omit async or set async: true.");
431
431
  }
432
- if (input.clarify === true) {
433
- throw new SubagentRpcError("invalid_params", "RPC spawn cannot open the clarify UI; omit clarify or set clarify: false.");
434
- }
435
- return { ...(input as SubagentParamsLike), async: true, clarify: false };
432
+ return { ...(normalized.params as SubagentParamsLike), async: true };
436
433
  }
437
434
 
438
435
  function steerParams(params: unknown): SubagentParamsLike {
@@ -441,10 +438,12 @@ function steerParams(params: unknown): SubagentParamsLike {
441
438
  throw new SubagentRpcError("invalid_params", "RPC steer requires a non-empty message.");
442
439
  const target = normalizeTargetParams(input, "steer");
443
440
  if (!target.id && !target.runId && !target.dir) throw new SubagentRpcError("invalid_params", "RPC steer requires id, runId, or dir.");
441
+ if (input.mode !== undefined && input.mode !== "steer" && input.mode !== "follow_up" && input.mode !== "auto") throw new SubagentRpcError("invalid_params", "RPC steer mode must be steer, follow_up, or auto.");
444
442
  return {
445
443
  action: "steer",
446
444
  ...target,
447
445
  message: input.message.trim(),
446
+ ...(typeof input.mode === "string" ? { mode: input.mode as "steer" | "follow_up" | "auto" } : {}),
448
447
  steeringRecovery: false,
449
448
  };
450
449
  }
@@ -255,11 +255,11 @@ const ControlOverrides = Type.Object({
255
255
  });
256
256
 
257
257
  const SubagentParamsSchema = Type.Object({
258
- agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode) or target for management get/update/delete" })),
259
- task: Type.Optional(Type.String({ description: "Task (SINGLE mode, optional for self-contained agents)" })),
258
+ agent: Type.Optional(Type.String({ description: "Agent target for management actions such as get, update, delete, and models." })),
259
+ resume: Type.Optional(Type.String({ description: "Retained child run id for a workflowScript runs.run/runs.all item. Mutually exclusive with agent; task supplies the follow-up." })),
260
260
  // Management action (when present, tool operates in management mode)
261
- action: Type.Optional(Type.String({
262
- description: "Optional management/control action. Omit this field entirely for execution/delegation ({agent, task} or {workflowScript}); use it only for management/control actions."
261
+ action: Type.Optional(Type.String({ minLength: 1,
262
+ description: "Optional management/control action. Omit this field for workflowScript execution; use it only for management/control actions."
263
263
  })),
264
264
  name: Type.Optional(Type.String({ description: "Human-readable name for action='schedule.create'." })),
265
265
  id: Type.Optional(Type.String({
@@ -279,7 +279,8 @@ const SubagentParamsSchema = Type.Object({
279
279
  })),
280
280
  lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 500, description: "Maximum transcript lines for action='status', view='transcript'. Defaults to 80." })),
281
281
  message: Type.Optional(Type.String({ description: "Follow-up message for resume, live guidance for steer, or optional startup prompt for project.open." })),
282
- steeringRecovery: Type.Optional(Type.Boolean({ description: "For action='steer', allow pause-and-revive recovery after a missed acknowledgment. Defaults true for direct tool calls; extension RPC steering forces false so callers retain exact child ownership." })),
282
+ mode: Type.Optional(Type.String({ enum: ["steer", "follow_up", "auto"], description: "Delivery mode for action='steer'. steer interrupts at the next safe point (default), follow_up waits for the next turn boundary, and auto follows up mid-turn but delivers immediately between turns." })),
283
+ steeringRecovery: Type.Optional(Type.Boolean({ description: "For action='steer', allow pause-and-revive recovery after a missed acknowledgment. Defaults true for direct tool calls in steer mode; extension RPC steering forces false so callers retain exact child ownership." })),
283
284
  additional: Type.Optional(Type.Integer({ minimum: 1, description: "Positive launches to add with action='grant-spawn-budget'. Root interactive parent with native user confirmation only; total grants cannot exceed the original configured cap." })),
284
285
  scope: Type.Optional(Type.String({ enum: ["session", "user", "project"], description: "Scope for action='watchdog.configure'. Defaults to session to avoid persistent settings writes unless user/project is explicit." })),
285
286
  target: Type.Optional(Type.String({ enum: ["main", "children", "child"], description: "Target for watchdog actions." })),
@@ -294,8 +295,8 @@ const SubagentParamsSchema = Type.Object({
294
295
  overlap: Type.Optional(Type.String({ enum: ["skip"], description: "Overlap policy. This slice supports skip only." })),
295
296
  catchUp: Type.Optional(Type.String({ enum: ["none", "latest"], description: "Missed occurrence policy for recurring schedules. Defaults to latest." })),
296
297
  missionId: Type.Optional(Type.String({ description: "Mission id." })),
297
- mission: Type.Optional(Type.Unsafe({ ...MissionLaunchOverride, description: "Mission object, or false for no mission." })),
298
- missionUpdate: Type.Optional(Type.Unsafe({ ...MissionUpdateOverride, description: "Mission update: summary, labels, decisions, artifacts, or delivery receipts." })),
298
+ mission: Type.Optional(Type.Unsafe({ ...MissionLaunchOverride, description: "Mission object, or false for no mission. Use objective for intent; goal:true with budget.tokens enables turn-end continuation notices." })),
299
+ missionUpdate: Type.Optional(Type.Unsafe({ ...MissionUpdateOverride, description: "Mission update: objective, goal false or {paused:boolean}, budget, summary, labels, decisions, artifacts, or delivery receipts." })),
299
300
  missionStatus: Type.Optional(Type.String({ description: "Mission status." })),
300
301
  missionScope: Type.Optional(Type.String({ description: "Mission list scope: project (default) or global pointer index." })),
301
302
  runMode: Type.Optional(Type.String({ description: "Attached run mode." })),
@@ -313,17 +314,17 @@ const SubagentParamsSchema = Type.Object({
313
314
  ],
314
315
  description: "Agent/chain config for create/update. Object or JSON string; presence of steps creates a chain."
315
316
  })),
316
- workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript orchestration. Starts asynchronously by default; pass async:false for a small foreground run. Use await runs.run(key, {agent, task, worktree?}), runs.all([...]), runs.status(id), runs.ref(s), emit(value), console, and return. Use ordinary JavaScript loops, branches, awaits, and arrays to mix sequential and parallel phases dynamically. Set worktree:true at workflow or child level for a separate managed worktree per child; child fields override workflow defaults. runs.run accepts one child only. No filesystem, shell, Pi tools, or host globals." })),
317
- chatProgress: Type.Optional(Type.String({ enum: ["auto", "off", "terminal", "milestones", "live-card"], description: "WorkflowScript chat progress projection. auto shows a live in-chat card only for watched foreground workflows in the same Git repository; background and other-repo workflows stay quieter with terminal/milestone summaries." })),
318
- worktree: Type.Optional(Type.Boolean({ description: "Managed child isolation. true gives a direct single child or each workflow child a separate git worktree; an individual runs.run/runs.all item can override a workflow default with worktree:false." })),
317
+ workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript statement body. Starts async by default; pass async:false for a small foreground run. Use explicit return for output. Use await runs.run(key, {agent, task, worktree?, gate?}) or runs.run(key, {resume, task}), runs.all([...]), runs.status(id), runs.ref(s), emit(value), console, and return. Mission workflows also have async state.get(key) and state.set(key, JSONValue). Use ordinary JavaScript loops, branches, awaits, and arrays to mix sequential and parallel phases dynamically. Set worktree:true at workflow or child level for a separate managed worktree; child fields override workflow defaults. gate is one host-run command and cannot be combined with acceptance. runs.run accepts one child only. No filesystem, shell, Pi tools, or host globals." })),
318
+ chatProgress: Type.Optional(Type.String({ enum: ["auto", "off", "live-card"], description: "WorkflowScript chat progress projection. auto shows a live in-chat card only for watched foreground workflows in the same Git repository; it is off otherwise." })),
319
+ worktree: Type.Optional(Type.Boolean({ description: "Managed child isolation. true gives each workflow child a separate git worktree; an individual runs.run/runs.all item can override a workflow default with worktree:false." })),
319
320
  step: Type.Optional(Type.Unsafe({ ...ChainItem, description: "One chain step for action='append-step' only. Not an execution mode." })),
320
321
  context: Type.Optional(Type.String({
321
322
  enum: ["fresh", "fork"],
322
323
  description: "'fresh' or 'fork' to branch from parent session. Explicit context overrides every child in the invocation. If omitted, each requested agent uses its own defaultContext; agents without defaultContext: 'fork' run fresh.",
323
324
  })),
324
325
  async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
325
- timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Timeout for foreground and async/background runs; foreground defaults to 30m absent call/agent. Alias maxRuntimeMs." })),
326
- maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs for foreground and async/background runs; foreground defaults to 30m absent call/agent." })),
326
+ timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Optional timeout for foreground and async/background runs. Foreground workflows default to 30m; async workflows have no default timeout. Alias maxRuntimeMs." })),
327
+ maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs for foreground and async/background runs. Foreground workflows default to 30m; async workflows have no default timeout." })),
327
328
  turnBudget: Type.Optional(TurnBudgetOverride),
328
329
  toolBudget: Type.Optional(ToolBudgetOverride),
329
330
  usageBudget: Type.Optional(UsageBudgetOverride),
@@ -335,23 +336,22 @@ const SubagentParamsSchema = Type.Object({
335
336
  sessionDir: Type.Optional(
336
337
  Type.String({ description: "Directory to store session logs (default: temp; enables sessions even if share=false)" }),
337
338
  ),
338
- // Clarification TUI
339
- clarify: Type.Optional(Type.Boolean({ description: "Show TUI to preview/edit before execution. Explicit clarify: true keeps the run foreground for the clarify UI; omitted clarify can still run in the background when async: true is set." })),
340
339
  control: Type.Optional(ControlOverrides),
341
- // Solo agent overrides
340
+ // Workflow defaults forwarded to each runs.run/runs.all child unless overridden there.
342
341
  output: Type.Optional(Type.Unsafe({
343
342
  anyOf: [
344
343
  { type: "string" },
345
344
  { type: "boolean" },
346
345
  ],
347
- description: "Output file for single agent (string), or false to disable. Relative paths resolve against cwd.",
346
+ description: "Default child output file (string), or false to disable. Relative paths resolve against cwd.",
348
347
  })),
349
348
  outputMode: Type.Optional(OutputModeOverride),
350
349
  skill: Type.Optional(SkillOverride),
351
- model: Type.Optional(Type.String({ description: "Override model for single agent (e.g. 'anthropic/claude-sonnet-4')" })),
350
+ model: Type.Optional(Type.String({ description: "Default child model override (e.g. 'anthropic/claude-sonnet-4')" })),
352
351
  outputSchema: Type.Optional(JsonSchemaObject),
353
352
  agentContract: Type.Optional(AgentContractOverride),
354
353
  acceptance: Type.Optional(AcceptanceOverride),
354
+ gate: Type.Optional(Type.String({ minLength: 1, description: "Host gate command. Cannot be combined with acceptance." })),
355
355
  });
356
356
 
357
357
  export const SubagentParams = keepTopLevelParameterDescriptions(SubagentParamsSchema);
@@ -8,42 +8,41 @@ const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
8
8
 
9
9
  export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
10
10
  • Use { action: "list" } before execution and only run executable/non-disabled agents.
11
- • Keep execution and management separate: omit action for single-child and workflowScript execution; use action only for management/control.
11
+ • Keep execution and management separate: omit action for workflowScript execution; use action only for management/control.
12
12
  • Async/background runs are the default. Use async:false only when a blocking foreground result is needed. Do not sleep or poll status just to wait; use subagent_wait only when the current request must finish in this turn.
13
13
  • Ordinary child subagents are not orchestrators. Only explicitly configured fanout children may use the child-safe subagent tool, still bounded by depth/session limits.
14
14
  • Keep one writer for the same cwd/worktree. Use fresh-context read-only reviewers for independent review, then have the parent synthesize and apply fixes.
15
15
  • Async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs, and status via { action: "status", id }. Include output paths and residual risks when reporting results.`;
16
16
 
17
- export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Delegate one child with { agent, task } or compose work with { workflowScript }; omit action. workflowScript is the sole public orchestration surface. Use action only for management/control actions.
17
+ export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run subagents only through { workflowScript }; omit action. Use action only for management/control actions.
18
18
 
19
- EXECUTION (use exactly one mode):
19
+ EXECUTION:
20
20
  • Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
21
- • SINGLE: { agent, task? } launches one child. Omit task for a self-contained agent.
22
- • SCRIPTED WORKFLOW: { workflowScript: "const scan = await runs.run('scan', {agent:'agent-a', task:'...'}); return scan.output" }. Use stable-key runs.run for one child and runs.all for parallel children; ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. Scripts start asynchronously by default; pass async:false only for a small foreground run. Same-repo foreground workflows default to a live in-chat card; set chatProgress to auto, off, terminal, milestones, or live-card to control that projection. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Direct single-child calls also support worktree:true; use workflowScript only when coordination is needed. For repository mutation lanes, set worktree:true on a direct single child, workflow, or individual runs.run/runs.all item for managed isolation instead of manual Git worktrees; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.status, runs.ref/refs, emit, console, and standard JavaScript only. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
23
- • Sequential replacement: { workflowScript: "const a = await runs.run('analyze', {agent:'agent-a', task:'Analyze the request'}); return (await runs.run('plan', {agent:'agent-b', task:'Plan from: '+a.output})).output" }
24
- • Parallel replacement: { workflowScript: "const [a,b] = await runs.all([{key:'correctness',agent:'agent-a',task:'Review correctness'},{key:'tests',agent:'agent-b',task:'Review tests'}]); return {correctness:a.output,tests:b.output}" }
25
- • Optional context is "fresh" or "fork". timeoutMs/maxRuntimeMs apply to foreground and async runs. Omit acceptance for reviewer/read-only calls; evidence levels end at verified, and acceptance.review.required requests independent writer review.
21
+ • WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Every execution is a workflow. Use stable-key runs.run for one child and runs.all for parallel children; ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. Scripts start asynchronously by default; pass async:false only for a small foreground run. Same-repo foreground workflows default to a live in-chat card; set chatProgress to auto, off, or live-card to control that projection. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use {action:"children.list"} to list up to 10 completed retained children from this parent session, then continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); resume and agent are mutually exclusive, and resume keeps the stored agent/model/tool contract. For repository mutation lanes, set worktree:true on the workflow or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.status, runs.ref/refs, emit, console, and standard JavaScript only. Mission-attached workflows also get async state.get(key) and state.set(key, JSONValue); mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
22
+ • Sequential example: { workflowScript: "const a = await runs.run('analyze', {agent:'agent-a', task:'Analyze the request'}); return (await runs.run('plan', {agent:'agent-b', task:'Plan from: '+a.output})).output" }
23
+ • Parallel example: { workflowScript: "const [a,b] = await runs.all([{key:'correctness',agent:'agent-a',task:'Review correctness'},{key:'tests',agent:'agent-b',task:'Review tests'}]); return {correctness:a.output,tests:b.output}" }
24
+ • Optional context is "fresh" or "fork". timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls; evidence levels end at verified, and acceptance.review.required requests independent writer review.
26
25
  • Durable mission attachment is automatic by default. Use missionId to attach an existing mission, mission:{...} to override auto-create, or mission:false for ephemeral work.
27
26
 
28
27
  MANAGEMENT / CONTROL (use action; omit execution fields):
29
- • list, get, models, create, update, delete, eject, disable, enable, reset, doctor, grant-spawn-budget, worktree.discard, mission.create/list/show/update/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available.
28
+ • list, get, models, children.list, create, update, delete, eject, disable, enable, reset, doctor, grant-spawn-budget, worktree.discard, refine/refine.show/refine.rollback, mission.create/list/show/update/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available.
30
29
  • status, interrupt, stop, resume, and steer manage live or persisted runs. Use status view:"fleet" for an overview or view:"transcript" with id and optional index to tail output.
31
30
  • { action: "append-step", id: "...", step: {agent:"agent-c", task:"Use {previous}"} } appends one step to an already-running durable legacy chain. step is control-only, not an execution mode.
32
31
  • approve-checkpoint and reject-checkpoint decide a paused durable legacy chain checkpoint.
33
- • Create durable project schedules with { action:"schedule.create", id?, name?, at:"+10m" | ISO, agent, task? } or { every:"6h", workflowScript }. Manage them with schedule.list/show/history/pause/resume/run/run-due/delete. This first slice supports fixed intervals; calendar schedules and schedule mission attachment are deferred.
32
+ • Create durable project schedules with { action:"schedule.create", id?, name?, at:"+10m" | ISO, workflowScript:"return runs.run('main', {agent:'worker', task:'...'})" } or { every:"6h", workflowScript:"..." }. Manage them with schedule.list/show/history/pause/resume/run/run-due/delete. This first slice supports fixed intervals; calendar schedules and schedule mission attachment are deferred.
34
33
 
35
34
  ${SUBAGENT_SAFETY_GUIDANCE}`;
36
35
 
37
- export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Delegate one child with { agent, task } or orchestrate with { workflowScript }; omit action. workflowScript is the sole public orchestration surface.
36
+ export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run subagents only through { workflowScript }; omit action. Use action only for management/control actions.
38
37
 
39
38
  EXECUTE:
40
39
  • Call { action:"list" } first and use only executable/non-disabled agents.
41
- • SINGLE {agent, task?}; SCRIPT {workflowScript:"..."} with stable-key runs.run for one child and runs.all for parallel work. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on a direct single child or runs.run/runs.all item for managed isolation instead of manual Git worktrees. Scripts start async by default; async:false is the foreground escape hatch and auto-enables a same-repo live chat card unless chatProgress is off/terminal/milestones.
40
+ • SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and runs.all for parallel work. Use {action:"children.list"} for the last 10 retained children in this parent session, then runs.run(key,{resume:"run-id",task:"follow-up"}) to continue one with its stored contract. Mission-attached workflows also get async state.get/state.set for durable JSON state; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on the workflow or runs.run/runs.all item for managed isolation. Scripts start async by default; async:false is the foreground escape hatch and auto-enables a same-repo live chat card unless chatProgress is off.
42
41
  • Example: {workflowScript:"const [a,b]=await runs.all([{key:'a',agent:'agent-a',task:'Implement A',worktree:true},{key:'b',agent:'agent-b',task:'Implement B',worktree:true}]); return [a.output,b.output]"}
43
- • context can be fresh or fork. timeoutMs/maxRuntimeMs apply to foreground and async runs. Omit acceptance for reviewer/read-only calls.
42
+ • context can be fresh or fork. timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls.
44
43
 
45
44
  MANAGE / CONTROL:
46
- • Use action without execution fields for list/get/models/authoring, mission, watchdog, status, interrupt, stop, resume, steer, scheduling, diagnostics, and other management actions.
45
+ • Use action without execution fields for list/get/models/authoring, refine/refine.show/refine.rollback, mission, watchdog, status, interrupt, stop, resume, steer, script-only scheduling, diagnostics, and other management actions.
47
46
  • append-step uses step:{...} only for an already-running durable legacy chain; step is not an execution mode.
48
47
 
49
48
  ASYNC / SAFETY:
@@ -51,6 +50,7 @@ ASYNC / SAFETY:
51
50
  • Ordinary children are not orchestrators. Keep one writer per cwd/worktree and use fresh read-only reviewers for independent checks.
52
51
  • Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, and {action:"status",id:"..."}.`;
53
52
 
53
+
54
54
  function isToolDescriptionMode(value: unknown): value is ToolDescriptionMode {
55
55
  return value === "full" || value === "compact" || value === "custom";
56
56
  }