pi-subagents 0.65.1 → 0.66.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 (85) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/agents/researcher.md +23 -13
  4. package/docs/agents.md +16 -2
  5. package/docs/configuration.md +18 -0
  6. package/docs/extension-api.md +91 -0
  7. package/docs/models.md +58 -1
  8. package/docs/observability.md +42 -2
  9. package/docs/tool-reference.md +3 -3
  10. package/docs/workflows.md +14 -7
  11. package/package.json +2 -1
  12. package/skills/pi-subagents/references/execution-controls.md +14 -1
  13. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  14. package/src/agents/advertised-agent-prompt.ts +63 -0
  15. package/src/agents/agent-management.ts +14 -1
  16. package/src/agents/agent-serializer.ts +2 -0
  17. package/src/agents/agents.ts +8 -0
  18. package/src/api/shared-types.ts +1 -1
  19. package/src/api/workflow-resources.ts +6 -0
  20. package/src/extension/index.ts +40 -2
  21. package/src/extension/public-execution.ts +0 -1
  22. package/src/extension/rpc.ts +4 -21
  23. package/src/extension/schemas.ts +8 -6
  24. package/src/extension/tool-description.ts +6 -5
  25. package/src/intercom/native-supervisor-channel.ts +82 -54
  26. package/src/runs/background/active-async-capacity.ts +18 -18
  27. package/src/runs/background/async-job-tracker.ts +35 -3
  28. package/src/runs/background/async-status-snapshot.ts +10 -12
  29. package/src/runs/background/async-status.ts +17 -9
  30. package/src/runs/background/auto-drain.ts +40 -29
  31. package/src/runs/background/chain-root-attachment.ts +8 -0
  32. package/src/runs/background/control-channel.ts +78 -44
  33. package/src/runs/background/notify.ts +86 -12
  34. package/src/runs/background/owned-process-tree.ts +6 -6
  35. package/src/runs/background/process-terminal.ts +23 -23
  36. package/src/runs/background/run-child-session.ts +60 -32
  37. package/src/runs/background/run-status.ts +75 -5
  38. package/src/runs/background/runner-aliases.ts +18 -7
  39. package/src/runs/background/runner-child-launch.ts +86 -0
  40. package/src/runs/background/stale-run-reconciler.ts +3 -1
  41. package/src/runs/background/subagent-runner.ts +412 -209
  42. package/src/runs/background/subagent-wait.ts +3 -0
  43. package/src/runs/background/wait-completions.ts +4 -0
  44. package/src/runs/foreground/async-steering-action.ts +19 -0
  45. package/src/runs/foreground/execution.ts +101 -25
  46. package/src/runs/foreground/subagent-executor.ts +474 -197
  47. package/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
  48. package/src/runs/foreground/workflow-foreground-steering.ts +56 -2
  49. package/src/runs/shared/acceptance.ts +2 -2
  50. package/src/runs/shared/agent-contract.ts +1 -1
  51. package/src/runs/shared/async-status-projection.ts +47 -47
  52. package/src/runs/shared/child-hooks.ts +151 -2
  53. package/src/runs/shared/child-launch.ts +18 -13
  54. package/src/runs/shared/child-session.ts +42 -6
  55. package/src/runs/shared/child-tool-plan.ts +2 -2
  56. package/src/runs/shared/completion-evidence.ts +2 -2
  57. package/src/runs/shared/completion-guard.ts +1 -0
  58. package/src/runs/shared/host-step-status.ts +11 -11
  59. package/src/runs/shared/llm-intent-arbiter.ts +10 -9
  60. package/src/runs/shared/model-fallback.ts +10 -6
  61. package/src/runs/shared/nested-events.ts +5 -5
  62. package/src/runs/shared/orca-progress-tabs.ts +6 -0
  63. package/src/runs/shared/parallel-handoff.ts +57 -12
  64. package/src/runs/shared/parallel-utils.ts +2 -2
  65. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  66. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  67. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  68. package/src/runs/shared/run-fanout-budget.ts +8 -8
  69. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  70. package/src/runs/shared/subagent-prompt-runtime.ts +13 -3
  71. package/src/runs/shared/worktree-setup-command.ts +190 -0
  72. package/src/runs/shared/worktree.ts +332 -204
  73. package/src/shared/types.ts +81 -60
  74. package/src/shared/utils.ts +7 -2
  75. package/src/shared/workflow-child-permit.ts +18 -13
  76. package/src/tui/fleet.ts +11 -5
  77. package/src/tui/render.ts +23 -5
  78. package/src/workflows/chat-progress.ts +3 -3
  79. package/src/workflows/scripted-workflow.ts +38 -10
  80. package/src/workflows/workflow-checklist.ts +11 -15
  81. package/src/workflows/workflow-child-summary.ts +57 -8
  82. package/src/workflows/workflow-preflight.ts +19 -19
  83. package/src/workflows/workflow-receipt.ts +3 -3
  84. package/src/workflows/workflow-resources.ts +96 -21
  85. package/src/workflows/workflow-settlement.ts +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.65.1",
3
+ "version": "0.66.0",
4
4
  "description": "Pi extension for single-agent delegation and scripted multi-agent workflows",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -13,6 +13,7 @@
13
13
  "./agents": "./src/api/agents.ts",
14
14
  "./delegation": "./src/api/delegation.ts",
15
15
  "./capability-ceiling": "./src/api/capability-ceiling.ts",
16
+ "./workflow-resources": "./src/api/workflow-resources.ts",
16
17
  "./preflight": "./src/api/preflight.ts",
17
18
  "./control-channel": "./src/api/control-channel.ts",
18
19
  "./intercom-bridge": "./src/api/intercom-bridge.ts",
@@ -258,7 +258,7 @@ A cooperating terminal runtime can register read-only external records through `
258
258
 
259
259
  ### Scheduled subagent runs
260
260
 
261
- Schedules are durable project records under `.pi/subagents/schedules/`. They are enabled by default; set `{ "scheduledRuns": { "enabled": false } }` in `~/.pi/agent/extensions/subagent/config.json` to disable them. Only schedule explicit work the user asked for.
261
+ Schedules are durable project records under `.pi/subagents/schedules/`. They are enabled by default; set `{ "scheduledRuns": { "enabled": false } }` in `~/.pi/agent/extensions/subagent/config.json` to disable them. Only schedule explicit work the user asked for. To keep schedules outside the project repository, set `{ "scheduledRuns": { "storeRoot": "~/.pi/subagent-schedules" } }` in the same config: `storeRoot` accepts an absolute path or a `~/`-prefixed path, and records land under `<storeRoot>/<sha256(path.resolve(cwd)) first 20 hex>/<scheduleId>/`.
262
262
 
263
263
  ```typescript
