pi-subagents 0.58.0 → 0.59.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 (105) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/docs/agents.md +5 -3
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +1 -1
  5. package/docs/models.md +24 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +89 -4
  8. package/docs/workflows.md +93 -2
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +1 -1
  12. package/skills/pi-subagents/SKILL.md +1 -1
  13. package/skills/pi-subagents/references/execution-controls.md +5 -1
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -2
  15. package/skills/pi-subagents/references/prompting-and-roles.md +24 -3
  16. package/src/agents/agent-management.ts +5 -17
  17. package/src/agents/agent-serializer.ts +4 -2
  18. package/src/agents/agents.ts +67 -26
  19. package/src/agents/runtime-agent-registry.ts +9 -16
  20. package/src/api/background-work.ts +5 -1
  21. package/src/api/delegation.ts +0 -7
  22. package/src/api/preflight.ts +6 -9
  23. package/src/extension/fanout-child.ts +5 -3
  24. package/src/extension/index.ts +51 -23
  25. package/src/extension/public-execution.ts +15 -2
  26. package/src/extension/schemas.ts +35 -10
  27. package/src/extension/tool-description.ts +18 -3
  28. package/src/intercom/result-intercom.ts +2 -0
  29. package/src/profiles/profiles.ts +5 -6
  30. package/src/runs/background/active-async-capacity.ts +2 -2
  31. package/src/runs/background/async-execution.ts +50 -35
  32. package/src/runs/background/async-job-tracker.ts +14 -12
  33. package/src/runs/background/async-resume.ts +23 -25
  34. package/src/runs/background/async-status-snapshot.ts +23 -261
  35. package/src/runs/background/async-status.ts +66 -7
  36. package/src/runs/background/chain-append.ts +6 -3
  37. package/src/runs/background/chain-root-attachment.ts +60 -8
  38. package/src/runs/background/fleet-view.ts +21 -11
  39. package/src/runs/background/notify.ts +158 -6
  40. package/src/runs/background/result-files.ts +2 -1
  41. package/src/runs/background/result-watcher.ts +2 -0
  42. package/src/runs/background/resume-guidance.ts +1 -1
  43. package/src/runs/background/retained-children.ts +1 -1
  44. package/src/runs/background/run-status.ts +18 -8
  45. package/src/runs/background/scheduled-runs.ts +86 -7
  46. package/src/runs/background/stale-run-reconciler.ts +10 -4
  47. package/src/runs/background/steering.ts +4 -14
  48. package/src/runs/background/subagent-runner.ts +346 -357
  49. package/src/runs/background/subagent-wait.ts +58 -10
  50. package/src/runs/background/terminal-run-index.ts +1 -1
  51. package/src/runs/background/wait-completions.ts +22 -1
  52. package/src/runs/background/wait-config.ts +23 -9
  53. package/src/runs/background/wait-tool.ts +9 -2
  54. package/src/runs/foreground/async-steering-action.ts +2 -2
  55. package/src/runs/foreground/execution.ts +114 -120
  56. package/src/runs/foreground/foreground-control.ts +3 -0
  57. package/src/runs/foreground/foreground-history.ts +1 -0
  58. package/src/runs/foreground/subagent-executor.ts +478 -239
  59. package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
  60. package/src/runs/shared/abort-recovery.ts +119 -0
  61. package/src/runs/shared/async-status-projection.ts +463 -0
  62. package/src/runs/shared/child-identity.ts +19 -4
  63. package/src/runs/shared/child-launch-plan.ts +151 -0
  64. package/src/runs/shared/completion-evidence.ts +89 -0
  65. package/src/runs/shared/completion-guard.ts +1 -1
  66. package/src/runs/shared/dynamic-fanout.ts +2 -2
  67. package/src/runs/shared/host-step-status.ts +230 -0
  68. package/src/runs/shared/lane-metadata.ts +105 -0
  69. package/src/runs/shared/mcp-config-sources.ts +42 -6
  70. package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
  71. package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  72. package/src/runs/shared/model-fallback.ts +9 -2
  73. package/src/runs/shared/nested-events.ts +6 -2
  74. package/src/runs/shared/nested-render.ts +7 -3
  75. package/src/runs/shared/parallel-handoff.ts +419 -7
  76. package/src/runs/shared/parallel-utils.ts +7 -0
  77. package/src/runs/shared/pi-args.ts +20 -2
  78. package/src/runs/shared/single-output.ts +27 -4
  79. package/src/runs/shared/subagent-prompt-runtime.ts +13 -4
  80. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  81. package/src/runs/shared/worktree.ts +18 -0
  82. package/src/shared/child-session-name.ts +46 -0
  83. package/src/shared/extension-context.ts +24 -0
  84. package/src/shared/formatters.ts +5 -2
  85. package/src/shared/launch-contract.ts +1 -1
  86. package/src/shared/settings.ts +9 -103
  87. package/src/shared/types.ts +198 -29
  88. package/src/shared/utils.ts +35 -55
  89. package/src/slash/delegation-adapters.ts +1 -8
  90. package/src/slash/delegation-request.ts +0 -4
  91. package/src/slash/slash-bridge.ts +1 -2
  92. package/src/slash/slash-commands.ts +369 -90
  93. package/src/slash/slash-live-state.ts +22 -11
  94. package/src/tui/fleet-status.ts +125 -74
  95. package/src/tui/fleet.ts +11 -5
  96. package/src/tui/render.ts +316 -27
  97. package/src/watchdog/turn-delta.ts +1 -1
  98. package/src/workflows/chat-progress.ts +6 -3
  99. package/src/workflows/host-command.ts +230 -0
  100. package/src/workflows/scripted-workflow.ts +452 -38
  101. package/src/workflows/workflow-child-summary.ts +9 -5
  102. package/src/workflows/workflow-preflight.ts +270 -0
  103. package/src/workflows/workflow-receipt.ts +43 -4
  104. package/src/workflows/workflow-settlement.ts +246 -0
  105. package/src/runs/shared/turn-budget.ts +0 -98
