pi-subagents 0.36.0 → 0.37.1

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 (54) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +111 -16
  3. package/agents/delegate.md +2 -0
  4. package/agents/planner.md +2 -1
  5. package/agents/worker.md +2 -0
  6. package/package.json +6 -4
  7. package/skills/pi-subagents/SKILL.md +18 -973
  8. package/skills/pi-subagents/references/constraints-and-recipes.md +256 -0
  9. package/skills/pi-subagents/references/execution-controls.md +411 -0
  10. package/skills/pi-subagents/references/management-authoring-rpc.md +140 -0
  11. package/skills/pi-subagents/references/prompting-and-roles.md +268 -0
  12. package/src/agents/agent-management.ts +7 -1
  13. package/src/agents/agents.ts +99 -12
  14. package/src/api/capability-ceiling.ts +17 -0
  15. package/src/api/delegation.ts +6 -1
  16. package/src/api/preflight.ts +399 -0
  17. package/src/extension/index.ts +16 -7
  18. package/src/extension/rpc.ts +29 -2
  19. package/src/extension/schemas.ts +10 -4
  20. package/src/extension/tool-description.ts +4 -2
  21. package/src/intercom/intercom-bridge.ts +5 -2
  22. package/src/runs/background/async-execution.ts +125 -14
  23. package/src/runs/background/async-job-tracker.ts +19 -12
  24. package/src/runs/background/async-resume.ts +27 -9
  25. package/src/runs/background/async-status.ts +18 -0
  26. package/src/runs/background/notify.ts +3 -0
  27. package/src/runs/background/process-terminal.ts +280 -0
  28. package/src/runs/background/result-watcher.ts +9 -3
  29. package/src/runs/background/run-status.ts +7 -1
  30. package/src/runs/background/scheduled-runs.ts +6 -1
  31. package/src/runs/background/stale-run-reconciler.ts +6 -0
  32. package/src/runs/background/subagent-runner.ts +182 -12
  33. package/src/runs/foreground/chain-execution.ts +5 -0
  34. package/src/runs/foreground/execution.ts +59 -13
  35. package/src/runs/foreground/subagent-executor.ts +142 -47
  36. package/src/runs/shared/acceptance.ts +41 -30
  37. package/src/runs/shared/capability-ceiling.ts +177 -0
  38. package/src/runs/shared/dynamic-fanout.ts +1 -1
  39. package/src/runs/shared/mcp-direct-tool-allowlist.ts +56 -17
  40. package/src/runs/shared/model-fallback.ts +8 -0
  41. package/src/runs/shared/nested-events.ts +8 -1
  42. package/src/runs/shared/parallel-utils.ts +5 -0
  43. package/src/runs/shared/pi-args.ts +141 -58
  44. package/src/runs/shared/session-lease.ts +25 -5
  45. package/src/runs/shared/subagent-prompt-runtime.ts +14 -1
  46. package/src/runs/shared/task-intent.ts +1 -1
  47. package/src/runs/shared/tool-availability.ts +18 -2
  48. package/src/shared/launch-contract.ts +123 -0
  49. package/src/shared/types.ts +118 -8
  50. package/src/shared/utils.ts +17 -42
  51. package/src/slash/delegation-adapters.ts +6 -1
  52. package/src/slash/slash-commands.ts +1 -1
  53. package/src/tui/fleet-status.ts +62 -16
  54. package/src/tui/fleet.ts +1 -1
@@ -9,6 +9,7 @@ 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
11
  import type { ModelScopeConfig } from "../runs/shared/model-scope.ts";
12
+ import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../runs/shared/capability-ceiling.ts";
12
13
 
13
14
  // ============================================================================
14
15
  // Basic Types
@@ -302,7 +303,7 @@ export interface ExecutionProjection {
302
303
  }
303
304
 
304
305
  export interface ReviewProjection {
305
- status: "not-requested" | "no-blockers" | "blockers" | "needs-parent-decision";
306
+ status: "not-requested" | "review-required" | "reviewed" | "blockers";
306
307
  findings?: AcceptanceReviewResult["findings"];
307
308
  }
308
309
 
@@ -317,9 +318,69 @@ export interface EffectsProjection {
317
318
  fileMutation?: FileMutationEffect;
318
319
  }
319
320
 