264
264
  // One-shot reviewer
@@ -327,6 +327,19 @@ subagent({ action: "steer", id: "abc123", message: "Focus on the failing test."
327
327
 
328
328
  The action waits up to three seconds for the child Pi session to accept the correlated user input and returns a request id with `delivered`, `scheduled`, `pending`, `partial`, `recovered`, or `failed` plus per-child states. Indexed pending children return `scheduled` immediately. Only a top-level single-child run may automatically interrupt after a missed acknowledgment and recover after confirmed pause within a further 15 seconds. Recovery preserves the original child contract and only its remaining deadline, turn, and tool budgets. If the session is missing, a budget is exhausted, the pause cannot be confirmed, or replacement launch fails, the source remains paused when pausing succeeded and the action returns the exact failure. Chain, parallel, and nested runs never auto-interrupt; inspect their per-child outcomes and handle failures explicitly. A late acknowledgment is recorded and cannot cancel committed recovery.
329
329
 
330
+ Steering supports three delivery modes via the `mode` parameter (`steer` is the default):
331
+
332
+ - `mode: "steer"` — interrupt the child at the next safe point of its current turn and deliver the message.
333
+ - `mode: "follow_up"` — do not interrupt; queue input through Pi's native follow-up path for the next turn boundary. Eligible completed retained workflow children (single-step runs in state `complete` with a stored session file) receive the message as a revival brief (`queueRevivalBrief`) when they are revived; paused children reject follow-up steering outright. The 20-message queue limit applies to retained revival briefs, not live follow-up input.
334
+ - `mode: "auto"` — same next-safe-point delivery path as `steer`, but without the automatic pause-and-revive recovery after a missed acknowledgment.
335
+
336
+ ```typescript
337
+ subagent({ action: "steer", id: "abc123", mode: "follow_up", message: "After this step, also validate the config file." })
338
+ subagent({ action: "steer", id: "abc123", mode: "auto", message: "Switch to the failing test now." })
339
+ ```
340
+
341
+ Direct input acceptance returns `delivered`, not proof of model compliance. A live follow-up acknowledgment reports `queued`, meaning Pi accepted it into its follow-up queue, not that it was delivered. The runtime does not provide a later correlated live queued-to-delivered receipt.
342
+
330
343
  ## Watchdog
331
344
 
332
345
  The subagent watchdog is an **opt-in** adversarial change reviewer. It is not the
@@ -96,6 +96,7 @@ A minimal agent file looks like this:
96
96
  name: my-agent
97
97
  package: code-analysis
98
98
  description: What this agent does
99
+ advertise: true
99
100
  aliases: developer, coder
100
101
  model: provider/model-id
101
102
  thinking: high
@@ -111,7 +112,7 @@ skillPath: ./skills, ../shared-skills
111
112
  Your system prompt here.
112
113
  ```
113
114
 
114
- That is only a starting point. Omit `package` for the traditional unqualified runtime name. Common optional fields include:
115
+ That is only a starting point. Omit `package` for the traditional unqualified runtime name. Set `advertise: true` only when the parent should receive this agent's name and description before deciding whether to delegate; advertisement is off by default. Common optional fields include:
115
116
  - `defaultProgress`
116
117
  - `defaultReads`
117
118
  - `output`
@@ -0,0 +1,63 @@
1
+ import { Buffer } from "node:buffer";
2
+ import type { ResolvedSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
3
+ import { isAgentAllowedByCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
4
+ import type { AgentConfig } from "./agents.ts";
5
+
6
+ const MAX_ADVERTISED_AGENTS = 16;
7
+ const MAX_CATALOG_BYTES = 12_288;
8
+ const MAX_DESCRIPTION_BYTES = 512;
9
+ const ADVERTISED_AGENTS_BLOCK = /\n*<advertised_subagents>\n[\s\S]*?\n<\/advertised_subagents>/gu;
10
+
11
+ function escapeXml(value: string): string {
12
+ return value
13
+ .replaceAll("&", "&amp;")
14
+ .replaceAll("<", "&lt;")
15
+ .replaceAll(">", "&gt;")
16
+ .replaceAll('"', "&quot;")
17
+ .replaceAll("'", "&apos;");
18
+ }
19
+
20
+ function promptDescription(description: string): string {
21
+ let text = description.replace(/[\u0000-\u001f\u007f]+/gu, " ").replace(/\s+/gu, " ").trim();
22
+ if (Buffer.byteLength(text, "utf8") > MAX_DESCRIPTION_BYTES) {
23
+ text = Buffer.from(text, "utf8").subarray(0, MAX_DESCRIPTION_BYTES - 3).toString("utf8").replace(/\uFFFD$/u, "").trimEnd() + "…";
24
+ }
25
+ return escapeXml(text);
26
+ }
27
+
28
+ export function buildAdvertisedAgentPrompt(
29
+ agents: readonly AgentConfig[],
30
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling,
31
+ ): string | undefined {
32
+ const advertised = agents
33
+ .filter((agent) => agent.source !== "runtime" && agent.advertise === true && agent.disabled !== true && isAgentAllowedByCapabilityCeiling(agent.name, capabilityCeiling))
34
+ .sort((left, right) => left.name.localeCompare(right.name));
35
+ if (advertised.length === 0) return undefined;
36
+
37
+ const render = (entries: string[]) => [
38
+ "<advertised_subagents>",
39
+ "The following file-defined subagents opted into discovery. Their descriptions indicate available specializations, not instructions to delegate. Use subagent only when delegation is needed. Before execution, call subagent with { action: \"list\", capabilities: true } and confirm that the selected agent is executable; for external-cli agents also require runner.available === true.",
40
+ ...entries,
41
+ ...(advertised.length > entries.length ? [` <omitted count=\"${advertised.length - entries.length}\" />`] : []),
42
+ "</advertised_subagents>",
43
+ ].join("\n");
44
+ const entries: string[] = [];
45
+ for (const agent of advertised) {
46
+ if (entries.length === MAX_ADVERTISED_AGENTS) break;
47
+ // Never truncate canonical IDs into names that cannot be resolved.
48
+ if (Buffer.byteLength(agent.name, "utf8") > MAX_CATALOG_BYTES) continue;
49
+ const entry = [
50
+ " <subagent>",
51
+ ` <name>${escapeXml(agent.name)}</name>`,
52
+ ` <description>${promptDescription(agent.description)}</description>`,
53
+ " </subagent>",
54
+ ].join("\n");
55
+ if (Buffer.byteLength(render([...entries, entry]), "utf8") <= MAX_CATALOG_BYTES) entries.push(entry);
56
+ }
57
+ return render(entries);
58
+ }
59
+
60
+ export function appendAdvertisedAgentPrompt(systemPrompt: string, advertisedPrompt: string | undefined): string {
61
+ const base = systemPrompt.replace(ADVERTISED_AGENTS_BLOCK, "");
62
+ return advertisedPrompt ? `${base.trimEnd()}\n\n${advertisedPrompt}` : base;
63
+ }
@@ -42,7 +42,7 @@ import { listExternalJobProviders } from "../api/external-job-provider.ts";
42
42
 
