pi-subagents 0.58.0 → 0.60.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 (116) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/docs/agents.md +5 -3
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +1 -1
  5. package/docs/models.md +24 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +93 -5
  8. package/docs/workflows.md +94 -3
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +48 -243
  12. package/skills/council-mode/references/pass-contracts.md +150 -0
  13. package/skills/pi-subagents/SKILL.md +87 -37
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
  15. package/skills/pi-subagents/references/execution-controls.md +52 -7
  16. package/skills/pi-subagents/references/management-authoring-rpc.md +3 -4
  17. package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
  18. package/skills/pi-subagents/references/prompting-and-roles.md +54 -26
  19. package/skills/pi-subagents/references/review-and-validation.md +73 -0
  20. package/src/agents/agent-management.ts +159 -42
  21. package/src/agents/agent-serializer.ts +4 -2
  22. package/src/agents/agents.ts +67 -26
  23. package/src/agents/runtime-agent-registry.ts +9 -16
  24. package/src/api/background-work.ts +5 -1
  25. package/src/api/delegation.ts +0 -7
  26. package/src/api/preflight.ts +6 -9
  27. package/src/api/shared-types.ts +2 -0
  28. package/src/extension/fanout-child.ts +5 -3
  29. package/src/extension/index.ts +51 -23
  30. package/src/extension/public-execution.ts +16 -2
  31. package/src/extension/schemas.ts +36 -10
  32. package/src/extension/tool-description.ts +24 -5
  33. package/src/intercom/result-intercom.ts +2 -0
  34. package/src/profiles/profiles.ts +5 -6
  35. package/src/runs/background/active-async-capacity.ts +2 -2
  36. package/src/runs/background/async-execution.ts +52 -37
  37. package/src/runs/background/async-job-tracker.ts +17 -12
  38. package/src/runs/background/async-resume.ts +23 -25
  39. package/src/runs/background/async-status-snapshot.ts +23 -261
  40. package/src/runs/background/async-status.ts +110 -8
  41. package/src/runs/background/chain-append.ts +6 -3
  42. package/src/runs/background/chain-root-attachment.ts +60 -8
  43. package/src/runs/background/control-channel.ts +3 -2
  44. package/src/runs/background/fleet-view.ts +21 -11
  45. package/src/runs/background/notify.ts +158 -6
  46. package/src/runs/background/result-files.ts +2 -1
  47. package/src/runs/background/result-watcher.ts +2 -0
  48. package/src/runs/background/resume-guidance.ts +1 -1
  49. package/src/runs/background/retained-children.ts +1 -1
  50. package/src/runs/background/run-status.ts +30 -9
  51. package/src/runs/background/scheduled-runs.ts +86 -7
  52. package/src/runs/background/stale-run-reconciler.ts +10 -4
  53. package/src/runs/background/steering.ts +4 -14
  54. package/src/runs/background/subagent-runner.ts +351 -358
  55. package/src/runs/background/subagent-wait.ts +68 -12
  56. package/src/runs/background/terminal-run-index.ts +1 -1
  57. package/src/runs/background/wait-completions.ts +25 -1
  58. package/src/runs/background/wait-config.ts +23 -9
  59. package/src/runs/background/wait-tool.ts +9 -2
  60. package/src/runs/foreground/async-steering-action.ts +2 -2
  61. package/src/runs/foreground/execution.ts +125 -122
  62. package/src/runs/foreground/foreground-control.ts +3 -0
  63. package/src/runs/foreground/foreground-history.ts +1 -0
  64. package/src/runs/foreground/subagent-executor.ts +576 -240
  65. package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
  66. package/src/runs/shared/abort-recovery.ts +119 -0
  67. package/src/runs/shared/async-status-projection.ts +597 -0
  68. package/src/runs/shared/background-process-options.ts +9 -0
  69. package/src/runs/shared/child-identity.ts +19 -4
  70. package/src/runs/shared/child-launch-plan.ts +151 -0
  71. package/src/runs/shared/completion-evidence.ts +89 -0
  72. package/src/runs/shared/completion-guard.ts +1 -1
  73. package/src/runs/shared/dynamic-fanout.ts +2 -2
  74. package/src/runs/shared/host-step-status.ts +230 -0
  75. package/src/runs/shared/lane-metadata.ts +105 -0
  76. package/src/runs/shared/mcp-config-sources.ts +42 -6
  77. package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
  78. package/src/runs/shared/mcp-direct-tool-grant.ts +194 -0
  79. package/src/runs/shared/model-fallback.ts +9 -2
  80. package/src/runs/shared/mutation-evidence.ts +52 -3
  81. package/src/runs/shared/nested-events.ts +6 -2
  82. package/src/runs/shared/nested-render.ts +7 -3
  83. package/src/runs/shared/parallel-handoff.ts +419 -7
  84. package/src/runs/shared/parallel-utils.ts +7 -0
  85. package/src/runs/shared/pi-args.ts +67 -3
  86. package/src/runs/shared/single-output.ts +72 -22
  87. package/src/runs/shared/subagent-prompt-runtime.ts +33 -6
  88. package/src/runs/shared/workflow-graph.ts +15 -0
  89. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  90. package/src/runs/shared/worktree.ts +18 -0
  91. package/src/shared/child-session-name.ts +46 -0
  92. package/src/shared/extension-context.ts +24 -0
  93. package/src/shared/formatters.ts +5 -2
  94. package/src/shared/launch-contract.ts +1 -1
  95. package/src/shared/settings.ts +9 -103
  96. package/src/shared/types.ts +232 -30
  97. package/src/shared/utils.ts +35 -55
  98. package/src/slash/delegation-adapters.ts +1 -8
  99. package/src/slash/delegation-request.ts +0 -4
  100. package/src/slash/slash-bridge.ts +1 -2
  101. package/src/slash/slash-commands.ts +369 -90
  102. package/src/slash/slash-live-state.ts +22 -11
  103. package/src/tui/fleet-status.ts +134 -75
  104. package/src/tui/fleet.ts +11 -5
  105. package/src/tui/render-helpers.ts +31 -0
  106. package/src/tui/render.ts +895 -121
  107. package/src/watchdog/change-signature.ts +40 -1
  108. package/src/watchdog/turn-delta.ts +1 -1
  109. package/src/workflows/chat-progress.ts +6 -3
  110. package/src/workflows/host-command.ts +235 -0
  111. package/src/workflows/scripted-workflow.ts +503 -38
  112. package/src/workflows/workflow-child-summary.ts +9 -5
  113. package/src/workflows/workflow-preflight.ts +270 -0
  114. package/src/workflows/workflow-receipt.ts +43 -4
  115. package/src/workflows/workflow-settlement.ts +246 -0
  116. package/src/runs/shared/turn-budget.ts +0 -98
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import * as fs from "node:fs";
3
4
  import * as path from "node:path";
