pi-crew 0.9.13 → 0.9.15

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.
@@ -1,20 +1,31 @@
1
1
  import { statSync } from "node:fs";
2
- import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import type {
3
+ ExtensionAPI,
4
+ ExtensionContext,
5
+ ToolDefinition,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import { Text } from "@earendil-works/pi-tui";
3
8
  import { loadConfig } from "../../config/config.ts";
4
- import { TeamToolParams, type TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
5
- import type { CrewWidgetState } from "../../ui/widget/index.ts";
6
- import { updateCrewWidget } from "../../ui/widget/index.ts";
7
- import { updatePiCrewPowerbar } from "../../ui/powerbar-publisher.ts";
9
+ import type { MetricRegistry } from "../../observability/metric-registry.ts";
8
10
  import type { createManifestCache } from "../../runtime/manifest-cache.ts";
11
+ import {
12
+ TeamToolParams,
13
+ type TeamToolParamsValue,
14
+ } from "../../schema/team-tool-schema.ts";
15
+ import { updatePiCrewPowerbar } from "../../ui/powerbar-publisher.ts";
9
16
  import type { createRunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
10
- import type { MetricRegistry } from "../../observability/metric-registry.ts";
17
+ import { statusIcon, teamToolRenderer } from "../../ui/tool-renderers/index.ts";
18
+ import type { CrewWidgetState } from "../../ui/widget/index.ts";
19
+ import { updateCrewWidget } from "../../ui/widget/index.ts";
11
20
  import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
12
- import { Text } from "@earendil-works/pi-tui";
13
- import { teamToolRenderer, statusIcon } from "../../ui/tool-renderers/index.ts";
14
21
  // Team tool handler — lazy-loaded because team-tool.ts imports many modules
15
22
  import type { handleTeamTool as HandleTeamToolFn } from "../team-tool.ts";
23
+
16
24
  let _cachedHandleTeamTool: typeof HandleTeamToolFn | undefined;
17
- async function handleTeamTool(params: Parameters<typeof HandleTeamToolFn>[0], ctx: Parameters<typeof HandleTeamToolFn>[1]): Promise<ReturnType<typeof HandleTeamToolFn>> {
25
+ async function handleTeamTool(
26
+ params: Parameters<typeof HandleTeamToolFn>[0],
27
+ ctx: Parameters<typeof HandleTeamToolFn>[1],
28
+ ): Promise<ReturnType<typeof HandleTeamToolFn>> {
18
29
  if (!_cachedHandleTeamTool) {
19
30
  // LAZY: team-tool.ts imports many modules — defer until first use.
20
31
  const mod = await import("../team-tool.ts");
@@ -22,12 +33,13 @@ async function handleTeamTool(params: Parameters<typeof HandleTeamToolFn>[0], ct
22
33
  }
23
34
  return _cachedHandleTeamTool(params, ctx);
24
35
  }
25
- import { withSessionId } from "../team-tool/context.ts";
26
- import { toolResult } from "../tool-result.ts";
27
- import { loadRunManifestById } from "../../state/state-store.ts";
36
+
28
37
  import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
38
+ import { loadRunManifestById } from "../../state/state-store.ts";
29
39
  import { formatCompactToolProgress } from "../../ui/tool-progress-formatter.ts";
30
40
  import { logInternalError } from "../../utils/internal-error.ts";
41
+ import { withSessionId } from "../team-tool/context.ts";
42
+ import { toolResult } from "../tool-result.ts";
31
43
 
32
44
  const TEAM_TOOL_PROGRESS_TICK_MS = 1000;
33
45
 
@@ -35,22 +47,35 @@ type OnUpdate = (chunk: { content: { type: "text"; text: string }[] }) => void;
35
47
 
36
48
  export interface RegisterTeamToolDeps {
37
49
  foregroundControllers: Map<string | symbol, AbortController>;
38
- startForegroundRun: (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>, runId?: string) => void;
50
+ startForegroundRun: (
51
+ ctx: ExtensionContext,
52
+ runner: (signal?: AbortSignal) => Promise<void>,
53
+ runId?: string,
54
+ ) => void;
39
55
  abortForegroundRun: (runId: string) => boolean;
40
56
  openLiveSidebar: (ctx: ExtensionContext, runId: string) => void;
41
57
  getManifestCache: (cwd: string) => ReturnType<typeof createManifestCache>;
42
- getRunSnapshotCache?: (cwd: string) => ReturnType<typeof createRunSnapshotCache>;
58
+ getRunSnapshotCache?: (
59
+ cwd: string,
60
+ ) => ReturnType<typeof createRunSnapshotCache>;
43
61
  getMetricRegistry?: () => MetricRegistry | undefined;
44
62
  widgetState: CrewWidgetState;
45
63
  onJsonEvent?: (taskId: string, runId: string, event: unknown) => void;
46
64
  }
47
65
 
48
- export function resolveCwdOverride(baseCwd: string, override: string | undefined): { ok: true; cwd: string } | { ok: false; error: string } {
66
+ export function resolveCwdOverride(
67
+ baseCwd: string,
68
+ override: string | undefined,
69
+ ): { ok: true; cwd: string } | { ok: false; error: string } {
49
70
  if (!override) return { ok: true, cwd: baseCwd };
50
71
  try {
51
72
  const resolved = resolveRealContainedPath(baseCwd, override);
52
73
  const stat = statSync(resolved);
53
- if (!stat.isDirectory()) return { ok: false, error: `cwd override is not a directory: ${resolved}` };
74
+ if (!stat.isDirectory())
75
+ return {
76
+ ok: false,
77
+ error: `cwd override is not a directory: ${resolved}`,
78
+ };
54
79
  return { ok: true, cwd: resolved };
55
80
  } catch (error) {
56
81
  const message = error instanceof Error ? error.message : String(error);
@@ -58,12 +83,28 @@ export function resolveCwdOverride(baseCwd: string, override: string | undefined
58
83
  }
59
84
  }
60
85
 
61
- export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps): void {
86
+ export function registerTeamTool(
87
+ pi: ExtensionAPI,
88
+ deps: RegisterTeamToolDeps,
89
+ ): void {
62
90
  const tool: ToolDefinition = {
63
91
  name: "team",
64
92
  label: "Team",
65
- description: "Coordinate Pi teams. Use proactively for complex multi-file work, planning, implementation, tests, reviews, security audits, research, async/background runs, and worktree-isolated execution. Use action='recommend' when unsure which team/workflow to choose. Destructive actions require explicit user confirmation.",
66
- promptSnippet: "Use the team tool proactively for coordinated multi-agent work. If unsure, call { action: 'recommend', goal } first, then run or plan with the suggested team/workflow.",
93
+ description: [
94
+ "Coordinate Pi teams. Use proactively for complex multi-file work, planning, implementation, tests, reviews, security audits, research, async/background runs, and worktree-isolated execution. Use action='recommend' when unsure which team/workflow to choose. Destructive actions require explicit user confirmation.",
95
+ "",
96
+ "ℹ️ ADVISORY NOTE (preflight, never blocks): pi-crew prints informational notes about workflow topology. Review the note, then decide. There is no BLOCK — agents always exercise judgment.",
97
+ "- Single-task run: raw `Agent` tool would be ~30× faster and ~5× cheaper. pi-crew notes this and proceeds anyway.",
98
+ "- 2–3 step sequential run: measured 5.7× slower than raw `Agent` calls in Run #3. pi-crew notes this and proceeds anyway.",
99
+ "- ≥3 concurrent or complex DAG: validated good use case, pi-crew notes 'validated use case'.",
100
+ "If unsure, call { action: 'recommend', goal } first to get a team/workflow suggestion.",
101
+ ].join("\n"),
102
+ promptSnippet: [
103
+ "Use the team tool for multi-agent orchestration when you need ≥3 concurrent agents or a complex DAG.",
104
+ "For single tasks or 2–3 sequential steps, the raw Agent tool is usually faster (raw calls = ~5× faster for 3-step chains).",
105
+ "pi-crew notes the topology (informational only) but proceeds either way — you decide based on your context (audit trail, team coordination, etc.).",
106
+ "If unsure, call { action: 'recommend', goal } first.",
107
+ ].join("\n"),
67
108
  parameters: TeamToolParams as never,
68
109
  async execute(_id, params, signal, onUpdate, ctx) {
69
110
  const controller = new AbortController();
@@ -71,19 +112,50 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
71
112
  deps.foregroundControllers.set(toolKey, controller);
72
113
  const abort = (): void => controller.abort();
73
114
  signal?.addEventListener("abort", abort, { once: true });
74
- const stopProgress = startTeamToolProgressBinder(onUpdate as OnUpdate | undefined);
115
+ const stopProgress = startTeamToolProgressBinder(
116
+ onUpdate as OnUpdate | undefined,
117
+ );
75
118
  try {
76
119
  const resolved = params as TeamToolParamsValue;
77
120
  const cwdOverride = resolveCwdOverride(ctx.cwd, resolved.cwd);
78
- if (!cwdOverride.ok) return toolResult(cwdOverride.error, { action: resolved.action ?? "list", status: "error" }, true);
121
+ if (!cwdOverride.ok)
122
+ return toolResult(
123
+ cwdOverride.error,
124
+ { action: resolved.action ?? "list", status: "error" },
125
+ true,
126
+ );
79
127
  const toolCtx = withSessionId({ ...ctx, cwd: cwdOverride.cwd });
80
128
  // Phase 1.5: Auto-set session name from team run context
81
- if (resolved.action === "run" && resolved.goal && !pi.getSessionName()) {
82
- const runLabel = resolved.team ?? resolved.agent ?? "direct";
83
- pi.setSessionName(`pi-crew: ${runLabel}/${resolved.workflow ?? "default"} — ${resolved.goal.slice(0, 60)}`);
129
+ if (
130
+ resolved.action === "run" &&
131
+ resolved.goal &&
132
+ !pi.getSessionName()
133
+ ) {
134
+ const runLabel =
135
+ resolved.team ?? resolved.agent ?? "direct";
136
+ pi.setSessionName(
137
+ `pi-crew: ${runLabel}/${resolved.workflow ?? "default"} — ${resolved.goal.slice(0, 60)}`,
138
+ );
84
139
  }
85
- const output = await handleTeamTool(resolved, { ...toolCtx, signal: controller.signal, metricRegistry: deps.getMetricRegistry?.(), startForegroundRun: (runner, runId) => deps.startForegroundRun(toolCtx, runner, runId), abortForegroundRun: deps.abortForegroundRun, onRunStarted: (runId) => { stopProgress.attach(toolCtx.cwd, runId); deps.openLiveSidebar(toolCtx, runId); }, onJsonEvent: deps.onJsonEvent, getRunSnapshotCache: deps.getRunSnapshotCache });
86
- if (resolved.action === "run" && !output.isError && typeof output.details?.runId === "string") {
140
+ const output = await handleTeamTool(resolved, {
141
+ ...toolCtx,
142
+ signal: controller.signal,
143
+ metricRegistry: deps.getMetricRegistry?.(),
144
+ startForegroundRun: (runner, runId) =>
145
+ deps.startForegroundRun(toolCtx, runner, runId),
146
+ abortForegroundRun: deps.abortForegroundRun,
147
+ onRunStarted: (runId) => {
148
+ stopProgress.attach(toolCtx.cwd, runId);
149
+ deps.openLiveSidebar(toolCtx, runId);
150
+ },
151
+ onJsonEvent: deps.onJsonEvent,
152
+ getRunSnapshotCache: deps.getRunSnapshotCache,
153
+ });
154
+ if (
155
+ resolved.action === "run" &&
156
+ !output.isError &&
157
+ typeof output.details?.runId === "string"
158
+ ) {
87
159
  pi.appendEntry("crew:run-started", {
88
160
  runId: output.details.runId,
89
161
  team: resolved.team,
@@ -97,8 +169,21 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
97
169
  const config = loadConfig(toolCtx.cwd).config.ui;
98
170
  const cache = deps.getManifestCache(toolCtx.cwd);
99
171
  const snapshotCache = deps.getRunSnapshotCache?.(toolCtx.cwd);
100
- updateCrewWidget(toolCtx, deps.widgetState, config, cache, snapshotCache);
101
- updatePiCrewPowerbar(pi.events, toolCtx.cwd, config, cache, snapshotCache, toolCtx);
172
+ updateCrewWidget(
173
+ toolCtx,
174
+ deps.widgetState,
175
+ config,
176
+ cache,
177
+ snapshotCache,
178
+ );
179
+ updatePiCrewPowerbar(
180
+ pi.events,
181
+ toolCtx.cwd,
182
+ config,
183
+ cache,
184
+ snapshotCache,
185
+ toolCtx,
186
+ );
102
187
  return output;
103
188
  } finally {
104
189
  signal?.removeEventListener("abort", abort);
@@ -113,7 +198,12 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
113
198
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
114
199
  renderResult(result: any, options: any, theme: any, context: any): any {
115
200
  try {
116
- return teamToolRenderer.renderResult(result, options, theme, context);
201
+ return teamToolRenderer.renderResult(
202
+ result,
203
+ options,
204
+ theme,
205
+ context,
206
+ );
117
207
  } catch {
118
208
  return new Text(statusIcon("completed", theme) + " done", 0, 0);
119
209
  }
@@ -127,7 +217,9 @@ interface TeamToolProgressBinder {
127
217
  stop: () => void;
128
218
  }
129
219
 
130
- function startTeamToolProgressBinder(onUpdate: OnUpdate | undefined): TeamToolProgressBinder {
220
+ function startTeamToolProgressBinder(
221
+ onUpdate: OnUpdate | undefined,
222
+ ): TeamToolProgressBinder {
131
223
  if (!onUpdate) {
132
224
  return { attach: () => {}, stop: () => {} };
133
225
  }
@@ -137,20 +229,30 @@ function startTeamToolProgressBinder(onUpdate: OnUpdate | undefined): TeamToolPr
137
229
  const tick = (): void => {
138
230
  try {
139
231
  if (!cwd || !runId) {
140
- const elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
232
+ const elapsed = Math.max(
233
+ 0,
234
+ Math.round((Date.now() - startedAt) / 1000),
235
+ );
141
236
  const msg = `team status=starting elapsed=${elapsed}s`;
142
237
  onUpdate({ content: [{ type: "text", text: msg }] });
143
238
  return;
144
239
  }
145
240
  const loaded = loadRunManifestById(cwd, runId);
146
241
  if (!loaded) {
147
- const elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
242
+ const elapsed = Math.max(
243
+ 0,
244
+ Math.round((Date.now() - startedAt) / 1000),
245
+ );
148
246
  const msg = `team run=${runId} elapsed=${elapsed}s (manifest pending)`;
149
247
  onUpdate({ content: [{ type: "text", text: msg }] });
150
248
  return;
151
249
  }
152
250
  let agents;
153
- try { agents = readCrewAgents(loaded.manifest); } catch { /* ignore */ }
251
+ try {
252
+ agents = readCrewAgents(loaded.manifest);
253
+ } catch {
254
+ /* ignore */
255
+ }
154
256
  const text = formatCompactToolProgress({
155
257
  agentId: runId,
156
258
  status: loaded.manifest.status,
@@ -162,14 +264,22 @@ function startTeamToolProgressBinder(onUpdate: OnUpdate | undefined): TeamToolPr
162
264
  });
163
265
  onUpdate({ content: [{ type: "text", text }] });
164
266
  } catch (error) {
165
- logInternalError("team-tool.progress", error, `runId=${runId ?? ""}`);
267
+ logInternalError(
268
+ "team-tool.progress",
269
+ error,
270
+ `runId=${runId ?? ""}`,
271
+ );
166
272
  }
167
273
  };
168
274
  tick();
169
275
  const timer = setInterval(tick, TEAM_TOOL_PROGRESS_TICK_MS);
170
276
  if (typeof timer.unref === "function") timer.unref();
171
277
  return {
172
- attach: (boundCwd: string, boundRunId: string) => { cwd = boundCwd; runId = boundRunId; tick(); },
278
+ attach: (boundCwd: string, boundRunId: string) => {
279
+ cwd = boundCwd;
280
+ runId = boundRunId;
281
+ tick();
282
+ },
173
283
  stop: () => clearInterval(timer),
174
284
  };
175
285
  }