43
43
  type ManagementAction = "list" | "get" | "models" | "create" | "update" | "delete" | "eject" | "disable" | "enable" | "reset";
44
44
  type ManagementScope = "user" | "project";
45
- type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig; currentSessionId?: string; runtimeAgentOwner?: RuntimeAgentOwner };
45
+ type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig; currentSessionId?: string; runtimeAgentOwner?: RuntimeAgentOwner; onAgentsChanged?: () => void };
46
46
 
47
47
  interface ManagementParams {
48
48
  action?: string;
@@ -348,6 +348,7 @@ export function preservedAgentFrontmatterFields(agent: AgentConfig, cfg: Record<
348
348
  if (hasKey(cfg, "name")) changed("name");
349
349
  if (hasKey(cfg, "package")) changed("package");
350
350
  if (hasKey(cfg, "description")) changed("description");
351
+ if (hasKey(cfg, "advertise")) changed("advertise");
351
352
  if (hasKey(cfg, "aliases")) changed("alias", "aliases");
352
353
  if (hasKey(cfg, "systemPrompt")) changed("systemPrompt");
353
354
  if (hasKey(cfg, "runner")) changed("runner");
@@ -415,6 +416,11 @@ function parseTools(raw: string): { tools?: string[]; mcpDirectTools?: string[]
415
416
  }
416
417
 
417
418
  function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): string | undefined {
419
+ if (hasKey(cfg, "advertise")) {
420
+ if (cfg.advertise === "") delete target.advertise;
421
+ else if (typeof cfg.advertise === "boolean") target.advertise = cfg.advertise;
422
+ else return "config.advertise must be a boolean or empty string when provided.";
423
+ }
418
424
  if (hasKey(cfg, "aliases")) {
419
425
  if (cfg.aliases === false || cfg.aliases === "") delete target.aliases;
420
426
  else if (typeof cfg.aliases === "string") {
@@ -1176,6 +1182,7 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
1176
1182
  const sw = skillsWarning(ctx.cwd, agent);
1177
1183
  if (sw) warnings.push(sw);
1178
1184
  fs.writeFileSync(targetPath, serializeAgent(agent), "utf-8");
1185
+ ctx.onAgentsChanged?.();
1179
1186
  return result([`Created agent '${runtimeName}' at ${targetPath}.`, ...warnings].join("\n"));
1180
1187
  }
1181
1188
 
@@ -1241,6 +1248,7 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
1241
1248
  updated.filePath = renamed.filePath!;
1242
1249
  }
1243
1250
  fs.writeFileSync(updated.filePath, serializeAgent(updated, { preserveFrontmatterFields }), "utf-8");
1251
+ ctx.onAgentsChanged?.();
1244
1252
  const headline = updated.name === oldName
1245
1253
  ? `Updated agent '${updated.name}' at ${updated.filePath}.`
1246
1254
  : `Updated agent '${oldName}' to '${updated.name}' at ${updated.filePath}.`;
@@ -1254,6 +1262,7 @@ function handleDelete(params: ManagementParams, ctx: ManagementContext): AgentTo
1254
1262
  if ("content" in targetOrError) return targetOrError;
1255
1263
  const target = targetOrError;
1256
1264
  fs.unlinkSync(target.filePath);
1265
+ ctx.onAgentsChanged?.();
1257
1266
  return result(`Deleted agent '${target.name}' at ${target.filePath}.`);
1258
1267
  }
1259
1268
 
@@ -1292,6 +1301,7 @@ function handleEject(params: ManagementParams, ctx: ManagementContext): AgentToo
1292
1301
  return result(`Failed to read source agent at ${source.filePath}: ${message}`, true);
1293
1302
  }
1294
1303
  fs.writeFileSync(targetPath, content, "utf-8");
1304
+ ctx.onAgentsChanged?.();
1295
1305
  return result(`Ejected agent '${runtimeName}' from ${source.source} to ${scope} scope at ${targetPath}. Edit it there to customize; it shadows the bundled ${source.source} agent of the same name.`);
1296
1306
  }
1297
1307
 
@@ -1314,6 +1324,7 @@ function handleDisable(params: ManagementParams, ctx: ManagementContext): AgentT
1314
1324
  const settingsPath = mergeBuiltinAgentOverride(ctx.cwd, runtimeName, scope, { disabled: true });
1315
1325
  const after = resolveEffectiveAgent(discoverAgentsAll(ctx.cwd), raw).agent;
1316
1326
  if (after?.disabled === true) {
1327
+ ctx.onAgentsChanged?.();
1317
1328
  return result(`Disabled agent '${runtimeName}' via ${scope} settings override at ${settingsPath}. It is now hidden from runtime discovery and { action: "list" }.`);
1318
1329
  }
1319
1330
  return result(`Wrote a disabled override for '${runtimeName}' at ${settingsPath}, but the agent is still enabled. A higher-precedence ${after?.override?.scope ?? "project"} override is likely winning. Try agentScope: '${after?.override?.scope ?? "project"}'.`, true);
