pi-subagents 0.49.0 → 0.51.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 (117) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/agents/gpt-pro.md +17 -0
  3. package/agents/oracle.md +7 -5
  4. package/agents/researcher.md +1 -1
  5. package/agents/reviewer.md +2 -2
  6. package/agents/scout.md +1 -1
  7. package/agents/worker.md +1 -1
  8. package/async-retention-discovery-worker.mjs +180 -0
  9. package/docs/agents.md +37 -2
  10. package/docs/configuration.md +76 -14
  11. package/docs/extension-api.md +78 -1
  12. package/docs/missions.md +1 -1
  13. package/docs/observability.md +20 -4
  14. package/docs/tool-reference.md +55 -39
  15. package/docs/workflows.md +171 -5
  16. package/package.json +4 -2
  17. package/skills/pi-subagents/SKILL.md +5 -4
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +9 -6
  19. package/skills/pi-subagents/references/execution-controls.md +22 -18
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  21. package/skills/pi-subagents/references/prompting-and-roles.md +33 -17
  22. package/src/agents/agent-management.ts +100 -345
  23. package/src/agents/agent-serializer.ts +2 -0
  24. package/src/agents/agents.ts +135 -25
  25. package/src/api/external-job-provider.ts +185 -0
  26. package/src/api/external-runs.ts +174 -84
  27. package/src/api/preflight.ts +42 -11
  28. package/src/api/shared-types.ts +2 -0
  29. package/src/extension/config.ts +36 -3
  30. package/src/extension/doctor.ts +3 -6
  31. package/src/extension/fanout-child.ts +2 -2
  32. package/src/extension/index.ts +210 -90
  33. package/src/extension/public-execution.ts +31 -2
  34. package/src/extension/rpc.ts +5 -1
  35. package/src/extension/schemas.ts +14 -36
  36. package/src/extension/tool-description.ts +37 -24
  37. package/src/inspectors/herdr/actions.ts +5 -9
  38. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  39. package/src/inspectors/herdr/project-panes.ts +4 -8
  40. package/src/inspectors/herdr/shell-command.ts +16 -0
  41. package/src/intercom/intercom-bridge.ts +2 -3
  42. package/src/intercom/native-supervisor-channel.ts +49 -51
  43. package/src/missions/goal-driver.ts +3 -1
  44. package/src/missions/lifecycle.ts +6 -1
  45. package/src/missions/store.ts +4 -9
  46. package/src/profiles/profiles.ts +3 -1
  47. package/src/runs/background/active-run-index.ts +94 -1
  48. package/src/runs/background/async-execution.ts +98 -38
  49. package/src/runs/background/async-job-tracker.ts +21 -4
  50. package/src/runs/background/async-resume.ts +30 -17
  51. package/src/runs/background/async-retention.ts +888 -0
  52. package/src/runs/background/async-status-snapshot.ts +277 -0
  53. package/src/runs/background/async-status.ts +47 -56
  54. package/src/runs/background/chain-append.ts +3 -33
  55. package/src/runs/background/chain-root-attachment.ts +2 -2
  56. package/src/runs/background/completion-replay.ts +11 -1
  57. package/src/runs/background/control-channel.ts +14 -68
  58. package/src/runs/background/fleet-view.ts +3 -1
  59. package/src/runs/background/index-segment.ts +59 -0
  60. package/src/runs/background/notify.ts +3 -1
  61. package/src/runs/background/result-files.ts +505 -0
  62. package/src/runs/background/result-watcher.ts +250 -51
  63. package/src/runs/background/retained-children.ts +79 -20
  64. package/src/runs/background/run-id-query.ts +7 -0
  65. package/src/runs/background/run-id-resolver.ts +37 -29
  66. package/src/runs/background/run-status.ts +34 -20
  67. package/src/runs/background/scheduled-runs.ts +71 -27
  68. package/src/runs/background/stale-run-reconciler.ts +33 -16
  69. package/src/runs/background/steering.ts +11 -1
  70. package/src/runs/background/subagent-runner.ts +535 -161
  71. package/src/runs/background/subagent-wait.ts +9 -7
  72. package/src/runs/background/terminal-run-index.ts +129 -0
  73. package/src/runs/background/wait-completions.ts +22 -2
  74. package/src/runs/background/wait-subscriptions.ts +80 -1
  75. package/src/runs/foreground/async-dismiss-action.ts +2 -1
  76. package/src/runs/foreground/async-steering-action.ts +21 -14
  77. package/src/runs/foreground/execution.ts +221 -15
  78. package/src/runs/foreground/subagent-executor.ts +529 -1519
  79. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  80. package/src/runs/shared/chain-outputs.ts +1 -3
  81. package/src/runs/shared/completion-guard.ts +96 -6
  82. package/src/runs/shared/external-cli-runner.ts +4 -0
  83. package/src/runs/shared/external-job-bridge.ts +450 -0
  84. package/src/runs/shared/external-job-runner.ts +286 -0
  85. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  86. package/src/runs/shared/model-fallback.ts +34 -3
  87. package/src/runs/shared/nested-events.ts +66 -62
  88. package/src/runs/shared/orca-progress-tabs.ts +437 -0
  89. package/src/runs/shared/parallel-handoff.ts +46 -4
  90. package/src/runs/shared/parallel-utils.ts +6 -15
  91. package/src/runs/shared/permissions.ts +5 -1
  92. package/src/runs/shared/pi-args.ts +8 -1
  93. package/src/runs/shared/subagent-control.ts +41 -4
  94. package/src/runs/shared/subagent-prompt-runtime.ts +13 -15
  95. package/src/runs/shared/subagent-startup-retry.ts +12 -0
  96. package/src/runs/shared/tool-timeout.ts +93 -0
  97. package/src/runs/shared/workflow-graph.ts +1 -23
  98. package/src/runs/shared/worktree.ts +12 -1
  99. package/src/shared/atomic-json.ts +22 -2
  100. package/src/shared/capacity-resilient-json.ts +102 -0
  101. package/src/shared/completion-owner.ts +14 -0
  102. package/src/shared/file-system-retry.ts +49 -1
  103. package/src/shared/fork-context.ts +42 -0
  104. package/src/shared/prompt-resources.ts +0 -40
  105. package/src/shared/settings.ts +3 -27
  106. package/src/shared/types.ts +88 -27
  107. package/src/shared/utils.ts +8 -0
  108. package/src/shared/watch-strategy.ts +10 -0
  109. package/src/slash/slash-commands.ts +45 -28
  110. package/src/slash/slash-live-state.ts +3 -0
  111. package/src/tui/fleet-status.ts +160 -45
  112. package/src/tui/fleet.ts +186 -28
  113. package/src/tui/render.ts +41 -10
  114. package/src/workflows/chat-progress.ts +7 -5
  115. package/src/workflows/scripted-workflow.ts +424 -125
  116. package/src/runs/foreground/chain-clarify.ts +0 -1354
  117. package/src/runs/foreground/chain-execution.ts +0 -1565
