pi-subagents 0.60.0 → 0.61.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 (57) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/docs/agents.md +2 -2
  3. package/docs/configuration.md +9 -5
  4. package/docs/extension-api.md +14 -7
  5. package/docs/models.md +1 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +12 -3
  8. package/docs/workflows.md +14 -13
  9. package/install.mjs +2 -1
  10. package/package.json +1 -1
  11. package/skills/pi-subagents/SKILL.md +6 -4
  12. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  13. package/skills/pi-subagents/references/execution-controls.md +42 -11
  14. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  15. package/skills/pi-subagents/references/prompting-and-roles.md +4 -4
  16. package/skills/pi-subagents/references/review-and-validation.md +1 -1
  17. package/src/agents/agent-management.ts +102 -63
  18. package/src/agents/agents.ts +527 -221
  19. package/src/api/background-work.ts +7 -2
  20. package/src/api/external-runs.ts +67 -4
  21. package/src/api/preflight.ts +13 -8
  22. package/src/api/shared-types.ts +1 -0
  23. package/src/extension/index.ts +7 -4
  24. package/src/extension/public-execution.ts +47 -4
  25. package/src/extension/rpc.ts +62 -4
  26. package/src/extension/schemas.ts +11 -7
  27. package/src/extension/tool-description.ts +14 -18
  28. package/src/runs/background/async-execution.ts +51 -35
  29. package/src/runs/background/async-job-tracker.ts +62 -3
  30. package/src/runs/background/async-resume.ts +3 -1
  31. package/src/runs/background/async-status.ts +59 -9
  32. package/src/runs/background/auto-drain.ts +1 -1
  33. package/src/runs/background/fleet-view.ts +1 -1
  34. package/src/runs/background/result-watcher.ts +1 -1
  35. package/src/runs/background/resume-guidance.ts +1 -1
  36. package/src/runs/background/run-status.ts +2 -2
  37. package/src/runs/background/subagent-runner.ts +1 -2
  38. package/src/runs/background/subagent-wait.ts +20 -21
  39. package/src/runs/background/wait-completions.ts +1 -1
  40. package/src/runs/background/wait-tool.ts +24 -18
  41. package/src/runs/foreground/execution.ts +61 -2
  42. package/src/runs/foreground/subagent-executor.ts +178 -58
  43. package/src/runs/shared/acceptance.ts +43 -18
  44. package/src/runs/shared/host-step-status.ts +1 -0
  45. package/src/runs/shared/model-fallback.ts +61 -17
  46. package/src/runs/shared/permissions.ts +1 -1
  47. package/src/runs/shared/tool-timeout.ts +1 -1
  48. package/src/runs/shared/workflow-graph.ts +3 -2
  49. package/src/shared/types.ts +40 -3
  50. package/src/shared/workflow-child-permit.ts +91 -0
  51. package/src/slash/prompt-template-bridge.ts +37 -1
  52. package/src/slash/slash-commands.ts +18 -26
  53. package/src/tui/render.ts +95 -52
  54. package/src/workflows/scripted-workflow.ts +153 -4
  55. package/src/workflows/workflow-child-summary.ts +1 -1
  56. package/src/workflows/workflow-receipt.ts +41 -4
  57. package/src/workflows/workflow-resources.ts +150 -0
@@ -22,9 +22,14 @@ export interface BackgroundWorkReconcileContext {
22
22
  nowMs: number;
23
23
  }
24
24
 
25
+ export interface BackgroundWorkListContext {
26
+ sessionId: string;
27
+ nowMs: number;
28
+ }
29
+
25
30
  export interface BackgroundWorkProvider {
26
31
  name: string;
27
- listActiveWork(): readonly BackgroundWorkItem[];
32
+ listActiveWork(context?: BackgroundWorkListContext): readonly BackgroundWorkItem[];
28
33
  wakeChannels?: readonly string[];
29
34
  reconcile?(context: BackgroundWorkReconcileContext): void;
30
35
  }
@@ -171,7 +176,7 @@ export function snapshotBackgroundWork(sessionId: string, nowMs = Date.now()): B
171
176
  }
172
177
  let active: readonly BackgroundWorkItem[];
