pi-crew 0.1.28 → 0.1.30

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 (59) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/NOTICE.md +1 -0
  3. package/docs/architecture.md +164 -92
  4. package/docs/refactor-tasks-phase6.md +662 -0
  5. package/docs/runtime-flow.md +148 -0
  6. package/package.json +1 -1
  7. package/schema.json +1 -0
  8. package/skills/git-master/SKILL.md +19 -0
  9. package/skills/read-only-explorer/SKILL.md +21 -0
  10. package/skills/safe-bash/SKILL.md +16 -0
  11. package/skills/task-packet/SKILL.md +23 -0
  12. package/skills/verify-evidence/SKILL.md +22 -0
  13. package/src/config/config.ts +2 -0
  14. package/src/config/defaults.ts +1 -0
  15. package/src/extension/async-notifier.ts +33 -4
  16. package/src/extension/register.ts +15 -522
  17. package/src/extension/registration/artifact-cleanup.ts +14 -0
  18. package/src/extension/registration/commands.ts +208 -0
  19. package/src/extension/registration/subagent-helpers.ts +1 -1
  20. package/src/extension/registration/subagent-tools.ts +110 -0
  21. package/src/extension/registration/team-tool.ts +44 -0
  22. package/src/extension/team-tool/api.ts +4 -4
  23. package/src/extension/team-tool/cancel.ts +31 -0
  24. package/src/extension/team-tool/inspect.ts +41 -0
  25. package/src/extension/team-tool/lifecycle-actions.ts +79 -0
  26. package/src/extension/team-tool/plan.ts +19 -0
  27. package/src/extension/team-tool/run.ts +41 -3
  28. package/src/extension/team-tool/status.ts +73 -0
  29. package/src/extension/team-tool.ts +57 -224
  30. package/src/runtime/async-marker.ts +26 -0
  31. package/src/runtime/async-runner.ts +44 -9
  32. package/src/runtime/background-runner.ts +2 -0
  33. package/src/runtime/child-pi.ts +5 -1
  34. package/src/runtime/concurrency.ts +9 -3
  35. package/src/runtime/crew-agent-records.ts +1 -0
  36. package/src/runtime/crew-agent-runtime.ts +2 -1
  37. package/src/runtime/model-fallback.ts +21 -4
  38. package/src/runtime/pi-args.ts +2 -0
  39. package/src/runtime/process-status.ts +1 -0
  40. package/src/runtime/role-permission.ts +11 -0
  41. package/src/runtime/task-runner/live-executor.ts +98 -0
  42. package/src/runtime/task-runner/progress.ts +111 -0
  43. package/src/runtime/task-runner/prompt-builder.ts +72 -0
  44. package/src/runtime/task-runner/result-utils.ts +14 -0
  45. package/src/runtime/task-runner/state-helpers.ts +22 -0
  46. package/src/runtime/task-runner.ts +38 -283
  47. package/src/runtime/team-runner.ts +116 -7
  48. package/src/schema/config-schema.ts +1 -0
  49. package/src/state/mailbox.ts +28 -0
  50. package/src/state/types.ts +16 -0
  51. package/src/subagents/async-entry.ts +1 -0
  52. package/src/subagents/index.ts +3 -0
  53. package/src/subagents/live/control.ts +1 -0
  54. package/src/subagents/live/manager.ts +1 -0
  55. package/src/subagents/live/realtime.ts +1 -0
  56. package/src/subagents/live/session-runtime.ts +1 -0
  57. package/src/subagents/manager.ts +1 -0
  58. package/src/subagents/spawn.ts +1 -0
  59. package/src/ui/live-run-sidebar.ts +1 -1
@@ -1,34 +1,24 @@
1
- import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
- import { Type } from "typebox";
1
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
3
2
  import { loadConfig } from "../config/config.ts";
4
3
  import { registerAutonomousPolicy } from "./autonomous-policy.ts";
5
- import { TeamToolParams, type TeamToolParamsValue } from "../schema/team-tool-schema.ts";
6
4
  import { startAsyncRunNotifier, stopAsyncRunNotifier, type AsyncNotifierState } from "./async-notifier.ts";
7
5
  import { notifyActiveRuns } from "./session-summary.ts";
8
- import { piTeamsHelp } from "./help.ts";
9
- import { handleTeamManagerCommand } from "./team-manager-command.ts";
10
- import { handleTeamTool } from "./team-tool.ts";
11
- import { RunDashboard, type RunDashboardSelection } from "../ui/run-dashboard.ts";
12
- import { AnimatedMascot } from "../ui/mascot.ts";
13
6
  import { LiveRunSidebar } from "../ui/live-run-sidebar.ts";
14
7
  import { registerPiCrewRpc, type PiCrewRpcHandle } from "./cross-extension-rpc.ts";
