pi-crew 0.1.41 → 0.1.44

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 (191) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +51 -0
  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/refactor-tasks-phase3.md +394 -394
  14. package/docs/refactor-tasks-phase4.md +564 -564
  15. package/docs/refactor-tasks-phase5.md +402 -402
  16. package/docs/refactor-tasks-phase6.md +662 -662
  17. package/docs/research-extension-examples.md +297 -297
  18. package/docs/research-extension-system.md +324 -324
  19. package/docs/research-optimization-plan.md +548 -548
  20. package/docs/research-phase10-distillation.md +199 -0
  21. package/docs/research-phase11-distillation.md +201 -0
  22. package/docs/research-pi-coding-agent.md +357 -357
  23. package/docs/research-source-pi-crew-reference.md +174 -174
  24. package/docs/runtime-flow.md +148 -148
  25. package/docs/source-runtime-refactor-map.md +83 -83
  26. package/index.ts +6 -6
  27. package/package.json +1 -1
  28. package/src/agents/agent-serializer.ts +34 -34
  29. package/src/agents/discover-agents.ts +5 -4
  30. package/src/config/config.ts +28 -4
  31. package/src/extension/cross-extension-rpc.ts +82 -82
  32. package/src/extension/management.ts +37 -8
  33. package/src/extension/notification-router.ts +2 -2
  34. package/src/extension/register.ts +130 -8
  35. package/src/extension/registration/commands.ts +11 -9
  36. package/src/extension/registration/compaction-guard.ts +125 -125
  37. package/src/extension/registration/subagent-tools.ts +28 -19
  38. package/src/extension/registration/team-tool.ts +2 -1
  39. package/src/extension/result-watcher.ts +4 -4
  40. package/src/extension/run-bundle-schema.ts +8 -4
  41. package/src/extension/run-import.ts +4 -0
  42. package/src/extension/run-index.ts +23 -1
  43. package/src/extension/run-maintenance.ts +43 -24
  44. package/src/extension/team-tool/api.ts +2 -2
  45. package/src/extension/team-tool/cancel.ts +76 -4
  46. package/src/extension/team-tool/context.ts +1 -0
  47. package/src/extension/team-tool/doctor.ts +8 -1
  48. package/src/extension/team-tool/handle-settings.ts +188 -0
  49. package/src/extension/team-tool/inspect.ts +41 -41
  50. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  51. package/src/extension/team-tool/plan.ts +19 -19
  52. package/src/extension/team-tool/respond.ts +67 -0
  53. package/src/extension/team-tool/run.ts +6 -4
  54. package/src/extension/team-tool/status.ts +99 -93
  55. package/src/extension/team-tool-types.ts +4 -0
  56. package/src/extension/team-tool.ts +5 -1
  57. package/src/i18n.ts +184 -0
  58. package/src/observability/correlation.ts +2 -2
  59. package/src/observability/event-to-metric.ts +10 -3
  60. package/src/observability/exporters/adapter.ts +7 -1
  61. package/src/observability/exporters/otlp-exporter.ts +14 -2
  62. package/src/observability/exporters/prometheus-exporter.ts +9 -2
  63. package/src/observability/metric-registry.ts +18 -3
  64. package/src/observability/metric-retention.ts +11 -3
  65. package/src/observability/metric-sink.ts +9 -4
  66. package/src/observability/metrics-primitives.ts +4 -3
  67. package/src/prompt/prompt-runtime.ts +72 -68
  68. package/src/runtime/agent-control.ts +63 -63
  69. package/src/runtime/agent-memory.ts +72 -72
  70. package/src/runtime/agent-observability.ts +114 -114
  71. package/src/runtime/async-marker.ts +26 -26
  72. package/src/runtime/attention-events.ts +28 -23
  73. package/src/runtime/background-runner.ts +53 -53
  74. package/src/runtime/child-pi.ts +4 -4
  75. package/src/runtime/completion-guard.ts +95 -4
  76. package/src/runtime/concurrency.ts +1 -1
  77. package/src/runtime/crash-recovery.ts +32 -1
  78. package/src/runtime/crew-agent-runtime.ts +59 -58
  79. package/src/runtime/deadletter.ts +14 -4
  80. package/src/runtime/delivery-coordinator.ts +143 -0
  81. package/src/runtime/direct-run.ts +35 -35
  82. package/src/runtime/foreground-control.ts +82 -82
  83. package/src/runtime/green-contract.ts +46 -46
  84. package/src/runtime/group-join.ts +106 -106
  85. package/src/runtime/heartbeat-gradient.ts +28 -28
  86. package/src/runtime/heartbeat-watcher.ts +48 -4
  87. package/src/runtime/live-agent-control.ts +87 -87
  88. package/src/runtime/live-agent-manager.ts +85 -85
  89. package/src/runtime/live-control-realtime.ts +36 -36
  90. package/src/runtime/live-session-runtime.ts +305 -305
  91. package/src/runtime/manifest-cache.ts +2 -2
  92. package/src/runtime/model-fallback.ts +272 -261
  93. package/src/runtime/overflow-recovery.ts +157 -0
  94. package/src/runtime/parallel-research.ts +44 -44
  95. package/src/runtime/parallel-utils.ts +1 -1
  96. package/src/runtime/pi-json-output.ts +111 -111
  97. package/src/runtime/policy-engine.ts +79 -78
  98. package/src/runtime/post-exit-stdio-guard.ts +2 -2
  99. package/src/runtime/process-status.ts +56 -56
  100. package/src/runtime/progress-event-coalescer.ts +43 -43
  101. package/src/runtime/recovery-recipes.ts +74 -74
  102. package/src/runtime/retry-executor.ts +5 -0
  103. package/src/runtime/role-permission.ts +39 -39
  104. package/src/runtime/runtime-resolver.ts +1 -1
  105. package/src/runtime/session-resources.ts +25 -0
  106. package/src/runtime/session-snapshot.ts +59 -0
  107. package/src/runtime/session-usage.ts +79 -79
  108. package/src/runtime/sidechain-output.ts +29 -29
  109. package/src/runtime/stale-reconciler.ts +179 -0
  110. package/src/runtime/subagent-manager.ts +3 -3
  111. package/src/runtime/supervisor-contact.ts +59 -0
  112. package/src/runtime/task-display.ts +38 -38
  113. package/src/runtime/task-output-context.ts +127 -127
  114. package/src/runtime/task-runner/live-executor.ts +101 -101
  115. package/src/runtime/task-runner/progress.ts +119 -111
  116. package/src/runtime/task-runner/result-utils.ts +14 -14
  117. package/src/runtime/task-runner/state-helpers.ts +22 -22
  118. package/src/runtime/task-runner.ts +14 -0
  119. package/src/runtime/team-runner.ts +9 -10
  120. package/src/runtime/worker-heartbeat.ts +21 -21
  121. package/src/runtime/worker-startup.ts +57 -57
  122. package/src/schema/config-schema.ts +2 -1
  123. package/src/schema/team-tool-schema.ts +115 -109
  124. package/src/state/artifact-store.ts +4 -2
  125. package/src/state/atomic-write.ts +12 -4
  126. package/src/state/contracts.ts +109 -105
  127. package/src/state/event-log.ts +3 -4
  128. package/src/state/jsonl-writer.ts +4 -1
  129. package/src/state/locks.ts +9 -1
  130. package/src/state/task-claims.ts +44 -42
  131. package/src/state/usage.ts +29 -29
  132. package/src/subagents/async-entry.ts +1 -1
  133. package/src/subagents/index.ts +3 -3
  134. package/src/subagents/live/control.ts +1 -1
  135. package/src/subagents/live/manager.ts +1 -1
  136. package/src/subagents/live/realtime.ts +1 -1
  137. package/src/subagents/live/session-runtime.ts +1 -1
  138. package/src/subagents/manager.ts +1 -1
  139. package/src/subagents/spawn.ts +1 -1
  140. package/src/teams/discover-teams.ts +2 -2
  141. package/src/teams/team-serializer.ts +38 -38
  142. package/src/types/diff.d.ts +18 -18
  143. package/src/ui/crew-footer.ts +101 -101
  144. package/src/ui/crew-select-list.ts +111 -111
  145. package/src/ui/crew-widget.ts +5 -4
  146. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  147. package/src/ui/dynamic-border.ts +25 -25
  148. package/src/ui/layout-primitives.ts +106 -106
  149. package/src/ui/live-run-sidebar.ts +1 -1
  150. package/src/ui/loaders.ts +158 -158
  151. package/src/ui/mascot.ts +3 -2
  152. package/src/ui/powerbar-publisher.ts +7 -6
  153. package/src/ui/render-diff.ts +119 -119
  154. package/src/ui/render-scheduler.ts +54 -14
  155. package/src/ui/run-dashboard.ts +39 -11
  156. package/src/ui/run-snapshot-cache.ts +336 -36
  157. package/src/ui/spinner.ts +17 -17
  158. package/src/ui/status-colors.ts +58 -54
  159. package/src/ui/syntax-highlight.ts +116 -116
  160. package/src/ui/theme-adapter.ts +1 -1
  161. package/src/ui/transcript-viewer.ts +7 -2
  162. package/src/utils/atomic-write.ts +33 -0
  163. package/src/utils/completion-dedupe.ts +63 -63
  164. package/src/utils/file-coalescer.ts +5 -3
  165. package/src/utils/frontmatter.ts +68 -36
  166. package/src/utils/git.ts +262 -262
  167. package/src/utils/ids.ts +12 -12
  168. package/src/utils/internal-error.ts +1 -1
  169. package/src/utils/names.ts +27 -26
  170. package/src/utils/paths.ts +1 -1
  171. package/src/utils/redaction.ts +44 -41
  172. package/src/utils/safe-paths.ts +47 -34
  173. package/src/utils/sleep.ts +2 -2
  174. package/src/utils/timings.ts +2 -0
  175. package/src/utils/visual.ts +9 -1
  176. package/src/workflows/discover-workflows.ts +4 -1
  177. package/src/workflows/validate-workflow.ts +40 -40
  178. package/src/worktree/branch-freshness.ts +45 -45
  179. package/src/worktree/worktree-manager.ts +6 -1
  180. package/teams/default.team.md +12 -12
  181. package/teams/fast-fix.team.md +11 -11
  182. package/teams/implementation.team.md +18 -18
  183. package/teams/parallel-research.team.md +14 -14
  184. package/teams/research.team.md +11 -11
  185. package/teams/review.team.md +12 -12
  186. package/workflows/default.workflow.md +29 -29
  187. package/workflows/fast-fix.workflow.md +22 -22
  188. package/workflows/implementation.workflow.md +38 -38
  189. package/workflows/parallel-research.workflow.md +46 -46
  190. package/workflows/research.workflow.md +22 -22
  191. package/workflows/review.workflow.md +30 -30