@@ -4,7 +4,11 @@ export const BACKGROUND_WORK_REGISTRY_KEY = "pi-subagents.background-work.v1";
4
4
  const MAX_PROVIDER_NAME_LENGTH = 128;
5
5
  const MAX_PROVIDERS = 100;
6
6
  const MAX_ITEM_ID_LENGTH = 256;
7
- const MAX_SESSION_ID_LENGTH = 256;
7
+ /**
8
+ * A Pi session id is the session file path, which routinely exceeds a short
9
+ * identity budget in nested worktrees, so it is bounded like the other paths.
10
+ */
11
+ const MAX_SESSION_ID_LENGTH = 4_096;
8
12
  const MAX_WAKE_CHANNEL_LENGTH = 256;
9
13
  const MAX_ITEMS_PER_PROVIDER = 10_000;
10
14
 
@@ -7,11 +7,6 @@ export const SUBAGENT_DELEGATION_UPDATE_EVENT = "prompt-template:subagent:update
7
7
  export const SUBAGENT_DELEGATION_RESPONSE_EVENT = "prompt-template:subagent:response";
8
8
  export const SUBAGENT_DELEGATION_CANCEL_EVENT = "prompt-template:subagent:cancel";
9
9
 
10
- export interface SubagentDelegationTurnBudget {
11
- maxTurns: number;
12
- graceTurns?: number;
13
- }
14
-
15
10
  export interface SubagentDelegationToolBudget {
16
11
  soft?: number;
17
12
  hard: number;
@@ -37,7 +32,6 @@ export interface SubagentDelegationRequest {
37
32
  model?: string;
38
33
  thinking?: SubagentDelegationThinking;
39
34
  timeoutMs?: number;
40
- turnBudget?: SubagentDelegationTurnBudget;
41
35
  toolBudget?: SubagentDelegationToolBudget;
42
36
  skill?: string | string[] | boolean;
43
37
  artifacts?: boolean;
@@ -69,7 +63,6 @@ export type SubagentDelegationStatus =
69
63
  | "timed_out"
70
64
  | "cancelled"
71
65
  | "interrupted"
72
- | "turn_budget_exhausted"
73
66
  | "tool_budget_exhausted"
74
67
  | "structured_output_failed"
75
68
  | "acceptance_failed"
@@ -14,9 +14,7 @@ import { resolveEffectiveThinking } from "../shared/model-info.ts";
14
14
  import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV, type ThinkingLevel } from "../shared/thinking-ceiling.ts";
15
15
  import { SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, type ArtifactDirPreference, type ArtifactPaths, type JsonSchemaObject, type OutputMode } from "../shared/types.ts";
16
16
  import { capabilityCeilingAgentRestrictionMessage, intersectSubagentCapabilityCeilings, type ResolvedSubagentCapabilityCeiling, type SubagentCapabilityAudit } from "../runs/shared/capability-ceiling.ts";
17
- import { appendTurnBudgetSystemPrompt } from "../runs/shared/turn-budget.ts";
18
17
  import { resolvePermissionRules } from "../runs/shared/permissions.ts";
19
- import type { ResolvedTurnBudget } from "../shared/types.ts";
20
18
  import type { ResolvedMcpDirectToolSelection } from "../runs/shared/mcp-direct-tool-allowlist.ts";
21
19
  import { resolveStepBehavior } from "../shared/settings.ts";
22
20
  import { canPreferForkFromSnapshot, resolveSubagentLaunchContext } from "../shared/fork-context.ts";
@@ -69,7 +67,6 @@ export interface SubagentLaunchContractInput {
69
67
  outputMode?: OutputMode;
70
68
  outputSchema?: JsonSchemaObject;
71
69
  extensionBindings?: ExtensionBindings;
72
- turnBudget?: ResolvedTurnBudget;
73
70
  artifacts?: boolean;
74
71
  artifactDir?: ArtifactDirPreference;
75
72
  parentSessionFile?: string | null;
@@ -225,8 +222,8 @@ function taskWorkspaceScopeAuthorityDiagnostic(task: string | undefined): Subage
225
222
  };
226
223
  }
227
224
 
