pi-subagents 0.50.0 → 0.52.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 (109) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/agents/oracle.md +3 -1
  3. package/agents/reviewer.md +1 -0
  4. package/agents/scout.md +2 -2
  5. package/agents/worker.md +2 -1
  6. package/async-retention-discovery-worker.mjs +180 -0
  7. package/docs/agents.md +40 -2
  8. package/docs/configuration.md +30 -12
  9. package/docs/extension-api.md +42 -1
  10. package/docs/models.md +2 -0
  11. package/docs/observability.md +41 -5
  12. package/docs/tool-reference.md +38 -39
  13. package/docs/workflows.md +171 -3
  14. package/package.json +4 -2
  15. package/skills/pi-subagents/SKILL.md +6 -4
  16. package/skills/pi-subagents/references/constraints-and-recipes.md +12 -6
  17. package/skills/pi-subagents/references/execution-controls.md +29 -15
  18. package/skills/pi-subagents/references/management-authoring-rpc.md +3 -3
  19. package/skills/pi-subagents/references/prompting-and-roles.md +4 -2
  20. package/src/agents/agent-management.ts +124 -351
  21. package/src/agents/agents.ts +157 -31
  22. package/src/agents/skills.ts +1 -1
  23. package/src/api/external-job-provider.ts +185 -0
  24. package/src/api/preflight.ts +36 -8
  25. package/src/api/shared-types.ts +2 -0
  26. package/src/extension/config.ts +3 -3
  27. package/src/extension/doctor.ts +3 -6
  28. package/src/extension/fanout-child.ts +2 -2
  29. package/src/extension/index.ts +166 -88
  30. package/src/extension/public-execution.ts +31 -2
  31. package/src/extension/schemas.ts +12 -35
  32. package/src/extension/tool-description.ts +35 -24
  33. package/src/inspectors/herdr/actions.ts +2 -2
  34. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  35. package/src/inspectors/herdr/project-panes.ts +2 -2
  36. package/src/intercom/native-supervisor-channel.ts +32 -10
  37. package/src/missions/lifecycle.ts +6 -1
  38. package/src/missions/store.ts +4 -9
  39. package/src/missions/workflow-state.ts +2 -2
  40. package/src/profiles/profiles.ts +3 -1
  41. package/src/runs/background/active-run-index.ts +31 -8
  42. package/src/runs/background/async-execution.ts +68 -40
  43. package/src/runs/background/async-job-tracker.ts +20 -4
  44. package/src/runs/background/async-resume.ts +47 -15
  45. package/src/runs/background/async-retention.ts +886 -0
  46. package/src/runs/background/async-status.ts +39 -53
  47. package/src/runs/background/chain-append.ts +3 -33
  48. package/src/runs/background/completion-dedupe.ts +5 -1
  49. package/src/runs/background/completion-replay.ts +22 -12
  50. package/src/runs/background/control-channel.ts +14 -68
  51. package/src/runs/background/fleet-view.ts +68 -19
  52. package/src/runs/background/index-segment.ts +59 -0
  53. package/src/runs/background/inspect-rpc.ts +443 -0
  54. package/src/runs/background/notify.ts +31 -5
  55. package/src/runs/background/result-files.ts +158 -90
  56. package/src/runs/background/result-watcher.ts +116 -20
  57. package/src/runs/background/resume-guidance.ts +8 -5
  58. package/src/runs/background/retained-children.ts +13 -3
  59. package/src/runs/background/run-id-query.ts +7 -0
  60. package/src/runs/background/run-id-resolver.ts +11 -9
  61. package/src/runs/background/run-status.ts +27 -18
  62. package/src/runs/background/scheduled-runs.ts +24 -4
  63. package/src/runs/background/stale-run-reconciler.ts +6 -3
  64. package/src/runs/background/steering.ts +11 -1
  65. package/src/runs/background/subagent-runner.ts +253 -140
  66. package/src/runs/background/subagent-wait.ts +8 -8
  67. package/src/runs/background/terminal-run-index.ts +129 -0
  68. package/src/runs/background/wait-completions.ts +21 -4
  69. package/src/runs/background/wait-subscriptions.ts +81 -2
  70. package/src/runs/foreground/async-steering-action.ts +21 -14
  71. package/src/runs/foreground/execution.ts +3 -1
  72. package/src/runs/foreground/subagent-executor.ts +666 -1579
  73. package/src/runs/foreground/workflow-detach-reconcile.ts +194 -0
  74. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  75. package/src/runs/shared/acceptance.ts +4 -4
  76. package/src/runs/shared/chain-outputs.ts +1 -3
  77. package/src/runs/shared/external-job-bridge.ts +444 -0
  78. package/src/runs/shared/external-job-runner.ts +286 -0
  79. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  80. package/src/runs/shared/model-fallback.ts +98 -38
  81. package/src/runs/shared/orca-progress-tabs.ts +84 -22
  82. package/src/runs/shared/parallel-handoff.ts +46 -4
  83. package/src/runs/shared/parallel-utils.ts +4 -15
  84. package/src/runs/shared/permissions.ts +5 -1
  85. package/src/runs/shared/pi-args.ts +8 -1
  86. package/src/runs/shared/session-lease.ts +0 -6
  87. package/src/runs/shared/subagent-control.ts +26 -4
  88. package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
  89. package/src/runs/shared/workflow-graph.ts +1 -23
  90. package/src/runs/shared/worktree.ts +14 -2
  91. package/src/shared/atomic-json.ts +22 -2
  92. package/src/shared/capacity-resilient-json.ts +102 -0
  93. package/src/shared/completion-owner.ts +14 -0
  94. package/src/shared/file-system-retry.ts +49 -1
  95. package/src/shared/fork-context.ts +42 -0
  96. package/src/shared/prompt-resources.ts +0 -40
  97. package/src/shared/settings.ts +3 -27
  98. package/src/shared/types.ts +71 -26
  99. package/src/shared/utils.ts +8 -0
  100. package/src/shared/watch-strategy.ts +10 -0
  101. package/src/slash/slash-bridge.ts +2 -1
  102. package/src/slash/slash-commands.ts +29 -3
  103. package/src/tui/fleet.ts +63 -15
  104. package/src/watchdog/change-signature.ts +1 -1
  105. package/src/watchdog/lsp-diagnostics.ts +1 -0
  106. package/src/workflows/chat-progress.ts +2 -2
  107. package/src/workflows/scripted-workflow.ts +220 -67
  108. package/src/runs/foreground/chain-clarify.ts +0 -1354
  109. package/src/runs/foreground/chain-execution.ts +0 -1581