@@ -20,6 +20,8 @@ import { normalizeParallelGroups } from "./parallel-groups.ts";
20
20
  import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
21
21
  import { findNestedRouteForRootId, hasLiveNestedDescendants, updateAsyncJobNestedProjection } from "../shared/nested-events.ts";
22
22
  import { listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
23
+ import { EXTERNAL_JOB_BRIDGE_REQUEST_DIR, serviceExternalJobBridgeRequests } from "../shared/external-job-bridge.ts";
24
+ import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
23
25
 
24
26
  interface AsyncJobTrackerOptions {
25
27
  completionRetentionMs?: number;
@@ -27,6 +29,9 @@ interface AsyncJobTrackerOptions {
27
29
  pollIntervalMs?: number;
28
30
  resultsDir?: string;
29
31
  widgetEnabled?: boolean;
32
+ platform?: NodeJS.Platform;
33
+ onJobTerminal?: () => void;
34
+ watch?: typeof fs.watch;
30
35
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
31
36
  now?: () => number;
32
37
  }
@@ -66,6 +71,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
66
71
  const runningJobIds = new Set<string>();
67
72
  let rootWatcher: fs.FSWatcher | undefined;
68
73
  let nextLivenessAt = Date.now() + livenessIntervalMs;
74
+ const watch = options.watch ?? fs.watch;
75
+ const useNativeWatcher = () => shouldUseNativeFsWatch("async-job-tracker", options.platform);
76
+ const terminalStatus = (status: string) => status === "complete" || status === "failed" || status === "paused" || status === "stopped";
69
77
  const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
70
78
  if (state.widgetsSuspended) return;
71
79
  renderWidget(ctx, options.widgetEnabled === false ? [] : jobs);
@@ -125,6 +133,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
125
133
  return {
126
134
  asyncId: run.id,
127
135
  asyncDir: run.asyncDir,
136
+ toolCallId: run.toolCallId,
128
137
  status: run.state,
129
138
  sessionId: run.sessionId,
130
139
  activityState: run.activityState,
@@ -138,6 +147,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
138
147
  mode: run.mode,
139
148
  context: run.context,
140
149
  cwd: run.cwd,
150
+ sessionRoot: run.sessionRoot,
141
151
  agents: visibleSteps.map((step) => step.agent),
142
152
  currentStep: run.currentStep,
143
153
  chainStepCount: run.chainStepCount,
@@ -326,6 +336,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
326
336
  };
327
337
  try {
328
338
  emitNewControlEvents(job);
339
+ serviceExternalJobBridgeRequests(job.asyncDir);
329
340
  try {
330
341
  if (job.nestedRoute) reconcileNestedAsyncDescendants(job.nestedRoute, { resultsDir, kill: options.kill, now: options.now });
331
342
  } catch (error) {
@@ -341,6 +352,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
341
352
  runId: job.asyncId,
342
353
  pid: job.pid,
343
354
  sessionId: job.sessionId,
355
+ completionOwnerId: job.completionOwnerId,
344
356
  mode: job.mode,
345
357
  agents: job.agents,
346
358
  chainStepCount: job.chainStepCount,
@@ -403,7 +415,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
403
415
  job.turnBudgetExceeded = status.turnBudgetExceeded ?? job.turnBudgetExceeded;
404
416
  job.wrapUpRequested = status.wrapUpRequested ?? job.wrapUpRequested;
405
417
  job.sessionFile = status.sessionFile ?? job.sessionFile;
406
- if (job.status === "complete" || job.status === "failed" || job.status === "paused" || job.status === "stopped") {
418
+ if (terminalStatus(job.status)) {
419
+ if (!terminalStatus(previousStatus)) options.onJobTerminal?.();
407
420
  rememberFleetJob(state, job);
408
421
  if (!nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
409
422
  scheduleCleanup(job.asyncId);
@@ -441,6 +454,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
441
454
  };
442
455
 
443
456
  const watchJob = (job: AsyncJobState) => {
457
+ if (!useNativeWatcher()) return;
444
458
  const watched = jobWatchers.get(job.asyncId) ?? { watchers: new Map<string, fs.FSWatcher>() };
445
459
  const active = job.status === "queued" || job.status === "running";
446
460
  const watchPath = (watchPath: string): boolean => {
@@ -448,11 +462,11 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
448
462
  const nestedEventSink = job.nestedRoute?.eventSink === watchPath;
449
463
  let watcher: fs.FSWatcher;
450
464
  try {
451
- watcher = fs.watch(resolveWatchPath(watchPath), (_event, file) => {
465
+ watcher = watch(resolveWatchPath(watchPath), (_event, file) => {
452
466
  const rawFileName = file?.toString();
453
467
  const fileName = rawFileName ?? path.basename(watchPath);
454
468
  const nestedEventFile = nestedEventSink && (!rawFileName || rawFileName.endsWith(".json") || rawFileName.endsWith(".jsonl"));
455
- if (fileName === "status.json" || fileName === "events.jsonl" || nestedEventFile) {
469
+ if (fileName === "status.json" || fileName === "events.jsonl" || fileName === EXTERNAL_JOB_BRIDGE_REQUEST_DIR || fileName.endsWith(".json") || nestedEventFile) {
456
470
  scheduleJobRefresh(job.asyncId);
457
471
  }
458
472
  if (watchPath !== job.asyncDir && !nestedEventSink) {
@@ -479,6 +493,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
479
493
  const statusWatched = watchPath(statusPath);
480
494
  if (statusWatched && !hadStatusWatcher) scheduleJobRefresh(job.asyncId);
481
495
  watchPath(path.join(job.asyncDir, "events.jsonl"));
496
+ watchPath(path.join(job.asyncDir, EXTERNAL_JOB_BRIDGE_REQUEST_DIR));
482
497
  if (job.nestedRoute) watchPath(job.nestedRoute.eventSink);
483
498
  if (!statusWatched && active && !watched.retryTimer) {
484
499
  watched.retryTimer = setTimeout(() => {
@@ -496,9 +511,10 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
496
511
  };
497
512
 
498
513
  const watchAsyncRoot = () => {
514
+ if (!useNativeWatcher()) return;
499
515
  if (rootWatcher) return;
500
516
  try {
501
- rootWatcher = fs.watch(resolveWatchPath(asyncDirRoot), (_event, file) => {
517
+ rootWatcher = watch(resolveWatchPath(asyncDirRoot), (_event, file) => {
502
518
  const runDirName = file?.toString();
503
519
  for (const job of state.asyncJobs.values()) {
504
520
  if (jobWatchers.has(job.asyncId) || (runDirName && path.basename(job.asyncDir) !== runDirName)) continue;
@@ -567,6 +583,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
567
583
  status: "queued",
568
584
  pid: typeof info.pid === "number" ? info.pid : undefined,
569
585
  ...(typeof info.sessionId === "string" ? { sessionId: info.sessionId } : {}),
586
+ ...(typeof info.completionOwnerId === "string" ? { completionOwnerId: info.completionOwnerId } : {}),
570
587
  mode: info.mode ?? (info.chain ? "chain" : "single"),
571
588
  description: info.goal ?? info.task,
572
589
  agents,
@@ -8,6 +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, 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";
11
14
 
12
15
  export interface AsyncResumeParams {
13
16
  id?: string;
@@ -172,7 +175,9 @@ function prefixedRunIds(dir: string, prefix: string, suffix = ""): string[] {
172
175
  }
173
176
 
174
177
  function exactResultPath(resultsDir: string, runId: string): string | null {
175
- const resultPath = path.join(resultsDir, `${runId}.json`);
178
+ const indexed = resultPayloadPathForIndexedRun(resultsDir, runId);
179
+ if (indexed) return indexed;
180
+ const resultPath = resultFilePath(resultsDir, runId);
176
181
  assertInsideRoot(resultsDir, resultPath, "Async result file");
177
182
  return fs.existsSync(resultPath) ? resultPath : null;
178
183
  }
@@ -180,12 +185,10 @@ function exactResultPath(resultsDir: string, runId: string): string | null {
180
185
  export function findAsyncRunPrefixMatches(prefix: string, asyncDirRoot: string, resultsDir: string): Array<{ id: string; location: AsyncRunLocation }> {
181
186
  const requestedId = assertRunId(prefix, "id");
182
187
  if (!requestedId) return [];
188
+ if (!canScanAsyncRunPrefix(requestedId)) return [];
183
189
  const asyncRoot = path.resolve(asyncDirRoot);
184
190
  const resultRoot = path.resolve(resultsDir);
185
- const matchingIds = [...new Set([
186
- ...prefixedRunIds(asyncRoot, requestedId),
187
- ...prefixedRunIds(resultRoot, requestedId, ".json"),
188
- ])].sort();
191
+ const matchingIds = prefixedRunIds(asyncRoot, requestedId).sort();
189
192
  return matchingIds.map((id) => {
190
193
  const asyncDir = path.join(asyncRoot, id);
191
194
  assertInsideRoot(asyncRoot, asyncDir, "Async run directory");
@@ -225,6 +228,10 @@ export function resolveAsyncRunLocation(params: AsyncResumeParams, asyncDirRoot:
225
228
  resolvedId: requestedId,
226
229
  };
227
230
  }
231
+ if (requestedId.length < MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH) {
232
+ throw new Error(`Async run id prefix '${requestedId}' is too short. Provide at least ${MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH} characters.`);
233
+ }
234
+ if (!canScanAsyncRunPrefix(requestedId)) return { asyncDir: null, resultPath: null, resolvedId: requestedId };
228
235
 
229
236
  const matching = findAsyncRunPrefixMatches(requestedId, asyncRoot, resultRoot);
230
237
  if (matching.length === 0) return { asyncDir: null, resultPath: null, resolvedId: requestedId };
@@ -263,12 +270,7 @@ function validateStatusForResume(status: AsyncStatus | null, source: string): vo
263
270
  }
264
271
  }
265
272
 
266
- function normalizeRecoveryAcceptance(value: unknown, descriptorPath: string): AcceptanceInput | undefined {
267
- if (value && typeof value === "object" && !Array.isArray(value) && ("explicit" in value || "inferredReason" in value)) {
268
- const { explicit, inferredReason: _inferredReason, ...publicAcceptance } = value as Record<string, unknown>;
269
- if (explicit === false) return undefined;
270
- value = publicAcceptance;
271
- }
273
+ function normalizeRecoveryAcceptance(value: unknown, descriptorPath: string): AcceptanceInput {
272
274
  const errors = validateAcceptanceInput(value, "recoveryDescriptor.acceptance");
273
275
  if (errors.length) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${errors.join(" ")}`);
274
276
  return value as AcceptanceInput;
@@ -397,11 +399,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
397
399
  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.`);
398
400
  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.`);
399
401
  }
400
- if (parsed.acceptance !== undefined) {
401
- const acceptance = normalizeRecoveryAcceptance(parsed.acceptance, descriptorPath);
402
- if (acceptance === undefined) delete parsed.acceptance;
403
- else parsed.acceptance = acceptance;
404
- }
402
+ if (parsed.acceptance !== undefined) parsed.acceptance = normalizeRecoveryAcceptance(parsed.acceptance, descriptorPath);
405
403
  return parsed as unknown as SteeringRecoveryDescriptor;
406
404
  }
407
405
 
@@ -412,6 +410,17 @@ function validateResumeSessionFile(runId: string, sessionFile: string): string {
412
410
  return resolved;
413
411
  }
414
412
 
413
+ function validateResumeCwd(runId: string, cwd: string | undefined): string | undefined {
414
+ if (!cwd) return undefined;
415
+ const resolved = path.resolve(cwd);
416
+ try {
417
+ if (!fs.statSync(resolved).isDirectory()) throw new Error("path is not a directory");
418
+ } catch (error) {
419
+ throw new Error(`Async run '${runId}' required cwd does not exist: ${cwd}`, { cause: error instanceof Error ? error : undefined });
420
+ }
421
+ return resolved;
422
+ }
423
+
415
424
  export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncResumeDeps = {}, options: AsyncResumeOptions = {}): AsyncResumeTarget {
416
425
  const asyncDirRoot = deps.asyncDirRoot ?? DIRS.async;
417
426
  const resultsDir = deps.resultsDir ?? DIRS.results;
@@ -512,6 +521,10 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
512
521
  const stepModel = statusSteps[index]?.model ?? resultSteps[index]?.model ?? (stepCount === 1 ? result?.model : undefined);
513
522
  const stepThinking = statusSteps[index]?.thinking ?? resultSteps[index]?.thinking ?? (stepCount === 1 ? result?.thinking : undefined);
514
523
  const capabilityCeiling = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, statusSteps[index]?.capabilityCeiling, result?.capabilityCeiling, resultSteps[index]?.capabilityCeiling);
524
+ const managedWorktreeCwd = location.asyncDir
525
+ ? resolveRetainedWorktreeCwd(parallelHandoffPath(location.asyncDir), runId, index)
526
+ : undefined;
527
+ const resumeCwd = validateResumeCwd(runId, managedWorktreeCwd ?? status?.cwd ?? result?.cwd ?? recoveryDescriptor?.cwd);
515
528
 
516
529
  return {
517
530
  kind: "revive",
@@ -520,7 +533,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
520
533
  state,
521
534
  agent,
522
535
  index,
523
- cwd: status?.cwd ?? result?.cwd,
536
+ ...(resumeCwd ? { cwd: resumeCwd } : {}),
524
537
  ...(resolvedSessionFile ? { sessionFile: resolvedSessionFile } : {}),
525
538
  ...(stepModel ? { model: stepModel } : {}),
526
539
  ...(stepThinking ? { thinking: stepThinking } : {}),