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.
- package/docs/refactor-tasks-phase3.md +394 -0
- package/docs/refactor-tasks-phase4.md +564 -0
- package/docs/refactor-tasks-phase5.md +402 -0
- package/docs/refactor-tasks.md +1484 -0
- package/package.json +98 -95
- package/src/agents/agent-config.ts +30 -30
- package/src/config/config.ts +153 -89
- package/src/config/defaults.ts +60 -0
- package/src/extension/autonomous-policy.ts +1 -1
- package/src/extension/help.ts +1 -0
- package/src/extension/management.ts +15 -2
- package/src/extension/register.ts +124 -170
- package/src/extension/registration/command-utils.ts +54 -0
- package/src/extension/registration/subagent-helpers.ts +70 -0
- package/src/extension/registration/viewers.ts +32 -0
- package/src/extension/result-watcher.ts +98 -89
- package/src/extension/team-tool/api.ts +276 -0
- package/src/extension/team-tool/config-patch.ts +36 -0
- package/src/extension/team-tool/context.ts +48 -0
- package/src/extension/team-tool/doctor.ts +178 -0
- package/src/extension/team-tool/run.ts +133 -0
- package/src/extension/team-tool-types.ts +6 -0
- package/src/extension/team-tool.ts +31 -623
- package/src/extension/tool-result.ts +16 -16
- package/src/runtime/async-runner.ts +42 -60
- package/src/runtime/child-pi.ts +434 -332
- package/src/runtime/concurrency.ts +50 -42
- package/src/runtime/crew-agent-records.ts +166 -156
- package/src/runtime/manifest-cache.ts +214 -0
- package/src/runtime/parallel-utils.ts +99 -0
- package/src/runtime/post-exit-stdio-guard.ts +86 -0
- package/src/runtime/runtime-resolver.ts +77 -74
- package/src/runtime/subagent-manager.ts +291 -236
- package/src/runtime/task-graph-scheduler.ts +122 -107
- package/src/runtime/team-runner.ts +46 -51
- package/src/schema/config-schema.ts +92 -0
- package/src/state/artifact-store.ts +108 -36
- package/src/state/atomic-write.ts +114 -49
- package/src/state/event-log.ts +189 -138
- package/src/state/jsonl-writer.ts +77 -0
- package/src/state/locks.ts +149 -40
- package/src/state/mailbox.ts +200 -188
- package/src/state/state-store.ts +104 -15
- package/src/teams/discover-teams.ts +94 -84
- package/src/teams/team-config.ts +26 -22
- package/src/ui/crew-footer.ts +101 -0
- package/src/ui/crew-select-list.ts +111 -0
- package/src/ui/crew-widget.ts +285 -219
- package/src/ui/dynamic-border.ts +25 -0
- package/src/ui/layout-primitives.ts +106 -0
- package/src/ui/live-run-sidebar.ts +163 -95
- package/src/ui/loaders.ts +158 -0
- package/src/ui/mascot.ts +441 -0
- package/src/ui/powerbar-publisher.ts +94 -71
- package/src/ui/render-diff.ts +119 -0
- package/src/ui/run-dashboard.ts +155 -120
- package/src/ui/status-colors.ts +54 -0
- package/src/ui/syntax-highlight.ts +116 -0
- package/src/ui/theme-adapter.ts +190 -0
- package/src/ui/transcript-viewer.ts +194 -111
- package/src/utils/completion-dedupe.ts +63 -0
- package/src/utils/file-coalescer.ts +84 -33
- package/src/utils/fs-watch.ts +31 -0
- package/src/utils/git.ts +262 -0
- package/src/utils/internal-error.ts +6 -0
- package/src/utils/paths.ts +33 -15
- package/src/utils/sleep.ts +32 -0
- package/src/utils/timings.ts +31 -0
- package/src/utils/visual.ts +159 -0
- package/src/workflows/discover-workflows.ts +109 -101
- package/src/workflows/workflow-config.ts +25 -24
- package/src/workflows/workflow-serializer.ts +32 -31
- 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
|
|
package/src/extension/help.ts
CHANGED
|
@@ -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({
|
|
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
|
|
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
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 {
|
|
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(
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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 ??
|
|
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 ??
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
316
|
-
|
|
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 {
|
|
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 =
|
|
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
|
+
}
|