pi-crew 0.6.1 → 0.6.4

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 (119) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/README.md +81 -33
  3. package/docs/issue-29-analysis.md +189 -0
  4. package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +1268 -0
  5. package/docs/ui-optimization-plan.md +447 -0
  6. package/package.json +2 -2
  7. package/src/config/config.ts +106 -15
  8. package/src/errors.ts +107 -0
  9. package/src/extension/async-notifier.ts +6 -2
  10. package/src/extension/crew-cleanup.ts +8 -5
  11. package/src/extension/management.ts +464 -109
  12. package/src/extension/register.ts +213 -35
  13. package/src/extension/registration/brief-tool-overrides.ts +400 -0
  14. package/src/extension/registration/commands.ts +27 -2
  15. package/src/extension/registration/subagent-helpers.ts +2 -2
  16. package/src/extension/registration/subagent-tools.ts +9 -4
  17. package/src/extension/registration/team-tool.ts +17 -10
  18. package/src/extension/registration/viewers.ts +2 -2
  19. package/src/extension/team-tool/api.ts +3 -3
  20. package/src/extension/team-tool/cancel.ts +3 -3
  21. package/src/extension/team-tool/explain.ts +1 -1
  22. package/src/extension/team-tool/handle-schedule.ts +40 -0
  23. package/src/extension/team-tool/inspect.ts +3 -3
  24. package/src/extension/team-tool/lifecycle-actions.ts +4 -4
  25. package/src/extension/team-tool/respond.ts +2 -2
  26. package/src/extension/team-tool/run.ts +64 -14
  27. package/src/extension/team-tool/status.ts +1 -1
  28. package/src/extension/team-tool-types.ts +2 -0
  29. package/src/extension/team-tool.ts +173 -46
  30. package/src/hooks/registry.ts +77 -13
  31. package/src/hooks/types.ts +9 -0
  32. package/src/plugins/plugin-define.ts +6 -0
  33. package/src/plugins/plugin-registry.ts +32 -0
  34. package/src/plugins/plugins/index.ts +3 -0
  35. package/src/plugins/plugins/nextjs.ts +19 -0
  36. package/src/plugins/plugins/vite.ts +14 -0
  37. package/src/plugins/plugins/vitest.ts +14 -0
  38. package/src/runtime/async-runner.ts +35 -7
  39. package/src/runtime/background-runner.ts +529 -144
  40. package/src/runtime/chain-parser.ts +5 -1
  41. package/src/runtime/chain-runner.ts +9 -3
  42. package/src/runtime/checkpoint.ts +262 -180
  43. package/src/runtime/child-pi.ts +164 -37
  44. package/src/runtime/crash-recovery.ts +25 -14
  45. package/src/runtime/crew-agent-records.ts +2 -0
  46. package/src/runtime/diagnostic-export.ts +1 -1
  47. package/src/runtime/dynamic-script-runner.ts +10 -7
  48. package/src/runtime/foreground-watchdog.ts +1 -1
  49. package/src/runtime/heartbeat-watcher.ts +19 -2
  50. package/src/runtime/intercom-bridge.ts +7 -0
  51. package/src/runtime/iteration-hooks.ts +1 -1
  52. package/src/runtime/manifest-cache.ts +4 -0
  53. package/src/runtime/orphan-worker-registry.ts +444 -0
  54. package/src/runtime/parent-guard.ts +70 -16
  55. package/src/runtime/pi-args.ts +437 -14
  56. package/src/runtime/pi-spawn.ts +1 -0
  57. package/src/runtime/post-checks.ts +1 -1
  58. package/src/runtime/retry-runner.ts +9 -3
  59. package/src/runtime/run-tracker.ts +38 -10
  60. package/src/runtime/sandbox.ts +17 -2
  61. package/src/runtime/scheduler.ts +25 -2
  62. package/src/runtime/skill-effectiveness.ts +105 -62
  63. package/src/runtime/skill-instructions.ts +110 -32
  64. package/src/runtime/stale-reconciler.ts +310 -69
  65. package/src/runtime/subagent-manager.ts +251 -57
  66. package/src/runtime/task-health.ts +76 -0
  67. package/src/runtime/task-id.ts +20 -0
  68. package/src/runtime/task-runner/live-executor.ts +1 -1
  69. package/src/runtime/task-runner/progress.ts +1 -1
  70. package/src/runtime/task-runner/state-helpers.ts +110 -6
  71. package/src/runtime/task-runner.ts +92 -70
  72. package/src/runtime/team-runner.ts +186 -20
  73. package/src/schema/team-tool-schema.ts +27 -9
  74. package/src/skills/discover-skills.ts +9 -3
  75. package/src/state/active-run-registry.ts +170 -38
  76. package/src/state/artifact-store.ts +25 -13
  77. package/src/state/atomic-write-v2.ts +86 -0
  78. package/src/state/atomic-write.ts +346 -55
  79. package/src/state/blob-store.ts +178 -10
  80. package/src/state/crew-init.ts +161 -28
  81. package/src/state/decision-ledger.ts +172 -111
  82. package/src/state/event-log-rotation.ts +82 -52
  83. package/src/state/event-log.ts +254 -70
  84. package/src/state/health-store.ts +71 -0
  85. package/src/state/locks.ts +102 -20
  86. package/src/state/mailbox.ts +45 -7
  87. package/src/state/observation-store.ts +4 -1
  88. package/src/state/run-graph.ts +141 -130
  89. package/src/state/run-metrics.ts +24 -8
  90. package/src/state/session-state-map.ts +51 -0
  91. package/src/state/state-store.ts +330 -43
  92. package/src/ui/live-run-sidebar.ts +1 -1
  93. package/src/ui/loaders.ts +4 -0
  94. package/src/ui/overlays/agent-picker-overlay.ts +1 -1
  95. package/src/ui/overlays/mailbox-detail-overlay.ts +1 -1
  96. package/src/ui/powerbar-publisher.ts +1 -1
  97. package/src/ui/run-action-dispatcher.ts +2 -2
  98. package/src/ui/run-snapshot-cache.ts +1 -1
  99. package/src/ui/status-colors.ts +5 -1
  100. package/src/ui/theme-adapter.ts +80 -1
  101. package/src/ui/tool-progress-formatter.ts +9 -5
  102. package/src/ui/tool-render.ts +4 -0
  103. package/src/ui/tool-renderers/brief-mode.ts +207 -0
  104. package/src/ui/tool-renderers/index.ts +627 -0
  105. package/src/ui/widget/index.ts +224 -0
  106. package/src/ui/widget/widget-formatters.ts +148 -0
  107. package/src/ui/widget/widget-model.ts +90 -0
  108. package/src/ui/widget/widget-renderer.ts +130 -0
  109. package/src/ui/widget/widget-types.ts +37 -0
  110. package/src/utils/env-filter.ts +66 -0
  111. package/src/utils/file-coalescer.ts +9 -1
  112. package/src/utils/guards.ts +110 -0
  113. package/src/utils/paths.ts +80 -5
  114. package/src/utils/redaction.ts +6 -1
  115. package/src/utils/safe-paths.ts +281 -10
  116. package/src/worktree/cleanup.ts +112 -24
  117. package/src/worktree/worktree-manager.ts +128 -15
  118. package/test-bugs-all.mjs +10 -6
  119. package/test-integration-check.ts +4 -0