320
- export const SUBAGENT_LIFECYCLE_ARTIFACT_VERSION = 2;
321
+ export const SUBAGENT_LIFECYCLE_ARTIFACT_VERSION = 3;
321
322
  export type SubagentLifecycleArtifactVersion = typeof SUBAGENT_LIFECYCLE_ARTIFACT_VERSION;
322
323
 
324
+ export type ProcessTerminalState = "pending" | "observed" | "unknown" | "not-started";
325
+ export type ProcessTerminalReason =
326
+ | "observer-unavailable"
327
+ | "runner-candidate-missing"
328
+ | "runner-instance-mismatch"
329
+ | "writer-close-unverified"
330
+ | "canonical-session-unavailable"
331
+ | "canonical-session-lease-active"
332
+ | "canonical-session-release-unverified"
333
+ | "proof-write-failed"
334
+ | "stale-repair";
335
+
336
+ export interface RunnerProcessInstanceExitV1 {
337
+ processInstanceId: string;
338
+ kind: "runner";
339
+ closeObservedAt: number;
340
+ exitCode: number | null;
341
+ signal: string | null;
342
+ }
343
+
344
+ export interface PiWriterProcessInstanceExitV1 {
345
+ processInstanceId: string;
346
+ kind: "pi-writer";
347
+ attempt: number;
348
+ closeObservedAt: number;
349
+ exitCode: number | null;
350
+ signal: string | null;
351
+ }
352
+
353
+ export type ProcessInstanceExitV1 = RunnerProcessInstanceExitV1 | PiWriterProcessInstanceExitV1;
354
+
355
+ export interface CanonicalSessionTerminalV1 {
356
+ canonicalSessionId: string;
357
+ leaseDisposition: "released" | "not-held";
358
+ freeAtObservation: true;
359
+ canonicalSessionLeaseReleased?: true;
360
+ }
361
+
362
+ interface ProcessTerminalBaseV1 {
363
+ version: 1;
364
+ runId: string;
365
+ childIndex?: number;
366
+ runnerProcessInstanceId: string;
367
+ resumeDisposition?: "resumable" | "non-resumable" | "unavailable";
368
+ }
369
+
370
+ export type ProcessTerminalV1 =
371
+ | (ProcessTerminalBaseV1 & { state: "pending" | "not-started" })
372
+ | (ProcessTerminalBaseV1 & {
373
+ state: "observed";
374
+ observedAt: number;
375
+ instances: ProcessInstanceExitV1[];
376
+ canonicalSession?: CanonicalSessionTerminalV1;
377
+ })
378
+ | (ProcessTerminalBaseV1 & {
379
+ state: "unknown";
380
+ reason: ProcessTerminalReason;
381
+ diagnostic?: string;
382
+ });
383
+
323
384
  export type SteeringActionState = "delivered" | "scheduled" | "pending" | "partial" | "recovered" | "failed";
324
385
  export type SteeringTargetState = "scheduled" | "routed" | "delivered" | "late" | "failed" | "recovered";
325
386
 
