pi-crew 0.1.24 → 0.1.26

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 (73) hide show
  1. package/docs/refactor-tasks-phase3.md +394 -0
  2. package/docs/refactor-tasks-phase4.md +564 -0
  3. package/docs/refactor-tasks-phase5.md +402 -0
  4. package/docs/refactor-tasks.md +1484 -0
  5. package/package.json +98 -95
  6. package/src/agents/agent-config.ts +30 -30
  7. package/src/config/config.ts +153 -89
  8. package/src/config/defaults.ts +60 -0
  9. package/src/extension/autonomous-policy.ts +1 -1
  10. package/src/extension/help.ts +1 -0
  11. package/src/extension/management.ts +15 -2
  12. package/src/extension/register.ts +124 -170
  13. package/src/extension/registration/command-utils.ts +54 -0
  14. package/src/extension/registration/subagent-helpers.ts +70 -0
  15. package/src/extension/registration/viewers.ts +32 -0
  16. package/src/extension/result-watcher.ts +98 -89
  17. package/src/extension/team-tool/api.ts +276 -0
  18. package/src/extension/team-tool/config-patch.ts +36 -0
  19. package/src/extension/team-tool/context.ts +48 -0
  20. package/src/extension/team-tool/doctor.ts +178 -0
  21. package/src/extension/team-tool/run.ts +133 -0
  22. package/src/extension/team-tool-types.ts +6 -0
  23. package/src/extension/team-tool.ts +31 -623
  24. package/src/extension/tool-result.ts +16 -16
  25. package/src/runtime/async-runner.ts +42 -60
  26. package/src/runtime/child-pi.ts +434 -332
  27. package/src/runtime/concurrency.ts +50 -42
  28. package/src/runtime/crew-agent-records.ts +166 -156
  29. package/src/runtime/manifest-cache.ts +214 -0
  30. package/src/runtime/parallel-utils.ts +99 -0
  31. package/src/runtime/post-exit-stdio-guard.ts +86 -0
  32. package/src/runtime/runtime-resolver.ts +77 -74
  33. package/src/runtime/subagent-manager.ts +291 -236
  34. package/src/runtime/task-graph-scheduler.ts +122 -107
  35. package/src/runtime/team-runner.ts +46 -51
  36. package/src/schema/config-schema.ts +92 -0
  37. package/src/state/artifact-store.ts +108 -36
  38. package/src/state/atomic-write.ts +114 -49
  39. package/src/state/event-log.ts +189 -138
  40. package/src/state/jsonl-writer.ts +77 -0
  41. package/src/state/locks.ts +149 -40
  42. package/src/state/mailbox.ts +200 -188
  43. package/src/state/state-store.ts +104 -15
  44. package/src/teams/discover-teams.ts +94 -84
  45. package/src/teams/team-config.ts +26 -22
  46. package/src/ui/crew-footer.ts +101 -0
  47. package/src/ui/crew-select-list.ts +111 -0
  48. package/src/ui/crew-widget.ts +285 -219
  49. package/src/ui/dynamic-border.ts +25 -0
  50. package/src/ui/layout-primitives.ts +106 -0
  51. package/src/ui/live-run-sidebar.ts +163 -95
  52. package/src/ui/loaders.ts +158 -0
  53. package/src/ui/mascot.ts +441 -0
  54. package/src/ui/powerbar-publisher.ts +94 -71
  55. package/src/ui/render-diff.ts +119 -0
  56. package/src/ui/run-dashboard.ts +155 -120
  57. package/src/ui/status-colors.ts +54 -0
  58. package/src/ui/syntax-highlight.ts +116 -0
  59. package/src/ui/theme-adapter.ts +190 -0
  60. package/src/ui/transcript-viewer.ts +194 -111
  61. package/src/utils/completion-dedupe.ts +63 -0
  62. package/src/utils/file-coalescer.ts +84 -33
  63. package/src/utils/fs-watch.ts +31 -0
  64. package/src/utils/git.ts +262 -0
  65. package/src/utils/internal-error.ts +6 -0
  66. package/src/utils/paths.ts +33 -15
  67. package/src/utils/sleep.ts +32 -0
  68. package/src/utils/timings.ts +31 -0
  69. package/src/utils/visual.ts +159 -0
  70. package/src/workflows/discover-workflows.ts +109 -101
  71. package/src/workflows/workflow-config.ts +25 -24
  72. package/src/workflows/workflow-serializer.ts +32 -31
  73. package/tsconfig.json +19 -19
