pi-subagents 0.40.0 → 0.41.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 (119) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +246 -525
  3. package/agents/oracle.md +1 -0
  4. package/package.json +12 -4
  5. package/prompts/parallel-context-build.md +1 -1
  6. package/prompts/parallel-handoff-plan.md +1 -1
  7. package/prompts/review-loop.md +1 -1
  8. package/skills/pi-subagents/SKILL.md +6 -6
  9. package/skills/pi-subagents/references/constraints-and-recipes.md +19 -26
  10. package/skills/pi-subagents/references/execution-controls.md +98 -97
  11. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  12. package/skills/pi-subagents/references/prompting-and-roles.md +18 -27
  13. package/src/agents/agent-management.ts +155 -65
  14. package/src/agents/agent-serializer.ts +19 -0
  15. package/src/agents/agents.ts +154 -71
  16. package/src/agents/chain-serializer.ts +10 -7
  17. package/src/agents/frontmatter.ts +5 -3
  18. package/src/agents/identity.ts +1 -1
  19. package/src/agents/proactive-skills.ts +13 -10
  20. package/src/agents/skills.ts +23 -6
  21. package/src/api/control-channel.ts +4 -0
  22. package/src/api/delegation.ts +26 -194
  23. package/src/api/external-runs.ts +129 -0
  24. package/src/api/intercom-bridge.ts +3 -0
  25. package/src/api/pi-args.ts +5 -0
  26. package/src/api/preflight.ts +3 -3
  27. package/src/api/shared-types.ts +19 -0
  28. package/src/extension/config.ts +10 -0
  29. package/src/extension/control-notices.ts +5 -39
  30. package/src/extension/doctor.ts +10 -9
  31. package/src/extension/fanout-child.ts +7 -4
  32. package/src/extension/index.ts +232 -68
  33. package/src/extension/rpc.ts +18 -12
  34. package/src/extension/schemas.ts +48 -37
  35. package/src/extension/tool-description.ts +36 -86
  36. package/src/inspectors/herdr/actions.ts +229 -0
  37. package/src/inspectors/herdr/client.ts +130 -0
  38. package/src/inspectors/herdr/inspector-runner.ts +141 -0
  39. package/src/inspectors/herdr/project-panes.ts +154 -0
  40. package/src/integrations/herdr-status.ts +330 -0
  41. package/src/intercom/intercom-bridge.ts +3 -2
  42. package/src/intercom/result-intercom.ts +5 -1
  43. package/src/missions/actions.ts +372 -0
  44. package/src/missions/lifecycle.ts +314 -0
  45. package/src/missions/store.ts +442 -0
  46. package/src/missions/types.ts +135 -0
  47. package/src/policy/authority.ts +46 -0
  48. package/src/profiles/profiles.ts +29 -3
  49. package/src/runs/background/async-execution.ts +98 -49
  50. package/src/runs/background/async-job-tracker.ts +10 -2
  51. package/src/runs/background/async-resume.ts +6 -6
  52. package/src/runs/background/async-status.ts +29 -1
  53. package/src/runs/background/auto-drain.ts +3 -3
  54. package/src/runs/background/chain-append.ts +3 -2
  55. package/src/runs/background/control-channel.ts +9 -7
  56. package/src/runs/background/fleet-view.ts +3 -4
  57. package/src/runs/background/notify.ts +2 -1
  58. package/src/runs/background/process-terminal.ts +5 -5
  59. package/src/runs/background/result-watcher.ts +13 -5
  60. package/src/runs/background/run-id-resolver.ts +3 -3
  61. package/src/runs/background/run-status.ts +35 -8
  62. package/src/runs/background/scheduled-runs.ts +602 -375
  63. package/src/runs/background/stale-run-reconciler.ts +3 -3
  64. package/src/runs/background/subagent-runner.ts +608 -445
  65. package/src/runs/background/subagent-wait.ts +50 -9
  66. package/src/runs/background/wait-subscriptions.ts +253 -0
  67. package/src/runs/background/wait-tool.ts +12 -4
  68. package/src/runs/foreground/async-steering-action.ts +3 -3
  69. package/src/runs/foreground/chain-clarify.ts +8 -4
  70. package/src/runs/foreground/chain-execution.ts +56 -30
  71. package/src/runs/foreground/execution.ts +15 -2
  72. package/src/runs/foreground/subagent-executor.ts +1023 -273
  73. package/src/runs/shared/acceptance.ts +28 -6
  74. package/src/runs/shared/child-protocol.ts +302 -22
  75. package/src/runs/shared/dynamic-fanout.ts +1 -1
  76. package/src/runs/shared/external-cli-runner.ts +130 -0
  77. package/src/runs/shared/long-running-guard.ts +42 -1
  78. package/src/runs/shared/nested-events.ts +59 -5
  79. package/src/runs/shared/nested-render.ts +9 -4
  80. package/src/runs/shared/parallel-handoff.ts +86 -2
  81. package/src/runs/shared/parallel-utils.ts +11 -2
  82. package/src/runs/shared/permissions.ts +95 -0
  83. package/src/runs/shared/pi-args.ts +11 -1
  84. package/src/runs/shared/pi-spawn.ts +11 -1
  85. package/src/runs/shared/run-history.ts +1 -1
  86. package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
  87. package/src/runs/shared/subagent-startup-retry.ts +5 -2
  88. package/src/runs/shared/turn-budget.ts +6 -6
  89. package/src/runs/shared/worktree.ts +122 -12
  90. package/src/shared/accessible-dir.ts +29 -7
  91. package/src/shared/artifacts.ts +18 -1
  92. package/src/shared/fork-context.ts +3 -2
  93. package/src/shared/launch-contract.ts +1 -0
  94. package/src/shared/settings.ts +10 -0
  95. package/src/shared/types.ts +156 -14
  96. package/src/shared/utils.ts +8 -6
  97. package/src/slash/delegation-adapters.ts +32 -194
  98. package/src/slash/delegation-request.ts +43 -126
  99. package/src/slash/prompt-template-bridge.ts +158 -205
  100. package/src/slash/prompt-workflows.ts +21 -57
  101. package/src/slash/slash-bridge.ts +14 -0
  102. package/src/slash/slash-commands.ts +31 -632
  103. package/src/slash/subagents-admin.ts +18 -14
  104. package/src/tui/fleet-status.ts +156 -21
  105. package/src/tui/fleet-transcript.ts +110 -5
  106. package/src/tui/fleet.ts +56 -24
  107. package/src/tui/render.ts +291 -109
  108. package/src/types/pi-runtime-compat.d.ts +14 -0
  109. package/src/watchdog/lsp-diagnostics.ts +12 -7
  110. package/src/watchdog/model-selection.ts +2 -2
  111. package/src/watchdog/permission-arbiter.ts +145 -0
  112. package/src/watchdog/register-child.ts +1 -1
  113. package/src/watchdog/register-main.ts +1 -1
  114. package/src/watchdog/review.ts +4 -1
  115. package/src/watchdog/runtime.ts +3 -2
  116. package/src/workflows/chat-progress.ts +140 -0
  117. package/src/workflows/scripted-workflow.ts +415 -0
  118. package/agents/advisor.md +0 -73
  119. package/src/extension/chain-validation.ts +0 -181
