pi-subagents 0.50.0 → 0.52.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 (109) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/agents/oracle.md +3 -1
  3. package/agents/reviewer.md +1 -0
  4. package/agents/scout.md +2 -2
  5. package/agents/worker.md +2 -1
  6. package/async-retention-discovery-worker.mjs +180 -0
  7. package/docs/agents.md +40 -2
  8. package/docs/configuration.md +30 -12
  9. package/docs/extension-api.md +42 -1
  10. package/docs/models.md +2 -0
  11. package/docs/observability.md +41 -5
  12. package/docs/tool-reference.md +38 -39
  13. package/docs/workflows.md +171 -3
  14. package/package.json +4 -2
  15. package/skills/pi-subagents/SKILL.md +6 -4
  16. package/skills/pi-subagents/references/constraints-and-recipes.md +12 -6
  17. package/skills/pi-subagents/references/execution-controls.md +29 -15
  18. package/skills/pi-subagents/references/management-authoring-rpc.md +3 -3
  19. package/skills/pi-subagents/references/prompting-and-roles.md +4 -2
  20. package/src/agents/agent-management.ts +124 -351
  21. package/src/agents/agents.ts +157 -31
  22. package/src/agents/skills.ts +1 -1
  23. package/src/api/external-job-provider.ts +185 -0
  24. package/src/api/preflight.ts +36 -8
  25. package/src/api/shared-types.ts +2 -0
  26. package/src/extension/config.ts +3 -3
  27. package/src/extension/doctor.ts +3 -6
  28. package/src/extension/fanout-child.ts +2 -2
  29. package/src/extension/index.ts +166 -88
  30. package/src/extension/public-execution.ts +31 -2
  31. package/src/extension/schemas.ts +12 -35
  32. package/src/extension/tool-description.ts +35 -24
  33. package/src/inspectors/herdr/actions.ts +2 -2
  34. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  35. package/src/inspectors/herdr/project-panes.ts +2 -2
  36. package/src/intercom/native-supervisor-channel.ts +32 -10
  37. package/src/missions/lifecycle.ts +6 -1
  38. package/src/missions/store.ts +4 -9
  39. package/src/missions/workflow-state.ts +2 -2
  40. package/src/profiles/profiles.ts +3 -1
  41. package/src/runs/background/active-run-index.ts +31 -8
  42. package/src/runs/background/async-execution.ts +68 -40
  43. package/src/runs/background/async-job-tracker.ts +20 -4
  44. package/src/runs/background/async-resume.ts +47 -15
  45. package/src/runs/background/async-retention.ts +886 -0
  46. package/src/runs/background/async-status.ts +39 -53
  47. package/src/runs/background/chain-append.ts +3 -33
  48. package/src/runs/background/completion-dedupe.ts +5 -1
  49. package/src/runs/background/completion-replay.ts +22 -12
  50. package/src/runs/background/control-channel.ts +14 -68
  51. package/src/runs/background/fleet-view.ts +68 -19
  52. package/src/runs/background/index-segment.ts +59 -0
  53. package/src/runs/background/inspect-rpc.ts +443 -0
  54. package/src/runs/background/notify.ts +31 -5
  55. package/src/runs/background/result-files.ts +158 -90
  56. package/src/runs/background/result-watcher.ts +116 -20
  57. package/src/runs/background/resume-guidance.ts +8 -5
  58. package/src/runs/background/retained-children.ts +13 -3
  59. package/src/runs/background/run-id-query.ts +7 -0
  60. package/src/runs/background/run-id-resolver.ts +11 -9
  61. package/src/runs/background/run-status.ts +27 -18
  62. package/src/runs/background/scheduled-runs.ts +24 -4
  63. package/src/runs/background/stale-run-reconciler.ts +6 -3
  64. package/src/runs/background/steering.ts +11 -1
  65. package/src/runs/background/subagent-runner.ts +253 -140
  66. package/src/runs/background/subagent-wait.ts +8 -8
  67. package/src/runs/background/terminal-run-index.ts +129 -0
  68. package/src/runs/background/wait-completions.ts +21 -4
  69. package/src/runs/background/wait-subscriptions.ts +81 -2
  70. package/src/runs/foreground/async-steering-action.ts +21 -14
  71. package/src/runs/foreground/execution.ts +3 -1
  72. package/src/runs/foreground/subagent-executor.ts +666 -1579
  73. package/src/runs/foreground/workflow-detach-reconcile.ts +194 -0
  74. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  75. package/src/runs/shared/acceptance.ts +4 -4
  76. package/src/runs/shared/chain-outputs.ts +1 -3
  77. package/src/runs/shared/external-job-bridge.ts +444 -0
  78. package/src/runs/shared/external-job-runner.ts +286 -0
  79. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  80. package/src/runs/shared/model-fallback.ts +98 -38
  81. package/src/runs/shared/orca-progress-tabs.ts +84 -22
  82. package/src/runs/shared/parallel-handoff.ts +46 -4
  83. package/src/runs/shared/parallel-utils.ts +4 -15
  84. package/src/runs/shared/permissions.ts +5 -1
  85. package/src/runs/shared/pi-args.ts +8 -1
  86. package/src/runs/shared/session-lease.ts +0 -6
  87. package/src/runs/shared/subagent-control.ts +26 -4
  88. package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
  89. package/src/runs/shared/workflow-graph.ts +1 -23
  90. package/src/runs/shared/worktree.ts +14 -2
  91. package/src/shared/atomic-json.ts +22 -2
  92. package/src/shared/capacity-resilient-json.ts +102 -0
  93. package/src/shared/completion-owner.ts +14 -0
  94. package/src/shared/file-system-retry.ts +49 -1
  95. package/src/shared/fork-context.ts +42 -0
  96. package/src/shared/prompt-resources.ts +0 -40
  97. package/src/shared/settings.ts +3 -27
  98. package/src/shared/types.ts +71 -26
  99. package/src/shared/utils.ts +8 -0
  100. package/src/shared/watch-strategy.ts +10 -0
  101. package/src/slash/slash-bridge.ts +2 -1
  102. package/src/slash/slash-commands.ts +29 -3
  103. package/src/tui/fleet.ts +63 -15
  104. package/src/watchdog/change-signature.ts +1 -1
  105. package/src/watchdog/lsp-diagnostics.ts +1 -0
  106. package/src/workflows/chat-progress.ts +2 -2
  107. package/src/workflows/scripted-workflow.ts +220 -67
  108. package/src/runs/foreground/chain-clarify.ts +0 -1354
  109. package/src/runs/foreground/chain-execution.ts +0 -1581