@@ -384,6 +445,7 @@ export interface SteeringNotice {
384
445
 
385
446
  export interface SteeringRecoveryDescriptor {
386
447
  version: 1;
448
+ launchContractDigest?: string;
387
449
  sourceRunId: string;
388
450
  agentContract?: AgentContract;
389
451
  agent: string;
@@ -415,6 +477,7 @@ export interface SteeringRecoveryDescriptor {
415
477
  initialToolBudget?: ResolvedToolBudget;
416
478
  maxSubagentDepth: number;
417
479
  maxOutput?: MaxOutputConfig;
480
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
418
481
  share: boolean;
419
482
  sessionDir?: string;
420
483
  artifactsDir?: string;
@@ -532,7 +595,7 @@ export interface ModelAttempt {
532
595
  usage?: Usage;
533
596
  }
534
597
 
535
- export type AcceptanceLevel = "auto" | "none" | "attested" | "checked" | "verified" | "reviewed";
598
+ export type AcceptanceLevel = "auto" | "none" | "attested" | "checked" | "verified";
536
599
 
537
600
  export type AcceptanceEvidenceKind =
538
601
  | "changed-files"
@@ -641,7 +704,7 @@ export interface AcceptanceVerifyResult {
641
704
  }
642
705
 
643
706
  export interface AcceptanceReviewResult {
644
- status: "no-blockers" | "blockers" | "needs-parent-decision";
707
+ status: "review-required" | "reviewed" | "blockers";
645
708
  findings: Array<{
646
709
  severity: "blocker" | "non-blocking";
647
710
  file?: string;
@@ -650,19 +713,24 @@ export interface AcceptanceReviewResult {
650
713
  }>;
651
714
  }
652
715
 
653
- export type AcceptanceLedgerStatus =
716
+ export type AcceptanceEvidenceStatus =
654
717
  | "pending"
655
718
  | "not-required"
656
719
  | "claimed"
657
720
  | "attested"
658
721
  | "checked"
659
722
  | "verified"
660
- | "reviewed"
661
- | "accepted"
662
723
  | "rejected";
663
724
 
725
+ export type AcceptanceLedgerStatus =
726
+ | AcceptanceEvidenceStatus
727
+ | "review-required"
728
+ | "reviewed"
729
+ | "accepted";
730
+
664
731
  export interface AcceptanceLedger {
665
732
  status: AcceptanceLedgerStatus;
733
+ evidenceStatus: AcceptanceEvidenceStatus;
666
734
  explicit: boolean;
667
735
  effectiveAcceptance: ResolvedAcceptanceConfig;
668
736
  inferredReason: string[];
@@ -733,6 +801,7 @@ export interface SingleResult {
733
801
  structuredOutputSchemaPath?: string;
734
802
  acceptance?: AcceptanceLedger;
735
803
  agentContract?: AgentContract;
804
+ launchContractDigest?: string;
736
805
  execution?: ExecutionProjection;
737
806
  review?: ReviewProjection;
738
807
  effects?: EffectsProjection;
@@ -740,6 +809,8 @@ export interface SingleResult {
740
809
  transcriptError?: string;
741
810
  children?: NestedRunSummary[];
742
811
  watchdog?: ChildWatchdogProgress;
812
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
813
+ capabilityAudit?: SubagentCapabilityAudit;
743
814
  }
744
815
 
745
816
  export interface SpawnBudgetGrant {
@@ -799,7 +870,15 @@ export interface Details {
799
870
  // Aggregated cost across all agents in the run
800
871
  totalCost?: CostSummary;
801
872
  spawnBudget?: SpawnBudgetSnapshot;
873
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
874
+ capabilityAudit?: SubagentCapabilityAudit;
802
875
  parallelHandoff?: ParallelHandoffReference;
876
+ lifecycleStatus?: {
877
+ processTerminal?: ProcessTerminalV1;
878
+ };
879
+ launchContractDigest?: string;
880
+ /** Original launch contract whose persisted session is being revived. */
881
+ sourceLaunchContractDigest?: string;
803
882
  }
804
883
 
805
884
  // ============================================================================
@@ -873,6 +952,9 @@ export interface NestedStepSummary {
873
952
  wrapUpRequested?: boolean;
874
953
  toolBudget?: ToolBudgetState;
875
954
  toolBudgetBlocked?: boolean;
955
+ processTerminal?: ProcessTerminalV1;
956
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
957
+ capabilityAudit?: SubagentCapabilityAudit;
876
958
  children?: NestedRunSummary[];
877
959
  }
878
960
 
@@ -888,6 +970,9 @@ export interface NestedRunSummary extends NestedRunAddress {
888
970
  controlInbox?: string;
889
971
  capabilityToken?: string;
890
972
  mode?: SubagentRunMode;
973
+ processTerminal?: ProcessTerminalV1;
974
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
975
+ capabilityAudit?: SubagentCapabilityAudit;
891
976
  state: NestedRunState;
892
977
  agent?: string;
893
978
  agents?: string[];
@@ -946,10 +1031,12 @@ export interface AsyncStartedEvent {
946
1031
  chainStepCount?: number;
947
1032
  parallelGroups?: AsyncParallelGroupStatus[];
948
1033
  workflowGraph?: WorkflowGraphSnapshot;
1034
+ launchContractDigest?: string;
949
1035
  timeoutMs?: number;
950
1036
  deadlineAt?: number;
951
1037
  turnBudget?: TurnBudgetState;
952
1038
  nestedRoute?: NestedRouteInfo;
1039
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
953
1040
  }
954
1041
 
955
1042
  export interface AsyncStatus {
@@ -987,6 +1074,10 @@ export interface AsyncStatus {
987
1074
  pendingAppends?: number;
988
1075
  parallelGroups?: AsyncParallelGroupStatus[];
989
1076
  workflowGraph?: WorkflowGraphSnapshot;
1077
+ processTerminal?: ProcessTerminalV1;
1078
+ launchContractDigest?: string;
1079
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
1080
+ capabilityAudit?: SubagentCapabilityAudit;
990
1081
  steps?: Array<{
991
1082
  agent: string;
992
1083
  /** Resolved launch context for this child step. */
@@ -1035,10 +1126,14 @@ export interface AsyncStatus {
1035
1126
  structuredOutputSchemaPath?: string;
1036
1127
  acceptance?: AcceptanceLedger;
1037
1128
  agentContract?: AgentContract;
1129
+ launchContractDigest?: string;
1038
1130
  execution?: ExecutionProjection;
1039
1131
  review?: ReviewProjection;
1040
1132
  effects?: EffectsProjection;
1041
1133
  watchdog?: ChildWatchdogProgress;
1134
+ processTerminal?: ProcessTerminalV1;
1135
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
1136
+ capabilityAudit?: SubagentCapabilityAudit;
1042
1137
  }>;
1043
1138
  sessionDir?: string;
1044
1139
  outputFile?: string;
@@ -1109,6 +1204,8 @@ export interface ForegroundResumeChild {
1109
1204
  index: number;
1110
1205
  context?: "fresh" | "fork";
1111
1206
  sessionFile?: string;
1207
+ model?: string;
1208
+ thinking?: string;
1112
1209
  status: SubagentResultStatus;
1113
1210
  activityState?: ActivityState;
1114
1211
  lastActivityAt?: number;
@@ -1130,9 +1227,12 @@ export interface ForegroundResumeChild {
1130
1227
  detachedReason?: string;
1131
1228
  acceptance?: AcceptanceLedger;
1132
1229
  agentContract?: AgentContract;
1230
+ launchContractDigest?: string;
1133
1231
  execution?: ExecutionProjection;
1134
1232
  review?: ReviewProjection;
1135
1233
  effects?: EffectsProjection;
1234
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
1235
+ capabilityAudit?: SubagentCapabilityAudit;
1136
1236
  updatedAt?: number;
1137
1237
  }
1138
1238
 
@@ -1195,6 +1295,8 @@ export interface SubagentState {
1195
1295
  /** Runtime-owned artifact resolution inputs used by Fleet transcript targeting. */
1196
1296
  artifactDirPreference?: ArtifactDirPreference;
1197
1297
  parentSessionFile?: string | null;
1298
+ /** Last valid parent session model observed for this session; used when continuation contexts omit ctx.model. */
1299
+ lastParentModel?: { provider: string; id: string };
1198
1300
  subagentInProgress?: boolean;
1199
1301
  subagentSpawns?: {
1200
1302
  sessionId: string | null;
@@ -1252,6 +1354,7 @@ export const INTERCOM_DETACH_REQUEST_EVENT = "pi-intercom:detach-request";
1252
1354
  export const INTERCOM_DETACH_RESPONSE_EVENT = "pi-intercom:detach-response";
1253
1355
  export const SUBAGENT_ASYNC_STARTED_EVENT = "subagent:async-started";
1254
1356
  export const SUBAGENT_ASYNC_COMPLETE_EVENT = "subagent:async-complete";
1357
+ export const SUBAGENT_PROCESS_TERMINAL_EVENT = "subagent:process-terminal";
1255
1358
  export const SUBAGENT_FOREGROUND_COMPLETE_EVENT = "subagent:foreground-complete";
1256
1359
  export const SUBAGENT_CONTROL_EVENT = "subagent:control-event";
1257
1360
  export const SUBAGENT_CONTROL_INTERCOM_EVENT = "subagent:control-intercom";
@@ -1299,6 +1402,7 @@ export interface RunSyncOptions {
1299
1402
  maxSubagentDepth?: number;
1300
1403
  /** Effective parent wait-tool setting propagated to the child runtime. */
1301
1404
  waitToolEnabled?: boolean;
1405
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
1302
1406
  nestedRoute?: NestedRouteInfo;
1303
1407
  /** Override the agent's default model (format: "provider/id" or just "id") */
1304
1408
  modelOverride?: string;
@@ -1332,6 +1436,8 @@ export type IntercomBridgeMode = "off" | "fork-only" | "always";
1332
1436
  export interface IntercomBridgeConfig {
1333
1437
  mode?: IntercomBridgeMode;
1334
1438
  instructionFile?: string;
1439
+ /** Deliver grouped completion messages through an external acknowledged intercom listener. */
1440
+ resultDelivery?: boolean;
1335
1441
  }
1336
1442
 
1337
1443
  interface TopLevelParallelConfig {
@@ -1360,10 +1466,14 @@ export interface ScheduledRunsConfig {
1360
1466
  maxPending?: number;
1361
1467
  }
1362
1468
 
1469
+ export type FleetViewPlacement = "aboveEditor" | "belowEditor";
1470
+
1363
1471
  export interface ExtensionConfig {
1364
1472
  asyncByDefault?: boolean;
1365
- /** Show the Claude Code-style navigable fleet below the editor. Defaults to true. */
1473
+ /** Show the Claude Code-style navigable fleet. Defaults to true. */
1366
1474
  fleetView?: boolean;
1475
+ /** Place the persistent FleetView above or below the editor. Defaults to belowEditor. */
1476
+ fleetViewPlacement?: FleetViewPlacement;
1367
1477
  /** Show the legacy above-editor async runs widget. Defaults to true only when fleetView is disabled. */
1368
1478
  asyncWidget?: boolean;
1369
1479
  /** Tool description variant registered for the parent-facing subagent tool. Defaults to full. */
@@ -412,6 +412,14 @@ export function compactForegroundDetails(details: Details): Details {
412
412
  };
413
413
  }
414
414
 
415
+ export function hasEmptyTerminalAssistantResponse(messages: Message[]): boolean {
416
+ const lastAssistant = messages.findLast((message) => message.role === "assistant");
417
+ return lastAssistant?.role === "assistant"
418
+ && Array.isArray(lastAssistant.content)
419
+ && lastAssistant.content.length === 0
420
+ && lastAssistant.usage.output === 0;
421
+ }
422
+
415
423
  /**
416
424
  * Detect errors in subagent execution from messages (only errors with no subsequent success)
417
425
  */
@@ -438,50 +446,17 @@ export function detectSubagentError(messages: Message[]): ErrorInfo {
438
446
  const toolName = "toolName" in msg && typeof msg.toolName === "string" ? msg.toolName : undefined;
439
447
  const isError = "isError" in msg && msg.isError === true;
440
448
 
441
- if (isError) {
442
- const text = msg.content.find((c) => c.type === "text");
443
- const details = text && "text" in text ? text.text : undefined;
444
- const exitMatch = details?.match(/exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i);
445
- return {
446
- hasError: true,
447
- exitCode: exitMatch ? parseInt(exitMatch[1], 10) : 1,
448
- errorType: toolName || "tool",
449
- details: details?.slice(0, 200),
450
- };
451
- }
452
-
453
- if (toolName !== "bash") continue;
449
+ if (!isError) continue;
454
450
 
455
451
  const text = msg.content.find((c) => c.type === "text");
456
- if (!text || !("text" in text)) continue;
457
- const output = text.text;
458
-
459
- const exitMatch = output.match(/exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i);
460
- if (exitMatch) {
461
- const code = parseInt(exitMatch[1], 10);
462
- if (code !== 0) {
463
- return { hasError: true, exitCode: code, errorType: "bash", details: output.slice(0, 200) };
464
- }
465
- }
466
-
467
- // NOTE: These patterns can match legitimate output (grep results, logs,
468
- // testing). With the assistant-message check above, most false positives
469
- // are mitigated since the agent will have responded after routine errors.
470
- const fatalPatterns = [
471
- /command not found/i,
472
- /permission denied/i,
473
- /no such file or directory/i,
474
- /segmentation fault/i,
475
- /killed|terminated/i,
476
- /out of memory/i,
477
- /connection refused/i,
478
- /timeout/i,
479
- ];
480
- for (const pattern of fatalPatterns) {
481
- if (pattern.test(output)) {
482
- return { hasError: true, exitCode: 1, errorType: "bash", details: output.slice(0, 200) };
483
- }
484
- }
452
+ const details = text && "text" in text ? text.text : undefined;
453
+ const exitMatch = details?.match(/exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i);
454
+ return {
455
+ hasError: true,
456
+ exitCode: exitMatch ? parseInt(exitMatch[1], 10) : 1,
457
+ errorType: toolName || "tool",
458
+ details: details?.slice(0, 200),
459
+ };
485
460
  }
486
461
 
487
462
  return { hasError: false };
@@ -69,6 +69,7 @@ interface PromptTemplateDelegationTaskProgress {
69
69
 
70
70
  export interface PromptTemplateDelegationUpdate {
71
71
  requestId: string;
72
+ runId?: string;
72
73
  currentTool?: string;
73
74
  currentToolArgs?: string;
74
75
  recentOutput?: string;
@@ -323,6 +324,7 @@ export function toDelegationUpdate(requestId: string, update: PromptTemplateBrid
323
324
  : undefined;
324
325
  return {
325
326
  requestId,
327
+ ...(update.details?.runId ? { runId: update.details.runId } : {}),
326
328
  currentTool: progress?.currentTool,
327
329
  currentToolArgs: progress?.currentToolArgs,
328
330
  recentOutput: safeLastOutput,
@@ -414,6 +416,7 @@ export function toSubagentDelegationUpdate(requestId: string, result: PromptTemp
414
416
  return {
415
417
  version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
416
418
  requestId,
419
+ ...(legacy.runId ? { runId: legacy.runId } : {}),
417
420
  ...(legacy.currentTool ? { currentTool: legacy.currentTool } : {}),
418
421
  ...(legacy.currentToolArgs ? { currentToolArgs: legacy.currentToolArgs } : {}),
419
422
  ...(legacy.recentOutput ? { recentOutput: legacy.recentOutput } : {}),
@@ -437,6 +440,7 @@ export function toSubagentDelegationV2Update(
437
440
  requestId: request.requestId,
438
441
  ownerRunId: request.ownerRunId,
439
442
  nodeId: request.nodeId,
443
+ ...(legacy.runId ? { runId: legacy.runId } : {}),
440
444
  ...(legacy.currentTool ? { currentTool: legacy.currentTool } : {}),
441
445
  ...(legacy.currentToolArgs ? { currentToolArgs: legacy.currentToolArgs } : {}),
442
446
  ...(legacy.recentOutput ? { recentOutput: legacy.recentOutput } : {}),
@@ -491,7 +495,7 @@ export function toSubagentDelegationResponse(
491
495
  ...(child?.finalOutput ? { output: child.finalOutput } : {}),
492
496
  ...(child?.savedOutputPath ? { outputPath: child.savedOutputPath } : {}),
493
497
  ...(child?.sessionFile ? { sessionFile: child.sessionFile } : {}),
494
- ...(child?.acceptance ? { acceptance: { status: child.acceptance.status, explicit: child.acceptance.explicit } } : {}),
498
+ ...(child?.acceptance ? { acceptance: { status: child.acceptance.status, evidenceStatus: child.acceptance.evidenceStatus, explicit: child.acceptance.explicit } } : {}),
495
499
  ...(child?.review ? { review: child.review as SubagentDelegationReviewResult } : {}),
496
500
  ...(child?.effects ? { effects: child.effects as SubagentDelegationEffectsResult } : {}),
497
501
  ...(typeof child?.usage?.turns === "number" ? { turns: child.usage.turns } : {}),
@@ -553,6 +557,7 @@ export function toSubagentDelegationV2Response(
553
557
  ...(child?.model ? { model: child.model } : {}),
554
558
  ...(child?.thinking ? { thinking: child.thinking } : {}),
555
559
  ...(typeof child?.exitCode === "number" ? { exitCode: child.exitCode } : {}),
560
+ ...(child?.launchContractDigest ? { launchContractDigest: child.launchContractDigest } : {}),
556
561
  ...(projectedResult ? { result: projectedResult } : {}),
557
562
  ...(usage ? {
558
563
  usage: {
@@ -1008,7 +1008,7 @@ function validateInlineAcceptanceInput(value: string, agent: string): void {
1008
1008
  const errors = validateAcceptanceInput(value, `acceptance for step '${agent}'`);
1009
1009
  if (errors.length > 0) throw new SlashParseError(errors[0]!);
1010
1010
  if (!INLINE_ACCEPTANCE_LEVELS.has(value)) {
1011
- throw new SlashParseError(`Inline acceptance for step '${agent}' supports auto, attested, or checked. Use the subagent tool API or a saved .chain.json file for none or verified acceptance contracts; reviewed is inferred-only.`);
1011
+ throw new SlashParseError(`Inline acceptance for step '${agent}' supports auto, attested, or checked. Use the subagent tool API or a saved .chain.json file for none, verified, or review requirements; reviewed is an achieved status, not an input level.`);
1012
1012
  }
1013
1013
  }
1014
1014
 
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { Editor, isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
- import type { AsyncJobStep, SubagentState } from "../shared/types.ts";
3
+ import type { AsyncJobStep, FleetViewPlacement, SubagentState } from "../shared/types.ts";
4
4
 
5
5
  export const FLEET_STATUS_WIDGET_KEY = "subagent-fleet-status";
6
6
 
@@ -22,6 +22,11 @@ type FleetStatusEntry = {
22
22
  export interface FleetStatusOptions {
23
23
  refreshMs?: number;
24
24
  maxAgentRows?: number;
25
+ placement?: FleetViewPlacement;
26
+ }
27
+
28
+ export function resolveFleetViewPlacement(value: unknown): FleetViewPlacement {
29
+ return value === "aboveEditor" ? "aboveEditor" : "belowEditor";
25
30
  }
26
31
 
27
32
  export function formatFleetElapsed(ms: number): string {
@@ -48,6 +53,13 @@ function isActiveState(value: string): boolean {
48
53
  return value === "running" || value === "queued" || value === "pending";
49
54
  }
50
55
 
56
+ function isStaleExtensionContextError(error: unknown): boolean {
57
+ // Pi currently exposes stale contexts as plain Errors without a stable code or subtype.
58
+ return error instanceof Error
59
+ && (error.message.includes("This extension ctx is stale")
60
+ || error.message.includes("Extension context no longer active"));
61
+ }
62
+
51
63
  export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntry[] {
52
64
  const entries: FleetStatusEntry[] = [];
53
65
  for (const control of state.foregroundControls.values()) {
@@ -111,6 +123,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
111
123
 
112
124
  export class SubagentFleetStatus {
113
125
  private ctx: ExtensionContext | undefined;
126
+ private ui: ExtensionContext["ui"] | undefined;
114
127
  private tui: FleetStatusTui | undefined;
115
128
  private inputUnsubscribe: (() => void) | undefined;
116
129
  private timer: ReturnType<typeof setInterval> | undefined;
@@ -124,6 +137,7 @@ export class SubagentFleetStatus {
124
137
  private readonly openInspector: (itemKey: string) => Promise<void> | void;
125
138
  private readonly refreshMs: number;
126
139
  private readonly maxAgentRows: number;
140
+ private readonly placement: FleetViewPlacement;
127
141
 
128
142
  constructor(
129
143
  state: SubagentState,
@@ -134,19 +148,22 @@ export class SubagentFleetStatus {
134
148
  this.openInspector = openInspector;
135
149
  this.refreshMs = options.refreshMs ?? REFRESH_MS;
136
150
  this.maxAgentRows = options.maxAgentRows ?? MAX_AGENT_ROWS;
151
+ this.placement = options.placement ?? "belowEditor";
137
152
  }
138
153
 
139
154
  setContext(ctx: ExtensionContext): void {
140
155
  if (!ctx.hasUI) return;
141
- if (this.ctx?.ui === ctx.ui) {
156
+ const ui = ctx.ui;
157
+ if (this.ui === ui) {
142
158
  this.ctx = ctx;
143
159
  this.refresh();
144
160
  return;
145
161
  }
146
162
  this.clearUiRegistration();
147
163
  this.ctx = ctx;
148
- if (typeof ctx.ui.onTerminalInput === "function") {
149
- this.inputUnsubscribe = ctx.ui.onTerminalInput((data) => this.handleKey(data));
164
+ this.ui = ui;
165
+ if (typeof ui.onTerminalInput === "function") {
166
+ this.inputUnsubscribe = ui.onTerminalInput((data) => this.handleKey(data));
150
167
  }
151
168
  this.timer = setInterval(() => this.refresh(), this.refreshMs);
152
169
  this.timer.unref?.();
@@ -156,6 +173,7 @@ export class SubagentFleetStatus {
156
173
  dispose(): void {
157
174
  this.clearUiRegistration();
158
175
  this.ctx = undefined;
176
+ this.ui = undefined;
159
177
  this.entries = [];
160
178
  this.active = false;
161
179
  this.selectedKey = "main";
@@ -164,8 +182,8 @@ export class SubagentFleetStatus {
164
182
  }
165
183
 
166
184
  refresh(): void {
167
- const ctx = this.ctx;
168
- if (!ctx?.hasUI) return;
185
+ const ctx = this.getActiveUiContext();
186
+ if (!ctx) return;
169
187
  this.entries = collectFleetStatusEntries(this.state);
170
188
  this.clampSelection();
171
189
  if (this.inspectorOpen || this.state.fleetInspectorOpen) {
@@ -204,7 +222,7 @@ export class SubagentFleetStatus {
204
222
  this.tui = undefined;
205
223
  },
206
224
  };
207
- }, { placement: "belowEditor" });
225
+ }, { placement: this.placement });
208
226
  this.widgetRegistered = true;
209
227
  this.lastRenderKey = renderKey;
210
228
  return;
@@ -215,8 +233,8 @@ export class SubagentFleetStatus {
215
233
  }
216
234
 
217
235
  handleKey(data: string): { consume?: boolean; data?: string } | undefined {
218
- const ctx = this.ctx;
219
- if (!ctx?.hasUI || this.entries.length === 0 || isKeyRelease(data)) return undefined;
236
+ const ctx = this.getActiveUiContext();
237
+ if (!ctx || this.entries.length === 0 || isKeyRelease(data)) return undefined;
220
238
  if (this.inspectorOpen) return undefined;
221
239
  if (!this.editorHasFocus()) {
222
240
  if (this.active) this.deactivate();
@@ -344,19 +362,47 @@ export class SubagentFleetStatus {
344
362
  });
345
363
  }
346
364
 
365
+ private getActiveUiContext(): ExtensionContext | undefined {
366
+ const ctx = this.ctx;
367
+ if (!ctx) return undefined;
368
+ try {
369
+ return ctx.hasUI ? ctx : undefined;
370
+ } catch (error) {
371
+ if (!isStaleExtensionContextError(error)) throw error;
372
+ this.clearUiRegistration();
373
+ return undefined;
374
+ }
375
+ }
376
+
347
377
  private clearUiRegistration(): void {
348
378
  if (this.timer) clearInterval(this.timer);
349
379
  this.timer = undefined;
350
- this.inputUnsubscribe?.();
380
+
381
+ const inputUnsubscribe = this.inputUnsubscribe;
382
+ const ui = this.ui;
383
+ const widgetRegistered = this.widgetRegistered;
351
384
  this.inputUnsubscribe = undefined;
352
- if (this.ctx?.hasUI && this.widgetRegistered) {
385
+ this.ctx = undefined;
386
+ this.ui = undefined;
387
+ this.widgetRegistered = false;
388
+ this.tui = undefined;
389
+
390
+ const cleanupErrors: unknown[] = [];
391
+ try {
392
+ inputUnsubscribe?.();
393
+ } catch (error) {
394
+ if (!isStaleExtensionContextError(error)) cleanupErrors.push(error);
395
+ }
396
+ if (ui && widgetRegistered) {
353
397
  try {
354
- this.ctx.ui.setWidget(FLEET_STATUS_WIDGET_KEY, undefined);
355
- } catch {
356
- // The previous extension context may already be stale during reload/session replacement.
398
+ ui.setWidget(FLEET_STATUS_WIDGET_KEY, undefined);
399
+ } catch (error) {
400
+ if (!isStaleExtensionContextError(error)) cleanupErrors.push(error);
357
401
  }
358
402
  }
359
- this.widgetRegistered = false;
360
- this.tui = undefined;
403
+ if (cleanupErrors.length === 1) throw cleanupErrors[0];
404
+ if (cleanupErrors.length > 1) {
405
+ throw new AggregateError(cleanupErrors, "Failed to clean up FleetView UI registration");
406
+ }
361
407
  }
362
408
  }
package/src/tui/fleet.ts CHANGED
@@ -463,7 +463,7 @@ export class SubagentFleetComponent implements Component {
463
463
  this.refresh();
464
464
  this.timer = setInterval(() => {
465
465
  if (this.disposed) return;
466
- this.refresh();
466
+ this.invalidate();
467
467
  this.tui.requestRender();
468
468
  }, options.refreshMs ?? REFRESH_MS);
469
469
  this.timer.unref?.();