pi-crew 0.1.45 → 0.1.49

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 (178) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +5 -5
  3. package/agents/analyst.md +11 -11
  4. package/agents/critic.md +11 -11
  5. package/agents/executor.md +11 -11
  6. package/agents/explorer.md +11 -11
  7. package/agents/planner.md +11 -11
  8. package/agents/reviewer.md +11 -11
  9. package/agents/security-reviewer.md +11 -11
  10. package/agents/test-engineer.md +11 -11
  11. package/agents/verifier.md +11 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/next-upgrade-roadmap.md +808 -0
  14. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +261 -0
  15. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +111 -0
  16. package/docs/research/AUDIT_OH_MY_PI.md +261 -0
  17. package/docs/research/AUDIT_PI_CREW.md +457 -0
  18. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +281 -0
  19. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +264 -0
  20. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +343 -0
  21. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +480 -0
  22. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +354 -0
  23. package/docs/research/IMPLEMENTATION_PLAN.md +385 -0
  24. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +502 -0
  25. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +266 -0
  26. package/docs/research/REMAINING-GAPS-PLAN.md +363 -0
  27. package/docs/research/SESSION-SUMMARY-2026-05-08.md +146 -0
  28. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +173 -0
  29. package/docs/research-awesome-agent-skills-distillation.md +100 -0
  30. package/docs/research-oh-my-pi-distillation.md +369 -0
  31. package/docs/source-runtime-refactor-map.md +24 -0
  32. package/docs/usage.md +3 -3
  33. package/install.mjs +52 -8
  34. package/package.json +99 -98
  35. package/schema.json +10 -1
  36. package/skills/async-worker-recovery/SKILL.md +42 -0
  37. package/skills/context-artifact-hygiene/SKILL.md +52 -0
  38. package/skills/delegation-patterns/SKILL.md +54 -0
  39. package/skills/mailbox-interactive/SKILL.md +40 -0
  40. package/skills/model-routing-context/SKILL.md +39 -0
  41. package/skills/multi-perspective-review/SKILL.md +58 -0
  42. package/skills/observability-reliability/SKILL.md +41 -0
  43. package/skills/orchestration/SKILL.md +157 -0
  44. package/skills/ownership-session-security/SKILL.md +41 -0
  45. package/skills/pi-extension-lifecycle/SKILL.md +39 -0
  46. package/skills/requirements-to-task-packet/SKILL.md +63 -0
  47. package/skills/resource-discovery-config/SKILL.md +41 -0
  48. package/skills/runtime-state-reader/SKILL.md +44 -0
  49. package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
  50. package/skills/state-mutation-locking/SKILL.md +42 -0
  51. package/skills/systematic-debugging/SKILL.md +67 -0
  52. package/skills/ui-render-performance/SKILL.md +39 -0
  53. package/skills/verification-before-done/SKILL.md +57 -0
  54. package/skills/worktree-isolation/SKILL.md +39 -0
  55. package/src/agents/agent-config.ts +6 -0
  56. package/src/agents/agent-search.ts +98 -0
  57. package/src/agents/agent-serializer.ts +38 -34
  58. package/src/agents/discover-agents.ts +29 -15
  59. package/src/config/config.ts +72 -24
  60. package/src/config/defaults.ts +25 -0
  61. package/src/extension/autonomous-policy.ts +26 -33
  62. package/src/extension/help.ts +1 -0
  63. package/src/extension/management.ts +5 -0
  64. package/src/extension/project-init.ts +62 -2
  65. package/src/extension/register.ts +69 -22
  66. package/src/extension/registration/commands.ts +64 -25
  67. package/src/extension/registration/compaction-guard.ts +1 -1
  68. package/src/extension/registration/subagent-helpers.ts +8 -0
  69. package/src/extension/registration/subagent-tools.ts +149 -148
  70. package/src/extension/registration/team-tool.ts +14 -10
  71. package/src/extension/run-index.ts +35 -21
  72. package/src/extension/run-maintenance.ts +30 -5
  73. package/src/extension/team-tool/api.ts +47 -9
  74. package/src/extension/team-tool/cancel.ts +109 -5
  75. package/src/extension/team-tool/context.ts +8 -0
  76. package/src/extension/team-tool/intent-policy.ts +42 -0
  77. package/src/extension/team-tool/lifecycle-actions.ts +120 -79
  78. package/src/extension/team-tool/parallel-dispatch.ts +156 -0
  79. package/src/extension/team-tool/respond.ts +46 -18
  80. package/src/extension/team-tool/run.ts +55 -12
  81. package/src/extension/team-tool/status.ts +13 -2
  82. package/src/extension/team-tool-types.ts +3 -0
  83. package/src/extension/team-tool.ts +45 -14
  84. package/src/hooks/registry.ts +61 -0
  85. package/src/hooks/types.ts +41 -0
  86. package/src/observability/event-to-metric.ts +8 -1
  87. package/src/runtime/agent-control.ts +169 -63
  88. package/src/runtime/async-runner.ts +3 -1
  89. package/src/runtime/background-runner.ts +78 -53
  90. package/src/runtime/cancellation-token.ts +89 -0
  91. package/src/runtime/cancellation.ts +61 -0
  92. package/src/runtime/capability-inventory.ts +116 -0
  93. package/src/runtime/child-pi.ts +458 -444
  94. package/src/runtime/code-summary.ts +247 -0
  95. package/src/runtime/crash-recovery.ts +182 -0
  96. package/src/runtime/crew-agent-records.ts +70 -10
  97. package/src/runtime/crew-agent-runtime.ts +1 -0
  98. package/src/runtime/custom-tools/irc-tool.ts +201 -0
  99. package/src/runtime/custom-tools/submit-result-tool.ts +90 -0
  100. package/src/runtime/deadletter.ts +1 -0
  101. package/src/runtime/delivery-coordinator.ts +48 -25
  102. package/src/runtime/effectiveness.ts +81 -0
  103. package/src/runtime/event-stream-bridge.ts +90 -0
  104. package/src/runtime/live-agent-control.ts +2 -1
  105. package/src/runtime/live-agent-manager.ts +179 -85
  106. package/src/runtime/live-control-realtime.ts +1 -1
  107. package/src/runtime/live-extension-bridge.ts +150 -0
  108. package/src/runtime/live-irc.ts +92 -0
  109. package/src/runtime/live-session-health.ts +100 -0
  110. package/src/runtime/live-session-runtime.ts +599 -305
  111. package/src/runtime/manifest-cache.ts +17 -2
  112. package/src/runtime/mcp-proxy.ts +113 -0
  113. package/src/runtime/model-fallback.ts +6 -4
  114. package/src/runtime/notebook-helpers.ts +90 -0
  115. package/src/runtime/orphan-sentinel.ts +7 -0
  116. package/src/runtime/output-validator.ts +187 -0
  117. package/src/runtime/parallel-utils.ts +57 -0
  118. package/src/runtime/parent-guard.ts +80 -0
  119. package/src/runtime/pi-args.ts +18 -3
  120. package/src/runtime/process-status.ts +5 -1
  121. package/src/runtime/prose-compressor.ts +164 -0
  122. package/src/runtime/result-extractor.ts +121 -0
  123. package/src/runtime/retry-executor.ts +81 -64
  124. package/src/runtime/runtime-resolver.ts +23 -10
  125. package/src/runtime/semaphore.ts +131 -0
  126. package/src/runtime/sensitive-paths.ts +92 -0
  127. package/src/runtime/skill-instructions.ts +222 -0
  128. package/src/runtime/stale-reconciler.ts +4 -14
  129. package/src/runtime/stream-preview.ts +177 -0
  130. package/src/runtime/subagent-manager.ts +6 -2
  131. package/src/runtime/subprocess-tool-registry.ts +67 -0
  132. package/src/runtime/task-output-context.ts +177 -127
  133. package/src/runtime/task-runner/capabilities.ts +78 -0
  134. package/src/runtime/task-runner/live-executor.ts +107 -101
  135. package/src/runtime/task-runner/prompt-builder.ts +72 -8
  136. package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
  137. package/src/runtime/task-runner/run-projection.ts +104 -0
  138. package/src/runtime/task-runner.ts +115 -5
  139. package/src/runtime/team-runner.ts +134 -19
  140. package/src/runtime/workspace-tree.ts +298 -0
  141. package/src/runtime/yield-handler.ts +189 -0
  142. package/src/schema/config-schema.ts +7 -0
  143. package/src/schema/team-tool-schema.ts +14 -4
  144. package/src/skills/discover-skills.ts +67 -0
  145. package/src/state/active-run-registry.ts +167 -0
  146. package/src/state/artifact-store.ts +4 -1
  147. package/src/state/atomic-write.ts +50 -1
  148. package/src/state/blob-store.ts +117 -0
  149. package/src/state/contracts.ts +2 -1
  150. package/src/state/event-log-rotation.ts +158 -0
  151. package/src/state/event-log.ts +52 -2
  152. package/src/state/mailbox.ts +129 -9
  153. package/src/state/state-store.ts +32 -5
  154. package/src/state/types.ts +64 -2
  155. package/src/teams/team-config.ts +1 -0
  156. package/src/ui/agent-management-overlay.ts +144 -0
  157. package/src/ui/crew-widget.ts +15 -5
  158. package/src/ui/dashboard-panes/cancellation-pane.ts +43 -0
  159. package/src/ui/dashboard-panes/capability-pane.ts +60 -0
  160. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -11
  161. package/src/ui/dashboard-panes/progress-pane.ts +2 -0
  162. package/src/ui/live-run-sidebar.ts +4 -0
  163. package/src/ui/powerbar-publisher.ts +77 -15
  164. package/src/ui/render-coalescer.ts +51 -0
  165. package/src/ui/run-dashboard.ts +4 -0
  166. package/src/ui/run-event-bus.ts +209 -0
  167. package/src/ui/run-snapshot-cache.ts +78 -18
  168. package/src/ui/snapshot-types.ts +10 -0
  169. package/src/ui/transcript-entries.ts +258 -0
  170. package/src/utils/ids.ts +5 -0
  171. package/src/utils/incremental-reader.ts +104 -0
  172. package/src/utils/paths.ts +4 -2
  173. package/src/utils/scan-cache.ts +137 -0
  174. package/src/utils/sse-parser.ts +134 -0
  175. package/src/utils/task-name-generator.ts +337 -0
  176. package/src/utils/visual.ts +33 -2
  177. package/src/workflows/workflow-config.ts +1 -0
  178. package/src/worktree/cleanup.ts +2 -1