@@ -0,0 +1,60 @@
1
+ export const DEFAULT_CHILD_PI = {
2
+ postExitStdioGuardMs: 3000,
3
+ finalDrainMs: 5000,
4
+ hardKillMs: 3000,
5
+ responseTimeoutMs: 15_000,
6
+ maxCaptureBytes: 256 * 1024,
7
+ maxAssistantTextChars: 8192,
8
+ maxToolResultChars: 1024,
9
+ maxToolInputChars: 2048,
10
+ maxCompactContentChars: 4096,
11
+ };
12
+
13
+ export const DEFAULT_LOCKS = {
14
+ staleMs: 30_000,
15
+ };
16
+
17
+ export const DEFAULT_CONCURRENCY = {
18
+ workflow: {
19
+ parallelResearch: 4,
20
+ research: 2,
21
+ implementation: 2,
22
+ review: 2,
23
+ default: 2,
24
+ },
25
+ fallback: 1,
26
+ };
27
+
28
+ export const DEFAULT_EVENT_LOG = {
29
+ terminalEventTypes: ["run.blocked", "run.completed", "run.failed", "run.cancelled", "task.completed", "task.failed", "task.skipped", "task.cancelled"],
30
+ };
31
+
32
+ export const DEFAULT_ARTIFACT_CLEANUP = {
33
+ maxAgeDays: 7,
34
+ };
35
+
36
+ export const DEFAULT_PATHS = {
37
+ state: {
38
+ projectBase: "teams",
39
+ userBase: "runs",
40
+ runsSubdir: "state/runs",
41
+ artifactsSubdir: "artifacts",
42
+ manifestFile: "manifest.json",
43
+ tasksFile: "tasks.json",
44
+ eventsFile: "events.jsonl",
45
+ },
46
+ };
47
+
48
+ export const DEFAULT_UI = {
49
+ refreshMs: 1000,
50
+ notifierIntervalMs: 5000,
51
+ widgetDefaultFrameMs: 1000,
52
+ };
53
+
54
+ export const DEFAULT_CACHE = {
55
+ manifestMaxEntries: 64,
56
+ };
57
+
58
+ export const DEFAULT_SUBAGENT = {
59
+ stuckBlockedNotifyMs: 5 * 60_000,
60
+ };
@@ -68,7 +68,7 @@ export function buildAutonomousPolicy(prompt: string, config: PiTeamsAutonomousC
68
68
 
69
69
  function sourcePriority(source: string): number {
70
70
  if (source === "project") return 0;
71
- if (source === "user") return 1;
71
+ if (source === "user" || source === "git") return 1;
72
72
  return 2;
73
73
  }
74
74
 
@@ -18,6 +18,7 @@ export function piTeamsHelp(): string {
18
18
  "- /team-worktrees <runId>",
19
19
  "- /team-api <runId> <operation> [taskId=<taskId>] [body=<message>]",
20
20
  "- /team-dashboard",
21
+ "- /team-mascot",
21
22
  "- /team-transcript <runId> [taskId]",
22
23
  "- /team-result <runId> [taskId]",
23
24
  "- /team-manager",
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import type { AgentConfig, ResourceSource, RoutingMetadata } from "../agents/agent-config.ts";
4
4
  import { serializeAgent } from "../agents/agent-serializer.ts";
5
5
  import { allAgents, discoverAgents } from "../agents/discover-agents.ts";
6
- import type { TeamToolDetails } from "./team-tool.ts";
6
+ import type { TeamToolDetails } from "./team-tool-types.ts";
7
7
  import { toolResult, type PiTeamsToolResult } from "./tool-result.ts";
8
8
  import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
9
9
  import type { TeamConfig, TeamRole } from "../teams/team-config.ts";
@@ -118,6 +118,11 @@ function parseSteps(value: unknown): { steps?: WorkflowStep[]; error?: string }
118
118
  return { steps };
119
119
  }
120
120
 
121
+ function parseWorkflowMaxConcurrency(value: unknown): number | undefined {
122
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) return undefined;
123
+ return value;
124
+ }
125
+
121
126
  function findResource(ctx: ManagementContext, resource: "agent" | "team" | "workflow", name: string, scope?: string): MutableResource[] {
122
127
  const normalized = sanitizeName(name);
123
128
  const sourceMatches = (item: { name: string; source: ResourceSource }) => (scope === "user" || scope === "project" ? item.source === scope : item.source !== "builtin") && item.name === normalized;
@@ -232,7 +237,14 @@ export function handleCreate(params: TeamToolParamsValue, ctx: ManagementContext
232
237
  } else {
233
238
  const parsedSteps = parseSteps(cfg.steps);
234
239
  if (parsedSteps.error) return result(parsedSteps.error, "error", true);
235
- content = serializeWorkflow({ name, description: descriptionValue.value!, source: scope, filePath, steps: parsedSteps.steps! });
240
+ content = serializeWorkflow({
241
+ name,
242
+ description: descriptionValue.value!,
243
+ source: scope,
244
+ filePath,
245
+ maxConcurrency: parseWorkflowMaxConcurrency(cfg.maxConcurrency),
246
+ steps: parsedSteps.steps!,
247
+ });
236
248
  }
237
249
 
238
250
  if (params.dryRun) return result(`[dry-run] Would create ${params.resource} '${name}' at ${filePath}:\n\n${content}`);
@@ -305,6 +317,7 @@ export function handleUpdate(params: TeamToolParamsValue, ctx: ManagementContext
305
317
  name: nextName,
306
318
  filePath: nextPath,
307
319
  description: typeof cfg.description === "string" && cfg.description.trim() ? cfg.description.trim() : workflow.description,
320
+ maxConcurrency: hasOwn(cfg, "maxConcurrency") ? parseWorkflowMaxConcurrency(cfg.maxConcurrency) : workflow.maxConcurrency,
308
321
  steps,
309
322
  });
