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
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { DIRS, type AcceptanceInput, type AsyncStatus, type ResolvedTurnBudget, type SteeringRecoveryDescriptor } from "../../shared/types.ts";
3
+ import { DIRS, type AcceptanceInput, type AsyncStatus, type ResolvedTurnBudget, type SteeringRecoveryDescriptor, type SubagentRunMode } from "../../shared/types.ts";
4
4
  import type { AgentConfig } from "../../agents/agents.ts";
5
5
  import { validateAcceptanceInput } from "../shared/acceptance.ts";
6
6
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
@@ -8,7 +8,9 @@ import { intersectSubagentCapabilityCeilings, parseSubagentCapabilityCeiling, ty
8
8
  import { validateRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
9
9
  import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
10
10
  import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
11
- import { resultFilePath } from "./result-files.ts";
11
+ import { resultFilePath, resultPayloadPathForIndexedRun } from "./result-files.ts";
12
+ import { canScanAsyncRunPrefix, MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH } from "./run-id-query.ts";
13
+ import { parallelHandoffPath, resolveRetainedWorktreeCwd } from "../shared/parallel-handoff.ts";
12
14
 
13
15
  export interface AsyncResumeParams {
14
16
  id?: string;
@@ -34,6 +36,7 @@ export type AsyncResumeTarget = {
34
36
  runId: string;
35
37
  asyncDir?: string;
36
38
  state: AsyncStatus["state"];
39
+ mode?: SubagentRunMode;
37
40
  agent: string;
38
41
  index: number;
39
42
  cwd?: string;
@@ -173,6 +176,8 @@ function prefixedRunIds(dir: string, prefix: string, suffix = ""): string[] {
173
176
  }
174
177
 
175
178
  function exactResultPath(resultsDir: string, runId: string): string | null {
179
+ const indexed = resultPayloadPathForIndexedRun(resultsDir, runId);
180
+ if (indexed) return indexed;
176
181
  const resultPath = resultFilePath(resultsDir, runId);
177
182
  assertInsideRoot(resultsDir, resultPath, "Async result file");
178
183
  return fs.existsSync(resultPath) ? resultPath : null;
@@ -181,6 +186,7 @@ function exactResultPath(resultsDir: string, runId: string): string | null {
181
186
  export function findAsyncRunPrefixMatches(prefix: string, asyncDirRoot: string, resultsDir: string): Array<{ id: string; location: AsyncRunLocation }> {
182
187
  const requestedId = assertRunId(prefix, "id");
183
188
  if (!requestedId) return [];
189
+ if (!canScanAsyncRunPrefix(requestedId)) return [];
184
190
  const asyncRoot = path.resolve(asyncDirRoot);
185
191
  const resultRoot = path.resolve(resultsDir);
186
192
  const matchingIds = prefixedRunIds(asyncRoot, requestedId).sort();
@@ -223,6 +229,10 @@ export function resolveAsyncRunLocation(params: AsyncResumeParams, asyncDirRoot:
223
229
  resolvedId: requestedId,
224
230
  };
225
231
  }
232
+ if (requestedId.length < MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH) {
233
+ throw new Error(`Async run id prefix '${requestedId}' is too short. Provide at least ${MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH} characters.`);
234
+ }
235
+ if (!canScanAsyncRunPrefix(requestedId)) return { asyncDir: null, resultPath: null, resolvedId: requestedId };
226
236
 
227
237
  const matching = findAsyncRunPrefixMatches(requestedId, asyncRoot, resultRoot);
228
238
  if (matching.length === 0) return { asyncDir: null, resultPath: null, resolvedId: requestedId };
@@ -261,12 +271,7 @@ function validateStatusForResume(status: AsyncStatus | null, source: string): vo
261
271
  }
262
272
  }
263
273
 
264
- function normalizeRecoveryAcceptance(value: unknown, descriptorPath: string): AcceptanceInput | undefined {
265
- if (value && typeof value === "object" && !Array.isArray(value) && ("explicit" in value || "inferredReason" in value)) {
266
- const { explicit, inferredReason: _inferredReason, ...publicAcceptance } = value as Record<string, unknown>;
267
- if (explicit === false) return undefined;
268
- value = publicAcceptance;
269
- }
274
+ function normalizeRecoveryAcceptance(value: unknown, descriptorPath: string): AcceptanceInput {
270
275
  const errors = validateAcceptanceInput(value, "recoveryDescriptor.acceptance");
271
276
  if (errors.length) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${errors.join(" ")}`);
272
277
  return value as AcceptanceInput;
@@ -289,6 +294,17 @@ function normalizeRecoveryTurnBudget(value: unknown, descriptorPath: string): Re
289
294
  return result.turnBudget;
290
295
  }
291
296
 
297
+ export function asyncReviveRequiresRecoveryDescriptor(target: Pick<AsyncResumeTarget, "recoveryDescriptor" | "mode" | "sessionFile">): boolean {
298
+ if (target.recoveryDescriptor) return false;
299
+ return !(target.mode === "workflow" && Boolean(target.sessionFile));
300
+ }
301
+
302
+ function resumeTargetMode(status: AsyncStatus | null, result: AsyncResultFile | undefined): SubagentRunMode | undefined {
303
+ if (status?.mode) return status.mode;
304
+ if (result?.mode === "single" || result?.mode === "parallel" || result?.mode === "chain" || result?.mode === "workflow") return result.mode;
305
+ return undefined;
306
+ }
307
+
292
308
  export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): SteeringRecoveryDescriptor | undefined {
293
309
  if (!asyncDir) return undefined;
294
310
  const descriptorPath = path.join(asyncDir, "recovery-descriptor.json");
@@ -302,7 +318,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
302
318
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
303
319
  const parsed = value as Record<string, unknown>;
304
320
  const allowedFields = new Set([
305
- "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "fallbackModels", "thinking", "tools", "extensions",
321
+ "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelOverrideFromParent", "fallbackModels", "thinking", "tools", "extensions",
306
322
  "subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
307
323
  "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
308
324
  "artifactsDir", "maxOutput", "controlConfig", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
@@ -329,6 +345,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
329
345
  }
330
346
  if (parsed.systemPromptMode !== "append" && parsed.systemPromptMode !== "replace") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': systemPromptMode is invalid.`);
331
347
  if (parsed.outputMode !== "inline" && parsed.outputMode !== "file-only") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': outputMode is invalid.`);
348
+ if (parsed.modelOverrideFromParent !== undefined && typeof parsed.modelOverrideFromParent !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': modelOverrideFromParent must be a boolean.`);
332
349
  for (const field of ["inheritProjectContext", "inheritSkills", "share"] as const) {
333
350
  if (typeof parsed[field] !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a boolean.`);
334
351
  }
@@ -395,11 +412,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
395
412
  if (!Array.isArray(control.notifyOn) || control.notifyOn.some((item) => item !== "active_long_running" && item !== "needs_attention")) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.notifyOn is invalid.`);
396
413
  if (!Array.isArray(control.notifyChannels) || control.notifyChannels.some((item) => item !== "event" && item !== "async" && item !== "intercom")) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.notifyChannels is invalid.`);
397
414
  }
398
- if (parsed.acceptance !== undefined) {
399
- const acceptance = normalizeRecoveryAcceptance(parsed.acceptance, descriptorPath);
400
- if (acceptance === undefined) delete parsed.acceptance;
401
- else parsed.acceptance = acceptance;
402
- }
415
+ if (parsed.acceptance !== undefined) parsed.acceptance = normalizeRecoveryAcceptance(parsed.acceptance, descriptorPath);
403
416
  return parsed as unknown as SteeringRecoveryDescriptor;
404
417
  }
405
418
 
@@ -410,6 +423,17 @@ function validateResumeSessionFile(runId: string, sessionFile: string): string {
410
423
  return resolved;
411
424
  }
412
425
 
426
+ function validateResumeCwd(runId: string, cwd: string | undefined): string | undefined {
427
+ if (!cwd) return undefined;
428
+ const resolved = path.resolve(cwd);
429
+ try {
430
+ if (!fs.statSync(resolved).isDirectory()) throw new Error("path is not a directory");
431
+ } catch (error) {
432
+ throw new Error(`Async run '${runId}' required cwd does not exist: ${cwd}`, { cause: error instanceof Error ? error : undefined });
433
+ }
434
+ return resolved;
435
+ }
436
+
413
437
  export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncResumeDeps = {}, options: AsyncResumeOptions = {}): AsyncResumeTarget {
414
438
  const asyncDirRoot = deps.asyncDirRoot ?? DIRS.async;
415
439
  const resultsDir = deps.resultsDir ?? DIRS.results;
@@ -427,6 +451,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
427
451
  const recoveryDescriptor = readAsyncRecoveryDescriptor(location.asyncDir ?? undefined);
428
452
  const result = location.resultPath ? readResultFile(location.resultPath) : undefined;
429
453
  const runId = status?.runId ?? result?.runId ?? result?.id ?? location.resolvedId ?? (location.asyncDir ? path.basename(location.asyncDir) : "unknown");
454
+ const mode = resumeTargetMode(status, result);
430
455
  if (options.sessionId && ((status && status.sessionId !== options.sessionId) || (result && result.sessionId !== options.sessionId))) {
431
456
  throw new Error(`Async run '${runId}' was not found in the active session.`);
432
457
  }
@@ -453,6 +478,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
453
478
  runId,
454
479
  asyncDir: location.asyncDir ?? undefined,
455
480
  state,
481
+ ...(mode ? { mode } : {}),
456
482
  agent: selectedStep.agent,
457
483
  index: requestedIndex,
458
484
  cwd: status?.cwd ?? result?.cwd,
@@ -480,6 +506,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
480
506
  runId,
481
507
  asyncDir: location.asyncDir ?? undefined,
482
508
  state,
509
+ ...(mode ? { mode } : {}),
483
510
  agent: selected.step.agent,
484
511
  index: selected.index,
485
512
  cwd: status?.cwd ?? result?.cwd,
@@ -510,15 +537,20 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
510
537
  const stepModel = statusSteps[index]?.model ?? resultSteps[index]?.model ?? (stepCount === 1 ? result?.model : undefined);
511
538
  const stepThinking = statusSteps[index]?.thinking ?? resultSteps[index]?.thinking ?? (stepCount === 1 ? result?.thinking : undefined);
512
539
  const capabilityCeiling = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, statusSteps[index]?.capabilityCeiling, result?.capabilityCeiling, resultSteps[index]?.capabilityCeiling);
540
+ const managedWorktreeCwd = location.asyncDir
541
+ ? resolveRetainedWorktreeCwd(parallelHandoffPath(location.asyncDir), runId, index)
542
+ : undefined;
543
+ const resumeCwd = validateResumeCwd(runId, managedWorktreeCwd ?? status?.cwd ?? result?.cwd ?? recoveryDescriptor?.cwd);
513
544
 
514
545
  return {
515
546
  kind: "revive",
516
547
  runId,
517
548
  asyncDir: location.asyncDir ?? undefined,
518
549
  state,
550
+ ...(mode ? { mode } : {}),
519
551
  agent,
520
552
  index,
521
- cwd: status?.cwd ?? result?.cwd,
553
+ ...(resumeCwd ? { cwd: resumeCwd } : {}),
522
554
  ...(resolvedSessionFile ? { sessionFile: resolvedSessionFile } : {}),
523
555
  ...(stepModel ? { model: stepModel } : {}),
524
556
  ...(stepThinking ? { thinking: stepThinking } : {}),