@@ -0,0 +1,194 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { writeAtomicJson } from "../../shared/atomic-json.ts";
4
+ import { isStorageCapacityError } from "../../shared/file-system-retry.ts";
5
+ import { getSingleResultOutput, readStatus } from "../../shared/utils.ts";
6
+ import {
7
+ DIRS,
8
+ SUBAGENT_ASYNC_COMPLETE_EVENT,
9
+ type AsyncStatus,
10
+ type IntercomEventBus,
11
+ type SingleResult,
12
+ type SubagentState,
13
+ } from "../../shared/types.ts";
14
+ import { updateActiveRunIndex } from "../background/active-run-index.ts";
15
+ import { resultFilePath, writeAsyncResultFile } from "../background/result-files.ts";
16
+
17
+ function cloneWorkflowStatus(status: AsyncStatus): AsyncStatus {
18
+ return {
19
+ ...status,
20
+ steps: status.steps?.map((step) => ({ ...step })),
21
+ workflow: status.workflow ? { ...status.workflow, trace: [...(status.workflow.trace ?? [])] } : status.workflow,
22
+ };
23
+ }
24
+
25
+ function childSucceeded(result: Pick<SingleResult, "exitCode" | "error">): boolean {
26
+ return result.exitCode === 0 && !result.error;
27
+ }
28
+
29
+ export function applyDetachedChildToPausedWorkflow(
30
+ status: AsyncStatus,
31
+ input: { childRunId: string; result: Pick<SingleResult, "exitCode" | "error" | "sessionFile">; workflowKey?: string },
32
+ ): AsyncStatus | undefined {
33
+ if (status.mode !== "workflow" || status.state !== "paused") return undefined;
34
+ const next = cloneWorkflowStatus(status);
35
+ const step = next.steps?.find((candidate) => candidate.runId === input.childRunId)
36
+ ?? (input.workflowKey ? next.steps?.find((candidate) => candidate.workflowKey === input.workflowKey) : undefined);
37
+ if (!step) return undefined;
38
+ const succeeded = childSucceeded(input.result);
39
+ const updatedAt = Date.now();
40
+ step.status = succeeded ? "completed" : "failed";
41
+ step.endedAt = updatedAt;
42
+ delete step.activityState;
43
+ delete step.currentTool;
44
+ delete step.currentToolStartedAt;
45
+ if (input.result.sessionFile) step.sessionFile = input.result.sessionFile;
46
+ if (succeeded) delete step.error;
47
+ else if (input.result.error) step.error = input.result.error;
48
+ next.lastUpdate = updatedAt;
49
+ const promoted = promotePausedWorkflowIfSettled(next);
50
+ if (promoted?.state === "failed" && input.result.error) promoted.error = input.result.error;
51
+ return promoted ?? next;
52
+ }
53
+
54
+ export function promotePausedWorkflowIfSettled(status: AsyncStatus): AsyncStatus | undefined {
55
+ if (status.mode !== "workflow" || status.state !== "paused") return undefined;
56
+ const next = cloneWorkflowStatus(status);
57
+ const stillOpen = next.steps?.some((candidate) =>
58
+ candidate.status === "running"
59
+ || (candidate.status === "paused" && candidate.activityState === "needs_attention")
60
+ ) === true;
61
+ if (stillOpen || !next.steps?.length) return undefined;
62
+ const failed = next.steps.some((candidate) => candidate.status === "failed");
63
+ const updatedAt = Date.now();
64
+ next.lastUpdate = updatedAt;
65
+ next.state = failed ? "failed" : "complete";
66
+ next.endedAt = updatedAt;
67
+ delete next.activityState;
68
+ if (!failed) delete next.error;
69
+ return next;
70
+ }
71
+
72
+ function workflowResultChildren(status: AsyncStatus, childRunId: string, result: SingleResult, existingResults: unknown): unknown {
73
+ const output = getSingleResultOutput(result);
74
+ if (Array.isArray(existingResults)) {
75
+ return existingResults.map((entry) => {
76
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return entry;
77
+ const child = entry as Record<string, unknown>;
78
+ if (child.runId !== childRunId) return child;
79
+ return {
80
+ ...child,
81
+ success: childSucceeded(result),
82
+ output,
83
+ outputState: output.trim() ? "present" : "absent",
84
+ detached: undefined,
85
+ ...(result.error ? { error: result.error } : {}),
86
+ };
87
+ });
88
+ }
89
+ return status.steps?.map((step) => ({
90
+ workflowKey: step.workflowKey,
91
+ agent: step.agent,
92
+ runId: step.runId,
93
+ success: step.status === "completed" || step.status === "complete",
94
+ output: step.runId === childRunId ? output : "",
95
+ outputState: step.runId === childRunId && output.trim() ? "present" : "absent",
96
+ ...(step.error ? { error: step.error } : {}),
97
+ }));
98
+ }
99
+
100
+ function publishedWorkflowResult(status: AsyncStatus, childRunId: string, result: SingleResult, asyncDir: string, existing?: Record<string, unknown>): Record<string, unknown> {
101
+ const sessionId = status.sessionId ?? (typeof existing?.sessionId === "string" ? existing.sessionId : undefined);
102
+ const summary = status.state === "complete"
103
+ ? `Workflow completed after detached child ${childRunId} finished.`
104
+ : status.error ?? (typeof existing?.summary === "string" ? existing.summary : undefined);
105
+ return {
106
+ ...(existing ?? {}),
107
+ id: status.runId,
108
+ runId: status.runId,
109
+ toolCallId: status.toolCallId,
110
+ agent: "workflow",
111
+ mode: "workflow",
112
+ success: status.state === "complete",
113
+ state: status.state,
114
+ summary,
115
+ error: status.error,
116
+ activityState: status.activityState,
117
+ endedAt: status.endedAt,
118
+ timestamp: Date.now(),
119
+ results: workflowResultChildren(status, childRunId, result, existing?.results),
120
+ workflow: status.workflow,
121
+ asyncDir,
122
+ cwd: status.cwd,
123
+ sessionId,
124
+ completionOwnerId: status.completionOwnerId,
125
+ };
126
+ }
127
+
128
+ export function reconcileDetachedWorkflowChildCompletion(input: {
129
+ state: SubagentState;
130
+ workflowRunId: string;
131
+ childRunId: string;
132
+ result: SingleResult;
133
+ events?: IntercomEventBus;
134
+ workflowKey?: string;
135
+ }): boolean {
136
+ const job = input.state.asyncJobs.get(input.workflowRunId);
137
+ const asyncDir = job?.asyncDir ?? path.join(DIRS.async, input.workflowRunId);
138
+ const status = readStatus(asyncDir);
139
+ if (!status) return false;
140
+ const next = applyDetachedChildToPausedWorkflow(status, {
141
+ childRunId: input.childRunId,
142
+ result: input.result,
143
+ workflowKey: input.workflowKey,
144
+ });
145
+ if (!next) return false;
146
+ writeAtomicJson(path.join(asyncDir, "status.json"), next);
147
+ updateActiveRunIndex(asyncDir, next.state, next.toolCallId);
148
+ if (job) {
149
+ job.status = next.state;
150
+ job.updatedAt = next.lastUpdate;
151
+ job.activityState = next.activityState;
152
+ job.steps = next.steps?.map((step, index) => ({ ...step, index }));
153
+ job.workflow = next.workflow;
154
+ }
155
+ const resultPath = resultFilePath(DIRS.results, input.workflowRunId);
156
+ let existing: Record<string, unknown> | undefined;
157
+ try {
158
+ const parsed: unknown = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
159
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) existing = parsed as Record<string, unknown>;
160
+ } catch (error) {
161
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
162
+ }
163
+ const published = publishedWorkflowResult(next, input.childRunId, input.result, asyncDir, existing);
164
+ writeAsyncResultFile(resultPath, published);
165
+ if (next.state === "complete" || next.state === "failed") {
166
+ try {
167
+ fs.appendFileSync(path.join(asyncDir, "events.jsonl"), `${JSON.stringify({
168
+ ts: Date.now(),
169
+ runId: input.workflowRunId,
170
+ type: "subagent.workflow.completed",
171
+ state: next.state,
172
+ ...(next.error ? { error: next.error } : {}),
173
+ reconciledFromDetachedChild: input.childRunId,
174
+ })}\n`, "utf-8");
175
+ } catch (error) {
176
+ if (!isStorageCapacityError(error)) throw error;
177
+ }
178
+ input.events?.emit(SUBAGENT_ASYNC_COMPLETE_EVENT, {
179
+ id: input.workflowRunId,
180
+ runId: input.workflowRunId,
181
+ source: "async",
182
+ mode: "workflow",
183
+ agent: "workflow",
184
+ success: next.state === "complete",
185
+ state: next.state,
186
+ summary: typeof published.summary === "string" ? published.summary : (next.state === "complete" ? "Workflow completed." : next.error),
187
+ sessionId: next.sessionId,
188
+ completionOwnerId: next.completionOwnerId,
189
+ timestamp: Date.now(),
190
+ triggerTurn: true,
191
+ });
192
+ }
193
+ return true;
194
+ }
@@ -4,6 +4,7 @@ import * as path from "node:path";
4
4
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
5
5
  import type { Details, ForegroundRunControl, SubagentState } from "../../shared/types.ts";
