pi-crew 0.1.37 → 0.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +27 -0
- package/README.md +5 -0
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +11 -11
- package/agents/writer.md +11 -11
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/resource-formats.md +10 -8
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +83 -83
- package/docs/usage.md +6 -0
- package/index.ts +6 -6
- package/package.json +3 -3
- package/schema.json +2 -2
- package/src/agents/agent-serializer.ts +34 -34
- package/src/config/config.ts +8 -4
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/import-index.ts +18 -2
- package/src/extension/register.ts +11 -1
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +30 -6
- package/src/extension/registration/subagent-tools.ts +8 -3
- package/src/extension/result-watcher.ts +98 -98
- package/src/extension/run-import.ts +12 -2
- package/src/extension/run-index.ts +12 -2
- package/src/extension/run-maintenance.ts +24 -24
- package/src/extension/team-tool/api.ts +54 -14
- package/src/extension/team-tool/cancel.ts +31 -31
- package/src/extension/team-tool/doctor.ts +179 -179
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +79 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/status.ts +73 -73
- package/src/observability/correlation.ts +35 -35
- package/src/observability/event-to-metric.ts +54 -54
- package/src/observability/exporters/adapter.ts +24 -24
- package/src/observability/exporters/otlp-exporter.ts +65 -65
- package/src/observability/exporters/prometheus-exporter.ts +47 -47
- package/src/observability/metric-registry.ts +72 -72
- package/src/observability/metric-retention.ts +46 -46
- package/src/observability/metric-sink.ts +51 -51
- package/src/observability/metrics-primitives.ts +166 -166
- package/src/prompt/prompt-runtime.ts +68 -68
- package/src/runtime/agent-control.ts +64 -64
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -113
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/crash-recovery.ts +56 -56
- package/src/runtime/crew-agent-records.ts +54 -9
- package/src/runtime/crew-agent-runtime.ts +58 -58
- package/src/runtime/deadletter.ts +36 -36
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +88 -88
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +80 -80
- package/src/runtime/live-agent-control.ts +87 -78
- package/src/runtime/live-agent-manager.ts +85 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +299 -299
- package/src/runtime/manifest-cache.ts +248 -212
- package/src/runtime/model-fallback.ts +261 -261
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +99 -99
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +78 -78
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- package/src/runtime/process-status.ts +56 -56
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +59 -59
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +28 -28
- package/src/runtime/subagent-manager.ts +80 -12
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -106
- package/src/runtime/task-runner/live-executor.ts +98 -98
- package/src/runtime/task-runner/progress.ts +111 -111
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/team-runner.ts +1 -1
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +21 -21
- package/src/schema/team-tool-schema.ts +100 -100
- package/src/state/artifact-store.ts +122 -108
- package/src/state/contracts.ts +105 -105
- package/src/state/jsonl-writer.ts +77 -77
- package/src/state/mailbox.ts +67 -22
- package/src/state/state-store.ts +36 -5
- package/src/state/task-claims.ts +42 -42
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/discover-teams.ts +27 -5
- package/src/teams/team-serializer.ts +38 -36
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/mascot.ts +441 -441
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/run-dashboard.ts +5 -2
- package/src/ui/run-snapshot-cache.ts +19 -8
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +54 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/transcript-viewer.ts +15 -1
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/file-coalescer.ts +84 -84
- package/src/utils/frontmatter.ts +36 -36
- package/src/utils/fs-watch.ts +31 -31
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/names.ts +26 -26
- package/src/utils/paths.ts +3 -2
- package/src/utils/safe-paths.ts +34 -0
- package/src/utils/sleep.ts +32 -32
- package/src/utils/timings.ts +31 -31
- package/src/utils/visual.ts +159 -159
- package/src/workflows/discover-workflows.ts +30 -3
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
|
@@ -1,179 +1,179 @@
|
|
|
1
|
-
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { allAgents, discoverAgents } from "../../agents/discover-agents.ts";
|
|
5
|
-
import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
|
|
6
|
-
import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
|
|
7
|
-
import { loadConfig } from "../../config/config.ts";
|
|
8
|
-
import { projectCrewRoot, userCrewRoot } from "../../utils/paths.ts";
|
|
9
|
-
import { DEFAULT_PATHS } from "../../config/defaults.ts";
|
|
10
|
-
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
11
|
-
import { getPiSpawnCommand } from "../../runtime/pi-spawn.ts";
|
|
12
|
-
import { validateResources } from "../validate-resources.ts";
|
|
13
|
-
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
14
|
-
import { configRecord, result, type TeamContext } from "./context.ts";
|
|
15
|
-
|
|
16
|
-
interface DoctorCheck {
|
|
17
|
-
label: string;
|
|
18
|
-
ok: boolean;
|
|
19
|
-
detail: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function firstOutputLine(stdout: string | null | undefined, stderr: string | null | undefined): string {
|
|
23
|
-
const output = `${stdout ?? ""}\n${stderr ?? ""}`.trim();
|
|
24
|
-
return output.split(/\r?\n/).find((line) => line.trim().length > 0)?.trim() ?? "available";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function commandExists(command: string, args: string[]): { ok: boolean; detail: string } {
|
|
28
|
-
try {
|
|
29
|
-
const output = spawnSync(command, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
30
|
-
if (output.error) {
|
|
31
|
-
return { ok: false, detail: output.error.message };
|
|
32
|
-
}
|
|
33
|
-
if (output.status !== 0) {
|
|
34
|
-
return { ok: false, detail: firstOutputLine(output.stdout, output.stderr) || `status ${output.status}` };
|
|
35
|
-
}
|
|
36
|
-
return { ok: true, detail: firstOutputLine(output.stdout, output.stderr) };
|
|
37
|
-
} catch (error) {
|
|
38
|
-
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function piCommandExists(): { ok: boolean; detail: string } {
|
|
43
|
-
const spec = getPiSpawnCommand(["--version"]);
|
|
44
|
-
const output = commandExists(spec.command, spec.args);
|
|
45
|
-
if (!output.ok) return output;
|
|
46
|
-
const executable = spec.command === "pi" ? "pi" : `${spec.command} ${spec.args[0] ?? ""}`.trim();
|
|
47
|
-
return { ok: true, detail: `${output.detail} (${executable})` };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function checkWritableDir(dir: string): { ok: boolean; detail: string } {
|
|
51
|
-
try {
|
|
52
|
-
if (!fs.existsSync(dir)) return { ok: false, detail: `${dir}: missing` };
|
|
53
|
-
if (!fs.statSync(dir).isDirectory()) return { ok: false, detail: `${dir}: not a directory` };
|
|
54
|
-
fs.accessSync(dir, fs.constants.W_OK);
|
|
55
|
-
return { ok: true, detail: dir };
|
|
56
|
-
} catch (error) {
|
|
57
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
58
|
-
return { ok: false, detail: `${dir}: ${message}` };
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function makeLine(check: DoctorCheck): string {
|
|
63
|
-
return `- ${check.ok ? "OK" : "FAIL"} ${check.label}: ${check.detail}`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function section(title: string, checks: () => DoctorCheck[]): string[] {
|
|
67
|
-
try {
|
|
68
|
-
return [title, ...checks().map(makeLine)];
|
|
69
|
-
} catch (error) {
|
|
70
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
71
|
-
return [title, `- FAIL ${title}: ${detail}`];
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface TeamDoctorReportInput {
|
|
76
|
-
cwd: string;
|
|
77
|
-
configPath: string;
|
|
78
|
-
configErrors: string[];
|
|
79
|
-
configWarnings: string[];
|
|
80
|
-
model?: { provider: string; id: string };
|
|
81
|
-
validationErrors: number;
|
|
82
|
-
validationWarnings: number;
|
|
83
|
-
smokeChildPi?: { ok: boolean; detail: string };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export interface TeamDoctorReport {
|
|
87
|
-
text: string;
|
|
88
|
-
hasErrors: boolean;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function buildTeamDoctorReport(input: TeamDoctorReportInput): TeamDoctorReport {
|
|
92
|
-
const sections = [
|
|
93
|
-
section("Runtime", () => {
|
|
94
|
-
const git = commandExists("git", ["--version"]);
|
|
95
|
-
const pi = piCommandExists();
|
|
96
|
-
return [
|
|
97
|
-
{ label: "cwd", ok: true, detail: input.cwd },
|
|
98
|
-
{ label: "platform", ok: true, detail: `${process.platform}/${process.arch} node=${process.version}` },
|
|
99
|
-
{ label: "pi command", ok: pi.ok, detail: pi.detail },
|
|
100
|
-
{ label: "git command", ok: git.ok, detail: git.detail },
|
|
101
|
-
{ label: "config", ok: input.configErrors.length === 0, detail: `${input.configPath} (${input.configErrors.length} errors)` },
|
|
102
|
-
{ label: "model", ok: true, detail: input.model ? `${input.model.provider}/${input.model.id}` : "not available in this context" },
|
|
103
|
-
{ label: "config warnings", ok: true, detail: `${input.configWarnings.length} warnings` },
|
|
104
|
-
];
|
|
105
|
-
}),
|
|
106
|
-
section("Filesystem", () => {
|
|
107
|
-
const userWritable = checkWritableDir(userCrewRoot());
|
|
108
|
-
const projectWritable = checkWritableDir(projectCrewRoot(input.cwd));
|
|
109
|
-
return [
|
|
110
|
-
{ label: "user state", ok: userWritable.ok, detail: userWritable.detail },
|
|
111
|
-
{ label: "project state", ok: projectWritable.ok, detail: projectWritable.detail },
|
|
112
|
-
{ label: "project state root", ok: true, detail: path.join(projectCrewRoot(input.cwd), DEFAULT_PATHS.state.runsSubdir) },
|
|
113
|
-
{ label: "artifacts root", ok: true, detail: path.join(projectCrewRoot(input.cwd), DEFAULT_PATHS.state.artifactsSubdir) },
|
|
114
|
-
];
|
|
115
|
-
}),
|
|
116
|
-
section("Discovery", () => {
|
|
117
|
-
const discoveredAgents = allAgents(discoverAgents(input.cwd));
|
|
118
|
-
const discoveredTeams = allTeams(discoverTeams(input.cwd));
|
|
119
|
-
const discoveredWorkflows = allWorkflows(discoverWorkflows(input.cwd));
|
|
120
|
-
const agentModelHints = discoveredAgents.filter((agent) => agent.model || agent.fallbackModels?.length).length;
|
|
121
|
-
return [
|
|
122
|
-
{ label: "agents", ok: true, detail: `${discoveredAgents.length} discovered` },
|
|
123
|
-
{ label: "teams", ok: true, detail: `${discoveredTeams.length} discovered` },
|
|
124
|
-
{ label: "workflows", ok: true, detail: `${discoveredWorkflows.length} discovered` },
|
|
125
|
-
{ label: "resource model hints", ok: true, detail: `${agentModelHints} agents declare model/fallback preferences` },
|
|
126
|
-
];
|
|
127
|
-
}),
|
|
128
|
-
section("Resource validation", () => [{
|
|
129
|
-
label: "resource validation",
|
|
130
|
-
ok: input.validationErrors === 0,
|
|
131
|
-
detail: `${input.validationErrors} errors, ${input.validationWarnings} warnings`,
|
|
132
|
-
}]),
|
|
133
|
-
];
|
|
134
|
-
if (input.smokeChildPi) {
|
|
135
|
-
sections.push([`Child check`, `- ${input.smokeChildPi.ok ? "OK" : "FAIL"} child Pi smoke: ${input.smokeChildPi.detail}`]);
|
|
136
|
-
}
|
|
137
|
-
const lines = ["pi-crew doctor report"];
|
|
138
|
-
for (const block of sections) {
|
|
139
|
-
if (block.length > 0) {
|
|
140
|
-
lines.push(...block);
|
|
141
|
-
lines.push("");
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
if (lines.at(-1) === "") lines.pop();
|
|
145
|
-
const text = lines.join("\n");
|
|
146
|
-
return { text, hasErrors: sections.some((sectionLines) => sectionLines.some((line) => line.includes("FAIL"))) };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export function handleDoctor(ctx: TeamContext, params: TeamToolParamsValue = {}): PiTeamsToolResult {
|
|
150
|
-
const loadedConfig = loadConfig(ctx.cwd);
|
|
151
|
-
let smokeChildPi: { ok: boolean; detail: string } | undefined;
|
|
152
|
-
if (configRecord(params.config).smokeChildPi === true) {
|
|
153
|
-
try {
|
|
154
|
-
const spec = getPiSpawnCommand(["--mode", "json", "-p", "Reply with exactly PI-TEAMS-SMOKE-OK"]);
|
|
155
|
-
const output = execFileSync(spec.command, spec.args, {
|
|
156
|
-
cwd: ctx.cwd,
|
|
157
|
-
encoding: "utf-8",
|
|
158
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
159
|
-
timeout: 15_000,
|
|
160
|
-
}).trim();
|
|
161
|
-
smokeChildPi = { ok: output.includes("PI-TEAMS-SMOKE-OK"), detail: output.split("\n").slice(-1)[0] ?? "completed" };
|
|
162
|
-
} catch (error) {
|
|
163
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
164
|
-
smokeChildPi = { ok: false, detail: message };
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
const validation = validateResources(ctx.cwd);
|
|
168
|
-
const { text, hasErrors } = buildTeamDoctorReport({
|
|
169
|
-
cwd: ctx.cwd,
|
|
170
|
-
configPath: loadedConfig.path,
|
|
171
|
-
configErrors: loadedConfig.error ? [loadedConfig.error] : [],
|
|
172
|
-
configWarnings: loadedConfig.warnings ?? [],
|
|
173
|
-
model: ctx.model,
|
|
174
|
-
validationErrors: validation.issues.filter((issue) => issue.level === "error").length,
|
|
175
|
-
validationWarnings: validation.issues.filter((issue) => issue.level === "warning").length,
|
|
176
|
-
smokeChildPi,
|
|
177
|
-
});
|
|
178
|
-
return result(text, { action: "doctor", status: hasErrors ? "error" : "ok" }, hasErrors);
|
|
179
|
-
}
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { allAgents, discoverAgents } from "../../agents/discover-agents.ts";
|
|
5
|
+
import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
|
|
6
|
+
import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
|
|
7
|
+
import { loadConfig } from "../../config/config.ts";
|
|
8
|
+
import { projectCrewRoot, userCrewRoot } from "../../utils/paths.ts";
|
|
9
|
+
import { DEFAULT_PATHS } from "../../config/defaults.ts";
|
|
10
|
+
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
11
|
+
import { getPiSpawnCommand } from "../../runtime/pi-spawn.ts";
|
|
12
|
+
import { validateResources } from "../validate-resources.ts";
|
|
13
|
+
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
14
|
+
import { configRecord, result, type TeamContext } from "./context.ts";
|
|
15
|
+
|
|
16
|
+
interface DoctorCheck {
|
|
17
|
+
label: string;
|
|
18
|
+
ok: boolean;
|
|
19
|
+
detail: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function firstOutputLine(stdout: string | null | undefined, stderr: string | null | undefined): string {
|
|
23
|
+
const output = `${stdout ?? ""}\n${stderr ?? ""}`.trim();
|
|
24
|
+
return output.split(/\r?\n/).find((line) => line.trim().length > 0)?.trim() ?? "available";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function commandExists(command: string, args: string[]): { ok: boolean; detail: string } {
|
|
28
|
+
try {
|
|
29
|
+
const output = spawnSync(command, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
30
|
+
if (output.error) {
|
|
31
|
+
return { ok: false, detail: output.error.message };
|
|
32
|
+
}
|
|
33
|
+
if (output.status !== 0) {
|
|
34
|
+
return { ok: false, detail: firstOutputLine(output.stdout, output.stderr) || `status ${output.status}` };
|
|
35
|
+
}
|
|
36
|
+
return { ok: true, detail: firstOutputLine(output.stdout, output.stderr) };
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function piCommandExists(): { ok: boolean; detail: string } {
|
|
43
|
+
const spec = getPiSpawnCommand(["--version"]);
|
|
44
|
+
const output = commandExists(spec.command, spec.args);
|
|
45
|
+
if (!output.ok) return output;
|
|
46
|
+
const executable = spec.command === "pi" ? "pi" : `${spec.command} ${spec.args[0] ?? ""}`.trim();
|
|
47
|
+
return { ok: true, detail: `${output.detail} (${executable})` };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function checkWritableDir(dir: string): { ok: boolean; detail: string } {
|
|
51
|
+
try {
|
|
52
|
+
if (!fs.existsSync(dir)) return { ok: false, detail: `${dir}: missing` };
|
|
53
|
+
if (!fs.statSync(dir).isDirectory()) return { ok: false, detail: `${dir}: not a directory` };
|
|
54
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
55
|
+
return { ok: true, detail: dir };
|
|
56
|
+
} catch (error) {
|
|
57
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
58
|
+
return { ok: false, detail: `${dir}: ${message}` };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function makeLine(check: DoctorCheck): string {
|
|
63
|
+
return `- ${check.ok ? "OK" : "FAIL"} ${check.label}: ${check.detail}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function section(title: string, checks: () => DoctorCheck[]): string[] {
|
|
67
|
+
try {
|
|
68
|
+
return [title, ...checks().map(makeLine)];
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
71
|
+
return [title, `- FAIL ${title}: ${detail}`];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface TeamDoctorReportInput {
|
|
76
|
+
cwd: string;
|
|
77
|
+
configPath: string;
|
|
78
|
+
configErrors: string[];
|
|
79
|
+
configWarnings: string[];
|
|
80
|
+
model?: { provider: string; id: string };
|
|
81
|
+
validationErrors: number;
|
|
82
|
+
validationWarnings: number;
|
|
83
|
+
smokeChildPi?: { ok: boolean; detail: string };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface TeamDoctorReport {
|
|
87
|
+
text: string;
|
|
88
|
+
hasErrors: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function buildTeamDoctorReport(input: TeamDoctorReportInput): TeamDoctorReport {
|
|
92
|
+
const sections = [
|
|
93
|
+
section("Runtime", () => {
|
|
94
|
+
const git = commandExists("git", ["--version"]);
|
|
95
|
+
const pi = piCommandExists();
|
|
96
|
+
return [
|
|
97
|
+
{ label: "cwd", ok: true, detail: input.cwd },
|
|
98
|
+
{ label: "platform", ok: true, detail: `${process.platform}/${process.arch} node=${process.version}` },
|
|
99
|
+
{ label: "pi command", ok: pi.ok, detail: pi.detail },
|
|
100
|
+
{ label: "git command", ok: git.ok, detail: git.detail },
|
|
101
|
+
{ label: "config", ok: input.configErrors.length === 0, detail: `${input.configPath} (${input.configErrors.length} errors)` },
|
|
102
|
+
{ label: "model", ok: true, detail: input.model ? `${input.model.provider}/${input.model.id}` : "not available in this context" },
|
|
103
|
+
{ label: "config warnings", ok: true, detail: `${input.configWarnings.length} warnings` },
|
|
104
|
+
];
|
|
105
|
+
}),
|
|
106
|
+
section("Filesystem", () => {
|
|
107
|
+
const userWritable = checkWritableDir(userCrewRoot());
|
|
108
|
+
const projectWritable = checkWritableDir(projectCrewRoot(input.cwd));
|
|
109
|
+
return [
|
|
110
|
+
{ label: "user state", ok: userWritable.ok || userWritable.detail.endsWith(": missing"), detail: userWritable.detail },
|
|
111
|
+
{ label: "project state", ok: projectWritable.ok || projectWritable.detail.endsWith(": missing"), detail: projectWritable.detail },
|
|
112
|
+
{ label: "project state root", ok: true, detail: path.join(projectCrewRoot(input.cwd), DEFAULT_PATHS.state.runsSubdir) },
|
|
113
|
+
{ label: "artifacts root", ok: true, detail: path.join(projectCrewRoot(input.cwd), DEFAULT_PATHS.state.artifactsSubdir) },
|
|
114
|
+
];
|
|
115
|
+
}),
|
|
116
|
+
section("Discovery", () => {
|
|
117
|
+
const discoveredAgents = allAgents(discoverAgents(input.cwd));
|
|
118
|
+
const discoveredTeams = allTeams(discoverTeams(input.cwd));
|
|
119
|
+
const discoveredWorkflows = allWorkflows(discoverWorkflows(input.cwd));
|
|
120
|
+
const agentModelHints = discoveredAgents.filter((agent) => agent.model || agent.fallbackModels?.length).length;
|
|
121
|
+
return [
|
|
122
|
+
{ label: "agents", ok: true, detail: `${discoveredAgents.length} discovered` },
|
|
123
|
+
{ label: "teams", ok: true, detail: `${discoveredTeams.length} discovered` },
|
|
124
|
+
{ label: "workflows", ok: true, detail: `${discoveredWorkflows.length} discovered` },
|
|
125
|
+
{ label: "resource model hints", ok: true, detail: `${agentModelHints} agents declare model/fallback preferences` },
|
|
126
|
+
];
|
|
127
|
+
}),
|
|
128
|
+
section("Resource validation", () => [{
|
|
129
|
+
label: "resource validation",
|
|
130
|
+
ok: input.validationErrors === 0,
|
|
131
|
+
detail: `${input.validationErrors} errors, ${input.validationWarnings} warnings`,
|
|
132
|
+
}]),
|
|
133
|
+
];
|
|
134
|
+
if (input.smokeChildPi) {
|
|
135
|
+
sections.push([`Child check`, `- ${input.smokeChildPi.ok ? "OK" : "FAIL"} child Pi smoke: ${input.smokeChildPi.detail}`]);
|
|
136
|
+
}
|
|
137
|
+
const lines = ["pi-crew doctor report"];
|
|
138
|
+
for (const block of sections) {
|
|
139
|
+
if (block.length > 0) {
|
|
140
|
+
lines.push(...block);
|
|
141
|
+
lines.push("");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (lines.at(-1) === "") lines.pop();
|
|
145
|
+
const text = lines.join("\n");
|
|
146
|
+
return { text, hasErrors: sections.some((sectionLines) => sectionLines.some((line) => line.includes("FAIL"))) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function handleDoctor(ctx: TeamContext, params: TeamToolParamsValue = {}): PiTeamsToolResult {
|
|
150
|
+
const loadedConfig = loadConfig(ctx.cwd);
|
|
151
|
+
let smokeChildPi: { ok: boolean; detail: string } | undefined;
|
|
152
|
+
if (configRecord(params.config).smokeChildPi === true) {
|
|
153
|
+
try {
|
|
154
|
+
const spec = getPiSpawnCommand(["--mode", "json", "-p", "Reply with exactly PI-TEAMS-SMOKE-OK"]);
|
|
155
|
+
const output = execFileSync(spec.command, spec.args, {
|
|
156
|
+
cwd: ctx.cwd,
|
|
157
|
+
encoding: "utf-8",
|
|
158
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
159
|
+
timeout: 15_000,
|
|
160
|
+
}).trim();
|
|
161
|
+
smokeChildPi = { ok: output.includes("PI-TEAMS-SMOKE-OK"), detail: output.split("\n").slice(-1)[0] ?? "completed" };
|
|
162
|
+
} catch (error) {
|
|
163
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
164
|
+
smokeChildPi = { ok: false, detail: message };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const validation = validateResources(ctx.cwd);
|
|
168
|
+
const { text, hasErrors } = buildTeamDoctorReport({
|
|
169
|
+
cwd: ctx.cwd,
|
|
170
|
+
configPath: loadedConfig.path,
|
|
171
|
+
configErrors: loadedConfig.error ? [loadedConfig.error] : [],
|
|
172
|
+
configWarnings: loadedConfig.warnings ?? [],
|
|
173
|
+
model: ctx.model,
|
|
174
|
+
validationErrors: validation.issues.filter((issue) => issue.level === "error").length,
|
|
175
|
+
validationWarnings: validation.issues.filter((issue) => issue.level === "warning").length,
|
|
176
|
+
smokeChildPi,
|
|
177
|
+
});
|
|
178
|
+
return result(text, { action: "doctor", status: hasErrors ? "error" : "ok" }, hasErrors);
|
|
179
|
+
}
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
2
|
-
import { readEvents } from "../../state/event-log.ts";
|
|
3
|
-
import { loadRunManifestById } from "../../state/state-store.ts";
|
|
4
|
-
import { aggregateUsage, formatUsage } from "../../state/usage.ts";
|
|
5
|
-
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
6
|
-
import { result, type TeamContext } from "./context.ts";
|
|
7
|
-
|
|
8
|
-
export function handleEvents(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
9
|
-
if (!params.runId) return result("Events requires runId.", { action: "events", status: "error" }, true);
|
|
10
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
11
|
-
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "events", status: "error" }, true);
|
|
12
|
-
const events = readEvents(loaded.manifest.eventsPath);
|
|
13
|
-
const lines = [`Events for ${loaded.manifest.runId}:`, ...(events.length ? events.map((event) => `${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}${event.data ? ` ${JSON.stringify(event.data)}` : ""}`) : ["(none)"])];
|
|
14
|
-
return result(lines.join("\n"), { action: "events", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function handleArtifacts(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
18
|
-
if (!params.runId) return result("Artifacts requires runId.", { action: "artifacts", status: "error" }, true);
|
|
19
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
20
|
-
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "artifacts", status: "error" }, true);
|
|
21
|
-
const lines = [`Artifacts for ${loaded.manifest.runId}:`, ...(loaded.manifest.artifacts.length ? loaded.manifest.artifacts.map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}${artifact.contentHash ? ` sha256=${artifact.contentHash.slice(0, 12)}` : ""}`) : ["- (none)"])];
|
|
22
|
-
return result(lines.join("\n"), { action: "artifacts", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function handleSummary(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
26
|
-
if (!params.runId) return result("Summary requires runId.", { action: "summary", status: "error" }, true);
|
|
27
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
28
|
-
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "summary", status: "error" }, true);
|
|
29
|
-
const usage = aggregateUsage(loaded.tasks);
|
|
30
|
-
const lines = [
|
|
31
|
-
`Summary for ${loaded.manifest.runId}`,
|
|
32
|
-
`Status: ${loaded.manifest.status}`,
|
|
33
|
-
`Team: ${loaded.manifest.team}`,
|
|
34
|
-
`Workflow: ${loaded.manifest.workflow ?? "(none)"}`,
|
|
35
|
-
`Goal: ${loaded.manifest.goal}`,
|
|
36
|
-
`Usage: ${formatUsage(usage)}`,
|
|
37
|
-
"Tasks:",
|
|
38
|
-
...loaded.tasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
|
|
39
|
-
];
|
|
40
|
-
return result(lines.join("\n"), { action: "summary", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
41
|
-
}
|
|
1
|
+
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
2
|
+
import { readEvents } from "../../state/event-log.ts";
|
|
3
|
+
import { loadRunManifestById } from "../../state/state-store.ts";
|
|
4
|
+
import { aggregateUsage, formatUsage } from "../../state/usage.ts";
|
|
5
|
+
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
6
|
+
import { result, type TeamContext } from "./context.ts";
|
|
7
|
+
|
|
8
|
+
export function handleEvents(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
9
|
+
if (!params.runId) return result("Events requires runId.", { action: "events", status: "error" }, true);
|
|
10
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
11
|
+
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "events", status: "error" }, true);
|
|
12
|
+
const events = readEvents(loaded.manifest.eventsPath);
|
|
13
|
+
const lines = [`Events for ${loaded.manifest.runId}:`, ...(events.length ? events.map((event) => `${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}${event.data ? ` ${JSON.stringify(event.data)}` : ""}`) : ["(none)"])];
|
|
14
|
+
return result(lines.join("\n"), { action: "events", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function handleArtifacts(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
18
|
+
if (!params.runId) return result("Artifacts requires runId.", { action: "artifacts", status: "error" }, true);
|
|
19
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
20
|
+
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "artifacts", status: "error" }, true);
|
|
21
|
+
const lines = [`Artifacts for ${loaded.manifest.runId}:`, ...(loaded.manifest.artifacts.length ? loaded.manifest.artifacts.map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}${artifact.contentHash ? ` sha256=${artifact.contentHash.slice(0, 12)}` : ""}`) : ["- (none)"])];
|
|
22
|
+
return result(lines.join("\n"), { action: "artifacts", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function handleSummary(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
26
|
+
if (!params.runId) return result("Summary requires runId.", { action: "summary", status: "error" }, true);
|
|
27
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
28
|
+
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "summary", status: "error" }, true);
|
|
29
|
+
const usage = aggregateUsage(loaded.tasks);
|
|
30
|
+
const lines = [
|
|
31
|
+
`Summary for ${loaded.manifest.runId}`,
|
|
32
|
+
`Status: ${loaded.manifest.status}`,
|
|
33
|
+
`Team: ${loaded.manifest.team}`,
|
|
34
|
+
`Workflow: ${loaded.manifest.workflow ?? "(none)"}`,
|
|
35
|
+
`Goal: ${loaded.manifest.goal}`,
|
|
36
|
+
`Usage: ${formatUsage(usage)}`,
|
|
37
|
+
"Tasks:",
|
|
38
|
+
...loaded.tasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
|
|
39
|
+
];
|
|
40
|
+
return result(lines.join("\n"), { action: "summary", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
41
|
+
}
|
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
3
|
-
import { appendEvent } from "../../state/event-log.ts";
|
|
4
|
-
import { loadRunManifestById } from "../../state/state-store.ts";
|
|
5
|
-
import { cleanupRunWorktrees } from "../../worktree/cleanup.ts";
|
|
6
|
-
import { listImportedRuns } from "../import-index.ts";
|
|
7
|
-
import { exportRunBundle } from "../run-export.ts";
|
|
8
|
-
import { importRunBundle } from "../run-import.ts";
|
|
9
|
-
import { pruneFinishedRuns } from "../run-maintenance.ts";
|
|
10
|
-
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
11
|
-
import { configRecord, result, type TeamContext } from "./context.ts";
|
|
12
|
-
|
|
13
|
-
export function handleWorktrees(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
14
|
-
if (!params.runId) return result("Worktrees requires runId.", { action: "worktrees", status: "error" }, true);
|
|
15
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
16
|
-
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "worktrees", status: "error" }, true);
|
|
17
|
-
const withWorktrees = loaded.tasks.filter((task) => task.worktree);
|
|
18
|
-
const lines = [`Worktrees for ${loaded.manifest.runId}:`, ...(withWorktrees.length ? withWorktrees.map((task) => `- ${task.id}: ${task.worktree!.path} branch=${task.worktree!.branch} reused=${task.worktree!.reused ? "true" : "false"}`) : ["- (none)"])];
|
|
19
|
-
return result(lines.join("\n"), { action: "worktrees", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function handleImports(_params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
23
|
-
const imports = listImportedRuns(ctx.cwd);
|
|
24
|
-
const lines = ["Imported pi-crew runs:", ...(imports.length ? imports.map((entry) => `- ${entry.runId} (${entry.scope})${entry.status ? ` [${entry.status}]` : ""} ${entry.team ?? "unknown"}/${entry.workflow ?? "none"}: ${entry.goal ?? ""}\n Bundle: ${entry.bundlePath}\n Summary: ${entry.summaryPath}`) : ["- (none)"])];
|
|
25
|
-
return result(lines.join("\n"), { action: "imports", status: "ok" });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function handleImport(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
29
|
-
const cfg = configRecord(params.config);
|
|
30
|
-
const bundlePath = typeof cfg.path === "string" ? cfg.path : typeof cfg.bundlePath === "string" ? cfg.bundlePath : undefined;
|
|
31
|
-
if (!bundlePath) return result("Import requires config.path pointing at run-export.json.", { action: "import", status: "error" }, true);
|
|
32
|
-
const scope = cfg.scope === "user" ? "user" : "project";
|
|
33
|
-
try {
|
|
34
|
-
const imported = importRunBundle(ctx.cwd, bundlePath, scope);
|
|
35
|
-
return result([`Imported run bundle ${imported.runId}.`, `Bundle: ${imported.bundlePath}`, `Summary: ${imported.summaryPath}`].join("\n"), { action: "import", status: "ok" });
|
|
36
|
-
} catch (error) {
|
|
37
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
38
|
-
return result(`Import failed: ${message}`, { action: "import", status: "error" }, true);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function handleExport(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
43
|
-
if (!params.runId) return result("Export requires runId.", { action: "export", status: "error" }, true);
|
|
44
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
45
|
-
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "export", status: "error" }, true);
|
|
46
|
-
const exported = exportRunBundle(loaded.manifest, loaded.tasks);
|
|
47
|
-
appendEvent(loaded.manifest.eventsPath, { type: "run.exported", runId: loaded.manifest.runId, data: exported });
|
|
48
|
-
return result([`Exported run ${loaded.manifest.runId}.`, `JSON: ${exported.jsonPath}`, `Markdown: ${exported.markdownPath}`].join("\n"), { action: "export", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function handlePrune(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
52
|
-
const keep = params.keep ?? 20;
|
|
53
|
-
if (!params.confirm) return result("prune requires confirm: true.", { action: "prune", status: "error" }, true);
|
|
54
|
-
if (keep < 0 || !Number.isInteger(keep)) return result("keep must be an integer >= 0.", { action: "prune", status: "error" }, true);
|
|
55
|
-
const pruned = pruneFinishedRuns(ctx.cwd, keep);
|
|
56
|
-
return result([`Pruned finished pi-crew runs.`, `Kept: ${pruned.kept.length}`, `Removed: ${pruned.removed.length}`, ...(pruned.removed.length ? ["Removed runs:", ...pruned.removed.map((runId) => `- ${runId}`)] : [])].join("\n"), { action: "prune", status: "ok" });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function handleForget(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
60
|
-
if (!params.runId) return result("Forget requires runId.", { action: "forget", status: "error" }, true);
|
|
61
|
-
if (!params.confirm) return result("forget requires confirm: true.", { action: "forget", status: "error" }, true);
|
|
62
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
63
|
-
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "forget", status: "error" }, true);
|
|
64
|
-
const cleanup = cleanupRunWorktrees(loaded.manifest, { force: params.force });
|
|
65
|
-
if (cleanup.preserved.length > 0 && !params.force) return result([`Run '${params.runId}' has preserved worktrees. Use force: true to forget anyway.`, ...cleanup.preserved.map((item) => `- ${item.path}: ${item.reason}`)].join("\n"), { action: "forget", status: "error", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot }, true);
|
|
66
|
-
fs.rmSync(loaded.manifest.stateRoot, { recursive: true, force: true });
|
|
67
|
-
fs.rmSync(loaded.manifest.artifactsRoot, { recursive: true, force: true });
|
|
68
|
-
return result([`Forgot run ${loaded.manifest.runId}.`, `Removed state: ${loaded.manifest.stateRoot}`, `Removed artifacts: ${loaded.manifest.artifactsRoot}`, ...(cleanup.removed.length ? ["Removed worktrees:", ...cleanup.removed.map((item) => `- ${item}`)] : [])].join("\n"), { action: "forget", status: "ok", runId: loaded.manifest.runId });
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function handleCleanup(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
72
|
-
if (!params.runId) return result("Cleanup requires runId.", { action: "cleanup", status: "error" }, true);
|
|
73
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
74
|
-
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cleanup", status: "error" }, true);
|
|
75
|
-
const cleanup = cleanupRunWorktrees(loaded.manifest, { force: params.force });
|
|
76
|
-
appendEvent(loaded.manifest.eventsPath, { type: "worktree.cleanup", runId: loaded.manifest.runId, data: { removed: cleanup.removed, preserved: cleanup.preserved, artifacts: cleanup.artifactPaths } });
|
|
77
|
-
const lines = [`Worktree cleanup for ${loaded.manifest.runId}:`, "Removed:", ...(cleanup.removed.length ? cleanup.removed.map((item) => `- ${item}`) : ["- (none)"]), "Preserved:", ...(cleanup.preserved.length ? cleanup.preserved.map((item) => `- ${item.path}: ${item.reason}`) : ["- (none)"]), "Artifacts:", ...(cleanup.artifactPaths.length ? cleanup.artifactPaths.map((item) => `- ${item}`) : ["- (none)"])];
|
|
78
|
-
return result(lines.join("\n"), { action: "cleanup", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
79
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
3
|
+
import { appendEvent } from "../../state/event-log.ts";
|
|
4
|
+
import { loadRunManifestById } from "../../state/state-store.ts";
|
|
5
|
+
import { cleanupRunWorktrees } from "../../worktree/cleanup.ts";
|
|
6
|
+
import { listImportedRuns } from "../import-index.ts";
|
|
7
|
+
import { exportRunBundle } from "../run-export.ts";
|
|
8
|
+
import { importRunBundle } from "../run-import.ts";
|
|
9
|
+
import { pruneFinishedRuns } from "../run-maintenance.ts";
|
|
10
|
+
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
11
|
+
import { configRecord, result, type TeamContext } from "./context.ts";
|
|
12
|
+
|
|
13
|
+
export function handleWorktrees(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
14
|
+
if (!params.runId) return result("Worktrees requires runId.", { action: "worktrees", status: "error" }, true);
|
|
15
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
16
|
+
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "worktrees", status: "error" }, true);
|
|
17
|
+
const withWorktrees = loaded.tasks.filter((task) => task.worktree);
|
|
18
|
+
const lines = [`Worktrees for ${loaded.manifest.runId}:`, ...(withWorktrees.length ? withWorktrees.map((task) => `- ${task.id}: ${task.worktree!.path} branch=${task.worktree!.branch} reused=${task.worktree!.reused ? "true" : "false"}`) : ["- (none)"])];
|
|
19
|
+
return result(lines.join("\n"), { action: "worktrees", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function handleImports(_params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
23
|
+
const imports = listImportedRuns(ctx.cwd);
|
|
24
|
+
const lines = ["Imported pi-crew runs:", ...(imports.length ? imports.map((entry) => `- ${entry.runId} (${entry.scope})${entry.status ? ` [${entry.status}]` : ""} ${entry.team ?? "unknown"}/${entry.workflow ?? "none"}: ${entry.goal ?? ""}\n Bundle: ${entry.bundlePath}\n Summary: ${entry.summaryPath}`) : ["- (none)"])];
|
|
25
|
+
return result(lines.join("\n"), { action: "imports", status: "ok" });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function handleImport(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
29
|
+
const cfg = configRecord(params.config);
|
|
30
|
+
const bundlePath = typeof cfg.path === "string" ? cfg.path : typeof cfg.bundlePath === "string" ? cfg.bundlePath : undefined;
|
|
31
|
+
if (!bundlePath) return result("Import requires config.path pointing at run-export.json.", { action: "import", status: "error" }, true);
|
|
32
|
+
const scope = cfg.scope === "user" ? "user" : "project";
|
|
33
|
+
try {
|
|
34
|
+
const imported = importRunBundle(ctx.cwd, bundlePath, scope);
|
|
35
|
+
return result([`Imported run bundle ${imported.runId}.`, `Bundle: ${imported.bundlePath}`, `Summary: ${imported.summaryPath}`].join("\n"), { action: "import", status: "ok" });
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
38
|
+
return result(`Import failed: ${message}`, { action: "import", status: "error" }, true);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function handleExport(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
43
|
+
if (!params.runId) return result("Export requires runId.", { action: "export", status: "error" }, true);
|
|
44
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
45
|
+
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "export", status: "error" }, true);
|
|
46
|
+
const exported = exportRunBundle(loaded.manifest, loaded.tasks);
|
|
47
|
+
appendEvent(loaded.manifest.eventsPath, { type: "run.exported", runId: loaded.manifest.runId, data: exported });
|
|
48
|
+
return result([`Exported run ${loaded.manifest.runId}.`, `JSON: ${exported.jsonPath}`, `Markdown: ${exported.markdownPath}`].join("\n"), { action: "export", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function handlePrune(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
52
|
+
const keep = params.keep ?? 20;
|
|
53
|
+
if (!params.confirm) return result("prune requires confirm: true.", { action: "prune", status: "error" }, true);
|
|
54
|
+
if (keep < 0 || !Number.isInteger(keep)) return result("keep must be an integer >= 0.", { action: "prune", status: "error" }, true);
|
|
55
|
+
const pruned = pruneFinishedRuns(ctx.cwd, keep);
|
|
56
|
+
return result([`Pruned finished pi-crew runs.`, `Kept: ${pruned.kept.length}`, `Removed: ${pruned.removed.length}`, ...(pruned.removed.length ? ["Removed runs:", ...pruned.removed.map((runId) => `- ${runId}`)] : [])].join("\n"), { action: "prune", status: "ok" });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function handleForget(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
60
|
+
if (!params.runId) return result("Forget requires runId.", { action: "forget", status: "error" }, true);
|
|
61
|
+
if (!params.confirm) return result("forget requires confirm: true.", { action: "forget", status: "error" }, true);
|
|
62
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
63
|
+
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "forget", status: "error" }, true);
|
|
64
|
+
const cleanup = cleanupRunWorktrees(loaded.manifest, { force: params.force });
|
|
65
|
+
if (cleanup.preserved.length > 0 && !params.force) return result([`Run '${params.runId}' has preserved worktrees. Use force: true to forget anyway.`, ...cleanup.preserved.map((item) => `- ${item.path}: ${item.reason}`)].join("\n"), { action: "forget", status: "error", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot }, true);
|
|
66
|
+
fs.rmSync(loaded.manifest.stateRoot, { recursive: true, force: true });
|
|
67
|
+
fs.rmSync(loaded.manifest.artifactsRoot, { recursive: true, force: true });
|
|
68
|
+
return result([`Forgot run ${loaded.manifest.runId}.`, `Removed state: ${loaded.manifest.stateRoot}`, `Removed artifacts: ${loaded.manifest.artifactsRoot}`, ...(cleanup.removed.length ? ["Removed worktrees:", ...cleanup.removed.map((item) => `- ${item}`)] : [])].join("\n"), { action: "forget", status: "ok", runId: loaded.manifest.runId });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function handleCleanup(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
72
|
+
if (!params.runId) return result("Cleanup requires runId.", { action: "cleanup", status: "error" }, true);
|
|
73
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
74
|
+
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cleanup", status: "error" }, true);
|
|
75
|
+
const cleanup = cleanupRunWorktrees(loaded.manifest, { force: params.force });
|
|
76
|
+
appendEvent(loaded.manifest.eventsPath, { type: "worktree.cleanup", runId: loaded.manifest.runId, data: { removed: cleanup.removed, preserved: cleanup.preserved, artifacts: cleanup.artifactPaths } });
|
|
77
|
+
const lines = [`Worktree cleanup for ${loaded.manifest.runId}:`, "Removed:", ...(cleanup.removed.length ? cleanup.removed.map((item) => `- ${item}`) : ["- (none)"]), "Preserved:", ...(cleanup.preserved.length ? cleanup.preserved.map((item) => `- ${item.path}: ${item.reason}`) : ["- (none)"]), "Artifacts:", ...(cleanup.artifactPaths.length ? cleanup.artifactPaths.map((item) => `- ${item}`) : ["- (none)"])];
|
|
78
|
+
return result(lines.join("\n"), { action: "cleanup", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
79
|
+
}
|