@@ -11,6 +12,8 @@ import type { SubagentParamsLike } from "../foreground/subagent-executor.ts";
11
12
  import { validateExecutionAcceptance } from "../shared/acceptance.ts";
12
13
  import type { ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
13
14
  import { previewSimpleWorkflowRun } from "../../workflows/scripted-workflow.ts";
15
+ import { resolveGitRepositoryIdentity } from "../../workflows/chat-progress.ts";
16
+ import { getConfigDirName } from "../../shared/utils.ts";
14
17
 
15
18
  export const SCHEDULED_RUN_ACTIONS = [
16
19
  "schedule.create",
@@ -142,11 +145,83 @@ function validateScheduleId(id: string): string {
142
145
  return id;
143
146
  }
144
147
 
148
+ function normalizedComparisonPath(value: string): string {
149
+ const absolute = path.resolve(value);
150
+ if (process.platform !== "win32") return absolute;
151
+ let normalized = absolute;
152
+ try { normalized = fs.realpathSync.native(absolute); } catch {}
153
+ normalized = normalized.replaceAll("/", "\\");
154
+ if (normalized.startsWith("\\\\?\\UNC\\")) normalized = `\\\\${normalized.slice(8)}`;
155
+ else if (normalized.startsWith("\\\\?\\")) normalized = normalized.slice(4);
156
+ normalized = path.win32.normalize(normalized).toLowerCase();
157
+ if (normalized.length > 3) normalized = normalized.replace(/[\\]+$/, "");
158
+ return normalized;
159
+ }
160
+
145
161
  function pathWithin(root: string, candidate: string): boolean {
146
- const relative = path.relative(root, candidate);
162
+ const relative = path.relative(normalizedComparisonPath(root), normalizedComparisonPath(candidate));
147
163
  return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
148
164
  }
149
165
 
166
+ function resolveGitCommonDirForCheckout(checkoutRoot: string): string | undefined {
167
+ const gitPath = path.join(checkoutRoot, ".git");
168
+ try {
169
+ const stat = fs.statSync(gitPath);
170
+ if (stat.isDirectory()) return fs.realpathSync.native(gitPath);
171
+ if (!stat.isFile()) return undefined;
172
+ const match = /^gitdir:[ \t]*([^\r\n]+)$/i.exec(fs.readFileSync(gitPath, "utf-8").trim());
173
+ return match?.[1] ? fs.realpathSync.native(path.resolve(checkoutRoot, match[1])) : undefined;
174
+ } catch {
175
+ return undefined;
176
+ }
177
+ }
178
+
179
+ function samePath(left: string, right: string): boolean {
180
+ return normalizedComparisonPath(left) === normalizedComparisonPath(right);
181
+ }
182
+
183
+ function resolveTrustedGitConfigRoot(checkoutRoot: string, commonDir: string): string | undefined {
184
+ const resolvedCommonDir = resolveGitCommonDirForCheckout(checkoutRoot);
185
+ if (!resolvedCommonDir || !samePath(resolvedCommonDir, commonDir)) return undefined;
186
+ const configRoot = path.join(checkoutRoot, getConfigDirName());
187
+ try {
188
+ const resolved = fs.realpathSync.native(configRoot);
189
+ return fs.statSync(resolved).isDirectory() && pathWithin(checkoutRoot, resolved) ? resolved : undefined;
190
+ } catch {
191
+ return undefined;
192
+ }
193
+ }
194
+
195
+ function registeredGitWorktreeRoots(projectCwd: string): string[] {
196
+ const result = spawnSync("git", ["-C", projectCwd, "worktree", "list", "--porcelain"], { encoding: "utf-8", windowsHide: true });
197
+ if (result.status !== 0 || typeof result.stdout !== "string") return [];
198
+ const roots: string[] = [];
199
+ for (const line of result.stdout.split(/\r?\n/)) {
200
+ if (!line.startsWith("worktree ")) continue;
201
+ try { roots.push(fs.realpathSync.native(line.slice("worktree ".length).trim())); } catch {}
202
+ }
203
+ return roots;
204
+ }
205
+
206
+ function resolveSharedGitConfigRoot(projectCwd: string): string | undefined {
207
+ const repository = resolveGitRepositoryIdentity(projectCwd);
208
+ if (!repository) return undefined;
209
+ let projectConfigRoot: string;
210
+ try {
211
+ projectConfigRoot = fs.realpathSync.native(path.join(projectCwd, getConfigDirName()));
212
+ } catch {
213
+ return undefined;
214
+ }
215
+ // Git may report a separate-git-dir primary checkout as its common git
216
+ // directory, not as the checkout root. Without a registered checkout root,
217
+ // do not infer a shared config path from that unprovable layout.
218
+ for (const checkoutRoot of new Set(registeredGitWorktreeRoots(projectCwd))) {
219
+ const resolved = resolveTrustedGitConfigRoot(checkoutRoot, repository.commonDir);
220
+ if (resolved && samePath(resolved, projectConfigRoot)) return resolved;
221
+ }
222
+ return undefined;
223
+ }
224
+
150
225
  function assertScheduleRoot(root: string, projectCwd: string | undefined, create: boolean): void {
151
226
  if (!projectCwd) {
152
227
  if (create) fs.mkdirSync(root, { recursive: true, mode: 0o700 });
@@ -154,7 +229,7 @@ function assertScheduleRoot(root: string, projectCwd: string | undefined, create
154
229
  }
155
230
  let projectPath: string;
156
231
  try {
157
- projectPath = fs.realpathSync(projectCwd);
232
+ projectPath = fs.realpathSync.native(projectCwd);
158
233
  } catch (error) {
159
234
  if (!create && (error as NodeJS.ErrnoException).code === "ENOENT") return;
160
235
  throw error;
@@ -165,10 +240,14 @@ function assertScheduleRoot(root: string, projectCwd: string | undefined, create
165
240
  if (parent === existing) break;
166
241
  existing = parent;
167
242
  }
168
- if (!pathWithin(projectPath, fs.realpathSync(existing))) throw new Error(`Project schedule root '${root}' resolves outside the real project.`);
243
+ const existingPath = fs.realpathSync.native(existing);
244
+ const sharedGitConfigRoot = pathWithin(projectPath, existingPath) ? undefined : resolveSharedGitConfigRoot(projectCwd);
245
+ const isTrustedPath = (candidate: string): boolean => pathWithin(projectPath, candidate)
246
+ || (sharedGitConfigRoot !== undefined && pathWithin(sharedGitConfigRoot, candidate));
247
+ if (!isTrustedPath(existingPath)) throw new Error(`Project schedule root '${root}' resolves outside the real project.`);
169
248
  if (!create) return;
170
249
  fs.mkdirSync(root, { recursive: true, mode: 0o700 });
171
- if (!pathWithin(projectPath, fs.realpathSync(root))) throw new Error(`Project schedule root '${root}' resolves outside the real project.`);
250
+ if (!isTrustedPath(fs.realpathSync.native(root))) throw new Error(`Project schedule root '${root}' resolves outside the real project.`);
172
251
  }
173
252
 
174
253
  function scheduleDir(root: string, id: string, create = false, projectCwd?: string): string {
@@ -182,9 +261,9 @@ function scheduleDir(root: string, id: string, create = false, projectCwd?: stri
182
261
  if (!create) return dir;
183
262
  fs.mkdirSync(dir, { mode: 0o700 });
184
263
  }
185
- const rootPath = fs.realpathSync(root);
186
- const dirPath = fs.realpathSync(dir);
187
- if (dirPath !== path.join(rootPath, id)) throw new Error(`Schedule path '${dir}' escapes the project schedule root.`);
264
+ const rootPath = fs.realpathSync.native(root);
265
+ const dirPath = fs.realpathSync.native(dir);
266
+ if (!samePath(dirPath, path.join(rootPath, id))) throw new Error(`Schedule path '${dir}' escapes the project schedule root.`);
188
267
  return dir;
189
268
  }
190
269
 
@@ -8,6 +8,7 @@ import { DIRS, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSu
8
8
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
9
9
  import { normalizeParallelGroups } from "./parallel-groups.ts";
10
10
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent, type NestedRoute } from "../shared/nested-events.ts";
11
+ import { assertWorkflowGraphHostSteps } from "../shared/host-step-status.ts";
11
12
 
12
13
  export type PidLiveness = "alive" | "dead" | "unknown";
13
14
 
@@ -24,6 +25,7 @@ interface StartedRunMetadata {
24
25
  parallelGroups?: AsyncParallelGroupStatus[];
25
26
  startedAt?: number;
26
27
  sessionFile?: string;
28
+ sessionName?: string;
27
29
  }
28
30
 
29
31
  interface ReconcileAsyncRunOptions {
@@ -90,6 +92,7 @@ function appendJsonlBestEffort(filePath: string, payload: object): void {
90
92
 
91
93
  interface ResultChildOutcome {
92
94
  agent?: string;
95
+ sessionName?: string;
93
96
  success?: boolean;
94
97
  error?: string;
95
98
  sessionFile?: string;
@@ -101,7 +104,7 @@ interface ResultChildOutcome {
101
104
  }
102
105
 
103
106
  interface ResultRepairData {
104
- state: "complete" | "failed" | "paused" | "stopped" | "rejected";
107
+ state: "complete" | "failed" | "partial" | "paused" | "stopped" | "rejected";
105
108
  results?: ResultChildOutcome[];
106
109
  }
107
110
 
@@ -117,7 +120,7 @@ function regularFileExists(filePath: string): boolean {
117
120
  function readResultRepairData(resultPath: string): ResultRepairData | undefined {
118
121
  try {
119
122
  const data = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as { success?: boolean; state?: string; exitCode?: number; results?: unknown };
120
- const state = data.success ? "complete" : data.state === "stopped" ? "stopped" : data.state === "rejected" ? "rejected" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
123
+ const state = data.success ? "complete" : data.state === "stopped" ? "stopped" : data.state === "rejected" ? "rejected" : data.state === "partial" ? "partial" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
121
124
  const results = Array.isArray(data.results)
122
125
  ? data.results.map((entry, index) => {
123
126
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) return {};
@@ -157,8 +160,9 @@ function terminalStatusFromResult(status: AsyncStatus, resultPath: string, now:
157
160
  endedAt: step.endedAt ?? now,
158
161
  durationMs: step.startedAt !== undefined && step.durationMs === undefined ? Math.max(0, now - step.startedAt) : step.durationMs,
159
162
  exitCode: step.exitCode ?? (state === "complete" || state === "paused" ? 0 : 1),
160
- error: state === "failed" || state === "stopped" ? step.error ?? child?.error : step.error,
163
+ error: state === "failed" || state === "partial" || state === "stopped" ? step.error ?? child?.error : step.error,
161
164
  stopped: state === "stopped" ? true : step.stopped,
165
+ sessionName: step.sessionName ?? child?.sessionName,
162
166
  sessionFile: step.sessionFile ?? child?.sessionFile,
163
167
  model,
164
168
  thinking,
@@ -254,6 +258,7 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
254
258
  summary: message,
255
259
  results: repairedSteps.map((step) => ({
256
260
  agent: step.agent,
261
+ ...(step.sessionName ? { sessionName: step.sessionName } : {}),
257
262
  output: step.status === "complete" || step.status === "completed" ? "" : message,
258
263
  error: step.status === "complete" || step.status === "completed" ? undefined : step.error ?? message,
259
264
  success: step.status === "complete" || step.status === "completed",
@@ -290,7 +295,7 @@ function writeFailedRepair(asyncDir: string, status: AsyncStatus, resultPath: st
290
295
  }
291
296
 
292
297
  function terminal(state: AsyncStatus["state"]): boolean {
293
- return state === "complete" || state === "failed" || state === "paused" || state === "stopped" || state === "rejected";
298
+ return state === "complete" || state === "failed" || state === "partial" || state === "paused" || state === "stopped" || state === "rejected";
294
299
  }
295
300
 
296
301
  function* nestedRuns(children: NestedRunSummary[] | undefined): Generator<NestedRunSummary> {
@@ -353,6 +358,7 @@ export function reconcileAsyncRun(asyncDir: string, options: ReconcileAsyncRunOp
353
358
  const startedStatus = !status && options.startedRun ? buildStartedStatus(asyncDir, options.startedRun, now) : undefined;
354
359
  const effectiveStatus = status ?? startedStatus;
355
360
  if (!effectiveStatus) return { status: null, repaired: false };
361
+ assertWorkflowGraphHostSteps(effectiveStatus.workflowGraph, path.join(asyncDir, "status.json"), effectiveStatus.runId);
356
362
  const statusPath = path.join(asyncDir, "status.json");
357
363
  for (const [index, step] of (effectiveStatus.steps ?? []).entries()) {
358
364
  const stepRecord = step as Record<string, unknown>;
@@ -4,7 +4,6 @@ import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
4
4
  import type {
5
5
  AsyncStatus,
6
6
  ResolvedToolBudget,
7
- ResolvedTurnBudget,
8
7
  SteerActionResult,
9
8
  SteeringRecoveryDescriptor,
10
9
  SteeringRequestStatus,
@@ -195,26 +194,17 @@ export function readSteeringStatus(asyncDir: string): SteeringStatus | undefined
195
194
  }
196
195
 
197
196
  export function remainingSteeringRecoveryLimits(
198
- descriptor: Pick<SteeringRecoveryDescriptor, "absoluteDeadlineAt" | "initialTurnBudget" | "initialToolBudget">,
199
- status: Pick<AsyncStatus, "turnBudget" | "turnCount" | "toolBudget" | "toolCount">,
197
+ descriptor: Pick<SteeringRecoveryDescriptor, "absoluteDeadlineAt" | "initialToolBudget">,
198
+ status: Pick<AsyncStatus, "toolBudget" | "toolCount">,
200
199
  now = Date.now(),
201
- ): { timeoutMs?: number; absoluteDeadlineAt?: number; turnBudget?: ResolvedTurnBudget; toolBudget?: ResolvedToolBudget } {
202
- const limits: { timeoutMs?: number; absoluteDeadlineAt?: number; turnBudget?: ResolvedTurnBudget; toolBudget?: ResolvedToolBudget } = {};
200
+ ): { timeoutMs?: number; absoluteDeadlineAt?: number; toolBudget?: ResolvedToolBudget } {
201
+ const limits: { timeoutMs?: number; absoluteDeadlineAt?: number; toolBudget?: ResolvedToolBudget } = {};
203
202
  if (descriptor.absoluteDeadlineAt !== undefined) {
204
203
  const timeoutMs = descriptor.absoluteDeadlineAt - now;
205
204
  if (timeoutMs <= 0) throw new Error("Source run has no remaining deadline budget; it remains paused.");
206
205
  limits.timeoutMs = timeoutMs;
207
206
  limits.absoluteDeadlineAt = descriptor.absoluteDeadlineAt;
208
207
  }
209
- if (descriptor.initialTurnBudget) {
210
- const consumed = status.turnBudget?.turnCount ?? status.turnCount ?? 0;
211
- const totalRemaining = descriptor.initialTurnBudget.maxTurns + descriptor.initialTurnBudget.graceTurns - consumed;
212
- if (totalRemaining <= 0) throw new Error("Source run has no remaining turn budget; it remains paused.");
213
- const softRemaining = Math.max(0, descriptor.initialTurnBudget.maxTurns - consumed);
214
- limits.turnBudget = softRemaining > 0
215
- ? { maxTurns: softRemaining, graceTurns: totalRemaining - softRemaining }
216
- : { maxTurns: 1, graceTurns: totalRemaining - 1 };
217
- }
218
208
  if (descriptor.initialToolBudget) {
219
209
  const consumed = status.toolBudget?.toolCount ?? status.toolCount ?? 0;
220
210
  const hard = descriptor.initialToolBudget.hard - consumed;