pi-subagents 0.63.0 → 0.65.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 (107) hide show
  1. package/CHANGELOG.md +57 -1
  2. package/README.md +2 -2
  3. package/agents/reviewer.md +1 -1
  4. package/agents/scout.md +1 -1
  5. package/docs/agents.md +18 -16
  6. package/docs/configuration.md +7 -15
  7. package/docs/extension-api.md +15 -6
  8. package/docs/missions.md +2 -0
  9. package/docs/observability.md +10 -12
  10. package/docs/tool-reference.md +7 -6
  11. package/docs/watchdog.md +92 -114
  12. package/docs/workflows.md +5 -3
  13. package/package.json +3 -4
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  15. package/skills/pi-subagents/references/execution-controls.md +6 -5
  16. package/src/agents/agent-management.ts +47 -15
  17. package/src/api/capability-ceiling.ts +0 -1
  18. package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  19. package/src/api/preflight.ts +2 -3
  20. package/src/extension/doctor.ts +2 -10
  21. package/src/extension/fanout-child.ts +9 -11
  22. package/src/extension/index.ts +27 -5
  23. package/src/extension/public-execution.ts +14 -0
  24. package/src/extension/rpc.ts +3 -2
  25. package/src/extension/schemas.ts +2 -1
  26. package/src/extension/tool-description.ts +9 -8
  27. package/src/intercom/native-supervisor-channel.ts +138 -60
  28. package/src/intercom/supervisor-ui.ts +243 -0
  29. package/src/runs/background/async-execution.ts +46 -24
  30. package/src/runs/background/async-job-tracker.ts +11 -0
  31. package/src/runs/background/async-resume.ts +11 -2
  32. package/src/runs/background/control-channel.ts +2 -204
  33. package/src/runs/background/notify.ts +41 -2
  34. package/src/runs/background/process-terminal.ts +1 -1
  35. package/src/runs/background/run-child-session.ts +613 -0
  36. package/src/runs/background/run-status.ts +0 -1
  37. package/src/runs/background/runner-aliases.ts +125 -0
  38. package/src/runs/background/runner-child-sessions.ts +31 -0
  39. package/src/runs/background/scheduled-runs.ts +18 -4
  40. package/src/runs/background/subagent-runner.ts +203 -898
  41. package/src/runs/foreground/async-steering-action.ts +1 -17
  42. package/src/runs/foreground/execution.ts +196 -378
  43. package/src/runs/foreground/foreground-control.ts +4 -0
  44. package/src/runs/foreground/subagent-executor.ts +121 -58
  45. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  46. package/src/runs/shared/abort-recovery.ts +3 -3
  47. package/src/runs/shared/acceptance.ts +10 -0
  48. package/src/runs/shared/async-status-projection.ts +11 -43
  49. package/src/runs/shared/capability-ceiling.ts +1 -2
  50. package/src/runs/shared/child-hooks.ts +25 -0
  51. package/src/runs/shared/child-identity.ts +13 -2
  52. package/src/runs/shared/child-launch.ts +314 -0
  53. package/src/runs/shared/child-lifecycle.ts +25 -0
  54. package/src/runs/shared/child-runtime-config.ts +126 -0
  55. package/src/runs/shared/child-session.ts +342 -0
  56. package/src/runs/shared/child-tool-plan.ts +530 -0
  57. package/src/runs/shared/claude-code-adapter.ts +5 -1
  58. package/src/runs/shared/completion-guard.ts +1 -1
  59. package/src/runs/shared/external-cli-preflight.ts +16 -0
  60. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  61. package/src/runs/shared/model-exclusions.ts +82 -14
  62. package/src/runs/shared/model-fallback.ts +47 -4
  63. package/src/runs/shared/nested-events.ts +29 -45
  64. package/src/runs/shared/nested-path.ts +0 -14
  65. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  66. package/src/runs/shared/parallel-utils.ts +0 -2
  67. package/src/runs/shared/permissions.ts +0 -13
  68. package/src/runs/shared/process-signal.ts +4 -1
  69. package/src/runs/shared/run-fanout-budget.ts +0 -13
  70. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  71. package/src/runs/shared/structured-output.ts +17 -4
  72. package/src/runs/shared/subagent-control.ts +6 -2
  73. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  74. package/src/runs/shared/tool-availability.ts +18 -62
  75. package/src/runs/shared/tool-budget.ts +0 -14
  76. package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
  77. package/src/runs/shared/worktree.ts +117 -30
  78. package/src/shared/child-session-name.ts +1 -1
  79. package/src/shared/jsonl-writer.ts +11 -0
  80. package/src/shared/thinking-ceiling.ts +0 -6
  81. package/src/shared/types.ts +64 -30
  82. package/src/shared/utils.ts +3 -4
  83. package/src/slash/slash-commands.ts +0 -6
  84. package/src/tui/fleet.ts +0 -1
  85. package/src/tui/render.ts +238 -31
  86. package/src/watchdog/child-status.ts +54 -34
  87. package/src/watchdog/diff-tool.ts +77 -0
  88. package/src/watchdog/emission-guard.ts +5 -3
  89. package/src/watchdog/guidance.ts +20 -0
  90. package/src/watchdog/register-child.ts +28 -25
  91. package/src/watchdog/register-main.ts +10 -9
  92. package/src/watchdog/render.ts +4 -5
  93. package/src/watchdog/review.ts +15 -4
  94. package/src/watchdog/rules.ts +70 -0
  95. package/src/watchdog/runtime.ts +75 -92
  96. package/src/watchdog/scope.ts +0 -11
  97. package/src/watchdog/settings.ts +48 -104
  98. package/src/watchdog/types.ts +18 -32
  99. package/src/watchdog/warning-format.ts +0 -1
  100. package/src/workflows/chat-progress.ts +3 -2
  101. package/src/workflows/scripted-workflow.ts +48 -1
  102. package/src/workflows/workflow-checklist.ts +10 -12
  103. package/src/workflows/workflow-preflight.ts +28 -1
  104. package/src/runs/shared/child-protocol.ts +0 -415
  105. package/src/runs/shared/pi-args.ts +0 -1062
  106. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  107. package/src/shared/post-exit-stdio-guard.ts +0 -85