@@ -1,125 +1,125 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import { listRecentRuns } from "../run-index.ts";
3
- import type { ArtifactDescriptor, TeamRunManifest } from "../../state/types.ts";
4
-
5
- export interface RegisterCompactionGuardOptions {
6
- foregroundControllers: Set<AbortController>;
7
- }
8
-
9
- const TRIGGER_RATIO = 0.75;
10
- const HARD_RATIO = 0.95;
11
- const DEFAULT_CONTEXT_WINDOW = 200_000;
12
- const MAX_ARTIFACT_INDEX_RUNS = 10;
13
- const MAX_ARTIFACT_INDEX_ITEMS = 80;
14
-
15
- function contextWindow(ctx: { model?: { contextWindow?: number } }): number {
16
- const value = ctx.model?.contextWindow;
17
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : DEFAULT_CONTEXT_WINDOW;
18
- }
19
-
20
- function usageRatio(ctx: { getContextUsage(): { tokens: number | null } | undefined; model?: { contextWindow?: number } }): number | undefined {
21
- const tokens = ctx.getContextUsage()?.tokens;
22
- if (tokens === null || tokens === undefined || !Number.isFinite(tokens)) return undefined;
23
- return tokens / contextWindow(ctx);
24
- }
25
-
26
- interface CrewArtifactIndexEntry {
27
- runId: string;
28
- status: TeamRunManifest["status"];
29
- team: string;
30
- workflow?: string;
31
- goal: string;
32
- artifact: Pick<ArtifactDescriptor, "kind" | "path" | "producer" | "sizeBytes" | "createdAt">;
33
- }
34
-
35
- function collectCrewArtifactIndex(cwd: string): CrewArtifactIndexEntry[] {
36
- const entries: CrewArtifactIndexEntry[] = [];
37
- for (const run of listRecentRuns(cwd, MAX_ARTIFACT_INDEX_RUNS)) {
38
- for (const artifact of run.artifacts) {
39
- entries.push({
40
- runId: run.runId,
41
- status: run.status,
42
- team: run.team,
43
- workflow: run.workflow,
44
- goal: run.goal,
45
- artifact: {
46
- kind: artifact.kind,
47
- path: artifact.path,
48
- producer: artifact.producer,
49
- sizeBytes: artifact.sizeBytes,
50
- createdAt: artifact.createdAt,
51
- },
52
- });
53
- if (entries.length >= MAX_ARTIFACT_INDEX_ITEMS) return entries;
54
- }
55
- }
56
- return entries;
57
- }
58
-
59
- function formatCrewArtifactIndex(entries: CrewArtifactIndexEntry[]): string {
60
- if (!entries.length) return "";
61
- const lines = ["", "# pi-crew artifact index", "Preserve these run artifact references in the compaction summary:"];
62
- for (const entry of entries) {
63
- lines.push(`- ${entry.artifact.kind}: ${entry.artifact.path} (run=${entry.runId}, status=${entry.status}, team=${entry.team}, workflow=${entry.workflow ?? "none"}, producer=${entry.artifact.producer})`);
64
- }
65
- return lines.join("\n");
66
- }
67
-
68
- export function registerCompactionGuard(pi: ExtensionAPI, options: RegisterCompactionGuardOptions): void {
69
- let pendingCompactReason: string | null = null;
70
- let compactionInProgress = false;
71
-
72
- const startCompact = (ctx: ExtensionContext, reason: string): void => {
73
- if (compactionInProgress) return;
74
- compactionInProgress = true;
75
- const artifactIndex = collectCrewArtifactIndex(ctx.cwd);
76
- if (artifactIndex.length > 0) {
77
- pi.appendEntry("crew:artifact-index", {
78
- reason,
79
- createdAt: new Date().toISOString(),
80
- artifacts: artifactIndex,
81
- });
82
- }
83
- ctx.compact({
84
- customInstructions: `Prioritize keeping pi-crew run state, task results, artifact references, run IDs, and next actions. Keep completed-task detail concise.${formatCrewArtifactIndex(artifactIndex)}`,
85
- onComplete: () => {
86
- compactionInProgress = false;
87
- ctx.ui.notify(reason === "deferred" ? "Deferred compaction completed" : "Auto-compacted context during team run", "info");
88
- },
89
- onError: (error) => {
90
- compactionInProgress = false;
91
- ctx.ui.notify(`${reason === "deferred" ? "Deferred" : "Auto"} compaction failed: ${error.message}`, "error");
92
- },
93
- });
94
- };
95
-
96
- // Phase 1.2: Defer compaction during foreground runs unless context is critically full.
97
- pi.on("session_before_compact", async (_event, ctx) => {
98
- if (options.foregroundControllers.size === 0) return;
99
- const ratio = usageRatio(ctx);
100
- if (ratio !== undefined && ratio >= HARD_RATIO) {
101
- ctx.ui.notify("Compaction allowed despite foreground run: context is critically full", "warning");
102
- return;
103
- }
104
- pendingCompactReason = "deferred-during-foreground-run";
105
- ctx.ui.notify("Compaction deferred: foreground team run in progress", "info");
106
- return { cancel: true };
107
- });
108
-
109
- // Phase 2.1: Proactive compaction with dynamic threshold based on model context window.
110
- pi.on("turn_end", (_event, ctx) => {
111
- if (compactionInProgress) return;
112
- if (options.foregroundControllers.size === 0 && pendingCompactReason) {
113
- pendingCompactReason = null;
114
- startCompact(ctx, "deferred");
115
- return;
116
- }
117
- const ratio = usageRatio(ctx);
118
- if (ratio === undefined || ratio < TRIGGER_RATIO) return;
119
- if (options.foregroundControllers.size > 0) {
120
- pendingCompactReason = "threshold-during-foreground-run";
121
- return;
122
- }
123
- startCompact(ctx, "threshold");
124
- });
125
- }
1
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import { listRecentRuns } from "../run-index.ts";
3
+ import type { ArtifactDescriptor, TeamRunManifest } from "../../state/types.ts";
4
+
5
+ export interface RegisterCompactionGuardOptions {
6
+ foregroundControllers: Set<AbortController>;
7
+ }
8
+
9
+ const TRIGGER_RATIO = 0.75;
10
+ const HARD_RATIO = 0.95;
11
+ const DEFAULT_CONTEXT_WINDOW = 200_000;
12
+ const MAX_ARTIFACT_INDEX_RUNS = 10;
13
+ const MAX_ARTIFACT_INDEX_ITEMS = 80;
14
+
15
+ function contextWindow(ctx: { model?: { contextWindow?: number } }): number {
16
+ const value = ctx.model?.contextWindow;
17
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : DEFAULT_CONTEXT_WINDOW;
18
+ }
19
+
20
+ function usageRatio(ctx: { getContextUsage(): { tokens: number | null } | undefined; model?: { contextWindow?: number } }): number | undefined {
21
+ const tokens = ctx.getContextUsage()?.tokens;
22
+ if (tokens === null || tokens === undefined || !Number.isFinite(tokens)) return undefined;
23
+ return tokens / contextWindow(ctx);
24
+ }
25
+
26
+ interface CrewArtifactIndexEntry {
27
+ runId: string;
28
+ status: TeamRunManifest["status"];
29
+ team: string;
30
+ workflow?: string;
31
+ goal: string;
32
+ artifact: Pick<ArtifactDescriptor, "kind" | "path" | "producer" | "sizeBytes" | "createdAt">;
33
+ }
34
+
35
+ function collectCrewArtifactIndex(cwd: string): CrewArtifactIndexEntry[] {
36
+ const entries: CrewArtifactIndexEntry[] = [];
37
+ for (const run of listRecentRuns(cwd, MAX_ARTIFACT_INDEX_RUNS)) {
38
+ for (const artifact of run.artifacts) {
39
+ entries.push({
40
+ runId: run.runId,
41
+ status: run.status,
42
+ team: run.team,
43
+ workflow: run.workflow,
44
+ goal: run.goal,
45
+ artifact: {
46
+ kind: artifact.kind,
47
+ path: artifact.path,
48
+ producer: artifact.producer,
49
+ sizeBytes: artifact.sizeBytes,
50
+ createdAt: artifact.createdAt,
51
+ },
52
+ });
53
+ if (entries.length >= MAX_ARTIFACT_INDEX_ITEMS) return entries;
54
+ }
55
+ }
56
+ return entries;
57
+ }
58
+
59
+ function formatCrewArtifactIndex(entries: CrewArtifactIndexEntry[]): string {
60
+ if (!entries.length) return "";
61
+ const lines = ["", "# pi-crew artifact index", "Preserve these run artifact references in the compaction summary:"];
62
+ for (const entry of entries) {
63
+ lines.push(`- ${entry.artifact.kind}: ${entry.artifact.path} (run=${entry.runId}, status=${entry.status}, team=${entry.team}, workflow=${entry.workflow ?? "none"}, producer=${entry.artifact.producer})`);
64
+ }
65
+ return lines.join("\n");
66
+ }
67
+
68
+ export function registerCompactionGuard(pi: ExtensionAPI, options: RegisterCompactionGuardOptions): void {
69
+ let pendingCompactReason: string | null = null;
70
+ let compactionInProgress = false;
71
+
72
+ const startCompact = (ctx: ExtensionContext, reason: string): void => {
73
+ if (compactionInProgress) return;
74
+ compactionInProgress = true;
75
+ const artifactIndex = collectCrewArtifactIndex(ctx.cwd);
76
+ if (artifactIndex.length > 0) {
77
+ pi.appendEntry("crew:artifact-index", {
78
+ reason,
79
+ createdAt: new Date().toISOString(),
80
+ artifacts: artifactIndex,
81
+ });
82
+ }
83
+ ctx.compact({
84
+ customInstructions: `Prioritize keeping pi-crew run state, task results, artifact references, run IDs, and next actions. Keep completed-task detail concise.${formatCrewArtifactIndex(artifactIndex)}`,
85
+ onComplete: () => {
86
+ compactionInProgress = false;
87
+ ctx.ui.notify(reason === "deferred" ? "Deferred compaction completed" : "Auto-compacted context during team run", "info");
88
+ },
89
+ onError: (error) => {
90
+ compactionInProgress = false;
91
+ ctx.ui.notify(`${reason === "deferred" ? "Deferred" : "Auto"} compaction failed: ${error.message}`, "error");
92
+ },
93
+ });
94
+ };
95
+
96
+ // Phase 1.2: Defer compaction during foreground runs unless context is critically full.
97
+ pi.on("session_before_compact", async (_event, ctx) => {
98
+ if (options.foregroundControllers.size === 0) return;
99
+ const ratio = usageRatio(ctx);
100
+ if (ratio !== undefined && ratio >= HARD_RATIO) {
101
+ ctx.ui.notify("Compaction allowed despite foreground run: context is critically full", "warning");
102
+ return;
103
+ }
104
+ pendingCompactReason = "deferred-during-foreground-run";
105
+ ctx.ui.notify("Compaction deferred: foreground team run in progress", "info");
106
+ return { cancel: true };
107
+ });
108
+
109
+ // Phase 2.1: Proactive compaction with dynamic threshold based on model context window.
110
+ pi.on("turn_end", (_event, ctx) => {
111
+ if (compactionInProgress) return;
112
+ if (options.foregroundControllers.size === 0 && pendingCompactReason) {
113
+ pendingCompactReason = null;
114
+ startCompact(ctx, "deferred");
115
+ return;
116
+ }
117
+ const ratio = usageRatio(ctx);
118
+ if (ratio === undefined || ratio < TRIGGER_RATIO) return;
119
+ if (options.foregroundControllers.size > 0) {
120
+ pendingCompactReason = "threshold-during-foreground-run";
121
+ return;
122
+ }
123
+ startCompact(ctx, "threshold");
124
+ });
125
+ }
@@ -7,6 +7,7 @@ import { readPersistedSubagentRecord, savePersistedSubagentRecord, type Subagent
7
7
  import { loadConfig } from "../../config/config.ts";
8
8
  import { logInternalError } from "../../utils/internal-error.ts";
9
9
  import { __test__subagentSpawnParams, formatSubagentRecord, readSubagentRunResult, refreshPersistedSubagentRecord, subagentToolResult } from "./subagent-helpers.ts";
10
+ import { t } from "../../i18n.ts";
10
11
 
11
12
  export interface SubagentToolRegistrationOptions {
12
13
  ownerSessionGeneration?: () => number;
@@ -37,7 +38,7 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
37
38
  if (!permission.allowed) return subagentToolResult(permission.reason ?? "Current role cannot spawn subagents.", { role: currentRole, mode: permission.mode }, true);
38
39
  const spawnOptions = __test__subagentSpawnParams(params as Record<string, unknown>, ctx);
39
40
  spawnOptions.ownerSessionGeneration = options.ownerSessionGeneration?.();
40
- if (!spawnOptions.prompt.trim()) return subagentToolResult("Agent requires prompt.", {}, true);
41
+ if (!spawnOptions.prompt.trim()) return subagentToolResult(t("agent.requiresPrompt"), {}, true);
41
42
  const runner = async (currentOptions: SubagentSpawnOptions, childSignal?: AbortSignal) => handleTeamTool({ action: "run", agent: currentOptions.type, goal: currentOptions.prompt, model: currentOptions.model, async: currentOptions.background, config: currentOptions.maxTurns ? { runtime: { maxTurns: currentOptions.maxTurns } } : undefined } as TeamToolParamsValue, currentOptions.background ? { ...ctx, signal: childSignal } : { ...ctx, signal: childSignal });
42
43
  const record = subagentManager.spawn(spawnOptions, runner, spawnOptions.background ? undefined : signal);
43
44
  if (spawnOptions.background || record.status === "queued") {
@@ -45,11 +46,11 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
45
46
  // Phase 1.6: Record was terminated for telemetry.
46
47
  record.terminated = true;
47
48
  savePersistedSubagentRecord(ctx.cwd, record);
48
- return { ...subagentToolResult([`Agent ${record.status === "queued" ? "queued" : "started"}.`, `Agent ID: ${record.id}`, `Type: ${record.type}`, `Description: ${record.description}`, "Use get_subagent_result to retrieve output. Do not duplicate this agent's work."].join("\n"), { agentId: record.id, status: record.status }), terminate: true };
49
+ return { ...subagentToolResult([t("agent.started", { state: record.status === "queued" ? "queued" : "started" }), t("agent.id", { id: record.id }), t("agent.type", { type: record.type }), t("agent.description", { description: record.description }), t("agent.retrieveHint")].join("\n"), { agentId: record.id, status: record.status }), terminate: true };
49
50
  }
50
51
  await record.promise;
51
- const output = readSubagentRunResult(ctx, record) ?? record.result ?? "No output.";
52
- const foregroundResult = subagentToolResult([`Agent ${record.id} ${record.status}.`, "", output].join("\n"), { agentId: record.id, runId: record.runId, status: record.status }, record.status === "failed" || record.status === "error");
52
+ const output = readSubagentRunResult(ctx, record) ?? record.result ?? t("agent.noOutput");
53
+ const foregroundResult = subagentToolResult([t("agent.foregroundStatus", { id: record.id, status: record.status }), "", output].join("\n"), { agentId: record.id, runId: record.runId, status: record.status }, record.status === "failed" || record.status === "error");
53
54
  if (loadConfig(ctx.cwd).config.tools?.terminateOnForeground === true) {
54
55
  record.terminated = true;
55
56
  savePersistedSubagentRecord(ctx.cwd, record);
@@ -66,14 +67,14 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
66
67
  parameters: Type.Object({ agent_id: Type.String({ description: "Agent ID returned by Agent." }), wait: Type.Optional(Type.Boolean({ description: "Wait for completion before returning." })), verbose: Type.Optional(Type.Boolean({ description: "Include status metadata before output." })) }) as never,
67
68
  async execute(_id, params, signal, _onUpdate, ctx) {
68
69
  const p = params as { agent_id?: string; wait?: boolean; verbose?: boolean };
69
- if (!p.agent_id) return subagentToolResult("get_subagent_result requires agent_id.", {}, true);
70
+ if (!p.agent_id) return subagentToolResult(t("result.requiresAgentId"), {}, true);
70
71
  const inMemory = subagentManager.getRecord(p.agent_id);
71
72
  const record = inMemory ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id);
72
- if (!record) return subagentToolResult(`Agent not found: ${p.agent_id}`, {}, true);
73
+ if (!record) return subagentToolResult(t("result.notFound", { id: p.agent_id }), {}, true);
73
74
  let current = refreshPersistedSubagentRecord(ctx, record);
74
75
  if (inMemory && current !== inMemory) Object.assign(inMemory, current);
75
76
  if (!inMemory && !current.runId && (current.status === "running" || current.status === "queued")) {
76
- current = { ...current, status: "error", error: "Subagent was interrupted before its durable run id was recorded; it cannot be recovered after restart.", completedAt: current.completedAt ?? Date.now() };
77
+ current = { ...current, status: "error", error: t("result.unrecoverable"), completedAt: current.completedAt ?? Date.now() };
77
78
  savePersistedSubagentRecord(ctx.cwd, current);
78
79
  }
79
80
  if (p.wait && (current.status === "running" || current.status === "queued")) {
@@ -83,16 +84,24 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
83
84
  current.resultConsumed = false;
84
85
  if (inMemory) inMemory.resultConsumed = false;
85
86
  savePersistedSubagentRecord(ctx.cwd, current);
86
- }
87
- else while (current.status === "running" || current.status === "queued") {
88
- if (signal?.aborted) {
89
- current = { ...current, status: "error", error: "Waiting for subagent result was aborted.", completedAt: Date.now() };
90
- savePersistedSubagentRecord(ctx.cwd, current);
91
- break;
87
+ } else {
88
+ const waitStartMs = Date.now();
89
+ const maxWaitMs = 300_000; // 5 minutes
90
+ while (current.status === "running" || current.status === "queued") {
91
+ if (signal?.aborted) {
92
+ current = { ...current, status: "error", error: t("result.waitAborted"), completedAt: Date.now() };
93
+ savePersistedSubagentRecord(ctx.cwd, current);
94
+ break;
95
+ }
96
+ if (Date.now() - waitStartMs > maxWaitMs) {
97
+ current = { ...current, status: "error", error: t("result.waitTimeout"), completedAt: Date.now() };
98
+ savePersistedSubagentRecord(ctx.cwd, current);
99
+ break;
100
+ }
101
+ await new Promise((resolve) => setTimeout(resolve, 1000));
102
+ current = refreshPersistedSubagentRecord(ctx, current);
103
+ if (!current.runId) break;
92
104
  }
93
- await new Promise((resolve) => setTimeout(resolve, 1000));
94
- current = refreshPersistedSubagentRecord(ctx, current);
95
- if (!current.runId) break;
96
105
  }
97
106
  }
98
107
  const output = readSubagentRunResult(ctx, current);
@@ -101,7 +110,7 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
101
110
  if (inMemory) inMemory.resultConsumed = true;
102
111
  savePersistedSubagentRecord(ctx.cwd, current);
103
112
  }
104
- const text = [p.verbose ? formatSubagentRecord(current) : undefined, output ? `${p.verbose ? "\n" : ""}${output}` : current.status === "running" || current.status === "queued" ? "Agent is still running. Use wait=true or check again later." : current.error ?? "No output."].filter((line): line is string => Boolean(line)).join("\n");
113
+ const text = [p.verbose ? formatSubagentRecord(current) : undefined, output ? `${p.verbose ? "\n" : ""}${output}` : current.status === "running" || current.status === "queued" ? t("result.stillRunning") : current.error ?? t("agent.noOutput")].filter((line): line is string => Boolean(line)).join("\n");
105
114
  return subagentToolResult(text, { agentId: current.id, runId: current.runId, status: current.status }, current.status === "failed" || current.status === "error");
106
115
  },
107
116
  };