15
8
  import { stopCrewWidget, updateCrewWidget, type CrewWidgetState } from "../ui/crew-widget.ts";
16
9
  import { clearPiCrewPowerbar, registerPiCrewPowerbarSegments, updatePiCrewPowerbar } from "../ui/powerbar-publisher.ts";
17
- import { DurableTextViewer } from "../ui/transcript-viewer.ts";
18
10
  import { loadRunManifestById, updateRunStatus } from "../state/state-store.ts";
19
- import { readCrewAgents } from "../runtime/crew-agent-records.ts";
20
- import { terminateActiveChildPiProcesses } from "../runtime/child-pi.ts";
21
- import { readPersistedSubagentRecord, savePersistedSubagentRecord, SubagentManager, type SubagentSpawnOptions } from "../runtime/subagent-manager.ts";
22
- import { commandText, notifyCommandResult, parseRunArgs, parseScalar, pushUnset, setNestedConfig } from "./registration/command-utils.ts";
23
- import { __test__subagentSpawnParams, formatSubagentRecord, readSubagentRunResult, refreshPersistedSubagentRecord, sendFollowUp, subagentToolResult } from "./registration/subagent-helpers.ts";
24
- import { DEFAULT_ARTIFACT_CLEANUP, DEFAULT_UI } from "../config/defaults.ts";
25
- import { CLEANUP_MARKER_FILE, cleanupOldArtifacts } from "../state/artifact-store.ts";
26
- import { openTranscriptViewer, selectAgentTask } from "./registration/viewers.ts";
11
+ import { terminateActiveChildPiProcesses } from "../subagents/spawn.ts";
12
+ import { SubagentManager } from "../subagents/manager.ts";
13
+ import { __test__subagentSpawnParams, sendFollowUp } from "./registration/subagent-helpers.ts";
14
+ import { DEFAULT_UI } from "../config/defaults.ts";
27
15
  import { logInternalError } from "../utils/internal-error.ts";
28
16
  import { createManifestCache } from "../runtime/manifest-cache.ts";
29
- import { printTimings, resetTimings, time } from "../utils/timings.ts";
30
- import * as path from "node:path";
31
- import { projectPiRoot, userPiRoot } from "../utils/paths.ts";
17
+ import { resetTimings, time } from "../utils/timings.ts";
18
+ import { registerTeamCommands } from "./registration/commands.ts";
19
+ import { registerSubagentTools } from "./registration/subagent-tools.ts";
20
+ import { runArtifactCleanup } from "./registration/artifact-cleanup.ts";
21
+ import { registerTeamTool } from "./registration/team-tool.ts";
32
22
 
33
23
  export { __test__subagentSpawnParams };
34
24
 
@@ -152,20 +142,6 @@ export function registerPiTeams(pi: ExtensionAPI): void {
152
142
  registerAutonomousPolicy(pi);
153
143
  time("register.rpc");
154
144
  rpcHandle = registerPiCrewRpc((pi as unknown as { events?: Parameters<typeof registerPiCrewRpc>[0] }).events, () => currentCtx);
155
- const runArtifactCleanup = (cwd: string): void => {
156
- try {
157
- cleanupOldArtifacts(path.join(userPiRoot(), "extensions", "pi-crew", "artifacts"), {
158
- maxAgeDays: DEFAULT_ARTIFACT_CLEANUP.maxAgeDays,
159
- markerFile: CLEANUP_MARKER_FILE,
160
- });
161
- cleanupOldArtifacts(path.join(projectPiRoot(cwd), "artifacts"), {
162
- maxAgeDays: DEFAULT_ARTIFACT_CLEANUP.maxAgeDays,
163
- markerFile: CLEANUP_MARKER_FILE,
164
- });
165
- } catch (error) {
166
- logInternalError("register.artifact-cleanup", error, `cwd=${cwd}`);
167
- }
168
- };
169
145
 
170
146
  const cleanupRuntime = (): void => {
171
147
  if (cleanedUp) return;
@@ -210,495 +186,12 @@ const runArtifactCleanup = (cwd: string): void => {
210
186
  }, DEFAULT_UI.widgetDefaultFrameMs);
211
187
  widgetState.interval.unref?.();
212
188
  });