173
178
  try {
174
- active = provider.listActiveWork();
179
+ active = provider.listActiveWork({ sessionId, nowMs });
175
180
  } catch (error) {
176
181
  throw new Error(
177
182
  `Background-work provider '${provider.name}' listActiveWork failed: ${error instanceof Error ? error.message : String(error)}`,
@@ -48,7 +48,16 @@ interface ExternalRunRegistry {
48
48
  runs: Map<string, ExternalRun>;
49
49
  }
50
50
 
51
- const RUN_FIELDS = new Set([
51
+ interface TrustedExternalRunRecord {
52
+ value: unknown;
53
+ normalized: ExternalRun;
54
+ keys: readonly string[];
55
+ values: readonly unknown[];
56
+ }
57
+
58
+ const trustedRecordsByRegistry = new WeakMap<object, Map<string, TrustedExternalRunRecord>>();
59
+
60
+ const RUN_FIELD_NAMES = [
52
61
  "id",
53
62
  "sessionId",
54
63
  "source",
@@ -61,7 +70,8 @@ const RUN_FIELDS = new Set([
61
70
  "preview",
62
71
  "reportPath",
63
72
  "transcriptPath",
64
- ]);
73
+ ] as const satisfies readonly (keyof ExternalRun)[];
74
+ const RUN_FIELDS = new Set<string>(RUN_FIELD_NAMES);
65
75
  const UPDATE_FIELDS = new Set([...RUN_FIELDS].filter((field) => field !== "id" && field !== "sessionId" && field !== "source"));
66
76
 
67
77
  function registry(): ExternalRunRegistry {
@@ -79,6 +89,14 @@ function registry(): ExternalRunRegistry {
79
89
  return candidate as ExternalRunRegistry;
80
90
  }
81
91
 
92
+ function trustedRecords(current: ExternalRunRegistry): Map<string, TrustedExternalRunRecord> {
93
+ const cached = trustedRecordsByRegistry.get(current);
94
+ if (cached) return cached;
95
+ const records = new Map<string, TrustedExternalRunRecord>();
96
+ trustedRecordsByRegistry.set(current, records);
97
+ return records;
98
+ }
99
+
82
100
  function inputObject(value: unknown, field: string, allowed: Set<string>): Record<string, unknown> {
83
101
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${field} must be an object.`);
84
102
  const input = value as Record<string, unknown>;
@@ -150,6 +168,41 @@ function clone(run: ExternalRun): ExternalRun {
150
168
  return { ...run };
151
169
  }
152
170
 
171
+ function trustedRecord(value: unknown, normalized: ExternalRun): TrustedExternalRunRecord {
172
+ const candidate = value as ExternalRun;
173
+ return {
174
+ value,
175
+ normalized,
176
+ keys: Object.keys(candidate),
177
+ values: RUN_FIELD_NAMES.map((field) => candidate[field]),
178
+ };
179
+ }
180
+
181
+ function trustedRecordValue(value: unknown, record: TrustedExternalRunRecord | undefined): ExternalRun | undefined {
182
+ if (!record || record.value !== value || !value || typeof value !== "object") return undefined;
183
+ try {
184
+ const candidate = value as ExternalRun;
185
+ const keys = Object.keys(candidate);
186
+ if (keys.length !== record.keys.length || keys.some((key, index) => key !== record.keys[index])) return undefined;
187
+ for (const [index, field] of RUN_FIELD_NAMES.entries()) {
188
+ if (candidate[field] !== record.values[index]) return undefined;
189
+ }
190
+ return record.normalized;
191
+ } catch {
192
+ return undefined;
193
+ }
194
+ }
195
+
196
+ function rememberTrustedRecord(current: ExternalRunRegistry, cacheKey: string, value: unknown, normalized: ExternalRun): void {
197
+ trustedRecords(current).set(cacheKey, trustedRecord(value, normalized));
198
+ }
199
+
200
+ function normalizeCachedRecord(current: ExternalRunRegistry, cacheKey: string, value: unknown): ExternalRun {
201
+ const run = validateRun(value);
202
+ rememberTrustedRecord(current, cacheKey, value, run);
203
+ return run;
204
+ }
205
+
153
206
  /** Register one current-session external job. pi-subagents never controls the job. */
154
207
  export function registerExternalRun(input: ExternalRun): ExternalRun {
155
208
  const run = validateRun(input);
@@ -158,6 +211,7 @@ export function registerExternalRun(input: ExternalRun): ExternalRun {
158
211
  if (current.runs.has(runKey)) throw new Error(`External run '${run.id}' is already registered for session '${run.sessionId}'.`);
159
212
  if (current.runs.size >= EXTERNAL_RUN_LIMITS.maxCachedRuns) throw new Error(`External-run registry supports at most ${EXTERNAL_RUN_LIMITS.maxCachedRuns} cached runs.`);
160
213
  current.runs.set(runKey, run);
214
+ rememberTrustedRecord(current, runKey, run, run);
161
215
  return clone(run);
162
216
  }
163
217
 
@@ -172,12 +226,17 @@ export function updateExternalRun(sessionId: string, id: string, update: Externa
172
226
  if (!previous) throw new Error(`External run '${safeId}' is not registered for session '${safeSessionId}'.`);
173
227
  const next = validateRun({ ...previous, ...patch });
174
228
  current.runs.set(runKey, next);
229
+ rememberTrustedRecord(current, runKey, next, next);
175
230
  return clone(next);
176
231
  }
177
232
 
178
233
  /** Remove a cached external job. The caller remains responsible for its process and artifacts. */
179
234
  export function unregisterExternalRun(sessionId: string, id: string): boolean {
180
- return registry().runs.delete(key(identity(sessionId, "External run sessionId", EXTERNAL_RUN_LIMITS.maxSessionIdLength), identity(id, "External run id")));
235
+ const current = registry();
236
+ const runKey = key(identity(sessionId, "External run sessionId", EXTERNAL_RUN_LIMITS.maxSessionIdLength), identity(id, "External run id"));
237
+ const deleted = current.runs.delete(runKey);
238
+ if (deleted) trustedRecords(current).delete(runKey);
239
+ return deleted;
181
240
  }
182
241
 
183
242
  function snapshotBytes(runs: readonly ExternalRun[]): number {
@@ -192,15 +251,19 @@ function getErrorMessage(error: unknown): string {
192
251
  export function snapshotExternalRuns(sessionId: string, options: ExternalRunSnapshotOptions = {}): readonly ExternalRun[] {
193
252
  const safeSessionId = identity(sessionId, "External-run snapshot sessionId", EXTERNAL_RUN_LIMITS.maxSessionIdLength);
194
253
  const current = registry();
254
+ const trusted = trustedRecords(current);
255
+ const sessionPrefix = `${safeSessionId}\0`;
195
256
  const runs: ExternalRun[] = [];
196
257
  for (const [cacheKey, value] of current.runs.entries()) {
258
+ if (typeof cacheKey !== "string" || !cacheKey.startsWith(sessionPrefix)) continue;
197
259
  try {
198
- const run = validateRun(value);
260
+ const run = trustedRecordValue(value, trusted.get(cacheKey)) ?? normalizeCachedRecord(current, cacheKey, value);
199
261
  if (run.sessionId === safeSessionId) runs.push(run);
200
262
  } catch (error) {
201
263
  const message = `Malformed cached external run '${cacheKey}': ${getErrorMessage(error)}`;
202
264
  if (!options.ignoreMalformed) throw new Error(message, { cause: error instanceof Error ? error : undefined });
203
265
  current.runs.delete(cacheKey);
266
+ trusted.delete(cacheKey);
204
267
  options.onMalformedRecord?.(message);
205
268
  }
206
269
  }
@@ -1,11 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { discoverAgents, discoverAgentsAll, findBlockingAgentDiagnostic, formatUnknownAgentError, resolveAgentName, unknownAgentDiagnosticContext, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts";
4
+ import { discoverAgentSnapshot, findBlockingAgentDiagnostic, formatUnknownAgentError, resolveAgentName, unknownAgentDiagnosticContext, type AgentConfig, type AgentDiscoveryAllResult, type AgentScope, type AgentSource } from "../agents/agents.ts";
5
5
  import { resolveExecutionAgentScope } from "../agents/agent-scope.ts";
6
6
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../agents/skills.ts";
7
7
  import { buildAgentMemoryInjection } from "../agents/agent-memory.ts";
8
- import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, type AvailableModelInfo, type ParentModel } from "../runs/shared/model-fallback.ts";
8
+ import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, resolveModelOrigin, type AvailableModelInfo, type ParentModel } from "../runs/shared/model-fallback.ts";
9
9
  import { resolveModelScopesForAgent } from "../runs/shared/model-scope.ts";
10
10
  import { applyThinkingSuffix, resolvePiLaunchToolPlan, type PiLaunchToolPlan } from "../runs/shared/pi-args.ts";
11
11
  import { injectOutputPathSystemPrompt, normalizeSingleOutputOverride, resolveSingleOutputPath } from "../runs/shared/single-output.ts";
@@ -222,8 +222,7 @@ function taskWorkspaceScopeAuthorityDiagnostic(task: string | undefined): Subage
222
222
  };
223
223
  }
224
224
 
225
- function candidateList(inputAgent: string, selected: AgentConfig | undefined, cwd: string, provider?: string): SubagentLaunchContractAgentCandidate[] {
226
- const all = discoverAgentsAll(cwd, provider);
225
+ function candidateList(inputAgent: string, selected: AgentConfig | undefined, all: AgentDiscoveryAllResult): SubagentLaunchContractAgentCandidate[] {
227
226
  return [...all.builtin, ...all.package, ...all.user, ...all.project]
228
227
  .filter((agent) => Boolean(resolveAgentName(inputAgent, [agent]).agent))
229
228
  .map((agent) => ({
@@ -258,7 +257,8 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
258
257
  }
259
258
  const scope = resolveExecutionAgentScope(input.agentScope);
260
259
  const parentProvider = input.preferredProvider ?? input.parentModel?.provider;
261
- const discovered = discoverAgents(effectiveCwd, scope, parentProvider);
260
+ const discovery = discoverAgentSnapshot(effectiveCwd, scope, parentProvider, { includeChains: false });
261
+ const discovered = discovery.effective;
262
262
  const resolvedAgent = resolveAgentName(input.agent, discovered.agents);
263
263
  const ambiguousCandidates = resolvedAgent.error
264
264
  ? discovered.agents.filter((agent) => resolveAgentName(input.agent, [agent]).agent)
@@ -317,9 +317,13 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
317
317
  const availableModels = normalizeAvailableModels(input.availableModels);
318
318
  const preferredProvider = agent.modelProvider ?? input.preferredProvider ?? input.parentModel?.provider;
319
319
  const modelScopes = resolveModelScopesForAgent(discovered.modelScope, agent.name, input.parentModel);
320
+ const modelOrigin = resolveModelOrigin({ explicitModel: input.model, agentModel: agent.model, parentModel: input.parentModel });
320
321
  const primaryModel = externalRunner
321
322
  ? undefined
322
- : resolveEffectiveSubagentModel(input.model, agent.model, input.parentModel, availableModels, preferredProvider, { scope: modelScopes });
323
+ : resolveEffectiveSubagentModel(input.model, agent.model, input.parentModel, availableModels, preferredProvider, {
324
+ scope: modelScopes,
325
+ source: modelOrigin === "explicit" ? "explicit" : "inherited",
326
+ });
323
327
  const effectiveThinkingConfig = input.thinking !== undefined ? input.thinking : agent.thinking;
324
328
  const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
325
329
  discovered.maxThinking,
@@ -332,7 +336,8 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
332
336
  ? []
333
337
  : buildModelCandidates(primaryModel, agent.fallbackModels, availableModels, preferredProvider, {
334
338
  scope: modelScopes,
335
- primaryModelFromParent: inheritsParentModel(input.model, agent.model, input.parentModel),
339
+ primaryModelFromParent: modelOrigin === "inherited" || inheritsParentModel(input.model, agent.model, input.parentModel),
340
+ origin: modelOrigin,
336
341
  })
337
342
  .map((candidate) => applyThinkingSuffix(candidate, effectiveThinkingConfig, input.thinking !== undefined) ?? candidate);
338
343
  if (!externalRunner) {
@@ -397,7 +402,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
397
402
  const memoryInjection = buildAgentMemoryInjection(agent, effectiveCwd);
398
403
  if (memoryInjection) effectiveSystemPrompt = effectiveSystemPrompt ? `${effectiveSystemPrompt}\n\n${memoryInjection}` : memoryInjection;
399
404
  effectiveSystemPrompt = injectOutputPathSystemPrompt(effectiveSystemPrompt, outputPath, agent);
400
- const candidates = candidateList(input.agent, agent, effectiveCwd, parentProvider);
405
+ const candidates = candidateList(input.agent, agent, discovery.all);
401
406
  const shadowedCandidates = candidates.filter((candidate) => !candidate.selected);
402
407
  const definitionDigest = agentDefinitionDigest(agent);
403
408
  const contractBase: Omit<SubagentLaunchContract, "digest"> = {
@@ -20,4 +20,5 @@ export {
20
20
  type SubagentResultStatus,
21
21
  type SubagentRunMode,
22
22
  type Usage,
23
+ type WorkflowResourceProvenanceV1,
23
24
  } from "../shared/types.ts";
@@ -19,7 +19,7 @@ import * as path from "node:path";
19
19
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
20
20
  import { keyText, type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
21
21
  import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
22
- import { discoverAgents, discoverAgentsAll, type AgentConfig, type AgentScope } from "../agents/agents.ts";
22
+ import { discoverAgentSnapshot, discoverAgents, type AgentConfig, type AgentScope } from "../agents/agents.ts";
23
23
  import { clearRuntimeAgentsForPi, listRuntimeAgentConfigs, mergeRuntimeAgents } from "../agents/runtime-agent-registry.ts";
24
24
  import { registerRuntimeAgentEventListener } from "../agents/runtime-agent-events.ts";
25
25
  import { ensureAccessibleDir } from "../shared/accessible-dir.ts";
@@ -438,6 +438,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
438
438
  const state: SubagentState = {
439
439
  baseCwd: "",
440
440
  currentSessionId: null,
441
+ statusProjectionSessionId: null,
441
442
  completionOwnerId: currentCompletionOwnerId(),
442
443
  artifactDirPreference: config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir,
443
444
  ...(config.authorityPolicy ? { authorityPolicy: config.authorityPolicy } : {}),
@@ -525,9 +526,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
525
526
  return missionObserverResultCandidateFiles(DIRS.results).length > 0;
526
527
  };
527
528
  const discoverAgentsForRuntime = (cwd: string, scope: AgentScope, preferredModelProvider?: string) => {
528
- const discovered = discoverAgents(cwd, scope, preferredModelProvider);
529
- if (listRuntimeAgentConfigs(pi).length === 0) return discovered;
530
- const all = discoverAgentsAll(cwd, preferredModelProvider);
529
+ if (listRuntimeAgentConfigs(pi).length === 0) return discoverAgents(cwd, scope, preferredModelProvider);
530
+ const snapshot = discoverAgentSnapshot(cwd, scope, preferredModelProvider, { includeChains: false });
531
+ const discovered = snapshot.effective;
532
+ const all = snapshot.all;
531
533
  const configuredAgents: AgentConfig[] = [
532
534
  ...all.builtin,
533
535
  ...all.package,
@@ -996,6 +998,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
996
998
  promptTemplateBridge.dispose();
997
999
  state.widgetsSuspended = false;
998
1000
  state.currentSessionId = null;
1001
+ state.statusProjectionSessionId = null;
999
1002
  state.parentSessionFile = null;
1000
1003
  parentSessionEnvValue = null;
1001
1004
  if (shuttingDownParentSession && process.env[SUBAGENT_PARENT_SESSION_ENV] === shuttingDownParentSession) {
@@ -18,8 +18,12 @@ export interface PublicSubagentExecutionParams {
18
18
  chainDir?: unknown;
19
19
  chainName?: unknown;
20
20
  config?: unknown;
21
+ workflow?: unknown;
22
+ args?: unknown;
21
23
  workflowScript?: unknown;
22
24
  workflowScriptPath?: unknown;
25
+ globalConcurrencyLimit?: unknown;
26
+ maxSubagentSpawnsPerRun?: unknown;
23
27
  preflight?: unknown;
24
28
  isolation?: unknown;
25
29
  worktree?: unknown;
@@ -45,20 +49,56 @@ export type PublicSubagentExecutionNormalization<T> =
45
49
  | { ok: true; params: T }
46
50
  | { ok: false; error: string; mode: PublicSubagentExecutionMode };
47
51
 
52
+ export function validateWorkflowCapacityOverrides(params: PublicSubagentExecutionParams): string | undefined {
53
+ for (const [name, value] of [
54
+ ["globalConcurrencyLimit", params.globalConcurrencyLimit],
55
+ ["maxSubagentSpawnsPerRun", params.maxSubagentSpawnsPerRun],
56
+ ] as const) {
57
+ if (value !== undefined && (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0)) return `${name} must be a positive safe integer.`;
58
+ }
59
+ }
60
+
48
61
  /**
49
62
  * Enforce the public execution cutover before requests reach the executor.
50
63
  * Internal runs.run children and structured owned delegation bypass this boundary.
51
64
  */
52
65
  export function normalizePublicSubagentExecution<T extends PublicSubagentExecutionParams>(params: T): PublicSubagentExecutionNormalization<T> {
66
+ for (const field of ["resource", "resourceProvenance", "workflowResource", "workflowResourceProvenance", "workflowResourcePermit", "resourcePermit", "permit"] as const) {
67
+ if (Object.hasOwn(params, field) && (params as Record<string, unknown>)[field] !== undefined) {
68
+ return { ok: false, error: "Public execution does not accept workflow resource provenance or permit fields.", mode: params.action === undefined ? "workflow" : "management" };
69
+ }
70
+ }
53
71
  if (params.workflowScript !== undefined && params.workflowScriptPath !== undefined) {
54
72
  return { ok: false, error: "workflowScript and workflowScriptPath are mutually exclusive.", mode: "workflow" };
55
73
  }
56
- const hasWorkflowInput = params.workflowScript !== undefined || params.workflowScriptPath !== undefined;
74
+ const hasNamedWorkflow = params.workflow !== undefined;
75
+ if (hasNamedWorkflow && (typeof params.workflow !== "string" || !params.workflow.trim())) {
76
+ return { ok: false, error: "workflow must be a non-empty named workflow resource.", mode: "workflow" };
77
+ }
78
+ if (hasNamedWorkflow && (params.workflowScript !== undefined || params.workflowScriptPath !== undefined)) {
79
+ return { ok: false, error: "workflow is mutually exclusive with workflowScript and workflowScriptPath.", mode: "workflow" };
80
+ }
81
+ if (!hasNamedWorkflow && params.args !== undefined) {
82
+ return { ok: false, error: "args requires a named workflow resource.", mode: "workflow" };
83
+ }
84
+ const hasWorkflowInput = params.workflowScript !== undefined || params.workflowScriptPath !== undefined || hasNamedWorkflow;
85
+ const hasCapacityOverride = params.globalConcurrencyLimit !== undefined || params.maxSubagentSpawnsPerRun !== undefined;
86
+ if (hasCapacityOverride) {
87
+ const capacityOverrideError = validateWorkflowCapacityOverrides(params);
88
+ if (capacityOverrideError) return { ok: false, error: capacityOverrideError, mode: params.action === undefined ? "workflow" : "management" };
89
+ if (params.action !== undefined || hasNamedWorkflow || (params.workflowScript === undefined && params.workflowScriptPath === undefined)) {
90
+ return { ok: false, error: "Workflow capacity overrides are only supported on top-level workflowScript or workflowScriptPath calls.", mode: params.action === undefined ? "workflow" : "management" };
91
+ }
92
+ }
57
93
  if (params.preflight !== undefined && !hasWorkflowInput) {
58
94
  return { ok: false, error: "preflight requires workflowScript or workflowScriptPath.", mode: params.action === undefined ? "workflow" : "management" };
59
95
  }
96
+ if (params.preflight !== undefined && hasNamedWorkflow && params.workflowScript === undefined && params.workflowScriptPath === undefined) {
97
+ return { ok: false, error: "preflight is not supported with named workflow resources.", mode: "workflow" };
98
+ }
60
99
  const hasValidWorkflowInput = (typeof params.workflowScript === "string" && Boolean(params.workflowScript.trim()))
61
- || (typeof params.workflowScriptPath === "string" && Boolean(params.workflowScriptPath.trim()));
100
+ || (typeof params.workflowScriptPath === "string" && Boolean(params.workflowScriptPath.trim()))
101
+ || (typeof params.workflow === "string" && Boolean(params.workflow.trim()));
62
102
  if (params.isolation !== undefined) {
63
103
  if (params.isolation !== "none" && params.isolation !== "worktree") {
64
104
  return { ok: false, error: "isolation must be 'none' or 'worktree'.", mode: hasWorkflowInput ? "workflow" : "management" };
@@ -81,6 +121,9 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
81
121
  return { ok: false, error: "action must be a non-empty management/control action, or omit action and use workflowScript.", mode: "management" };
82
122
  }
83
123
  const normalizedAction = typeof action === "string" ? action.trim() : undefined;
124
+ if (normalizedAction !== undefined && hasNamedWorkflow) {
125
+ return { ok: false, error: "Named workflow resource execution must omit action.", mode: "management" };
126
+ }
84
127
  if (params.clarify !== undefined) {
85
128
  return { ok: false, error: "Public workflowScript execution does not support clarify UI.", mode: "workflow" };
86
129
  }
@@ -141,7 +184,7 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
141
184
  return { ok: false, error: "step is not a public execution field; use workflowScript for orchestration.", mode: "workflow" };
142
185
  }
143
186
  if (hasWorkflowInput && (params.agent !== undefined || params.task !== undefined)) {
144
- return { ok: false, error: "Structured single-child execution cannot be combined with workflowScript or workflowScriptPath.", mode: "workflow" };
187
+ return { ok: false, error: "Structured single-child execution cannot be combined with workflow, workflowScript, or workflowScriptPath.", mode: "workflow" };
145
188
  }
146
189
  if (params.agent !== undefined || params.task !== undefined) {
147
190
  if (typeof params.agent !== "string" || !params.agent.trim()) {
@@ -160,7 +203,7 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
160
203
  };
161
204
  }
162
205
  if (!hasValidWorkflowInput) {
163
- return { ok: false, error: "Execution requires either { agent, task? } for one child or a non-empty workflowScript or workflowScriptPath for orchestration.", mode: "workflow" };
206
+ return { ok: false, error: "Execution requires either { agent, task? } for one child, a named workflow resource, or a non-empty workflowScript or workflowScriptPath for orchestration.", mode: "workflow" };
164
207
  }
165
208
  return { ok: true, params };
166
209
  }
@@ -172,6 +172,8 @@ interface FleetCandidate {
172
172
  goal?: unknown;
173
173
  }
174
174
 
175
+ type StatusRpcParams = Pick<SubagentParamsLike, "id" | "runId" | "dir" | "index" | "view" | "lines">;
176
+
175
177
  function buildFleetStatus(
176
178
  state: SubagentState | undefined,
177
179
  keyState: FleetKeyState,
@@ -380,8 +382,7 @@ function failIfToolError(result: ToolResultWithError): void {
380
382
  throw new SubagentRpcError("execution_failed", textFromToolResult(result) || "Subagent RPC execution failed.");
381
383
  }
382
384
 
383
- function normalizeTargetParams(params: unknown, method: SubagentRpcMethod): Pick<SubagentParamsLike, "id" | "runId" | "dir" | "index"> {
384
- const input = assertRecordParams(params, method);
385
+ function normalizeTargetParamsFromRecord(input: Record<string, unknown>): Pick<SubagentParamsLike, "id" | "runId" | "dir" | "index"> {
385
386
  const output: Pick<SubagentParamsLike, "id" | "runId" | "dir" | "index"> = {};
386
387
  if (input.id !== undefined) output.id = input.id as string;
387
388
  if (input.runId !== undefined) output.runId = input.runId as string;
@@ -390,6 +391,43 @@ function normalizeTargetParams(params: unknown, method: SubagentRpcMethod): Pick
390
391
  return output;
391
392
  }
392
393
 
394
+ function normalizeTargetParams(params: unknown, method: SubagentRpcMethod): Pick<SubagentParamsLike, "id" | "runId" | "dir" | "index"> {
395
+ return normalizeTargetParamsFromRecord(assertRecordParams(params, method));
396
+ }
397
+
398
+ function normalizeStatusParams(params: unknown): StatusRpcParams {
399
+ const input = assertRecordParams(params, "status");
400
+ const output: StatusRpcParams = normalizeTargetParamsFromRecord(input);
401
+ if (input.view !== undefined) output.view = input.view as StatusRpcParams["view"];
402
+ if (input.lines !== undefined) output.lines = input.lines as number;
403
+ return output;
404
+ }
405
+
406
+ function hasStatusTarget(params: StatusRpcParams): boolean {
407
+ return params.id !== undefined
408
+ || params.runId !== undefined
409
+ || params.dir !== undefined
410
+ || params.index !== undefined
411
+ || params.view !== undefined
412
+ || params.lines !== undefined;
413
+ }
414
+
415
+ function canUseInMemoryStatus(state: SubagentState | undefined, sessionId: string | undefined): state is SubagentState {
416
+ return Boolean(
417
+ state
418
+ && sessionId
419
+ && state.currentSessionId === sessionId
420
+ && state.statusProjectionSessionId === sessionId
421
+ && state.foregroundControls instanceof Map
422
+ && state.asyncJobs instanceof Map,
423
+ );
424
+ }
425
+
426
+ function inMemoryStatusSummary(fleet: SubagentRpcFleetStatus): string {
427
+ const noun = fleet.totalActive === 1 ? "child" : "children";
428
+ return `In-memory subagent status: ${fleet.totalActive} active ${noun}.`;
429
+ }
430
+
393
431
  function sessionData(ctx: ExtensionContext | null): { cwd?: string; sessionId?: string; sessionFile?: string | null } {
394
432
  if (!ctx) return {};
395
433
  return {
@@ -405,6 +443,7 @@ function pingData(ctx: ExtensionContext | null) {
405
443
  methods: [...SUBAGENT_RPC_METHODS],
406
444
  capabilities: {
407
445
  status: true,
446
+ statusProjection: { version: 1, untargeted: "in-memory-when-ready", targeted: "executor" },
408
447
  managementActions: [...SUBAGENT_RPC_MANAGEMENT_ACTIONS],
409
448
  fleetStatus: { version: 1 },
410
449
  asyncStatusSnapshot: { kind: ASYNC_STATUS_SNAPSHOT_KIND, version: ASYNC_STATUS_SNAPSHOT_VERSION },
@@ -689,14 +728,33 @@ async function handleRequest(
689
728
  return executeChecked(options, ctx, request.requestId, request.method, spawnParams(request.params));
690
729
  }
691
730
  if (request.method === "status") {
731
+ const statusParams = normalizeStatusParams(request.params);
732
+ let sessionId: string | undefined;
733
+ if (!hasStatusTarget(statusParams)) {
734
+ try {
735
+ sessionId = resolveCurrentSessionId(ctx.sessionManager);
736
+ } catch {
737
+ // Let the executor produce the canonical error when session identity is unavailable.
738
+ }
739
+ if (canUseInMemoryStatus(options.state, sessionId)) {
740
+ const fleet = buildFleetStatus(options.state, fleetKeys, sessionId);
741
+ const asyncSnapshot = buildAsyncStatusSnapshotForState(options.state, sessionId);
742
+ return {
743
+ text: inMemoryStatusSummary(fleet),
744
+ details: { mode: "management", results: [] },
745
+ fleet,
746
+ asyncSnapshot,
747
+ };
748
+ }
749
+ }
692
750
  const status = await executeChecked(
693
751
  options,
694
752
  ctx,
695
753
  request.requestId,
696
754
  request.method,
697
- { action: "status", ...normalizeTargetParams(request.params, "status") },
755
+ { action: "status", ...statusParams },
698
756
  );
699
- const sessionId = resolveCurrentSessionId(ctx.sessionManager);
757
+ sessionId ??= resolveCurrentSessionId(ctx.sessionManager);
700
758
  return {
701
759
  ...status,
702
760
  fleet: buildFleetStatus(
@@ -337,8 +337,12 @@ const SubagentParamProperties = {
337
337
  ],
338
338
  description: "Agent config for create/update. Object or JSON string."
339
339
  })),
340
- 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." })),
341
- 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." })),
340
+ workflow: Type.Optional(Type.String({ minLength: 1, description: "Extension-owned workflow resource; resolves its script and authority internally." })),
341
+ args: Type.Optional(Type.Unsafe({ type: "object", maxProperties: 16, additionalProperties: true, description: "Bounded plain-JSON args for workflow; resource validation applies." })),
342
+ workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Inline JavaScript statement body with unknown resource provenance. 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." })),
343
+ 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." })),
344
+ globalConcurrencyLimit: Type.Optional(Type.Integer({ minimum: 1 })),
345
+ maxSubagentSpawnsPerRun: Type.Optional(Type.Integer({ minimum: 1 })),
342
346
  preflight: Type.Optional(WorkflowPreflightOverride),
343
347
  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." })),
344
348
  isolation: Type.Optional(Type.String({ enum: ["none", "worktree"], description: "Workflow child isolation. none runs in the shared cwd; worktree requires managed git worktree isolation." })),
@@ -391,20 +395,20 @@ export function createSubagentParamsSchema(): typeof SubagentParams {
391
395
 
392
396
  const SubagentWaitParamsSchema = Type.Object({
393
397
  id: Type.Optional(Type.String({
394
- description: "Async run or remembered detached foreground run id/prefix to wait for one specific run. Omit to wait across every active async run started in this session.",
398
+ description: "Async run or remembered detached foreground run id/prefix to wait for one specific run. Ordinary async subagent runs already notify this session natively; use bg_wait for provider, detached, or other background work without native notification, or when same-turn blocking results are truly needed. Omit to wait across every active async run started in this session only when a same-turn wait is truly needed.",
395
399
  })),
396
400
  nonBlocking: Type.Optional(Type.Boolean({
397
- description: "When true, resolve id to one exact run, persist a wake subscription, and return immediately. The originating session is woken on completion, failure, attention, reconciliation failure, or timeout. Requires id and cannot be combined with all.",
401
+ description: "When true, resolve id to one exact run, persist a wake subscription, and return immediately. Use this only for provider, detached, or other background work without a native completion notification; ordinary async subagent runs already notify this session natively and do not need a subscription. The originating session is woken on completion, failure, attention, reconciliation failure, or timeout. Requires id and cannot be combined with all.",
398
402
  })),
399
403
  all: Type.Optional(Type.Boolean({
400
- description: "Wait for ALL active runs to finish. Default false: return as soon as the first run finishes, so a fleet manager can spawn a replacement and wait again. Ignored when id targets a single run.",
404
+ description: "Wait for ALL active runs to finish. Ordinary async subagent runs already notify this session natively; use all only when a same-turn result from tracked background work is truly needed. Default false: return when the first tracked run or provider item finishes or needs attention. Ignored when id targets a single run.",
401
405
  })),
402
406
  timeoutMs: Type.Optional(Type.Integer({
403
407
  minimum: 1,
404
- 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.",
408
+ description: "Give up waiting after this many milliseconds (the runs keep going regardless). Ordinary async subagent runs already notify this session natively; use a wait timeout only when same-turn results are truly needed for provider, detached, or other background work without native notification. Defaults to config waitTool.defaultTimeoutMs, then 1800000 (30 minutes). Window expiry is a non-error active-work result.",
405
409
  })),
406
410
  stopOnAttention: Type.Optional(Type.Boolean({
407
- 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.",
411
+ description: "For a blocking wait that is truly needed, 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.",
408
412
  })),
409
413
  });
410
414
 
@@ -10,33 +10,25 @@ const WORKFLOW_RESUME_KEY_GUIDANCE = "Each workflow key identifies one result la
10
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
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
12
  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.";
13
- const WORKFLOW_HOST_GUIDANCE = "For one non-interactive operator-owned command, await runs.host(key,{kind:'command',command,timeoutMs,output?,role?,provider?}). runs.host has no per-step cwd: commands and relative output paths use the workflow cwd; set cwd on the outer subagent request instead (for example, {cwd:'/path/to/worktree',workflowScript:'...'}), or put a trusted directory change in the command (for example, 'cd /path/to/worktree && npm test'). v1 supports only command steps; output is bounded and command failure fails the workflow.";
14
- const AGENT_CAPABILITY_GUIDANCE = "For capability selection, use { action: \"list\", capabilities: true } for compact prompt-free rows.";
13
+ 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.";
14
+ const WORKFLOW_HOST_GUIDANCE = "For permission-sensitive host calls, use an extension-owned resource such as {workflow:'run-ci',args:{command:'npm test'}}; raw workflowScript/workflowScriptPath have unknown resource provenance and cannot use runs.host. In a resource that grants it, await runs.host(key,{kind:'command',command,timeoutMs,output?,role?,provider?}). runs.host has no per-step cwd: commands and relative output paths use the workflow cwd; set cwd on the outer subagent request instead (for example, {cwd:'/path/to/worktree',workflowScript:'...'}), or put a trusted directory change in the command (for example, 'cd /path/to/worktree && npm test'). v1 supports only command steps; output is bounded and command failure fails the workflow.";
15
15
 
16
- 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_SCRIPT_PORTABILITY_GUIDANCE} ${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.`;
16
+ 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} The script inputs are mutually exclusive. Use action:'validate' with either script input to check it without launching children. For multi-step or parallel work, make exactly one top-level subagent call with async:true; launch children only inside that workflow and do not make another top-level call for them. Use runs.run('key',{agent,task}) for one child, await runs.all([{key:'a',agent:'reviewer',task:'...'},{key:'b',agent:'reviewer',task:'...'}]) for ordinary parallel children, and read its ordered array result with indexes, destructuring, or .map(...), not by key property. ${WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE} ${WORKFLOW_LANES_GUIDANCE} ${WORKFLOW_HOST_GUIDANCE} ${EXTERNAL_CLI_RUNNER_GUIDANCE} Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
17
17
 
18
18
  export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflowScript call.";
19
19
 
20
20
  export const SUBAGENT_TOOL_PROMPT_GUIDELINES = [
21
- `Use subagent only when delegation is needed. Before executing, call { action: "list" } and run only executable, non-disabled agents. ${AGENT_CAPABILITY_GUIDANCE}`,
22
- "Omit action for execution. Use { agent, task? } only for one child; use workflowScript for multi-step or parallel work.",
23
- "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.",
24
- WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE,
25
- WORKFLOW_LANES_GUIDANCE,
26
- WORKFLOW_HOST_GUIDANCE,
27
- WORKFLOW_RESUME_KEY_GUIDANCE,
28
- WORKFLOW_OUTPUT_BINDING_GUIDANCE,
29
- "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.",
30
- "Keep one writer per cwd/worktree unless writers run in isolated worktrees.",
31
- "To pass an explicit model to a child, first call { action: \"models\" } and copy an exact provider/id (e.g. provider/model-id); 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. provider/model-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.",
32
- EXTERNAL_CLI_RUNNER_GUIDANCE,
33
- "Use guide or the pi-subagents skill for advanced scheduling, missions, steering, and retention.",
21
+ 'Use subagent only when delegation is needed. Before execution, call { action: "list" } and run only executable, non-disabled agents.',
22
+ '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.',
23
+ "workflowScript rejects nested async function, arrow, and method helpers; use top-level await, plain helper functions, or explicit Promise chains.",
24
+ "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.",
25
+ '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" }.',
34
26
  ];
35
27
 
36
28
  export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
37
29
  • Use { action: "list" } before execution and only run executable/non-disabled agents.
38
30
  • Keep execution and management separate: omit action for structured single-child or workflowScript execution; use action only for management/control.
39
- • 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.
31
+ • 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.
40
32
  • ${WORKFLOW_RESUME_KEY_GUIDANCE}
41
33
  • ${WORKFLOW_OUTPUT_BINDING_GUIDANCE}
42
34
  • ${WORKFLOW_HOST_GUIDANCE}
@@ -47,6 +39,8 @@ export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
47
39
 
48
40
  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.
49
41
 
42
+ ${WORKFLOW_RESOURCE_GUIDANCE}
43
+
50
44
  EXECUTION:
51
45
  • ${EXTERNAL_CLI_RUNNER_GUIDANCE}
52
46
  • Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
@@ -69,6 +63,8 @@ ${SUBAGENT_SAFETY_GUIDANCE}`;
69
63
 
70
64
  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.
71
65
 
66
+ ${WORKFLOW_RESOURCE_GUIDANCE}
67
+
72
68
  EXECUTE:
73
69
  • ${EXTERNAL_CLI_RUNNER_GUIDANCE}
74
70
  • Call { action:"list" } first and use only executable/non-disabled agents.
@@ -85,7 +81,7 @@ MANAGE / CONTROL:
85
81
  • 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
82
 
87
83
  ASYNC / SAFETY:
88
- • 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.
84
+ • 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.
89
85
  • ${WORKFLOW_RESUME_KEY_GUIDANCE}
90
86
  • ${WORKFLOW_OUTPUT_BINDING_GUIDANCE}
91
87
  • ${WORKFLOW_HOST_GUIDANCE}