pi-subagents 0.53.0 → 0.55.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 (76) hide show
  1. package/CHANGELOG.md +66 -5
  2. package/README.md +1 -1
  3. package/agents/reviewer.md +12 -2
  4. package/docs/agents.md +4 -4
  5. package/docs/configuration.md +6 -6
  6. package/docs/extension-api.md +14 -4
  7. package/docs/models.md +41 -6
  8. package/docs/observability.md +14 -3
  9. package/docs/tool-reference.md +21 -6
  10. package/docs/workflows.md +7 -1
  11. package/index.ts +10 -1
  12. package/package.json +1 -1
  13. package/prompts/council.md +19 -7
  14. package/prompts/parallel-review.md +5 -1
  15. package/prompts/review-loop.md +9 -5
  16. package/skills/council-mode/SKILL.md +50 -26
  17. package/skills/pi-subagents/SKILL.md +5 -1
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +16 -1
  19. package/skills/pi-subagents/references/execution-controls.md +13 -4
  20. package/skills/pi-subagents/references/multi-lane-orchestration.md +3 -3
  21. package/skills/pi-subagents/references/prompting-and-roles.md +14 -7
  22. package/src/agents/agent-management.ts +115 -14
  23. package/src/agents/agent-serializer.ts +2 -2
  24. package/src/agents/agents.ts +195 -47
  25. package/src/api/external-job-provider.ts +10 -1
  26. package/src/api/preflight.ts +33 -19
  27. package/src/api/project-panes.ts +2 -0
  28. package/src/extension/doctor.ts +10 -0
  29. package/src/extension/fanout-child.ts +3 -2
  30. package/src/extension/index.ts +24 -4
  31. package/src/extension/public-execution.ts +12 -9
  32. package/src/extension/rpc.ts +77 -3
  33. package/src/extension/schemas.ts +2 -1
  34. package/src/extension/tool-description.ts +9 -6
  35. package/src/extension/tool-result.ts +19 -0
  36. package/src/inspectors/herdr/client.ts +3 -3
  37. package/src/inspectors/herdr/focus.ts +55 -0
  38. package/src/inspectors/herdr/project-panes.ts +228 -44
  39. package/src/integrations/herdr-status.ts +26 -4
  40. package/src/runs/background/async-execution.ts +112 -13
  41. package/src/runs/background/async-job-tracker.ts +33 -14
  42. package/src/runs/background/async-resume.ts +20 -2
  43. package/src/runs/background/async-retention.ts +1 -1
  44. package/src/runs/background/async-status.ts +10 -0
  45. package/src/runs/background/chain-root-attachment.ts +5 -0
  46. package/src/runs/background/control-channel.ts +98 -10
  47. package/src/runs/background/notify.ts +62 -1
  48. package/src/runs/background/result-watcher.ts +4 -3
  49. package/src/runs/background/run-status.ts +15 -1
  50. package/src/runs/background/stale-run-reconciler.ts +3 -0
  51. package/src/runs/background/subagent-runner.ts +251 -38
  52. package/src/runs/background/wait-completions.ts +2 -0
  53. package/src/runs/background/wait-tool.ts +4 -3
  54. package/src/runs/foreground/async-stop-action.ts +23 -2
  55. package/src/runs/foreground/execution.ts +110 -8
  56. package/src/runs/foreground/subagent-executor.ts +361 -45
  57. package/src/runs/foreground/workflow-detach-reconcile.ts +3 -0
  58. package/src/runs/shared/acceptance.ts +1 -0
  59. package/src/runs/shared/child-identity.ts +36 -0
  60. package/src/runs/shared/completion-guard.ts +50 -1
  61. package/src/runs/shared/external-job-bridge.ts +53 -37
  62. package/src/runs/shared/external-job-runner.ts +126 -23
  63. package/src/runs/shared/model-fallback.ts +46 -15
  64. package/src/runs/shared/model-scope.ts +106 -39
  65. package/src/runs/shared/orca-progress-tabs.ts +71 -9
  66. package/src/runs/shared/parallel-utils.ts +12 -0
  67. package/src/runs/shared/pi-args.ts +31 -1
  68. package/src/runs/shared/subagent-prompt-runtime.ts +39 -10
  69. package/src/runs/shared/tool-availability.ts +1 -3
  70. package/src/shared/launch-contract.ts +6 -5
  71. package/src/shared/thinking-ceiling.ts +52 -0
  72. package/src/shared/types.ts +72 -5
  73. package/src/tui/fleet-status.ts +66 -13
  74. package/src/tui/render.ts +5 -4
  75. package/src/watchdog/permission-arbiter.ts +59 -51
  76. package/src/workflows/scripted-workflow.ts +107 -10