213
- pi.on("session_before_switch", () => {
214
- stopSessionBoundSubagents();
215
- });
216
- pi.on("session_shutdown", () => {
217
- cleanupRuntime();
218
- });
219
-
220
- const tool: ToolDefinition = {
221
- name: "team",
222
- label: "Team",
223
- 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.",
224
- 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.",
225
- parameters: TeamToolParams as never,
226
- async execute(_id, params, signal, _onUpdate, ctx) {
227
- const controller = new AbortController();
228
- foregroundControllers.add(controller);
229
- const abort = (): void => controller.abort();
230
- signal?.addEventListener("abort", abort, { once: true });
231
- try {
232
- const output = await handleTeamTool(params as TeamToolParamsValue, { ...ctx, signal: controller.signal, startForegroundRun: (runner, runId) => startForegroundRun(ctx, runner, runId), onRunStarted: (runId) => openLiveSidebar(ctx, runId) });
233
- const config = loadConfig(ctx.cwd).config.ui;
234
- const cache = getManifestCache(ctx.cwd);
235
- updateCrewWidget(ctx, widgetState, config, cache);
236
- updatePiCrewPowerbar(pi.events, ctx.cwd, config, cache);
237
- return output;
238
- } finally {
239
- signal?.removeEventListener("abort", abort);
240
- foregroundControllers.delete(controller);
241
- }
242
- },
243
- };
244
-
245
- pi.registerTool(tool);
246
-
247
- const agentTool: ToolDefinition = {
248
- name: "Agent",
249
- label: "Agent",
250
- description: "Launch a real pi-crew subagent. Uses pi-crew's durable child-process runtime by default; set run_in_background=true for parallel/background work, then use get_subagent_result.",
251
- promptSnippet: "Use Agent to delegate focused work to a real pi-crew subagent. Use run_in_background=true for parallel work and get_subagent_result to join results.",
252
- promptGuidelines: [
253
- "Use Agent for independent exploration, review, verification, or implementation subtasks instead of doing all work in the parent turn.",
254
- "For parallel work, launch multiple Agent calls with run_in_background=true, then call get_subagent_result for each result.",
255
- "Available pi-crew subagent types include explorer, planner, analyst, executor, reviewer, verifier, writer, security-reviewer, and test-engineer.",
256
- ],
257
- parameters: Type.Object({
258
- prompt: Type.String({ description: "The task for the subagent to perform." }),
259
- description: Type.String({ description: "Short 3-5 word task description." }),
260
- subagent_type: Type.String({ description: "pi-crew agent name, e.g. explorer, planner, executor, reviewer, verifier, writer, security-reviewer, test-engineer." }),
261
- model: Type.Optional(Type.String({ description: "Optional model override. If omitted, pi-crew uses Pi-configured model fallback." })),
262
- max_turns: Type.Optional(Type.Number({ description: "Reserved for live-session subagents; child-process runtime may ignore this." })),
263
- run_in_background: Type.Optional(Type.Boolean({ description: "Run in background and return an agent ID immediately." })),
264
- }) as never,
265
- async execute(_id, params, signal, _onUpdate, ctx) {
266
- const options = __test__subagentSpawnParams(params as Record<string, unknown>, ctx);
267
- if (!options.prompt.trim()) return subagentToolResult("Agent requires prompt.", {}, true);
268
- const runner = async (spawnOptions: SubagentSpawnOptions, childSignal?: AbortSignal) => handleTeamTool({
269
- action: "run",
270
- agent: spawnOptions.type,
271
- goal: spawnOptions.prompt,
272
- model: spawnOptions.model,
273
- async: spawnOptions.background,
274
- config: spawnOptions.maxTurns ? { runtime: { maxTurns: spawnOptions.maxTurns } } : undefined,
275
- }, spawnOptions.background ? { ...ctx, signal: childSignal } : { ...ctx, signal: childSignal });
276
- const record = subagentManager.spawn(options, runner, options.background ? undefined : signal);
277
- if (options.background || record.status === "queued") {
278
- return subagentToolResult([`Agent ${record.status === "queued" ? "queued" : "started"}.`, `Agent ID: ${record.id}`, `Type: ${record.type}`, `Description: ${record.description}`, "Use get_subagent_result to retrieve output. Do not duplicate this agent's work."].join("\n"), { agentId: record.id, status: record.status });
279
- }
280
- await record.promise;
281
- const output = readSubagentRunResult(ctx, record) ?? record.result ?? "No output.";
282
- return subagentToolResult([`Agent ${record.id} ${record.status}.`, "", output].join("\n"), { agentId: record.id, runId: record.runId, status: record.status }, record.status === "failed" || record.status === "error");
283
- },
284
- };
189
+ pi.on("session_before_switch", () => stopSessionBoundSubagents());
190
+ pi.on("session_shutdown", () => cleanupRuntime());
285
191
 
