pi-subagents 0.47.0 → 0.48.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 (57) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +2 -0
  3. package/agents/reviewer.md +3 -4
  4. package/docs/configuration.md +46 -2
  5. package/docs/observability.md +1 -1
  6. package/docs/tool-reference.md +1 -1
  7. package/package.json +1 -1
  8. package/src/agents/agents.ts +9 -3
  9. package/src/extension/config.ts +16 -0
  10. package/src/extension/doctor.ts +40 -0
  11. package/src/extension/index.ts +30 -11
  12. package/src/extension/public-execution.ts +5 -0
  13. package/src/extension/rpc.ts +7 -9
  14. package/src/extension/schemas.ts +4 -4
  15. package/src/intercom/intercom-bridge.ts +4 -1
  16. package/src/intercom/native-supervisor-channel.ts +102 -4
  17. package/src/missions/lifecycle.ts +4 -7
  18. package/src/missions/store.ts +4 -4
  19. package/src/missions/workflow-state.ts +6 -2
  20. package/src/runs/background/active-async-capacity.ts +374 -0
  21. package/src/runs/background/active-run-index.ts +46 -0
  22. package/src/runs/background/async-execution.ts +117 -29
  23. package/src/runs/background/async-job-tracker.ts +279 -134
  24. package/src/runs/background/async-resume.ts +7 -1
  25. package/src/runs/background/async-status.ts +33 -4
  26. package/src/runs/background/chain-append.ts +33 -15
  27. package/src/runs/background/control-channel.ts +55 -17
  28. package/src/runs/background/owned-process-tree.ts +104 -0
  29. package/src/runs/background/process-terminal.ts +17 -3
  30. package/src/runs/background/result-watcher.ts +87 -6
  31. package/src/runs/background/run-status.ts +23 -1
  32. package/src/runs/background/scheduled-runs.ts +23 -0
  33. package/src/runs/background/stale-run-reconciler.ts +10 -3
  34. package/src/runs/background/subagent-runner.ts +75 -33
  35. package/src/runs/foreground/async-dismiss-action.ts +85 -0
  36. package/src/runs/foreground/async-steering-action.ts +6 -7
  37. package/src/runs/foreground/chain-execution.ts +37 -2
  38. package/src/runs/foreground/execution.ts +90 -19
  39. package/src/runs/foreground/foreground-control.ts +12 -0
  40. package/src/runs/foreground/prompt-audit.ts +171 -0
  41. package/src/runs/foreground/subagent-executor.ts +648 -195
  42. package/src/runs/shared/acceptance.ts +13 -4
  43. package/src/runs/shared/llm-intent-arbiter.ts +286 -0
  44. package/src/runs/shared/parallel-utils.ts +2 -0
  45. package/src/runs/shared/pi-args.ts +44 -1
  46. package/src/runs/shared/run-fanout-budget.ts +280 -0
  47. package/src/runs/shared/single-output.ts +4 -2
  48. package/src/runs/shared/subagent-prompt-runtime.ts +24 -5
  49. package/src/runs/shared/task-intent.ts +19 -3
  50. package/src/runs/shared/worktree.ts +17 -5
  51. package/src/shared/artifacts.ts +1 -1
  52. package/src/shared/file-coalescer.ts +9 -0
  53. package/src/shared/types.ts +97 -1
  54. package/src/shared/utils.ts +3 -1
  55. package/src/tui/fleet-status.ts +7 -5
  56. package/src/tui/fleet.ts +225 -12
  57. package/src/workflows/scripted-workflow.ts +26 -6
@@ -328,6 +328,7 @@ export interface FileMutationEffect {
328
328
  expected: boolean;
329
329
  attempted: boolean;
330
330
  message?: string;
331
+ resolvedBy?: "llm-intent-arbiter";
331
332
  }
332
333
 