@@ -27,12 +27,15 @@ interface ManifestCacheEntry {
27
27
  manifestSize: number;
28
28
  tasksMtimeMs: number;
29
29
  tasksSize: number;
30
+ cachedAt?: number;
30
31
  }
31
32
 
33
+ const MANIFEST_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
32
34
  const manifestCache = new Map<string, ManifestCacheEntry>();
33
35
 
34
36
  function setManifestCache(stateRoot: string, entry: ManifestCacheEntry): void {
35
37
  if (manifestCache.has(stateRoot)) manifestCache.delete(stateRoot);
38
+ entry.cachedAt = Date.now();
36
39
  manifestCache.set(stateRoot, entry);
37
40
  while (manifestCache.size > DEFAULT_CACHE.manifestMaxEntries) {
38
41
  const oldest = manifestCache.keys().next().value;
@@ -196,7 +199,21 @@ export async function saveRunTasksAsync(manifest: TeamRunManifest, tasks: TeamTa
196
199
  invalidateRunCache(manifest.stateRoot);
197
200
  }
198
201
 
199
- export function updateRunStatus(manifest: TeamRunManifest, status: TeamRunManifest["status"], summary?: string): TeamRunManifest {
202
+ /** M8: Atomically save manifest + tasks and invalidate cache once to prevent stale reads between saves */
203
+ export async function saveManifestAndTasksAtomic(manifest: TeamRunManifest, tasks: TeamTaskState[]): Promise<void> {
204
+ await Promise.all([
205
+ atomicWriteJsonAsync(path.join(manifest.stateRoot, "manifest.json"), manifest),
206
+ atomicWriteJsonAsync(manifest.tasksPath, tasks),
207
+ ]);
208
+ invalidateRunCache(manifest.stateRoot);
209
+ }
210
+
211
+ export interface UpdateRunStatusOptions {
212
+ data?: Record<string, unknown>;
213
+ metadata?: Parameters<typeof appendEvent>[1]["metadata"];
214
+ }
215
+
216
+ export function updateRunStatus(manifest: TeamRunManifest, status: TeamRunManifest["status"], summary?: string, options: UpdateRunStatusOptions = {}): TeamRunManifest {
200
217
  if (!canTransitionRunStatus(manifest.status, status)) {
201
218
  throw new Error(`Invalid run status transition: ${manifest.status} -> ${status}`);
202
219
  }
@@ -206,11 +223,13 @@ export function updateRunStatus(manifest: TeamRunManifest, status: TeamRunManife
206
223
  type: `run.${status}`,
207
224
  runId: updated.runId,
208
225
  message: summary,
226
+ ...(options.data ? { data: options.data } : {}),
209
227
  metadata: {
210
228
  provenance: "team_runner",
211
229
  sessionIdentity: { title: updated.team, workspace: updated.cwd, purpose: updated.goal },
212
230
  ownership: { owner: updated.team, workflowScope: updated.workflow ?? "manual", watcherAction: "act" },
213
231
  confidence: "high",
232
+ ...options.metadata,
214
233
  },
215
234
  });
216
235
  return updated;
@@ -259,11 +278,15 @@ export function loadRunManifestById(cwd: string, runId: string): { manifest: Tea
259
278
  && cached.tasksMtimeMs === tasksMtimeMs
260
279
  && cached.tasksSize === (tasksStat?.size ?? 0)
261
280
  ) {
262
- if (!validateRunManifestPaths(cwd, runId, cached.manifest, stateRoot, tasksPath)) {
281
+ // TTL eviction: expire stale entries even if mtime matches
282
+ if (cached.cachedAt && Date.now() - cached.cachedAt > MANIFEST_CACHE_TTL_MS) {
283
+ manifestCache.delete(stateRoot);
284
+ } else if (!validateRunManifestPaths(cwd, runId, cached.manifest, stateRoot, tasksPath)) {
263
285
  manifestCache.delete(stateRoot);
264
286
  return undefined;
287
+ } else {
288
+ return { manifest: cached.manifest, tasks: cached.tasks };
265
289
  }
266
- return { manifest: cached.manifest, tasks: cached.tasks };
267
290
  }
268
291
 
269
292
  const manifest = readJsonFile<TeamRunManifest>(manifestPath);
@@ -300,11 +323,15 @@ export async function loadRunManifestByIdAsync(cwd: string, runId: string): Prom
300
323
  }
301
324
  const tasksMtimeMs = tasksStat?.mtimeMs ?? 0;
302
325
  if (cached && cached.manifestMtimeMs === manifestStat.mtimeMs && cached.manifestSize === manifestStat.size && cached.tasksMtimeMs === tasksMtimeMs && cached.tasksSize === (tasksStat?.size ?? 0)) {
303
- if (!validateRunManifestPaths(cwd, runId, cached.manifest, stateRoot, tasksPath)) {
326
+ // TTL eviction: expire stale entries even if mtime matches
327
+ if (cached.cachedAt && Date.now() - cached.cachedAt > MANIFEST_CACHE_TTL_MS) {
328
+ manifestCache.delete(stateRoot);
329
+ } else if (!validateRunManifestPaths(cwd, runId, cached.manifest, stateRoot, tasksPath)) {
304
330
  manifestCache.delete(stateRoot);
305
331
  return undefined;
332
+ } else {
333
+ return { manifest: cached.manifest, tasks: cached.tasks };
306
334
  }
307
- return { manifest: cached.manifest, tasks: cached.tasks };
308
335
  }
309
336
  const manifest = await readJsonFileAsync<TeamRunManifest>(manifestPath);
310
337
  if (!manifest || !validateRunManifestPaths(cwd, runId, manifest, stateRoot, tasksPath)) return undefined;
@@ -39,6 +39,17 @@ export interface VerificationEvidence {
39
39
  notes?: string;
40
40
  }
41
41
 
42
+ export interface TaskOutputSchema {
43
+ /** Output format expected from the worker */
44
+ format: "json" | "markdown" | "text";
45
+ /** JTD or JSON Schema for validating JSON output (only when format="json") */
46
+ schema?: Record<string, unknown>;
47
+ /** Human-readable description of expected output */
48
+ description?: string;
49
+ /** Example of valid output (for prompt guidance) */
50
+ example?: string;
51
+ }
52
+
42
53
  export interface TaskPacket {
43
54
  objective: string;
44
55
  scope: TaskScope;
@@ -53,10 +64,11 @@ export interface TaskPacket {
53
64
  constraints: string[];
54
65
  expectedArtifacts: string[];
55
66
  verification: VerificationContract;
67
+ outputSchema?: TaskOutputSchema;
56
68
  }
57
69
 
58
- export type PolicyDecisionAction = "retry" | "reassign" | "escalate" | "block" | "notify" | "cleanup" | "closeout";
59
- export type PolicyDecisionReason = "task_failed" | "worker_stale" | "green_unsatisfied" | "limit_exceeded" | "run_complete" | "mailbox_timeout" | "review_rejected" | "branch_stale" | "scope_mismatch";
70
+ export type PolicyDecisionAction = "retry" | "reassign" | "escalate" | "block" | "notify" | "cleanup" | "closeout" | "fail";
71
+ export type PolicyDecisionReason = "task_failed" | "worker_stale" | "green_unsatisfied" | "limit_exceeded" | "run_complete" | "mailbox_timeout" | "review_rejected" | "branch_stale" | "scope_mismatch" | "ineffective_worker";
60
72
 
61
73
  export interface PolicyDecision {
62
74
  action: PolicyDecisionAction;
@@ -81,6 +93,39 @@ export interface AsyncRunState {
81
93
  spawnedAt: string;
82
94
  }
83
95
 
96
+ export interface RuntimeResolutionState {
97
+ kind: "scaffold" | "child-process" | "live-session";
98
+ requestedMode: "auto" | "scaffold" | "child-process" | "live-session";
99
+ safety: "trusted" | "explicit_dry_run" | "blocked";
100
+ available: boolean;
101
+ fallback?: "scaffold" | "child-process" | "live-session";
102
+ reason?: string;
103
+ resolvedAt: string;
104
+ }
105
+
106
+ export interface WorkerExitStatus {
107
+ exitCode: number | null;
108
+ cancelled: boolean;
109
+ timedOut: boolean;
110
+ killed: boolean;
111
+ signal?: string;
112
+ cleanupErrors: string[];
113
+ finalDrainMs: number;
114
+ }
115
+
116
+ export interface OperationTerminalEvidence {
117
+ operation: "worker" | "tool" | "model";
118
+ status: "cancelled" | "failed" | "completed";
119
+ startedAt?: string;
120
+ finishedAt: string;
121
+ attemptId?: string;
122
+ reason?: {
123
+ code: string;
124
+ message: string;
125
+ };
126
+ exitStatus?: WorkerExitStatus;
127
+ }
128
+
84
129
  export interface PlanApprovalState {
85
130
  required: boolean;
86
131
  status: "pending" | "approved" | "cancelled";
@@ -125,6 +170,12 @@ export interface TeamRunManifest {
125
170
  planApproval?: PlanApprovalState;
126
171
  /** Pi session that created the run, when available. Used to prevent cross-session destructive actions. */
127
172
  ownerSessionId?: string;
173
+ /** pi-crew skill override selected when the run was created. false disables injected skill instructions. */
174
+ skillOverride?: string[] | false;
175
+ /** Resolved runtime/safety mode used for execution. Optional for backward compatibility with older manifests. */
176
+ runtimeResolution?: RuntimeResolutionState;
177
+ /** Effective run config snapshot used by async background workers. Optional for backward compatibility. */
178
+ runConfig?: unknown;
128
179
  summary?: string;
129
180
  policyDecisions?: PolicyDecision[];
130
181
  }
@@ -166,6 +217,7 @@ export interface TaskCheckpointState {
166
217
  }
167
218
 
168
219
  export interface TaskAttemptState {
220
+ attemptId?: string;
169
221
  startedAt: string;
170
222
  endedAt?: string;
171
223
  error?: string;
@@ -178,6 +230,7 @@ export interface TeamTaskState {
178
230
  role: string;
179
231
  agent: string;
180
232
  title: string;
233
+ displayName?: string;
181
234
  status: TeamTaskStatus;
182
235
  dependsOn: string[];
183
236
  cwd: string;
@@ -200,6 +253,8 @@ export interface TeamTaskState {
200
253
  heartbeat?: WorkerHeartbeatState;
201
254
  checkpoint?: TaskCheckpointState;
202
255
  attempts?: TaskAttemptState[];
256
+ workerExitStatus?: WorkerExitStatus;
257
+ terminalEvidence?: OperationTerminalEvidence[];
203
258
  taskPacket?: TaskPacket;
204
259
  verification?: VerificationEvidence;
205
260
  graph?: TaskGraphNode;
@@ -211,4 +266,11 @@ export interface TeamTaskState {
211
266
  retryCount?: number;
212
267
  lastDecision?: PolicyDecision;
213
268
  };
269
+ controlReservation?: ControlReservation;
270
+ }
271
+
272
+ export interface ControlReservation {
273
+ reservedAt: string;
274
+ controllerId: string;
275
+ acceptsControlEvents: boolean;
214
276
  }
@@ -5,6 +5,7 @@ export interface TeamRole {
5
5
  agent: string;
6
6
  description?: string;
7
7
  model?: string;
8
+ /** Additional skills for this role; false disables role-default injected skills for tasks using this role. */
8
9
  skills?: string[] | false;
9
10
  maxConcurrency?: number;
10
11
  }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Agent Management Overlay — displays discovered agents with their configuration.
3
+ * Read-only view of agent definitions from builtin/user/project sources.
4
+ * Future: enable/disable toggle, model override editing.
5
+ */
6
+ import type { AgentConfig, ResourceSource } from "../agents/agent-config.ts";
7
+ import { truncate } from "../utils/visual.ts";
8
+
9
+ export interface AgentEntry {
10
+ name: string;
11
+ description: string;
12
+ source: ResourceSource;
13
+ model?: string;
14
+ thinking?: string;
15
+ loadMode?: string;
16
+ contextMode?: string;
17
+ disabled?: boolean;
18
+ filePath: string;
19
+ }
20
+
21
+ export function agentToEntry(agent: AgentConfig): AgentEntry {
22
+ return {
23
+ name: agent.name,
24
+ description: agent.description,
25
+ source: agent.source,
26
+ model: agent.model,
27
+ thinking: agent.thinking,
28
+ loadMode: agent.loadMode,
29
+ contextMode: agent.contextMode,
30
+ disabled: agent.disabled,
31
+ filePath: agent.filePath,
32
+ };
33
+ }
34
+
35
+ function sourceIcon(source: ResourceSource): string {
36
+ switch (source) {
37
+ case "builtin": return "📦";
38
+ case "user": return "👤";
39
+ case "project": return "📂";
40
+ case "git": return "🔗";
41
+ }
42
+ }
43
+
44
+ function sourceLabel(source: ResourceSource): string {
45
+ switch (source) {
46
+ case "builtin": return "builtin";
47
+ case "user": return "user";
48
+ case "project": return "project";
49
+ case "git": return "git";
50
+ }
51
+ }
52
+
53
+ export interface AgentOverlayState {
54
+ entries: AgentEntry[];
55
+ selectedIndex: number;
56
+ scrollOffset: number;
57
+ expanded: Set<number>;
58
+ maxVisible: number;
59
+ }
60
+
61
+ export function createAgentOverlayState(entries: AgentEntry[], maxVisible = 20): AgentOverlayState {
62
+ return {
63
+ entries: entries.sort((a, b) => {
64
+ const order: Record<ResourceSource, number> = { project: 0, user: 1, git: 2, builtin: 3 };
65
+ const diff = (order[a.source] ?? 4) - (order[b.source] ?? 4);
66
+ return diff !== 0 ? diff : a.name.localeCompare(b.name);
67
+ }),
68
+ selectedIndex: 0,
69
+ scrollOffset: 0,
70
+ expanded: new Set(),
71
+ maxVisible,
72
+ };
73
+ }
74
+
75
+ export function moveSelection(state: AgentOverlayState, direction: -1 | 1): AgentOverlayState {
76
+ const next = Math.max(0, Math.min(state.entries.length - 1, state.selectedIndex + direction));
77
+ const visibleStart = state.scrollOffset;
78
+ const visibleEnd = state.scrollOffset + state.maxVisible;
79
+ const newScroll = next < visibleStart
80
+ ? next
81
+ : next >= visibleEnd
82
+ ? Math.max(0, next - state.maxVisible + 1)
83
+ : state.scrollOffset;
84
+ return { ...state, selectedIndex: next, scrollOffset: newScroll };
85
+ }
86
+
87
+ export function toggleExpand(state: AgentOverlayState): AgentOverlayState {
88
+ const expanded = new Set(state.expanded);
89
+ if (expanded.has(state.selectedIndex)) {
90
+ expanded.delete(state.selectedIndex);
91
+ } else {
92
+ expanded.add(state.selectedIndex);
93
+ }
94
+ return { ...state, expanded };
95
+ }
96
+
97
+ export function renderAgentOverlay(state: AgentOverlayState, width: number): string[] {
98
+ const lines: string[] = [];
99
+ const header = ` Agent Configuration (${state.entries.length} agents)`;
100
+ lines.push(truncate(header, width));
101
+ lines.push(truncate("─".repeat(Math.min(width, 60)), width));
102
+
103
+ if (state.entries.length === 0) {
104
+ lines.push(truncate(" No agents discovered.", width));
105
+ return lines;
106
+ }
107
+
108
+ const visible = state.entries.slice(
109
+ state.scrollOffset,
110
+ state.scrollOffset + state.maxVisible,
111
+ );
112
+
113
+ for (const [i, entry] of visible.entries()) {
114
+ const globalIndex = state.scrollOffset + i;
115
+ const isSelected = globalIndex === state.selectedIndex;
116
+ const isExpanded = state.expanded.has(globalIndex);
117
+ const cursor = isSelected ? "▸" : " ";
118
+ const disabled = entry.disabled ? " [disabled]" : "";
119
+ const model = entry.model ? ` (${entry.model})` : "";
120
+
121
+ const summary = `${cursor} ${sourceIcon(entry.source)} ${entry.name}${model}${disabled}`;
122
+ lines.push(truncate(summary, width));
123
+
124
+ if (isExpanded) {
125
+ const desc = ` ${entry.description}`;
126
+ lines.push(truncate(desc, width));
127
+ const meta: string[] = [` source: ${sourceLabel(entry.source)}`];
128
+ if (entry.model) meta.push(`model: ${entry.model}`);
129
+ if (entry.thinking) meta.push(`thinking: ${entry.thinking}`);
130
+ if (entry.loadMode) meta.push(`loadMode: ${entry.loadMode}`);
131
+ if (entry.contextMode) meta.push(`context: ${entry.contextMode}`);
132
+ meta.push(`file: ${entry.filePath}`);
133
+ lines.push(truncate(meta.join(" · "), width));
134
+ lines.push(truncate("─".repeat(Math.min(width - 4, 50)), width));
135
+ }
136
+ }
137
+
138
+ if (state.scrollOffset + state.maxVisible < state.entries.length) {
139
+ const remaining = state.entries.length - state.scrollOffset - state.maxVisible;
140
+ lines.push(truncate(` … +${remaining} more`, width));
141
+ }
142
+
143
+ return lines;
144
+ }
@@ -13,6 +13,8 @@ import { asCrewTheme, subscribeThemeChange } from "./theme-adapter.ts";
13
13
  import { Box, Text } from "./layout-primitives.ts";
14
14
  import { requestRender, setExtensionWidget } from "./pi-ui-compat.ts";
15
15
  import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
16
+ import { runEventBus } from "./run-event-bus.ts";
17
+ import { DEFAULT_UI } from "../config/defaults.ts";
16
18
 
17
19
  const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
18
20
  const TOOL_LABELS: Record<string, string> = {
@@ -28,7 +30,7 @@ const LEGACY_WIDGET_KEY = "pi-crew";
28
30
  const WIDGET_KEY = "pi-crew-active";
29
31
  const STATUS_KEY = "pi-crew";
30
32
 
31
- const MAX_LINES_DEFAULT = 10;
33
+ const MAX_LINES_DEFAULT = DEFAULT_UI.widgetMaxLines;
32
34
  const MAX_AGENTS_DISPLAY = 3;
33
35
 
34
36
  type WidgetComponent = { render(width: number): string[]; invalidate(): void };
@@ -77,7 +79,11 @@ function agentActivity(agent: CrewAgentRecord): string {
77
79
  if (recent) return recent.replace(/\s+/g, " ").trim();
78
80
  if (agent.progress?.activityState === "needs_attention") return "needs attention";
79
81
  if (agent.status === "queued") return "queued";
80
- if (agent.status === "running") return "thinking…";
82
+ if (agent.status === "running") {
83
+ const age = agent.startedAt ? Date.now() - new Date(agent.startedAt).getTime() : Infinity;
84
+ if (age < 5000 && !agent.progress?.currentTool) return "spawning…";
85
+ return "thinking…";
86
+ }
81
87
  if (agent.status === "failed") return agent.error ?? "failed";
82
88
  return "done";
83
89
  }
@@ -218,6 +224,7 @@ class CrewWidgetComponent implements WidgetComponent {
218
224
  private cachedBaseLines: string[] = [];
219
225
  private cachedTheme: CrewTheme;
220
226
  private readonly unsubscribeTheme: () => void;
227
+ private readonly unsubscribeEventBus: () => void;
221
228
 
222
229
  constructor(model: CrewWidgetModel, themeLike: unknown) {
223
230
  this.model = model;
@@ -225,6 +232,7 @@ class CrewWidgetComponent implements WidgetComponent {
225
232
  this.cachedTheme = this.theme;
226
233
  this.cacheSignature = "";
227
234
  this.unsubscribeTheme = subscribeThemeChange(themeLike, () => this.invalidate());
235
+ this.unsubscribeEventBus = runEventBus.onAny(() => this.invalidate());
228
236
  }
229
237
 
230
238
  private buildSignature(runs: WidgetRun[]): string {
@@ -249,6 +257,7 @@ class CrewWidgetComponent implements WidgetComponent {
249
257
 
250
258
  dispose(): void {
251
259
  this.unsubscribeTheme();
260
+ this.unsubscribeEventBus();
252
261
  }
253
262
 
254
263
  render(width: number): string[] {
@@ -273,7 +282,8 @@ class CrewWidgetComponent implements WidgetComponent {
273
282
  // Update only spinner and command icon on header line to avoid full re-color for every frame.
274
283
  const updatedHeader = `${runningGlyph}${this.cachedBaseLines[0]?.slice(1) ?? ""}`;
275
284
  this.cachedLines[0] = truncate(colorWidgetLine(updatedHeader, 0, this.theme), width);
276
- return this.cachedLines;
285
+ // Safety: ensure all lines fit within terminal width (handles emoji/CJK width mismatch)
286
+ return this.cachedLines.map((line) => truncate(line, width));
277
287
  }
278
288
  }
279
289
 
@@ -290,7 +300,7 @@ export function updateCrewWidget(
290
300
  const maxLines = config?.widgetMaxLines ?? MAX_LINES_DEFAULT;
291
301
  const runs = activeWidgetRuns(ctx.cwd, manifestCache, snapshotCache, preloadedManifests);
292
302
  const lines = buildCrewWidgetLines(ctx.cwd, state.frame, maxLines, runs, state.notificationCount ?? 0);
293
- const placement = config?.widgetPlacement ?? "aboveEditor";
303
+ const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
294
304
  ctx.ui.setStatus(STATUS_KEY, lines.length ? statusSummary(runs) : undefined);
295
305
  const shouldClearLegacy = state.legacyCleared !== true || state.lastPlacement !== placement;
296
306
  if (shouldClearLegacy) {
@@ -341,7 +351,7 @@ export function stopCrewWidget(ctx: Pick<ExtensionContext, "hasUI" | "ui"> | und
341
351
  if (state.interval) clearInterval(state.interval);
342
352
  state.interval = undefined;
343
353
  if (ctx?.hasUI) {
344
- const placement = config?.widgetPlacement ?? "aboveEditor";
354
+ const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
345
355
  ctx.ui.setStatus(STATUS_KEY, undefined);
346
356
  setExtensionWidget(ctx, LEGACY_WIDGET_KEY, undefined, { placement });
347
357
  setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement });
@@ -0,0 +1,43 @@
1
+ import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
2
+
3
+ export interface CancellationPaneOptions {
4
+ maxReasons?: number;
5
+ }
6
+
7
+ export function renderCancellationPane(manifest: TeamRunManifest, tasks: TeamTaskState[], opts: CancellationPaneOptions = {}): string[] {
8
+ const maxReasons = opts.maxReasons ?? 5;
9
+ if (manifest.status !== "cancelled" && manifest.status !== "blocked") {
10
+ const cancellingTasks = tasks.filter((t) => t.status === "cancelled");
11
+ if (cancellingTasks.length === 0) return ["Cancellation pane: no active cancellations"];
12
+ }
13
+
14
+ const lines: string[] = ["Cancellation pane"];
15
+
16
+ if (manifest.status === "cancelled") {
17
+ lines.push(` Run status: cancelled`);
18
+ } else if (manifest.status === "blocked") {
19
+ lines.push(` Run status: blocked`);
20
+ }
21
+
22
+ const cancelledTasks = tasks.filter((t) => t.status === "cancelled");
23
+ if (cancelledTasks.length > 0) {
24
+ lines.push(` Cancelled tasks (${cancelledTasks.length}):`);
25
+ for (const task of cancelledTasks.slice(0, maxReasons)) {
26
+ const reason = task.error ?? "unknown";
27
+ lines.push(` ✗ ${task.id}: ${reason}`);
28
+ }
29
+ if (cancelledTasks.length > maxReasons) {
30
+ lines.push(` ... and ${cancelledTasks.length - maxReasons} more`);
31
+ }
32
+ }
33
+
34
+ if (manifest.policyDecisions?.length) {
35
+ const decisions = manifest.policyDecisions.slice(0, maxReasons);
36
+ lines.push(` Policy decisions (${manifest.policyDecisions.length}):`);
37
+ for (const d of decisions) {
38
+ lines.push(` ${d.action}: ${d.message}`);
39
+ }
40
+ }
41
+
42
+ return lines;
43
+ }
@@ -0,0 +1,60 @@
1
+ import { buildCapabilityInventory } from "../../runtime/capability-inventory.ts";
2
+ import type { PiTeamsConfig } from "../../config/config.ts";
3
+ import type { CapabilityItem } from "../../runtime/capability-inventory.ts";
4
+
5
+ export interface CapabilityPaneOptions {
6
+ config?: PiTeamsConfig;
7
+ filter?: string;
8
+ }
9
+
10
+ function kindIcon(kind: string): string {
11
+ switch (kind) {
12
+ case "team": return "👥";
13
+ case "workflow": return "📋";
14
+ case "agent": return "🤖";
15
+ case "skill": return "🔧";
16
+ case "tool": return "🛠";
17
+ case "runtime": return "⚙";
18
+ default: return "•";
19
+ }
20
+ }
21
+
22
+ function stateLabel(state: string): string {
23
+ switch (state) {
24
+ case "active": return "";
25
+ case "disabled": return " [DISABLED]";
26
+ case "shadowed": return " [SHADOWED]";
27
+ case "missing": return " [MISSING]";
28
+ default: return "";
29
+ }
30
+ }
31
+
32
+ export function renderCapabilityPane(cwd: string, opts: CapabilityPaneOptions = {}): string[] {
33
+ const inventory = buildCapabilityInventory(cwd, opts.config);
34
+ const filtered = opts.filter
35
+ ? inventory.filter((item) => item.kind.includes(opts.filter!.toLowerCase()) || item.name.toLowerCase().includes(opts.filter!.toLowerCase()) || item.id.toLowerCase().includes(opts.filter!.toLowerCase()))
36
+ : inventory;
37
+
38
+ if (filtered.length === 0) return ["Capability pane: no items found"];
39
+
40
+ const byKind = new Map<string, CapabilityItem[]>();
41
+ for (const item of filtered) {
42
+ const group = byKind.get(item.kind) ?? [];
43
+ group.push(item);
44
+ byKind.set(item.kind, group);
45
+ }
46
+
47
+ const lines = [`Capability pane: ${filtered.length} item(s) (filter: ${opts.filter ?? "none"})`];
48
+ for (const [kind, items] of byKind) {
49
+ lines.push(` ${kindIcon(kind)} ${kind} (${items.length}):`);
50
+ for (const item of items.slice(0, 10)) {
51
+ const icon = item.state === "active" ? "✓" : "✗";
52
+ lines.push(` ${icon} ${item.name}${stateLabel(item.state)} [${item.source}]`);
53
+ }
54
+ if (items.length > 10) lines.push(` ... and ${items.length - 10} more`);
55
+ }
56
+
57
+ const disabled = filtered.filter((i) => i.state === "disabled").length;
58
+ if (disabled > 0) lines.push(` Disabled: ${disabled}`);
59
+ return lines;
60
+ }
@@ -1,11 +1,35 @@
1
- import type { RunUiSnapshot } from "../snapshot-types.ts";
2
-
3
- export function renderMailboxPane(snapshot: RunUiSnapshot | undefined): string[] {
4
- if (!snapshot) return ["Mailbox pane: snapshot unavailable"];
5
- const mailbox = snapshot.mailbox;
6
- const approx = mailbox.approximate ? " · approximate (tail)" : "";
7
- return [
8
- `Mailbox pane: inbox unread=${mailbox.inboxUnread} · outbox pending=${mailbox.outboxPending} · attention=${mailbox.needsAttention}${approx}`,
9
- mailbox.needsAttention > 0 ? "Needs attention: press Enter for detail · A ack · N nudge · C compose · X ack all." : "No mailbox items need attention. Press Enter for detail or C compose.",
10
- ];
11
- }
1
+ import type { RunUiSnapshot } from "../snapshot-types.ts";
2
+
3
+ export function renderMailboxPane(snapshot: RunUiSnapshot | undefined): string[] {
4
+ if (!snapshot) return ["Mailbox pane: snapshot unavailable"];
5
+ const mailbox = snapshot.mailbox;
6
+ const approx = mailbox.approximate ? " · approximate (tail)" : "";
7
+ const lines: string[] = [
8
+ `Mailbox pane: inbox unread=${mailbox.inboxUnread} · outbox pending=${mailbox.outboxPending} · attention=${mailbox.needsAttention}${approx}`,
9
+ ];
10
+ // Kind-separated breakdown
11
+ const kindParts: string[] = [];
12
+ const steer = mailbox.steerUnread ?? 0;
13
+ const followUp = mailbox.followUpUnread ?? 0;
14
+ const response = mailbox.responseUnread ?? 0;
15
+ const message = mailbox.messageUnread ?? 0;
16
+ if (steer > 0) kindParts.push(`steer=${steer}`);
17
+ if (followUp > 0) kindParts.push(`follow-up=${followUp}`);
18
+ if (response > 0) kindParts.push(`response=${response}`);
19
+ if (message > 0) kindParts.push(`message=${message}`);
20
+ if (kindParts.length > 0) {
21
+ lines.push(` Breakdown: ${kindParts.join(" · ")}`);
22
+ if (steer > 0) {
23
+ lines.push(" ⚠ Urgent: steering messages require immediate attention.");
24
+ }
25
+ if (followUp > 0) {
26
+ lines.push(` 📋 ${followUp} follow-up(s) pending review.`);
27
+ }
28
+ }
29
+ if (mailbox.needsAttention > 0) {
30
+ lines.push("Needs attention: press Enter for detail · A ack · N nudge · C compose · X ack all.");
31
+ } else {
32
+ lines.push("No mailbox items need attention. Press Enter for detail or C compose.");
33
+ }
34
+ return lines;
35
+ }
@@ -5,8 +5,10 @@ export function renderProgressPane(snapshot: RunUiSnapshot | undefined): string[
5
5
  const progress = snapshot.progress;
6
6
  const groupJoins = snapshot.groupJoins ?? [];
7
7
  const groupJoinLines = groupJoins.length ? groupJoins.map((item) => `group join ${item.partial ? "partial" : "completed"}: ${item.requestId} ack=${item.ack}`) : ["group joins: none"];
8
+ const cancellationLine = snapshot.cancellationReason ? [`cancelled: reason=${snapshot.cancellationReason}`] : [];
8
9
  return [
9
10
  `Progress pane: ${progress.completed}/${progress.total} completed · running=${progress.running} queued=${progress.queued} failed=${progress.failed}`,
11
+ ...cancellationLine,
10
12
  ...groupJoinLines,
11
13
  ...snapshot.recentEvents.slice(-10).map((event) => {
12
14
  const seq = event.metadata?.seq !== undefined ? `#${event.metadata.seq}` : "#?";
@@ -14,6 +14,7 @@ import { asCrewTheme, subscribeThemeChange } from "./theme-adapter.ts";
14
14
  import { Box, Text } from "./layout-primitives.ts";
15
15
  import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
16
16
  import { spinnerBucket, spinnerFrame } from "./spinner.ts";
17
+ import { runEventBus } from "./run-event-bus.ts";
17
18
 
18
19
  const TASK_READ_TTL_MS = 200;
19
20
 
@@ -59,6 +60,7 @@ export class LiveRunSidebar {
59
60
  private readonly theme: CrewTheme;
60
61
  private readonly config: CrewUiConfig;
61
62
  private readonly unsubscribeTheme: () => void;
63
+ private readonly unsubscribeEventBus: () => void;
62
64
  private readonly snapshotCache?: RunSnapshotCache;
63
65
  private cachedLines: string[] = [];
64
66
  private cachedWidth = 0;
@@ -72,6 +74,7 @@ export class LiveRunSidebar {
72
74
  this.config = input.config ?? {};
73
75
  this.snapshotCache = input.snapshotCache;
74
76
  this.unsubscribeTheme = subscribeThemeChange(input.theme, () => this.invalidate());
77
+ this.unsubscribeEventBus = runEventBus.onAny(() => this.invalidate());
75
78
  }
76
79
 
77
80
  private buildSignature(manifestStatus: string, tasks: TeamTaskState[], agents: ReturnType<typeof readCrewAgents>, waitingCount: number, snapshot?: RunUiSnapshot): string {
@@ -99,6 +102,7 @@ export class LiveRunSidebar {
99
102
 
100
103
  dispose(): void {
101
104
  this.unsubscribeTheme();
105
+ this.unsubscribeEventBus();
102
106
  }
103
107
 
104
108
  render(width: number): string[] {