228
- function candidateList(inputAgent: string, selected: AgentConfig | undefined, cwd: string): SubagentLaunchContractAgentCandidate[] {
229
- const all = discoverAgentsAll(cwd);
225
+ function candidateList(inputAgent: string, selected: AgentConfig | undefined, cwd: string, provider?: string): SubagentLaunchContractAgentCandidate[] {
226
+ const all = discoverAgentsAll(cwd, provider);
230
227
  return [...all.builtin, ...all.package, ...all.user, ...all.project]
231
228
  .filter((agent) => Boolean(resolveAgentName(inputAgent, [agent]).agent))
232
229
  .map((agent) => ({
@@ -260,7 +257,8 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
260
257
  return { ok: false, code: "invalid_artifact_dir", message: `Unsupported artifactDir '${String(input.artifactDir)}'; expected 'project', 'session', or 'temp'.`, diagnostics };
261
258
  }
262
259
  const scope = resolveExecutionAgentScope(input.agentScope);
263
- const discovered = discoverAgents(effectiveCwd, scope);
260
+ const parentProvider = input.preferredProvider ?? input.parentModel?.provider;
261
+ const discovered = discoverAgents(effectiveCwd, scope, parentProvider);
264
262
  const resolvedAgent = resolveAgentName(input.agent, discovered.agents);
265
263
  const ambiguousCandidates = resolvedAgent.error
266
264
  ? discovered.agents.filter((agent) => resolveAgentName(input.agent, [agent]).agent)
@@ -353,6 +351,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
353
351
  try {
354
352
  toolPlan = resolvePiLaunchToolPlan({
355
353
  tools: agent.tools,
354
+ allowNestedSubagents: agent.allowNestedSubagents,
356
355
  extensions: agent.extensions,
357
356
  subagentOnlyExtensions: agent.subagentOnlyExtensions,
358
357
  mcpDirectTools: agent.mcpDirectTools,
@@ -398,9 +397,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
398
397
  const memoryInjection = buildAgentMemoryInjection(agent, effectiveCwd);
399
398
  if (memoryInjection) effectiveSystemPrompt = effectiveSystemPrompt ? `${effectiveSystemPrompt}\n\n${memoryInjection}` : memoryInjection;
400
399
  effectiveSystemPrompt = injectOutputPathSystemPrompt(effectiveSystemPrompt, outputPath, agent);
401
- const turnBudget = input.turnBudget ?? agent.defaultTurnBudget;
402
- effectiveSystemPrompt = appendTurnBudgetSystemPrompt(effectiveSystemPrompt, turnBudget);
403
- const candidates = candidateList(input.agent, agent, effectiveCwd);
400
+ const candidates = candidateList(input.agent, agent, effectiveCwd, parentProvider);
404
401
  const shadowedCandidates = candidates.filter((candidate) => !candidate.selected);
405
402
  const definitionDigest = agentDefinitionDigest(agent);
406
403
  const contractBase: Omit<SubagentLaunchContract, "digest"> = {
@@ -158,13 +158,15 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
158
158
  registeredApis.add(pi);
159
159
 
160
160
  const config = loadConfig();
161
+ const waitToolConfig = resolveWaitToolConfig(config.waitTool);
161
162
  const state = createChildSafeState();
162
163
  const executor = createSubagentExecutor({
163
164
  pi,
164
165
  state,
165
166
  config,
166
167
  asyncByDefault: resolveAsyncByDefault(config),
167
- waitToolEnabled: resolveWaitToolConfig(config.waitTool).enabled,
168
+ waitToolEnabled: waitToolConfig.enabled,
169
+ waitToolDefaultTimeoutMs: waitToolConfig.defaultTimeoutMs,
168
170
  tempArtifactsDir: getArtifactsDir(null),
169
171
  getSubagentSessionRoot,
170
172
  expandTilde,
@@ -178,8 +180,8 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
178
180
  label: "Subagent",
179
181
  description: [
180
182
  "Delegate to subagents from child-safe fanout mode.",
181
- "Allowed management/control actions: list, get, status, interrupt, resume, steer, doctor.",
182
- "Mutating management actions (create, update, delete, eject, disable, enable, reset, grant-spawn-budget) are blocked in this mode.",
183
+ "Allowed management/control actions: list, get, status, lane.status, interrupt, resume, steer, doctor.",
184
+ "Mutating management actions (create, update, delete, eject, disable, enable, reset, grant-spawn-budget, lane.recordMerge, lane.recordSupersession) are blocked in this mode.",
183
185
  ].join("\n"),
184
186
  parameters: params,
185
187
  async execute(id, params, signal, onUpdate, ctx) {
@@ -26,6 +26,7 @@ import { ensureAccessibleDir } from "../shared/accessible-dir.ts";
26
26
  import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
27
27
  import { resolveCurrentSessionId } from "../shared/session-identity.ts";
28
28
  import { getAgentDir } from "../shared/utils.ts";
29
+ import { isStaleExtensionContextError, withCachedUiContext } from "../shared/extension-context.ts";
29
30
  import { currentCompletionOwnerId } from "../shared/completion-owner.ts";
30
31
  import { cleanupOldChainDirs } from "../shared/settings.ts";
31
32
  import { clearLegacyResultAnimationTimer, renderSubagentResult, renderSubagentSummary } from "../tui/render.ts";
@@ -49,7 +50,6 @@ import { registerHerdrStatusBridge, type HerdrStatusRun } from "../integrations/
49
50
  import { listHerdrProjectPaneRoots, restoreHerdrProjectPaneSnapshots } from "../inspectors/herdr/project-panes.ts";
50
51
  import { registerSubagentRpcBridge } from "./rpc.ts";
51
52
  import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
52
- import { inspectSubagentStatus } from "../runs/background/run-status.ts";
53
53
  import { resolveWaitToolConfig } from "../runs/background/subagent-wait.ts";
54
54
  import { registerWaitTool } from "../runs/background/wait-tool.ts";
55
55
  import { createWaitSubscriptionManager } from "../runs/background/wait-subscriptions.ts";
@@ -61,6 +61,7 @@ import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capabili
61
61
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
62
62
  import { applyModelExclusionsConfig, loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
63
63
  import { buildSubagentToolDescription, buildSubagentToolPromptMetadata } from "./tool-description.ts";
64
+ import { formatWorkflowPreflightSummary, normalizeWorkflowPreflight } from "../workflows/workflow-preflight.ts";
64
65
  import { finalizeToolResult } from "./tool-result.ts";
65
66
  import { collectGoalContinuationNotices } from "../missions/goal-driver.ts";
66
67
  import { restoreForegroundRunHistory } from "../runs/foreground/foreground-history.ts";
@@ -251,15 +252,25 @@ function workflowLaneKeys(script: string): string[] {
251
252
  return keys;
252
253
  }
253
254
 
254
- function formatWorkflowManifest(script: string, async: unknown, clarify: unknown): string {
255
+ function formatWorkflowPreflightCall(input: unknown): string {
256
+ if (input === undefined) return "";
257
+ try {
258
+ return formatWorkflowPreflightSummary(normalizeWorkflowPreflight(input));
259
+ } catch (error) {
260
+ return `preflight · rejected: ${error instanceof Error ? error.message : String(error)}`;
261
+ }
262
+ }
263
+
264
+ function formatWorkflowManifest(script: string, async: unknown, clarify: unknown, preflightInput?: unknown): string {
255
265
  if (clarify === true) return "workflow script · rejected: clarify UI unsupported";
256
266
  const keys = workflowLaneKeys(script);
257
267
  // The workflow executor starts background work unless callers pass async:false.
258
268
  const mode = async === false ? "foreground" : "background";
259
- if (keys.length === 0) return `workflow script · ${mode}`;
269
+ const preflight = formatWorkflowPreflightCall(preflightInput);
270
+ if (keys.length === 0) return `workflow script · ${mode}${preflight ? ` · ${preflight}` : ""}`;
260
271
  const visibleKeys = keys.slice(0, 4).join(", ");
261
272
  const remainder = keys.length > 4 ? `, +${keys.length - 4}` : "";
262
- return `workflow · ${mode} · ${keys.length} lane${keys.length === 1 ? "" : "s"}: ${visibleKeys}${remainder}`;
273
+ return `workflow · ${mode} · ${keys.length} lane${keys.length === 1 ? "" : "s"}: ${visibleKeys}${remainder}${preflight ? ` · ${preflight}` : ""}`;
263
274
  }
264
275
 
265
276
  /**
@@ -292,10 +303,6 @@ function isSlashResultError(result: { details?: Details }): boolean {
292
303
  return result.details?.results.some((entry) => entry.exitCode !== 0 && entry.progress?.status !== "running") || false;
293
304
  }
294
305
 
295
- function isStaleExtensionContextError(error: unknown): boolean {
296
- return error instanceof Error && error.message.includes("Extension context no longer active");
297
- }
298
-
299
306
  function rebuildSlashResultContainer(
300
307
  container: Container,
301
308
  result: AgentToolResult<Details>,
@@ -464,6 +471,12 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
464
471
  clear: () => {},
465
472
  },
466
473
  };
474
+ const withLastUiContext = <T>(run: (ctx: ExtensionContext) => T): T | undefined => {
475
+ const cached = state.lastUiContext;
476
+ return withCachedUiContext(cached, () => {
477
+ if (state.lastUiContext === cached) state.lastUiContext = null;
478
+ }, run);
479
+ };
467
480
 
468
481
  const supervisorChannel = createNativeSupervisorChannel(pi, state);
469
482
  const waitSubscriptionManager = createWaitSubscriptionManager(pi, state);
@@ -472,9 +485,17 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
472
485
  const completionNotifier = registerSubagentNotify(pi, state, { batchConfig: config.completionBatch, ownership: resultDeliveryOwnership });
473
486
  const fleetStatus = fleetViewEnabled
474
487
  ? new SubagentFleetStatus(state, async (itemKey) => {
475
- const ctx = state.lastUiContext;
476
- if (!ctx?.hasUI) return;
477
- await openSubagentFleet(ctx, state, { initialKey: itemKey, asyncDirRoot: DIRS.async, resultsDir: DIRS.results, fleetKeybindings: config.fleetKeybindings });
488
+ const ctx = withLastUiContext((current) => current);
489
+ if (!ctx) return;
490
+ try {
491
+ await openSubagentFleet(ctx, state, { initialKey: itemKey, asyncDirRoot: DIRS.async, resultsDir: DIRS.results, fleetKeybindings: config.fleetKeybindings });
492
+ } catch (error) {
493
+ if (isStaleExtensionContextError(error)) {
494
+ if (state.lastUiContext === ctx) state.lastUiContext = null;
495
+ return;
496
+ }
497
+ throw error;
498
+ }
478
499
  }, { placement: fleetViewPlacement })
479
500
  : undefined;
480
501
  let executorScheduled: ((id: string, params: SubagentParamsLike, signal: AbortSignal, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>) | undefined;
@@ -503,10 +524,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
503
524
  if (scheduledRunManager.observedCompletionRunIds().size > 0) return true;
504
525
  return missionObserverResultCandidateFiles(DIRS.results).length > 0;
505
526
  };
506
- const discoverAgentsForRuntime = (cwd: string, scope: AgentScope) => {
507
- const discovered = discoverAgents(cwd, scope);
527
+ const discoverAgentsForRuntime = (cwd: string, scope: AgentScope, preferredModelProvider?: string) => {
528
+ const discovered = discoverAgents(cwd, scope, preferredModelProvider);
508
529
  if (listRuntimeAgentConfigs(pi).length === 0) return discovered;
509
- const all = discoverAgentsAll(cwd);
530
+ const all = discoverAgentsAll(cwd, preferredModelProvider);
510
531
  const configuredAgents: AgentConfig[] = [
511
532
  ...all.builtin,
512
533
  ...all.package,
@@ -566,6 +587,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
566
587
  config,
567
588
  asyncByDefault,
568
589
  waitToolEnabled: waitToolConfig.enabled,
590
+ waitToolDefaultTimeoutMs: waitToolConfig.defaultTimeoutMs,
569
591
  handleScheduledRunAction: (params, ctx) => scheduledRunManager.handleToolCall(params, ctx),
570
592
  watchdog: mainWatchdog,
571
593
  tempArtifactsDir,
@@ -653,7 +675,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
653
675
 
654
676
  const slashBridge = registerSlashSubagentBridge({
655
677
  events: pi.events,
656
- asyncByDefault,
657
678
  getContext: () => state.lastUiContext,
658
679
  execute: (id, params, signal, onUpdate, ctx) =>
659
680
  executeSubagentCollapsed(id, params, signal, onUpdate, ctx),
@@ -702,13 +723,13 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
702
723
  }
703
724
  if (args.workflowScript)
704
725
  return new Text(
705
- `${title}${gap}${formatWorkflowManifest(args.workflowScript, args.async, false)}`,
726
+ `${title}${gap}${formatWorkflowManifest(args.workflowScript, args.async, false, args.preflight)}`,
706
727
  0,
707
728
  0,
708
729
  );
709
730
  if (args.workflowScriptPath)
710
731
  return new Text(
711
- `${title}${gap}${theme.fg("accent", args.workflowScriptPath)}${args.async === true ? `${gap}${theme.fg("warning", "[async]")}` : ""}`,
732
+ `${title}${gap}${theme.fg("accent", args.workflowScriptPath)}${args.async === true ? `${gap}${theme.fg("warning", "[async]")}` : ""}${args.preflight !== undefined ? `${gap}${theme.fg("dim", formatWorkflowPreflightCall(args.preflight))}` : ""}`,
712
733
  0,
713
734
  0,
714
735
  );
@@ -732,7 +753,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
732
753
 
733
754
  pi.registerTool(tool);
734
755
 
735
- registerWaitTool(pi, state, waitToolConfig.enabled, waitSubscriptionManager);
756
+ registerWaitTool(pi, state, waitToolConfig.enabled, waitSubscriptionManager, waitToolConfig.defaultTimeoutMs);
736
757
 
737
758
  pi.on("agent_end", async (_event, ctx) => {
738
759
  if (!ctx.hasUI) await drainOutstandingWork({ state, events: pi.events });
@@ -755,7 +776,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
755
776
  }
756
777
  });
757
778
 
758
- registerSlashCommands(pi, state, {
779
+ const disposeSlashCommands = registerSlashCommands(pi, state, {
759
780
  fleetKeybindings: config.fleetKeybindings,
760
781
  foregroundDetachShortcut: config.foregroundDetachShortcut,
761
782
  });
@@ -837,14 +858,13 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
837
858
  const suspendWidgetsForCompaction = () => {
838
859
  if (state.widgetsSuspended) return;
839
860
  state.widgetsSuspended = true;
840
- if (state.lastUiContext?.hasUI) state.lastUiContext.ui.setWidget(WIDGET_KEY, undefined);
861
+ withLastUiContext((ctx) => ctx.ui.setWidget(WIDGET_KEY, undefined));
841
862
  fleetStatus?.refresh();
842
863
  };
843
864
  const resumeWidgetsAfterCompaction = () => {
844
865
  if (!state.widgetsSuspended) return;
845
866
  state.widgetsSuspended = false;
846
- const ctx = state.lastUiContext;
847
- if (ctx?.hasUI) refreshWidget(ctx);
867
+ withLastUiContext((ctx) => refreshWidget(ctx));
848
868
  fleetStatus?.refresh();
849
869
  };
850
870
 
@@ -939,6 +959,13 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
939
959
  if (runtimeCleaned) return;
940
960
  runtimeCleaned = true;
941
961
  const shuttingDownParentSession = parentSessionEnvValue;
962
+ // Workflow continuations retain their launch context; abort them before
963
+ // teardown so a reload cannot launch through a stale context.
964
+ for (const controller of state.workflowControllers?.values() ?? []) {
965
+ if (!controller.signal.aborted) controller.abort(new Error("Workflow stopped because the extension session was replaced or reloaded."));
966
+ }
967
+ state.workflowControllers?.clear();
968
+ state.workflowChildStops?.clear();
942
969
  clearRuntimeAgentsForPi(pi);
943
970
  clearTimeout(resultIndexCleanupTimer);
944
971
  clearTimeout(asyncRetentionTimer);
@@ -962,6 +989,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
962
989
  // Best effort cleanup during shutdown or reload.
963
990
  }
964
991
  }
992
+ disposeSlashCommands.dispose();
965
993
  slashBridge.cancelAll();
966
994
  slashBridge.dispose();
967
995
  promptTemplateBridge.cancelAll();
@@ -1031,7 +1059,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
1031
1059
 
1032
1060
  pi.on("session_compact", () => {
1033
1061
  const hasActiveAsyncWork = [...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running");
1034
- if (!hasActiveAsyncWork || state.lastUiContext?.hasUI !== true) return;
1062
+ if (!hasActiveAsyncWork || !withLastUiContext(() => true)) return;
1035
1063
  pi.sendMessage(
1036
1064
  {
1037
1065
  customType: "subagent-compaction-resume",
@@ -1,7 +1,14 @@
1
1
  export interface PublicSubagentExecutionParams {
2
2
  action?: unknown;
3
+ mode?: unknown;
4
+ repo?: unknown;
5
+ planId?: unknown;
3
6
  agent?: unknown;
4
7
  task?: unknown;
8
+ handoffPath?: unknown;
9
+ laneId?: unknown;
10
+ merge?: unknown;
11
+ supersession?: unknown;
5
12
  step?: unknown;
6
13
  tasks?: unknown;
7
14
  chain?: unknown;
@@ -12,8 +19,10 @@ export interface PublicSubagentExecutionParams {
12
19
  config?: unknown;
13
20
  workflowScript?: unknown;
14
21
  workflowScriptPath?: unknown;
22
+ preflight?: unknown;
15
23
  isolation?: unknown;
16
24
  worktree?: unknown;
25
+ lane?: unknown;
17
26
  async?: unknown;
18
27
  output?: unknown;
19
28
  resume?: unknown;
@@ -22,6 +31,7 @@ export interface PublicSubagentExecutionParams {
22
31
  workflowKey?: unknown;
23
32
  workflowChildAsyncId?: unknown;
24
33
  workflowAwaitAsync?: unknown;
34
+ workflowAwaitDetached?: unknown;
25
35
  workflowParentDeadlineAt?: unknown;
26
36
  suppressRoutineResultIntercom?: unknown;
27
37
  runFanoutBudget?: unknown;
@@ -38,11 +48,14 @@ export type PublicSubagentExecutionNormalization<T> =
38
48
  * Enforce the public execution cutover before requests reach the executor.
39
49
  * Internal runs.run children and structured owned delegation bypass this boundary.
40
50
  */
41
- export function normalizePublicSubagentExecution<T extends PublicSubagentExecutionParams>(params: T, options: { asyncByDefault?: boolean } = {}): PublicSubagentExecutionNormalization<T> {
51
+ export function normalizePublicSubagentExecution<T extends PublicSubagentExecutionParams>(params: T): PublicSubagentExecutionNormalization<T> {
42
52
  if (params.workflowScript !== undefined && params.workflowScriptPath !== undefined) {
43
53
  return { ok: false, error: "workflowScript and workflowScriptPath are mutually exclusive.", mode: "workflow" };
44
54
  }
45
55
  const hasWorkflowInput = params.workflowScript !== undefined || params.workflowScriptPath !== undefined;
56
+ if (params.preflight !== undefined && !hasWorkflowInput) {
57
+ return { ok: false, error: "preflight requires workflowScript or workflowScriptPath.", mode: params.action === undefined ? "workflow" : "management" };
58
+ }
46
59
  const hasValidWorkflowInput = (typeof params.workflowScript === "string" && Boolean(params.workflowScript.trim()))
47
60
  || (typeof params.workflowScriptPath === "string" && Boolean(params.workflowScriptPath.trim()));
48
61
  if (params.isolation !== undefined) {
@@ -59,7 +72,7 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
59
72
  if (params.runFanoutBudget !== undefined || params.runFanoutAdmitted !== undefined) {
60
73
  return { ok: false, error: "Public execution does not accept internal run fan-out fields.", mode: hasWorkflowInput ? "workflow" : "management" };
61
74
  }
62
- if (params.workflowParentRunId !== undefined || params.workflowKey !== undefined || params.workflowChildAsyncId !== undefined || params.workflowAwaitAsync !== undefined || params.workflowParentDeadlineAt !== undefined || params.suppressRoutineResultIntercom !== undefined) {
75
+ if (params.workflowParentRunId !== undefined || params.workflowKey !== undefined || params.workflowChildAsyncId !== undefined || params.workflowAwaitAsync !== undefined || params.workflowAwaitDetached !== undefined || params.workflowParentDeadlineAt !== undefined || params.suppressRoutineResultIntercom !== undefined) {
63
76
  return { ok: false, error: "Public execution does not accept internal workflow child fields.", mode: hasWorkflowInput ? "workflow" : "management" };
64
77
  }
65
78
  const action = params.action;
@@ -101,10 +101,14 @@ const ChainGateOverride = Type.String({
101
101
  description: "For chain steps with agentContract, choose whether the chain advances on execution success or acceptance success. Defaults to execution.",
102
102
  });
103
103
 
104
- const TurnBudgetOverride = Type.Object({
105
- maxTurns: Type.Integer({ minimum: 1 }),
106
- graceTurns: Type.Optional(Type.Integer({ minimum: 0 })),
107
- }, { additionalProperties: false, description: "Optional assistant-turn budget. At maxTurns the child is asked to wrap up; after graceTurns additional assistant turns it is aborted and partial output is returned." });
104
+ const WorkflowLaneMetadata = Type.Object({
105
+ version: Type.Integer({ minimum: 1, maximum: 1 }),
106
+ key: Type.String({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$" }),
107
+ mode: Type.Optional(Type.String({ enum: ["mutation", "review", "scout", "gate"] })),
108
+ sourceRef: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),
109
+ claims: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 160 }), { maxItems: 20 })),
110
+ outputPaths: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 256 }), { maxItems: 10 })),
111
+ }, { additionalProperties: false, description: "Optional bounded child lane metadata. Display/triage only; sourceRef is opaque and never resolved during status rendering." });
108
112
 
109
113
  const ToolBudgetBlock = Type.Unsafe({
110
114
  anyOf: [
@@ -129,6 +133,21 @@ const UsageBudgetOverride = Type.Object({
129
133
  costUsd: Type.Optional(UsageBudgetLimitOverride),
130
134
  }, { additionalProperties: false, description: "Optional root-only reported-usage budget. Hard limits prevent future child launches; running children are not stopped." });
131
135
 
136
+ const WorkflowPreflightLane = Type.Object({
137
+ key: Type.String({ minLength: 1, maxLength: 128 }),
138
+ mode: Type.Optional(Type.String({ enum: ["mutation", "review", "scout", "gate"] })),
139
+ decision: Type.Optional(Type.String({ maxLength: 256 })),
140
+ claims: Type.Optional(Type.Array(Type.String({ maxLength: 256 }), { maxItems: 16 })),
141
+ expectedOutput: Type.Optional(Type.String({ maxLength: 256 })),
142
+ independence: Type.Optional(Type.String({ maxLength: 256 })),
143
+ }, { additionalProperties: false });
144
+
145
+ const WorkflowPreflightOverride = Type.Object({
146
+ version: Type.Integer({ minimum: 1, maximum: 1 }),
147
+ coverage: Type.Optional(Type.String({ enum: ["complete", "partial"] })),
148
+ lanes: Type.Array(WorkflowPreflightLane, { maxItems: 64 }),
149
+ }, { additionalProperties: false, description: "Bounded display-only lane hints for workflow launch/status. V1 coverage mismatches warn but never change launch authority or execution." });
150
+
132
151
  // Parallel task item (within a parallel step)
133
152
  export const ParallelTaskSchema = Type.Object({
134
153
  agent: Type.String(),
@@ -273,7 +292,12 @@ const SubagentParamProperties = {
273
292
  dir: Type.Optional(Type.String({
274
293
  description: "Async run directory for status/debug.run, stop, resume, or steer."
275
294
  })),
276
- handoffPath: Type.Optional(Type.String({ description: "worktree.discard manifest." })),
295
+ handoffPath: Type.Optional(Type.String({ description: "Existing parallel handoff manifest for worktree.discard, worktree.cleanup metadata, or lane evidence actions." })),
296
+ repo: Type.Optional(Type.String({ description: "Repository path for action='worktree.cleanup'; defaults to cwd." })),
297
+ planId: Type.Optional(Type.String({ description: "Cleanup plan id reserved for a future worktree.cleanup apply action." })),
298
+ laneId: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "Exact manifest run id for lane.status, lane.recordMerge, or lane.recordSupersession." })),
299
+ merge: Type.Optional(Type.Unsafe({ type: "object", additionalProperties: true, description: "Attested merge evidence for lane.recordMerge: prNumber, reviewedHead, mergeCommit, treeEquivalent, postMergeChecks, attestedBy, and attestedAt." })),
300
+ supersession: Type.Optional(Type.Unsafe({ type: "object", additionalProperties: true, description: "Attested replacement-lane evidence for lane.recordSupersession: supersededBy, attestedBy, and attestedAt." })),
277
301
  index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child or transcript." })),
278
302
  childId: Type.Optional(Type.String({ minLength: 1, maxLength: 256, description: "Stable child identity for child-scoped stop requests." })),
279
303
  view: Type.Optional(Type.String({
@@ -283,7 +307,7 @@ const SubagentParamProperties = {
283
307
  lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 500, description: "Maximum transcript lines for action='status', view='transcript'. Defaults to 80." })),
284
308
  topic: Type.Optional(Type.String()),
285
309
  message: Type.Optional(Type.String({ description: "Follow-up message for resume, live guidance for steer, or optional startup prompt for project.open." })),
286
- mode: Type.Optional(Type.String({ enum: ["steer", "follow_up", "auto"], description: "Delivery mode for action='steer'. steer interrupts at the next safe point (default), follow_up waits for the next turn boundary, and auto follows up mid-turn but delivers immediately between turns." })),
310
+ 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." })),
287
311
  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." })),
288
312
  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." })),
289
313
  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." })),
@@ -312,11 +336,13 @@ const SubagentParamProperties = {
312
336
  ],
313
337
  description: "Agent config for create/update. Object or JSON string."
314
338
  })),
315
- 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}), where resume is a retained run id or {workflowRunId,key,latest:true} from a durable async workflow receipt. Use 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}, ...]); it resolves to an ordered array, not a key map, so 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. 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." })),
339
+ 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}), where resume is a retained run id or {workflowRunId,key,latest:true} from a durable async workflow receipt. 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. Use runs.all([...]), runs.host(key,{kind:'command',command,timeoutMs,output?,role?,provider?}), await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}), runs.status(id), runs.ref(s), emit(value), console, and return. 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. For ordinary parallel fanout, use await runs.all([{key, agent, task}, ...]); it resolves to an ordered array, not a key map, so 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. 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 except through runs.host." })),
316
340
  workflowScriptPath: Type.Optional(Type.String({ minLength: 1, description: "Path to a trusted JavaScript workflow file. Mutually exclusive with workflowScript. Relative paths resolve against the request cwd. The host reads the file before the filesystem-free workflow sandbox starts." })),
341
+ preflight: Type.Optional(WorkflowPreflightOverride),
317
342
  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." })),
318
343
  isolation: Type.Optional(Type.String({ enum: ["none", "worktree"], description: "Workflow child isolation. none runs in the shared cwd; worktree requires managed git worktree isolation." })),
319
344
  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." })),
345
+ lane: Type.Optional(WorkflowLaneMetadata),
320
346
  context: Type.Optional(Type.String({
321
347
  enum: ["fresh", "fork", "profile"],
322
348
  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.",
@@ -325,7 +351,6 @@ const SubagentParamProperties = {
325
351
  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." })),
326
352
  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." })),
327
353
  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." })),
328
- turnBudget: Type.Optional(TurnBudgetOverride),
329
354
  toolBudget: Type.Optional(ToolBudgetOverride),
330
355
  usageBudget: Type.Optional(UsageBudgetOverride),
331
356
  agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
@@ -343,7 +368,7 @@ const SubagentParamProperties = {
343
368
  { type: "string" },
344
369
  { type: "boolean" },
345
370
  ],
346
- description: "Default child output file (string), or false to disable. Relative paths resolve against cwd.",
371
+ 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.",
347
372
  })),
348
373
  outputMode: Type.Optional(OutputModeOverride),
349
374
  skill: Type.Optional(SkillOverride),
@@ -375,7 +400,7 @@ const SubagentWaitParamsSchema = Type.Object({
375
400
  })),
376
401
  timeoutMs: Type.Optional(Type.Integer({
377
402
  minimum: 1,
378
- description: "Give up waiting after this many milliseconds (the runs keep going regardless). Defaults to 1800000 (30 minutes).",
403
+ description: "Give up waiting after this many milliseconds (the runs keep going regardless). Defaults to config waitTool.defaultTimeoutMs, then 1800000 (30 minutes). Window expiry is a non-error active-work result.",
379
404
  })),
380
405
  stopOnAttention: Type.Optional(Type.Boolean({
381
406
  description: "Blocking waits stop when a run needs attention by default. Set false to keep waiting through idle or long-thinking attention; supervisor/contact requests still stop the wait.",
@@ -6,8 +6,12 @@ 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
  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 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.";
10
+ 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.";
11
+ 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.";
12
+ const WORKFLOW_HOST_GUIDANCE = "For one non-interactive operator-owned command, await runs.host(key,{kind:'command',command,timeoutMs,output?,role?,provider?}). v1 supports only command steps; output is bounded and command failure fails the workflow.";
9
13
 
10
- 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, or workflowScriptPath to load a script from the request cwd. 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. ${EXTERNAL_CLI_RUNNER_GUIDANCE} Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
14
+ 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, or workflowScriptPath to load a script from the request cwd. 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_LANES_GUIDANCE} ${WORKFLOW_HOST_GUIDANCE} ${EXTERNAL_CLI_RUNNER_GUIDANCE} Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
11
15
 
12
16
  export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflowScript call.";
13
17
 
@@ -15,6 +19,10 @@ export const SUBAGENT_TOOL_PROMPT_GUIDELINES = [
15
19
  "Use subagent only when delegation is needed. Before executing, call { action: \"list\" } and run only executable, non-disabled agents.",
16
20
  "Omit action for execution. Use { agent, task? } only for one child; use workflowScript for multi-step or parallel work.",
17
21
  "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.",
22
+ WORKFLOW_LANES_GUIDANCE,
23
+ WORKFLOW_HOST_GUIDANCE,
24
+ WORKFLOW_RESUME_KEY_GUIDANCE,
25
+ WORKFLOW_OUTPUT_BINDING_GUIDANCE,
18
26
  "For ordinary parallel work, use await runs.all([{key,agent,task}, ...]); it resolves to an ordered array, not a key map, so 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.",
19
27
  "Keep one writer per cwd/worktree unless writers run in isolated worktrees.",
20
28
  "To pass an explicit model to a child, first call { action: \"models\" } and copy an exact provider/id (e.g. openai-codex/gpt-5.6-sol); bare ids resolve only when unique in the registry, and agent names (gpt-pro, advisor) are not model ids. Set per-run thinking with a suffix on the model string (e.g. openai-codex/gpt-5.6-sol: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.",
@@ -26,6 +34,9 @@ export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
26
34
  • Use { action: "list" } before execution and only run executable/non-disabled agents.
27
35
  • Keep execution and management separate: omit action for structured single-child or workflowScript execution; use action only for management/control.
28
36
  • 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.
37
+ • ${WORKFLOW_RESUME_KEY_GUIDANCE}
38
+ • ${WORKFLOW_OUTPUT_BINDING_GUIDANCE}
39
+ • ${WORKFLOW_HOST_GUIDANCE}
29
40
  • Ordinary child subagents are not orchestrators. Only explicitly configured fanout children may use the child-safe subagent tool, still bounded by depth/session limits.
30
41
  • Oracle/advisor consultations should use supervisor dialogue for material unknowns when available; request one-shot only when desired.
31
42
  • 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.
@@ -39,6 +50,7 @@ EXECUTION:
39
50
  • 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.
40
51
  • 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.
41
52
  • WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel children. runs.all resolves to an ordered array, not a key map, so use results[0], array destructuring, or results.map((result) => result.output), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Pass async:false only when the parent must block until completion, never for final reviews or gates. Same-repo blocking workflows default to a live in-chat card; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use {action:"children.list"} to list recent retained workflow children with resumable/not-resumable reasons. Resume only rows reported resumable. For a simple follow-up or implementation challenge, use {action:"resume", id:"run-id", message:"..."}. Resume keeps the stored agent/model/tool contract. If no resumable child is listed, launch a same-role fallback challenge and label it as fallback. Inside workflowScript, continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); workflow resumes wait for completed output, and loops must continue from each latest returned runId. Await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}) to guide a prior keyed child without exposing its run id; receipts are queued, delivered, missed, or failed. Always await or return runs.steer. For repository mutation lanes, set worktree:true on the workflow or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.steer, runs.status, runs.ref/refs, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
53
+ • ${WORKFLOW_LANES_GUIDANCE}
42
54
  • 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.
43
55
  • 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" }
44
56
  • 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}" }
@@ -46,7 +58,7 @@ EXECUTION:
46
58
  • 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}.