@@ -114,8 +123,8 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
114
123
  async execute(_id, params, _signal, _onUpdate, ctx) {
115
124
  const p = params as { agent_id?: string; message?: string };
116
125
  const record = p.agent_id ? subagentManager.getRecord(p.agent_id) ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id) : undefined;
117
- if (!record) return subagentToolResult(`Agent not found: ${p.agent_id ?? ""}`, {}, true);
118
- return subagentToolResult([`Steering request noted for ${record.id}.`, "Current default pi-crew backend is child-process, so mid-turn session.steer is not available yet.", record.runId ? `Use team cancel runId=${record.runId} if the agent must be interrupted.` : undefined].filter((line): line is string => Boolean(line)).join("\n"), { agentId: record.id, runId: record.runId, status: record.status });
126
+ if (!record) return subagentToolResult(t("result.notFound", { id: p.agent_id ?? "" }), {}, true);
127
+ return subagentToolResult([t("steer.noted", { id: record.id }), t("steer.unavailable"), record.runId ? t("steer.cancelHint", { runId: record.runId }) : undefined].filter((line): line is string => Boolean(line)).join("\n"), { agentId: record.id, runId: record.runId, status: record.status });
119
128
  },
120
129
  };
121
130
 
@@ -17,6 +17,7 @@ export interface RegisterTeamToolDeps {
17
17
  getRunSnapshotCache?: (cwd: string) => ReturnType<typeof createRunSnapshotCache>;
18
18
  getMetricRegistry?: () => MetricRegistry | undefined;
19
19
  widgetState: CrewWidgetState;
20
+ onJsonEvent?: (taskId: string, runId: string, event: unknown) => void;
20
21
  }