@@ -1338,6 +1349,7 @@ function handleEnable(params: ManagementParams, ctx: ManagementContext): AgentTo
1338
1349
  const { path: settingsPath, removed } = removeBuiltinAgentOverrideFields(ctx.cwd, runtimeName, scope, ["disabled"]);
1339
1350
  const after = resolveEffectiveAgent(discoverAgentsAll(ctx.cwd), raw).agent;
1340
1351
  if (after && after.disabled !== true) {
1352
+ if (removed) ctx.onAgentsChanged?.();
1341
1353
  if (removed) return result(`Enabled agent '${runtimeName}' (removed disabled override at ${settingsPath}).`);
1342
1354
  return result(`Agent '${runtimeName}' is already enabled.`);
1343
1355
  }
@@ -1385,6 +1397,7 @@ function handleReset(params: ManagementParams, ctx: ManagementContext): AgentToo
1385
1397
  return result(`Agent '${runtimeName}' has no ${scope} customization to reset.${note} It is at its bundled ${bundled.source} default.`);
1386
1398
  }
1387
1399
  lines.push(`Reset agent '${runtimeName}' to its bundled ${bundled.source} default.`);
1400
+ ctx.onAgentsChanged?.();
1388
1401
  return result(lines.join("\n"));
1389
1402
  }
1390
1403
 
@@ -6,6 +6,7 @@ export const KNOWN_FIELDS = new Set([
6
6
  "name",
7
7
  "package",
8
8
  "description",
9
+ "advertise",
9
10
  "alias",
10
11
  "aliases",
11
12
  "tools",
@@ -62,6 +63,7 @@ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptio
62
63
  lines.push(`name: ${frontmatterNameForConfig(config)}`);
63
64
  if (config.packageName) lines.push(`package: ${config.packageName}`);
64
65
  lines.push(`description: ${config.description}`);
66
+ if (config.advertise === true || preserve("advertise")) lines.push(`advertise: ${config.advertise === true ? "true" : "false"}`);
65
67
  const aliasesValue = joinComma(config.aliases);
66
68
  if (aliasesValue || preserve("alias", "aliases")) lines.push(`aliases: ${aliasesValue ?? ""}`);
67
69
 
@@ -135,6 +135,7 @@ export interface AgentConfig {
135
135
  packageSourceVersion?: string;
136
136
  packageSourceRoot?: string;
137
137
  description: string;
138
+ advertise?: boolean;
138
139
  aliases?: string[];
139
140
  tools?: string[];
140
141
  excludeTools?: string[];
@@ -1981,6 +1982,12 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
1981
1982
 
1982
1983
  const runner = parseAgentRunnerFrontmatter(frontmatter.runner, localName);
1983
1984
  validateExternalRunnerProfile(frontmatter, localName, runner);
1985
+ let advertise: boolean | undefined;
1986
+ if (frontmatter.advertise !== undefined) {
1987
+ if (frontmatter.advertise === "true") advertise = true;
1988
+ else if (frontmatter.advertise === "false") advertise = false;
1989
+ else throw new Error(`Agent '${localName}' has invalid advertise frontmatter; expected true or false.`);
1990
+ }
1984
1991
  const rawTools = parseFrontmatterList(frontmatter.tools);
1985
1992
  const parsedTools = splitToolList(rawTools);
1986
1993
  const tools = parsedTools.tools ?? [];
@@ -2105,6 +2112,7 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
2105
2112
  ...(packageSource?.packageVersion ? { packageSourceVersion: packageSource.packageVersion } : {}),
2106
2113
  ...(packageSource?.packageRoot ? { packageSourceRoot: packageSource.packageRoot } : {}),
2107
2114
  description: frontmatter.description,
2115
+ ...(advertise !== undefined ? { advertise } : {}),
2108
2116
  ...(aliases !== undefined ? { aliases } : {}),
2109
2117
  ...(rawTools !== undefined ? { tools } : {}),
2110
2118
  ...(excludeTools !== undefined ? { excludeTools } : {}),
@@ -23,5 +23,5 @@ export {
23
23
  type ManagedWorktreeProvider,
24
24
  type WorktreeNaming,
25
25
  type WorktreeProvider,
26
- type WorkflowResourceProvenanceV1,
26
+ type WorkflowResourceProvenance,
27
27
  } from "../shared/types.ts";
@@ -0,0 +1,6 @@
1
+ export { registerWorkflowResource } from "../workflows/workflow-resources.ts";
2
+ export type {
3
+ RegisterWorkflowResourceInput,
4
+ WorkflowResourceDefinition,
5
+ WorkflowResourceRegistration,
6
+ } from "../workflows/workflow-resources.ts";
@@ -19,7 +19,8 @@ import * as path from "node:path";
19
19
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
20
20
  import { keyText, type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
21
21
  import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
22
- import { discoverAgentSnapshot, discoverAgents, type AgentConfig, type AgentScope } from "../agents/agents.ts";
22
+ import { clearAgentDiscoveryCache, discoverAgentSnapshot, discoverAgents, type AgentConfig, type AgentScope } from "../agents/agents.ts";
23
+ import { appendAdvertisedAgentPrompt, buildAdvertisedAgentPrompt } from "../agents/advertised-agent-prompt.ts";
23
24
  import { clearRuntimeAgentsForPi, listRuntimeAgentConfigs, mergeRuntimeAgents } from "../agents/runtime-agent-registry.ts";
24
25
  import { registerRuntimeAgentEventListener } from "../agents/runtime-agent-events.ts";
25
26
  import { ensureAccessibleDir } from "../shared/accessible-dir.ts";
@@ -489,7 +490,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
489
490
  }, run);
490
491
  };
491
492
 
492
- const supervisorChannel = createNativeSupervisorChannel(pi, state);
493
+ const supervisorChannel = createNativeSupervisorChannel(pi, state, {
494
+ getCurrentOwnerStates: () => executor.getCurrentSupervisorOwnerStates(),
495
+ });
493
496
  const waitSubscriptionManager = createWaitSubscriptionManager(pi, state);
494
497
  const mainWatchdog = registerMainWatchdog(pi);
495
498
  const resultDeliveryOwnership = createResultDeliveryOwnership(state);
@@ -531,6 +534,15 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
531
534
  resolveCapabilityCeiling: (sessionId) => resolveCurrentSubagentCapabilityCeiling(sessionId),
532
535
  });
