pi-crew 0.1.0
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/AGENTS.md +32 -0
- package/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/NOTICE.md +15 -0
- package/README.md +703 -0
- package/agents/analyst.md +11 -0
- package/agents/critic.md +11 -0
- package/agents/executor.md +11 -0
- package/agents/explorer.md +11 -0
- package/agents/planner.md +11 -0
- package/agents/reviewer.md +11 -0
- package/agents/security-reviewer.md +11 -0
- package/agents/test-engineer.md +11 -0
- package/agents/verifier.md +11 -0
- package/agents/writer.md +11 -0
- package/docs/architecture.md +92 -0
- package/docs/live-mailbox-runtime.md +36 -0
- package/docs/publishing.md +65 -0
- package/docs/resource-formats.md +131 -0
- package/docs/usage.md +203 -0
- package/index.ts +6 -0
- package/install.mjs +19 -0
- package/package.json +79 -0
- package/schema.json +45 -0
- package/skills/.gitkeep +0 -0
- package/src/agents/agent-config.ts +27 -0
- package/src/agents/agent-serializer.ts +34 -0
- package/src/agents/discover-agents.ts +73 -0
- package/src/config/config.ts +193 -0
- package/src/extension/async-notifier.ts +36 -0
- package/src/extension/autonomous-policy.ts +122 -0
- package/src/extension/help.ts +43 -0
- package/src/extension/import-index.ts +52 -0
- package/src/extension/management.ts +335 -0
- package/src/extension/project-init.ts +74 -0
- package/src/extension/register.ts +349 -0
- package/src/extension/run-bundle-schema.ts +85 -0
- package/src/extension/run-export.ts +59 -0
- package/src/extension/run-import.ts +46 -0
- package/src/extension/run-index.ts +28 -0
- package/src/extension/run-maintenance.ts +24 -0
- package/src/extension/session-summary.ts +8 -0
- package/src/extension/team-manager-command.ts +86 -0
- package/src/extension/team-recommendation.ts +174 -0
- package/src/extension/team-tool.ts +783 -0
- package/src/extension/tool-result.ts +16 -0
- package/src/extension/validate-resources.ts +77 -0
- package/src/prompt/prompt-runtime.ts +58 -0
- package/src/runtime/async-runner.ts +26 -0
- package/src/runtime/background-runner.ts +43 -0
- package/src/runtime/child-pi.ts +75 -0
- package/src/runtime/model-fallback.ts +101 -0
- package/src/runtime/pi-args.ts +81 -0
- package/src/runtime/pi-json-output.ts +110 -0
- package/src/runtime/pi-spawn.ts +96 -0
- package/src/runtime/process-status.ts +25 -0
- package/src/runtime/task-runner.ts +164 -0
- package/src/runtime/team-runner.ts +135 -0
- package/src/runtime/worker-heartbeat.ts +21 -0
- package/src/schema/team-tool-schema.ts +100 -0
- package/src/state/artifact-store.ts +36 -0
- package/src/state/atomic-write.ts +18 -0
- package/src/state/contracts.ts +88 -0
- package/src/state/event-log.ts +27 -0
- package/src/state/locks.ts +40 -0
- package/src/state/mailbox.ts +188 -0
- package/src/state/state-store.ts +119 -0
- package/src/state/task-claims.ts +42 -0
- package/src/state/types.ts +88 -0
- package/src/state/usage.ts +29 -0
- package/src/teams/discover-teams.ts +84 -0
- package/src/teams/team-config.ts +22 -0
- package/src/teams/team-serializer.ts +36 -0
- package/src/ui/run-dashboard.ts +138 -0
- package/src/utils/frontmatter.ts +36 -0
- package/src/utils/ids.ts +12 -0
- package/src/utils/names.ts +26 -0
- package/src/utils/paths.ts +15 -0
- package/src/workflows/discover-workflows.ts +101 -0
- package/src/workflows/validate-workflow.ts +40 -0
- package/src/workflows/workflow-config.ts +24 -0
- package/src/workflows/workflow-serializer.ts +31 -0
- package/src/worktree/cleanup.ts +69 -0
- package/src/worktree/worktree-manager.ts +60 -0
- package/teams/default.team.md +12 -0
- package/teams/fast-fix.team.md +11 -0
- package/teams/implementation.team.md +15 -0
- package/teams/research.team.md +11 -0
- package/teams/review.team.md +12 -0
- package/tsconfig.json +19 -0
- package/workflows/default.workflow.md +29 -0
- package/workflows/fast-fix.workflow.md +22 -0
- package/workflows/implementation.workflow.md +47 -0
- package/workflows/research.workflow.md +22 -0
- package/workflows/review.workflow.md +30 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { loadConfig } from "../config/config.ts";
|
|
3
|
+
import { registerAutonomousPolicy } from "./autonomous-policy.ts";
|
|
4
|
+
import { TeamToolParams, type TeamToolParamsValue } from "../schema/team-tool-schema.ts";
|
|
5
|
+
import { startAsyncRunNotifier, stopAsyncRunNotifier, type AsyncNotifierState } from "./async-notifier.ts";
|
|
6
|
+
import { notifyActiveRuns } from "./session-summary.ts";
|
|
7
|
+
import { piTeamsHelp } from "./help.ts";
|
|
8
|
+
import { handleTeamManagerCommand } from "./team-manager-command.ts";
|
|
9
|
+
import { handleTeamTool, type TeamToolDetails } from "./team-tool.ts";
|
|
10
|
+
import { listRuns } from "./run-index.ts";
|
|
11
|
+
import { RunDashboard, type RunDashboardSelection } from "../ui/run-dashboard.ts";
|
|
12
|
+
|
|
13
|
+
function parseRunArgs(args: string): TeamToolParamsValue {
|
|
14
|
+
const tokens = args.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
|
|
15
|
+
const params: TeamToolParamsValue = { action: "run" };
|
|
16
|
+
const goalParts: string[] = [];
|
|
17
|
+
for (const token of tokens) {
|
|
18
|
+
if (token === "--async") params.async = true;
|
|
19
|
+
else if (token === "--worktree") params.workspaceMode = "worktree";
|
|
20
|
+
else if (token.startsWith("--team=")) params.team = token.slice("--team=".length);
|
|
21
|
+
else if (token.startsWith("--workflow=")) params.workflow = token.slice("--workflow=".length);
|
|
22
|
+
else if (token.startsWith("--agent=")) params.agent = token.slice("--agent=".length);
|
|
23
|
+
else if (token.startsWith("--role=")) params.role = token.slice("--role=".length);
|
|
24
|
+
else if (!params.team && goalParts.length === 0 && !token.startsWith("--")) params.team = token;
|
|
25
|
+
else goalParts.push(token);
|
|
26
|
+
}
|
|
27
|
+
params.goal = goalParts.join(" ").trim() || undefined;
|
|
28
|
+
return params;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function commandText(result: { content?: Array<{ type: string; text?: string }> }): string {
|
|
32
|
+
return result.content?.map((item) => item.text ?? "").join("\n") ?? "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function notifyCommandResult(ctx: ExtensionCommandContext, text: string): Promise<void> {
|
|
36
|
+
ctx.ui.notify(text.length > 800 ? `${text.slice(0, 797)}...` : text, "info");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseScalar(raw: string): unknown {
|
|
40
|
+
if (raw === "true") return true;
|
|
41
|
+
if (raw === "false") return false;
|
|
42
|
+
if (/^-?\d+$/.test(raw)) return Number(raw);
|
|
43
|
+
if (raw.includes(",")) return raw.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
44
|
+
return raw;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function pushUnset(config: Record<string, unknown>, key: string): void {
|
|
48
|
+
const current = Array.isArray(config.unset) ? config.unset : [];
|
|
49
|
+
current.push(key);
|
|
50
|
+
config.unset = current;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function setNestedConfig(config: Record<string, unknown>, key: string, value: unknown): void {
|
|
54
|
+
const parts = key.split(".").filter(Boolean);
|
|
55
|
+
if (parts.length === 0) return;
|
|
56
|
+
let target = config;
|
|
57
|
+
for (const part of parts.slice(0, -1)) {
|
|
58
|
+
const current = target[part];
|
|
59
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) target[part] = {};
|
|
60
|
+
target = target[part] as Record<string, unknown>;
|
|
61
|
+
}
|
|
62
|
+
target[parts[parts.length - 1]!] = value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function registerPiTeams(pi: ExtensionAPI): void {
|
|
66
|
+
const notifierState: AsyncNotifierState = { seenFinishedRunIds: new Set() };
|
|
67
|
+
registerAutonomousPolicy(pi);
|
|
68
|
+
|
|
69
|
+
pi.on("session_start", (_event, ctx) => {
|
|
70
|
+
notifyActiveRuns(ctx);
|
|
71
|
+
const loadedConfig = loadConfig(ctx.cwd);
|
|
72
|
+
startAsyncRunNotifier(ctx, notifierState, loadedConfig.config.notifierIntervalMs ?? 5000);
|
|
73
|
+
});
|
|
74
|
+
pi.on("session_shutdown", () => {
|
|
75
|
+
stopAsyncRunNotifier(notifierState);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const tool: ToolDefinition = {
|
|
79
|
+
name: "team",
|
|
80
|
+
label: "Team",
|
|
81
|
+
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.",
|
|
82
|
+
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.",
|
|
83
|
+
parameters: TeamToolParams as never,
|
|
84
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
85
|
+
return await handleTeamTool(params as TeamToolParamsValue, ctx);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
pi.registerTool(tool);
|
|
90
|
+
|
|
91
|
+
pi.registerCommand("teams", {
|
|
92
|
+
description: "List pi-crew teams, workflows, and agents",
|
|
93
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
94
|
+
const result = await handleTeamTool({ action: "list" }, ctx);
|
|
95
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
pi.registerCommand("team-run", {
|
|
100
|
+
description: "Manually start a pi-crew run (agent may also use the team tool autonomously)",
|
|
101
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
102
|
+
const result = await handleTeamTool(parseRunArgs(args), ctx);
|
|
103
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
pi.registerCommand("team-status", {
|
|
108
|
+
description: "Show pi-crew run status",
|
|
109
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
110
|
+
const runId = args.trim() || undefined;
|
|
111
|
+
const result = await handleTeamTool({ action: "status", runId }, ctx);
|
|
112
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
pi.registerCommand("team-resume", {
|
|
117
|
+
description: "Resume a pi-crew run by re-queueing failed/cancelled/skipped/running tasks",
|
|
118
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
119
|
+
const runId = args.trim() || undefined;
|
|
120
|
+
const result = await handleTeamTool({ action: "resume", runId }, ctx);
|
|
121
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
pi.registerCommand("team-summary", {
|
|
126
|
+
description: "Show pi-crew run summary",
|
|
127
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
128
|
+
const runId = args.trim() || undefined;
|
|
129
|
+
const result = await handleTeamTool({ action: "summary", runId }, ctx);
|
|
130
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
pi.registerCommand("team-events", {
|
|
135
|
+
description: "Show full pi-crew event log for a run",
|
|
136
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
137
|
+
const runId = args.trim() || undefined;
|
|
138
|
+
const result = await handleTeamTool({ action: "events", runId }, ctx);
|
|
139
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
pi.registerCommand("team-artifacts", {
|
|
144
|
+
description: "List pi-crew artifacts for a run",
|
|
145
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
146
|
+
const runId = args.trim() || undefined;
|
|
147
|
+
const result = await handleTeamTool({ action: "artifacts", runId }, ctx);
|
|
148
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
pi.registerCommand("team-worktrees", {
|
|
153
|
+
description: "List pi-crew worktrees for a run",
|
|
154
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
155
|
+
const runId = args.trim() || undefined;
|
|
156
|
+
const result = await handleTeamTool({ action: "worktrees", runId }, ctx);
|
|
157
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
pi.registerCommand("team-api", {
|
|
162
|
+
description: "Run safe pi-crew API interop operations: <runId> <operation> [key=value]",
|
|
163
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
164
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
165
|
+
const runId = tokens.find((token) => !token.includes("=") && !token.startsWith("--"));
|
|
166
|
+
const operation = tokens.find((token) => token !== runId && !token.includes("=") && !token.startsWith("--")) ?? "read-manifest";
|
|
167
|
+
const config: Record<string, unknown> = { operation };
|
|
168
|
+
for (const token of tokens.filter((item) => item.includes("="))) {
|
|
169
|
+
const [key, ...rest] = token.split("=");
|
|
170
|
+
if (key) config[key] = rest.join("=");
|
|
171
|
+
}
|
|
172
|
+
const result = await handleTeamTool({ action: "api", runId, config }, ctx);
|
|
173
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
pi.registerCommand("team-imports", {
|
|
178
|
+
description: "List imported pi-crew run bundles",
|
|
179
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
180
|
+
const result = await handleTeamTool({ action: "imports" }, ctx);
|
|
181
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
pi.registerCommand("team-import", {
|
|
186
|
+
description: "Import a pi-crew run-export.json bundle into local imports",
|
|
187
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
188
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
189
|
+
const pathArg = tokens.find((token) => !token.startsWith("--"));
|
|
190
|
+
const scope = tokens.includes("--user") ? "user" : "project";
|
|
191
|
+
const result = await handleTeamTool({ action: "import", config: { path: pathArg, scope } }, ctx);
|
|
192
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
pi.registerCommand("team-export", {
|
|
197
|
+
description: "Export a pi-crew run bundle to artifacts/export",
|
|
198
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
199
|
+
const runId = args.trim() || undefined;
|
|
200
|
+
const result = await handleTeamTool({ action: "export", runId }, ctx);
|
|
201
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
pi.registerCommand("team-prune", {
|
|
206
|
+
description: "Prune old finished pi-crew runs, keeping the newest N",
|
|
207
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
208
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
209
|
+
const keepToken = tokens.find((token) => token.startsWith("--keep="));
|
|
210
|
+
const keep = keepToken ? Number.parseInt(keepToken.slice("--keep=".length), 10) : undefined;
|
|
211
|
+
const confirm = tokens.includes("--confirm");
|
|
212
|
+
const result = await handleTeamTool({ action: "prune", keep, confirm }, ctx);
|
|
213
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
pi.registerCommand("team-forget", {
|
|
218
|
+
description: "Forget a pi-crew run by deleting its state and artifacts",
|
|
219
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
220
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
221
|
+
const runId = tokens.find((token) => !token.startsWith("--"));
|
|
222
|
+
const force = tokens.includes("--force");
|
|
223
|
+
const confirm = tokens.includes("--confirm");
|
|
224
|
+
const result = await handleTeamTool({ action: "forget", runId, force, confirm }, ctx);
|
|
225
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
pi.registerCommand("team-cleanup", {
|
|
230
|
+
description: "Clean up pi-crew worktrees for a run",
|
|
231
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
232
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
233
|
+
const runId = tokens.find((token) => !token.startsWith("--"));
|
|
234
|
+
const force = tokens.includes("--force");
|
|
235
|
+
const result = await handleTeamTool({ action: "cleanup", runId, force }, ctx);
|
|
236
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
pi.registerCommand("team-manager", {
|
|
241
|
+
description: "Open a simple pi-crew interactive manager",
|
|
242
|
+
handler: handleTeamManagerCommand,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
pi.registerCommand("team-dashboard", {
|
|
246
|
+
description: "Open a pi-crew run dashboard overlay",
|
|
247
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
248
|
+
for (;;) {
|
|
249
|
+
const runs = listRuns(ctx.cwd).slice(0, 50);
|
|
250
|
+
const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((_tui, _theme, _keybindings, done) => new RunDashboard(runs, done), {
|
|
251
|
+
overlay: true,
|
|
252
|
+
overlayOptions: { width: "90%", maxHeight: "80%", anchor: "center" },
|
|
253
|
+
});
|
|
254
|
+
if (!selection) return;
|
|
255
|
+
if (selection.action === "reload") continue;
|
|
256
|
+
const result = selection.action === "api"
|
|
257
|
+
? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-manifest" } }, ctx)
|
|
258
|
+
: await handleTeamTool({ action: selection.action, runId: selection.runId }, ctx);
|
|
259
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
pi.registerCommand("team-init", {
|
|
266
|
+
description: "Initialize project-local pi-crew directories and gitignore entries",
|
|
267
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
268
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
269
|
+
const result = await handleTeamTool({ action: "init", config: { copyBuiltins: tokens.includes("--copy-builtins"), overwrite: tokens.includes("--overwrite") } }, ctx);
|
|
270
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
pi.registerCommand("team-autonomy", {
|
|
275
|
+
description: "Show or toggle pi-crew autonomous delegation policy: status|on|off",
|
|
276
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
277
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
278
|
+
const mode = tokens[0]?.toLowerCase();
|
|
279
|
+
const config = mode === "on" ? { profile: "suggested", enabled: true, injectPolicy: true }
|
|
280
|
+
: mode === "off" ? { profile: "manual", enabled: false }
|
|
281
|
+
: mode === "manual" || mode === "suggested" || mode === "assisted" || mode === "aggressive" ? { profile: mode, enabled: mode !== "manual", injectPolicy: mode !== "manual" }
|
|
282
|
+
: {
|
|
283
|
+
preferAsyncForLongTasks: tokens.includes("--prefer-async") ? true : undefined,
|
|
284
|
+
allowWorktreeSuggestion: tokens.includes("--no-worktree-suggest") ? false : undefined,
|
|
285
|
+
};
|
|
286
|
+
const result = await handleTeamTool({ action: "autonomy", config }, ctx);
|
|
287
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
pi.registerCommand("team-config", {
|
|
292
|
+
description: "Show or update pi-crew config. Use key=value [--project] to update.",
|
|
293
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
294
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
295
|
+
if (tokens.length === 0) {
|
|
296
|
+
const result = await handleTeamTool({ action: "config" }, ctx);
|
|
297
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const config: Record<string, unknown> = { scope: tokens.includes("--project") ? "project" : "user" };
|
|
301
|
+
for (const token of tokens) {
|
|
302
|
+
if (token.startsWith("--unset=")) {
|
|
303
|
+
pushUnset(config, token.slice("--unset=".length));
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (!token.includes("=")) continue;
|
|
307
|
+
const [key, ...rest] = token.split("=");
|
|
308
|
+
if (!key) continue;
|
|
309
|
+
const raw = rest.join("=");
|
|
310
|
+
if (raw === "unset" || raw === "null") pushUnset(config, key);
|
|
311
|
+
else setNestedConfig(config, key, parseScalar(raw));
|
|
312
|
+
}
|
|
313
|
+
const result = await handleTeamTool({ action: "config", config }, ctx);
|
|
314
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
pi.registerCommand("team-validate", {
|
|
319
|
+
description: "Validate pi-crew agents, teams, and workflows",
|
|
320
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
321
|
+
const result = await handleTeamTool({ action: "validate" }, ctx);
|
|
322
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
pi.registerCommand("team-help", {
|
|
327
|
+
description: "Show pi-crew command help",
|
|
328
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
329
|
+
await notifyCommandResult(ctx, piTeamsHelp());
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
pi.registerCommand("team-cancel", {
|
|
334
|
+
description: "Cancel a pi-crew run",
|
|
335
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
336
|
+
const runId = args.trim() || undefined;
|
|
337
|
+
const result = await handleTeamTool({ action: "cancel", runId }, ctx);
|
|
338
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
pi.registerCommand("team-doctor", {
|
|
343
|
+
description: "Check pi-crew installation and discovery readiness",
|
|
344
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
345
|
+
const result = await handleTeamTool({ action: "doctor" }, ctx);
|
|
346
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { isTeamRunStatus, isTeamTaskStatus } from "../state/contracts.ts";
|
|
2
|
+
import type { TeamRunManifest, TeamTaskState, ArtifactDescriptor } from "../state/types.ts";
|
|
3
|
+
import type { TeamEvent } from "../state/event-log.ts";
|
|
4
|
+
import type { ExportedRunBundle } from "./run-export.ts";
|
|
5
|
+
|
|
6
|
+
export interface BundleValidationResult {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
errors: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
12
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function validateArtifact(value: unknown, index: number, errors: string[]): value is ArtifactDescriptor {
|
|
16
|
+
if (!isRecord(value)) {
|
|
17
|
+
errors.push(`manifest.artifacts[${index}] must be an object.`);
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (typeof value.kind !== "string") errors.push(`manifest.artifacts[${index}].kind must be a string.`);
|
|
21
|
+
if (typeof value.path !== "string") errors.push(`manifest.artifacts[${index}].path must be a string.`);
|
|
22
|
+
if (typeof value.createdAt !== "string") errors.push(`manifest.artifacts[${index}].createdAt must be a string.`);
|
|
23
|
+
if (typeof value.producer !== "string") errors.push(`manifest.artifacts[${index}].producer must be a string.`);
|
|
24
|
+
if (value.retention !== "run" && value.retention !== "project" && value.retention !== "temporary") errors.push(`manifest.artifacts[${index}].retention is invalid.`);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validateManifest(value: unknown, errors: string[]): value is TeamRunManifest {
|
|
29
|
+
if (!isRecord(value)) {
|
|
30
|
+
errors.push("manifest must be an object.");
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (value.schemaVersion !== 1) errors.push("manifest.schemaVersion must be 1.");
|
|
34
|
+
for (const field of ["runId", "team", "goal", "createdAt", "updatedAt", "cwd", "stateRoot", "artifactsRoot", "tasksPath", "eventsPath"] as const) {
|
|
35
|
+
if (typeof value[field] !== "string") errors.push(`manifest.${field} must be a string.`);
|
|
36
|
+
}
|
|
37
|
+
if (!isTeamRunStatus(value.status)) errors.push("manifest.status is invalid.");
|
|
38
|
+
if (value.workspaceMode !== "single" && value.workspaceMode !== "worktree") errors.push("manifest.workspaceMode must be single or worktree.");
|
|
39
|
+
if (!Array.isArray(value.artifacts)) errors.push("manifest.artifacts must be an array.");
|
|
40
|
+
else value.artifacts.forEach((artifact, index) => validateArtifact(artifact, index, errors));
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validateTask(value: unknown, index: number, errors: string[]): value is TeamTaskState {
|
|
45
|
+
if (!isRecord(value)) {
|
|
46
|
+
errors.push(`tasks[${index}] must be an object.`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
for (const field of ["id", "runId", "role", "agent", "title", "cwd"] as const) {
|
|
50
|
+
if (typeof value[field] !== "string") errors.push(`tasks[${index}].${field} must be a string.`);
|
|
51
|
+
}
|
|
52
|
+
if (!isTeamTaskStatus(value.status)) errors.push(`tasks[${index}].status is invalid.`);
|
|
53
|
+
if (!Array.isArray(value.dependsOn)) errors.push(`tasks[${index}].dependsOn must be an array.`);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validateEvent(value: unknown, index: number, errors: string[]): value is TeamEvent {
|
|
58
|
+
if (!isRecord(value)) {
|
|
59
|
+
errors.push(`events[${index}] must be an object.`);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
for (const field of ["time", "type", "runId"] as const) {
|
|
63
|
+
if (typeof value[field] !== "string") errors.push(`events[${index}].${field} must be a string.`);
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function validateRunBundle(value: unknown): BundleValidationResult {
|
|
69
|
+
const errors: string[] = [];
|
|
70
|
+
if (!isRecord(value)) return { ok: false, errors: ["bundle must be an object."] };
|
|
71
|
+
if (value.schemaVersion !== 1) errors.push("schemaVersion must be 1.");
|
|
72
|
+
if (typeof value.exportedAt !== "string") errors.push("exportedAt must be a string.");
|
|
73
|
+
validateManifest(value.manifest, errors);
|
|
74
|
+
if (!Array.isArray(value.tasks)) errors.push("tasks must be an array.");
|
|
75
|
+
else value.tasks.forEach((task, index) => validateTask(task, index, errors));
|
|
76
|
+
if (!Array.isArray(value.events)) errors.push("events must be an array.");
|
|
77
|
+
else value.events.forEach((event, index) => validateEvent(event, index, errors));
|
|
78
|
+
if (!Array.isArray(value.artifactPaths) || !value.artifactPaths.every((item) => typeof item === "string")) errors.push("artifactPaths must be an array of strings.");
|
|
79
|
+
return { ok: errors.length === 0, errors };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function assertRunBundle(value: unknown): asserts value is ExportedRunBundle {
|
|
83
|
+
const validation = validateRunBundle(value);
|
|
84
|
+
if (!validation.ok) throw new Error(`File is not a valid pi-crew exported run bundle:\n${validation.errors.map((error) => `- ${error}`).join("\n")}`);
|
|
85
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
4
|
+
import { writeArtifact } from "../state/artifact-store.ts";
|
|
5
|
+
import { readEvents, type TeamEvent } from "../state/event-log.ts";
|
|
6
|
+
|
|
7
|
+
export interface ExportedRunBundle {
|
|
8
|
+
schemaVersion: 1;
|
|
9
|
+
exportedAt: string;
|
|
10
|
+
manifest: TeamRunManifest;
|
|
11
|
+
tasks: TeamTaskState[];
|
|
12
|
+
events: TeamEvent[];
|
|
13
|
+
artifactPaths: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function exportRunBundle(manifest: TeamRunManifest, tasks: TeamTaskState[]): { jsonPath: string; markdownPath: string } {
|
|
17
|
+
const events = readEvents(manifest.eventsPath);
|
|
18
|
+
const bundle: ExportedRunBundle = {
|
|
19
|
+
schemaVersion: 1,
|
|
20
|
+
exportedAt: new Date().toISOString(),
|
|
21
|
+
manifest,
|
|
22
|
+
tasks,
|
|
23
|
+
events,
|
|
24
|
+
artifactPaths: manifest.artifacts.map((artifact) => artifact.path),
|
|
25
|
+
};
|
|
26
|
+
const json = writeArtifact(manifest.artifactsRoot, {
|
|
27
|
+
kind: "metadata",
|
|
28
|
+
relativePath: "export/run-export.json",
|
|
29
|
+
producer: "run-export",
|
|
30
|
+
content: `${JSON.stringify(bundle, null, 2)}\n`,
|
|
31
|
+
});
|
|
32
|
+
const markdown = writeArtifact(manifest.artifactsRoot, {
|
|
33
|
+
kind: "summary",
|
|
34
|
+
relativePath: "export/run-export.md",
|
|
35
|
+
producer: "run-export",
|
|
36
|
+
content: [
|
|
37
|
+
`# pi-crew export ${manifest.runId}`,
|
|
38
|
+
"",
|
|
39
|
+
`Exported: ${bundle.exportedAt}`,
|
|
40
|
+
`Status: ${manifest.status}`,
|
|
41
|
+
`Team: ${manifest.team}`,
|
|
42
|
+
`Workflow: ${manifest.workflow ?? "(none)"}`,
|
|
43
|
+
`Goal: ${manifest.goal}`,
|
|
44
|
+
"",
|
|
45
|
+
"## Tasks",
|
|
46
|
+
...tasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
|
|
47
|
+
"",
|
|
48
|
+
"## Artifacts",
|
|
49
|
+
...(manifest.artifacts.length ? manifest.artifacts.map((artifact) => `- ${artifact.kind}: ${artifact.path}`) : ["- (none)"]),
|
|
50
|
+
"",
|
|
51
|
+
"## Recent Events",
|
|
52
|
+
...(events.slice(-20).map((event) => `- ${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}`)),
|
|
53
|
+
"",
|
|
54
|
+
].join("\n"),
|
|
55
|
+
});
|
|
56
|
+
// Ensure artifact dirs are materialized before returning paths on filesystems with delayed metadata.
|
|
57
|
+
fs.statSync(path.dirname(json.path));
|
|
58
|
+
return { jsonPath: json.path, markdownPath: markdown.path };
|
|
59
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { assertRunBundle } from "./run-bundle-schema.ts";
|
|
4
|
+
import { projectPiRoot, userPiRoot } from "../utils/paths.ts";
|
|
5
|
+
|
|
6
|
+
export interface ImportedRunBundleInfo {
|
|
7
|
+
runId: string;
|
|
8
|
+
importedAt: string;
|
|
9
|
+
bundlePath: string;
|
|
10
|
+
summaryPath: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function importRoot(cwd: string, scope: "project" | "user"): string {
|
|
14
|
+
return scope === "project"
|
|
15
|
+
? path.join(projectPiRoot(cwd), "teams", "imports")
|
|
16
|
+
: path.join(userPiRoot(), "extensions", "pi-crew", "imports");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function importRunBundle(cwd: string, bundlePath: string, scope: "project" | "user" = "project"): ImportedRunBundleInfo {
|
|
20
|
+
const resolvedPath = path.isAbsolute(bundlePath) ? bundlePath : path.resolve(cwd, bundlePath);
|
|
21
|
+
const raw = JSON.parse(fs.readFileSync(resolvedPath, "utf-8")) as unknown;
|
|
22
|
+
assertRunBundle(raw);
|
|
23
|
+
const runId = raw.manifest.runId;
|
|
24
|
+
const importedAt = new Date().toISOString();
|
|
25
|
+
const root = path.join(importRoot(cwd, scope), runId);
|
|
26
|
+
fs.mkdirSync(root, { recursive: true });
|
|
27
|
+
const targetJson = path.join(root, "run-export.json");
|
|
28
|
+
const targetSummary = path.join(root, "README.md");
|
|
29
|
+
fs.writeFileSync(targetJson, `${JSON.stringify({ ...raw, importedAt, importedFrom: resolvedPath }, null, 2)}\n`, "utf-8");
|
|
30
|
+
fs.writeFileSync(targetSummary, [
|
|
31
|
+
`# Imported pi-crew run ${runId}`,
|
|
32
|
+
"",
|
|
33
|
+
`Imported: ${importedAt}`,
|
|
34
|
+
`Source: ${resolvedPath}`,
|
|
35
|
+
`Original export: ${raw.exportedAt}`,
|
|
36
|
+
`Status: ${raw.manifest.status}`,
|
|
37
|
+
`Team: ${raw.manifest.team}`,
|
|
38
|
+
`Workflow: ${raw.manifest.workflow ?? "(none)"}`,
|
|
39
|
+
`Goal: ${raw.manifest.goal}`,
|
|
40
|
+
"",
|
|
41
|
+
"## Tasks",
|
|
42
|
+
...raw.tasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
|
|
43
|
+
"",
|
|
44
|
+
].join("\n"), "utf-8");
|
|
45
|
+
return { runId, importedAt, bundlePath: targetJson, summaryPath: targetSummary };
|
|
46
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
4
|
+
import { projectPiRoot, userPiRoot } from "../utils/paths.ts";
|
|
5
|
+
|
|
6
|
+
function readManifest(filePath: string): TeamRunManifest | undefined {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8")) as TeamRunManifest;
|
|
9
|
+
} catch {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function collectRuns(root: string): TeamRunManifest[] {
|
|
15
|
+
const runsRoot = path.join(root, "state", "runs");
|
|
16
|
+
if (!fs.existsSync(runsRoot)) return [];
|
|
17
|
+
return fs.readdirSync(runsRoot)
|
|
18
|
+
.map((entry) => readManifest(path.join(runsRoot, entry, "manifest.json")))
|
|
19
|
+
.filter((manifest): manifest is TeamRunManifest => manifest !== undefined);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function listRuns(cwd: string): TeamRunManifest[] {
|
|
23
|
+
const projectRuns = collectRuns(path.join(projectPiRoot(cwd), "teams"));
|
|
24
|
+
const userRuns = collectRuns(path.join(userPiRoot(), "extensions", "pi-crew", "runs"));
|
|
25
|
+
const byId = new Map<string, TeamRunManifest>();
|
|
26
|
+
for (const run of [...userRuns, ...projectRuns]) byId.set(run.runId, run);
|
|
27
|
+
return [...byId.values()].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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(isFinished).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
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { listRuns } from "./run-index.ts";
|
|
3
|
+
|
|
4
|
+
export function notifyActiveRuns(ctx: ExtensionContext): void {
|
|
5
|
+
const active = listRuns(ctx.cwd).filter((run) => run.status === "queued" || run.status === "planning" || run.status === "running").slice(0, 5);
|
|
6
|
+
if (active.length === 0) return;
|
|
7
|
+
ctx.ui.notify(`pi-crew active runs: ${active.map((run) => `${run.runId} [${run.status}]`).join(", ")}`, "info");
|
|
8
|
+
}
|