@@ -1,20 +1,10 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import * as fs from "node:fs";
3
2
  import * as path from "node:path";
4
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
5
4
  import type { Details, ForegroundRunControl, SubagentState } from "../../shared/types.ts";
6
5
  import { readStatus } from "../../shared/utils.ts";
7
6
  import { steeringReceipt } from "../background/steering.ts";
8
- import {
9
- consumeSteerAckFromDir,
10
- readSteerCapability,
11
- steerAcksDir,
12
- steerCapabilityPath,
13
- stepSteerInboxDir,
14
- writeSteerRequestToExistingDir,
15
- type SteerDeliveryMode,
16
- type SteerRequest,
17
- } from "../background/control-channel.ts";
7
+ import type { SteerDeliveryMode } from "../background/control-channel.ts";
18
8
 
19
9
  export interface WorkflowForegroundSteeringTarget {
20
10
  control: ForegroundRunControl;
@@ -40,7 +30,6 @@ function activeWorkflowError(state: SubagentState, workflowRunId: string, asyncD
40
30
  function controlIsLiveInWorkflow(control: ForegroundRunControl, workflowRunId: string, sessionId: string): boolean {
41
31
  return control.parentWorkflowRunId === workflowRunId
42
32
  && control.sessionId === sessionId
43
- && Boolean(control.workflowSteeringDir)
44
33
  && (control.activeChildren?.size ?? 0) > 0;
45
34
  }
46
35
 
@@ -77,17 +66,18 @@ function managementError(message: string): AgentToolResult<Details> {
77
66
  return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
78
67
  }
79
68
 
69
+ /**
70
+ * Steer a live workflow-owned foreground child through its in-process session.
71
+ * `steer` (and `auto`) interrupt the child at its next safe point; `follow_up`
72
+ * queues the message until the current run settles.
73
+ */
80
74
  export async function steerWorkflowForegroundTarget(input: {
81
75
  target: WorkflowForegroundSteeringTarget;
82
76
  message: string;
83
77
  mode?: SteerDeliveryMode;
84
78
  index?: number;
85
- signal?: AbortSignal;
86
- ackTimeoutMs?: number;
87
79
  }): Promise<AgentToolResult<Details>> {
88
80
  const { control, sourceRunId } = input.target;
89
- const routeDir = control.workflowSteeringDir;
90
- if (!routeDir || !fs.existsSync(routeDir)) return managementError(`Foreground run '${control.runId}' has no live workflow steering route.`);
91
81
  const activeIndexes = [...(control.activeChildren?.keys() ?? [])].sort((left, right) => left - right);
92
82
  const index = input.index ?? (activeIndexes.length === 1 ? activeIndexes[0] : undefined);
93
83
  if (index === undefined) {
@@ -95,94 +85,30 @@ export async function steerWorkflowForegroundTarget(input: {
95
85
  ? `Foreground run '${control.runId}' has no live child session.`
96
86
  : `Foreground run '${control.runId}' has ${activeIndexes.length} live child sessions; provide index.`);
97
87
  }
98
- if (!activeIndexes.includes(index)) return managementError(`Foreground run '${control.runId}' child ${index} is not live.`);
99
- const capability = readSteerCapability(routeDir, index);
100
- if (capability?.supported === false) return managementError(`Foreground run '${control.runId}' child ${index} does not support steering.`);
88
+ const child = control.activeChildren?.get(index);
89
+ if (!child) return managementError(`Foreground run '${control.runId}' child ${index} is not live.`);
90
+ if (!child.steer) return managementError(`Foreground run '${control.runId}' child ${index} does not support steering.`);
101
91
 
102
- const request: SteerRequest = {
103
- type: "steer",
104
- id: randomUUID(),
105
- ts: Date.now(),
106
- message: input.message.trim(),
107
- ...(input.mode && input.mode !== "steer" ? { mode: input.mode } : {}),
108
- targetIndex: index,
109
- source: "steer-action",
110
- };
111
- try {
112
- writeSteerRequestToExistingDir(stepSteerInboxDir(routeDir, index), request);
113
- } catch (error) {
114
- if (typeof error === "object" && error !== null && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") {
115
- return managementError(`Foreground run '${control.runId}' has no live workflow steering route.`);
116
- }
117
- return managementError(`Failed to queue steering for foreground run ${control.runId}: ${error instanceof Error ? error.message : String(error)}`);
118
- }
119
-
120
- const deadline = Date.now() + (input.ackTimeoutMs ?? 3_000);
121
- let ack;
122
- let routeRemoved = false;
123
- while (Date.now() <= deadline) {
124
- ack = consumeSteerAckFromDir(steerAcksDir(routeDir, index), request.id);
125
- if (ack || input.signal?.aborted) break;
126
- if (!fs.existsSync(routeDir)) {
127
- routeRemoved = true;
128
- break;
129
- }
130
- await new Promise<void>((resolve) => setTimeout(resolve, Math.min(50, Math.max(1, deadline - Date.now()))));
131
- }
132
- if (routeRemoved || (!ack && !input.signal?.aborted && !fs.existsSync(routeDir))) {
133
- return managementError(`Foreground run '${control.runId}' has no live child session.`);
134
- }
135
- const target = ack?.state === "delivered"
136
- ? { index, state: "delivered" as const, deliveredAt: ack.ts }
137
- : ack?.state === "queued"
92
+ const message = input.message.trim();
93
+ const requestId = randomUUID();
94
+ const outcome = await child.steer({ message, ...(input.mode && input.mode !== "steer" ? { mode: input.mode } : {}) });
95
+ const target = outcome.state === "delivered"
96
+ ? { index, state: "delivered" as const, deliveredAt: Date.now() }
97
+ : outcome.state === "queued"
138
98
  ? { index, state: "queued" as const }
139
- : ack?.state === "failed"
140
- ? { index, state: "failed" as const, reason: ack.message }
141
- : { index, state: "pending" as const };
99
+ : { index, state: "failed" as const, reason: outcome.reason };
142
100
  const steering = {
143
- requestId: request.id,
144
- state: ack?.state === "delivered" ? "delivered" as const : ack?.state === "failed" ? "failed" as const : "pending" as const,
145
- deliveryStatus: ack?.state === "delivered" ? "delivered" as const : "queued" as const,
101
+ requestId,
102
+ state: outcome.state === "delivered" ? "delivered" as const : outcome.state === "failed" ? "failed" as const : "pending" as const,
103
+ deliveryStatus: outcome.state === "delivered" ? "delivered" as const : "queued" as const,
146
104
  sourceRunId,
147
105
  targets: [target],
148
106
  };
149
- if (input.signal?.aborted) {
150
- return { content: [{ type: "text", text: steeringReceipt(request.message, `Steering pending for foreground run ${control.runId} (request ${request.id}); caller aborted before acknowledgment.`) }], details: { mode: "management", results: [], steering } };
151
- }
152
- if (ack?.state === "delivered") {
153
- return { content: [{ type: "text", text: steeringReceipt(request.message, `Steering delivered for foreground run ${control.runId} (request ${request.id}).`) }], details: { mode: "management", results: [], steering } };
154
- }
155
- if (ack?.state === "queued") {
156
- return { content: [{ type: "text", text: steeringReceipt(request.message, `Steering queued for foreground run ${control.runId} (request ${request.id}).`) }], details: { mode: "management", results: [], steering } };
107
+ if (outcome.state === "delivered") {
108
+ return { content: [{ type: "text", text: steeringReceipt(message, `Steering delivered for foreground run ${control.runId} (request ${requestId}).`) }], details: { mode: "management", results: [], steering } };
157
109
  }
158
- if (ack?.state === "failed") {
159
- return { content: [{ type: "text", text: steeringReceipt(request.message, `Steering failed for foreground run ${control.runId} (request ${request.id}): ${ack.message}`) }], isError: true, details: { mode: "management", results: [], steering } };
110
+ if (outcome.state === "queued") {
111
+ return { content: [{ type: "text", text: steeringReceipt(message, `Steering queued for foreground run ${control.runId} (request ${requestId}).`) }], details: { mode: "management", results: [], steering } };
160
112
  }
161
- return { content: [{ type: "text", text: steeringReceipt(request.message, `Steering pending for foreground run ${control.runId} (request ${request.id}); no acknowledgment was received.`) }], details: { mode: "management", results: [], steering } };
162
- }
163
-
164
- export function workflowForegroundSteeringDir(asyncDirRoot: string, workflowRunId: string, childRunId: string): string {
165
- return path.join(asyncDirRoot, workflowRunId, "control", "workflow-foreground", childRunId);
166
- }
167
-
168
- export function removeWorkflowForegroundSteeringRoute(control: ForegroundRunControl): void {
169
- if (!control.workflowSteeringDir) return;
170
- try {
171
- fs.rmSync(control.workflowSteeringDir, { recursive: true, force: true });
172
- } catch (error) {
173
- console.warn(`[pi-subagents] Failed to remove workflow foreground steering route '${control.workflowSteeringDir}': ${error instanceof Error ? error.message : String(error)}`);
174
- }
175
- }
176
-
177
- export function workflowForegroundSteeringLaunchOptions(control: ForegroundRunControl | undefined, index: number): Pick<import("../../shared/types.ts").RunSyncOptions, "steerInboxDir" | "steerCapabilityPath" | "steerAckDir"> {
178
- if (!control?.workflowSteeringDir) return {};
179
- const steerInboxDir = stepSteerInboxDir(control.workflowSteeringDir, index);
180
- const steerAckDir = steerAcksDir(control.workflowSteeringDir, index);
181
- fs.mkdirSync(steerInboxDir, { recursive: true });
182
- fs.mkdirSync(steerAckDir, { recursive: true });
183
- return {
184
- steerInboxDir,
185
- steerCapabilityPath: steerCapabilityPath(control.workflowSteeringDir, index),
186
- steerAckDir,
187
- };
113
+ return { content: [{ type: "text", text: steeringReceipt(message, `Steering failed for foreground run ${control.runId} (request ${requestId}): ${outcome.reason ?? "unknown error"}`) }], isError: true, details: { mode: "management", results: [], steering } };
188
114
  }
@@ -101,9 +101,9 @@ export function planAbortRecovery(input: {
101
101
  if (input.alreadyResumed) return settle("resume already attempted");
102
102
  if (!input.sessionAvailable) return settle("retained session unavailable");
103
103
  if (input.stopped || input.interrupted) return settle("explicit stop or interrupt");
104
- // A compaction abort can leave the child process open until the runner's
105
- // terminal drain sends SIGTERM. The compaction + settlement markers make
106
- // that cleanup signal non-authoritative; explicit stop/interrupt still wins.
104
+ // An external-job runner reports a process signal when its terminal drain
105
+ // ends a child left open by a compaction abort. The compaction + settlement
106
+ // markers make that cleanup signal non-authoritative; explicit stop/interrupt still wins.
107
107
  if (input.processSignal && !compactionAbortCandidate) return settle("process terminated by signal");
108
108
  if (input.timedOut) return settle("elapsed timeout");
109
109
  if (input.toolBudgetExhausted || input.usageBudgetExhausted) return settle("budget exhausted");
@@ -21,7 +21,9 @@ import type {
21
21
  ResolvedAcceptanceGate,
22
22
  SingleResult,
23
23
  SubagentRunMode,
24
+ ChildWatchdogProgress,
24
25
  } from "../../shared/types.ts";
26
+ import { unresolvedChildWatchdogBlockers } from "../../watchdog/child-status.ts";
25
27
  import { isAgentContractV1 } from "./agent-contract.ts";
26
28
  import { classifyTaskMutationIntent, stripSeverityCompounds, taskMayMutate } from "./task-intent.ts";
27
29
 
@@ -1360,6 +1362,7 @@ export async function evaluateAcceptance(input: {
1360
1362
  reportOptional?: boolean;
1361
1363
  artifactsDir?: string;
1362
1364
  runId?: string;
1365
+ watchdog?: ChildWatchdogProgress;
1363
1366
  }): Promise<AcceptanceLedger> {
1364
1367
  const acceptance = input.acceptance;
1365
1368
  const initialStatus = acceptance.level === "none" ? "not-required" : "claimed";
@@ -1375,6 +1378,13 @@ export async function evaluateAcceptance(input: {
1375
1378
  };
1376
1379
  if (acceptance.level === "none") return ledger;
1377
1380
 
1381
+ if (input.watchdog) {
1382
+ const unresolved = unresolvedChildWatchdogBlockers(input.watchdog);
1383
+ ledger.runtimeChecks.push(unresolved.length
1384
+ ? { id: "watchdog-blocker", status: "failed", message: `Unresolved watchdog blocker: ${unresolved[0]!.summary}` }
1385
+ : { id: "watchdog-blocker", status: "passed", message: "No unresolved watchdog blockers." });
1386
+ }
1387
+
1378
1388
  const parsed: AcceptanceReportParseResult = input.reportError
1379
1389
  ? { error: input.reportError }
1380
1390
  : input.report !== undefined
@@ -2,6 +2,7 @@ import { sanitizeDisplayText, truncateDisplayText } from "../../shared/display-t
2
2
  import { formatModelThinking } from "../../shared/formatters.ts";
3
3
  import type { AsyncJobState, AsyncJobStep, HostStepFreshnessV1, HostStepMonitorKind, HostStepNodeV1, HostStepState, HostStepVerdict, NestedRunSummary, NestedStepSummary, SubagentRunMode, WorkflowGraphSnapshot, WorkflowPreflightLaneV1, WorkflowPreflightV1 } from "../../shared/types.ts";
4
4
  import { HOST_STEP_MAX_COUNT, HOST_STEP_MAX_DETAIL_CHARS, HOST_STEP_MAX_LABEL_CHARS, HOST_STEP_MAX_PROVIDER_CHARS, HOST_STEP_MAX_REASON_CHARS, HOST_STEP_MAX_REF_CHARS, HOST_STEP_MAX_ROLE_CHARS, HOST_STEP_MAX_TARGET_CHARS, hostStepReportName, parseHostStepNode, validHostStepNodes } from "./host-step-status.ts";
5
+ import { workflowPreflightLaneForRuntimeKey } from "../../workflows/workflow-preflight.ts";
5
6
  import { workflowGraphStageNodes } from "./workflow-graph.ts";
6
7
 
7
8
  export const ASYNC_STATUS_SNAPSHOT_KIND = "pi-subagents.async-status-snapshot";
@@ -472,7 +473,7 @@ function projectWorkflowGraphRow(node: WorkflowGraphSnapshot["nodes"][number], p
472
473
  };
473
474
  }
474
475
 
475
- /** Project loaded workflow child facts plus stored preflight hints into compact rows. */
476
+ /** Project authoritative workflow facts into compact rows, annotated by preflight hints. */
476
477
  export function projectAsyncWorkflowRows(
477
478
  steps: readonly AsyncJobStep[] | undefined,
478
479
  hostStepsOrPreflight?: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | WorkflowPreflightV1,
@@ -482,7 +483,8 @@ export function projectAsyncWorkflowRows(
482
483
  const graph = isWorkflowGraph(hostStepsOrPreflight) ? hostStepsOrPreflight : undefined;
483
484
  const hostSteps = isWorkflowPreflight(hostStepsOrPreflight) || graph ? undefined : hostStepsOrPreflight;
484
485
  const loaded = steps ?? [];
485
- const declared = new Map<string, WorkflowPreflightLaneV1>();
486
+ const preflightForKey = (key: string, groupKeys: readonly (string | undefined)[] = []): WorkflowPreflightLaneV1 | undefined =>
487
+ workflowPreflightLaneForRuntimeKey(preflight, key, groupKeys);
486
488
  if (graph) {
487
489
  const loadedIndexesByKey = new Map<string, number[]>();
488
490
  for (const [index, step] of loaded.entries()) {
@@ -493,7 +495,6 @@ export function projectAsyncWorkflowRows(
493
495
  }
494
496
  const consumed = new Set<number>();
495
497
  const childRows: AsyncStatusWorkflowRow[] = [];
496
- for (const lane of preflight?.lanes ?? []) declared.set(lane.key, lane);
497
498
  const graphStages = workflowGraphStageNodes(graph);
498
499
  const graphKeys = new Set(graphStages.map((node) => node.id));
499
500
  const graphPhaseByNodeId = new Map<string, string>();
@@ -502,60 +503,27 @@ export function projectAsyncWorkflowRows(
502
503
  if (graphKeys.has(nodeId) && !graphPhaseByNodeId.has(nodeId)) graphPhaseByNodeId.set(nodeId, phase.title);
503
504
  }
504
505
  }
505
- const graphLaneKeys = new Set(graphPhaseByNodeId.values());
506
- const preflightForNode = (node: WorkflowGraphSnapshot["nodes"][number]): WorkflowPreflightLaneV1 | undefined => {
507
- const phaseTitle = graphPhaseByNodeId.get(node.id);
508
- return declared.get(node.id) ?? (node.phase ? declared.get(node.phase) : undefined) ?? (phaseTitle ? declared.get(phaseTitle) : undefined);
509
- };
510
506
  for (const node of graphStages) {
507
+ const lane = preflightForKey(node.id, [node.phase, graphPhaseByNodeId.get(node.id)]);
511
508
  const indexes = (loadedIndexesByKey.get(node.id) ?? []).filter((index) => !consumed.has(index));
512
- if (indexes.length > 0) {
513
- for (const index of indexes) {
514
- consumed.add(index);
515
- childRows.push(projectLoadedWorkflowRow(loaded[index]!, index, preflightForNode(node)));
516
- }
517
- } else {
518
- childRows.push(projectWorkflowGraphRow(node, preflightForNode(node)));
519
- }
520
- }
521
- for (const lane of preflight?.lanes ?? []) {
522
- if (graphKeys.has(lane.key) || graphLaneKeys.has(lane.key)) continue;
523
- const indexes = (loadedIndexesByKey.get(lane.key) ?? []).filter((index) => !consumed.has(index));
524
509
  if (indexes.length > 0) {
525
510
  for (const index of indexes) {
526
511
  consumed.add(index);
527
512
  childRows.push(projectLoadedWorkflowRow(loaded[index]!, index, lane));
528
513
  }
529
514
  } else {
530
- childRows.push({ name: lane.key, state: "planned", preflight: lane });
515
+ childRows.push(projectWorkflowGraphRow(node, lane));
531
516
  }
532
517
  }
533
518
  for (const [index, step] of loaded.entries()) {
534
- if (!consumed.has(index)) childRows.push(projectLoadedWorkflowRow(step, index, step.workflowKey ? declared.get(step.workflowKey) : undefined));
519
+ if (!consumed.has(index)) childRows.push(projectLoadedWorkflowRow(step, index, step.workflowKey ? preflightForKey(step.workflowKey, [step.phase]) : undefined));
535
520
  }
536
521
  return [...childRows, ...validHostStepList(graph).map(hostStepRow)];
537
522
  }
538
- const loadedIndexByKey = new Map<string, number>();
539
- for (const [index, step] of loaded.entries()) {
540
- if (step.workflowKey !== undefined && !loadedIndexByKey.has(step.workflowKey)) loadedIndexByKey.set(step.workflowKey, index);
541
- }
542
- const consumed = new Set<number>();
543
- const childRows: AsyncStatusWorkflowRow[] = [];
544
- for (const lane of preflight?.lanes ?? []) {
545
- declared.set(lane.key, lane);
546
- const index = loadedIndexByKey.get(lane.key);
547
- if (index === undefined) {
548
- childRows.push({ name: lane.key, state: "planned", preflight: lane });
549
- continue;
550
- }
551
- consumed.add(index);
552
- childRows.push(projectLoadedWorkflowRow(loaded[index]!, index, lane));
553
- }
554
- for (const [index, step] of loaded.entries()) {
555
- if (consumed.has(index)) continue;
556
- childRows.push(projectLoadedWorkflowRow(step, index, step.workflowKey ? declared.get(step.workflowKey) : undefined));
557
- }
558
- return [...childRows, ...validHostStepList(hostSteps).map(hostStepRow)];
523
+ return [
524
+ ...loaded.map((step, index) => projectLoadedWorkflowRow(step, index, step.workflowKey ? preflightForKey(step.workflowKey, [step.phase]) : undefined)),
525
+ ...validHostStepList(hostSteps).map(hostStepRow),
526
+ ];
559
527
  }
560
528
 
561
529
  function projectLoadedWorkflowRow(step: AsyncJobStep, index: number, preflight?: WorkflowPreflightLaneV1): AsyncStatusWorkflowRow {
@@ -2,7 +2,6 @@ import { Buffer } from "node:buffer";
2
2
 
3
3
  export const SUBAGENT_CAPABILITY_CEILING_VERSION = 1 as const;
4
4
  export const SUBAGENT_CAPABILITY_CEILING_REGISTRY_KEY = "pi-subagents.capability-ceiling.v1";
5
- export const SUBAGENT_CAPABILITY_CEILING_ENV = "PI_SUBAGENT_CAPABILITY_CEILING_V1";
6
5
 
7
6
  export type SubagentCapabilityCeiling =
8
7
  | { allowedTools: readonly string[]; allowedAgents?: readonly string[]; denyExtensions?: boolean }
@@ -167,7 +166,7 @@ export function resolveSubagentCapabilityCeiling(sessionId: string | undefined,
167
166
  }
168
167
 
169
168
  export function resolveCurrentSubagentCapabilityCeiling(sessionId: string | undefined): ResolvedSubagentCapabilityCeiling | undefined {
170
- return resolveSubagentCapabilityCeiling(sessionId, decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]));
169
+ return resolveSubagentCapabilityCeiling(sessionId, undefined);
171
170
  }
172
171
 
173
172
  export function isAgentAllowedByCapabilityCeiling(agentName: string, ceiling: ResolvedSubagentCapabilityCeiling | undefined): boolean {
@@ -0,0 +1,25 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import registerFanoutChildSubagentExtension from "../../extension/fanout-child.ts";
3
+ import registerSubagentFastModeExtension from "./fast-mode-extension.ts";
4
+ import registerSubagentPromptRuntime from "./subagent-prompt-runtime.ts";
5
+ import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
6
+
7
+ /** Inline extension shape accepted by pi's resource loader (`extensionFactories`). */
8
+ export interface ChildHookExtension {
9
+ name: string;
10
+ factory: (pi: ExtensionAPI) => void;
11
+ }
12
+
13
+ /**
14
+ * The child-side hooks pi-subagents installs in every child, keyed off the
15
+ * launch config. The registrations live in `subagent-prompt-runtime.ts`,
16
+ * `fast-mode-extension.ts`, and `fanout-child.ts`.
17
+ */
18
+ export function createChildHooks(config: ChildRuntimeConfig): ChildHookExtension[] {
19
+ const hooks: ChildHookExtension[] = [
20
+ { name: "pi-subagents:prompt-runtime", factory: (pi) => registerSubagentPromptRuntime(pi, config) },
21
+ ];
22
+ if (config.fast) hooks.push({ name: "pi-subagents:fast-mode", factory: (pi) => registerSubagentFastModeExtension(pi) });
23
+ if (config.fanoutChild) hooks.push({ name: "pi-subagents:fanout-child", factory: (pi) => registerFanoutChildSubagentExtension(pi, config) });
24
+ return hooks;
25
+ }
@@ -14,11 +14,11 @@ export type AsyncStatusChildResolution =
14
14
  | { ok: false; code: "not_found" | "ambiguous"; message: string };
15
15
 
16
16
  export function asyncStatusChildIdentity(step: AsyncStatusStep, index: number): string {
17
- return step.workflowKey ?? step.runId ?? `step:${index}`;
17
+ return asyncStatusChildIdentityCandidates(step, index)[0]!;
18
18
  }
19
19
 
20
20
  export function asyncStatusChildIdentityCandidates(step: AsyncStatusStep, index: number): string[] {
21
- return [...new Set([step.workflowKey, step.runId, `step:${index}`].filter((value): value is string => typeof value === "string" && value.length > 0))];
21
+ return [...new Set([step.childId, step.workflowKey, step.runId, `step:${index}`].filter((value): value is string => typeof value === "string" && value.length > 0))];
22
22
  }
23
23
 
24
24
  export function resolveAsyncStatusChild(
@@ -49,3 +49,14 @@ export function resolveAsyncStatusChild(
49
49
  export function isStoppableAsyncStatusStep(step: AsyncStatusStep): boolean {
50
50
  return step.status === "pending" || step.status === "running";
51
51
  }
52
+
53
+ export function stopStoppableAsyncStatusChildren(
54
+ status: Pick<AsyncStatus, "steps">,
55
+ stopChild: ((childId: string, message?: string) => boolean) | undefined,
56
+ message: string,
57
+ ): void {
58
+ if (!stopChild) return;
59
+ for (const [index, step] of (status.steps ?? []).entries()) {
60
+ if (isStoppableAsyncStatusStep(step)) stopChild(asyncStatusChildIdentity(step, index), message);
61
+ }
62
+ }