533
536
  let refreshResultDelivery = () => {};
537
+ let advertisedAgents: AgentConfig[] = [];
538
+ let advertisedContext: Pick<ExtensionContext, "cwd" | "model"> | undefined;
539
+ const refreshAdvertisedAgents = () => {
540
+ advertisedAgents = [];
541
+ if (!advertisedContext) return;
542
+ clearAgentDiscoveryCache();
543
+ advertisedAgents = discoverAgents(advertisedContext.cwd, "both", advertisedContext.model?.provider).agents
544
+ .filter((agent) => agent.advertise === true);
545
+ };
534
546
  const hasResultDeliveryDemand = () => {
535
547
  if ([...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running")) return true;
536
548
  if (state.foregroundControls.size > 0) return true;
@@ -609,7 +621,16 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
609
621
  getSubagentSessionRoot,
610
622
  expandTilde,
611
623
  discoverAgents: discoverAgentsForRuntime,
624
+ onAgentsChanged: () => {
625
+ try {
626
+ refreshAdvertisedAgents();
627
+ } catch (error) {
628
+ // The mutation already persisted. Withdraw stale guidance, not its result.
629
+ console.error("Failed to refresh advertised agents; catalog withdrawn until refresh:", error);
630
+ }
631
+ },
612
632
  activateSupervisorTransport: () => supervisorChannel.activateTransport(),
633
+ findPendingAsks: (target) => supervisorChannel.findPendingAsks(target),
613
634
  refreshResultDelivery: () => refreshResultDelivery(),
614
635
  trackRetainedNestedRoute: undefined,
615
636
  };
@@ -778,6 +799,16 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
778
799
 
779
800
  pi.registerTool(tool);
780
801
 
802
+ pi.on("before_agent_start", (event, ctx) => {
803
+ const selectedTools = event.systemPromptOptions.selectedTools ?? pi.getActiveTools();
804
+ const sessionId = state.currentSessionId ?? resolveCurrentSessionId(ctx.sessionManager);
805
+ const advertisedPrompt = selectedTools.includes("subagent")
806
+ ? buildAdvertisedAgentPrompt(advertisedAgents, resolveCurrentSubagentCapabilityCeiling(sessionId))
807
+ : undefined;
808
+ const systemPrompt = appendAdvertisedAgentPrompt(event.systemPrompt, advertisedPrompt);
809
+ if (systemPrompt !== event.systemPrompt) return { systemPrompt };
810
+ });
811
+
781
812
  registerWaitTool(pi, state, waitToolConfig.enabled, waitSubscriptionManager, waitToolConfig.defaultTimeoutMs);
782
813
 
783
814
  pi.on("agent_end", async (_event, ctx) => {
@@ -912,6 +943,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
912
943
  const previousRuntimeSessionId = state.currentSessionId;
913
944
  resultDeliveryOwnership.claimPredecessor(previousSessionFile, previousRuntimeSessionId);
914
945
  state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
946
+ state.supervisorOwnerSessionId = ctx.sessionManager.getSessionId() || null;
915
947
  transitionResultDelivery();
916
948
  state.parentSessionFile = ctx.sessionManager.getSessionFile();
917
949
  state.trustedSessionFileRoot = state.parentSessionFile ? path.join(getAgentDir(), "sessions") : undefined;
@@ -1029,6 +1061,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
1029
1061
  promptTemplateBridge.dispose();
1030
1062
  state.widgetsSuspended = false;
1031
1063
  state.currentSessionId = null;
1064
+ state.supervisorOwnerSessionId = null;
1032
1065
  state.statusProjectionSessionId = null;
1033
1066
  state.parentSessionFile = null;
1034
1067
  parentSessionEnvValue = null;
@@ -1141,4 +1174,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
1141
1174
  }
1142
1175
  await herdrStatusBridge.flush();
1143
1176
  });
1177
+
1178
+ pi.on("session_start", (_event, ctx) => {
1179
+ advertisedContext = { cwd: ctx.cwd, model: ctx.model };
1180
+ refreshAdvertisedAgents();
1181
+ });
1144
1182
  }
@@ -73,7 +73,6 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
73
73
  }
74
74
  }
75
75
  if (params.baseRef !== undefined) {
76
- if (typeof params.baseRef !== "string") return { ok: false, error: "baseRef must be a valid Git ref.", mode: params.action === undefined ? "workflow" : "management" };
77
76
  try {
78
77
  normalizeWorktreeBaseRef(params.baseRef);
79
78
  } catch (error) {
@@ -643,27 +643,10 @@ function stopAsyncRun(
643
643
  message: `Stop requested for async run ${initialRunId}.`,
644
644
  };
645
645
  }
646
- try {
647
- deliverStopRequest({
648
- asyncDir: location.asyncDir,
649
- pid: initialStatus.pid,
650
- kill: options.kill,
651
- now: options.now,
652
- source: "rpc-stop",
653
- ...(child ? { targetIndex: child.index, childId: child.id } : {}),
654
- });
655
- } catch (error) {
656
- throw new SubagentRpcError("execution_failed", error instanceof Error ? error.message : String(error));
657
- }
658
- if (child) emitChildStopping(initialRunId, location.asyncDir, child);
659
- return {
660
- runId: initialRunId,
661
- asyncDir: location.asyncDir,
662
- previousState: initialStatus.state,
663
- state: "stopping",
664
- ...(child ? { childId: child.id } : {}),
665
- message: child ? `Stop requested for child ${child.id} in async run ${initialRunId}.` : `Stop requested for async run ${initialRunId}.`,
666
- };
646
+ // Workflow controls live in-process; a persisted run directory cannot restore them.
647
+ throw new SubagentRpcError("invalid_state", child
648
+ ? `Child '${child.id}' in workflow ${initialRunId} has no live stop callback available.`
649
+ : `Workflow ${initialRunId} has no live run controller available to stop.`);
667
650
  }
668
651
 
669
652
  let status;
@@ -77,6 +77,8 @@ const AcceptanceEvidenceKinds = [
77
77
  "manual-notes",
78
78
  ];
79
79
 