@@ -11,6 +11,7 @@ import { RUNTIME_EXTENSION_ACK_EVENT, RUNTIME_EXTENSION_ACK_PATH_ENV, isRuntimeA
11
11
  import { createStructuredOutputToolParameters, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
12
12
  import {
13
13
  CHILD_TOOL_DIAGNOSTIC_PATH_ENV,
14
+ formatChildToolDiagnostic,
14
15
  MCP_DIRECT_CHILD_TOOLS_ENV,
15
16
  REQUIRED_CHILD_TOOLS_ENV,
16
17
  writeChildToolDiagnostic,
@@ -21,7 +22,7 @@ import type { JsonSchemaObject, ResolvedToolBudget, SubagentState } from "../../
21
22
  import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
22
23
  import { resolveWatchPath } from "../../shared/utils.ts";
23
24
  import { registerChildWatchdog } from "../../watchdog/register-child.ts";
24
- import { CHILD_WATCHDOG_CONFIG_ENV } from "../../watchdog/child-status.ts";
25
+ import { CHILD_WATCHDOG_CONFIG_ENV, decodeChildWatchdogConfig } from "../../watchdog/child-status.ts";
25
26
  import { requestWatchdogPermission, type WatchdogPermissionRequest, type WatchdogPermissionResult } from "../../watchdog/permission-arbiter.ts";
26
27
  import { SUBAGENT_WATCHDOG_WARNING_TYPE } from "../../watchdog/types.ts";
27
28
  import { resolveWaitToolConfig } from "../background/wait-config.ts";
@@ -296,14 +297,41 @@ export function registerPermissionGate(
296
297
  const decision = permissionDecision(rules, toolName);
297
298
  if (decision === "allow") return undefined;
298
299
  if (decision === "deny") return { block: true, reason: `Blocked by pi-subagents permission rule: '${toolName}' is denied.` };
299
- const result = await requestPermission({
300
- ctx,
301
- toolName,
302
- args: event.input ?? {},
303
- rawWatchdogConfig: process.env[CHILD_WATCHDOG_CONFIG_ENV],
304
- auditPath: process.env[PERMISSION_AUDIT_PATH_ENV],
305
- ...(ctx.signal ? { signal: ctx.signal } : {}),
306
- });
300
+ const rawWatchdogConfig = process.env[CHILD_WATCHDOG_CONFIG_ENV];
301
+ let timeoutMs = 30_000;
302
+ try {
303
+ timeoutMs = decodeChildWatchdogConfig(rawWatchdogConfig)?.agentEndTimeoutMs ?? timeoutMs;
304
+ } catch {
305
+ // The arbiter reports invalid configuration with the concrete decode error.
306
+ }
307
+ if (ctx.signal?.aborted) return { block: true, reason: "Blocked by pi-subagents permission rule: Watchdog permission decision was cancelled." };
308
+ let timeout: ReturnType<typeof setTimeout> | undefined;
309
+ let abort: (() => void) | undefined;
310
+ let result: WatchdogPermissionResult;
311
+ try {
312
+ result = await Promise.race([
313
+ requestPermission({
314
+ ctx,
315
+ toolName,
316
+ args: event.input ?? {},
317
+ rawWatchdogConfig,
318
+ auditPath: process.env[PERMISSION_AUDIT_PATH_ENV],
319
+ ...(ctx.signal ? { signal: ctx.signal } : {}),
320
+ }),
321
+ new Promise<WatchdogPermissionResult>((resolve) => {
322
+ if (!ctx.signal) return;
323
+ abort = () => resolve({ approved: false, reason: "Watchdog permission decision was cancelled.", source: "watchdog" });
324
+ ctx.signal.addEventListener("abort", abort, { once: true });
325
+ }),
326
+ new Promise<never>((_, reject) => { timeout = setTimeout(() => reject(new Error(`Watchdog permission decision timed out after ${timeoutMs}ms.`)), timeoutMs); }),
327
+ ]);
328
+ } catch (error) {
329
+ const reason = error instanceof Error ? error.message : String(error);
330
+ return { block: true, reason: `Blocked by pi-subagents permission rule: Watchdog permission arbiter failed closed: ${reason}` };
331
+ } finally {
332
+ if (timeout) clearTimeout(timeout);
333
+ if (abort) ctx.signal?.removeEventListener("abort", abort);
334
+ }
307
335
  if (result.approved) return undefined;
308
336
  return { block: true, reason: `Blocked by pi-subagents permission rule: ${result.reason}` };
309
337
  });
@@ -598,7 +626,8 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
598
626
  registerNativeSupervisorClientOnce();
599
627
  });
600
628
  onRuntimeEvent("agent_start", () => {
601
- refreshChildToolDiagnostic(pi);
629
+ const diagnostic = refreshChildToolDiagnostic(pi);
630
+ if (diagnostic) throw new Error(formatChildToolDiagnostic(diagnostic));
602
631
  });