47
59
 
48
60
  MANAGEMENT / CONTROL (use action; omit execution fields):
49
- • 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, 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.
61
+ • 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.
50
62
  • 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.
51
63
  • Create durable project schedules with { action:"schedule.create", id?, name?, at:"+10m" | ISO, workflowScript:"return runs.run('main', {agent:'worker', task:'...'})" }, or use workflowScriptPath instead. 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.
52
64
 
@@ -60,16 +72,19 @@ EXECUTE:
60
72
  • 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.
61
73
  • SINGLE {agent:"worker",task:"..."} starts exactly one direct child. Fields apply to that child. Do not combine agent/task with action, workflowScript, or workflowScriptPath.
62
74
  • 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. 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.
75
+ • ${WORKFLOW_LANES_GUIDANCE}
63
76
  • 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.
64
77
  • 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]"}
65
78
  • 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.
66
79
 
67
80
  MANAGE / CONTROL:
68
- • 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.
81
+ • 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.
69
82
  • 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}.
70
83
 
71
84
  ASYNC / SAFETY:
72
85
  • 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.
86
+ • ${WORKFLOW_RESUME_KEY_GUIDANCE}
87
+ • ${WORKFLOW_OUTPUT_BINDING_GUIDANCE}
73
88
  • Ordinary children are not orchestrators. Keep one writer per cwd/worktree and use fresh read-only reviewers for independent checks.