310
323
  }
@@ -1,4 +1,3 @@
1
- import * as fs from "node:fs";
2
1
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
3
2
  import { Type } from "typebox";
4
3
  import { loadConfig } from "../config/config.ts";
@@ -8,180 +7,76 @@ import { startAsyncRunNotifier, stopAsyncRunNotifier, type AsyncNotifierState }
8
7
  import { notifyActiveRuns } from "./session-summary.ts";
9
8
  import { piTeamsHelp } from "./help.ts";
10
9
  import { handleTeamManagerCommand } from "./team-manager-command.ts";
11
- import { handleTeamTool, type TeamToolDetails } from "./team-tool.ts";
12
- import { listRecentRuns } from "./run-index.ts";
10
+ import { handleTeamTool } from "./team-tool.ts";
13
11
  import { RunDashboard, type RunDashboardSelection } from "../ui/run-dashboard.ts";
12
+ import { AnimatedMascot } from "../ui/mascot.ts";
14
13
  import { LiveRunSidebar } from "../ui/live-run-sidebar.ts";
15
14
  import { registerPiCrewRpc, type PiCrewRpcHandle } from "./cross-extension-rpc.ts";
16
15
  import { stopCrewWidget, updateCrewWidget, type CrewWidgetState } from "../ui/crew-widget.ts";
17
16
  import { clearPiCrewPowerbar, registerPiCrewPowerbarSegments, updatePiCrewPowerbar } from "../ui/powerbar-publisher.ts";
18
- import { DurableTextViewer, DurableTranscriptViewer } from "../ui/transcript-viewer.ts";
17
+ import { DurableTextViewer } from "../ui/transcript-viewer.ts";
19
18
  import { loadRunManifestById, updateRunStatus } from "../state/state-store.ts";
20
19
  import { readCrewAgents } from "../runtime/crew-agent-records.ts";
21
20
  import { terminateActiveChildPiProcesses } from "../runtime/child-pi.ts";