@@ -8,7 +8,12 @@ export interface PublicSubagentExecutionParams {
8
8
  parallel?: unknown;
9
9
  concurrency?: unknown;
10
10
  chainDir?: unknown;
11
+ chainName?: unknown;
12
+ config?: unknown;
11
13
  workflowScript?: unknown;
14
+ isolation?: unknown;
15
+ worktree?: unknown;
16
+ async?: unknown;
12
17
  output?: unknown;
13
18
  resume?: unknown;
14
19
  clarify?: unknown;
@@ -26,7 +31,18 @@ export type PublicSubagentExecutionNormalization<T> =
26
31
  * Enforce the public execution cutover before requests reach the executor.
27
32
  * Internal runs.run children and structured owned delegation bypass this boundary.
28
33
  */
29
- export function normalizePublicSubagentExecution<T extends PublicSubagentExecutionParams>(params: T): PublicSubagentExecutionNormalization<T> {
34
+ export function normalizePublicSubagentExecution<T extends PublicSubagentExecutionParams>(params: T, options: { asyncByDefault?: boolean } = {}): PublicSubagentExecutionNormalization<T> {
35
+ if (params.isolation !== undefined) {
36
+ if (params.isolation !== "none" && params.isolation !== "worktree") {
37
+ return { ok: false, error: "isolation must be 'none' or 'worktree'.", mode: params.workflowScript !== undefined ? "workflow" : "management" };
38
+ }
39
+ const isolationWorktree = params.isolation === "worktree";
40
+ if (params.worktree !== undefined && params.worktree !== isolationWorktree) {
41
+ return { ok: false, error: `isolation '${params.isolation}' conflicts with worktree: ${String(params.worktree)}.`, mode: params.workflowScript !== undefined ? "workflow" : "management" };
42
+ }
43
+ const { isolation: _isolation, ...normalizedParams } = params;
44
+ params = { ...normalizedParams, worktree: isolationWorktree } as T;
45
+ }
30
46
  if (params.runFanoutBudget !== undefined || params.runFanoutAdmitted !== undefined) {
31
47
  return { ok: false, error: "Public execution does not accept internal run fan-out fields.", mode: params.workflowScript !== undefined ? "workflow" : "management" };
32
48
  }
@@ -38,6 +54,12 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
38
54
  if (params.clarify !== undefined) {
39
55
  return { ok: false, error: "Public workflowScript execution does not support clarify UI.", mode: "workflow" };
40
56
  }
57
+ if (params.chainName !== undefined) {
58
+ return { ok: false, error: "Durable chain management was removed; use workflowScript or /prompt-workflow for repeatable workflows.", mode: "management" };
59
+ }
60
+ if (params.config && typeof params.config === "object" && !Array.isArray(params.config) && Object.prototype.hasOwnProperty.call(params.config, "steps")) {
61
+ return { ok: false, error: "Durable chain definitions were removed; use workflowScript or /prompt-workflow for repeatable workflows.", mode: "management" };
62
+ }
41
63
  if (params.resume !== undefined) {
42
64
  return { ok: false, error: "Top-level resume execution is not available. Put resume on a workflowScript runs.run/runs.all item.", mode: "workflow" };
43
65
  }
@@ -47,6 +69,12 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
47
69
  }
48
70
  if (normalizedAction !== undefined) {
49
71
  const legacyAction = normalizedAction.toLowerCase();
72
+ if (legacyAction === "append-step") {
73
+ return { ok: false, error: "Legacy append-step control was removed from the public subagent tool; use current workflowScript orchestration.", mode: "management" };
74
+ }
75
+ if (legacyAction === "approve-checkpoint" || legacyAction === "reject-checkpoint") {
76
+ return { ok: false, error: "Legacy checkpoint approval controls were removed from the public subagent tool; use current workflowScript orchestration.", mode: "management" };
77
+ }
50
78
  if (legacyAction === "single") {
51
79
  return { ok: false, error: "action='single' is not supported. Omit action and pass { agent, task } for one child.", mode: "workflow" };
52
80
  }
@@ -71,7 +99,7 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
71
99
  return { ok: true, params: { ...params, action: normalizedAction } };
72
100
  }
73
101
  if (params.step !== undefined) {
74
- return { ok: false, error: "step is only available with action='append-step'; it is not an execution mode.", mode: "workflow" };
102
+ return { ok: false, error: "step is not a public execution field; use workflowScript for orchestration.", mode: "workflow" };
75
103
  }
76
104
  if (params.workflowScript !== undefined && (params.agent !== undefined || params.task !== undefined)) {
77
105
  return { ok: false, error: "Structured single-child execution cannot be combined with workflowScript.", mode: "workflow" };
@@ -93,6 +121,7 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
93
121
  ok: true,
94
122
  params: {
95
123
  ...workflowDefaults,
124
+ ...(params.async === undefined && options.asyncByDefault === false ? { async: false } : {}),
96
125
  workflowScript: `console.info("Converted structured single-child request to workflow runs.run('main', ...)."); return runs.run("main", ${JSON.stringify(child)})`,
97
126
  } as T,
98
127
  };
@@ -188,8 +188,6 @@ export const DynamicCollectSchema = Type.Object({
188
188
 
189
189
  // Flattened so chain steps do not need an object-shape anyOf/oneOf union.
190
190
  export const ChainItem = Type.Object({
191
- checkpoint: Type.Optional(Type.String({ description: "Approval checkpoint name. Pauses the chain without launching a child until approve-checkpoint or reject-checkpoint is called." })),
192
- message: Type.Optional(Type.String({ description: "Optional approval message shown while the checkpoint is paused." })),
193
191
  agent: Type.Optional(Type.String({ description: "Sequential step agent name" })),
194
192
  task: Type.Optional(Type.String({
195
193
  description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder, {outputs.name}=prior named output. Required for first step, defaults to '{previous}' for subsequent steps."
@@ -224,7 +222,7 @@ export const ChainItem = Type.Object({
224
222
  description: "Create isolated git worktrees for each parallel task."
225
223
  })),
226
224
  }, {
227
- description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution, {expand, parallel: {...}, collect} for dynamic fanout, or {checkpoint: name, message?} for an approval pause.",
225
+ description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution, or {expand, parallel: {...}, collect} for dynamic fanout.",
228
226
  additionalProperties: false,
229
227
  });
230
228
 
@@ -257,17 +255,16 @@ const ControlOverrides = Type.Object({
257
255
  const SubagentParamProperties = {
258
256
  agent: Type.Optional(Type.String({ description: "Agent for one-child execution, or target for agent management actions." })),
259
257
  task: Type.Optional(Type.String({ description: "Optional one-child task. Requires agent; cannot combine with action or workflowScript." })),
260
- 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." })),
261
258
  // Management action (when present, tool operates in management mode)
262
259
  action: Type.Optional(Type.String({ minLength: 1,
263
260
  description: "Optional management/control action. Omit this field for structured single-child or workflowScript execution; use it only for management/control actions."
264
261
  })),
265
262
  name: Type.Optional(Type.String({ description: "Human-readable name for action='schedule.create'." })),
266
263
  id: Type.Optional(Type.String({
267
- description: "Run id/prefix for status/debug.run, interrupt, steer, append-step, approve-checkpoint, reject-checkpoint, or mission."
264
+ description: "Run id/prefix for status/debug.run, interrupt, steer, or mission.attach-run."
268
265
  })),
269
266
  runId: Type.Optional(Type.String({
270
- description: "Target run ID for debug.run, interrupt, steer, append-step, or mission.attach-run. Prefer id."
267
+ description: "Target run ID for debug.run, interrupt, steer, or mission.attach-run. Prefer id."
271
268
  })),
272
269
  dir: Type.Optional(Type.String({
273
270
  description: "Async run directory for status/debug.run, stop, resume, or steer."
@@ -288,8 +285,6 @@ const SubagentParamProperties = {
288
285
  target: Type.Optional(Type.String({ enum: ["main", "children", "child"], description: "Target for watchdog actions." })),
289
286
  focus: Type.Optional(Type.Boolean({ description: "Focus the new Herdr pane for inspector.open or project.open." })),
290
287
  thinking: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "boolean", enum: [false] }], description: "Thinking level for action='watchdog.configure' (off/minimal/low/medium/high/xhigh/max, inherit, or false for off)." })),
291
- schedule: Type.Optional(Type.String({ deprecated: true, description: "Removed one-shot schedule field. Use action='schedule.create' with at." })),
292
- scheduleName: Type.Optional(Type.String({ deprecated: true, description: "Removed schedule display field. Use name." })),
293
288
  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." })),
294
289
  every: Type.Optional(Type.String({ description: "Fixed recurring interval for action='schedule.create', such as '30m', '6h', '2d', or '2w'." })),
295
290
  on: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "integer" }], description: "Calendar selector reserved for a later schedule slice." })),
@@ -304,27 +299,23 @@ const SubagentParamProperties = {
304
299
  runMode: Type.Optional(Type.String({ description: "Attached run mode." })),
305
300
  runStatus: Type.Optional(Type.String({ description: "Attached run status." })),
306
301
  summary: Type.Optional(Type.String({ description: "Mission close summary." })),
307
- // Chain identifier for management (can't reuse 'chain' — that's the execution array)
308
- chainName: Type.Optional(Type.String({
309
- description: "Chain name for get/update/delete management actions"
310
- })),
311
- // Agent/chain configuration for create/update (nested to avoid conflicts with execution fields)
302
+ // Agent configuration for create/update (nested to avoid conflicts with execution fields)
312
303
  config: Type.Optional(Type.Unsafe({
313
304
  anyOf: [
314
305
  { type: "object", additionalProperties: true },
315
306
  { type: "string" },
316
307
  ],
317
- description: "Agent/chain config for create/update. Object or JSON string; presence of steps creates a chain."
308
+ description: "Agent config for create/update. Object or JSON string."
318
309
  })),
319
- 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 prompts.render(ref, vars?) for task text. 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). Compose 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." })),
320
- 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." })),
310
+ workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript statement body. Normally async unless asyncByDefault:false; set async:true when async matters. Use async:false only when the parent must block until completion, never for reviews or gates. Use explicit return for output. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. Use await runs.run(key, {agent, task, worktree?, gate?}) or runs.run(key, {resume, task}), runs.all([...]), await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}), runs.status(id), runs.ref(s), emit(value), console, and return. For ordinary parallel fanout, use await runs.all([{key, agent, task}, ...]); 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. runs.steer targets a prior stable child key, never a raw run id, and must be awaited or returned. Mission workflows also have async state.get(key) and state.set(key, JSONValue). Compose 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." })),
311
+ 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. Explicit live-card requires same-repository async:false; async workflows should omit chatProgress or use auto/off." })),
312
+ isolation: Type.Optional(Type.String({ enum: ["none", "worktree"], description: "Workflow child isolation. none runs in the shared cwd; worktree requires managed git worktree isolation." })),
321
313
  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." })),
322
- step: Type.Optional(Type.Unsafe({ ...ChainItem, description: "One chain step for action='append-step' only. Not an execution mode." })),
323
314
  context: Type.Optional(Type.String({
324
315
  enum: ["fresh", "fork"],
325
- 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.",
316
+ description: "'fresh' or 'fork' to branch from parent session. Explicit context overrides every child. If omitted, config defaultSubagentContext wins over each agent defaultContext; implicit fork needs a persisted parent session and leaf, else fresh.",
326
317
  })),
327
- async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
318
+ async: Type.Optional(Type.Boolean({ description: "Run in background unless asyncByDefault:false. Set false only when the parent must block until completion." })),
328
319
  timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Timeout. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline. Alias maxRuntimeMs." })),
329
320
  maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline." })),
330
321
  toolTimeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Optional hard per-tool-call timeout in milliseconds; known-fast built-in tools have a five-minute default." })),
@@ -357,26 +348,12 @@ const SubagentParamProperties = {
357
348
  gate: Type.Optional(Type.String({ minLength: 1, description: "Host gate command. Cannot be combined with acceptance." })),
358
349
  };
359
350
 
360
- const { step: _legacyChainStep, ...subagentParamPropertiesWithoutStep } = SubagentParamProperties;
361
- const trimmedSubagentParamProperties = {
362
- ...subagentParamPropertiesWithoutStep,
363
- id: Type.Optional(Type.String({
364
- description: "Run id/prefix for status/debug.run, interrupt, steer, or mission.attach-run."
365
- })),
366
- runId: Type.Optional(Type.String({
367
- description: "Target run ID for debug.run, interrupt, steer, or mission.attach-run. Prefer id."
368
- })),
369
- };
370
351
  const SubagentParamsSchema = Type.Object(SubagentParamProperties);
371
- const TrimmedSubagentParamsSchema = Type.Object(trimmedSubagentParamProperties);
372
352
 
373
353
  export const SubagentParams = keepTopLevelParameterDescriptions(SubagentParamsSchema);
374
- export const SubagentParamsWithoutLegacyChainControls = keepTopLevelParameterDescriptions(TrimmedSubagentParamsSchema);
375
354
 
376
- export function createSubagentParamsSchema(options: { legacyChainControls?: boolean } = {}): typeof SubagentParams | typeof SubagentParamsWithoutLegacyChainControls {
377
- return options.legacyChainControls === true
378
- ? SubagentParams
379
- : SubagentParamsWithoutLegacyChainControls;
355
+ export function createSubagentParamsSchema(): typeof SubagentParams {
356
+ return SubagentParams;
380
357
  }
381
358
 
382
359
  const SubagentWaitParamsSchema = Type.Object({
@@ -6,10 +6,23 @@ import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
6
6
  const CUSTOM_TOOL_DESCRIPTION_FILE = "subagent-tool-description.md";
7
7
  const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
8
8
 
9
+ export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child or workflowScript for orchestration. For multi-step or parallel work, make exactly one top-level subagent call with workflowScript and async:true; launch children only inside that script 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 pass completed child results via .output. Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
10
+
11
+ export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflowScript call.";
12
+
13
+ export const SUBAGENT_TOOL_PROMPT_GUIDELINES = [
14
+ "Use subagent only when delegation is needed. Before executing, call { action: \"list\" } and run only executable, non-disabled agents.",
15
+ "Omit action for execution. Use { agent, task? } only for one child; use workflowScript for multi-step or parallel work.",
16
+ "workflowScript means exactly one top-level subagent tool call with async:true. Inside it, use runs.run/runs.all to launch children; do not make another top-level subagent call for those children.",
17
+ "For ordinary parallel work, use await runs.all([{key,agent,task}, ...]); 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.",
18
+ "Keep one writer per cwd/worktree unless writers run in isolated worktrees.",
19
+ "Use guide or the pi-subagents skill for advanced scheduling, missions, steering, and retention.",
20
+ ];
21
+
9
22
  export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
10
23
  • Use { action: "list" } before execution and only run executable/non-disabled agents.
11
24
  • Keep execution and management separate: omit action for structured single-child or workflowScript execution; use action only for management/control.
12
- • Async/background runs are the default. Use async:false only when a blocking foreground result is needed. After an async launch, continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll status just to wait; use subagent_wait only when the current request must finish in this turn.
25
+ • Async/background runs are the normal default unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Use async:false only when the parent must block until completion. Async mode still shows progress. Final reviews and gate checks stay async; needing a result is not a blocking reason. After an async launch, continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll status just to wait; use subagent_wait only when the current request must finish in this turn.
13
26
  • 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
27
  • Oracle/advisor consultations should use supervisor dialogue for material unknowns when available; request one-shot only when desired.
15
28
  • 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.
@@ -20,17 +33,15 @@ export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task?
20
33
  EXECUTION:
21
34
  • Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
22
35
  • SINGLE CHILD: { agent:"worker", task:"..." }. This structured form starts exactly one child through the workflow runtime. Workflow-level fields such as model, context, cwd, worktree, output, budgets, acceptance, and async remain defaults for that child. Do not combine agent/task with action or workflowScript.
23
- • WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. 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. 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 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 await prompts.render("package:name" | "user:name" | "project:name", vars?) for reusable plain task text, then pass the result explicitly as task. 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. 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, prompts.render, 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.
36
+ • 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; 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. 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.
24
37
  • 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" }
25
38
  • 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}" }
26
- • 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.
39
+ • Optional context is "fresh" or "fork". Explicit context wins. When omitted, config defaultSubagentContext wins over agent defaultContext. 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.
27
40
  • 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. A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
28
41
 
29
42
  MANAGEMENT / CONTROL (use action; omit execution fields):
30
43
  • list, get, models, guide, children.list, create, update, delete, eject, disable, enable, reset, status, debug.run, doctor, grant-spawn-budget, worktree.discard, 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.
31
44
  • 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.
32
- • { 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.
33
- • approve-checkpoint and reject-checkpoint decide a paused durable legacy chain checkpoint.
34
45
  • 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.
35
46
 
36
47
  ${SUBAGENT_SAFETY_GUIDANCE}`;
@@ -40,17 +51,16 @@ export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, ta
40
51
  EXECUTE:
41
52
  • Call { action:"list" } first and use only executable/non-disabled agents.
42
53
  • SINGLE {agent:"worker",task:"..."} starts exactly one child through the workflow runtime. Workflow-level fields remain child defaults. Do not combine agent/task with action or workflowScript.
43
- • SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and runs.all for parallel work. Use await prompts.render("package:name" | "user:name" | "project:name", vars?) for reusable task text and pass it explicitly to runs.run. 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. 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. 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.
54
+ • 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; 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. 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.
44
55
  • 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]"}
45
- • 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.
56
+ • context can be fresh or fork. Explicit context wins; omitted context follows defaultSubagentContext before agent defaultContext. 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.
46
57
 
47
58
  MANAGE / CONTROL:
48
59
  • Use action without execution fields for list/get/models/guide/authoring, refine/refine.show/refine.rollback, mission, watchdog, status, interrupt, stop, resume, steer, script-only scheduling, diagnostics, and other management actions. guide reads shipped current-version docs by topic.
49
- • append-step uses step:{...} only for an already-running durable legacy chain; step is not an execution mode.
50
60
  • A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
51
61
 
52
62
  ASYNC / SAFETY:
53
- • Omitted async detaches background work. Continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll merely to wait; use subagent_wait only when this turn must receive results.
63
+ • Omitted async follows asyncByDefault config; set async:true explicitly when async behavior matters. Continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll merely to wait; use subagent_wait only when this turn must receive results.
54
64
  • Ordinary children are not orchestrators. Keep one writer per cwd/worktree and use fresh read-only reviewers for independent checks.
55
65
  • Oracle/advisor consultations use available supervisor dialogue for material unknowns; request one-shot when desired.
56
66
  • Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, and {action:"status",id:"..."}.`;
@@ -70,6 +80,19 @@ export interface ToolDescriptionOptions {
70
80
  warn?: (message: string) => void;
71
81
  }
72
82
 
83
+ export interface SubagentToolPromptMetadata {
84
+ promptSnippet?: string;
85
+ promptGuidelines?: string[];
86
+ }
87
+
88
+ export function buildSubagentToolPromptMetadata(config: Pick<ExtensionConfig, "toolDescriptionMode"> = {}): SubagentToolPromptMetadata {
89
+ if (config.toolDescriptionMode !== undefined) return {};
90
+ return {
91
+ promptSnippet: SUBAGENT_TOOL_PROMPT_SNIPPET,
92
+ promptGuidelines: SUBAGENT_TOOL_PROMPT_GUIDELINES,
93
+ };
94
+ }
95
+
73
96
  export function resolveToolDescriptionMode(config: Pick<ExtensionConfig, "toolDescriptionMode">, options?: ToolDescriptionOptions): ToolDescriptionMode {
74
97
  const mode = config.toolDescriptionMode;
75
98
  if (mode === undefined) return "full";
@@ -157,20 +180,8 @@ function withMandatorySafetyGuidance(description: string): string {
157
180
  : SUBAGENT_SAFETY_GUIDANCE;
158
181
  }
159
182
 
160
- const LEGACY_CHAIN_CONTROL_GUIDANCE_LINES = new Set([
161
- '• { 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.',
162
- "• approve-checkpoint and reject-checkpoint decide a paused durable legacy chain checkpoint.",
163
- "• append-step uses step:{...} only for an already-running durable legacy chain; step is not an execution mode.",
164
- ]);
165
-
166
- function withoutLegacyChainControlGuidance(description: string): string {
167
- return description
168
- .split("\n")
169
- .filter((line) => !LEGACY_CHAIN_CONTROL_GUIDANCE_LINES.has(line.trim()))
170
- .join("\n");
171
- }
172
-
173
- export function buildSubagentToolDescription(config: Pick<ExtensionConfig, "toolDescriptionMode" | "legacyChainControls"> = {}, options?: ToolDescriptionOptions): string {
183
+ export function buildSubagentToolDescription(config: Pick<ExtensionConfig, "toolDescriptionMode"> = {}, options?: ToolDescriptionOptions): string {
184
+ if (config.toolDescriptionMode === undefined) return DEFAULT_SUBAGENT_TOOL_DESCRIPTION;
174
185
  const mode = resolveToolDescriptionMode(config, options);
175
186
  let description: string;
176
187
  if (mode === "compact") description = COMPACT_SUBAGENT_TOOL_DESCRIPTION;
@@ -182,5 +193,5 @@ export function buildSubagentToolDescription(config: Pick<ExtensionConfig, "tool
182
193
  description = FULL_SUBAGENT_TOOL_DESCRIPTION;
183
194
  }
184
195
  } else description = FULL_SUBAGENT_TOOL_DESCRIPTION;
185
- return config.legacyChainControls === true ? description : withoutLegacyChainControlGuidance(description);
196
+ return description;
186
197
  }
@@ -193,7 +193,7 @@ export async function handleHerdrInspectorAction(action: HerdrInspectorAction, p
193
193
  if (live.ok) return result(`Herdr inspector pane ${existing.paneId} is already open for async run ${target.runId}.${params.focus ? " Herdr cannot refocus an arbitrary raw pane id; select it in the Herdr UI." : ""}`);
194
194
  }
195
195
  const splitArgs = ["pane", "split", "--current", "--direction", "right", "--cwd", status.cwd ?? deps.cwd];
196
- if (params.focus !== false) splitArgs.push("--focus");
196
+ splitArgs.push(params.focus === true ? "--focus" : "--no-focus");
197
197
  const split = await client.run(splitArgs, { timeoutMs: 15_000, signal: deps.signal });
198
198
  if (split.ok === false) return result(formatHerdrError(split.error), true);
199
199
  const paneId = extractPaneId(split.data);
@@ -225,7 +225,7 @@ export async function handleHerdrInspectorAction(action: HerdrInspectorAction, p
225
225
  ...(mission ? { missionId: mission.id, missionPath: mission.path } : {}),
226
226
  paneId,
227
227
  openedAt: now,
228
- ...(params.focus !== false ? { lastFocusedAt: now } : {}),
228
+ ...(params.focus === true ? { lastFocusedAt: now } : {}),
229
229
  herdrVersion: detected.data.versionText,
230
230
  command,
231
231
  };
@@ -6,6 +6,7 @@ import { parseMissionRecord } from "../../missions/store.ts";
6
6
  import type { MissionRecord } from "../../missions/types.ts";
7
7
  import { requestAsyncSteer, requestAsyncStop } from "../../runs/background/control-channel.ts";
8
8
  import { formatAsyncRunTranscript } from "../../runs/background/fleet-view.ts";
9
+ import { steeringReceipt } from "../../runs/background/steering.ts";
9
10
  import type { AsyncStatus } from "../../shared/types.ts";
10
11
  import { readStatus } from "../../shared/utils.ts";
11
12
 
@@ -112,7 +113,7 @@ export function submitInspectorControl(options: RunnerOptions, line: string): st
112
113
  ...(targetIndex !== undefined ? { targetIndex } : { targetIndexes: runningIndexes }),
113
114
  source: "herdr-inspector",
114
115
  });
115
- return `Steering queued for run ${options.runId}.`;
116
+ return steeringReceipt(message, `Steering queued for run ${options.runId}.`);
116
117
  }
117
118
  if (command.startsWith("reply ")) throw new Error("Supervisor replies are owned by the parent Pi session; use subagent_supervisor/intercom there.");
118
119
  throw new Error("Unknown control. Use steer <message>, stop, or status.");
@@ -411,7 +411,7 @@ function createProjectPaneManagerInternal(options: InternalProjectPaneManagerOpt
411
411
  }
412
412
  }
413
413
  const splitArgs = ["pane", "split", "--current", "--direction", "right", "--cwd", projectRoot];
414
- if (input.focus !== false) splitArgs.push("--focus");
414
+ splitArgs.push(input.focus === true ? "--focus" : "--no-focus");
415
415
  const split = await client.run(splitArgs, { timeoutMs: 15_000, signal: input.signal });
416
416
  if (!split.ok) return projectPaneError(split.error.code, split.error.message, { projectRoot, details: split.error.details });
417
417
  const paneId = extractPaneId(split.data);
@@ -430,7 +430,7 @@ function createProjectPaneManagerInternal(options: InternalProjectPaneManagerOpt
430
430
  projectRoot,
431
431
  paneId,
432
432
  openedAt: now,
433
- ...(input.focus !== false ? { lastFocusedAt: now } : {}),
433
+ ...(input.focus === true ? { lastFocusedAt: now } : {}),
434
434
  herdrVersion: detected.data.versionText,
435
435
  command,
436
436
  ...(startupMessage ? { startupMessage } : {}),
@@ -14,6 +14,7 @@ import {
14
14
  } from "../runs/shared/pi-args.ts";
15
15
  import { INTERCOM_DETACH_REQUEST_EVENT, POLL_INTERVAL_MS, TEMP_ROOT_DIR, type IntercomEventBus, type SubagentState } from "../shared/types.ts";
16
16
  import { writeAtomicJson } from "../shared/atomic-json.ts";
17
+ import { shouldUseNativeFsWatch } from "../shared/watch-strategy.ts";
17
18
 
18
19
  const SUPERVISOR_CHANNEL_ROOT = path.join(TEMP_ROOT_DIR, "supervisor-channels");
19
20
  const REQUESTS_DIR = "requests";
@@ -75,6 +76,7 @@ type SupervisorWatch = (filename: fs.PathLike, listener: fs.WatchListener<string
75
76
  interface NativeSupervisorChannelDeps {
76
77
  platform?: NodeJS.Platform;
77
78
  watch?: SupervisorWatch;
79
+ timers?: Pick<typeof globalThis, "setInterval" | "clearInterval" | "setImmediate" | "clearImmediate">;
78
80
  }
79
81
 
80
82
  const ContactSupervisorParamsSchema = Type.Object({
@@ -462,7 +464,7 @@ function clearForegroundSupervisorAttention(request: SupervisorRequest, pending:
462
464
  && candidate.childIndex === request.childIndex
463
465
  )) return;
464
466
  const remembered = rememberedForegroundChild(request, state);
465
- if (!remembered || remembered.child.status !== "detached" || remembered.child.currentTool !== "contact_supervisor") return;
467
+ if (!remembered || remembered.child.status !== "detached") return;
466
468
  const updatedAt = Date.now();
467
469
  remembered.run.updatedAt = updatedAt;
468
470
  remembered.child.activityState = undefined;
@@ -612,8 +614,9 @@ function buildParentSupervisorTool(pending: Map<string, PendingSupervisorRequest
612
614
  };
613
615
  }
614
616
 
615
- export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): { start: () => void; dispose: () => void; pending: Map<string, PendingSupervisorRequest> } {
617
+ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): { start: () => void; activateTransport: () => void; dispose: () => void; pending: Map<string, PendingSupervisorRequest> } {
616
618
  const watch = deps.watch ?? fs.watch;
619
+ const timers = deps.timers ?? globalThis;
617
620
  const pending = new Map<string, PendingSupervisorRequest>();
618
621
  const seenFiles = new Set<string>();
619
622
  const requestWatchers = new Map<string, fs.FSWatcher>();
@@ -623,6 +626,13 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
623
626
  let deferredWatcherRefresh: ReturnType<typeof setImmediate> | undefined;
624
627
  let started = false;
625
628
  let lastStaleCleanupAt = 0;
629
+ const platform = deps.platform ?? process.platform;
630
+ const useNativeWatcher = () => shouldUseNativeFsWatch("supervisor-channel", platform) && platform !== "win32";
631
+ const hasTransportDemand = () => {
632
+ if (pending.size > 0) return true;
633
+ if (state.foregroundControls.size > 0) return true;
634
+ return [...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running");
635
+ };
626
636
 
627
637
  const registerParentTools = (): void => {
628
638
  if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentSupervisorTool(pending, state));
@@ -683,18 +693,25 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
683
693
  agent: request.agent,
684
694
  childIndex: request.childIndex,
685
695
  });
696
+ if (pending.has(request.id)) markForegroundSupervisorAttention(request, state);
686
697
  }
687
698
  }
