pi-subagents 0.66.0 → 0.67.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 (115) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +4 -3
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/reviewer.md +3 -2
  5. package/docs/agents.md +6 -3
  6. package/docs/configuration.md +7 -5
  7. package/docs/extension-api.md +33 -18
  8. package/docs/missions.md +8 -0
  9. package/docs/models.md +1 -1
  10. package/docs/observability.md +4 -4
  11. package/docs/standalone-background.md +49 -0
  12. package/docs/tool-reference.md +18 -8
  13. package/docs/watchdog.md +35 -4
  14. package/docs/workflows.md +26 -12
  15. package/inspector-runner.mjs +2 -2
  16. package/package.json +1 -1
  17. package/prompts/parallel-review.md +1 -1
  18. package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
  19. package/skills/pi-subagents/SKILL.md +14 -0
  20. package/skills/pi-subagents/references/execution-controls.md +7 -5
  21. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  22. package/src/agents/advertised-agent-prompt.ts +34 -3
  23. package/src/agents/agents.ts +6 -0
  24. package/src/agents/builtin-names.ts +1 -0
  25. package/src/api/delegation.ts +4 -0
  26. package/src/api/preflight.ts +76 -45
  27. package/src/api/shared-types.ts +2 -0
  28. package/src/extension/fanout-child.ts +63 -4
  29. package/src/extension/index.ts +20 -8
  30. package/src/extension/public-execution.ts +4 -2
  31. package/src/extension/rpc.ts +4 -0
  32. package/src/extension/schemas.ts +67 -78
  33. package/src/extension/tool-description.ts +29 -82
  34. package/src/inspectors/actions.ts +148 -0
  35. package/src/inspectors/ghostty/actions.ts +74 -0
  36. package/src/inspectors/ghostty/plugin.ts +17 -0
  37. package/src/inspectors/herdr/actions.ts +99 -179
  38. package/src/inspectors/herdr/plugin.ts +20 -0
  39. package/src/inspectors/herdr/project-panes.ts +1 -1
  40. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  41. package/src/inspectors/plugins.ts +8 -0
  42. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  43. package/src/inspectors/types.ts +51 -0
  44. package/src/intercom/intercom-bridge.ts +50 -8
  45. package/src/intercom/native-supervisor-channel.ts +22 -13
  46. package/src/runs/background/active-async-capacity.ts +4 -0
  47. package/src/runs/background/async-execution.ts +45 -56
  48. package/src/runs/background/async-resume.ts +5 -9
  49. package/src/runs/background/auto-drain.ts +6 -3
  50. package/src/runs/background/binary-bootstrap.ts +33 -0
  51. package/src/runs/background/fleet-view.ts +30 -2
  52. package/src/runs/background/notify.ts +31 -1
  53. package/src/runs/background/owned-process-tree.ts +29 -2
  54. package/src/runs/background/run-child-session.ts +61 -4
  55. package/src/runs/background/run-status.ts +3 -0
  56. package/src/runs/background/runner-aliases.ts +11 -3
  57. package/src/runs/background/runner-child-launch.ts +2 -0
  58. package/src/runs/background/runner-child-sessions.ts +5 -4
  59. package/src/runs/background/scheduled-runs.ts +40 -13
  60. package/src/runs/background/steering.ts +20 -2
  61. package/src/runs/background/subagent-runner.ts +47 -31
  62. package/src/runs/background/subagent-wait.ts +51 -8
  63. package/src/runs/background/wait-tool.ts +1 -1
  64. package/src/runs/foreground/async-steering-action.ts +18 -7
  65. package/src/runs/foreground/execution.ts +44 -31
  66. package/src/runs/foreground/prompt-audit.ts +3 -1
  67. package/src/runs/foreground/subagent-executor.ts +110 -100
  68. package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
  69. package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
  70. package/src/runs/shared/acceptance.ts +5 -2
  71. package/src/runs/shared/async-status-projection.ts +4 -0
  72. package/src/runs/shared/capability-ceiling.ts +2 -0
  73. package/src/runs/shared/child-hooks.ts +25 -10
  74. package/src/runs/shared/child-launch.ts +12 -2
  75. package/src/runs/shared/child-lifecycle.ts +6 -3
  76. package/src/runs/shared/child-runtime-config.ts +3 -1
  77. package/src/runs/shared/child-session.ts +33 -2
  78. package/src/runs/shared/child-tool-plan.ts +122 -3
  79. package/src/runs/shared/completion-guard.ts +5 -3
  80. package/src/runs/shared/effective-system-prompt.ts +33 -0
  81. package/src/runs/shared/external-cli-runner.ts +9 -7
  82. package/src/runs/shared/llm-intent-arbiter.ts +12 -3
  83. package/src/runs/shared/model-fallback.ts +2 -0
  84. package/src/runs/shared/orca-progress-tabs.ts +1 -1
  85. package/src/runs/shared/pi-spawn.ts +10 -0
  86. package/src/runs/shared/subagent-prompt-runtime.ts +9 -3
  87. package/src/runs/shared/task-intent.ts +46 -13
  88. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  89. package/src/runs/shared/worktree.ts +42 -12
  90. package/src/shared/fork-context.ts +15 -72
  91. package/src/shared/launch-contract.ts +65 -2
  92. package/src/shared/opencode-session-headers.ts +30 -0
  93. package/src/shared/types.ts +4 -1
  94. package/src/slash/delegation-adapters.ts +3 -1
  95. package/src/slash/delegation-request.ts +14 -0
  96. package/src/slash/slash-commands.ts +2 -1
  97. package/src/slash/subagents-admin.ts +11 -4
  98. package/src/tui/fleet-status.ts +164 -19
  99. package/src/tui/fleet.ts +16 -14
  100. package/src/tui/render.ts +149 -28
  101. package/src/watchdog/child-status.ts +8 -0
  102. package/src/watchdog/model-selection.ts +20 -0
  103. package/src/watchdog/permission-arbiter.ts +3 -1
  104. package/src/watchdog/register-child.ts +1 -0
  105. package/src/watchdog/register-main.ts +31 -27
  106. package/src/watchdog/review.ts +132 -67
  107. package/src/watchdog/runtime.ts +82 -20
  108. package/src/watchdog/scope.ts +1 -1
  109. package/src/watchdog/settings.ts +9 -3
  110. package/src/watchdog/tool-actions.ts +13 -12
  111. package/src/watchdog/turn-delta.ts +23 -0
  112. package/src/watchdog/types.ts +4 -0
  113. package/src/workflows/scripted-workflow.ts +237 -7
  114. package/src/workflows/workflow-checklist.ts +2 -2
  115. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -35,7 +35,7 @@ const SkillOverride = Type.Unsafe({
35
35
  { type: "boolean" },
36
36
  { type: "string" },
37
37
  ],
38
- description: "Skill name(s) to make available (comma-separated), array of strings, or boolean (false disables, true uses default)",
38
+ description: "Skills: names/CSV/array; false disables, true uses default.",
39
39
  });