6
6
  import { readStatus } from "../../shared/utils.ts";
7
+ import { steeringReceipt } from "../background/steering.ts";
7
8
  import {
8
9
  consumeSteerAckFromDir,
9
10
  readSteerCapability,
@@ -146,18 +147,18 @@ export async function steerWorkflowForegroundTarget(input: {
146
147
  targets: [target],
147
148
  };
148
149
  if (input.signal?.aborted) {
149
- return { content: [{ type: "text", text: `Steering pending for foreground run ${control.runId} (request ${request.id}); caller aborted before acknowledgment.` }], details: { mode: "management", results: [], steering } };
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 } };
150
151
  }
151
152
  if (ack?.state === "delivered") {
152
- return { content: [{ type: "text", text: `Steering delivered for foreground run ${control.runId} (request ${request.id}).` }], details: { mode: "management", results: [], steering } };
153
+ return { content: [{ type: "text", text: steeringReceipt(request.message, `Steering delivered for foreground run ${control.runId} (request ${request.id}).`) }], details: { mode: "management", results: [], steering } };
153
154
  }
154
155
  if (ack?.state === "queued") {
155
- return { content: [{ type: "text", text: `Steering queued for foreground run ${control.runId} (request ${request.id}).` }], details: { mode: "management", results: [], steering } };
156
+ return { content: [{ type: "text", text: steeringReceipt(request.message, `Steering queued for foreground run ${control.runId} (request ${request.id}).`) }], details: { mode: "management", results: [], steering } };
156
157
  }