22
- import { readPersistedSubagentRecord, savePersistedSubagentRecord, SubagentManager, type SubagentRecord, type SubagentSpawnOptions } from "../runtime/subagent-manager.ts";
23
-
24
- function parseRunArgs(args: string): TeamToolParamsValue {
25
- const tokens = args.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
26
- const params: TeamToolParamsValue = { action: "run" };
27
- const goalParts: string[] = [];
28
- for (const token of tokens) {
29
- if (token === "--async") params.async = true;
30
- else if (token === "--worktree") params.workspaceMode = "worktree";
31
- else if (token.startsWith("--team=")) params.team = token.slice("--team=".length);
32
- else if (token.startsWith("--workflow=")) params.workflow = token.slice("--workflow=".length);
33
- else if (token.startsWith("--agent=")) params.agent = token.slice("--agent=".length);
34
- else if (token.startsWith("--role=")) params.role = token.slice("--role=".length);
35
- else if (!params.team && goalParts.length === 0 && !token.startsWith("--")) params.team = token;
36
- else goalParts.push(token);
37
- }
38
- params.goal = goalParts.join(" ").trim() || undefined;
39
- return params;
40
- }
41
-
42
- function commandText(result: { content?: Array<{ type: string; text?: string }> }): string {
43
- return result.content?.map((item) => item.text ?? "").join("\n") ?? "";
44
- }
45
-
46
- async function notifyCommandResult(ctx: ExtensionCommandContext, text: string): Promise<void> {
47
- ctx.ui.notify(text.length > 800 ? `${text.slice(0, 797)}...` : text, "info");
48
- }
49
-
50
- function parseScalar(raw: string): unknown {
51
- if (raw === "true") return true;
52
- if (raw === "false") return false;
53
- if (/^-?\d+$/.test(raw)) return Number(raw);
54
- if (raw.includes(",")) return raw.split(",").map((entry) => entry.trim()).filter(Boolean);
55
- return raw;
56
- }
57
-
58
- async function selectAgentTask(ctx: ExtensionCommandContext, runId: string | undefined, taskId?: string): Promise<{ runId: string; taskId?: string } | undefined> {
59
- if (!runId) return undefined;
60
- if (taskId) return { runId, taskId };
61
- const loaded = loadRunManifestById(ctx.cwd, runId);
62
- if (!loaded) return { runId };
63
- const agents = readCrewAgents(loaded.manifest);
64
- if (ctx.hasUI && agents.length > 1) {
65
- const choice = await ctx.ui.select("Select pi-crew agent", agents.map((agent) => `${agent.taskId} ${agent.role}→${agent.agent} [${agent.status}]`));
66
- return { runId, taskId: choice?.split(" ")[0] };
67
- }
68
- return { runId, taskId: agents[0]?.taskId };
69
- }
70
-
71
- async function openTranscriptViewer(ctx: ExtensionCommandContext, runId: string | undefined, taskId?: string): Promise<boolean> {
72
- const selected = await selectAgentTask(ctx, runId, taskId);
73
- if (!selected) return false;
74
- // eslint-disable-next-line no-param-reassign
75
- runId = selected.runId;
76
- // eslint-disable-next-line no-param-reassign
77
- taskId = selected.taskId;
78
- if (!runId || !ctx.hasUI) return false;
79
- const loaded = loadRunManifestById(ctx.cwd, runId);
80
- if (!loaded) return false;
81
- await ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new DurableTranscriptViewer(loaded.manifest, theme, done, taskId), {
82
- overlay: true,
83
- overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" },
84
- });
85
- return true;
86
- }
87
-
88
- function pushUnset(config: Record<string, unknown>, key: string): void {
89
- const current = Array.isArray(config.unset) ? config.unset : [];
90
- current.push(key);
91
- config.unset = current;
92
- }
93
-
94
- function setNestedConfig(config: Record<string, unknown>, key: string, value: unknown): void {
95
- const parts = key.split(".").filter(Boolean);
96
- if (parts.length === 0) return;
97
- let target = config;
98
- for (const part of parts.slice(0, -1)) {
99
- const current = target[part];
100
- if (!current || typeof current !== "object" || Array.isArray(current)) target[part] = {};
101
- target = target[part] as Record<string, unknown>;
102
- }
103
- target[parts[parts.length - 1]!] = value;
104
- }
105
-
106
- function sendFollowUp(pi: ExtensionAPI, content: string): void {
107
- const sender = (pi as unknown as { sendMessage?: (message: unknown, options?: unknown) => void }).sendMessage;
108
- if (typeof sender !== "function") return;
109
- sender.call(pi, { customType: "pi-crew-subagent-notification", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
110
- }
111
-
112
- function refreshPersistedSubagentRecord(ctx: ExtensionContext | ExtensionCommandContext, record: SubagentRecord): SubagentRecord {
113
- if (!record.runId) return record;
114
- const loaded = loadRunManifestById(ctx.cwd, record.runId);
115
- if (!loaded) return record;
116
- if (loaded.manifest.status === "completed" || loaded.manifest.status === "failed" || loaded.manifest.status === "cancelled" || loaded.manifest.status === "blocked") {
117
- const refreshed = { ...record, status: loaded.manifest.status === "completed" ? "completed" as const : loaded.manifest.status === "cancelled" ? "cancelled" as const : "failed" as const, error: loaded.manifest.status === "completed" ? undefined : loaded.manifest.summary, completedAt: record.completedAt ?? Date.now() };
118
- savePersistedSubagentRecord(ctx.cwd, refreshed);
119
- return refreshed;
120
- }
121
- return record;
122
- }
123
-
124
- function formatSubagentRecord(record: SubagentRecord): string {
125
- const duration = record.completedAt ? `${Math.round((record.completedAt - record.startedAt) / 1000)}s` : "running";
126
- return [
127
- `Agent: ${record.id}`,
128
- `Type: ${record.type}`,
129
- `Status: ${record.status}`,
130
- record.runId ? `Run: ${record.runId}` : undefined,
131
- `Description: ${record.description}`,
132
- record.model ? `Model: ${record.model}` : undefined,
133
- `Duration: ${duration}`,
134
- record.error ? `Error: ${record.error}` : undefined,
135
- ].filter((line): line is string => Boolean(line)).join("\n");
136
- }
137
-
138
- function readSubagentRunResult(ctx: ExtensionContext | ExtensionCommandContext, record: SubagentRecord): string | undefined {
139
- if (!record.runId) return record.result;
140
- const loaded = loadRunManifestById(ctx.cwd, record.runId);
141
- const task = loaded?.tasks.find((item) => item.resultArtifact) ?? loaded?.tasks[0];
142
- const path = task?.resultArtifact?.path;
143
- if (!path) return undefined;
144
- try {
145
- return fs.readFileSync(path, "utf-8").trim();
146
- } catch {
147
- return undefined;
148
- }
149
- }
150
-
151
- function subagentToolResult(text: string, details: Record<string, unknown> = {}, isError = false) {
152
- return { content: [{ type: "text" as const, text }], details, isError };
153
- }
154
-
155
- export function __test__subagentSpawnParams(params: Record<string, unknown>, ctx: Pick<ExtensionContext, "cwd">): SubagentSpawnOptions {
156
- return {
157
- cwd: ctx.cwd,
158
- type: typeof params.subagent_type === "string" && params.subagent_type.trim() ? params.subagent_type.trim() : "executor",
159
- description: typeof params.description === "string" && params.description.trim() ? params.description.trim() : "pi-crew subagent",
160
- prompt: typeof params.prompt === "string" ? params.prompt : "",
161
- background: params.run_in_background === true,
162
- model: typeof params.model === "string" && params.model.trim() ? params.model.trim() : undefined,
163
- maxTurns: typeof params.max_turns === "number" && Number.isFinite(params.max_turns) ? params.max_turns : undefined,
164
- };
165
- }
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";
27
+ import { logInternalError } from "../utils/internal-error.ts";
28
+ 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";
32
+
33
+ export { __test__subagentSpawnParams };
166
34
 
167
35
  export function registerPiTeams(pi: ExtensionAPI): void {
36
+ resetTimings();
37
+ time("register:start");
168
38
  const globalStore = globalThis as Record<string, unknown>;
169
39
  const runtimeCleanupStoreKey = "__piCrewRuntimeCleanup";
170
40
  const previousRuntimeCleanup = globalStore[runtimeCleanupStoreKey];
41
+ time("register:init");
171
42
  if (typeof previousRuntimeCleanup === "function") {
172
- try { previousRuntimeCleanup(); } catch {}
43
+ try {
44
+ previousRuntimeCleanup();
45
+ } catch (error) {
46
+ logInternalError("register.prev-cleanup", error);
47
+ }
173
48
  }
174
49
  const notifierState: AsyncNotifierState = { seenFinishedRunIds: new Set() };
175
50
  let currentCtx: ExtensionContext | undefined;
176
51
  let rpcHandle: PiCrewRpcHandle | undefined;
177
52
  let cleanedUp = false;
53
+ let manifestCache = createManifestCache(process.cwd());
54
+ const getManifestCache = (cwd: string): ReturnType<typeof createManifestCache> => {
55
+ if (manifestCache && currentCtx?.cwd === cwd) return manifestCache;
56
+ if (manifestCache) manifestCache.dispose();
57
+ manifestCache = createManifestCache(cwd);
58
+ return manifestCache;
59
+ };
178
60
  const widgetState: CrewWidgetState = { frame: 0 };
179
- const subagentManager = new SubagentManager(4, (record) => {
180
- if (!record.background || record.resultConsumed) return;
181
- if (record.status === "completed" || record.status === "failed" || record.status === "cancelled" || record.status === "error") {
182
- sendFollowUp(pi, [`pi-crew subagent ${record.id} ${record.status}.`, record.runId ? `Run: ${record.runId}` : undefined, `Use get_subagent_result with agent_id=${record.id} for output.`].filter((line): line is string => Boolean(line)).join("\n"));
183
- }
184
- });
61
+ const subagentManager = new SubagentManager(
62
+ 4,
63
+ (record) => {
64
+ if (!record.background || record.resultConsumed) return;
65
+ if (record.status === "completed" || record.status === "failed" || record.status === "cancelled" || record.status === "blocked" || record.status === "error") {
66
+ sendFollowUp(pi, [`pi-crew subagent ${record.id} ${record.status}.`, record.runId ? `Run: ${record.runId}` : undefined, `Use get_subagent_result with agent_id=${record.id} for output.`].filter((line): line is string => Boolean(line)).join("\n"));
67
+ }
68
+ },
69
+ 1000,
70
+ (event, payload) => {
71
+ if (event === "subagent.stuck-blocked") {
72
+ const id = typeof payload.id === "string" ? payload.id : "unknown";
73
+ const runId = typeof payload.runId === "string" ? payload.runId : "unknown";
74
+ const durationMs = typeof payload.durationMs === "number" ? payload.durationMs : 0;
75
+ sendFollowUp(pi, [`pi-crew subagent ${id} may be stuck in blocked state for ${Math.max(1, Math.round(durationMs / 1000))}s.`, `Run: ${runId}`, `Use team status runId=${runId} and investigate.`, "Subagent may need manual intervention."].filter((line): line is string => Boolean(line)).join("\n"));
76
+ }
77
+ pi.events?.emit?.(event, payload);
78
+ },
79
+ );
185
80
  const foregroundControllers = new Set<AbortController>();
186
81
  let liveSidebarRunId: string | undefined;
187
82
  let liveSidebarTimer: ReturnType<typeof setInterval> | undefined;
@@ -208,7 +103,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
208
103
  ctx.ui.setWidget("pi-crew", undefined, { placement: uiConfig?.widgetPlacement ?? "aboveEditor" });
209
104
  ctx.ui.setWidget("pi-crew-active", undefined, { placement: uiConfig?.widgetPlacement ?? "aboveEditor" });
210
105
  const width = Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? 56));
211
- liveSidebarTimer = setInterval(() => requestRender(ctx), uiConfig?.dashboardLiveRefreshMs ?? 1000);
106
+ liveSidebarTimer = setInterval(() => requestRender(ctx), uiConfig?.dashboardLiveRefreshMs ?? DEFAULT_UI.refreshMs);
212
107
  liveSidebarTimer.unref?.();
213
108
  void ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new LiveRunSidebar({ cwd: ctx.cwd, runId, done, theme, config: uiConfig }), {
214
109
  overlay: true,
@@ -217,7 +112,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
217
112
  if (liveSidebarRunId === runId) liveSidebarRunId = undefined;
218
113
  if (liveSidebarTimer) clearInterval(liveSidebarTimer);
219
114
  liveSidebarTimer = undefined;
220
- updateCrewWidget(ctx, widgetState, loadConfig(ctx.cwd).config.ui);
115
+ updateCrewWidget(ctx, widgetState, loadConfig(ctx.cwd).config.ui, getManifestCache(ctx.cwd));
221
116
  });