333
334
  export interface EffectsProjection {
@@ -343,6 +344,7 @@ export type ProcessTerminalReason =
343
344
  | "runner-candidate-missing"
344
345
  | "runner-instance-mismatch"
345
346
  | "writer-close-unverified"
347
+ | "process-tree-unverified"
346
348
  | "canonical-session-unavailable"
347
349
  | "canonical-session-lease-active"
348
350
  | "canonical-session-release-unverified"
@@ -357,6 +359,19 @@ export interface RunnerProcessInstanceExitV1 {
357
359
  signal: string | null;
358
360
  }
359
361
 
362
+ export type ProcessTreeTerminalV1 =
363
+ | {
364
+ state: "observed";
365
+ mechanism: "posix-process-group";
366
+ processGroupId: number;
367
+ verifiedAt: number;
368
+ }
369
+ | {
370
+ state: "unknown";
371
+ reason: "unsupported-platform" | "signal-failed" | "verification-failed";
372
+ diagnostic?: string;
373
+ };
374
+
360
375
  export interface PiWriterProcessInstanceExitV1 {
361
376
  processInstanceId: string;
362
377
  kind: "pi-writer";
@@ -364,6 +379,7 @@ export interface PiWriterProcessInstanceExitV1 {
364
379
  closeObservedAt: number;
365
380
  exitCode: number | null;
366
381
  signal: string | null;
382
+ processTree: ProcessTreeTerminalV1;
367
383
  }
368
384
 
369
385
  export type ProcessInstanceExitV1 = RunnerProcessInstanceExitV1 | PiWriterProcessInstanceExitV1;
@@ -462,9 +478,30 @@ export interface SteeringNotice {
462
478
  currentSessionId?: string;
463
479
  }
464
480
 
481
+ export interface RunFanoutBudgetDescriptor {
482
+ version: 1;
483
+ rootRunId: string;
484
+ directory: string;
485
+ limit: number;
486
+ parentPath?: string;
487
+ }
488
+
489
+ export interface RunFanoutBudgetSnapshot {
490
+ used: number;
491
+ limit: number;
492
+ remaining: number;
493
+ }
494
+
495
+ export interface RunFanoutRejection extends RunFanoutBudgetSnapshot {
496
+ code: "RUN_FANOUT_LIMIT";
497
+ path: string;
498
+ requested: number;
499
+ }
500
+
465
501
  export interface SteeringRecoveryDescriptor {
466
502
  version: 1;
467
503
  launchContractDigest?: string;
504
+ runFanoutBudget: RunFanoutBudgetDescriptor;
468
505
  sourceRunId: string;
469
506
  agentContract?: AgentContract;
470
507
  agent: string;
@@ -1024,6 +1061,9 @@ export interface Details {
1024
1061
  // Aggregated cost across all agents in the run
1025
1062
  totalCost?: CostSummary;
1026
1063
  spawnBudget?: SpawnBudgetSnapshot;
1064
+ runFanoutBudget?: RunFanoutBudgetSnapshot;
1065
+ runFanoutRejection?: RunFanoutRejection;
1066
+ activeAsyncCapacity?: ActiveAsyncCapacitySnapshot;
1027
1067
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
1028
1068
  capabilityAudit?: SubagentCapabilityAudit;
1029
1069
  parallelHandoff?: ParallelHandoffReference;
@@ -1290,6 +1330,8 @@ export interface AsyncStatus {
1290
1330
  context?: "fresh" | "fork" | "mixed";
1291
1331
  isNested?: boolean;
1292
1332
  state: "queued" | "running" | "complete" | "failed" | "paused" | "stopped" | "rejected";
1333
+ /** Display-only dismissal marker for a reload-orphaned workflow. */
1334
+ displayDismissedAt?: number;
1293
1335
  error?: string;
1294
1336
  activityState?: ActivityState;
1295
1337
  lastActivityAt?: number;
@@ -1321,6 +1363,8 @@ export interface AsyncStatus {
1321
1363
  workflowGraph?: WorkflowGraphSnapshot;
1322
1364
  checkpoint?: ChainCheckpointState;
1323
1365
  processTerminal?: ProcessTerminalV1;
1366
+ runFanoutBudget?: RunFanoutBudgetSnapshot;
1367
+ runFanoutBudgetDescriptor?: RunFanoutBudgetDescriptor;
1324
1368
  launchContractDigest?: string;
1325
1369
  launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
1326
1370
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
@@ -1340,6 +1384,10 @@ export interface AsyncStatus {
1340
1384
  phase?: string;
1341
1385
  label?: string;
1342
1386
  workflowKey?: string;
1387
+ /** Child run identity for workflow capacity reconciliation. */
1388
+ runId?: string;
1389
+ /** True only when this workflow child owns a detached async runner. */
1390
+ async?: boolean;
1343
1391
  parentWorkflowRunId?: string;
1344
1392
  outputName?: string;
1345
1393
  structured?: boolean;
@@ -1572,6 +1620,12 @@ export interface ForegroundRunControl {
1572
1620
  toolCount?: number;
1573
1621
  /** Independently tracked children for foreground parallel work and fleet inspection. */
1574
1622
  activeChildren?: Map<number, ForegroundChildControl>;
1623
+ /** Live Prompt Audit redo callback. It is current-session memory only. */
1624
+ promptAuditRedo?: (index: number, guidance: string) => Promise<{ text: string; isError?: boolean }>;
1625
+ /** Memory-only source run for Prompt Audit redo. */
1626
+ sourceRunId?: string;
1627
+ /** Memory-only replacement run started by Prompt Audit redo. */
1628
+ supersededByRunId?: string;
1575
1629
  /** Scheduling owners that may still launch another child. Removal is safe only at zero. */
1576
1630
  schedulingOwners?: number;
1577
1631
  nestedRoute?: NestedRouteInfo;
@@ -1591,6 +1645,12 @@ export interface WaitSubscriptionRecord {
1591
1645
  expiresAt: number;
1592
1646
  }
1593
1647
 
1648
+ export interface ActiveAsyncCapacitySnapshot {
1649
+ used: number;
1650
+ /** Zero means the opt-in cap is disabled. */
1651
+ limit: number;
1652
+ }
1653
+
1594
1654
  export interface SubagentState {
1595
1655
  baseCwd: string;
1596
1656
  currentSessionId: string | null;
@@ -1611,6 +1671,8 @@ export interface SubagentState {
1611
1671
  granted?: number;
1612
1672
  grantHistory?: SpawnBudgetGrant[];
1613
1673
  };
1674
+ /** Current-session top-level async capacity projection. */
1675
+ activeAsyncCapacity?: ActiveAsyncCapacitySnapshot;
1614
1676
  asyncJobs: Map<string, AsyncJobState>;
1615
1677
  /** Current-session active and recent async runs for the native fleet inspector. */
1616
1678
  fleetJobs?: Map<string, AsyncJobState>;
@@ -1728,9 +1790,12 @@ export interface RunSyncOptions {
1728
1790
  /** Effective parent wait-tool setting propagated to the child runtime. */
1729
1791
  waitToolEnabled?: boolean;
1730
1792
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
1793
+ runFanoutBudget?: RunFanoutBudgetDescriptor;
1731
1794
  nestedRoute?: NestedRouteInfo;
1732
1795
  /** Override the agent's default model (format: "provider/id" or just "id") */
1733
1796
  modelOverride?: string;
1797
+ /** LLM intent arbiter for the completion mutation guard (rescues read-only review runs). */
1798
+ llmIntentArbiter?: import("../runs/shared/llm-intent-arbiter.ts").TaskMutationArbiter;
1734
1799
  /** Override the agent's default thinking level for this run */
1735
1800
  thinkingOverride?: AgentConfig["thinking"];
1736
1801
  /** Registry models available for heuristic bare-model resolution */
@@ -1754,6 +1819,8 @@ export interface RunSyncOptions {
1754
1819
  dynamic?: boolean;
1755
1820
  dynamicGroup?: boolean;
1756
1821
  };
1822
+ /** Private live callback for the exact child prompt after runtime acceptance injection. */
1823
+ onEffectivePrompt?: (prompt: string) => void;
1757
1824
  }
1758
1825
 
1759
1826
  export type IntercomBridgeMode = "off" | "fork-only" | "always";
@@ -1838,8 +1905,22 @@ export interface ExtensionConfig {
1838
1905
  maxSubagentDepth?: number;
1839
1906
  /** Optional cumulative session cap. Unset or 0 means unlimited. */
1840
1907
  maxSubagentSpawnsPerSession?: number;
1908
+ /** Cumulative logical-child cap for one top-level run tree. Defaults to 64. */
1909
+ maxSubagentSpawnsPerRun?: number;
1910
+ /** Optional active top-level async run cap per parent session. Unset or 0 means unlimited. */
1911
+ maxActiveAsyncRunsPerSession?: number;
1841
1912
  /** Global cap on simultaneously-running subagent tasks within a single run. Defaults to 20. */
1842
1913
  globalConcurrencyLimit?: number;
1914
+ /**
1915
+ * Global default runtime deadline in milliseconds. It replaces the built-in
1916
+ * 30-minute backstop for single, parallel, and chain launches (foreground, plus
1917
+ * plain single-agent async runs) when neither the call (`timeoutMs`/`maxRuntimeMs`)
1918
+ * nor the selected agent provides a timeout. Explicit call values and agent
1919
+ * frontmatter defaults still win. Composite async runs (chain/parallel/workflow)
1920
+ * stay unbounded at the top level by design — their children are bounded individually.
1921
+ * Must be a positive integer; invalid values are ignored.
1922
+ */
1923
+ timeoutMs?: number;
1843
1924
  control?: ControlConfig;
1844
1925
  completionBatch?: CompletionBatchConfig;
1845
1926
  turnBudget?: TurnBudgetConfig;
@@ -1854,6 +1935,8 @@ export interface ExtensionConfig {
1854
1935
  worktreeBaseDir?: string;
1855
1936
  /** Where to store subagent artifact files. Defaults to "project" (cwd/.pi/subagents). Set to "session" for pi session dir, or "temp" for OS temp. */
1856
1937
  artifactDir?: ArtifactDirPreference;
1938
+ /** Artifact cleanup retention. Set cleanupDays to 0 to disable cleanup. */
1939
+ artifactConfig?: Pick<ArtifactConfig, "cleanupDays">;
1857
1940
  intercomBridge?: IntercomBridgeConfig;
1858
1941
  proactiveSkillSubagents?: ProactiveSkillSubagentsConfig | false;
1859
1942
  scheduledRuns?: ScheduledRunsConfig;
@@ -1961,7 +2044,7 @@ export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
1961
2044
  export const POLL_INTERVAL_MS = 250;
1962
2045
  export const MAX_WIDGET_JOBS = 4;
1963
2046
  export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
1964
- export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "guide", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "worktree.discard", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "append-step", "approve-checkpoint", "reject-checkpoint", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
2047
+ export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "guide", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "worktree.discard", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "dismiss", "append-step", "approve-checkpoint", "reject-checkpoint", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
1965
2048
 
1966
2049
  export const DEFAULT_FORK_PREAMBLE =
1967
2050
  "You are a delegated subagent running from a fork of the parent session. " +
@@ -2053,6 +2136,19 @@ export function resolveMaxSubagentSpawnsPerSession(configMaxSpawns?: number): nu
2053
2136
  return configuredMaxSpawns === 0 ? undefined : configuredMaxSpawns;
2054
2137
  }
2055
2138
 
2139
+ export const DEFAULT_MAX_SUBAGENT_SPAWNS_PER_RUN = 64;
2140
+
2141
+ export function normalizeMaxSubagentSpawnsPerRun(value: unknown): number | undefined {
2142
+ const normalized = normalizeNonNegativeInteger(value);
2143
+ return normalized !== undefined && normalized > 0 ? normalized : undefined;
2144
+ }
2145
+
2146
+ export function resolveMaxSubagentSpawnsPerRun(configMaxSpawns?: number): number {
2147
+ return normalizeMaxSubagentSpawnsPerRun(process.env.PI_SUBAGENT_MAX_SPAWNS_PER_RUN)
2148
+ ?? normalizeMaxSubagentSpawnsPerRun(configMaxSpawns)
2149
+ ?? DEFAULT_MAX_SUBAGENT_SPAWNS_PER_RUN;
2150
+ }
2151
+
2056
2152
  // ============================================================================
2057
2153
  // Utility Functions
2058
2154
  // ============================================================================
@@ -17,6 +17,7 @@ import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, Neste
17
17
  const DEFAULT_CONFIG_DIR_NAME = ".pi";
18
18
  const PI_CODING_AGENT_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
19
19
  export const PI_CODING_AGENT_PACKAGE_ROOT_ENV = "PI_SUBAGENTS_PI_CODING_AGENT_PACKAGE_ROOT";
20
+ export const PROMPT_REDACTED = "[prompt redacted]";
20
21
 
21
22
  export function resolveWatchPath(
22
23
  watchPath: string,
@@ -354,7 +355,7 @@ function compactCompletedProgress(progress: AgentProgress): AgentProgress {
354
355
  agent: progress.agent,
355
356
  status: progress.status,
356
357
  activityState: progress.activityState,
357
- task: progress.task,
358
+ task: "[prompt redacted]",
358
359
  skills: progress.skills,
359
360
  toolCount: progress.toolCount,
360
361
  tokens: progress.tokens,
@@ -428,6 +429,7 @@ export function compactForegroundResult(result: SingleResult): SingleResult {
428
429
  const toolCalls = result.toolCalls?.length ? result.toolCalls : extractToolCallSummaries(result.messages);
429
430
  return {
430
431
  ...result,
432
+ task: "[prompt redacted]",
431
433
  messages: undefined,
432
434
  progress: undefined,
433
435
  toolCalls: toolCalls.length ? toolCalls : undefined,
@@ -318,7 +318,7 @@ export class SubagentFleetStatus {
318
318
  this.clearWidget();
319
319
  return;
320
320
  }
321
- if (this.entries.length === 0) {
321
+ if (this.entries.length === 0 && !(this.state.activeAsyncCapacity?.used)) {
322
322
  this.active = false;
323
323
  this.selectedKey = "main";
324
324
  this.lastRenderKey = "";
@@ -413,10 +413,13 @@ export class SubagentFleetStatus {
413
413
  }
414
414
 
415
415
  render(width: number, theme: Theme): string[] {
416
- if (this.entries.length === 0) return [];
416
+ if (this.entries.length === 0 && !(this.state.activeAsyncCapacity?.used)) return [];
417
417
  if (!this.active) {
418
418
  const tokens = this.entries.reduce((total, entry) => total + entry.tokens, 0);
419
- const label = `${this.entries.length} active ${this.entries.length === 1 ? "agent" : "agents"}`;
419
+ const capacity = this.state.activeAsyncCapacity;
420
+ const asyncRuns = capacity ? `Async runs ${capacity.used}/${capacity.limit || "∞"}` : "";
421
+ const agents = this.entries.length > 0 ? `${this.entries.length} active ${this.entries.length === 1 ? "agent" : "agents"}` : "";
422
+ const label = [agents, asyncRuns].filter(Boolean).join(" · ");
420
423
  return [truncateToWidth(` ${theme.fg("muted", label)} · ${theme.fg("dim", `${formatFleetTokens(tokens)} · ↓/← to inspect`)}`, width)];
421
424
  }
422
425
  const roster = this.rosterKeys();
@@ -444,9 +447,8 @@ export class SubagentFleetStatus {
444
447
  }
445
448
 
446
449
  private renderEntry(rosterIndex: number, selectedIndex: number, entry: FleetStatusEntry, width: number, theme: Theme): string {
447
- const description = entry.description?.replace(/\s+/g, " ").trim();
448
450
  const agent = entry.modelThinking ? `${entry.agent} (${entry.modelThinking})` : entry.agent;
449
- const left = ` ${this.bullet(rosterIndex, selectedIndex, theme)} ${theme.fg("muted", agent)} · ${entry.state}${description ? ` ${description}` : ""}`;
451
+ const left = ` ${this.bullet(rosterIndex, selectedIndex, theme)} ${theme.fg("muted", agent)} · ${entry.state}`;
450
452
  const elapsed = Date.now() - entry.startedAt;
451
453
  const right = theme.fg("dim", `${formatFleetElapsed(elapsed)} · ${formatFleetTokens(entry.tokens)}`);
452
454
  return rightAlign(left, right, width);