@@ -1,8 +1,7 @@
1
- export const SUBAGENT_DELEGATION_PROTOCOL_VERSION = 1 as const;
2
- export const SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION = 2 as const;
3
-
4
- // This is the established extension-to-extension transport. The public API
5
- // intentionally reuses it instead of adding a second event protocol.
1
+ // This is the established extension-to-extension transport. The structured
2
+ // delegation API intentionally reuses it instead of adding a second event
3
+ // protocol. Unstructured payloads on the same events remain the legacy
4
+ // prompt-template bridge while we validate whether any integrations still use it.
6
5
  export const SUBAGENT_DELEGATION_REQUEST_EVENT = "prompt-template:subagent:request";
7
6
  export const SUBAGENT_DELEGATION_STARTED_EVENT = "prompt-template:subagent:started";
8
7
  export const SUBAGENT_DELEGATION_UPDATE_EVENT = "prompt-template:subagent:update";
@@ -20,114 +19,36 @@ export interface SubagentDelegationToolBudget {
20
19
  block?: string[] | "*";
21
20
  }
22
21
 
23
- export interface SubagentDelegationAgentContract {
24
- version: 1;
25
- }
26
-
27
22
  export type SubagentDelegationJsonSchemaObject = Record<string, unknown>;
28
23
 
29
- export interface SubagentDelegationExecutionResult {
30
- status: "completed" | "failed" | "paused" | "stopped" | "detached";
31
- success: boolean;
32
- exitCode: number;
33
- error?: string;
34
- interrupted?: boolean;
35
- timedOut?: boolean;
36
- stopped?: boolean;
37
- detached?: boolean;
38
- }
39
-
40
- export interface SubagentDelegationReviewResult {
41
- status: "not-requested" | "review-required" | "reviewed" | "blockers";
42
- findings?: Array<{ severity: "blocker" | "non-blocking"; file?: string; issue: string; rationale: string }>;
43
- }
44
-
45
- export interface SubagentDelegationEffectsResult {
46
- fileMutation?: {
47
- status: "not-requested" | "not-applicable" | "observed" | "missing";
48
- expected: boolean;
49
- attempted: boolean;
50
- message?: string;
51
- };
52
- }
53
-
54
- export type SubagentDelegationAcceptanceEvidence =
55
- | "changed-files"
56
- | "tests-added"
57
- | "commands-run"
58
- | "validation-output"
59
- | "residual-risks"
60
- | "no-staged-files"
61
- | "diff-summary"
62
- | "review-findings"
63
- | "manual-notes";
64
-
65
- export interface SubagentDelegationAcceptanceCriterion {
66
- id: string;
67
- must: string;
68
- evidence?: SubagentDelegationAcceptanceEvidence[];
69
- severity?: "required" | "recommended";
70
- }
71
-
72
- export interface SubagentDelegationAcceptanceVerifyCommand {
73
- id: string;
74
- command: string;
75
- timeoutMs?: number;
76
- cwd?: string;
77
- env?: Record<string, string>;
78
- allowFailure?: boolean;
79
- }
80
-
81
- export interface SubagentDelegationAcceptanceReview {
82
- agent?: string;
83
- focus?: string;
84
- required?: boolean;
85
- }
86
-
87
- interface SubagentDelegationAcceptanceFields {
88
- criteria?: Array<string | SubagentDelegationAcceptanceCriterion>;
89
- evidence?: SubagentDelegationAcceptanceEvidence[];
90
- verify?: SubagentDelegationAcceptanceVerifyCommand[];
91
- review?: SubagentDelegationAcceptanceReview | false;
92
- stopRules?: string[];
93
- }
94
-
95
- export type SubagentDelegationAcceptanceConfig = SubagentDelegationAcceptanceFields & (
96
- | { level: "none"; reason: string }
97
- | { level?: "auto" | "attested" | "checked" | "verified"; reason?: string }
98
- );
24
+ export type SubagentDelegationThinking = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
99
25
 