286
- const getSubagentResultTool: ToolDefinition = {
287
- name: "get_subagent_result",
288
- label: "Get Agent Result",
289
- description: "Check status and retrieve results from a pi-crew background subagent.",
290
- parameters: Type.Object({
291
- agent_id: Type.String({ description: "Agent ID returned by Agent." }),
292
- wait: Type.Optional(Type.Boolean({ description: "Wait for completion before returning." })),
293
- verbose: Type.Optional(Type.Boolean({ description: "Include status metadata before output." })),
294
- }) as never,
295
- async execute(_id, params, signal, _onUpdate, ctx) {
296
- const p = params as { agent_id?: string; wait?: boolean; verbose?: boolean };
297
- if (!p.agent_id) return subagentToolResult("get_subagent_result requires agent_id.", {}, true);
298
- const inMemory = subagentManager.getRecord(p.agent_id);
299
- const record = inMemory ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id);
300
- if (!record) return subagentToolResult(`Agent not found: ${p.agent_id}`, {}, true);
301
- let current = refreshPersistedSubagentRecord(ctx, record);
302
- if (!inMemory && !current.runId && (current.status === "running" || current.status === "queued")) {
303
- current = { ...current, status: "error", error: "Subagent was interrupted before its durable run id was recorded; it cannot be recovered after restart.", completedAt: current.completedAt ?? Date.now() };
304
- savePersistedSubagentRecord(ctx.cwd, current);
305
- }
306
- if (p.wait && (current.status === "running" || current.status === "queued")) {
307
- current.resultConsumed = true;
308
- savePersistedSubagentRecord(ctx.cwd, current);
309
- const waited = await subagentManager.waitForRecord(current.id);
310
- if (waited) current = waited;
311
- else {
312
- while (current.status === "running" || current.status === "queued") {
313
- if (signal?.aborted) {
314
- current = { ...current, status: "error", error: "Waiting for subagent result was aborted.", completedAt: Date.now() };
315
- savePersistedSubagentRecord(ctx.cwd, current);
316
- break;
317
- }
318
- await new Promise((resolve) => setTimeout(resolve, 1000));
319
- current = refreshPersistedSubagentRecord(ctx, current);
320
- if (!current.runId) break;
321
- }
322
- }
323
- }
324
- const output = readSubagentRunResult(ctx, current);
325
- if (current.status !== "running" && current.status !== "queued") {
326
- current.resultConsumed = true;
327
- savePersistedSubagentRecord(ctx.cwd, current);
328
- }
329
- const text = [p.verbose ? formatSubagentRecord(current) : undefined, output ? `${p.verbose ? "\n" : ""}${output}` : current.status === "running" || current.status === "queued" ? "Agent is still running. Use wait=true or check again later." : current.error ?? "No output."].filter((line): line is string => Boolean(line)).join("\n");
330
- return subagentToolResult(text, { agentId: current.id, runId: current.runId, status: current.status }, current.status === "failed" || current.status === "error");
331
- },
332
- };
333
-
334
- const steerSubagentTool: ToolDefinition = {
335
- name: "steer_subagent",
336
- label: "Steer Agent",
337
- description: "Send a steering note to a running pi-crew subagent. Live-session steering is planned; child-process runs expose durable status and can be cancelled if needed.",
338
- parameters: Type.Object({ agent_id: Type.String(), message: Type.String() }) as never,
339
- async execute(_id, params, _signal, _onUpdate, ctx) {
340
- const p = params as { agent_id?: string; message?: string };
341
- const record = p.agent_id ? subagentManager.getRecord(p.agent_id) ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id) : undefined;
342
- if (!record) return subagentToolResult(`Agent not found: ${p.agent_id ?? ""}`, {}, true);
343
- return subagentToolResult([`Steering request noted for ${record.id}.`, "Current default pi-crew backend is child-process, so mid-turn session.steer is not available yet.", record.runId ? `Use team cancel runId=${record.runId} if the agent must be interrupted.` : undefined].filter((line): line is string => Boolean(line)).join("\n"), { agentId: record.id, runId: record.runId, status: record.status });
344
- },
345
- };
346
-
347
- const crewAgentTool: ToolDefinition = {
348
- ...agentTool,
349
- name: "crew_agent",
350
- label: "Crew Agent",
351
- description: "Launch a real pi-crew subagent using a conflict-safe pi-crew-specific tool name.",
352
- promptSnippet: "Use crew_agent when you need pi-crew subagents and another extension may own the generic Agent tool.",
353
- };
354
- const crewAgentResultTool: ToolDefinition = {
355
- ...getSubagentResultTool,
356
- name: "crew_agent_result",
357
- label: "Get Crew Agent Result",
358
- description: "Check status and retrieve results from a pi-crew subagent using the conflict-safe tool name.",
359
- };
360
- const crewAgentSteerTool: ToolDefinition = {
361
- ...steerSubagentTool,
362
- name: "crew_agent_steer",
363
- label: "Steer Crew Agent",
364
- description: "Send a steering note to a pi-crew subagent using the conflict-safe tool name.",
365
- };
366
- for (const extraTool of [crewAgentTool, crewAgentResultTool, crewAgentSteerTool]) pi.registerTool(extraTool);
367
- for (const extraTool of [agentTool, getSubagentResultTool, steerSubagentTool]) {
368
- try {
369
- pi.registerTool(extraTool);
370
- } catch (error) {
371
- logInternalError("register.duplicate-tool", error, `tool=${extraTool.name}`);
372
- }
373
- }
192
+ registerTeamTool(pi, { foregroundControllers, startForegroundRun, openLiveSidebar, getManifestCache, widgetState });
193
+ registerSubagentTools(pi, subagentManager);
374
194
  time("register.tools");