688
699
  };
689
700
 
690
701
  const startPolling = (): void => {
691
702
  if (poller) return;
692
- poller = setInterval(poll, CHANNEL_POLL_MS);
703
+ poller = timers.setInterval(() => {
704
+ poll();
705
+ if (!useNativeWatcher() && platform === "darwin" && !hasTransportDemand()) {
706
+ if (poller) timers.clearInterval(poller);
707
+ poller = undefined;
708
+ }
709
+ }, CHANNEL_POLL_MS);
693
710
  poller.unref?.();
694
711
  };
695
712
  const startSafetyPolling = (): void => {
696
713
  if (safetyPoller) return;
697
- safetyPoller = setInterval(() => {
714
+ safetyPoller = timers.setInterval(() => {
698
715
  watchExistingRequestDirs();
699
716
  poll();
700
717
  }, CHANNEL_SAFETY_POLL_MS);
@@ -730,7 +747,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
730
747
  };
731
748
  const scheduleWatcherRefresh = (): void => {
732
749
  if (deferredWatcherRefresh) return;
733
- deferredWatcherRefresh = setImmediate(() => {
750
+ deferredWatcherRefresh = timers.setImmediate(() => {
734
751
  deferredWatcherRefresh = undefined;
735
752
  if (!started) return;
736
753
  watchExistingRequestDirs();
@@ -740,6 +757,11 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
740
757
  };
741
758
 
742
759
  return {
760
+ activateTransport: () => {
761
+ if (!started) return;
762
+ poll();
763
+ if (!useNativeWatcher() && hasTransportDemand()) startPolling();
764
+ },
743
765
  start: () => {
744
766
  if (started) return;
745
767
  started = true;
@@ -747,8 +769,8 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
747
769
  poll();
748
770
  try {
749
771
  fs.mkdirSync(SUPERVISOR_CHANNEL_ROOT, { recursive: true });
750
- if ((deps.platform ?? process.platform) === "win32") {
751
- startPolling();
772
+ if (!useNativeWatcher()) {
773
+ if (platform === "win32") startPolling();
752
774
  return;
753
775
  }
754
776
  watchExistingRequestDirs();
@@ -776,11 +798,11 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
776
798
  try { watcher.close(); } catch {}
777
799
  }
778
800
  requestWatchers.clear();
779
- if (poller) clearInterval(poller);
801
+ if (poller) timers.clearInterval(poller);
780
802
  poller = undefined;
781
- if (safetyPoller) clearInterval(safetyPoller);
803
+ if (safetyPoller) timers.clearInterval(safetyPoller);
782
804
  safetyPoller = undefined;
783
- if (deferredWatcherRefresh) clearImmediate(deferredWatcherRefresh);
805
+ if (deferredWatcherRefresh) timers.clearImmediate(deferredWatcherRefresh);
784
806
  deferredWatcherRefresh = undefined;
785
807
  pending.clear();
786
808
  seenFiles.clear();
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { createHash } from "node:crypto";
3
4
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
5
  import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
5
6
  import { PROMPT_REDACTED } from "../shared/utils.ts";
@@ -294,13 +295,17 @@ export function syncMissionFromAsyncCompletion(value: unknown): MissionRecord |
294
295
  current = readMission(binding.location, binding.missionId);
295
296
  } catch (error) {
296
297
  if (!(error instanceof MissionNotFoundError)) throw error;
298
+ const reason = "mission-record-missing";
299
+ const markerId = createHash("sha256").update(JSON.stringify([runId, binding.missionId, reason])).digest("hex");
300
+ const markerPath = path.join(event.asyncDir, `.mission-sync-skipped-${markerId}.json`);
297
301
  try {
302
+ fs.writeFileSync(markerPath, `${JSON.stringify({ runId, missionId: binding.missionId, reason })}\n`, { encoding: "utf-8", mode: 0o600, flag: "wx" });
298
303
  fs.appendFileSync(path.join(event.asyncDir, "events.jsonl"), `${JSON.stringify({
299
304
  type: "subagent.mission.sync.skipped",
300
305
  ts: Date.now(),
301
306
  runId,
302
307
  missionId: binding.missionId,
303
- reason: "mission-record-missing",
308
+ reason,
304
309
  missionPath: missionRecordPath(binding.location, binding.missionId),
305
310
  })}\n`, "utf-8");
306
311
  } catch {
@@ -77,11 +77,6 @@ function nonNegativeTokenCount(value: unknown, label: string): number {
77
77
  return value as number;
78
78
  }
79
79
 
80
- function parseStoredGoal(value: unknown, label: string): { goal?: MissionGoal; legacyObjective?: string } {
81
- if (typeof value === "string") return { legacyObjective: requiredString(value, label).trim() };
82
- return { goal: parseGoal(value, label) };
83
- }
84
-
85
80
  function parseGoal(value: unknown, label: string): MissionGoal {
86
81
  const input = asObject(value, label);
87
82
  if (input.status !== "active" && input.status !== "paused" && input.status !== "budget-exhausted") throw new Error(`${label}.status is invalid`);
@@ -223,18 +218,18 @@ export function parseMissionRecord(value: unknown, source = "mission record"): M
223
218
  const decisions = input.decisions as unknown[];
224
219
  const artifacts = input.artifacts as unknown[];
225
220
  const receipts = (input.receipts ?? []) as unknown[];
226
- const parsedGoal = input.goal !== undefined ? parseStoredGoal(input.goal, `${source}.goal`) : {};
221
+ const goal = input.goal !== undefined ? parseGoal(input.goal, `${source}.goal`) : undefined;
227
222
  const budget = input.budget !== undefined ? parseBudget(input.budget, `${source}.budget`) : undefined;
228
223
  const usage = input.usage !== undefined ? parseUsage(input.usage, `${source}.usage`) : undefined;
229
- const objective = optionalString(input.objective, `${source}.objective`)?.trim() ?? parsedGoal.legacyObjective;
224
+ const objective = optionalString(input.objective, `${source}.objective`)?.trim();
230
225
  if (!objective) throw new Error(`${source}.objective must be a non-empty string`);
231
- if (parsedGoal.goal && !budget) throw new Error(`${source}.budget is required for a goal mission`);
226
+ if (goal && !budget) throw new Error(`${source}.budget is required for a goal mission`);
232
227
  return {
233
228
  schemaVersion: 1,
234
229
  id: validateMissionId(input.id, `${source}.id`),
235
230
  title: requiredString(input.title, `${source}.title`),
236
231
  objective,
237
- ...(parsedGoal.goal ? { goal: parsedGoal.goal } : {}),
232
+ ...(goal ? { goal } : {}),
238
233
  ...(budget ? { budget } : {}),
239
234
  ...(usage ? { usage } : {}),
240
235
  status: missionStatus(input.status, `${source}.status`),
@@ -59,7 +59,7 @@ function psProcessStartKey(pid: number): string | undefined {
59
59
 
60
60
  function windowsProcessStartKey(pid: number): string | undefined {
61
61
  try {
62
- const raw = execFileSync("powershell.exe", ["-NoProfile", "-Command", `(Get-CimInstance Win32_Process -Filter \"ProcessId=${pid}\").CreationDate`], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 1000 }).trim();
62
+ const raw = execFileSync("powershell.exe", ["-NoProfile", "-Command", `(Get-CimInstance Win32_Process -Filter \"ProcessId=${pid}\").CreationDate`], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 1000, windowsHide: true }).trim();
63
63
  return raw ? `win:${raw}` : undefined;
64
64
  } catch {
65
65
  return undefined;
@@ -69,7 +69,7 @@ function windowsProcessStartKey(pid: number): string | undefined {
69
69
  function processStartKey(pid: number): string | undefined {
70
70
  if (process.platform === "linux") return linuxProcessStartKey(pid) ?? psProcessStartKey(pid);
71
71
  if (process.platform === "win32") return windowsProcessStartKey(pid);
72
- return psProcessStartKey(pid);
72
+ return undefined;
73
73
  }
74
74
 
75
75
  const CURRENT_PROCESS_KEY = processStartKey(process.pid);
@@ -3,6 +3,7 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
  import { BUILTIN_AGENT_NAMES } from "../agents/agents.ts";
6
+ import { getPiSpawnCommand } from "../runs/shared/pi-spawn.ts";
6
7
  import { findModelInfo, getSupportedThinkingLevels, splitKnownThinkingSuffix, toModelInfo } from "../shared/model-info.ts";
7
8
  import { getAgentDir } from "../shared/utils.ts";
8
9
 
@@ -340,7 +341,8 @@ async function probeModel(
340
341
  if (typeof pi.exec !== "function") {
341
342
  return { status: "skipped", message: "pi.exec is unavailable in this runtime." };
342
343
  }
343
- const result = await pi.exec("pi", ["-p", "--model", fullId, "--no-tools", 'Reply with exactly "OK".'], {
344
+ const spawnSpec = getPiSpawnCommand(["-p", "--model", fullId, "--no-tools", 'Reply with exactly "OK".']);
345
+ const result = await pi.exec(spawnSpec.command, spawnSpec.args, {
344
346
  cwd: os.tmpdir(),
345
347
  timeout: 45_000,
346
348
  } as Record<string, unknown>);