21
22
 
22
23
  export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps): void {
@@ -38,7 +39,7 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
38
39
  const runLabel = resolved.team ?? resolved.agent ?? "direct";
39
40
  pi.setSessionName(`pi-crew: ${runLabel}/${resolved.workflow ?? "default"} — ${resolved.goal.slice(0, 60)}`);
40
41
  }
41
- const output = await handleTeamTool(resolved, { ...ctx, signal: controller.signal, metricRegistry: deps.getMetricRegistry?.(), startForegroundRun: (runner, runId) => deps.startForegroundRun(ctx, runner, runId), onRunStarted: (runId) => deps.openLiveSidebar(ctx, runId) });
42
+ const output = await handleTeamTool(resolved, { ...ctx, signal: controller.signal, metricRegistry: deps.getMetricRegistry?.(), startForegroundRun: (runner, runId) => deps.startForegroundRun(ctx, runner, runId), onRunStarted: (runId) => deps.openLiveSidebar(ctx, runId), onJsonEvent: deps.onJsonEvent });
42
43
  if (resolved.action === "run") {
43
44
  pi.appendEntry("crew:run-started", {
44
45
  runId: output.details?.runId,
@@ -61,7 +61,7 @@ export function createResultWatcher(events: ResultWatcherEvents, resultsDir: str
61
61
  coalescer.schedule(file, RESULT_WATCHER_POLL_MS);
62
62
  return;
63
63
  }
64
- const key = buildCompletionKey(payload as Record<string, unknown>, `file:${file}`);
64
+ const key = buildCompletionKey(payload && typeof payload === "object" && !Array.isArray(payload) ? payload as Record<string, unknown> : {}, `file:${file}`);
65
65
  if (!markSeenWithTtl(seen, key, Date.now(), completionTtlMs)) {
66
66
  events.emit(eventName, payload);
67
67
  }
@@ -78,7 +78,7 @@ export function createResultWatcher(events: ResultWatcherEvents, resultsDir: str
78
78
  const startPolling = () => {
79
79
  if (pollTimer) return;
80
80
  pollTimer = setInterval(poll, RESULT_WATCHER_POLL_MS);
81
- pollTimer.unref?.();
81
+ pollTimer.unref();
82
82
  poll();
83
83
  };
84
84
  const stopPolling = () => {
@@ -98,7 +98,7 @@ export function createResultWatcher(events: ResultWatcherEvents, resultsDir: str
98
98
  logInternalError("result-watcher.restart", error, `resultsDir=${resultsDir}`);
99
99
  }
100
100
  }, RESULT_WATCHER_RESTART_MS);
101
- restartTimer.unref?.();
101
+ restartTimer.unref();
102
102
  };
103
103
  const handle: ResultWatcherHandle = {
104
104
  start() {
@@ -110,7 +110,7 @@ export function createResultWatcher(events: ResultWatcherEvents, resultsDir: str
110
110
  coalescer.schedule(fileName.toString());
111
111
  }, scheduleRestart);
112
112
  if (watcher) stopPolling();
113
- watcher?.unref?.();
113
+ watcher?.unref();
114
114
  },
115
115
  prime() {
116
116
  poll();
@@ -17,12 +17,13 @@ function validateArtifact(value: unknown, index: number, errors: string[]): valu
17
17
  errors.push(`manifest.artifacts[${index}] must be an object.`);
18
18
  return false;
19
19
  }
20
+ const before = errors.length;
20
21
  if (typeof value.kind !== "string") errors.push(`manifest.artifacts[${index}].kind must be a string.`);
21
22
  if (typeof value.path !== "string") errors.push(`manifest.artifacts[${index}].path must be a string.`);
22
23
  if (typeof value.createdAt !== "string") errors.push(`manifest.artifacts[${index}].createdAt must be a string.`);
23
24
  if (typeof value.producer !== "string") errors.push(`manifest.artifacts[${index}].producer must be a string.`);
24
25
  if (value.retention !== "run" && value.retention !== "project" && value.retention !== "temporary") errors.push(`manifest.artifacts[${index}].retention is invalid.`);
25
- return true;
26
+ return errors.length === before;
26
27
  }
27
28
 
28
29
  function validateManifest(value: unknown, errors: string[]): value is TeamRunManifest {
@@ -30,6 +31,7 @@ function validateManifest(value: unknown, errors: string[]): value is TeamRunMan
30
31
  errors.push("manifest must be an object.");
31
32
  return false;
32
33
  }
34
+ const before = errors.length;
33
35
  if (value.schemaVersion !== 1) errors.push("manifest.schemaVersion must be 1.");
34
36
  for (const field of ["runId", "team", "goal", "createdAt", "updatedAt", "cwd", "stateRoot", "artifactsRoot", "tasksPath", "eventsPath"] as const) {
35
37
  if (typeof value[field] !== "string") errors.push(`manifest.${field} must be a string.`);
@@ -38,7 +40,7 @@ function validateManifest(value: unknown, errors: string[]): value is TeamRunMan
38
40
  if (value.workspaceMode !== "single" && value.workspaceMode !== "worktree") errors.push("manifest.workspaceMode must be single or worktree.");
39
41
  if (!Array.isArray(value.artifacts)) errors.push("manifest.artifacts must be an array.");
40
42
  else value.artifacts.forEach((artifact, index) => validateArtifact(artifact, index, errors));
41
- return true;
43
+ return errors.length === before;
42
44
  }
43
45
 
44
46
  function validateTask(value: unknown, index: number, errors: string[]): value is TeamTaskState {
@@ -46,12 +48,13 @@ function validateTask(value: unknown, index: number, errors: string[]): value is
46
48
  errors.push(`tasks[${index}] must be an object.`);
47
49
  return false;
48
50
  }
51
+ const before = errors.length;
49
52
  for (const field of ["id", "runId", "role", "agent", "title", "cwd"] as const) {
50
53
  if (typeof value[field] !== "string") errors.push(`tasks[${index}].${field} must be a string.`);
51
54
  }
52
55
  if (!isTeamTaskStatus(value.status)) errors.push(`tasks[${index}].status is invalid.`);
53
56
  if (!Array.isArray(value.dependsOn)) errors.push(`tasks[${index}].dependsOn must be an array.`);
54
- return true;
57
+ return errors.length === before;
55
58
  }