@@ -163,6 +163,43 @@ function persistScheduledJob(cwd: string, job: import("../../runtime/scheduler.t
163
163
  }
164
164
  }
165
165
 
166
+ /** Update an existing scheduled job in persistent settings. */
167
+ export function persistScheduledJobUpdate(cwd: string, job: import("../../runtime/scheduler.ts").ScheduledJob): void {
168
+ try {
169
+ const settings = loadCrewSettings(cwd);
170
+ const existingJobs: import("../../runtime/scheduler.ts").ScheduledJob[] = Array.isArray(
171
+ (settings as Record<string, unknown>).scheduledJobs,
172
+ )
173
+ ? ((settings as Record<string, unknown>).scheduledJobs as import("../../runtime/scheduler.ts").ScheduledJob[])
174
+ : [];
175
+ const updated = existingJobs.map((j) => j.id === job.id ? job : j);
176
+ saveCrewSettings(
177
+ { ...settings, scheduledJobs: updated } as Parameters<typeof saveCrewSettings>[0],
178
+ cwd,
179
+ );
180
+ } catch {
181
+ /* best-effort persistence */
182
+ }
183
+ }
184
+
185
+ /** Remove a scheduled job from persistent settings. */
186
+ function persistScheduledJobRemove(cwd: string, jobId: string): void {
187
+ try {
188
+ const settings = loadCrewSettings(cwd);
189
+ const existingJobs: import("../../runtime/scheduler.ts").ScheduledJob[] = Array.isArray(
190
+ (settings as Record<string, unknown>).scheduledJobs,
191
+ )
192
+ ? ((settings as Record<string, unknown>).scheduledJobs as import("../../runtime/scheduler.ts").ScheduledJob[])
193
+ : [];
194
+ saveCrewSettings(
195
+ { ...settings, scheduledJobs: existingJobs.filter((j) => j.id !== jobId) } as Parameters<typeof saveCrewSettings>[0],
196
+ cwd,
197
+ );
198
+ } catch {
199
+ /* best-effort persistence */
200
+ }
201
+ }
202
+
166
203
  export function handleListScheduled(_params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
167
204
  const scheduler = getCrewScheduler();
168
205
  if (!scheduler) return result("Scheduler not running.", { action: "scheduled", status: "error" }, true);
@@ -177,6 +214,9 @@ export function handleListScheduled(_params: TeamToolParamsValue, ctx: TeamConte
177
214
  ` Next run: ${job.nextRun ?? "(unscheduled)"}`,
178
215
  ` Runs: ${job.runCount}, Last: ${job.lastRun ?? "(never)"} [${job.lastStatus ?? "?"}]`,
179
216
  );
217
+ if (job.spawnedRunIds && job.spawnedRunIds.length > 0) {
218
+ lines.push(` Spawned runs: ${job.spawnedRunIds.join(", ")}`);
219
+ }
180
220
  }