40
40
 
41
41
  const OutputOverride = Type.Unsafe({
@@ -48,7 +48,7 @@ const OutputOverride = Type.Unsafe({
48
48
 
49
49
  const OutputModeOverride = Type.String({
50
50
  enum: ["inline", "file-only"],
51
- description: "Return saved output inline (default) or only a concise file reference. file-only requires output to be a path.",
51
+ description: "Default inline; file-only requires output path.",
52
52
  });
53
53
 
54
54
  const ReadsOverride = Type.Unsafe({
@@ -62,21 +62,9 @@ const ReadsOverride = Type.Unsafe({
62
62
  const JsonSchemaObject = Type.Unsafe({
63
63
  type: "object",
64
64
  additionalProperties: true,
65
- description: "JSON Schema object for strict structured output. Non-object roots are rejected.",
65
+ description: "Strict structured output; object-root JSON Schema only.",
66
66
  });
67
67
 
68
- const AcceptanceEvidenceKinds = [
69
- "changed-files",
70
- "tests-added",
71
- "commands-run",
72
- "validation-output",
73
- "residual-risks",
74
- "no-staged-files",
75
- "diff-summary",
76
- "review-findings",
77
- "manual-notes",
78
- ];
79
-
80
68
  // Provider boolean branches intentionally overapproximate false-only runtime inputs.
81
69
  // Restricted function-declaration converters only support string enum members.
82
70
  const AcceptanceOverride = Type.Unsafe({
@@ -94,7 +82,7 @@ const AcceptanceOverride = Type.Unsafe({
94
82
  { type: "boolean" },
95
83
  { type: "object", additionalProperties: true },
96
84
  ],
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.`,
85
+ description: "Evidence policy; omit for read-only/review. false disables; true invalid. Prefer object; see guide tool-reference for levels, evidence and review.required.",
98
86
  });
99
87
 
100
88
  const AgentContractOverride = Type.Object({
@@ -113,7 +101,7 @@ const WorkflowLaneMetadata = Type.Object({
113
101
  sourceRef: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),
114
102
  claims: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 160 }), { maxItems: 20 })),
115
103
  outputPaths: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 256 }), { maxItems: 10 })),
116
- }, { additionalProperties: false, description: "Optional bounded child lane metadata. Display/triage only; sourceRef is opaque and never resolved during status rendering." });
104
+ }, { additionalProperties: false, description: "Display/triage only; sourceRef is opaque, never resolved by status." });
117
105
 
118
106
  const ToolBudgetBlock = Type.Unsafe({
119
107
  anyOf: [
@@ -126,7 +114,7 @@ const ToolBudgetOverride = Type.Object({
126
114
  soft: Type.Optional(Type.Integer({ minimum: 1 })),
127
115
  hard: Type.Integer({ minimum: 1 }),
128
116
  block: Type.Optional(ToolBudgetBlock),
129
- }, { additionalProperties: false, description: "Optional child tool-call budget. soft nudges the child; after hard, block tools (default read/grep/find/ls, or '*' for all tools) are blocked so the child can finalize." });
117
+ }, { additionalProperties: false, description: "soft nudges; after hard, block tools (default read/grep/find/ls, '*' for all) so child can finalize." });
130
118
 
131
119
  const UsageBudgetLimitOverride = Type.Object({
132
120
  soft: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
@@ -136,7 +124,7 @@ const UsageBudgetLimitOverride = Type.Object({
136
124
  const UsageBudgetOverride = Type.Object({
137
125
  tokens: Type.Optional(UsageBudgetLimitOverride),
138
126
  costUsd: Type.Optional(UsageBudgetLimitOverride),
139
- }, { additionalProperties: false, description: "Optional root-only reported-usage budget. Hard limits prevent future child launches; running children are not stopped." });
127
+ }, { additionalProperties: false, description: "Root-only reported usage; hard prevents later launches. Running children are not stopped." });
140
128
 
141
129
  const WorkflowPreflightLane = Type.Object({
142
130
  key: Type.String({ minLength: 1, maxLength: 128 }),
@@ -151,7 +139,7 @@ const WorkflowPreflightOverride = Type.Object({
151
139
  version: Type.Integer({ minimum: 1, maximum: 1 }),
152
140
  coverage: Type.Optional(Type.String({ enum: ["complete", "partial"] })),
153
141
  lanes: Type.Array(WorkflowPreflightLane, { maxItems: 64 }),
154
- }, { additionalProperties: false, description: "Bounded display-only lane hints for workflow launch/status. Coverage mismatches warn but never change launch authority or execution." });
142
+ }, { additionalProperties: false, description: "Display-only lane hints; coverage mismatches warn, never change authority/execution." });
155
143
 
156
144
  // Parallel task item (within a parallel step)
157
145
  export const ParallelTaskSchema = Type.Object({
@@ -280,58 +268,59 @@ const ControlOverrides = Type.Object({
280
268
  });
281
269
 
282
270
  const SubagentParamProperties = {
283
- agent: Type.Optional(Type.String({ description: "Agent for one-child execution, or target for agent management actions." })),
284
- task: Type.Optional(Type.String({ description: "Optional one-child task. Requires agent; cannot combine with action, workflowScript, or workflowScriptPath." })),
285
- extensionBindings: Type.Optional(Type.Unsafe({ type: "object", maxProperties: 16, additionalProperties: true, description: "Namespaced, bounded plain-JSON metadata delivered only to the child runtime. Namespace keys use package.name/1 syntax." })),
271
+ agent: Type.Optional(Type.String({ description: "One-child agent or management target." })),
272
+ task: Type.Optional(Type.String({ description: "One-child task; requires agent." })),
273
+ extensionBindings: Type.Optional(Type.Unsafe({ type: "object", maxProperties: 16, additionalProperties: true, description: "Child-only bounded JSON; namespaces package.name/1." })),
286
274
  // Management action (when present, tool operates in management mode)
287
275
  action: Type.Optional(Type.String({ minLength: 1,
288
- description: "Optional management/control action. Use action='validate' with workflowScript or workflowScriptPath for offline checks. Omit this field for structured single-child or workflow execution; otherwise, use it only for management/control actions."
276
+ description: "Management/control only; omit for execution. validate accepts either script input. Discover actions with guide topic tool-reference."
289
277
  })),
290
- capabilities: Type.Optional(Type.Boolean({ description: "For action='list', return compact capability rows and structured details without system prompts." })),
291
- name: Type.Optional(Type.String({ description: "Human-readable name for action='schedule.create'." })),
278
+ capabilities: Type.Optional(Type.Boolean({ description: "list: compact capability rows/details without system prompts." })),
279
+ name: Type.Optional(Type.String({ description: "schedule.create name." })),
292
280
  id: Type.Optional(Type.String({
293
- description: "Run id/prefix for status/debug.run, interrupt, steer, or mission.attach-run."
281
+ description: "Run id/prefix for status/control."
294
282
  })),
295
283
  runId: Type.Optional(Type.String({
296
- description: "Target run ID for debug.run, interrupt, steer, or mission.attach-run. Prefer id."
284
+ description: "Target run ID; prefer id."
297
285
  })),
298
286
  dir: Type.Optional(Type.String({
299
- description: "Async run directory for status/debug.run, stop, resume, or steer."
287
+ description: "Async directory for status/control."
300
288
  })),
301
- handoffPath: Type.Optional(Type.String({ description: "Existing parallel handoff manifest for worktree.discard, worktree.cleanup metadata, or lane evidence actions." })),
302
- repo: Type.Optional(Type.String({ description: "Repository path for action='worktree.cleanup'; defaults to cwd." })),
303
- planId: Type.Optional(Type.String({ description: "Cleanup plan id reserved for a future worktree.cleanup apply action." })),
304
- laneId: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "Exact manifest run id for lane.status, lane.recordMerge, or lane.recordSupersession." })),
305
- merge: Type.Optional(Type.Unsafe({ type: "object", additionalProperties: true, description: "Attested merge evidence for lane.recordMerge: prNumber, reviewedHead, mergeCommit, treeEquivalent, postMergeChecks, attestedBy, and attestedAt." })),
306
- supersession: Type.Optional(Type.Unsafe({ type: "object", additionalProperties: true, description: "Attested replacement-lane evidence for lane.recordSupersession: supersededBy, attestedBy, and attestedAt." })),
307
- index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child or transcript." })),
308
- childId: Type.Optional(Type.String({ minLength: 1, maxLength: 256, description: "Stable child identity for child-scoped stop requests." })),
289
+ handoffPath: Type.Optional(Type.String({ description: "Existing manifest for worktree/lane actions." })),
290
+ repo: Type.Optional(Type.String({ description: "worktree.cleanup repo; default cwd." })),
291
+ planId: Type.Optional(Type.String({ description: "Reserved; cleanup is plan-only." })),
292
+ laneId: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "Exact manifest run id for lane actions." })),
293
+ merge: Type.Optional(Type.Unsafe({ type: "object", additionalProperties: true, description: "lane.recordMerge evidence; read guide tool-reference." })),
294
+ supersession: Type.Optional(Type.Unsafe({ type: "object", additionalProperties: true, description: "lane.recordSupersession evidence; read guide tool-reference." })),
295
+ index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child/transcript index." })),
296
+ childId: Type.Optional(Type.String({ minLength: 1, maxLength: 256, description: "Child-scoped stop identity." })),
309
297
  view: Type.Optional(Type.String({
310
298
  enum: ["fleet", "transcript"],
311
- description: "Optional status view. Use view='fleet' for a read-only active foreground/async fleet surface, or view='transcript' with id/dir (and optional index) to tail a run transcript.",
299
+ description: "status view: fleet overview or transcript tail with id/dir and optional index.",
312
300
  })),
313
- lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 500, description: "Maximum transcript lines for action='status', view='transcript'. Defaults to 80." })),
301
+ lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 500, description: "Transcript tail lines; default 80." })),
314
302
  topic: Type.Optional(Type.String()),
315
- message: Type.Optional(Type.String({ description: "Follow-up message for resume, live guidance for steer, or optional startup prompt for project.open." })),
316
- mode: Type.Optional(Type.String({ enum: ["steer", "follow_up", "auto", "plan", "apply"], description: "Delivery mode for action='steer', or plan/apply mode for worktree.cleanup. worktree.cleanup currently supports plan only; apply/removal is not available yet." })),
317
- steeringRecovery: Type.Optional(Type.Boolean({ description: "For action='steer', allow pause-and-revive recovery after a missed acknowledgment. Defaults true for direct tool calls in steer mode; extension RPC steering forces false so callers retain exact child ownership." })),
318
- additional: Type.Optional(Type.Integer({ minimum: 1, description: "Positive launches to add with action='grant-spawn-budget'. Root interactive parent with native user confirmation only; total grants cannot exceed the original configured cap." })),
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." })),
320
- target: Type.Optional(Type.String({ enum: ["main", "children", "child"], description: "Target for watchdog actions." })),
321
- focus: Type.Optional(Type.Boolean({ description: "Focus the new Herdr pane for inspector.open or project.open." })),
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'." })),
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." })),
324
- every: Type.Optional(Type.String({ description: "Fixed recurring interval for action='schedule.create', such as '30m', '6h', '2d', or '2w'." })),
303
+ message: Type.Optional(Type.String({ description: "resume/steer guidance or project.open prompt." })),
304
+ mode: Type.Optional(Type.String({ enum: ["steer", "follow_up", "auto", "plan", "apply"], description: "steer delivery mode; worktree.cleanup supports plan only, no apply/removal." })),
305
+ steeringRecovery: Type.Optional(Type.Boolean({ description: "steer: pause/revive after missed acknowledgment; default true in direct steer mode, forced false by extension RPC for exact ownership." })),
306
+ additional: Type.Optional(Type.Integer({ minimum: 1, description: "grant-spawn-budget: root interactive parent + native user confirmation only; total grants capped at original configured cap." })),
307
+ scope: Type.Optional(Type.String({ enum: ["session", "user", "project"], description: "watchdog.configure scope; default session, persistent only if explicit." })),
308
+ target: Type.Optional(Type.String({ enum: ["main", "children", "child"], description: "Watchdog target." })),
309
+ focus: Type.Optional(Type.Boolean({ description: "Focus inspector.open/project.open pane." })),
310
+ thinking: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "boolean" }], description: "watchdog.configure only: off/minimal/low/medium/high/xhigh/max, inherit, false=off; true invalid. Dispatch ignores this; use model suffix." })),
311
+ at: Type.Optional(Type.String({ description: "schedule.create: delay (+10m) or zoned ISO timestamp." })),
312
+ every: Type.Optional(Type.String({ description: "schedule.create interval, e.g. 30m/6h/2d/2w." })),
325
313
  sessionOnly: Type.Optional(Type.Boolean()),
326
- on: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "integer" }], description: "Calendar selector reserved for a later schedule slice." })),
314
+ quiet: Type.Optional(Type.Boolean()),
315
+ on: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "integer" }], description: "Reserved calendar selector." })),
327
316
  timezone: Type.Optional(Type.String()),
328
- overlap: Type.Optional(Type.String({ enum: ["skip"], description: "Overlap policy. This slice supports skip only." })),
329
- catchUp: Type.Optional(Type.String({ enum: ["none", "latest"], description: "Missed occurrence policy for recurring schedules. Defaults to latest." })),
330
- missionId: Type.Optional(Type.String({ description: "Mission id." })),
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." })),
332
- missionUpdate: Type.Optional(Type.Unsafe({ ...MissionUpdateOverride, description: "Mission update: objective, goal false or {paused:boolean}, budget, summary, labels, decisions, artifacts, or delivery receipts." })),
333
- missionStatus: Type.Optional(Type.String({ description: "Mission status." })),
334
- missionScope: Type.Optional(Type.String({ description: "Mission list scope: project (default) or global pointer index." })),
317
+ overlap: Type.Optional(Type.String({ enum: ["skip"] })),
318
+ catchUp: Type.Optional(Type.String({ enum: ["none", "latest"], description: "Missed schedule occurrences; default latest." })),
319
+ missionId: Type.Optional(Type.String()),
320
+ mission: Type.Optional(Type.Unsafe({ ...MissionLaunchOverride, description: "false disables; true invalid. Object: exactly one non-empty title or summary; objective/labels optional; goal only true, requires budget.tokens." })),
321
+ missionUpdate: Type.Optional(Type.Unsafe({ ...MissionUpdateOverride, description: "Mission patch; read guide missions." })),
322
+ missionStatus: Type.Optional(Type.String()),
323
+ missionScope: Type.Optional(Type.String({ description: "project (default) or global pointer index." })),
335
324
  runMode: Type.Optional(Type.String({ description: "Attached run mode." })),
336
325
  runStatus: Type.Optional(Type.String({ description: "Attached run status." })),
337
326
  summary: Type.Optional(Type.String({ description: "Mission close summary." })),
@@ -341,37 +330,37 @@ const SubagentParamProperties = {
341
330
  { type: "object", additionalProperties: true },
342
331
  { type: "string" },
343
332
  ],
344
- description: "Agent config for create/update. Object or JSON string."
333
+ description: "create/update agent config; object or JSON string."
345
334
  })),
346
- workflow: Type.Optional(Type.String({ minLength: 1, description: "Extension-owned workflow resource; resolves its script and authority internally." })),
347
- args: Type.Optional(Type.Unsafe({ type: "object", maxProperties: 16, additionalProperties: true, description: "Bounded plain-JSON args for workflow; resource validation applies." })),
348
- workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Inline JavaScript statement body with unknown resource provenance. Normally async unless asyncByDefault:false; set async:true for async workflows and async:false only when the parent must block. Use explicit return, top-level await, plain helper functions, or explicit Promise chains. Nested async function, arrow, and method helpers are rejected. Globals: runs, emit, console, and mission state when enabled. No filesystem, shell, Pi tools, or host globals except through runs.host." })),
349
- workflowScriptPath: Type.Optional(Type.String({ minLength: 1, description: "Path to a JavaScript workflow file with unknown resource provenance. Mutually exclusive with workflowScript and workflow. Relative paths resolve against the request cwd. The host reads the file before the filesystem-free workflow sandbox starts." })),
335
+ workflow: Type.Optional(Type.String({ minLength: 1, description: "Extension-owned workflow resource." })),
336
+ args: Type.Optional(Type.Unsafe({ type: "object", maxProperties: 16, additionalProperties: true, description: "Bounded plain-JSON resource args." })),
337
+ workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Inline JavaScript statement body; raw/unknown provenance, no runs.host. Use explicit return and top-level await; see tool guidance/guide workflows." })),
338
+ workflowScriptPath: Type.Optional(Type.String({ minLength: 1, description: "Raw script file; host reads from request cwd before sandbox. Mutually exclusive with workflowScript and workflow." })),
350
339
  globalConcurrencyLimit: Type.Optional(Type.Integer({ minimum: 1 })),
351
340
  maxSubagentSpawnsPerRun: Type.Optional(Type.Integer({ minimum: 1 })),
352
341
  preflight: Type.Optional(WorkflowPreflightOverride),
353
- 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." })),
354
- isolation: Type.Optional(Type.String({ enum: ["none", "worktree"], description: "Workflow child isolation. none runs in the shared cwd; worktree requires managed git worktree isolation." })),
355
- 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." })),
342
+ chatProgress: Type.Optional(Type.String({ enum: ["auto", "off", "live-card"], description: "auto: live card only for watched foreground in same Git repository. live-card requires same-repo async:false; async: omit or auto/off." })),
343
+ isolation: Type.Optional(Type.String({ enum: ["none", "worktree"], description: "Shared cwd or managed git worktrees." })),
344
+ worktree: Type.Optional(Type.Boolean({ description: "Isolate each workflow child in a managed git worktree; child worktree:false overrides default." })),
356
345
  baseRef: Type.Optional(Type.String()),
357
346
  lane: Type.Optional(WorkflowLaneMetadata),
358
347
  context: Type.Optional(Type.String({
359
348
  enum: ["fresh", "fork", "profile"],
360
- description: "'fresh' or 'fork' to branch from parent session, or 'profile' to require the selected agent's declared defaultContext. Explicit fresh/fork overrides every child; profile ignores config defaultSubagentContext and fails when an agent has no defaultContext. If omitted, config defaultSubagentContext wins over each agent defaultContext; implicit fork needs a persisted parent session and leaf, else fresh. Config forkContext may prune resolved forks before spawn without adding another context value.",
349
+ description: "fresh/fork overrides every child; profile requires agent's declared defaultContext, ignoring config. Omitted: defaultSubagentContext wins over each agent defaultContext; implicit fork needs persisted parent + leaf, else fresh. forkContext may prune forks before spawn.",
361
350
  })),
362
- async: Type.Optional(Type.Boolean({ description: "Run in background unless asyncByDefault:false. Set false only when the parent must block until completion." })),
351
+ async: Type.Optional(Type.Boolean({ description: "Background; default asyncByDefault. false only to block parent." })),
363
352
  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." })),
364
- 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." })),
365
- 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." })),
353
+ maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs (same defaults)." })),
354
+ toolTimeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Per-tool deadline (ms); fast builtins default 5m." })),
366
355
  toolBudget: Type.Optional(ToolBudgetOverride),
367
356
  usageBudget: Type.Optional(UsageBudgetOverride),
368
- agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
369
- cwd: Type.Optional(Type.String({ description: "Execution cwd, or target project directory for project.open/status/close." })),
370
- artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: true)" })),
371
- includeProgress: Type.Optional(Type.Boolean({ description: "Include full progress in result (default: false)" })),
372
- share: Type.Optional(Type.Boolean({ description: "Upload session to GitHub Gist for sharing (default: false)" })),
357
+ agentScope: Type.Optional(Type.String({ description: "user/project/both (default); project wins collisions." })),
358
+ cwd: Type.Optional(Type.String({ description: "Execution/project-pane directory." })),
359
+ artifacts: Type.Optional(Type.Boolean({ description: "Debug artifacts; default true." })),
360
+ includeProgress: Type.Optional(Type.Boolean({ description: "Full result progress; default false." })),
361
+ share: Type.Optional(Type.Boolean({ description: "Upload session to GitHub Gist; default false." })),
373
362
  sessionDir: Type.Optional(
374
- Type.String({ description: "Directory to store session logs (default: temp; enables sessions even if share=false)" }),
363
+ Type.String({ description: "Session log directory; default temp, independent of share." }),
375
364
  ),
376
365
  control: Type.Optional(ControlOverrides),
377
366
  // Workflow defaults forwarded to each runs.run/runs.all child unless overridden there.
@@ -380,12 +369,12 @@ const SubagentParamProperties = {
380
369
  { type: "string" },
381
370
  { type: "boolean" },
382
371
  ],
383
- description: "Default child output file (string), or false to disable. Relative workflow child paths use managed artifact routing. Task filename prose is not an output declaration; for durable workflow handoff, return the child's outputReference, outputPathMapping, or artifactPaths.",
372
+ description: "Child output path or false; relative workflow paths use managed artifact routing. Bind durable output here, not task prose; return outputReference/outputPathMapping/artifactPaths.",
384
373
  })),
385
374
  outputMode: Type.Optional(OutputModeOverride),
386
375
  skill: Type.Optional(SkillOverride),
387
- model: Type.Optional(Type.String({ description: "Default child model override. Full provider/id values are accepted; bare ids resolve from the active registry. Append a thinking suffix (off/minimal/low/medium/high/xhigh/max, e.g. 'provider/id:low') to set the child's thinking level for the run; the suffix wins over the agent's thinking default." })),
388
- fast: Type.Optional(Type.Boolean({ description: "Opt into priority service tier for supported native OpenAI-Codex child models. Default false. This can increase quota or cost." })),
376
+ model: Type.Optional(Type.String({ description: "Child model provider/id; bare id only if unique. Suffix :off/minimal/low/medium/high/xhigh/max overrides agent thinking default." })),
377
+ fast: Type.Optional(Type.Boolean({ description: "Native OpenAI-Codex priority tier; default false, may cost more/quota." })),
389
378
  outputSchema: Type.Optional(JsonSchemaObject),
390
379
  agentContract: Type.Optional(AgentContractOverride),
391
380
  acceptance: Type.Optional(AcceptanceOverride),
@@ -5,95 +5,42 @@ import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
5
5
 
6
6
  const CUSTOM_TOOL_DESCRIPTION_FILE = "subagent-tool-description.md";
7
7
  const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
8
- const EXTERNAL_CLI_RUNNER_GUIDANCE = "External CLI agents (codex-exec, codex-exec-writer, claude-code, claude-code-writer, cursor-agent, cursor-agent-writer) use their own runner contract and do not support native Pi child options such as model override, structured output, acceptance/agent contract, tool budget, fast mode, fork context, skills, or native Pi tools unless the runner explicitly implements them.";
9
- const SUBAGENT_FAILURE_RECOVERY_GUIDANCE = "If a subagent workflow, child launch, prompt runtime, extension load, or child tooling setup fails, treat it as a lane infrastructure blocker—not permission to change execution mode. Stop and report the exact failure, run/status, and repo/cwd/worktree/branch/ref state; verify the worktree is clean or capture a partial diff before retrying or asking the owner. Retry or fix the subagent path only through a clear same-protocol retry. Do not silently switch to interactive_shell, pi -ne, Codex/Claude/Cursor CLI, a foreground agent, or another external mode. For backlog lanes and other subagent-governed workflows, external/foreground/CLI fallback requires explicit owner approval. Pi core may print a generic pi -ne extension-load hint; that out-of-repo hint is not protocol-approved fallback. interactive_shell remains valid when the user explicitly requests foreground/CLI work or the task is outside the governed subagent protocol.";
10
- const AGENT_SELECTION_GUIDANCE = "Before execution, call { action: \"list\", capabilities: true } and run only executable, non-disabled agents; for external-cli rows, also require runner.available === true. This is a passive PATH/PATHEXT/X_OK lookup, not authentication, version, or launch proof; launch preflight remains authoritative.";
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
- 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
- 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.";
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.";
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.";
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.";
18
-
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.`;
20
-
21
- export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflowScript call.";
22
-
23
- export const SUBAGENT_TOOL_PROMPT_GUIDELINES = [
24
- `Use subagent only when delegation is needed. ${AGENT_SELECTION_GUIDANCE}`,
25
- 'Omit action for execution; use { agent, task? } for one child. For multi-step or parallel work, make exactly one top-level { workflowScript, async: true } call and launch children only inside it. Use action only for management/control.',
26
- "workflowScript rejects nested async function, arrow, and method helpers; use top-level await, plain helper functions, or explicit Promise chains.",
27
- "Inside workflowScript, use runs.run/runs.all and await their results. runs.all returns an ordered array, not a key map; stored runs.run promises must later be observed with direct await, Promise.race, or Promise.all.",
28
- 'Keep one writer per cwd/worktree; isolate concurrent writers. For durable files, set output on runs.run/runs.all and return the child\'s outputReference, outputPathMapping, or artifactPaths. For advanced workflows, read the bundled pi-subagents skill or call { action: "guide", topic: "workflows" }.',
29
- ];
8
+ const AGENT_SELECTION_GUIDANCE = 'First call {action:"list",capabilities:true}: executable, non-disabled agents only; external-cli requires runner.available === true. Passive PATH/PATHEXT/X_OK is not authentication/version/launch proof; preflight is authoritative.';
9
+ const SUBAGENT_FAILURE_RECOVERY_GUIDANCE = "Workflow, child launch, prompt runtime, extension load or child tooling failure is a lane infrastructure blocker. Stop; report exact failure, run/status and repo/cwd/worktree/branch/ref; verify clean worktree or capture partial diff before same-protocol retry or asking the owner. Never silently switch to interactive_shell, pi -ne, Codex/Claude/Cursor CLI or foreground/external mode: governed-workflow fallback requires explicit owner approval, not Pi core's generic pi -ne hint. Explicit foreground/CLI requests and work outside that protocol remain valid.";
30
10
 
31
11
  export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
32
12
  • ${AGENT_SELECTION_GUIDANCE}
33
13
  • ${SUBAGENT_FAILURE_RECOVERY_GUIDANCE}
34
- • Keep execution and management separate: omit action for structured single-child or workflowScript execution; use action only for management/control.
35
- • 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. Ordinary async subagents notify this session natively, so return control and do not call bg_wait merely to get a completion wake. Do not sleep or poll status just to wait; use bg_wait only for provider, detached, or other background work without a native notification when this turn must receive its result.
36
- • ${WORKFLOW_RESUME_KEY_GUIDANCE}
37
- • ${WORKFLOW_OUTPUT_BINDING_GUIDANCE}
38
- • ${WORKFLOW_HOST_GUIDANCE}
39
- • Ordinary child subagents are not orchestrators. Only explicitly configured fanout children may use the child-safe subagent tool, still bounded by depth/session limits.
40
- • Oracle/advisor consultations should use supervisor dialogue for material unknowns when available; request one-shot only when desired.
41
- • 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.
42
- • Async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs, status via { action: "status", id }, and lifecycle diagnostics via { action: "debug.run", id }. Include output paths and residual risks when reporting results.`;
43
-
44
- export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task? }; use { workflowScript } for inline orchestration or { workflowScriptPath } to load it from the request cwd. The script inputs are mutually exclusive. Omit action for execution. Use action only for management/control actions.
45
-
46
- ${WORKFLOW_RESOURCE_GUIDANCE}
47
-
48
- EXECUTION:
49
- • ${EXTERNAL_CLI_RUNNER_GUIDANCE}
50
- • ${AGENT_SELECTION_GUIDANCE}
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.
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.
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.
54
- • ${WORKFLOW_LANES_GUIDANCE}
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.
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" }
57
- • 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}" }
58
- • Optional context is "fresh", "fork", or "profile". profile requires the selected agent's declared defaultContext and ignores config defaultSubagentContext. Explicit fresh/fork wins. When omitted, config defaultSubagentContext wins over agent defaultContext. Config forkContext can summarize transcript overflow with stable recovery refs before spawn without adding another public context value. 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.
59
- • 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}.
60
-
61
- MANAGEMENT / CONTROL (use action; omit execution fields):
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.
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.
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.
65
-
66
- ${SUBAGENT_SAFETY_GUIDANCE}`;
67
-
68
- export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task? }; use { workflowScript } for inline orchestration or { workflowScriptPath } to load it from the request cwd. The script inputs are mutually exclusive. Omit action for execution. Use action only for management/control actions.
14
+ • Omit action for execution. Multi-step/parallel work: exactly one top-level subagent workflow call with async:true; children launch only inside it.
15
+ • Async follows asyncByDefault (normally true); async:false only to block the parent, not for final reviews/gates. Consume results at dependency barriers. Native async completion wakes this session: return control, no sleep/poll or bg_wait merely for a wake. bg_wait is for provider/detached work without native notification needing a same-turn result.
16
+ • Ordinary child subagents are not orchestrators; only configured fanout within depth/session limits. One writer per cwd/worktree; isolate concurrent writers. Fresh-context read-only reviewers, then parent synthesis/fixes. Oracle/advisor unknowns use supervisor dialogue; one-shot only when requested.
17
+ • Bind durable output on runs.run/runs.all, not task filename prose; return actual outputReference/outputPathMapping/artifactPaths, evidence and residual risks.
18
+ • children.list: resume only resumable rows. {action:"resume",id,message} detaches a follow-up/challenge with stored agent/model/tool contract. If none is resumable, label a same-role fallback challenge. Scripts await runs.run(newKey,{resume:runId,task}); continue from latest returned runId. Each distinct resume pass needs a new stable key; same-key reuse requires identical launch parameters.
19
+ • Named resources own authority; raw workflowScript/workflowScriptPath cannot use runs.host. Granted commands/relative outputs use workflow cwd, never per-step cwd.
20
+ • Inspect asyncId/asyncDir (status.json, events.jsonl, logs) with status/debug.run; control with interrupt/stop/resume/steer. Read {action:"guide",topic:"tool-reference"} for controls/evidence gates.`;
21
+
22
+ const EXECUTION_GUIDANCE = `Delegate one child with {agent,task?}; otherwise choose exactly one of workflowScript, workflowScriptPath or {workflow,args}. agent/task exclude workflow inputs; task excludes action. agent may target management actions. action is management/control; validate accepts either script without launching. workflowScriptPath loads from request cwd before sandbox execution.
23
+ Scripts: JavaScript statement bodies with explicit return, top-level await, plain helpers/Promise chains; nested async function/arrow/method helpers are rejected. Await runs.run('key',{agent,task}) before .output; await runs.all([{key,agent,task},...]) for an ordered array, not a key map. Observe every stored run promise with direct await, Promise.race or Promise.all. Await/return runs.steer(key,message,options?) for a prior key, never raw run ids; queued/delivered/missed/failed receipts are not compliance proof.
24
+ Before advanced orchestration (runs.lanes, rolling fanout, mission state, handoffs), read {action:"guide",topic:"workflows"} or the pi-subagents skill. Sandbox: runs, emit, console, JavaScript and enabled mission state; no filesystem/shell/Pi tools/host globals. External CLI agents support native options only when their runner declares them; read guide tool-reference before passing model, structured output, acceptance/agentContract, tool budget, fast, fork context or skills/tools.
25
+ Model override: first call {action:"models"}; copy exact provider/id, not agent names. Thinking uses model suffix, not watchdog-only thinking.
26
+ Named resources: {workflow:'review',args:{task:'...'}} or {workflow:'run-ci',args:{command:'npm test'}}; args are bounded plain data. worktree:true requires clean source; baseRef defaults to HEAD at allocation or a supported named ref, never full 40/64-character commit IDs or revision expressions.`;
27
+
28
+ export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `${EXECUTION_GUIDANCE}\n\n${SUBAGENT_SAFETY_GUIDANCE}`;
29
+
30
+ export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflow call.";
31
+ export const SUBAGENT_TOOL_PROMPT_GUIDELINES = [
32
+ "Use subagent only when delegation is needed.",
33
+ ];
69
34
 
70
- ${WORKFLOW_RESOURCE_GUIDANCE}
35
+ export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = DEFAULT_SUBAGENT_TOOL_DESCRIPTION;
71
36
 
72
- EXECUTE:
73
- • ${EXTERNAL_CLI_RUNNER_GUIDANCE}
74
- • ${AGENT_SELECTION_GUIDANCE}
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.
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.
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.
78
- • ${WORKFLOW_LANES_GUIDANCE}
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.
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]"}
81
- • context can be fresh, fork, or profile. profile requires the selected agent's declared defaultContext and ignores defaultSubagentContext. Explicit fresh/fork wins; omitted context follows defaultSubagentContext before agent defaultContext. Config forkContext can summarize transcript overflow with stable recovery refs before spawn without adding another public context value. 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.
82
-
83
- MANAGE / CONTROL:
84
- • Use action without execution fields for list/get/models/guide/authoring, refine/refine.show/refine.rollback, mission, watchdog, status, interrupt, stop, resume, steer, worktree.cleanup (mode:'plan' only), script-only scheduling, diagnostics, and other management actions. guide reads shipped current-version docs by topic.
85
- • 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}.
86
-
87
- ASYNC / SAFETY:
88
- • ${SUBAGENT_FAILURE_RECOVERY_GUIDANCE}
89
- • 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. Ordinary async subagents notify this session natively, so return control and do not call bg_wait merely to get a completion wake. Do not sleep or poll merely to wait; use bg_wait only for provider, detached, or other background work without a native notification when this turn must receive its result.
90
- • ${WORKFLOW_RESUME_KEY_GUIDANCE}
91
- • ${WORKFLOW_OUTPUT_BINDING_GUIDANCE}
92
- • ${WORKFLOW_HOST_GUIDANCE}
93
- • Ordinary children are not orchestrators. Keep one writer per cwd/worktree and use fresh read-only reviewers for independent checks.
94
- • Oracle/advisor consultations use available supervisor dialogue for material unknowns; request one-shot when desired.
95
- • Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, and {action:"status",id:"..."}.`;
37
+ export const FULL_SUBAGENT_TOOL_DESCRIPTION = `${DEFAULT_SUBAGENT_TOOL_DESCRIPTION}
96
38
 
39
+ WORKFLOW DETAILS:
40
+ • runs.lanes([{key,stages:[{key,agent,task},{key,resume:'previous',task}]}]) runs first stages together, later stages sequentially per lane. Failures stay lane-local; only explicit structuredOutput.verdict === 'blocked' blocks a successful stage, never reviewer prose.
41
+ • Workflow child controls default onto runs.run/runs.all items; child fields override them. worktree:true isolates each child and returns handoff artifacts. usageBudget is shared across the workflow; already-running children are not stopped.
42
+ • Missions auto-attach unless mission:false; await state.get(key)/state.set(key,JSONValue) requires a mission. See guide topic missions. Omit acceptance for reviewer/read-only calls; acceptance.review.required requests independent writer review.
43
+ • Management discovery: list/get/models/guide; create/update/delete/eject/disable/enable/reset/refine; mission.*, schedule.*, watchdog.*, inspector.*, project.*, lane.status/recordMerge/recordSupersession; worktree.discard and plan-only worktree.cleanup; doctor and grant-spawn-budget. Use guide topics agents, missions, observability, tool-reference, configuration, models, watchdog or extension-api for exact action fields. Schedules take script inputs, not direct children; recipes live in the missions guide.`;
97
44
 
98
45
  function isToolDescriptionMode(value: unknown): value is ToolDescriptionMode {
99
46
  return value === "full" || value === "compact" || value === "custom";
@@ -0,0 +1,148 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
5
+ import { readMissionBinding } from "../missions/lifecycle.ts";
6
+ import { listMissions, missionRecordPath, resolveMissionStoreLocation } from "../missions/store.ts";
7
+ import type { MissionStoreConfig } from "../missions/types.ts";
8
+ import { resolveAuthorityDecision, type AuthorityPolicyConfig } from "../policy/authority.ts";
9
+ import { DIRS, type Details, type SubagentState } from "../shared/types.ts";
10
+ import { readStatus } from "../shared/utils.ts";
11
+ import { resolveSubagentRunId } from "../runs/background/run-id-resolver.ts";
12
+ import { resolveNodeExecutable } from "../shared/node-executable.ts";
13
+ import { encodeSessionRoots } from "./session-roots-codec.ts";
14
+ import { formatShellCommand } from "./shell-command.ts";
15
+ import type { InspectorAction, InspectorContext, InspectorLaunch, InspectorParams, InspectorPlugin, InspectorTarget } from "./types.ts";
16
+
17
+ export { INSPECTOR_ACTIONS } from "./types.ts";
18
+ export type { InspectorAction, InspectorParams, InspectorPlugin } from "./types.ts";
19
+
20
+ function result(text: string, isError = false): AgentToolResult<Details> {
21
+ const response: AgentToolResult<Details> = {
22
+ content: [{ type: "text", text }],
23
+ details: { mode: "management", results: [] },
24
+ };
25
+ if (isError) response.isError = true;
26
+ return response;
27
+ }
28
+
29
+ function pathWithin(base: string, candidate: string): boolean {
30
+ const resolvedBase = path.resolve(base);
31
+ const resolvedCandidate = path.resolve(candidate);
32
+ return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`);
33
+ }
34
+ function trustedDir(dir: string, deps: InspectorDispatcherDeps): boolean {
35
+ try {
36
+ if (fs.lstatSync(dir).isSymbolicLink() || !fs.statSync(dir).isDirectory()) return false;
37
+ const real = fs.realpathSync(dir);
38
+ if ([...(deps.state?.asyncJobs.values() ?? []), ...(deps.state?.fleetJobs?.values() ?? [])].some((job) => {
39
+ try {
40
+ return fs.realpathSync(job.asyncDir) === real;
41
+ } catch {
42
+ return false;
43
+ }
44
+ })) return true;
45
+ const root = deps.asyncDirRoot ?? DIRS.async;
46
+ return fs.existsSync(root) && pathWithin(root, dir) && pathWithin(fs.realpathSync(root), real);
47
+ } catch { return false; }
48
+ }
49
+ function resolveTarget(params: InspectorParams, deps: InspectorDispatcherDeps): InspectorTarget | { error: string } {
50
+ const requested = params.id ?? params.runId;
51
+ let runId: string;
52
+ let asyncDir: string;
53
+ if (params.dir) {
54
+ asyncDir = path.resolve(params.dir);
55
+ if (!trustedDir(asyncDir, deps)) return { error: `Async run directory '${asyncDir}' is outside trusted run roots.` };
56
+ const status = readStatus(asyncDir);
57
+ if (!status) return { error: `No async run status found in '${asyncDir}'.` };
58
+ if (requested && requested !== status.runId && !status.runId.startsWith(requested)) return { error: `Run '${requested}' does not match status run '${status.runId}'.` };
59
+ runId = status.runId;
60
+ } else {
61
+ if (!requested) return { error: "Inspector actions require id or dir." };
62
+ try {
63
+ const found = resolveSubagentRunId(requested, { state: deps.state, asyncDirRoot: deps.asyncDirRoot ?? DIRS.async, resultsDir: deps.resultsDir ?? DIRS.results });
64
+ if (!found) return { error: `No subagent run found for '${requested}'.` };
65
+ if (found.kind !== "async" || !found.location.asyncDir) return { error: `Run '${found.id}' is not an inspectable async run with lifecycle artifacts.` };
66
+ runId = found.id;
67
+ asyncDir = found.location.asyncDir;
68
+ } catch (cause) {
69
+ return { error: cause instanceof Error ? cause.message : String(cause) };
70
+ }
71
+ }
72
+ const status = readStatus(asyncDir);
73
+ if (!status) return { error: `No lifecycle status exists for async run '${runId}'.` };
74
+ if (params.index !== undefined && (params.index < 0 || params.index >= (status.steps?.length ?? 0))) return { error: `Async run '${runId}' has ${status.steps?.length ?? 0} children. Index ${params.index} is out of range.` };
75
+ const target: InspectorTarget = { runId, asyncDir, status: { cwd: status.cwd, state: status.state, steps: status.steps } };
76
+ if (params.index !== undefined) target.index = params.index;
77
+ return target;
78
+ }
79
+ function missionFor(target: InspectorTarget, deps: InspectorDispatcherDeps): { id: string; path: string } | undefined {
80
+ try {
81
+ const binding = readMissionBinding(target.asyncDir);
82
+ if (binding) return { id: binding.missionId, path: missionRecordPath(binding.location, binding.missionId) };
83
+ const location = resolveMissionStoreLocation(deps.missions ? { projectRoot: deps.cwd, config: deps.missions } : { projectRoot: deps.cwd });
84
+ const mission = listMissions(location).records.find((record) => record.runs.some((run) => run.runId === target.runId));
85
+ return mission ? { id: mission.id, path: missionRecordPath(location, mission.id) } : undefined;
86
+ } catch {
87
+ return undefined;
88
+ }
89
+ }
90
+ function launchFor(target: InspectorTarget, deps: InspectorDispatcherDeps): InspectorLaunch {
91
+ const mission = missionFor(target, deps);
92
+ const runnerPath = deps.runnerPath ?? fileURLToPath(new URL("../../inspector-runner.mjs", import.meta.url));
93
+ const job = deps.state?.asyncJobs.get(target.runId) ?? deps.state?.fleetJobs?.get(target.runId);
94
+ const sessionRoots = [...new Set([...(deps.sessionRoots ?? deps.state?.trustedSessionRoots ?? []), ...(job?.sessionRoot ? [job.sessionRoot] : [])])];
95
+ const allowSteer = resolveAuthorityDecision({ action: "steerRun", policy: deps.authorityPolicy }) === "auto";
96
+ const allowStop = resolveAuthorityDecision({ action: "stopRun", policy: deps.authorityPolicy }) === "auto";
97
+ const argv = [runnerPath, "--async-dir", target.asyncDir, "--run-id", target.runId, "--allow-steer", String(allowSteer), "--allow-stop", String(allowStop), "--session-roots", encodeSessionRoots(sessionRoots)];
98
+ if (target.index !== undefined) argv.push("--index", String(target.index));
99
+ if (mission) argv.push("--mission-path", mission.path);
100
+ const executable = resolveNodeExecutable();
101
+ const launch: InspectorLaunch = { executable, argv, displayCommand: formatShellCommand(executable, argv), allowSteer, allowStop, sessionRoots };
102
+ if (mission) launch.mission = mission;
103
+ return launch;
104
+ }
105
+
106
+ export interface InspectorDispatcherDeps {
107
+ state?: SubagentState;
108
+ asyncDirRoot?: string;
109
+ resultsDir?: string;
110
+ missions?: MissionStoreConfig;
111
+ authorityPolicy?: AuthorityPolicyConfig;
112
+ sessionRoots?: string[];
113
+ cwd: string;
114
+ signal?: AbortSignal;
115
+ now?: () => Date;
116
+ runnerPath?: string;
117
+ env?: NodeJS.ProcessEnv;
118
+ plugins?: readonly InspectorPlugin[];
119
+ }
120
+ export async function handleInspectorAction(action: InspectorAction, params: InspectorParams, deps: InspectorDispatcherDeps): Promise<AgentToolResult<Details>> {
121
+ const target = resolveTarget(params, deps);
122
+ if ("error" in target) return result(target.error, true);
123
+ const context: InspectorContext = {
124
+ cwd: deps.cwd,
125
+ signal: deps.signal,
126
+ env: deps.env ?? process.env,
127
+ target,
128
+ };
129
+ if (deps.now) context.now = deps.now;
130
+ if (action === "inspector.command") return result(launchFor(target, deps).displayCommand);
131
+ const plugins = deps.plugins ?? [];
132
+ if (action === "inspector.open") {
133
+ for (const plugin of plugins) {
134
+ if (await plugin.available(context)) return plugin.open(context, launchFor(target, deps), params);
135
+ }
136
+ return result("No inspector plugin is available. Start a supported inspector host, or use inspector.command for a standalone command.", true);
137
+ }
138
+ const owner = plugins.find((plugin) => plugin.owns(context));
139
+ if (!owner) return result(`No inspector plugin owns this binding for async run ${target.runId}.`);
140
+ if (action === "inspector.status") {
141
+ return owner.status
142
+ ? owner.status(context)
143
+ : result(`Inspector plugin '${owner.name}' does not support status for async run ${target.runId}.`, true);
144
+ }
145
+ return owner.close
146
+ ? owner.close(context)
147
+ : result(`Inspector plugin '${owner.name}' does not support close for async run ${target.runId}.`, true);
148
+ }