80
+ // Provider boolean branches intentionally overapproximate false-only runtime inputs.
81
+ // Restricted function-declaration converters only support string enum members.
80
82
  const AcceptanceOverride = Type.Unsafe({
81
83
  anyOf: [
82
84
  { type: "string", enum: ["auto", "attested", "checked"] },
@@ -89,10 +91,10 @@ const AcceptanceOverride = Type.Unsafe({
89
91
  {
90
92
  type: "string",
91
93
  },
92
- { type: "boolean", enum: [false] },
94
+ { type: "boolean" },
93
95
  { type: "object", additionalProperties: true },
94
96
  ],
95
- description: `Optional acceptance policy. Prefer an inline JSON object. JSON-encoded object strings are tolerated only during input normalization; invalid strings fail closed. Reviewer/read-only calls, omit acceptance. { level: "checked", evidence: ["commands-run", "changed-files"] }. Supported evidence kinds: ${AcceptanceEvidenceKinds.join(",")}. acceptance.review.required.`,
97
+ description: `Optional acceptance policy. false disables acceptance; true is invalid. Prefer an inline JSON object. JSON-encoded object strings are tolerated only during input normalization; invalid strings fail closed. Reviewer/read-only calls, omit acceptance. { level: "checked", evidence: ["commands-run", "changed-files"] }. Supported evidence kinds: ${AcceptanceEvidenceKinds.join(",")}. acceptance.review.required.`,
96
98
  });
97
99
 
98
100
  const AgentContractOverride = Type.Object({
@@ -149,7 +151,7 @@ const WorkflowPreflightOverride = Type.Object({
149
151
  version: Type.Integer({ minimum: 1, maximum: 1 }),
150
152
  coverage: Type.Optional(Type.String({ enum: ["complete", "partial"] })),
151
153
  lanes: Type.Array(WorkflowPreflightLane, { maxItems: 64 }),
152
- }, { additionalProperties: false, description: "Bounded display-only lane hints for workflow launch/status. V1 coverage mismatches warn but never change launch authority or execution." });
154
+ }, { additionalProperties: false, description: "Bounded display-only lane hints for workflow launch/status. Coverage mismatches warn but never change launch authority or execution." });
153
155
 
154
156
  // Parallel task item (within a parallel step)
155
157
  export const ParallelTaskSchema = Type.Object({
@@ -257,7 +259,7 @@ export const ChainItem = Type.Object({
257
259
  const MissionLaunchOverride = Type.Unsafe({
258
260
  anyOf: [
259
261
  { type: "object", additionalProperties: true },
260
- { type: "boolean", enum: [false] },
262
+ { type: "boolean" },
261
263
  ],
262
264
  });
263
265
  const MissionUpdateOverride = Type.Unsafe({ type: "object", additionalProperties: true });
@@ -317,7 +319,7 @@ const SubagentParamProperties = {
317
319
  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." })),
318
320
  target: Type.Optional(Type.String({ enum: ["main", "children", "child"], description: "Target for watchdog actions." })),
319
321
  focus: Type.Optional(Type.Boolean({ description: "Focus the new Herdr pane for inspector.open or project.open." })),
320
- thinking: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "boolean", enum: [false] }], description: "Thinking level for action='watchdog.configure' only (off/minimal/low/medium/high/xhigh/max, inherit, or false for off). Ignored on dispatch; set per-run child thinking with a suffix on the model string, e.g. model: 'provider/id:high'." })),
322
+ thinking: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "boolean" }], description: "Thinking level for action='watchdog.configure' only (off/minimal/low/medium/high/xhigh/max, inherit, or false for off; true is invalid). Ignored on dispatch; set per-run child thinking with a suffix on the model string, e.g. model: 'provider/id:high'." })),
321
323
  at: Type.Optional(Type.String({ description: "One-shot trigger for action='schedule.create': a relative delay such as '+10m' or an ISO timestamp with timezone." })),
322
324
  every: Type.Optional(Type.String({ description: "Fixed recurring interval for action='schedule.create', such as '30m', '6h', '2d', or '2w'." })),
323
325
  sessionOnly: Type.Optional(Type.Boolean()),