74
89
  • Oracle/advisor consultations use available supervisor dialogue for material unknowns; request one-shot when desired.
75
90
  • Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, and {action:"status",id:"..."}.`;
@@ -104,6 +104,7 @@ function compactNestedRun(run: NestedRunSummary | PublicNestedRunSummary, depth
104
104
  })),
105
105
  ...(run.asyncDir ? { asyncDir: run.asyncDir } : {}),
106
106
  ...(run.sessionId ? { sessionId: run.sessionId } : {}),
107
+ ...(run.sessionName ? { sessionName: run.sessionName } : {}),
107
108
  ...(run.sessionFile ? { sessionFile: run.sessionFile } : {}),
108
109
  ...(run.intercomTarget ? { intercomTarget: run.intercomTarget } : {}),
109
110
  ...(run.ownerIntercomTarget ? { ownerIntercomTarget: run.ownerIntercomTarget } : {}),
@@ -132,6 +133,7 @@ function compactNestedRun(run: NestedRunSummary | PublicNestedRunSummary, depth
132
133
  ...(run.error ? { error: run.error } : {}),
133
134
  ...(run.steps?.length ? { steps: run.steps.slice(0, 12).map((step) => ({
134
135
  agent: step.agent,
136
+ ...(step.sessionName ? { sessionName: step.sessionName } : {}),
135
137
  status: step.status,
136
138
  ...(step.model ? { model: step.model } : {}),
137
139
  ...(step.thinking ? { thinking: step.thinking } : {}),