222
117
  };
223
118
  const startForegroundRun = (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>, runId?: string): void => {
@@ -231,7 +126,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
231
126
  try {
232
127
  const loaded = loadRunManifestById(ctx.cwd, runId);
233
128
  if (loaded && loaded.manifest.status !== "completed" && loaded.manifest.status !== "failed" && loaded.manifest.status !== "cancelled" && loaded.manifest.status !== "blocked") updateRunStatus(loaded.manifest, "failed", message);
234
- } catch {}
129
+ } catch (statusError) {
130
+ logInternalError("register.foreground-run-failure", statusError, `runId=${runId}`);
131
+ }
235
132
  }
236
133
  ctx.ui.notify(`pi-crew foreground run failed: ${message}`, "error");
237
134
  })
@@ -245,14 +142,31 @@ export function registerPiTeams(pi: ExtensionAPI): void {
245
142
  }
246
143
  if (currentCtx) {
247
144
  const config = loadConfig(currentCtx.cwd).config.ui;
248
- updateCrewWidget(currentCtx, widgetState, config);
249
- updatePiCrewPowerbar(pi.events, currentCtx.cwd, config);
145
+ updateCrewWidget(currentCtx, widgetState, config, getManifestCache(currentCtx.cwd));
146
+ updatePiCrewPowerbar(pi.events, currentCtx.cwd, config, getManifestCache(currentCtx.cwd));
250
147
  }
