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.
- package/CHANGELOG.md +114 -0
- package/README.md +66 -0
- package/docs/bugs/bug-021-notification-badge-counter-misleading.md +293 -0
- package/docs/bugs/bug-023-chain-windows-path-resolution.md +106 -0
- package/package.json +1 -1
- package/src/config/defaults.ts +3 -1
- package/src/config/types.ts +5 -0
- package/src/extension/pi-api.ts +1 -1
- package/src/extension/registration/team-tool.ts +145 -35
- package/src/extension/team-tool/run.ts +646 -150
- package/src/runtime/child-pi.ts +87 -1
- package/src/runtime/goal-achievement.ts +131 -0
- package/src/runtime/recovery-recipes.ts +35 -0
- package/src/runtime/task-runner.ts +6 -0
- package/src/runtime/team-runner.ts +1482 -310
- package/src/state/types.ts +3 -0
- package/src/ui/widget/index.ts +1 -1
- package/src/ui/widget/widget-formatters.ts +14 -1
- package/src/ui/widget/widget-renderer.ts +13 -2
- package/src/utils/redaction.ts +20 -0
- package/src/workflows/discover-workflows.ts +144 -29
- package/src/workflows/preflight-validator.ts +195 -0
- package/src/workflows/topology-analyzer.ts +219 -0
- package/src/workflows/workflow-config.ts +11 -0
- package/workflows/chain.workflow.md +6 -0
- package/workflows/default.workflow.md +1 -0
- package/workflows/fast-fix.workflow.md +1 -0
- package/workflows/implementation.workflow.md +1 -0
- package/workflows/parallel-research.workflow.md +1 -0
- package/workflows/pipeline.workflow.md +1 -0
- package/workflows/research.workflow.md +1 -0
- package/workflows/review.workflow.md +1 -0
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
|
-
import type {
|
|
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 {
|
|
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
|
|
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(
|
|
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
|
-
|
|
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: (
|
|
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?: (
|
|
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(
|
|
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())
|
|
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(
|
|
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:
|
|
66
|
-
|
|
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(
|
|
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)
|
|
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 (
|
|
82
|
-
|
|
83
|
-
|
|
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, {
|
|
86
|
-
|
|
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(
|
|
101
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
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(
|
|
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) => {
|
|
278
|
+
attach: (boundCwd: string, boundRunId: string) => {
|
|
279
|
+
cwd = boundCwd;
|
|
280
|
+
runId = boundRunId;
|
|
281
|
+
tick();
|
|
282
|
+
},
|
|
173
283
|
stop: () => clearInterval(timer),
|
|
174
284
|
};
|
|
175
285
|
}
|