375
195
 
376
- pi.registerCommand("teams", {
377
- description: "List pi-crew teams, workflows, and agents",
378
- handler: async (_args: string, ctx: ExtensionCommandContext) => {
379
- const result = await handleTeamTool({ action: "list" }, ctx);
380
- await notifyCommandResult(ctx, commandText(result));
381
- },
382
- });
383
-
384
- pi.registerCommand("team-run", {
385
- description: "Manually start a pi-crew run (agent may also use the team tool autonomously)",
386
- handler: async (args: string, ctx: ExtensionCommandContext) => {
387
- const result = await handleTeamTool(parseRunArgs(args), { ...ctx, startForegroundRun: (runner, runId) => startForegroundRun(ctx as ExtensionContext, runner, runId), onRunStarted: (runId) => openLiveSidebar(ctx as ExtensionContext, runId) });
388
- await notifyCommandResult(ctx, commandText(result));
389
- },
390
- });
391
-
392
- pi.registerCommand("team-status", {
393
- description: "Show pi-crew run status",
394
- handler: async (args: string, ctx: ExtensionCommandContext) => {
395
- const runId = args.trim() || undefined;
396
- const result = await handleTeamTool({ action: "status", runId }, ctx);
397
- await notifyCommandResult(ctx, commandText(result));
398
- },
399
- });
400
-
401
- pi.registerCommand("team-resume", {
402
- description: "Resume a pi-crew run by re-queueing failed/cancelled/skipped/running tasks",
403
- handler: async (args: string, ctx: ExtensionCommandContext) => {
404
- const runId = args.trim() || undefined;
405
- const result = await handleTeamTool({ action: "resume", runId }, ctx);
406
- await notifyCommandResult(ctx, commandText(result));
407
- },
408
- });
409
-
410
- pi.registerCommand("team-summary", {
411
- description: "Show pi-crew run summary",
412
- handler: async (args: string, ctx: ExtensionCommandContext) => {
413
- const runId = args.trim() || undefined;
414
- const result = await handleTeamTool({ action: "summary", runId }, ctx);
415
- await notifyCommandResult(ctx, commandText(result));
416
- },
417
- });
418
-
419
- pi.registerCommand("team-events", {
420
- description: "Show full pi-crew event log for a run",
421
- handler: async (args: string, ctx: ExtensionCommandContext) => {
422
- const runId = args.trim() || undefined;
423
- const result = await handleTeamTool({ action: "events", runId }, ctx);
424
- await notifyCommandResult(ctx, commandText(result));
425
- },
426
- });
427
-
428
- pi.registerCommand("team-artifacts", {
429
- description: "List pi-crew artifacts for a run",
430
- handler: async (args: string, ctx: ExtensionCommandContext) => {
431
- const runId = args.trim() || undefined;
432
- const result = await handleTeamTool({ action: "artifacts", runId }, ctx);
433
- await notifyCommandResult(ctx, commandText(result));
434
- },
435
- });
436
-
437
- pi.registerCommand("team-worktrees", {
438
- description: "List pi-crew worktrees for a run",
439
- handler: async (args: string, ctx: ExtensionCommandContext) => {
440
- const runId = args.trim() || undefined;
441
- const result = await handleTeamTool({ action: "worktrees", runId }, ctx);
442
- await notifyCommandResult(ctx, commandText(result));
443
- },
444
- });
445
-
446
- pi.registerCommand("team-api", {
447
- description: "Run safe pi-crew API interop operations: <runId> <operation> [key=value]",
448
- handler: async (args: string, ctx: ExtensionCommandContext) => {
449
- const tokens = args.trim().split(/\s+/).filter(Boolean);
450
- const runId = tokens.find((token) => !token.includes("=") && !token.startsWith("--"));
451
- const operation = tokens.find((token) => token !== runId && !token.includes("=") && !token.startsWith("--")) ?? "read-manifest";
452
- const config: Record<string, unknown> = { operation };
453
- for (const token of tokens.filter((item) => item.includes("="))) {
454
- const [key, ...rest] = token.split("=");
455
- if (key) config[key] = parseScalar(rest.join("="));
456
- }
457
- const result = await handleTeamTool({ action: "api", runId, config }, ctx);
458
- await notifyCommandResult(ctx, commandText(result));
459
- },
460
- });
461
-
462
- pi.registerCommand("team-imports", {
463
- description: "List imported pi-crew run bundles",
464
- handler: async (_args: string, ctx: ExtensionCommandContext) => {
465
- const result = await handleTeamTool({ action: "imports" }, ctx);
466
- await notifyCommandResult(ctx, commandText(result));
467
- },
468
- });
469
-
470
- pi.registerCommand("team-import", {
471
- description: "Import a pi-crew run-export.json bundle into local imports",
472
- handler: async (args: string, ctx: ExtensionCommandContext) => {
473
- const tokens = args.trim().split(/\s+/).filter(Boolean);
474
- const pathArg = tokens.find((token) => !token.startsWith("--"));
475
- const scope = tokens.includes("--user") ? "user" : "project";
476
- const result = await handleTeamTool({ action: "import", config: { path: pathArg, scope } }, ctx);
477
- await notifyCommandResult(ctx, commandText(result));
478
- },
479
- });
480
-
481
- pi.registerCommand("team-export", {
482
- description: "Export a pi-crew run bundle to artifacts/export",
483
- handler: async (args: string, ctx: ExtensionCommandContext) => {
484
- const runId = args.trim() || undefined;
485
- const result = await handleTeamTool({ action: "export", runId }, ctx);
486
- await notifyCommandResult(ctx, commandText(result));
487
- },
488
- });
489
-
490
- pi.registerCommand("team-prune", {
491
- description: "Prune old finished pi-crew runs, keeping the newest N",
492
- handler: async (args: string, ctx: ExtensionCommandContext) => {
493
- const tokens = args.trim().split(/\s+/).filter(Boolean);
494
- const keepToken = tokens.find((token) => token.startsWith("--keep="));
495
- const keep = keepToken ? Number.parseInt(keepToken.slice("--keep=".length), 10) : undefined;
496
- const confirm = tokens.includes("--confirm");
497
- const result = await handleTeamTool({ action: "prune", keep, confirm }, ctx);
498
- await notifyCommandResult(ctx, commandText(result));
499
- },
500
- });
501
-
502
- pi.registerCommand("team-forget", {
503
- description: "Forget a pi-crew run by deleting its state and artifacts",
504
- handler: async (args: string, ctx: ExtensionCommandContext) => {
505
- const tokens = args.trim().split(/\s+/).filter(Boolean);
506
- const runId = tokens.find((token) => !token.startsWith("--"));
507
- const force = tokens.includes("--force");
508
- const confirm = tokens.includes("--confirm");
509
- const result = await handleTeamTool({ action: "forget", runId, force, confirm }, ctx);
510
- await notifyCommandResult(ctx, commandText(result));
511
- },
512
- });
513
-
514
- pi.registerCommand("team-cleanup", {
515
- description: "Clean up pi-crew worktrees for a run",
516
- handler: async (args: string, ctx: ExtensionCommandContext) => {
517
- const tokens = args.trim().split(/\s+/).filter(Boolean);
518
- const runId = tokens.find((token) => !token.startsWith("--"));
519
- const force = tokens.includes("--force");
520
- const result = await handleTeamTool({ action: "cleanup", runId, force }, ctx);
521
- await notifyCommandResult(ctx, commandText(result));
522
- },
523
- });
524
-
525
- pi.registerCommand("team-manager", {
526
- description: "Open a simple pi-crew interactive manager",
527
- handler: handleTeamManagerCommand,
528
- });
529
-
530
- pi.registerCommand("team-result", {
531
- description: "Open a pi-crew agent result viewer: <runId> [taskId]",
532
- handler: async (args: string, ctx: ExtensionCommandContext) => {
533
- const [runId, rawTaskId] = args.trim().split(/\s+/).filter(Boolean);
534
- const selected = await selectAgentTask(ctx, runId, rawTaskId);
535
- const loaded = selected ? loadRunManifestById(ctx.cwd, selected.runId) : undefined;
536
- if (ctx.hasUI && loaded) {
537
- const agent = readCrewAgents(loaded.manifest).find((item) => item.taskId === selected?.taskId || item.id === selected?.taskId) ?? readCrewAgents(loaded.manifest)[0];
538
- const text = agent?.resultArtifactPath ? commandText(await handleTeamTool({ action: "api", runId: selected!.runId, config: { operation: "read-agent-output", agentId: agent.taskId, maxBytes: 64_000 } }, ctx)) : "(no result)";
539
- await ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new DurableTextViewer("pi-crew result", `${selected!.runId}:${agent?.taskId ?? "unknown"}`, text.split(/\r?\n/), theme, done), { overlay: true, overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" } });
540
- return;
541
- }
542
- const result = await handleTeamTool({ action: "api", runId, config: { operation: "read-agent-output", agentId: rawTaskId, maxBytes: 64_000 } }, ctx);
543
- await notifyCommandResult(ctx, commandText(result));
544
- },
545
- });
546
-
547
- pi.registerCommand("team-transcript", {
548
- description: "Open a pi-crew transcript viewer: <runId> [taskId]",
549
- handler: async (args: string, ctx: ExtensionCommandContext) => {
550
- const [runId, taskId] = args.trim().split(/\s+/).filter(Boolean);
551
- if (await openTranscriptViewer(ctx, runId, taskId)) return;
552
- const result = await handleTeamTool({ action: "api", runId, config: { operation: "read-agent-transcript", agentId: taskId } }, ctx);
553
- await notifyCommandResult(ctx, commandText(result));
554
- },
555
- });
556
-
557
- pi.registerCommand("team-dashboard", {
558
- description: "Open a pi-crew run dashboard overlay",
559
- handler: async (_args: string, ctx: ExtensionCommandContext) => {
560
- for (;;) {
561
- const runs = getManifestCache(ctx.cwd).list(50);
562
- const uiConfig = loadConfig(ctx.cwd).config.ui;
563
- const rightPanel = uiConfig?.dashboardPlacement !== "center";
564
- const width = rightPanel ? Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? 56)) : "90%";
565
- const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((_tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme, { placement: rightPanel ? "right" : "center", showModel: uiConfig?.showModel, showTokens: uiConfig?.showTokens, showTools: uiConfig?.showTools }), {
566
- overlay: true,
567
- overlayOptions: rightPanel
568
- ? { width, minWidth: 40, maxHeight: "100%", anchor: "top-right", offsetX: 0, offsetY: 0, margin: { top: 0, right: 0, bottom: 0, left: 0 } }
569
- : { width, maxHeight: "90%", anchor: "center", margin: 2 },
570
- });
571
- if (!selection) return;
572
- if (selection.action === "reload") continue;
573
- if (selection.action === "agent-transcript" && await openTranscriptViewer(ctx, selection.runId)) continue;
574
- const result = selection.action === "api"
575
- ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-manifest" } }, ctx)
576
- : selection.action === "agents"
577
- ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "agent-dashboard" } }, ctx)
578
- : selection.action === "agent-events"
579
- ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-events", limit: 50 } }, ctx)
580
- : selection.action === "agent-output"
581
- ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-output", maxBytes: 32_000 } }, ctx)
582
- : selection.action === "agent-transcript"
583
- ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-transcript" } }, ctx)
584
- : await handleTeamTool({ action: selection.action, runId: selection.runId }, ctx);
585
- await notifyCommandResult(ctx, commandText(result));
586
- return;
587
- }
588
- },
589
- });
590
-
591
- pi.registerCommand("team-mascot", {
592
- description: "Show an animated mascot splash",
593
- handler: async (args: string, ctx: ExtensionCommandContext) => {
594
- if (!ctx.hasUI) return;
595
- const tokens = args.trim().split(/\s+/).filter(Boolean);
596
- const uiConfig = loadConfig(ctx.cwd).config.ui;
597
- const styleArg = tokens.find((t) => t === "cat" || t === "armin");
598
- const effectArg = tokens.find((t) => ["random", "none", "typewriter", "scanline", "rain", "fade", "crt", "glitch", "dissolve"].includes(t));
599
- const style = (styleArg as "cat" | "armin" | undefined) ?? uiConfig?.mascotStyle ?? "cat";
600
- const effect = (effectArg as "random" | "none" | "typewriter" | "scanline" | "rain" | "fade" | "crt" | "glitch" | "dissolve" | undefined) ?? uiConfig?.mascotEffect ?? "random";
601
- const overlayWidth = style === "armin" ? 48 : 62;
602
- await ctx.ui.custom<undefined>((tui, theme, _keybindings, done) => {
603
- const requestRender = () => (tui as { requestRender?: () => void }).requestRender?.();
604
- return new AnimatedMascot(theme, () => done(undefined), {
605
- frameIntervalMs: style === "armin" ? 33 : 180,
606
- autoCloseMs: 7000,
607
- requestRender,
608
- style,
609
- effect,
610
- });
611
- }, {
612
- overlay: true,
613
- overlayOptions: { width: overlayWidth, maxHeight: "85%", anchor: "center" },
614
- });
615
- },
616
- });
617
-
618
- pi.registerCommand("team-init", {
619
- description: "Initialize project-local pi-crew directories and gitignore entries",
620
- handler: async (args: string, ctx: ExtensionCommandContext) => {
621
- const tokens = args.trim().split(/\s+/).filter(Boolean);
622
- const result = await handleTeamTool({ action: "init", config: { copyBuiltins: tokens.includes("--copy-builtins"), overwrite: tokens.includes("--overwrite") } }, ctx);
623
- await notifyCommandResult(ctx, commandText(result));
624
- },
625
- });
626
-
627
- pi.registerCommand("team-autonomy", {
628
- description: "Show or toggle pi-crew autonomous delegation policy: status|on|off",
629
- handler: async (args: string, ctx: ExtensionCommandContext) => {
630
- const tokens = args.trim().split(/\s+/).filter(Boolean);
631
- const mode = tokens[0]?.toLowerCase();
632
- const config = mode === "on" ? { profile: "suggested", enabled: true, injectPolicy: true }
633
- : mode === "off" ? { profile: "manual", enabled: false }
634
- : mode === "manual" || mode === "suggested" || mode === "assisted" || mode === "aggressive" ? { profile: mode, enabled: mode !== "manual", injectPolicy: mode !== "manual" }
635
- : {
636
- preferAsyncForLongTasks: tokens.includes("--prefer-async") ? true : undefined,
637
- allowWorktreeSuggestion: tokens.includes("--no-worktree-suggest") ? false : undefined,
638
- };
639
- const result = await handleTeamTool({ action: "autonomy", config }, ctx);
640
- await notifyCommandResult(ctx, commandText(result));
641
- },
642
- });
643
-
644
- pi.registerCommand("team-config", {
645
- description: "Show or update pi-crew config. Use key=value [--project] to update.",
646
- handler: async (args: string, ctx: ExtensionCommandContext) => {
647
- const tokens = args.trim().split(/\s+/).filter(Boolean);
648
- if (tokens.length === 0) {
649
- const result = await handleTeamTool({ action: "config" }, ctx);
650
- await notifyCommandResult(ctx, commandText(result));
651
- return;
652
- }
653
- const config: Record<string, unknown> = { scope: tokens.includes("--project") ? "project" : "user" };
654
- for (const token of tokens) {
655
- if (token.startsWith("--unset=")) {
656
- pushUnset(config, token.slice("--unset=".length));
657
- continue;
658
- }
659
- if (!token.includes("=")) continue;
660
- const [key, ...rest] = token.split("=");
661
- if (!key) continue;
662
- const raw = rest.join("=");
663
- if (raw === "unset" || raw === "null") pushUnset(config, key);
664
- else setNestedConfig(config, key, parseScalar(raw));
665
- }
666
- const result = await handleTeamTool({ action: "config", config }, ctx);
667
- await notifyCommandResult(ctx, commandText(result));
668
- },
669
- });
670
-
671
- pi.registerCommand("team-validate", {
672
- description: "Validate pi-crew agents, teams, and workflows",
673
- handler: async (_args: string, ctx: ExtensionCommandContext) => {
674
- const result = await handleTeamTool({ action: "validate" }, ctx);
675
- await notifyCommandResult(ctx, commandText(result));
676
- },
677
- });
678
-
679
- pi.registerCommand("team-help", {
680
- description: "Show pi-crew command help",
681
- handler: async (_args: string, ctx: ExtensionCommandContext) => {
682
- await notifyCommandResult(ctx, piTeamsHelp());
683
- },
684
- });
685
-
686
- pi.registerCommand("team-cancel", {
687
- description: "Cancel a pi-crew run",
688
- handler: async (args: string, ctx: ExtensionCommandContext) => {
689
- const runId = args.trim() || undefined;
690
- const result = await handleTeamTool({ action: "cancel", runId }, ctx);
691
- await notifyCommandResult(ctx, commandText(result));
692
- },
693
- });
694
-
695
- pi.registerCommand("team-doctor", {
696
- description: "Check pi-crew installation and discovery readiness",
697
- handler: async (_args: string, ctx: ExtensionCommandContext) => {
698
- const result = await handleTeamTool({ action: "doctor" }, ctx);
699
- await notifyCommandResult(ctx, commandText(result));
700
- },
701
- });
702
- time("register.commands");
703
- printTimings();
196
+ registerTeamCommands(pi, { startForegroundRun, openLiveSidebar, getManifestCache });
704
197
  }
@@ -0,0 +1,14 @@
1
+ import * as path from "node:path";
2
+ import { DEFAULT_ARTIFACT_CLEANUP } from "../../config/defaults.ts";
3
+ import { CLEANUP_MARKER_FILE, cleanupOldArtifacts } from "../../state/artifact-store.ts";
4
+ import { logInternalError } from "../../utils/internal-error.ts";
5
+ import { projectPiRoot, userPiRoot } from "../../utils/paths.ts";
6
+
7
+ export function runArtifactCleanup(cwd: string): void {
8
+ try {
9
+ cleanupOldArtifacts(path.join(userPiRoot(), "extensions", "pi-crew", "artifacts"), { maxAgeDays: DEFAULT_ARTIFACT_CLEANUP.maxAgeDays, markerFile: CLEANUP_MARKER_FILE });
10
+ cleanupOldArtifacts(path.join(projectPiRoot(cwd), "artifacts"), { maxAgeDays: DEFAULT_ARTIFACT_CLEANUP.maxAgeDays, markerFile: CLEANUP_MARKER_FILE });
11
+ } catch (error) {
12
+ logInternalError("register.artifact-cleanup", error, `cwd=${cwd}`);
13
+ }
14
+ }