@@ -326,7 +328,7 @@ const SubagentParamProperties = {
326
328
  overlap: Type.Optional(Type.String({ enum: ["skip"], description: "Overlap policy. This slice supports skip only." })),
327
329
  catchUp: Type.Optional(Type.String({ enum: ["none", "latest"], description: "Missed occurrence policy for recurring schedules. Defaults to latest." })),
328
330
  missionId: Type.Optional(Type.String({ description: "Mission id." })),
329
- mission: Type.Optional(Type.Unsafe({ ...MissionLaunchOverride, description: "Mission object, or false for no mission. Set exactly one non-empty title or summary; objective and labels are optional. goal may only be true and then requires budget.tokens." })),
331
+ mission: Type.Optional(Type.Unsafe({ ...MissionLaunchOverride, description: "Mission object, or false for no mission; true is invalid. Set exactly one non-empty title or summary; objective and labels are optional. goal may only be true and then requires budget.tokens." })),
330
332
  missionUpdate: Type.Optional(Type.Unsafe({ ...MissionUpdateOverride, description: "Mission update: objective, goal false or {paused:boolean}, budget, summary, labels, decisions, artifacts, or delivery receipts." })),
331
333
  missionStatus: Type.Optional(Type.String({ description: "Mission status." })),
332
334
  missionScope: Type.Optional(Type.String({ description: "Mission list scope: project (default) or global pointer index." })),
@@ -11,11 +11,12 @@ const AGENT_SELECTION_GUIDANCE = "Before execution, call { action: \"list\", cap
11
11
  const WORKFLOW_RESUME_KEY_GUIDANCE = "Each workflow key identifies one result lane: use a new stable workflow key for every distinct retained resume pass; same-key calls are reused only when launch parameters are identical, and incompatible parameters are rejected.";
12
12
  const WORKFLOW_OUTPUT_BINDING_GUIDANCE = "For durable workflow child files, set output on runs.run/runs.all; task filename prose is not an output declaration, and return the child's outputReference, outputPathMapping, or artifactPaths instead of inventing a literal path.";
13
13
  const WORKFLOW_LANES_GUIDANCE = "For bounded parallel sequential chains, use runs.lanes([{key,stages:[{key,agent,task},{key,resume:'previous',task},...]}]); first stages run together, later stages sequence per lane, and the bounded board reports lane-local failures. Only an explicit structuredOutput.verdict === 'blocked' blocks a successful stage; reviewer prose is not parsed.";
14
+ const WORKTREE_BASE_REF_GUIDANCE = "baseRef must be HEAD or a supported named ref such as refs/heads/main; full 40/64-character commit IDs and revision expressions such as HEAD~1 are unsupported. Omitted baseRef defaults to HEAD resolved at worktree allocation. The source checkout must still be clean.";
14
15
  const WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE = "workflowScript rejects nested async function, arrow, and method helpers; use top-level await, plain helper functions that return runs.run(...), or explicit Promise chains instead.";
15
16
  const WORKFLOW_RESOURCE_GUIDANCE = "For permission/policy-extension interoperability, use an extension-owned named resource such as {workflow:'review',args:{task:'...'}} or {workflow:'run-ci',args:{command:'npm test'}}. The host resolves the script and authority internally so policy can distinguish it from raw workflowScript/workflowScriptPath; args are bounded plain data, and do not combine workflow with agent, task, workflowScript, or workflowScriptPath.";
16
- const WORKFLOW_HOST_GUIDANCE = "For permission-sensitive host calls, use an extension-owned resource such as {workflow:'run-ci',args:{command:'npm test'}}; raw workflowScript/workflowScriptPath have unknown resource provenance and cannot use runs.host. In a resource that grants it, await runs.host(key,{kind:'command',command,timeoutMs,output?,role?,provider?}). runs.host has no per-step cwd: commands and relative output paths use the workflow cwd; set cwd on the outer subagent request instead (for example, {cwd:'/path/to/worktree',workflowScript:'...'}), or put a trusted directory change in the command (for example, 'cd /path/to/worktree && npm test'). v1 supports only command steps; output is bounded and command failure fails the workflow.";
17
+ const WORKFLOW_HOST_GUIDANCE = "For permission-sensitive host calls, use an extension-owned resource such as {workflow:'run-ci',args:{command:'npm test'}}; raw workflowScript/workflowScriptPath have unknown resource provenance and cannot use runs.host. In a resource that grants it, await runs.host(key,{kind:'command',command,timeoutMs,output?,role?,provider?}). runs.host has no per-step cwd: commands and relative output paths use the workflow cwd; set cwd on the outer subagent request instead (for example, {cwd:'/path/to/worktree',workflowScript:'...'}), or put a trusted directory change in the command (for example, 'cd /path/to/worktree && npm test'). runs.host supports only command steps; output is bounded and command failure fails the workflow.";
17
18
 
18
- export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child, workflowScript for inline orchestration, workflowScriptPath to load a script from the request cwd, or a named workflow resource for permission/policy-aware execution. ${WORKFLOW_RESOURCE_GUIDANCE} ${AGENT_SELECTION_GUIDANCE} The script inputs are mutually exclusive. Use action:'validate' with either script input to check it without launching children. For multi-step or parallel work, make exactly one top-level subagent call with async:true; launch children only inside that workflow and do not make another top-level call for them. Use runs.run('key',{agent,task}) for one child, await runs.all([{key:'a',agent:'reviewer',task:'...'},{key:'b',agent:'reviewer',task:'...'}]) for ordinary parallel children, and read its ordered array result with indexes, destructuring, or .map(...), not by key property. ${WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE} ${WORKFLOW_LANES_GUIDANCE} ${WORKFLOW_HOST_GUIDANCE} ${EXTERNAL_CLI_RUNNER_GUIDANCE} ${SUBAGENT_FAILURE_RECOVERY_GUIDANCE} Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
19
+ export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child, workflowScript for inline orchestration, workflowScriptPath to load a script from the request cwd, or a named workflow resource for permission/policy-aware execution. ${WORKFLOW_RESOURCE_GUIDANCE} ${AGENT_SELECTION_GUIDANCE} The script inputs are mutually exclusive. Use action:'validate' with either script input to check it without launching children. For multi-step or parallel work, make exactly one top-level subagent call with async:true; launch children only inside that workflow and do not make another top-level call for them. Use runs.run('key',{agent,task}) for one child, await runs.all([{key:'a',agent:'reviewer',task:'...'},{key:'b',agent:'reviewer',task:'...'}]) for ordinary parallel children, and read its ordered array result with indexes, destructuring, or .map(...), not by key property. ${WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE} ${WORKFLOW_LANES_GUIDANCE} ${WORKFLOW_HOST_GUIDANCE} ${WORKTREE_BASE_REF_GUIDANCE} ${EXTERNAL_CLI_RUNNER_GUIDANCE} ${SUBAGENT_FAILURE_RECOVERY_GUIDANCE} Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
19
20
 
20
21
  export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflowScript call.";
21
22
 
@@ -49,7 +50,7 @@ EXECUTION:
49
50
  • ${AGENT_SELECTION_GUIDANCE}
50
51
  • When passing an explicit model to a child (on the call or a runs.run/runs.all item), first call { action: "models" } and copy an exact provider/id; bare ids resolve only when unique in the registry, and agent names (e.g. gpt-pro, advisor) are not model ids. Set per-run thinking with a suffix on the model string (e.g. provider/id:high; off/minimal/low/medium/high/xhigh/max); the suffix wins over the agent's thinking default. The thinking field only applies to action='watchdog.configure' and is ignored on dispatch.
51
52
  • SINGLE CHILD: { agent:"worker", task:"..." }. This structured form starts exactly one direct child. Fields such as model, context, cwd, worktree, output, budgets, acceptance, and async apply to that child. Do not combine agent/task with action, workflowScript, or workflowScriptPath.
52
- • WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel children. runs.all resolves to an ordered array, not a key map, so use results[0], array destructuring, or results.map((result) => result.output), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. 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. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Pass async:false only when the parent must block until completion, never for final reviews or gates. Same-repo blocking workflows default to a live in-chat card; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use {action:"children.list"} to list recent retained workflow children with resumable/not-resumable reasons. Resume only rows reported resumable. For a simple follow-up or implementation challenge, use {action:"resume", id:"run-id", message:"..."}. Resume keeps the stored agent/model/tool contract. If no resumable child is listed, launch a same-role fallback challenge and label it as fallback. Inside workflowScript, continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); workflow resumes wait for completed output, and loops must continue from each latest returned runId. Await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}) to guide a prior keyed child without exposing its run id; receipts are queued, delivered, missed, or failed. Always await or return runs.steer. 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. Set baseRef to a safe Git ref (default HEAD) to choose the managed worktree starting commit; the source checkout must still be clean. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.steer, runs.status, runs.ref/refs, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
53
+ • WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel children. runs.all resolves to an ordered array, not a key map, so use results[0], array destructuring, or results.map((result) => result.output), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. 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. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Pass async:false only when the parent must block until completion, never for final reviews or gates. Same-repo blocking workflows default to a live in-chat card; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use {action:"children.list"} to list recent retained workflow children with resumable/not-resumable reasons. Resume only rows reported resumable. For a simple follow-up or implementation challenge, use {action:"resume", id:"run-id", message:"..."}. Resume keeps the stored agent/model/tool contract. If no resumable child is listed, launch a same-role fallback challenge and label it as fallback. Inside workflowScript, continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); workflow resumes wait for completed output, and loops must continue from each latest returned runId. Await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}) to guide a prior keyed child without exposing its run id; receipts are queued, delivered, missed, or failed. Always await or return runs.steer. 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. ${WORKTREE_BASE_REF_GUIDANCE} A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.steer, runs.status, runs.ref/refs, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
53
54
  • ${WORKFLOW_LANES_GUIDANCE}