100
- export type SubagentDelegationAcceptance =
101
- | "auto"
102
- | "attested"
103
- | "checked"
104
- | "verified"
105
- | false
106
- | SubagentDelegationAcceptanceConfig;
26
+ export type SubagentDelegationResultRequest =
27
+ | { kind: "text" }
28
+ | { kind: "structured"; schema: SubagentDelegationJsonSchemaObject };
107
29
 
108
30
  export interface SubagentDelegationRequest {
109
- version: typeof SUBAGENT_DELEGATION_PROTOCOL_VERSION;
110
31
  requestId: string;
32
+ ownerRunId: string;
33
+ nodeId: string;
111
34
  agent: string;
112
35
  task: string;
113
36
  context: "fresh" | "fork";
114
37
  cwd: string;
115
38
  model?: string;
39
+ thinking?: SubagentDelegationThinking;
116
40
  timeoutMs?: number;
117
41
  turnBudget?: SubagentDelegationTurnBudget;
118
42
  toolBudget?: SubagentDelegationToolBudget;
119
43
  skill?: string | string[] | boolean;
120
- output?: string | boolean;
121
- outputMode?: "inline" | "file-only";
122
- outputSchema?: SubagentDelegationJsonSchemaObject;
123
- agentContract?: SubagentDelegationAgentContract;
124
- acceptance?: SubagentDelegationAcceptance;
125
44
  artifacts?: boolean;
45
+ result: SubagentDelegationResultRequest;
126
46
  }
127
47
 
128
48
  export interface SubagentDelegationStarted {
129
- version: typeof SUBAGENT_DELEGATION_PROTOCOL_VERSION;
130
49
  requestId: string;
50
+ ownerRunId: string;
51
+ nodeId: string;
131
52
  }
132
53
 