157
158
  if (ack?.state === "failed") {
158
- return { content: [{ type: "text", text: `Steering failed for foreground run ${control.runId} (request ${request.id}): ${ack.message}` }], isError: true, details: { mode: "management", results: [], steering } };
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 } };
159
160
  }
160
- return { content: [{ type: "text", text: `Steering pending for foreground run ${control.runId} (request ${request.id}); no acknowledgment was received.` }], details: { mode: "management", results: [], steering } };
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 } };
161
162
  }
162
163
 
163
164
  export function workflowForegroundSteeringDir(asyncDirRoot: string, workflowRunId: string, childRunId: string): string {
@@ -946,7 +946,7 @@ function reportEvidenceStatus(report: AcceptanceReport, kind: AcceptanceEvidence
946
946
  }
947
947
 
948
948
  function checkNoStagedFiles(cwd: string): AcceptanceRuntimeCheck {
949
- const result = spawnSync("git", ["status", "--short"], { cwd, encoding: "utf-8" });
949
+ const result = spawnSync("git", ["status", "--short"], { cwd, encoding: "utf-8", windowsHide: true });
950
950
  if (result.status !== 0) {
951
951
  return { id: "no-staged-files", status: "not-applicable", message: "git status unavailable; no staged-files check skipped" };
952
952
  }
@@ -1053,11 +1053,11 @@ interface VerifyWorkspaceState {
1053
1053
  }
1054
1054
 
1055
1055
  function readVerifyWorkspaceState(cwd: string): VerifyWorkspaceState | undefined {
1056
- const repo = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8" });
1056
+ const repo = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8", windowsHide: true });
1057
1057
  if (repo.status !== 0 || !repo.stdout.trim()) return undefined;
1058
1058
  const repoRoot = fs.realpathSync(repo.stdout.trim());
1059
- const head = spawnSync("git", ["rev-parse", "HEAD"], { cwd: repoRoot, encoding: "utf-8" });
1060
- const diff = spawnSync("git", ["diff", "--binary", "--full-index", "HEAD", "--"], { cwd: repoRoot, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
1059
+ const head = spawnSync("git", ["rev-parse", "HEAD"], { cwd: repoRoot, encoding: "utf-8", windowsHide: true });
1060
+ const diff = spawnSync("git", ["diff", "--binary", "--full-index", "HEAD", "--"], { cwd: repoRoot, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024, windowsHide: true });
1061
1061
  if (head.status !== 0 || diff.status !== 0 || !head.stdout.trim()) return undefined;
1062
1062
  return {
1063
1063
  kind: "git-tracked",
@@ -1,4 +1,4 @@
1
- import { isCheckpointStep, isDynamicParallelStep, isParallelStep, type ChainStep, type SequentialStep } from "../../shared/settings.ts";
1
+ import { isDynamicParallelStep, isParallelStep, type ChainStep, type SequentialStep } from "../../shared/settings.ts";
2
2
  import type { ChainOutputMap, ChainOutputMapEntry, SingleResult } from "../../shared/types.ts";
3
3
  import { getSingleResultOutput } from "../../shared/utils.ts";
4
4
  import { DynamicFanoutError, hasDynamicFanoutFields, type DynamicFanoutConfig, validateDynamicStepShape } from "./dynamic-fanout.ts";
@@ -14,7 +14,6 @@ export interface ChainOutputValidationContext {
14
14
  }
15
15
 
16
16
  function outputNamesForStep(step: ChainStep): string[] {
17
- if (isCheckpointStep(step)) return [];
18
17
  if (isParallelStep(step)) return step.parallel.map((task) => task.as).filter((name): name is string => Boolean(name));
19
18
  if (isDynamicParallelStep(step)) return [step.collect.as];
20
19
  const name = (step as SequentialStep).as;
@@ -22,7 +21,6 @@ function outputNamesForStep(step: ChainStep): string[] {
22
21
  }
23
22
 
24
23
  function taskTemplatesForStep(step: ChainStep): string[] {
25
- if (isCheckpointStep(step)) return [];
26
24
  if (isParallelStep(step)) return step.parallel.map((task) => task.task ?? "{previous}");
27
25
  if (isDynamicParallelStep(step)) return [step.parallel.task ?? "{previous}", step.parallel.label ?? ""].filter(Boolean);
28
26
  return [(step as SequentialStep).task ?? "{previous}"];