251
148
  });
252
149
  });
253
150
  };
151
+ time("register.policy");
254
152
  registerAutonomousPolicy(pi);
153
+ time("register.rpc");
255
154
  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
+
256
170
  const cleanupRuntime = (): void => {
257
171
  if (cleanedUp) return;
258
172
  cleanedUp = true;
@@ -260,6 +174,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
260
174
  stopAsyncRunNotifier(notifierState);
261
175
  stopCrewWidget(currentCtx, widgetState, currentCtx ? loadConfig(currentCtx.cwd).config.ui : undefined);
262
176
  clearPiCrewPowerbar(pi.events);
177
+ manifestCache.dispose();
263
178
  rpcHandle?.unsubscribe();
264
179
  rpcHandle = undefined;
265
180
  currentCtx = undefined;
@@ -268,6 +183,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
268
183
  globalStore[runtimeCleanupStoreKey] = cleanupRuntime;
269
184
 
270
185
  pi.on("session_start", (_event, ctx) => {
186
+ runArtifactCleanup(ctx.cwd);
187
+ time("register.session-start");
271
188
  cleanedUp = false;
272
189
  currentCtx = ctx;
273
190
  if (widgetState.interval) clearInterval(widgetState.interval);
@@ -275,20 +192,22 @@ export function registerPiTeams(pi: ExtensionAPI): void {
275
192
  notifyActiveRuns(ctx);
276
193
  const loadedConfig = loadConfig(ctx.cwd);
277
194
  registerPiCrewPowerbarSegments(pi.events, loadedConfig.config.ui);
278
- startAsyncRunNotifier(ctx, notifierState, loadedConfig.config.notifierIntervalMs ?? 5000);
279
- updateCrewWidget(ctx, widgetState, loadedConfig.config.ui);
280
- updatePiCrewPowerbar(pi.events, ctx.cwd, loadedConfig.config.ui);
195
+ startAsyncRunNotifier(ctx, notifierState, loadedConfig.config.notifierIntervalMs ?? DEFAULT_UI.notifierIntervalMs);
196
+ const cache = getManifestCache(ctx.cwd);
197
+ updateCrewWidget(ctx, widgetState, loadedConfig.config.ui, cache);
198
+ updatePiCrewPowerbar(pi.events, ctx.cwd, loadedConfig.config.ui, cache);
281
199
  widgetState.interval = setInterval(() => {
282
200
  if (!currentCtx) return;
283
201
  const config = loadConfig(currentCtx.cwd).config.ui;
202
+ const cache = getManifestCache(currentCtx.cwd);
284
203
  if (liveSidebarRunId) {
285
204
  currentCtx.ui.setWidget("pi-crew", undefined, { placement: config?.widgetPlacement ?? "aboveEditor" });
286
205
  currentCtx.ui.setWidget("pi-crew-active", undefined, { placement: config?.widgetPlacement ?? "aboveEditor" });
287
206
  } else {
288
- updateCrewWidget(currentCtx, widgetState, config);
207
+ updateCrewWidget(currentCtx, widgetState, config, cache);
289
208
  }
290
- updatePiCrewPowerbar(pi.events, currentCtx.cwd, config);
291
- }, 1000);
209
+ updatePiCrewPowerbar(pi.events, currentCtx.cwd, config, cache);
210
+ }, DEFAULT_UI.widgetDefaultFrameMs);
292
211
  widgetState.interval.unref?.();
293
212
  });