133
54
  export interface SubagentDelegationUpdate extends SubagentDelegationStarted {
@@ -154,102 +75,14 @@ export type SubagentDelegationStatus =
154
75
  | "structured_output_failed"
155
76
  | "acceptance_failed"
156
77
  | "invalid_request"
157
- | "unavailable_context";
158
-
159
- export type SubagentDelegationAcceptanceStatus =
160
- | "pending"
161
- | "not-required"
162
- | "claimed"
163
- | "attested"
164
- | "checked"
165
- | "verified"
166
- | "review-required"
167
- | "reviewed"
168
- | "accepted"
169
- | "rejected";
170
-
171
- export interface SubagentDelegationAcceptanceResult {
172
- status: SubagentDelegationAcceptanceStatus;
173
- evidenceStatus: Exclude<SubagentDelegationAcceptanceStatus, "review-required" | "reviewed" | "accepted">;
174
- explicit: boolean;
175
- }
176
-
177
- export interface SubagentDelegationResponse extends SubagentDelegationStarted {
178
- status: SubagentDelegationStatus;
179
- error?: string;
180
- runId?: string;
181
- childIndex?: number;
182
- agent?: string;
183
- model?: string;
184
- exitCode?: number;
185
- execution?: SubagentDelegationExecutionResult;
186
- output?: string;
187
- outputPath?: string;
188
- sessionFile?: string;
189
- acceptance?: SubagentDelegationAcceptanceResult;
190
- review?: SubagentDelegationReviewResult;
191
- effects?: SubagentDelegationEffectsResult;
192
- turns?: number;
193
- toolCount?: number;
194
- durationMs?: number;
195
- tokens?: number;
196
- warnings?: string[];
197
- }
78
+ | "unavailable_context"
79
+ | "duplicate_node";
198
80
 
199
- export interface SubagentDelegationCancel extends SubagentDelegationStarted {}
200
-
201
- export type SubagentDelegationV2Thinking = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
202
-
203
- export type SubagentDelegationV2ResultRequest =
204
- | { kind: "text" }
205
- | { kind: "structured"; schema: SubagentDelegationJsonSchemaObject };
206
-
207
- export interface SubagentDelegationV2Request {
208
- version: typeof SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION;
209
- requestId: string;
210
- ownerRunId: string;
211
- nodeId: string;
212
- agent: string;
213
- task: string;
214
- context: "fresh" | "fork";
215
- cwd: string;
216
- model?: string;
217
- thinking?: SubagentDelegationV2Thinking;
218
- timeoutMs?: number;
219
- turnBudget?: SubagentDelegationTurnBudget;
220
- toolBudget?: SubagentDelegationToolBudget;
221
- skill?: string | string[] | boolean;
222
- artifacts?: boolean;
223
- result: SubagentDelegationV2ResultRequest;
224
- }
225
-
226
- export interface SubagentDelegationV2Started {
227
- version: typeof SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION;
228
- requestId: string;
229
- ownerRunId: string;
230
- nodeId: string;
231
- }
232
-
233
- export interface SubagentDelegationV2Update extends SubagentDelegationV2Started {
234
- runId?: string;
235
- currentTool?: string;
236
- currentToolArgs?: string;
237
- recentOutput?: string;
238
- recentOutputLines?: string[];
239
- recentTools?: Array<{ tool: string; args: string }>;
240
- model?: string;
241
- toolCount?: number;
242
- durationMs?: number;
243
- tokens?: number;
244
- }
245
-
246
- export type SubagentDelegationV2Status = SubagentDelegationStatus | "duplicate_node";
247
-
248
- export type SubagentDelegationV2Value =
81
+ export type SubagentDelegationValue =
249
82
  | { kind: "text"; text: string }
250
83
  | { kind: "structured"; value: unknown };
251
84
 
252
- export interface SubagentDelegationV2Usage {
85
+ export interface SubagentDelegationUsage {
253
86
  input: number;
254
87
  output: number;
255
88
  cacheRead: number;
@@ -260,8 +93,8 @@ export interface SubagentDelegationV2Usage {
260
93
  durationMs: number;
261
94
  }
262
95
 
263
- export interface SubagentDelegationV2TerminalResponse extends SubagentDelegationV2Started {
264
- status: Exclude<SubagentDelegationV2Status, "invalid_request">;
96
+ export interface SubagentDelegationTerminalResponse extends SubagentDelegationStarted {
97
+ status: Exclude<SubagentDelegationStatus, "invalid_request">;
265
98
  error?: string;
266
99
  runId?: string;
267
100
  agent?: string;
@@ -269,13 +102,12 @@ export interface SubagentDelegationV2TerminalResponse extends SubagentDelegation
269
102
  thinking?: string;
270
103
  exitCode?: number;
271
104
  launchContractDigest?: string;
272
- result?: SubagentDelegationV2Value;
273
- usage?: SubagentDelegationV2Usage;
105
+ result?: SubagentDelegationValue;
106
+ usage?: SubagentDelegationUsage;
274
107
  }
275
108
 
276
- /** A malformed V2 request can only be correlated by the valid identity fields it supplied. */
277
- export interface SubagentDelegationV2InvalidResponse {
278
- version: typeof SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION;
109
+ /** A malformed structured request can only be correlated by the valid identity fields it supplied. */
110
+ export interface SubagentDelegationInvalidResponse {
279
111
  requestId: string;
280
112
  ownerRunId?: string;
281
113
  nodeId?: string;
@@ -283,6 +115,6 @@ export interface SubagentDelegationV2InvalidResponse {
283
115
  error?: string;
284
116
  }
285
117
 
286
- export type SubagentDelegationV2Response = SubagentDelegationV2TerminalResponse | SubagentDelegationV2InvalidResponse;
118
+ export type SubagentDelegationResponse = SubagentDelegationTerminalResponse | SubagentDelegationInvalidResponse;
287
119
 
288
- export interface SubagentDelegationV2Cancel extends SubagentDelegationV2Started {}
120
+ export interface SubagentDelegationCancel extends SubagentDelegationStarted {}
@@ -0,0 +1,129 @@
1
+ export const EXTERNAL_RUN_REGISTRY_VERSION = 1;
2
+ export const EXTERNAL_RUN_REGISTRY_KEY = "pi-subagents.external-runs.v1";
3
+
4
+ const MAX_PROVIDERS = 100;
5
+ const MAX_RUNS_PER_PROVIDER = 10_000;
6
+ const MAX_TEXT_LENGTH = 4_096;
7
+
8
+ export type ExternalRunState = "running" | "completed" | "failed" | "stopped";
9
+ export type ExternalRunCompletionReason = "exit" | "timeout" | "user-kill" | "auto-close-quiet" | "crash" | "unknown";
10
+
11
+ /** An observational record for work owned by another runtime. */
12
+ export interface ExternalRun {
13
+ id: string;
14
+ sessionId: string;
15
+ source: string;
16
+ state: ExternalRunState;
17
+ command?: string;
18
+ cwd?: string;
19
+ isolationPath?: string;
20
+ owner?: string;
21
+ completionReason?: ExternalRunCompletionReason;
22
+ reportPath?: string;
23
+ startedAt?: number;
24
+ completedAt?: number;
25
+ exitCode?: number | null;
26
+ residualRisks?: string[];
27
+ }
28
+
29
+ export interface ExternalRunProvider {
30
+ name: string;
31
+ listExternalRuns(): readonly ExternalRun[];
32
+ }
33
+
34
+ export interface RegisteredExternalRun extends ExternalRun {
35
+ provider: string;
36
+ }
37
+
38
+ interface ExternalRunRegistry {
39
+ version: typeof EXTERNAL_RUN_REGISTRY_VERSION;
40
+ providers: Map<string, ExternalRunProvider>;
41
+ }
42
+
43
+ function registry(): ExternalRunRegistry {
44
+ const key = Symbol.for(EXTERNAL_RUN_REGISTRY_KEY);
45
+ const target = globalThis as Record<PropertyKey, unknown>;
46
+ const existing = target[key];
47
+ if (existing === undefined) {
48
+ const created: ExternalRunRegistry = { version: EXTERNAL_RUN_REGISTRY_VERSION, providers: new Map() };
49
+ target[key] = created;
50
+ return created;
51
+ }
52
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) throw new Error(`Malformed external-run registry at Symbol.for("${EXTERNAL_RUN_REGISTRY_KEY}").`);
53
+ const candidate = existing as Partial<ExternalRunRegistry>;
54
+ if (candidate.version !== EXTERNAL_RUN_REGISTRY_VERSION || !(candidate.providers instanceof Map)) throw new Error(`Unsupported external-run registry at Symbol.for("${EXTERNAL_RUN_REGISTRY_KEY}").`);
55
+ return candidate as ExternalRunRegistry;
56
+ }
57
+
58
+ function text(value: unknown, field: string, required = false): string | undefined {
59
+ if (value === undefined && !required) return undefined;
60
+ if (typeof value !== "string" || value.length === 0 || value.trim() !== value || value.includes("\0")) throw new Error(`${field} must be a non-empty trimmed string without NUL characters.`);
61
+ if (value.length > MAX_TEXT_LENGTH) throw new Error(`${field} must be at most ${MAX_TEXT_LENGTH} characters.`);
62
+ return value;
63
+ }
64
+
65
+ function timestamp(value: unknown, field: string): number | undefined {
66
+ if (value === undefined) return undefined;
67
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new Error(`${field} must be a non-negative finite number.`);
68
+ return value;
69
+ }
70
+
71
+ function validateRun(value: unknown, provider: string, index: number): ExternalRun {
72
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`External-run provider '${provider}' item ${index} must be an object.`);
73
+ const run = value as Record<string, unknown>;
74
+ const fields = new Set(["id", "sessionId", "source", "state", "command", "cwd", "isolationPath", "owner", "completionReason", "reportPath", "startedAt", "completedAt", "exitCode", "residualRisks"]);
75
+ const unknown = Object.keys(run).filter((key) => !fields.has(key));
76
+ if (unknown.length) throw new Error(`External-run provider '${provider}' item ${index} has unknown fields: ${unknown.join(", ")}.`);
77
+ if (!["running", "completed", "failed", "stopped"].includes(run.state as string)) throw new Error(`External-run provider '${provider}' item ${index} state is invalid.`);
78
+ if (run.completionReason !== undefined && !["exit", "timeout", "user-kill", "auto-close-quiet", "crash", "unknown"].includes(run.completionReason as string)) throw new Error(`External-run provider '${provider}' item ${index} completionReason is invalid.`);
79
+ if (run.exitCode !== undefined && run.exitCode !== null && (!Number.isInteger(run.exitCode) || !Number.isFinite(run.exitCode))) throw new Error(`External-run provider '${provider}' item ${index} exitCode must be an integer or null.`);
80
+ if (run.residualRisks !== undefined && (!Array.isArray(run.residualRisks) || run.residualRisks.some((risk, riskIndex) => text(risk, `External-run provider '${provider}' item ${index} residualRisks[${riskIndex}]`, true) === undefined))) throw new Error(`External-run provider '${provider}' item ${index} residualRisks must be an array of strings.`);
81
+ return {
82
+ id: text(run.id, `External-run provider '${provider}' item ${index} id`, true)!,
83
+ sessionId: text(run.sessionId, `External-run provider '${provider}' item ${index} sessionId`, true)!,
84
+ source: text(run.source, `External-run provider '${provider}' item ${index} source`, true)!,
85
+ state: run.state as ExternalRunState,
86
+ ...(text(run.command, `External-run provider '${provider}' item ${index} command`) !== undefined ? { command: text(run.command, `External-run provider '${provider}' item ${index} command`) } : {}),
87
+ ...(text(run.cwd, `External-run provider '${provider}' item ${index} cwd`) !== undefined ? { cwd: text(run.cwd, `External-run provider '${provider}' item ${index} cwd`) } : {}),
88
+ ...(text(run.isolationPath, `External-run provider '${provider}' item ${index} isolationPath`) !== undefined ? { isolationPath: text(run.isolationPath, `External-run provider '${provider}' item ${index} isolationPath`) } : {}),
89
+ ...(text(run.owner, `External-run provider '${provider}' item ${index} owner`) !== undefined ? { owner: text(run.owner, `External-run provider '${provider}' item ${index} owner`) } : {}),
90
+ ...(run.completionReason !== undefined ? { completionReason: run.completionReason as ExternalRunCompletionReason } : {}),
91
+ ...(text(run.reportPath, `External-run provider '${provider}' item ${index} reportPath`) !== undefined ? { reportPath: text(run.reportPath, `External-run provider '${provider}' item ${index} reportPath`) } : {}),
92
+ ...(timestamp(run.startedAt, `External-run provider '${provider}' item ${index} startedAt`) !== undefined ? { startedAt: timestamp(run.startedAt, `External-run provider '${provider}' item ${index} startedAt`) } : {}),
93
+ ...(timestamp(run.completedAt, `External-run provider '${provider}' item ${index} completedAt`) !== undefined ? { completedAt: timestamp(run.completedAt, `External-run provider '${provider}' item ${index} completedAt`) } : {}),
94
+ ...(run.exitCode !== undefined ? { exitCode: run.exitCode as number | null } : {}),
95
+ ...(run.residualRisks !== undefined ? { residualRisks: [...run.residualRisks] as string[] } : {}),
96
+ };
97
+ }
98
+
99
+ /** Register an observational provider. pi-subagents never starts, stops, or steers these processes. */
100
+ export function registerExternalRunProvider(provider: ExternalRunProvider): () => void {
101
+ if (!provider || typeof provider !== "object" || Array.isArray(provider) || Object.keys(provider).some((key) => key !== "name" && key !== "listExternalRuns")) throw new Error("External-run provider must contain only name and listExternalRuns.");
102
+ const name = text(provider.name, "External-run provider name", true)!;
103
+ if (typeof provider.listExternalRuns !== "function") throw new Error(`External-run provider '${name}' must expose listExternalRuns().`);
104
+ const current = registry();
105
+ if (!current.providers.has(name) && current.providers.size >= MAX_PROVIDERS) throw new Error(`External-run registry supports at most ${MAX_PROVIDERS} providers.`);
106
+ current.providers.set(name, provider);
107
+ return () => { if (current.providers.get(name) === provider) current.providers.delete(name); };
108
+ }
109
+
110
+ /** Snapshot records for one Pi session. Records are read-only observations of foreign process ownership. */
111
+ export function snapshotExternalRuns(sessionId: string): readonly RegisteredExternalRun[] {
112
+ text(sessionId, "External-run snapshot sessionId", true);
113
+ const records: RegisteredExternalRun[] = [];
114
+ const identities = new Set<string>();
115
+ for (const [name, provider] of registry().providers) {
116
+ if (provider.name !== name) throw new Error(`External-run registry key '${name}' does not match provider name '${provider.name}'.`);
117
+ const runs = provider.listExternalRuns();
118
+ if (!Array.isArray(runs)) throw new Error(`External-run provider '${name}' listExternalRuns() must return an array.`);
119
+ if (runs.length > MAX_RUNS_PER_PROVIDER) throw new Error(`External-run provider '${name}' returned more than ${MAX_RUNS_PER_PROVIDER} runs.`);
120
+ runs.forEach((value, index) => {
121
+ const run = validateRun(value, name, index);
122
+ const identity = `${name}\0${run.sessionId}\0${run.id}`;
123
+ if (identities.has(identity)) throw new Error(`External-run provider '${name}' returned duplicate run '${run.id}' for session '${run.sessionId}'.`);
124
+ identities.add(identity);
125
+ if (run.sessionId === sessionId) records.push({ provider: name, ...run });
126
+ });
127
+ }
128
+ return records;
129
+ }
@@ -0,0 +1,3 @@
1
+ export {
2
+ resolveIntercomSessionTarget,
3
+ } from "../intercom/intercom-bridge.ts";
@@ -0,0 +1,5 @@
1
+ export {
2
+ resolvePiLaunchToolPlan,
3
+ type PiLaunchToolPlan,
4
+ type ResolvePiLaunchToolPlanInput,
5
+ } from "../runs/shared/pi-args.ts";
@@ -18,7 +18,7 @@ import type { ResolvedTurnBudget } from "../shared/types.ts";
18
18
  import type { ResolvedMcpDirectToolSelection } from "../runs/shared/mcp-direct-tool-allowlist.ts";
19
19
  import { resolveStepBehavior } from "../shared/settings.ts";
20
20
  import { agentDefinitionDigest, AGENT_DEFINITION_PROJECTION_VERSION, launchBindingDigest } from "../shared/launch-contract.ts";
21
- import { ASYNC_DIR, RESULTS_DIR, TEMP_ROOT_DIR } from "../shared/types.ts";
21
+ import { DIRS, TEMP_ROOT_DIR } from "../shared/types.ts";
22
22
  import { processTerminalCandidatePath, processTerminalPath } from "../runs/background/process-terminal.ts";
23
23
  import { nestedResultsPath } from "../runs/shared/nested-events.ts";
24
24
 
@@ -292,10 +292,10 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
292
292
  const sessionDir = sessionRoot ? path.join(sessionRoot, "run-0") : undefined;
293
293
  const lifecycleAsyncDir = input.nestedRootRunId
294
294
  ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", input.nestedRootRunId, runId)
295
- : path.join(ASYNC_DIR, runId);
295
+ : path.join(DIRS.async, runId);
296
296
  const lifecycleResultPath = input.nestedRootRunId
297
297
  ? nestedResultsPath(input.nestedRootRunId, runId)
298
- : path.join(RESULTS_DIR, `${runId}.json`);
298
+ : path.join(DIRS.results, `${runId}.json`);
299
299
  if (!sessionDir) diagnostics.push({ code: "host_required", severity: "host-required", message: "No sessionRoot/sessionDir was supplied; exact child session paths require the Pi host session-root policy." });
300
300
  if (input.availableModels === undefined && (input.model || agent.model || input.parentModel)) {
301
301
  diagnostics.push({ code: "host_required", severity: "host-required", message: "No availableModels snapshot was supplied; model resolution may differ from the active Pi host registry." });
@@ -0,0 +1,19 @@
1
+ export {
2
+ DEFAULT_FORK_PREAMBLE,
3
+ wrapForkTask,
4
+ type AcceptanceEvidenceKind,
5
+ type AcceptanceInput,
6
+ type AcceptanceLedger,
7
+ type AcceptanceLevel,
8
+ type AsyncStartedEvent,
9
+ type ControlEvent,
10
+ type Details,
11
+ type ExecutionProjection,
12
+ type JsonSchemaObject,
13
+ type OutputMode,
14
+ type ReviewProjection,
15
+ type SingleResult,
16
+ type SubagentResultStatus,
17
+ type SubagentRunMode,
18
+ type Usage,
19
+ } from "../shared/types.ts";
@@ -1,7 +1,10 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { ArtifactDirPreference, ExtensionConfig } from "../shared/types.ts";
4
+ import { validateMissionStoreConfig } from "../missions/store.ts";
5
+ import { validateAuthorityPolicy } from "../policy/authority.ts";
4
6
  import { getAgentDir } from "../shared/utils.ts";
7
+ import { validatePermissionConfig } from "../runs/shared/permissions.ts";
5
8
 
6
9
  const ARTIFACT_DIR_PREFERENCES = new Set<ArtifactDirPreference>(["project", "session", "temp"]);
7
10
 
@@ -19,6 +22,9 @@ function readConfigForUpdate(configPath = getConfigPath()): ExtensionConfig {
19
22
  if (config.artifactDir !== undefined && !ARTIFACT_DIR_PREFERENCES.has(config.artifactDir as ArtifactDirPreference)) {
20
23
  throw new Error(`config.artifactDir must be "project", "session", or "temp"`);
21
24
  }
25
+ validateMissionStoreConfig(config.missions);
26
+ validateAuthorityPolicy(config.authorityPolicy);
27
+ validatePermissionConfig(config.permissions);
22
28
  return parsed as ExtensionConfig;
23
29
  }
24
30
 
@@ -34,6 +40,10 @@ export function updateConfig(updater: (config: ExtensionConfig) => ExtensionConf
34
40
  return next;
35
41
  }
36
42
 
43
+ export function resolveAsyncByDefault(config: Pick<ExtensionConfig, "asyncByDefault">): boolean {
44
+ return config.asyncByDefault !== false;
45
+ }
46
+
37
47
  export function loadConfig(): ExtensionConfig {
38
48
  const configPath = getConfigPath();
39
49
  try {
@@ -20,21 +20,6 @@ export function formatSubagentControlNotice(details: SubagentControlMessageDetai
20
20
  return details.noticeText ?? content ?? formatControlNoticeMessage(details.event, controlNoticeTarget(details));
21
21
  }
22
22
 
23
- function noticeTimerKey(details: SubagentControlMessageDetails): string {
24
- const childIntercomTarget = controlNoticeTarget(details);
25
- return `${details.event.runId}:${controlNotificationKey(details.event, childIntercomTarget)}`;
26
- }
27
-
28
- export function clearPendingForegroundControlNotices(state: SubagentState, runId?: string): void {
29
- const pending = state.pendingForegroundControlNotices;
30
- if (!pending) return;
31
- for (const [key, timer] of pending) {
32
- if (runId !== undefined && !key.startsWith(`${runId}:`)) continue;
33
- clearTimeout(timer);
34
- pending.delete(key);
35
- }
36
- }
37
-
38
23
  function deliverControlNotice(input: {
39
24
  pi: Pick<ExtensionAPI, "sendMessage">;
40
25
  visibleControlNotices: Set<string>;
@@ -56,37 +41,18 @@ function deliverControlNotice(input: {
56
41
  );
57
42
  }
58
43
 
59
- function isForegroundNoticeStillActionable(state: SubagentState, details: SubagentControlMessageDetails): boolean {
60
- const control = state.foregroundControls.get(details.event.runId);
61
- if (!control) return false;
62
- if (control.currentAgent && control.currentAgent !== details.event.agent) return false;
63
- if (details.event.index !== undefined && control.currentIndex !== details.event.index) return false;
64
- return control.currentActivityState === "needs_attention";
65
- }
66
-
67
44
  export function handleSubagentControlNotice(input: {
68
45
  pi: Pick<ExtensionAPI, "sendMessage">;
69
46
  state: SubagentState;
70
47
  visibleControlNotices: Set<string>;
71
48
  details: SubagentControlMessageDetails;
72
- foregroundDelayMs?: number;
73
49
  }): void {
74
50
  if (!input.details?.event || input.details.event.type === "active_long_running") return;
75
- if (input.details.source !== "foreground") {
76
- deliverControlNotice(input);
51
+ if (input.details.source === "foreground") {
52
+ // A foreground tool blocks Pi from displaying this message. The run can
53
+ // finish before Pi flushes it, and queued messages cannot be withdrawn.
54
+ // Foreground control remains available through the live tool and fleet state.
77
55
  return;
78
56
  }
79
-
80
- const pending = input.state.pendingForegroundControlNotices ?? new Map<string, ReturnType<typeof setTimeout>>();
81
- input.state.pendingForegroundControlNotices = pending;
82
- const timerKey = noticeTimerKey(input.details);
83
- const existing = pending.get(timerKey);
84
- if (existing) clearTimeout(existing);
85
- const timer = setTimeout(() => {
86
- pending.delete(timerKey);
87
- if (!isForegroundNoticeStillActionable(input.state, input.details)) return;
88
- deliverControlNotice(input);
89
- }, input.foregroundDelayMs ?? 1000);
90
- timer.unref?.();
91
- pending.set(timerKey, timer);
57
+ deliverControlNotice(input);
92
58
  }
@@ -6,9 +6,8 @@ import { formatSpawnBudgetSummary, getSpawnBudgetSnapshot } from "../runs/shared
6
6
  import { diagnoseIntercomBridge, type IntercomBridgeDiagnostic } from "../intercom/intercom-bridge.ts";
7
7
  import { discoverAvailableSkills, type SkillSource } from "../agents/skills.ts";
8
8
  import {
9
- ASYNC_DIR,
9
+ DIRS,
10
10
  CHAIN_RUNS_DIR,
11
- RESULTS_DIR,
12
11
  TEMP_ROOT_DIR,
13
12
  type ExtensionConfig,
14
13
  type SubagentState,
@@ -43,12 +42,14 @@ interface DoctorReportInput {
43
42
  deps?: Partial<DoctorDeps>;
44
43
  }
45
44
 
46
- const DEFAULT_PATHS: DoctorPaths = {
47
- tempRootDir: TEMP_ROOT_DIR,
48
- asyncDir: ASYNC_DIR,
49
- resultsDir: RESULTS_DIR,
50
- chainRunsDir: CHAIN_RUNS_DIR,
51
- };
45
+ function defaultPaths(): DoctorPaths {
46
+ return {
47
+ tempRootDir: TEMP_ROOT_DIR,
48
+ asyncDir: DIRS.async,
49
+ resultsDir: DIRS.results,
50
+ chainRunsDir: CHAIN_RUNS_DIR,
51
+ };
52
+ }
52
53
 
53
54
  const DEFAULT_DEPS: DoctorDeps = {
54
55
  isAsyncAvailable,
@@ -192,7 +193,7 @@ function formatPermissionSystemSection(): string[] {
192
193
  }
193
194
 
194
195
  export function buildDoctorReport(input: DoctorReportInput): string {
195
- const paths = input.paths ?? DEFAULT_PATHS;
196
+ const paths = input.paths ?? defaultPaths();
196
197
  const deps = { ...DEFAULT_DEPS, ...input.deps };
197
198
  const lines = [
198
199
  "Subagents doctor report",
@@ -11,7 +11,7 @@ import { readNestedControlRequests, resolveNestedRouteFromEnv, writeNestedContro
11
11
  import { deliverSubagentIntercomMessageEvent } from "../intercom/result-intercom.ts";
12
12
  import { resolveSubagentIntercomTarget } from "../intercom/intercom-bridge.ts";
13
13
  import { SubagentParams } from "./schemas.ts";
14
- import { loadConfig } from "./config.ts";
14
+ import { loadConfig, resolveAsyncByDefault } from "./config.ts";
15
15
  import { type Details, type SubagentState } from "../shared/types.ts";
16
16
 
17
17
  function getSubagentSessionRoot(parentSessionFile: string | null): string {
@@ -37,7 +37,6 @@ function createChildSafeState(): SubagentState {
37
37
  foregroundRuns: new Map(),
38
38
  foregroundControls: new Map(),
39
39
  lastForegroundControlId: null,
40
- pendingForegroundControlNotices: new Map(),
41
40
  cleanupTimers: new Map(),
42
41
  lastUiContext: null,
43
42
  poller: null,
@@ -146,7 +145,7 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
146
145
  pi,
147
146
  state,
148
147
  config,
149
- asyncByDefault: config.asyncByDefault === true,
148
+ asyncByDefault: resolveAsyncByDefault(config),
150
149
  waitToolEnabled: resolveWaitToolConfig(config.waitTool).enabled,
151
150
  tempArtifactsDir: getArtifactsDir(null),
152
151
  getSubagentSessionRoot,
@@ -165,7 +164,11 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
165
164
  ].join("\n"),
166
165
  parameters: SubagentParams,
167
166
  execute(id, params, signal, onUpdate, ctx) {
168
- return executor.execute(id, params as SubagentParamsLike, signal, onUpdate, ctx);
167
+ const input = params as SubagentParamsLike;
168
+ if (input.tasks !== undefined || input.chain !== undefined || input.concurrency !== undefined || input.chainDir !== undefined || (input.worktree !== undefined && !(input.worktree === true && input.agent))) {
169
+ return Promise.resolve({ content: [{ type: "text", text: "Legacy top-level chain and parallel inputs were removed; use workflowScript." }], isError: true, details: { mode: "management", results: [] } });
170
+ }
171
+ return executor.execute(id, input, signal ?? new AbortController().signal, onUpdate, ctx);
169
172
  },
170
173
  };
171
174