181
221
  return result(lines.join("\n"), { action: "scheduled", status: "ok" });
182
222
  }
@@ -10,7 +10,7 @@ export function handleEvents(params: TeamToolParamsValue, ctx: TeamContext): PiT
10
10
  if (!params.runId) return result("Events requires runId.", { action: "events", status: "error" }, true);
11
11
  const runCwd = locateRunCwd(params.runId, ctx.cwd);
12
12
  if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "events", status: "error" }, true);
13
- const loaded = loadRunManifestById(runCwd, params.runId);
13
+ const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
14
14
  if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "events", status: "error" }, true);
15
15
  const events = readEvents(loaded.manifest.eventsPath);
16
16
  const lines = [`Events for ${loaded.manifest.runId}:`, ...(events.length ? events.map((event) => `${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}${event.data ? ` ${JSON.stringify(event.data)}` : ""}`) : ["(none)"])];
@@ -21,7 +21,7 @@ export function handleArtifacts(params: TeamToolParamsValue, ctx: TeamContext):
21
21
  if (!params.runId) return result("Artifacts requires runId.", { action: "artifacts", status: "error" }, true);
22
22
  const runCwd = locateRunCwd(params.runId, ctx.cwd);
23
23
  if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "artifacts", status: "error" }, true);
24
- const loaded = loadRunManifestById(runCwd, params.runId);
24
+ const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
25
25
  if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "artifacts", status: "error" }, true);