294
213
  pi.on("session_before_switch", () => {
@@ -312,8 +231,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
312
231
  try {
313
232
  const output = await handleTeamTool(params as TeamToolParamsValue, { ...ctx, signal: controller.signal, startForegroundRun: (runner, runId) => startForegroundRun(ctx, runner, runId), onRunStarted: (runId) => openLiveSidebar(ctx, runId) });
314
233
  const config = loadConfig(ctx.cwd).config.ui;
315
- updateCrewWidget(ctx, widgetState, config);
316
- updatePiCrewPowerbar(pi.events, ctx.cwd, config);
234
+ const cache = getManifestCache(ctx.cwd);
235
+ updateCrewWidget(ctx, widgetState, config, cache);
236
+ updatePiCrewPowerbar(pi.events, ctx.cwd, config, cache);
317
237
  return output;
318
238
  } finally {
319
239
  signal?.removeEventListener("abort", abort);
@@ -445,8 +365,13 @@ export function registerPiTeams(pi: ExtensionAPI): void {
445
365
  };
446
366
  for (const extraTool of [crewAgentTool, crewAgentResultTool, crewAgentSteerTool]) pi.registerTool(extraTool);
447
367
  for (const extraTool of [agentTool, getSubagentResultTool, steerSubagentTool]) {
448
- try { pi.registerTool(extraTool); } catch {}
368
+ try {
369
+ pi.registerTool(extraTool);
370
+ } catch (error) {
371
+ logInternalError("register.duplicate-tool", error, `tool=${extraTool.name}`);
372
+ }
449
373
  }
374
+ time("register.tools");
450
375
 
451
376
  pi.registerCommand("teams", {
452
377
  description: "List pi-crew teams, workflows, and agents",
@@ -633,7 +558,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
633
558
  description: "Open a pi-crew run dashboard overlay",
634
559
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
635
560
  for (;;) {
636
- const runs = listRecentRuns(ctx.cwd, 50);
561
+ const runs = getManifestCache(ctx.cwd).list(50);
637
562
  const uiConfig = loadConfig(ctx.cwd).config.ui;
638
563
  const rightPanel = uiConfig?.dashboardPlacement !== "center";
639
564
  const width = rightPanel ? Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? 56)) : "90%";
@@ -663,6 +588,33 @@ export function registerPiTeams(pi: ExtensionAPI): void {
663
588
  },
664
589
  });
665
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
+
666
618
  pi.registerCommand("team-init", {
667
619
  description: "Initialize project-local pi-crew directories and gitignore entries",
668
620
  handler: async (args: string, ctx: ExtensionCommandContext) => {
@@ -747,4 +699,6 @@ export function registerPiTeams(pi: ExtensionAPI): void {
747
699
  await notifyCommandResult(ctx, commandText(result));
748
700
  },
749
701
  });
702
+ time("register.commands");
703
+ printTimings();
750
704
  }
@@ -0,0 +1,54 @@
1
+ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
3
+
4
+ export function parseRunArgs(args: string): TeamToolParamsValue {
5
+ const tokens = args.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
6
+ const params: TeamToolParamsValue = { action: "run" };
7
+ const goalParts: string[] = [];
8
+ for (const token of tokens) {
9
+ if (token === "--async") params.async = true;
10
+ else if (token === "--worktree") params.workspaceMode = "worktree";
11
+ else if (token.startsWith("--team=")) params.team = token.slice("--team=".length);
12
+ else if (token.startsWith("--workflow=")) params.workflow = token.slice("--workflow=".length);
13
+ else if (token.startsWith("--agent=")) params.agent = token.slice("--agent=".length);
14
+ else if (token.startsWith("--role=")) params.role = token.slice("--role=".length);
15
+ else if (!params.team && goalParts.length === 0 && !token.startsWith("--")) params.team = token;
16
+ else goalParts.push(token);
17
+ }
18
+ params.goal = goalParts.join(" ").trim() || undefined;
19
+ return params;
20
+ }
21
+
22
+ export function commandText(result: { content?: Array<{ type: string; text?: string }> }): string {
23
+ return result.content?.map((item) => item.text ?? "").join("\n") ?? "";
24
+ }
25
+
26
+ export async function notifyCommandResult(ctx: ExtensionCommandContext, text: string): Promise<void> {
27
+ ctx.ui.notify(text.length > 800 ? `${text.slice(0, 797)}...` : text, "info");
28
+ }
29
+
30
+ export function parseScalar(raw: string): unknown {
31
+ if (raw === "true") return true;
32
+ if (raw === "false") return false;
33
+ if (/^-?\d+$/.test(raw)) return Number(raw);
34
+ if (raw.includes(",")) return raw.split(",").map((entry) => entry.trim()).filter(Boolean);
35
+ return raw;
36
+ }
37
+
38
+ export function pushUnset(config: Record<string, unknown>, key: string): void {
39
+ const current = Array.isArray(config.unset) ? config.unset : [];
40
+ current.push(key);
41
+ config.unset = current;
42
+ }
43
+
44
+ export function setNestedConfig(config: Record<string, unknown>, key: string, value: unknown): void {
45
+ const parts = key.split(".").filter(Boolean);
46
+ if (parts.length === 0) return;
47
+ let target = config;
48
+ for (const part of parts.slice(0, -1)) {
49
+ const current = target[part];
50
+ if (!current || typeof current !== "object" || Array.isArray(current)) target[part] = {};
51
+ target = target[part] as Record<string, unknown>;
52
+ }
53
+ target[parts[parts.length - 1]!] = value;
54
+ }