56
59
 
57
60
  function validateEvent(value: unknown, index: number, errors: string[]): value is TeamEvent {
@@ -59,10 +62,11 @@ function validateEvent(value: unknown, index: number, errors: string[]): value i
59
62
  errors.push(`events[${index}] must be an object.`);
60
63
  return false;
61
64
  }
65
+ const before = errors.length;
62
66
  for (const field of ["time", "type", "runId"] as const) {
63
67
  if (typeof value[field] !== "string") errors.push(`events[${index}].${field} must be a string.`);
64
68
  }
65
- return true;
69
+ return errors.length === before;
66
70
  }
67
71
 
68
72
  export function validateRunBundle(value: unknown): BundleValidationResult {
@@ -29,6 +29,10 @@ export function importRunBundle(cwd: string, bundlePath: string, scope: "project
29
29
  resolveRealContainedPath(path.dirname(importsRoot), path.basename(importsRoot));
30
30
  const root = resolveContainedRelativePath(importsRoot, runId, "runId");
31
31
  fs.mkdirSync(root, { recursive: true });
32
+ // TOCTOU note: mkdirSync would throw EEXIST if a symlink already existed.
33
+ // The lstatSync check catches a symlink swapped in between mkdirSync and the check
34
+ // (theoretically possible but requires local attacker with exact timing).
35
+ // resolveRealContainedPath provides an additional real-path containment barrier.
32
36
  if (fs.lstatSync(root).isSymbolicLink()) throw new Error(`Invalid import directory: ${root}`);
33
37
  resolveRealContainedPath(importsRoot, runId);
34
38
  const targetJson = path.join(root, "run-export.json");
@@ -40,7 +40,10 @@ function mergeRuns(runSets: TeamRunManifest[][], max?: number): TeamRunManifest[
40
40
  }
41
41
 
42
42
  function scopedRunRoots(cwd: string): string[] {
43
- return [findRepoRoot(cwd) ? projectCrewRoot(cwd) : userCrewRoot()];
43
+ const roots: string[] = [userCrewRoot()];
44
+ const projectRoot = findRepoRoot(cwd);
45
+ if (projectRoot) roots.unshift(projectCrewRoot(cwd));
46
+ return roots;
44
47
  }
45
48
 
46
49
  export function listRuns(cwd: string): TeamRunManifest[] {
@@ -52,3 +55,22 @@ export function listRecentRuns(cwd: string, max = 20): TeamRunManifest[] {
52
55
  const roots = scopedRunRoots(cwd);
53
56
  return mergeRuns(roots.map((root) => collectRuns(root, max)), max);
54
57
  }
58
+
59
+ /**
60
+ * List runs filtered to a specific scope.
61
+ * - "project": only runs in the project crew root
62
+ * - "user": only runs in the user crew root
63
+ * - "all" (default): merge both scopes (current behavior)
64
+ */
65
+ export function listRunsByScope(cwd: string, scope: "project" | "user" | "all" = "all", max?: number): TeamRunManifest[] {
66
+ const projectRoot = findRepoRoot(cwd);
67
+ switch (scope) {
68
+ case "project":
69
+ return projectRoot ? collectRuns(projectCrewRoot(cwd), max) : [];
70
+ case "user":
71
+ return collectRuns(userCrewRoot(), max);
72
+ case "all":
73
+ default:
74
+ return max !== undefined ? listRecentRuns(cwd, max) : listRuns(cwd);
75
+ }
76
+ }
@@ -1,24 +1,43 @@
1
- import * as fs from "node:fs";
2
- import type { TeamRunManifest } from "../state/types.ts";
3
- import { listRuns } from "./run-index.ts";
4
-
5
- export interface PruneRunsResult {
6
- kept: string[];
7
- removed: string[];
8
- }
9
-
10
- function isFinished(run: TeamRunManifest): boolean {
11
- return run.status === "completed" || run.status === "failed" || run.status === "cancelled" || run.status === "blocked";
12
- }
13
-
14
- export function pruneFinishedRuns(cwd: string, keep: number): PruneRunsResult {
15
- const finished = listRuns(cwd).filter((run) => run.cwd === cwd && isFinished(run)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
16
- const kept = finished.slice(0, keep).map((run) => run.runId);
17
- const removed: string[] = [];
18
- for (const run of finished.slice(keep)) {
19
- fs.rmSync(run.stateRoot, { recursive: true, force: true });
20
- fs.rmSync(run.artifactsRoot, { recursive: true, force: true });
21
- removed.push(run.runId);
22
- }
23
- return { kept, removed };
24
- }
1
+ import * as fs from "node:fs";
2
+ import type { TeamRunManifest } from "../state/types.ts";
3
+ import { resolveRealContainedPath } from "../utils/safe-paths.ts";
4
+ import { projectCrewRoot } from "../utils/paths.ts";
5
+ import { listRuns } from "./run-index.ts";
6
+ import { logInternalError } from "../utils/internal-error.ts";
7
+
8
+ export interface PruneRunsResult {
9
+ kept: string[];
10
+ removed: string[];
11
+ }
12
+
13
+ function isFinished(run: TeamRunManifest): boolean {
14
+ return run.status === "completed" || run.status === "failed" || run.status === "cancelled" || run.status === "blocked";
15
+ }
16
+
17
+ function isSafeToPrune(cwd: string, run: TeamRunManifest): boolean {
18
+ try {
19
+ const crewRoot = projectCrewRoot(cwd);
20
+ resolveRealContainedPath(crewRoot, run.stateRoot);
21
+ resolveRealContainedPath(crewRoot, run.artifactsRoot);
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ export function pruneFinishedRuns(cwd: string, keep: number): PruneRunsResult {
29
+ const crewRoot = projectCrewRoot(cwd);
30
+ const finished = listRuns(cwd).filter((run) => run.cwd === cwd && isFinished(run)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
31
+ const kept = finished.slice(0, keep).map((run) => run.runId);
32
+ const removed: string[] = [];
33
+ for (const run of finished.slice(keep)) {
34
+ if (!isSafeToPrune(cwd, run)) {
35
+ logInternalError("prune.path-unsafe", new Error(`Skipping unsafe prune: stateRoot=${run.stateRoot}, artifactsRoot=${run.artifactsRoot}`), `runId=${run.runId}`);
36
+ continue;
37
+ }
38
+ fs.rmSync(run.stateRoot, { recursive: true, force: true });
39
+ fs.rmSync(run.artifactsRoot, { recursive: true, force: true });
40
+ removed.push(run.runId);
41
+ }
42
+ return { kept, removed };
43
+ }
@@ -22,7 +22,7 @@ import type { PiTeamsToolResult } from "../tool-result.ts";
22
22
  import { configRecord, result, type TeamContext } from "./context.ts";
23
23
 
24
24
  function globMatch(value: string, pattern: string): boolean {
25
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
25
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\?/g, "\\?").replace(/\*/g, ".*");
26
26
  return new RegExp(`^${escaped}$`).test(value);
27
27
  }
28
28
 
@@ -110,7 +110,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
110
110
  const approval = current.manifest.planApproval;
111
111
  if (!approval?.required || approval.status !== "pending") return result("Run has no pending plan approval request.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
112
112
  const now = new Date().toISOString();
113
- const tasks = current.tasks.map((task) => task.status === "queued" || task.status === "running" ? { ...task, status: "cancelled" as const, finishedAt: now, error: "Plan approval was cancelled." } : task);
113
+ const tasks = current.tasks.map((task) => task.status === "queued" || task.status === "running" || task.status === "waiting" ? { ...task, status: "cancelled" as const, finishedAt: now, error: "Plan approval was cancelled." } : task);
114
114
  let manifest: typeof current.manifest = { ...current.manifest, updatedAt: now, planApproval: { ...approval, status: "cancelled" as const, cancelledAt: now, updatedAt: now } };
115
115
  saveRunManifest(manifest);
116
116
  saveRunTasks(manifest, tasks);