54
55
  • FILE SCRIPT: { workflowScriptPath:"workflows/review.js" }. Relative paths resolve against the request cwd. The host reads the file before the filesystem-free workflow sandbox starts. Do not combine this field with workflowScript.
55
56
  • 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" }
@@ -60,7 +61,7 @@ EXECUTION:
60
61
  MANAGEMENT / CONTROL (use action; omit execution fields):
61
62
  • validate checks workflowScript or workflowScriptPath syntax and statically decidable structure without launching children. list, get, models, guide, children.list, create, update, delete, eject, disable, enable, reset, status, debug.run, doctor, grant-spawn-budget, worktree.discard, worktree.cleanup (plan-only), lane.status, lane.recordMerge, lane.recordSupersession, refine/refine.show/refine.rollback, mission.create/list/show/update/resolve-decision/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available. Use {action:"guide", topic:"overview"} for packaged current-version help; topics are overview, workflows, agents, missions, observability, tool-reference, configuration, models, watchdog, and extension-api.
62
63
  • 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.
63
- • Create durable project schedules with { action:"schedule.create", id?, name?, sessionOnly?:true, at:"+10m" | ISO, baseRef?, workflowScript:"return runs.run('main', {agent:'worker', task:'...'})" }, or use workflowScriptPath instead. An optional baseRef selects the safe Git ref used by managed worktrees (default HEAD); the source checkout must still be clean. With sessionOnly:true, the schedule records the creating session file and only that session can restore or execute it; omitted/false preserves project-wide behavior. 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.
64
+ • Create durable project schedules with { action:"schedule.create", id?, name?, sessionOnly?:true, at:"+10m" | ISO, baseRef?, workflowScript:"return runs.run('main', {agent:'worker', task:'...'})" }, or use workflowScriptPath instead. An optional baseRef uses the same managed-worktree ref policy and resolves at allocation; the source checkout must still be clean. With sessionOnly:true, the schedule records the creating session file and only that session can restore or execute it; omitted/false preserves project-wide behavior. 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.
64
65
 
65
66
  ${SUBAGENT_SAFETY_GUIDANCE}`;
66
67
 
@@ -73,7 +74,7 @@ EXECUTE:
73
74
  • ${AGENT_SELECTION_GUIDANCE}
74
75
  • Passing an explicit model? Call {action:"models"} first and copy an exact provider/id; bare ids resolve only when unique in the registry; agent names (e.g. gpt-pro, advisor) are not model ids. Per-run thinking is a suffix on the model string (provider/id:high; off/minimal/low/medium/high/xhigh/max), and the suffix wins over the agent's thinking default; the thinking field only applies to action='watchdog.configure' and is ignored on dispatch.
75
76
  • SINGLE {agent:"worker",task:"..."} starts exactly one direct child. Fields apply to that child. Do not combine agent/task with action, workflowScript, or workflowScriptPath.
76
- • SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel work. runs.all resolves to an ordered array, not a key map; use results[0], destructuring, or results.map(...), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Await runs.steer(key,message,options?) to guide a prior keyed child; it returns queued, delivered, missed, or failed and never accepts a raw run id. Always await or return steering calls. Use {action:"children.list"} for recent retained workflow children and resume only rows reported resumable. Use {action:"resume",id:"run-id",message:"..."} for a simple follow-up or challenge; resume keeps the stored agent/model/tool contract. If none is resumable, launch a same-role fallback challenge and label it as fallback. Inside workflowScript use runs.run(key,{resume:"run-id",task:"follow-up"}) when the script must wait for completion and continue from the latest returned runId. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. 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; set baseRef to a safe Git ref (default HEAD) to choose the managed worktree starting commit. The source checkout must still be clean. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. async:false blocks the parent until completion and auto-enables a same-repo live chat card unless chatProgress is off; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off.
77
+ • SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel work. runs.all resolves to an ordered array, not a key map; use results[0], destructuring, or results.map(...), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Await runs.steer(key,message,options?) to guide a prior keyed child; it returns queued, delivered, missed, or failed and never accepts a raw run id. Always await or return steering calls. Use {action:"children.list"} for recent retained workflow children and resume only rows reported resumable. Use {action:"resume",id:"run-id",message:"..."} for a simple follow-up or challenge; resume keeps the stored agent/model/tool contract. If none is resumable, launch a same-role fallback challenge and label it as fallback. Inside workflowScript use runs.run(key,{resume:"run-id",task:"follow-up"}) when the script must wait for completion and continue from the latest returned runId. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. 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. ${WORKTREE_BASE_REF_GUIDANCE} Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. async:false blocks the parent until completion and auto-enables a same-repo live chat card unless chatProgress is off; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off.
77
78
  • ${WORKFLOW_LANES_GUIDANCE}
78
79
  • FILE SCRIPT {workflowScriptPath:"workflows/review.js"} loads the script on the host relative to the request cwd before sandbox execution. Do not combine it with workflowScript.
79
80
  • 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]"}