603
632
  onRuntimeEvent("agent_end", async (_event: unknown, ctx: unknown) => {
604
633
  if ((ctx as { hasUI?: boolean } | undefined)?.hasUI === true) return;
@@ -13,8 +13,6 @@ export interface ChildToolDiagnostic {
13
13
  missingMcpDirectTools?: string[];
14
14
  }
15
15
 
16
- const PI_CORE_CHILD_TOOLS = new Set(["bash", "edit", "find", "grep", "ls", "read", "write"]);
17
-
18
16
  export function writeChildToolDiagnostic(
19
17
  filePath: string,
20
18
  required: string[],
@@ -22,7 +20,7 @@ export function writeChildToolDiagnostic(
22
20
  agent?: string,
23
21
  mcpDirectTools?: string[],
24
22
  ): ChildToolDiagnostic | undefined {
25
- const availableNames = new Set([...available, ...PI_CORE_CHILD_TOOLS]);
23
+ const availableNames = new Set(available);
26
24
  const missing = required.filter((name) => !availableNames.has(name));
27
25
  if (missing.length === 0) {
28
26
  fs.rmSync(filePath, { force: true });
@@ -17,7 +17,7 @@ function stableJson(value: unknown): string {
17
17
  return JSON.stringify(value);
18
18
  }
19
19
 
20
- function sha256(value: unknown): string {
20
+ export function stableJsonDigest(value: unknown): string {
21
21
  return createHash("sha256").update(stableJson(value)).digest("hex");
22
22
  }
23
23
 
@@ -44,6 +44,7 @@ export function projectAgentDefinition(agent: AgentConfig): Record<string, unkno
44
44
  inheritProjectContext: agent.inheritProjectContext,
45
45
  inheritSkills: agent.inheritSkills,
46
46
  model: agent.model,
47
+ modelProvider: agent.modelProvider,
47
48
  fallbackModels: agent.fallbackModels,
48
49
  thinking: agent.thinking,
49
50
  tools: agent.tools,
@@ -70,7 +71,7 @@ export function projectAgentDefinition(agent: AgentConfig): Record<string, unkno
70
71
  }
71
72
 
72
73
  export function agentDefinitionDigest(agent: AgentConfig): string {
73
- return sha256(projectAgentDefinition(agent));
74
+ return stableJsonDigest(projectAgentDefinition(agent));
74
75
  }
75
76
 
76
77
  export interface LaunchBindingInput {
@@ -99,12 +100,12 @@ export function projectLaunchBinding(input: LaunchBindingInput): Record<string,
99
100
  return {
100
101
  version: LAUNCH_BINDING_PROJECTION_VERSION,
101
102
  definitionDigest: input.definitionDigest,
102
- taskDigest: input.task === undefined ? undefined : sha256(input.task),
103
+ taskDigest: input.task === undefined ? undefined : stableJsonDigest(input.task),
103
104
  // The ordered candidate set already contains each attempted model; keeping only
104
105
  // this set makes retries correlate to the same preflight binding.
105
106
  modelCandidates: input.modelCandidates,
106
107
  thinking: input.thinking,
107
- systemPromptDigest: input.systemPrompt === undefined || input.systemPrompt === null ? undefined : sha256(input.systemPrompt),
108
+ systemPromptDigest: input.systemPrompt === undefined || input.systemPrompt === null ? undefined : stableJsonDigest(input.systemPrompt),
108
109
  systemPromptMode: input.systemPromptMode,
109
110
  inheritProjectContext: input.inheritProjectContext,
110
111
  inheritSkills: input.inheritSkills,
@@ -120,5 +121,5 @@ export function projectLaunchBinding(input: LaunchBindingInput): Record<string,
120
121
  }
121
122
 
122
123
  export function launchBindingDigest(input: LaunchBindingInput): string {
123
- return sha256(projectLaunchBinding(input));
124
+ return stableJsonDigest(projectLaunchBinding(input));
124
125
  }
@@ -0,0 +1,52 @@
1
+ import { resolveEffectiveThinking, THINKING_LEVELS, type ThinkingLevel } from "./model-info.ts";
2
+ export type { ThinkingLevel } from "./model-info.ts";
3
+
4
+ export const SUBAGENT_THINKING_CEILING_ENV = "PI_SUBAGENT_THINKING_CEILING";
5
+
6
+ const thinkingLevelRanks = new Map<ThinkingLevel, number>(THINKING_LEVELS.map((level, index) => [level, index]));
7
+
8
+ export function parseThinkingLevel(value: unknown, field = "thinking level"): ThinkingLevel {
9
+ if (typeof value === "string") {
10
+ const trimmed = value.trim();
11
+ if (thinkingLevelRanks.has(trimmed as ThinkingLevel)) return trimmed as ThinkingLevel;
12
+ }
13
+ throw new Error(`Invalid ${field}; expected one of ${THINKING_LEVELS.join(", ")}.`);
14
+ }
15
+
16
+ export function compareThinkingLevels(left: ThinkingLevel, right: ThinkingLevel): number {
17
+ const leftRank = thinkingLevelRanks.get(left);
18
+ const rightRank = thinkingLevelRanks.get(right);
19
+ if (leftRank === undefined || rightRank === undefined) throw new Error(`Invalid thinking level comparison; expected one of ${THINKING_LEVELS.join(", ")}.`);
20
+ return leftRank - rightRank;
21
+ }
22
+
23
+ export function intersectThinkingCeilings(...ceilings: Array<ThinkingLevel | undefined>): ThinkingLevel | undefined {
24
+ const active = ceilings.filter((ceiling): ceiling is ThinkingLevel => ceiling !== undefined);
25
+ if (active.length === 0) return undefined;
26
+ return active.reduce((lowest, ceiling) => compareThinkingLevels(ceiling, lowest) < 0 ? ceiling : lowest);
27
+ }
28
+
29
+ export function decodeThinkingCeiling(value: string | undefined): ThinkingLevel | undefined {
30
+ if (value === undefined || value === "") return undefined;
31
+ return parseThinkingLevel(value, "inherited thinking ceiling");
32
+ }
33
+
34
+ export interface ThinkingCeilingCheck {
35
+ model?: string;
36
+ configThinking?: string | false;
37
+ ceiling?: ThinkingLevel;
38
+ agent?: string;
39
+ runId?: string;
40
+ }
41
+
42
+ export function assertThinkingWithinCeiling(input: ThinkingCeilingCheck): void {
43
+ if (!input.ceiling) return;
44
+ const requested = resolveEffectiveThinking(input.model, input.configThinking);
45
+ if (!requested) return;
46
+ const requestedLevel = parseThinkingLevel(requested, "requested thinking level");
47
+ if (compareThinkingLevels(requestedLevel, input.ceiling) <= 0) return;
48
+ const subject = [input.agent ? `agent '${input.agent}'` : undefined, input.runId ? `run '${input.runId}'` : undefined]
49
+ .filter((value): value is string => Boolean(value))
50
+ .join(" ");
51
+ throw new Error(`Thinking level '${requestedLevel}' exceeds configured maximum '${input.ceiling}'${subject ? ` for ${subject}` : ""}.`);
52
+ }
@@ -8,9 +8,10 @@ import type { Message } from "@earendil-works/pi-ai";
8
8
  import type { AgentConfig } from "../agents/agents.ts";
9
9
  import type { FSWatcher } from "node:fs";
10
10
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
11
- import type { ModelScopeConfig } from "../runs/shared/model-scope.ts";
11
+ import type { ModelScopeRule } from "../runs/shared/model-scope.ts";
12
12
  import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../runs/shared/capability-ceiling.ts";
13
13
  import type { AuthorityPolicyConfig } from "../policy/authority.ts";
14
+ import type { ThinkingLevel } from "./model-info.ts";
14
15
  import type { GlobalMissionIndexRecord, MissionRecord, MissionStoreConfig } from "../missions/types.ts";
15
16
 
16
17
  // ============================================================================
@@ -342,7 +343,7 @@ export interface ReviewProjection {
342
343
  }
343
344
 
344
345
  export interface FileMutationEffect {
345
- status: "not-requested" | "not-applicable" | "observed" | "missing";
346
+ status: "not-requested" | "not-applicable" | "observed" | "missing" | "blocked";
346
347
  expected: boolean;
347
348
  attempted: boolean;
348
349
  message?: string;
@@ -532,9 +533,11 @@ export interface SteeringRecoveryDescriptor {
532
533
  sessionFile?: string;
533
534
  cwd: string;
534
535
  model?: string;
536
+ modelProvider?: string;
535
537
  modelOverrideFromParent?: boolean;
536
538
  fallbackModels?: string[];
537
539
  thinking?: string;
540
+ thinkingCeiling?: ThinkingLevel;
538
541
  tools?: string[];
539
542
  extensions?: string[];
540
543
  subagentOnlyExtensions?: string[];
@@ -957,6 +960,13 @@ export interface SingleResult {
957
960
  modelAttempts?: ModelAttempt[];
958
961
  controlEvents?: ControlEvent[];
959
962
  error?: string;
963
+ /**
964
+ * True when the dispatch failed because the input exceeded the model's
965
+ * context window. The model fallback loop stops immediately (retrying the
966
+ * same input on another model cannot succeed). Callers should treat this as
967
+ * a signal to reduce input size or re-decompose the task.
968
+ */
969
+ contextOverflow?: boolean;
960
970
  protocolError?: ProtocolOutputLimit;
961
971
  sessionFile?: string;
962
972
  skills?: string[];
@@ -1022,6 +1032,7 @@ export interface WaitCompletionChild {
1022
1032
  outputState?: SubagentOutputState;
1023
1033
  error?: string;
1024
1034
  model?: string;
1035
+ contextOverflow?: boolean;
1025
1036
  artifactPaths?: Partial<ArtifactPaths>;
1026
1037
  }
1027
1038
 
@@ -1364,6 +1375,12 @@ export interface ExternalJobStatus {
1364
1375
  provider: string;
1365
1376
  providerJobId?: string;
1366
1377
  promptDigest: string;
1378
+ operation?: "start" | "follow-up";
1379
+ sourceRunId?: string;
1380
+ sourceStepIndex?: number;
1381
+ parentProviderJobId?: string;
1382
+ requestId?: string;
1383
+ requestDigest?: string;
1367
1384
  options: Record<string, unknown>;
1368
1385
  handleUrl?: string;
1369
1386
  conversationUrl?: string;
@@ -1446,6 +1463,8 @@ export interface AsyncStatus {
1446
1463
  /** Set when a durable schedule launched this run, so completions can name their origin. */
1447
1464
  scheduleOrigin?: ScheduleOrigin;
1448
1465
  steps?: Array<{
1466
+ /** Stable caller-facing child identity for inspect/status/stop. */
1467
+ childId?: string;
1449
1468
  agent: string;
1450
1469
  runner?: ExternalCliRunnerStatus | ExternalJobRunnerStatus;
1451
1470
  externalProcess?: ExternalProcessStatus;
@@ -1465,6 +1484,8 @@ export interface AsyncStatus {
1465
1484
  outputName?: string;
1466
1485
  structured?: boolean;
1467
1486
  status: "pending" | "running" | "complete" | "completed" | "failed" | "paused" | "stopped" | "rejected";
1487
+ stopRequested?: boolean;
1488
+ stopRequestedAt?: number;
1468
1489
  children?: NestedRunSummary[];
1469
1490
  sessionFile?: string;
1470
1491
  transcriptPath?: string;
@@ -1494,8 +1515,11 @@ export interface AsyncStatus {
1494
1515
  skills?: string[];
1495
1516
  model?: string;
1496
1517
  thinking?: string;
1518
+ thinkingCeiling?: ThinkingLevel;
1497
1519
  attemptedModels?: string[];
1498
1520
  modelAttempts?: ModelAttempt[];
1521
+ /** True when the child input exceeded the model context window. */
1522
+ contextOverflow?: boolean;
1499
1523
  totalCost?: CostSummary;
1500
1524
  steering?: SteeringStatus;
1501
1525
  error?: string;
@@ -1754,6 +1778,8 @@ export interface SubagentState {
1754
1778
  };
1755
1779
  /** Current-session top-level async capacity projection. */
1756
1780
  activeAsyncCapacity?: ActiveAsyncCapacitySnapshot;
1781
+ /** Herdr project panes opened by this Pi session, keyed by project root. */
1782
+ herdrProjectPanes?: Map<string, HerdrProjectPaneSnapshot>;
1757
1783
  asyncJobs: Map<string, AsyncJobState>;
1758
1784
  /** Current-session active and recent async runs for the native fleet inspector. */
1759
1785
  fleetJobs?: Map<string, AsyncJobState>;
@@ -1780,6 +1806,26 @@ export interface SubagentState {
1780
1806
  waitSubscriptions?: Map<string, WaitSubscriptionRecord>;
1781
1807
  /** Live in-process workflow controllers. Durable status remains on disk after settlement. */
1782
1808
  workflowControllers?: Map<string, AbortController>;
1809
+ /** Live in-process workflow child stoppers keyed by parent workflow run id. */
1810
+ workflowChildStops?: Map<string, (childId: string, message?: string) => boolean>;
1811
+ }
1812
+
1813
+ export interface HerdrProjectPaneSnapshot {
1814
+ projectRoot: string;
1815
+ bindingPath: string;
1816
+ paneId: string;
1817
+ openedAt: string;
1818
+ lastFocusedAt?: string;
1819
+ state: "open" | "stale";
1820
+ agentStatus: string;
1821
+ ownership: "verified" | "unknown" | "mismatch";
1822
+ safeToClose: boolean;
1823
+ refreshedAt: number;
1824
+ summary?: string;
1825
+ tabId?: string;
1826
+ workspaceId?: string;
1827
+ terminalTitle?: string;
1828
+ staleReason?: string;
1783
1829
  }
1784
1830
 
1785
1831
  // ============================================================================
@@ -1815,9 +1861,28 @@ export const SUBAGENT_FOREGROUND_COMPLETE_EVENT = "subagent:foreground-complete"
1815
1861
  export const SUBAGENT_CONTROL_EVENT = "subagent:control-event";
1816
1862
  export const SUBAGENT_CONTROL_INTERCOM_EVENT = "subagent:control-intercom";
1817
1863
  export const SUBAGENT_STEERING_NOTICE_EVENT = "subagent:steering-notice";
1864
+ export const SUBAGENT_CHILD_STATUS_EVENT = "subagent:child-status";
1818
1865
  export const SUBAGENT_RESULT_INTERCOM_EVENT = "subagent:result-intercom";
1819
1866
  export const SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT = "subagent:result-intercom-delivery";
1820
1867
 
1868
+ export interface SubagentChildStatusEvent {
1869
+ type: "subagent.child-status";
1870
+ version: 1;
1871
+ runId: string;
1872
+ childId: string;
1873
+ status: "stopping" | "stopped";
1874
+ ts: number;
1875
+ reason?: string;
1876
+ source?: "rpc" | "async";
1877
+ asyncDir?: string;
1878
+ stepIndex?: number;
1879
+ agent?: string;
1880
+ childRunId?: string;
1881
+ workflowKey?: string;
1882
+ phase?: string;
1883
+ label?: string;
1884
+ }
1885
+
1821
1886
  // ============================================================================
1822
1887
  // Execution Options
1823
1888
  // ============================================================================
@@ -1885,12 +1950,13 @@ export interface RunSyncOptions {
1885
1950
  llmIntentArbiter?: import("../runs/shared/llm-intent-arbiter.ts").TaskMutationArbiter;
1886
1951
  /** Override the agent's default thinking level for this run */
1887
1952
  thinkingOverride?: AgentConfig["thinking"];
1953
+ thinkingCeiling?: ThinkingLevel;
1888
1954
  /** Registry models available for heuristic bare-model resolution */
1889
1955
  availableModels?: Array<{ provider: string; id: string; fullId: string }>;
1890
1956
  /** Current parent-session provider to prefer for ambiguous bare model ids */
1891
1957
  preferredModelProvider?: string;
1892
1958
  /** Optional subagent model-scope enforcement for fallback candidates */
1893
- modelScope?: ModelScopeConfig;
1959
+ modelScope?: ModelScopeRule | ModelScopeRule[];
1894
1960
  /** Skills to make available (overrides agent default if provided) */
1895
1961
  skills?: string[];
1896
1962
  structuredOutput?: {
@@ -1972,7 +2038,7 @@ export type FleetKeybindingAction = typeof FLEET_KEYBINDING_ACTIONS[number];
1972
2038
  export type FleetKeybindingsConfig = Partial<Record<FleetKeybindingAction, string[]>>;
1973
2039
 
1974
2040
  export interface OrcaProgressTabsConfig {
1975
- /** Create one Orca terminal tab per running subagent. Experimental and opt-in. */
2041
+ /** Create one Orca observer tab per top-level subagent call. Experimental and opt-in. */
1976
2042
  enabled?: boolean;
1977
2043
  }
1978
2044
 
@@ -2054,7 +2120,7 @@ export interface ExtensionConfig {
2054
2120
  /** Artifact cleanup retention. Set cleanupDays to 0 to disable cleanup. */
2055
2121
  artifactConfig?: Pick<ArtifactConfig, "cleanupDays">;
2056
2122
  intercomBridge?: IntercomBridgeConfig;
2057
- /** Control how slow result-index scans are logged. Defaults to \"all\".
2123
+ /** Control how slow result-index scans are logged. Defaults to \"activity\".
2058
2124
  * - \"all\": log every slow scan, including scans that find nothing.
2059
2125
  * - \"activity\": log only slow scans that found or scheduled work. Silences
2060
2126
  * the periodic healthy rescan that inspects zero files while no async runs
@@ -2168,6 +2234,7 @@ export const SLASH_SUBAGENT_RESPONSE_EVENT = "subagent:slash:response";
2168
2234
  export const SLASH_SUBAGENT_UPDATE_EVENT = "subagent:slash:update";
2169
2235
  export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
2170
2236
  export const POLL_INTERVAL_MS = 250;
2237
+ export const WIDGET_ANIMATION_INTERVAL_MS = 1000;
2171
2238
  export const MAX_WIDGET_JOBS = 4;
2172
2239
  export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
2173
2240
  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", "debug.run", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "dismiss", "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;
@@ -2,7 +2,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { type EditorComponent, isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
3
  import { snapshotExternalRuns } from "../api/external-runs.ts";
4
4
  import { formatModelThinking } from "../shared/formatters.ts";
5
- import type { AsyncJobStep, FleetViewPlacement, NestedRunSummary, NestedStepSummary, SubagentState } from "../shared/types.ts";
5
+ import type { AsyncJobState, AsyncJobStep, FleetViewPlacement, HerdrProjectPaneSnapshot, NestedRunSummary, NestedStepSummary, SubagentState } from "../shared/types.ts";
6
6
  import { formatWorkflowJsonPreview } from "../workflows/scripted-workflow.ts";
7
7
 
8
8
  export const FLEET_STATUS_WIDGET_KEY = "subagent-fleet-status";
@@ -17,6 +17,7 @@ type FleetStatusTui = {
17
17
  };
18
18
  type FleetStatusEntry = {
19
19
  key: string;
20
+ surface?: "project-pane";
20
21
  parentKey?: string;
21
22
  workflowWrapper?: boolean;
22
23
  agent: string;
@@ -26,6 +27,7 @@ type FleetStatusEntry = {
26
27
  tokens: number;
27
28
  state: string;
28
29
  external?: true;
30
+ projectPane?: HerdrProjectPaneSnapshot;
29
31
  nestedChildren?: NestedRunSummary[];
30
32
  workflowRows?: FleetWorkflowRow[];
31
33
  };
@@ -284,7 +286,32 @@ function foregroundDescription(control: { parentWorkflowRunId?: string; workflow
284
286
  }
285
287
 
286
288
  function activeLeafAgentCount(entries: FleetStatusEntry[]): number {
287
- return entries.filter((entry) => !entry.workflowWrapper).length;
289
+ return entries.filter((entry) => !entry.workflowWrapper && !entry.surface).length;
290
+ }
291
+
292
+ function projectPaneNeedsAttention(pane: HerdrProjectPaneSnapshot): boolean {
293
+ return ["attention", "blocked", "paused", "failed", "error"].some((status) => pane.agentStatus.includes(status))
294
+ || pane.summary?.includes("⚠") === true;
295
+ }
296
+
297
+ function projectName(projectRoot: string): string {
298
+ return projectRoot.split(/[\\/]/).filter(Boolean).at(-1) ?? projectRoot;
299
+ }
300
+
301
+ function projectPaneEntries(state: SubagentState): FleetStatusEntry[] {
302
+ return [...(state.herdrProjectPanes?.values() ?? [])]
303
+ .filter((pane) => pane.state === "open")
304
+ .sort((left, right) => left.openedAt.localeCompare(right.openedAt) || left.projectRoot.localeCompare(right.projectRoot))
305
+ .map((pane) => ({
306
+ key: `project-pane:${pane.projectRoot}`,
307
+ surface: "project-pane" as const,
308
+ agent: `${projectName(pane.projectRoot)} · ${pane.paneId}`,
309
+ description: pane.summary,
310
+ startedAt: Date.parse(pane.openedAt) || pane.refreshedAt,
311
+ tokens: 0,
312
+ state: pane.agentStatus || "unknown",
313
+ projectPane: pane,
314
+ }));
288
315
  }
289
316
 
290
317
  export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntry[] {
@@ -406,6 +433,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
406
433
  }
407
434
  }
408
435
 
436
+ entries.push(...projectPaneEntries(state));
409
437
  return entries.sort((left, right) => left.startedAt - right.startedAt || left.key.localeCompare(right.key));
410
438
  }
411
439
 
@@ -483,7 +511,7 @@ export class SubagentFleetStatus {
483
511
  this.clearWidget();
484
512
  return;
485
513
  }
486
- if (this.entries.length === 0 && !(this.state.activeAsyncCapacity?.used)) {
514
+ if (!this.hasInlineSurface()) {
487
515
  this.active = false;
488
516
  this.selectedKey = "main";
489
517
  this.lastRenderKey = "";
@@ -578,26 +606,31 @@ export class SubagentFleetStatus {
578
606
  }
579
607
 
580
608
  render(width: number, theme: Theme): string[] {
581
- if (this.entries.length === 0 && !(this.state.activeAsyncCapacity?.used)) return [];
609
+ if (!this.hasInlineSurface()) return [];
582
610
  if (!this.active) {
583
- const tokens = this.entries.reduce((total, entry) => total + entry.tokens, 0);
611
+ const workEntries = this.entries.filter((entry) => !entry.surface);
612
+ const projectEntries = this.entries.filter((entry) => entry.surface === "project-pane");
613
+ const tokens = workEntries.reduce((total, entry) => total + entry.tokens, 0);
584
614
  const capacity = this.state.activeAsyncCapacity;
585
- const hasNativeRows = this.entries.some((entry) => !entry.external);
615
+ const hasNativeRows = workEntries.some((entry) => !entry.external);
586
616
  const showNativeSummary = hasNativeRows || Boolean(capacity?.used);
587
617
  const asyncRuns = capacity && showNativeSummary ? `Async runs ${capacity.used}/${capacity.limit || "∞"}` : "";
588
- const activeEntries = activeLeafAgentCount(this.entries);
589
- const noun = this.entries.some((entry) => entry.external) ? "job" : "agent";
618
+ const activeEntries = activeLeafAgentCount(workEntries);
619
+ const noun = workEntries.some((entry) => entry.external) ? "job" : "agent";
590
620
  const agents = activeEntries > 0 ? `${activeEntries} active ${noun}${activeEntries === 1 ? "" : "s"}` : "";
591
- const label = [agents, asyncRuns].filter(Boolean).join(" · ");
621
+ const paneAttention = projectEntries.filter((entry) => entry.projectPane && projectPaneNeedsAttention(entry.projectPane)).length;
622
+ const panes = projectEntries.length > 0 ? `${projectEntries.length} pane${projectEntries.length === 1 ? "" : "s"}${paneAttention ? ` (${paneAttention} ⚠)` : ""}` : "";
623
+ const label = [agents, asyncRuns, panes].filter(Boolean).join(" · ");
592
624
  const detail = [showNativeSummary ? formatFleetTokens(tokens) : undefined, "↓/← to inspect"].filter(Boolean).join(" · ");
593
- return [truncateToWidth(` ${theme.fg("muted", label)} · ${theme.fg("dim", detail)}`, width)];
625
+ return [truncateToWidth(` ${theme.fg("muted", label)}${label && detail ? " · " : ""}${theme.fg("dim", detail)}`, width)];
594
626
  }
595
627
  const roster = this.rosterKeys();
596
628
  const selectedIndex = Math.max(0, roster.indexOf(this.selectedKey));
597
629
  const lines = [truncateToWidth(` ${theme.fg("dim", "↑↓/jk select · enter inspect · esc back")}`, width), ""];
598
630
  lines.push(truncateToWidth(` ${this.bullet(0, selectedIndex, theme)} main`, width));
599
631
 
600
- const tree = fleetTreeRows(this.entries);
632
+ const workEntries = this.entries.filter((entry) => !entry.surface);
633
+ const tree = fleetTreeRows(workEntries);
601
634
  const selectedTreeIndex = Math.max(0, tree.findIndex((row) => (row.kind === "owner" || row.kind === "child") && row.entry.key === this.selectedKey));
602
635
  const visibleCount = Math.min(this.maxAgentRows, tree.length);
603
636
  const start = selectedTreeIndex < visibleCount ? 0 : selectedTreeIndex - visibleCount + 1;
@@ -615,15 +648,30 @@ export class SubagentFleetStatus {
615
648
  }
616
649
  }
617
650
  if (hiddenBelow > 0) lines.push(rightAlign("", theme.fg("dim", `↓ ${hiddenBelow} more`), width));
651
+ this.renderProjectPaneSection(lines, selectedIndex, width, theme);
618
652
  return lines;
619
653
  }
620
654
 
655
+ private renderProjectPaneSection(lines: string[], selectedIndex: number, width: number, theme: Theme): void {
656
+ const entries = this.entries.filter((entry) => entry.surface === "project-pane");
657
+ if (!entries.length) return;
658
+ lines.push("", truncateToWidth(` ${theme.fg("dim", "project panes")}`, width));
659
+ for (const entry of entries) {
660
+ const rosterIndex = this.entries.findIndex((candidate) => candidate.key === entry.key) + 1;
661
+ lines.push(this.renderEntry(rosterIndex, selectedIndex, entry, width, theme));
662
+ }
663
+ }
664
+
665
+
621
666
  private renderEntry(rosterIndex: number, selectedIndex: number, entry: FleetStatusEntry, width: number, theme: Theme, branch?: string): string {
622
667
  const agent = entry.modelThinking ? `${entry.agent} (${entry.modelThinking})` : entry.agent;
623
668
  const prefix = branch ? ` ${branch}` : " ";
624
669
  const left = `${prefix} ${this.bullet(rosterIndex, selectedIndex, theme)} ${theme.fg("muted", agent)} · ${entry.state}`;
625
670
  const elapsed = Date.now() - entry.startedAt;
626
- const right = theme.fg("dim", entry.external ? formatFleetElapsed(elapsed) : `${formatFleetElapsed(elapsed)} · ${formatFleetTokens(entry.tokens)}`);
671
+ const rightText = entry.projectPane
672
+ ? `${entry.projectPane.summary ?? "—"} · ${formatFleetElapsed(Date.now() - entry.projectPane.refreshedAt)} ago`
673
+ : entry.external ? formatFleetElapsed(elapsed) : `${formatFleetElapsed(elapsed)} · ${formatFleetTokens(entry.tokens)}`;
674
+ const right = theme.fg("dim", rightText);
627
675
  return rightAlign(left, right, width);
628
676
  }
629
677
 
@@ -692,6 +740,7 @@ export class SubagentFleetStatus {
692
740
  entries: this.entries.map((entry) => this.active
693
741
  ? [
694
742
  entry.key,
743
+ entry.surface,
695
744
  entry.parentKey,
696
745
  entry.agent,
697
746
  entry.state,
@@ -719,10 +768,14 @@ export class SubagentFleetStatus {
719
768
  row.overflow,
720
769
  ]),
721
770
  ]
722
- : [entry.key, entry.state, entry.external, entry.tokens]),
771
+ : [entry.key, entry.state, entry.external, entry.surface, entry.tokens, entry.projectPane?.refreshedAt, entry.projectPane?.summary]),
723
772
  });
724
773
  }
725
774
 
775
+ private hasInlineSurface(): boolean {
776
+ return this.entries.length > 0 || Boolean(this.state.activeAsyncCapacity?.used);
777
+ }
778
+
726
779
  private getActiveUiContext(): ExtensionContext | undefined {
727
780
  const ctx = this.ctx;
728
781
  if (!ctx) return undefined;
package/src/tui/render.ts CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  type WorkflowNodeStatus,
19
19
  type MainWindowRendererConfig,
20
20
  MAX_WIDGET_JOBS,
21
- POLL_INTERVAL_MS,
21
+ WIDGET_ANIMATION_INTERVAL_MS,
22
22
  WIDGET_KEY,
23
23
  } from "../shared/types.ts";
24
24
  import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
@@ -1580,7 +1580,7 @@ function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (_tui:
1580
1580
  const container = new Container();
1581
1581
  container.render = (renderWidth: number): string[] => {
1582
1582
  const width = Math.max(0, renderWidth - 2);
1583
- const frame = Math.floor(Date.now() / POLL_INTERVAL_MS);
1583
+ const frame = Math.floor(Date.now() / WIDGET_ANIMATION_INTERVAL_MS);
1584
1584
  const buildLines = (): string[] => expanded
1585
1585
  ? buildWidgetLines(jobs, theme, width, true, frame)
1586
1586
  : jobs.length === 1
@@ -1746,8 +1746,9 @@ function workflowRowStateLabel(row: WorkflowChatProgressRow, theme: Theme): stri
1746
1746
  }
1747
1747
 
1748
1748
  function workflowOverallState(rows: WorkflowChatProgressRow[], hasTerminalValue: boolean, isError?: boolean): "running" | "complete" | "failed" | "paused" {
1749
- if (isError || rows.some((row) => row.state === "failed")) return "failed";
1749
+ if (rows.some((row) => row.state === "failed")) return "failed";
1750
1750
  if (rows.some((row) => row.state === "detached")) return "paused";
1751
+ if (isError) return "failed";
1751
1752
  if ((rows.length > 0 && rows.every((row) => row.state === "complete")) || hasTerminalValue) return "complete";
1752
1753
  return "running";
1753
1754
  }
@@ -1948,7 +1949,7 @@ export function renderSubagentResult(
1948
1949
  const layout = resolveMainWindowRenderLayout(rendererConfig);
1949
1950
  const compact = (component: Component): Component => capCompactMainWindowResult(component, layout, theme, !options.expanded);
1950
1951
  const d = result.details;
1951
- if (d?.mode === "workflow" && d.chatProgress?.mode === "live-card" && !result.isError && d.workflow?.value === undefined) {
1952
+ if (d?.mode === "workflow" && d.chatProgress?.mode === "live-card" && d.workflow?.value === undefined && (!result.isError || (d.workflow?.trace.length ?? 0) > 0)) {
1952
1953
  return compact(renderWorkflowChatProgress(d, result, theme, options.expanded ? resolveMainWindowRenderLayout() : layout, frame));
1953
1954
  }
1954
1955
  if (!d || !d.results.length) {