26
26
  const lines = [`Artifacts for ${loaded.manifest.runId}:`, ...(loaded.manifest.artifacts.length ? loaded.manifest.artifacts.map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}${artifact.contentHash ? ` sha256=${artifact.contentHash.slice(0, 12)}` : ""}`) : ["- (none)"])];
27
27
  return result(lines.join("\n"), { action: "artifacts", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
@@ -31,7 +31,7 @@ export function handleSummary(params: TeamToolParamsValue, ctx: TeamContext): Pi
31
31
  if (!params.runId) return result("Summary requires runId.", { action: "summary", status: "error" }, true);
32
32
  const runCwd = locateRunCwd(params.runId, ctx.cwd);
33
33
  if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "summary", status: "error" }, true);
34
- const loaded = loadRunManifestById(runCwd, params.runId);
34
+ const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
35
35
  if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "summary", status: "error" }, true);
36
36
  const usage = aggregateUsage(loaded.tasks);
37
37
  const lines = [
@@ -17,7 +17,7 @@ import * as path from "node:path";
17
17
 
18
18
  export function handleWorktrees(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
19
19
  if (!params.runId) return result("Worktrees requires runId.", { action: "worktrees", status: "error" }, true);
20
- const loaded = loadRunManifestById(ctx.cwd, params.runId);
20
+ const loaded = loadRunManifestById(ctx.cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
21
21
  if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "worktrees", status: "error" }, true);
22
22
  const withWorktrees = loaded.tasks.filter((task) => task.worktree);
23
23
  const lines = [`Worktrees for ${loaded.manifest.runId}:`, ...(withWorktrees.length ? withWorktrees.map((task) => `- ${task.id}: ${task.worktree!.path} branch=${task.worktree!.branch} reused=${task.worktree!.reused ? "true" : "false"}`) : ["- (none)"])];
@@ -46,7 +46,7 @@ export function handleImport(params: TeamToolParamsValue, ctx: TeamContext): PiT
46
46
 
47
47
  export async function handleExport(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
48
48
  if (!params.runId) return result("Export requires runId.", { action: "export", status: "error" }, true);
49
- const loaded = loadRunManifestById(ctx.cwd, params.runId);
49
+ const loaded = loadRunManifestById(ctx.cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
50
50
  if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "export", status: "error" }, true);
51
51
 
52
52
  // SECURITY: Ownership check — only the owner session may export a run.
@@ -95,7 +95,7 @@ export async function handleForget(params: TeamToolParamsValue, ctx: TeamContext
95
95
  if (intentError) return intentError;
96
96
  if (!params.runId) return result("Forget requires runId.", { action: "forget", status: "error" }, true);
97
97
  if (!params.confirm) return result("forget requires confirm: true.", { action: "forget", status: "error" }, true);
98
- const loaded = loadRunManifestById(ctx.cwd, params.runId);
98
+ const loaded = loadRunManifestById(ctx.cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
99
99
  if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "forget", status: "error" }, true);
100
100
 
101
101
  // Ownership check — prevent cross-session deletion unless force is set
@@ -125,7 +125,7 @@ export async function handleCleanup(params: TeamToolParamsValue, ctx: TeamContex
125
125
  const intentError = enforceDestructiveIntent("cleanup", params, ctx.config);
126
126
  if (intentError) return intentError;
127
127
  if (!params.runId) return result("Cleanup requires runId.", { action: "cleanup", status: "error" }, true);
128
- const loaded = loadRunManifestById(ctx.cwd, params.runId);
128
+ const loaded = loadRunManifestById(ctx.cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
129
129
  if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cleanup", status: "error" }, true);
130
130
 
131
131
  // Ownership check — prevent cross-session worktree cleanup unless force is set
@@ -20,11 +20,11 @@ export function handleRespond(params: TeamToolParamsValue, ctx: TeamContext): Pi
20
20
 
21
21
  const runCwd = locateRunCwd(params.runId, ctx.cwd);
22
22
  if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "respond", status: "error" }, true);
23
- const loaded = loadRunManifestById(runCwd, params.runId);
23
+ const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
24
24
  if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "respond", status: "error" }, true);
25
25
 
26
26
  return withRunLockSync(loaded.manifest, () => {
27
- const fresh = loadRunManifestById(loaded.manifest.cwd, params.runId!);
27
+ const fresh = loadRunManifestById(loaded.manifest.cwd, params.runId!); // NOTE: inside withRunLockSync - consistent read
28
28
  if (!fresh) return result(`Run '${params.runId}' not found.`, { action: "respond", status: "error" }, true);
29
29
  const foreignRun = typeof fresh.manifest.ownerSessionId === "string" && fresh.manifest.ownerSessionId !== ctx.sessionId;
30
30
  if (foreignRun && !params.force) return result(`Run ${fresh.manifest.runId} belongs to another session. Use force: true to override.`, { action: "respond", status: "error", runId: fresh.manifest.runId }, true);
@@ -2,6 +2,7 @@ import { allAgents, discoverAgents } from "../../agents/discover-agents.ts";
2
2
  import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
3
3
  import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
4
4
  import { loadConfig } from "../../config/config.ts";
5
+ import { findGitRoot, assertCleanLeader } from "../../worktree/worktree-manager.ts";
5
6
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
6
7
  import { writeArtifact } from "../../state/artifact-store.ts";
7
8
  import { registerActiveRun, unregisterActiveRun } from "../../state/active-run-registry.ts";
@@ -85,9 +86,57 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
85
86
  const { ensureCrewDirectory } = await import("../../state/crew-init.ts");
86
87
  await ensureCrewDirectory(workingDir);
87
88
 
88
- const teams = allTeams(discoverTeams(ctx.cwd));
89
- const workflows = allWorkflows(discoverWorkflows(ctx.cwd));
90
- const agents = allAgents(discoverAgents(ctx.cwd));
89
+ // WORKTREE FIX: If worktree mode is needed but cwd is not a git repo,
90
+ // auto-correct to the nearest git repo root. This prevents "not a git repository"
91
+ // errors when ctx.cwd points to a parent directory that isn't a git repo.
92
+ let resolvedCtx = ctx;
93
+ if (workingDir) {
94
+ try {
95
+ const gitRoot = findGitRoot(workingDir);
96
+ if (gitRoot && gitRoot !== workingDir) {
97
+ resolvedCtx = { ...ctx, cwd: gitRoot };
98
+ }
99
+ } catch {
100
+ // cwd is not in a git repo — validate below if worktree mode is needed
101
+ }
102
+ }
103
+
104
+ // WORKTREE PRECONDITION CHECK: validate git repo exists and is clean
105
+ // BEFORE creating the run manifest, so we return a friendly error
106
+ // instead of crashing mid-execution in prepareTaskWorkspace.
107
+ if (params.workspaceMode === "worktree") {
108
+ let gitRoot: string | undefined;
109
+ try {
110
+ gitRoot = findGitRoot(resolvedCtx.cwd);
111
+ } catch {
112
+ // not a git repo
113
+ }
114
+ if (!gitRoot) {
115
+ return result(
116
+ `Worktree mode requires a git repository. '${resolvedCtx.cwd}' is not inside a git repo.\nUse workspaceMode: 'single' or run from a git repository.`,
117
+ { action: "run", status: "error" },
118
+ true,
119
+ );
120
+ }
121
+ // Check if clean leader is required (can be disabled via config)
122
+ const preCheckConfig = loadConfig(resolvedCtx.cwd);
123
+ if (preCheckConfig.config.requireCleanWorktreeLeader !== false) {
124
+ try {
125
+ assertCleanLeader(gitRoot);
126
+ } catch (err) {
127
+ const msg = err instanceof Error ? err.message : String(err);
128
+ return result(
129
+ `${msg}\nCommit or stash changes before using worktree mode, or use workspaceMode: 'single'.`,
130
+ { action: "run", status: "error" },
131
+ true,
132
+ );
133
+ }
134
+ }
135
+ }
136
+
137
+ const teams = allTeams(discoverTeams(resolvedCtx.cwd));
138
+ const workflows = allWorkflows(discoverWorkflows(resolvedCtx.cwd));
139
+ const agents = allAgents(discoverAgents(resolvedCtx.cwd));
91
140
  const directAgent = params.agent ? agents.find((item) => item.name === params.agent) : undefined;
92
141
  if (params.agent && !directAgent) return result(`Agent '${params.agent}' not found.`, { action: "run", status: "error" }, true);
93
142
  const teamName = params.team ?? "default";
@@ -110,7 +159,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
110
159
  steps: [{ id: "01_agent", role: params.role ?? "agent", task: "{goal}", model: params.model }],
111
160
  } : workflows.find((item) => item.name === workflowName);
112
161
  if (!baseWorkflow) return result(`Workflow '${workflowName}' not found.`, { action: "run", status: "error" }, true);
113
- const workflow = directAgent ? baseWorkflow : expandParallelResearchWorkflow(baseWorkflow, ctx.cwd);
162
+ const workflow = directAgent ? baseWorkflow : expandParallelResearchWorkflow(baseWorkflow, resolvedCtx.cwd);
114
163
 
115
164
  // Check if this is a pipeline workflow - special handling for multi-stage execution
116
165
  const isPipelineWorkflow = workflowName === "pipeline" && !directAgent;
@@ -152,7 +201,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
152
201
 
153
202
  const skillOverride = normalizeSkillOverride(params.skill);
154
203
  const { manifest, tasks, paths } = createRunManifest({
155
- cwd: ctx.cwd,
204
+ cwd: resolvedCtx.cwd,
156
205
  team,
157
206
  workflow,
158
207
  goal,
@@ -169,7 +218,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
169
218
  atomicWriteJson(paths.manifestPath, updatedManifest);
170
219
  registerActiveRun(updatedManifest);
171
220
 
172
- const loadedConfig = loadConfig(ctx.cwd);
221
+ const loadedConfig = loadConfig(resolvedCtx.cwd);
173
222
  const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
174
223
  const runtime = await resolveCrewRuntime(executedConfig);
175
224
  const runtimeResolution = runtimeResolutionState(runtime);
@@ -205,11 +254,12 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
205
254
  const asyncManifest = { ...effectiveManifest, async: { pid: spawned.pid, logPath: spawned.logPath, spawnedAt: new Date().toISOString() } };
206
255
  atomicWriteJson(paths.manifestPath, asyncManifest);
207
256
  void appendEventAsync(effectiveManifest.eventsPath, { type: "async.spawned", runId: effectiveManifest.runId, data: { pid: spawned.pid, logPath: spawned.logPath } });
208
- scheduleBackgroundEarlyExitGuard(ctx.cwd, effectiveManifest.runId, spawned.pid, spawned.logPath);
257
+ ctx.onRunStarted?.(effectiveManifest.runId);
258
+ scheduleBackgroundEarlyExitGuard(resolvedCtx.cwd, effectiveManifest.runId, spawned.pid, spawned.logPath);
209
259
  // Wait for the async run to complete and return actual results.
210
260
  try {
211
- const completed = await waitForRun(updatedManifest.runId, ctx.cwd, { timeoutMs: 3600000 });
212
- const metrics = collectRunMetrics(ctx.cwd, completed.manifest.runId);
261
+ const completed = await waitForRun(updatedManifest.runId, resolvedCtx.cwd, { timeoutMs: 3600000 });
262
+ const metrics = collectRunMetrics(resolvedCtx.cwd, completed.manifest.runId);
213
263
  const lines: string[] = [
214
264
  `pi-crew run ${completed.manifest.status}: ${completed.manifest.runId} (${team.name})`,
215
265
  `Goal: ${goal.slice(0, 100)}`,
@@ -292,7 +342,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
292
342
  }
293
343
 
294
344
  const runFailed = completed.manifest.status === "failed" || completed.manifest.status === "blocked";
295
- return result(lines.join("\n"), { action: "run", status: runFailed ? "error" : "ok", runId: completed.manifest.runId, artifactsRoot: completed.manifest.artifactsRoot }, runFailed);
345
+ return result(lines.join("\n"), { action: "run", status: runFailed ? "error" : "ok", runId: completed.manifest.runId, artifactsRoot: completed.manifest.artifactsRoot, metrics }, runFailed);
296
346
  } catch (waitError: unknown) {
297
347
  const errorMessage = waitError instanceof Error ? waitError.message : String(waitError);
298
348
  return result(
@@ -341,8 +391,8 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
341
391
 
342
392
  // Wait for the foreground run to complete and return actual results.
343
393
  try {
344
- const completed = await waitForRun(updatedManifest.runId, ctx.cwd, { timeoutMs: 3600000 });
345
- const metrics = collectRunMetrics(ctx.cwd, completed.manifest.runId);
394
+ const completed = await waitForRun(updatedManifest.runId, resolvedCtx.cwd, { timeoutMs: 3600000 });
395
+ const metrics = collectRunMetrics(resolvedCtx.cwd, completed.manifest.runId);
346
396
  const lines: string[] = [
347
397
  `pi-crew run ${completed.manifest.status}: ${completed.manifest.runId} (${team.name})`,
348
398
  `Goal: ${goal.slice(0, 100)}`,
@@ -425,7 +475,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
425
475
  }
426
476
 
427
477
  const runFailed = completed.manifest.status === "failed" || completed.manifest.status === "blocked";
428
- return result(lines.join("\n"), { action: "run", status: runFailed ? "error" : "ok", runId: completed.manifest.runId, artifactsRoot: completed.manifest.artifactsRoot }, runFailed);
478
+ return result(lines.join("\n"), { action: "run", status: runFailed ? "error" : "ok", runId: completed.manifest.runId, artifactsRoot: completed.manifest.artifactsRoot, metrics }, runFailed);
429
479
  } catch (waitError: unknown) {
430
480
  const errorMessage = waitError instanceof Error ? waitError.message : String(waitError);
431
481
  return result(
@@ -465,5 +515,5 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
465
515
  ? "Experimental live-session worker execution was enabled."
466
516
  : "Safe scaffold mode: child Pi workers were not launched because runtime.mode=scaffold or executeWorkers=false was configured.",
467
517
  ].join("\n");
468
- return result(text, { action: "run", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
518
+ return result(text, { action: "run", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot, metrics: collectRunMetrics(resolvedCtx.cwd, executed.manifest.runId) }, executed.manifest.status === "failed");
469
519
  }
@@ -18,7 +18,7 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
18
18
  if (!params.runId) return result("Status requires runId.", { action: "status", status: "error" }, true);
19
19
  const runCwd = locateRunCwd(params.runId, ctx.cwd);
20
20
  if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "status", status: "error" }, true);
21
- const loaded = loadRunManifestById(runCwd, params.runId);
21
+ const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
22
22
  if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "status", status: "error" }, true);
23
23
  let { manifest, tasks } = loaded;
24
24
  let asyncLivenessLine: string | undefined;
@@ -10,6 +10,8 @@ export interface TeamToolDetails {
10
10
  resumedIds?: string[];
11
11
  retriedTaskIds?: string[];
12
12
  mailboxIds?: string[];
13
+ /** Run metrics for compact display in TUI tool result rendering. */
14
+ metrics?: { taskCount?: number; completedCount?: number; totalTokens?: number; totalCost?: number; durationMs?: number; consistencyScore?: number };
13
15
  /** Structured data for programmatic consumption (e.g. TUI widgets). */
14
16
  data?: Record<string, unknown>;
15
17
  }