pi-crew 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/AGENTS.md +32 -0
  2. package/CHANGELOG.md +6 -0
  3. package/LICENSE +21 -0
  4. package/NOTICE.md +15 -0
  5. package/README.md +703 -0
  6. package/agents/analyst.md +11 -0
  7. package/agents/critic.md +11 -0
  8. package/agents/executor.md +11 -0
  9. package/agents/explorer.md +11 -0
  10. package/agents/planner.md +11 -0
  11. package/agents/reviewer.md +11 -0
  12. package/agents/security-reviewer.md +11 -0
  13. package/agents/test-engineer.md +11 -0
  14. package/agents/verifier.md +11 -0
  15. package/agents/writer.md +11 -0
  16. package/docs/architecture.md +92 -0
  17. package/docs/live-mailbox-runtime.md +36 -0
  18. package/docs/publishing.md +65 -0
  19. package/docs/resource-formats.md +131 -0
  20. package/docs/usage.md +203 -0
  21. package/index.ts +6 -0
  22. package/install.mjs +19 -0
  23. package/package.json +79 -0
  24. package/schema.json +45 -0
  25. package/skills/.gitkeep +0 -0
  26. package/src/agents/agent-config.ts +27 -0
  27. package/src/agents/agent-serializer.ts +34 -0
  28. package/src/agents/discover-agents.ts +73 -0
  29. package/src/config/config.ts +193 -0
  30. package/src/extension/async-notifier.ts +36 -0
  31. package/src/extension/autonomous-policy.ts +122 -0
  32. package/src/extension/help.ts +43 -0
  33. package/src/extension/import-index.ts +52 -0
  34. package/src/extension/management.ts +335 -0
  35. package/src/extension/project-init.ts +74 -0
  36. package/src/extension/register.ts +349 -0
  37. package/src/extension/run-bundle-schema.ts +85 -0
  38. package/src/extension/run-export.ts +59 -0
  39. package/src/extension/run-import.ts +46 -0
  40. package/src/extension/run-index.ts +28 -0
  41. package/src/extension/run-maintenance.ts +24 -0
  42. package/src/extension/session-summary.ts +8 -0
  43. package/src/extension/team-manager-command.ts +86 -0
  44. package/src/extension/team-recommendation.ts +174 -0
  45. package/src/extension/team-tool.ts +783 -0
  46. package/src/extension/tool-result.ts +16 -0
  47. package/src/extension/validate-resources.ts +77 -0
  48. package/src/prompt/prompt-runtime.ts +58 -0
  49. package/src/runtime/async-runner.ts +26 -0
  50. package/src/runtime/background-runner.ts +43 -0
  51. package/src/runtime/child-pi.ts +75 -0
  52. package/src/runtime/model-fallback.ts +101 -0
  53. package/src/runtime/pi-args.ts +81 -0
  54. package/src/runtime/pi-json-output.ts +110 -0
  55. package/src/runtime/pi-spawn.ts +96 -0
  56. package/src/runtime/process-status.ts +25 -0
  57. package/src/runtime/task-runner.ts +164 -0
  58. package/src/runtime/team-runner.ts +135 -0
  59. package/src/runtime/worker-heartbeat.ts +21 -0
  60. package/src/schema/team-tool-schema.ts +100 -0
  61. package/src/state/artifact-store.ts +36 -0
  62. package/src/state/atomic-write.ts +18 -0
  63. package/src/state/contracts.ts +88 -0
  64. package/src/state/event-log.ts +27 -0
  65. package/src/state/locks.ts +40 -0
  66. package/src/state/mailbox.ts +188 -0
  67. package/src/state/state-store.ts +119 -0
  68. package/src/state/task-claims.ts +42 -0
  69. package/src/state/types.ts +88 -0
  70. package/src/state/usage.ts +29 -0
  71. package/src/teams/discover-teams.ts +84 -0
  72. package/src/teams/team-config.ts +22 -0
  73. package/src/teams/team-serializer.ts +36 -0
  74. package/src/ui/run-dashboard.ts +138 -0
  75. package/src/utils/frontmatter.ts +36 -0
  76. package/src/utils/ids.ts +12 -0
  77. package/src/utils/names.ts +26 -0
  78. package/src/utils/paths.ts +15 -0
  79. package/src/workflows/discover-workflows.ts +101 -0
  80. package/src/workflows/validate-workflow.ts +40 -0
  81. package/src/workflows/workflow-config.ts +24 -0
  82. package/src/workflows/workflow-serializer.ts +31 -0
  83. package/src/worktree/cleanup.ts +69 -0
  84. package/src/worktree/worktree-manager.ts +60 -0
  85. package/teams/default.team.md +12 -0
  86. package/teams/fast-fix.team.md +11 -0
  87. package/teams/implementation.team.md +15 -0
  88. package/teams/research.team.md +11 -0
  89. package/teams/review.team.md +12 -0
  90. package/tsconfig.json +19 -0
  91. package/workflows/default.workflow.md +29 -0
  92. package/workflows/fast-fix.workflow.md +22 -0
  93. package/workflows/implementation.workflow.md +47 -0
  94. package/workflows/research.workflow.md +22 -0
  95. package/workflows/review.workflow.md +30 -0
@@ -0,0 +1,138 @@
1
+ import * as fs from "node:fs";
2
+ import type { Component } from "@mariozechner/pi-tui";
3
+ import type { TeamRunManifest } from "../state/types.ts";
4
+
5
+ export type RunDashboardAction = "status" | "summary" | "artifacts" | "api" | "reload";
6
+ export interface RunDashboardSelection {
7
+ runId: string;
8
+ action: RunDashboardAction;
9
+ }
10
+
11
+ function truncate(value: string, width: number): string {
12
+ if (width <= 0) return "";
13
+ if (value.length <= width) return value;
14
+ if (width <= 1) return "…";
15
+ return `${value.slice(0, width - 1)}…`;
16
+ }
17
+
18
+ function statusIcon(status: string): string {
19
+ if (status === "completed") return "✓";
20
+ if (status === "failed") return "✗";
21
+ if (status === "cancelled") return "!";
22
+ if (status === "running") return "▶";
23
+ if (status === "blocked") return "■";
24
+ return "·";
25
+ }
26
+
27
+ function readProgressPreview(run: TeamRunManifest, maxLines = 5): string[] {
28
+ const progress = [...run.artifacts].reverse().find((artifact) => artifact.kind === "progress");
29
+ if (!progress || !fs.existsSync(progress.path)) return ["Progress: (none)"];
30
+ try {
31
+ return ["Progress:", ...fs.readFileSync(progress.path, "utf-8").split(/\r?\n/).filter(Boolean).slice(0, maxLines)];
32
+ } catch (error) {
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ return [`Progress: failed to read (${message})`];
35
+ }
36
+ }
37
+
38
+ function countByStatus(runs: TeamRunManifest[]): string {
39
+ const counts = new Map<string, number>();
40
+ for (const run of runs) counts.set(run.status, (counts.get(run.status) ?? 0) + 1);
41
+ return [...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none";
42
+ }
43
+
44
+ export class RunDashboard implements Component {
45
+ private selected = 0;
46
+ private showFullProgress = false;
47
+ private readonly runs: TeamRunManifest[];
48
+ private readonly done: (selection: RunDashboardSelection | undefined) => void;
49
+
50
+ constructor(runs: TeamRunManifest[], done: (selection: RunDashboardSelection | undefined) => void) {
51
+ this.runs = runs;
52
+ this.done = done;
53
+ }
54
+
55
+ invalidate(): void {}
56
+
57
+ render(width: number): string[] {
58
+ const innerWidth = Math.max(20, width - 4);
59
+ const borderWidth = Math.min(innerWidth, Math.max(0, width - 2));
60
+ const lines = [
61
+ `╭${"─".repeat(borderWidth)}╮`,
62
+ `│ ${truncate("pi-crew dashboard", innerWidth - 1).padEnd(innerWidth - 1)}│`,
63
+ `│ ${truncate("↑/↓/j/k select • r reload • p progress • s/u/a/i actions • q close", innerWidth - 1).padEnd(innerWidth - 1)}│`,
64
+ `│ ${truncate(`Runs: ${this.runs.length} • ${countByStatus(this.runs)}`, innerWidth - 1).padEnd(innerWidth - 1)}│`,
65
+ `├${"─".repeat(borderWidth)}┤`,
66
+ ];
67
+ if (this.runs.length === 0) {
68
+ lines.push(`│ ${truncate("No runs found.", innerWidth - 1).padEnd(innerWidth - 1)}│`);
69
+ } else {
70
+ for (let i = 0; i < Math.min(this.runs.length, 10); i++) {
71
+ const run = this.runs[i]!;
72
+ const marker = i === this.selected ? "›" : " ";
73
+ const text = `${marker} ${statusIcon(run.status)} ${run.runId} ${run.status} ${run.team}/${run.workflow ?? "none"} ${run.goal}`;
74
+ lines.push(`│ ${truncate(text, innerWidth - 1).padEnd(innerWidth - 1)}│`);
75
+ }
76
+ const selectedRun = this.runs[this.selected];
77
+ if (selectedRun) {
78
+ lines.push(`├${"─".repeat(borderWidth)}┤`);
79
+ const details = [
80
+ `Selected: ${selectedRun.runId}`,
81
+ `Status: ${selectedRun.status} | Team: ${selectedRun.team} | Workflow: ${selectedRun.workflow ?? "none"}`,
82
+ `Created: ${selectedRun.createdAt}`,
83
+ `Updated: ${selectedRun.updatedAt}`,
84
+ `Artifacts: ${selectedRun.artifacts.length} | Workspace: ${selectedRun.workspaceMode}`,
85
+ selectedRun.async ? `Async: pid=${selectedRun.async.pid ?? "unknown"} log=${selectedRun.async.logPath}` : "Async: no",
86
+ `Goal: ${selectedRun.goal}`,
87
+ ];
88
+ for (const detail of [...details, ...readProgressPreview(selectedRun, this.showFullProgress ? 20 : 5)]) {
89
+ lines.push(`│ ${truncate(detail, innerWidth - 1).padEnd(innerWidth - 1)}│`);
90
+ }
91
+ }
92
+ }
93
+ lines.push(`╰${"─".repeat(borderWidth)}╯`);
94
+ return lines.map((line) => truncate(line, width));
95
+ }
96
+
97
+ handleInput(data: string): void {
98
+ if (data === "q" || data === "\u001b") {
99
+ this.done(undefined);
100
+ return;
101
+ }
102
+ if (data === "\r" || data === "\n" || data === "s") {
103
+ const runId = this.runs[this.selected]?.runId;
104
+ this.done(runId ? { runId, action: "status" } : undefined);
105
+ return;
106
+ }
107
+ if (data === "u") {
108
+ const runId = this.runs[this.selected]?.runId;
109
+ this.done(runId ? { runId, action: "summary" } : undefined);
110
+ return;
111
+ }
112
+ if (data === "a") {
113
+ const runId = this.runs[this.selected]?.runId;
114
+ this.done(runId ? { runId, action: "artifacts" } : undefined);
115
+ return;
116
+ }
117
+ if (data === "i") {
118
+ const runId = this.runs[this.selected]?.runId;
119
+ this.done(runId ? { runId, action: "api" } : undefined);
120
+ return;
121
+ }
122
+ if (data === "r") {
123
+ this.done({ runId: this.runs[this.selected]?.runId ?? "", action: "reload" });
124
+ return;
125
+ }
126
+ if (data === "p") {
127
+ this.showFullProgress = !this.showFullProgress;
128
+ return;
129
+ }
130
+ if (data === "k" || data === "\u001b[A") {
131
+ this.selected = Math.max(0, this.selected - 1);
132
+ return;
133
+ }
134
+ if (data === "j" || data === "\u001b[B") {
135
+ this.selected = Math.min(Math.max(0, this.runs.length - 1), this.selected + 1);
136
+ }
137
+ }
138
+ }
@@ -0,0 +1,36 @@
1
+ export interface ParsedFrontmatter {
2
+ frontmatter: Record<string, string>;
3
+ body: string;
4
+ }
5
+
6
+ export function parseFrontmatter(content: string): ParsedFrontmatter {
7
+ if (!content.startsWith("---\n") && !content.startsWith("---\r\n")) {
8
+ return { frontmatter: {}, body: content };
9
+ }
10
+
11
+ const normalized = content.replaceAll("\r\n", "\n");
12
+ const end = normalized.indexOf("\n---\n", 4);
13
+ if (end === -1) return { frontmatter: {}, body: content };
14
+
15
+ const raw = normalized.slice(4, end);
16
+ const body = normalized.slice(end + "\n---\n".length);
17
+ const frontmatter: Record<string, string> = {};
18
+
19
+ for (const line of raw.split("\n")) {
20
+ const trimmed = line.trim();
21
+ if (!trimmed || trimmed.startsWith("#")) continue;
22
+ const separator = trimmed.indexOf(":");
23
+ if (separator === -1) continue;
24
+ const key = trimmed.slice(0, separator).trim();
25
+ const value = trimmed.slice(separator + 1).trim();
26
+ if (key) frontmatter[key] = value;
27
+ }
28
+
29
+ return { frontmatter, body };
30
+ }
31
+
32
+ export function parseCsv(value: string | undefined): string[] | undefined {
33
+ if (value === undefined) return undefined;
34
+ const values = value.split(",").map((item) => item.trim()).filter(Boolean);
35
+ return values.length > 0 ? [...new Set(values)] : undefined;
36
+ }
@@ -0,0 +1,12 @@
1
+ import { randomBytes } from "node:crypto";
2
+
3
+ export function createRunId(prefix = "team"): string {
4
+ const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
5
+ const suffix = randomBytes(4).toString("hex");
6
+ return `${prefix}_${stamp}_${suffix}`;
7
+ }
8
+
9
+ export function createTaskId(stepId: string, index: number): string {
10
+ const normalized = stepId.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "task";
11
+ return `${String(index + 1).padStart(2, "0")}_${normalized}`;
12
+ }
@@ -0,0 +1,26 @@
1
+ export function sanitizeName(name: string): string {
2
+ return name.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
3
+ }
4
+
5
+ export function requireString(value: unknown, label: string): { value?: string; error?: string } {
6
+ if (typeof value !== "string" || !value.trim()) return { error: `${label} must be a non-empty string.` };
7
+ return { value: value.trim() };
8
+ }
9
+
10
+ export function parseConfigObject(config: unknown): { value?: Record<string, unknown>; error?: string } {
11
+ let parsed = config;
12
+ if (typeof parsed === "string") {
13
+ try {
14
+ parsed = JSON.parse(parsed) as unknown;
15
+ } catch (error) {
16
+ const message = error instanceof Error ? error.message : String(error);
17
+ return { error: `config must be valid JSON: ${message}` };
18
+ }
19
+ }
20
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { error: "config must be an object." };
21
+ return { value: parsed as Record<string, unknown> };
22
+ }
23
+
24
+ export function hasOwn(obj: Record<string, unknown>, key: string): boolean {
25
+ return Object.prototype.hasOwnProperty.call(obj, key);
26
+ }
@@ -0,0 +1,15 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ export function packageRoot(): string {
6
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
7
+ }
8
+
9
+ export function userPiRoot(): string {
10
+ return path.join(os.homedir(), ".pi", "agent");
11
+ }
12
+
13
+ export function projectPiRoot(cwd: string): string {
14
+ return path.join(cwd, ".pi");
15
+ }
@@ -0,0 +1,101 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { ResourceSource } from "../agents/agent-config.ts";
4
+ import { parseCsv, parseFrontmatter } from "../utils/frontmatter.ts";
5
+ import { packageRoot, projectPiRoot, userPiRoot } from "../utils/paths.ts";
6
+ import type { WorkflowConfig, WorkflowStep } from "./workflow-config.ts";
7
+
8
+ export interface WorkflowDiscoveryResult {
9
+ builtin: WorkflowConfig[];
10
+ user: WorkflowConfig[];
11
+ project: WorkflowConfig[];
12
+ }
13
+
14
+ function parseStepSection(id: string, body: string): WorkflowStep | undefined {
15
+ const lines = body.trim().split("\n");
16
+ const config: Record<string, string> = {};
17
+ const taskLines: string[] = [];
18
+ let inTask = false;
19
+ for (const line of lines) {
20
+ if (!inTask) {
21
+ if (line.trim() === "") {
22
+ inTask = true;
23
+ continue;
24
+ }
25
+ const match = line.match(/^([\w-]+):\s*(.*)$/);
26
+ if (match) {
27
+ config[match[1]!.trim()] = match[2]!.trim();
28
+ continue;
29
+ }
30
+ inTask = true;
31
+ }
32
+ taskLines.push(line);
33
+ }
34
+ const role = config.role || id;
35
+ return {
36
+ id,
37
+ role,
38
+ task: taskLines.join("\n").trim() || config.task || "{goal}",
39
+ dependsOn: parseCsv(config.dependsOn),
40
+ parallelGroup: config.parallelGroup || undefined,
41
+ output: config.output === "false" ? false : config.output || undefined,
42
+ reads: config.reads === "false" ? false : parseCsv(config.reads),
43
+ model: config.model || undefined,
44
+ skills: config.skills === "false" ? false : parseCsv(config.skills),
45
+ progress: config.progress === "true" ? true : config.progress === "false" ? false : undefined,
46
+ worktree: config.worktree === "true" ? true : config.worktree === "false" ? false : undefined,
47
+ verify: config.verify === "true" ? true : config.verify === "false" ? false : undefined,
48
+ };
49
+ }
50
+
51
+ function parseWorkflowFile(filePath: string, source: ResourceSource): WorkflowConfig | undefined {
52
+ try {
53
+ const content = fs.readFileSync(filePath, "utf-8");
54
+ const { frontmatter, body } = parseFrontmatter(content);
55
+ const name = frontmatter.name?.trim() || path.basename(filePath, ".workflow.md");
56
+ const matches = [...body.matchAll(/^##\s+(.+)[^\S\n]*$/gm)];
57
+ const steps: WorkflowStep[] = [];
58
+ for (let i = 0; i < matches.length; i++) {
59
+ const match = matches[i]!;
60
+ const id = match[1]!.trim();
61
+ const sectionStart = match.index! + match[0].length + (body[match.index! + match[0].length] === "\n" ? 1 : 0);
62
+ const sectionEnd = i + 1 < matches.length ? matches[i + 1]!.index! : body.length;
63
+ const step = parseStepSection(id, body.slice(sectionStart, sectionEnd));
64
+ if (step) steps.push(step);
65
+ }
66
+ return {
67
+ name,
68
+ description: frontmatter.description?.trim() || "No description provided.",
69
+ source,
70
+ filePath,
71
+ steps,
72
+ };
73
+ } catch {
74
+ return undefined;
75
+ }
76
+ }
77
+
78
+ function readWorkflowDir(dir: string, source: ResourceSource): WorkflowConfig[] {
79
+ if (!fs.existsSync(dir)) return [];
80
+ return fs.readdirSync(dir)
81
+ .filter((entry) => entry.endsWith(".workflow.md"))
82
+ .map((entry) => parseWorkflowFile(path.join(dir, entry), source))
83
+ .filter((workflow): workflow is WorkflowConfig => workflow !== undefined)
84
+ .sort((a, b) => a.name.localeCompare(b.name));
85
+ }
86
+
87
+ export function discoverWorkflows(cwd: string): WorkflowDiscoveryResult {
88
+ return {
89
+ builtin: readWorkflowDir(path.join(packageRoot(), "workflows"), "builtin"),
90
+ user: readWorkflowDir(path.join(userPiRoot(), "workflows"), "user"),
91
+ project: readWorkflowDir(path.join(projectPiRoot(cwd), "workflows"), "project"),
92
+ };
93
+ }
94
+
95
+ export function allWorkflows(discovery: WorkflowDiscoveryResult): WorkflowConfig[] {
96
+ const byName = new Map<string, WorkflowConfig>();
97
+ for (const workflow of [...discovery.builtin, ...discovery.user, ...discovery.project]) {
98
+ byName.set(workflow.name, workflow);
99
+ }
100
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
101
+ }
@@ -0,0 +1,40 @@
1
+ import type { TeamConfig } from "../teams/team-config.ts";
2
+ import type { WorkflowConfig } from "./workflow-config.ts";
3
+
4
+ export function validateWorkflowForTeam(workflow: WorkflowConfig, team: TeamConfig): string[] {
5
+ const errors: string[] = [];
6
+ const roles = new Set(team.roles.map((role) => role.name));
7
+ const stepIds = new Set<string>();
8
+
9
+ for (const step of workflow.steps) {
10
+ if (stepIds.has(step.id)) errors.push(`Duplicate workflow step id '${step.id}'.`);
11
+ stepIds.add(step.id);
12
+ if (!roles.has(step.role)) errors.push(`Step '${step.id}' references unknown team role '${step.role}'.`);
13
+ }
14
+
15
+ for (const step of workflow.steps) {
16
+ for (const dep of step.dependsOn ?? []) {
17
+ if (!stepIds.has(dep)) errors.push(`Step '${step.id}' depends on unknown step '${dep}'.`);
18
+ }
19
+ }
20
+
21
+ const visiting = new Set<string>();
22
+ const visited = new Set<string>();
23
+ const byId = new Map(workflow.steps.map((step) => [step.id, step]));
24
+
25
+ function visit(id: string, trail: string[]): void {
26
+ if (visited.has(id)) return;
27
+ if (visiting.has(id)) {
28
+ errors.push(`Workflow dependency cycle detected: ${[...trail, id].join(" -> ")}.`);
29
+ return;
30
+ }
31
+ visiting.add(id);
32
+ const step = byId.get(id);
33
+ for (const dep of step?.dependsOn ?? []) visit(dep, [...trail, id]);
34
+ visiting.delete(id);
35
+ visited.add(id);
36
+ }
37
+
38
+ for (const step of workflow.steps) visit(step.id, []);
39
+ return [...new Set(errors)];
40
+ }
@@ -0,0 +1,24 @@
1
+ import type { ResourceSource } from "../agents/agent-config.ts";
2
+
3
+ export interface WorkflowStep {
4
+ id: string;
5
+ role: string;
6
+ task: string;
7
+ dependsOn?: string[];
8
+ parallelGroup?: string;
9
+ output?: string | false;
10
+ reads?: string[] | false;
11
+ model?: string;
12
+ skills?: string[] | false;
13
+ progress?: boolean;
14
+ worktree?: boolean;
15
+ verify?: boolean;
16
+ }
17
+
18
+ export interface WorkflowConfig {
19
+ name: string;
20
+ description: string;
21
+ source: ResourceSource;
22
+ filePath: string;
23
+ steps: WorkflowStep[];
24
+ }
@@ -0,0 +1,31 @@
1
+ import type { WorkflowConfig, WorkflowStep } from "./workflow-config.ts";
2
+
3
+ function serializeStep(step: WorkflowStep): string[] {
4
+ const lines = [`## ${step.id}`, `role: ${step.role}`];
5
+ if (step.dependsOn?.length) lines.push(`dependsOn: ${step.dependsOn.join(", ")}`);
6
+ if (step.parallelGroup) lines.push(`parallelGroup: ${step.parallelGroup}`);
7
+ if (step.output === false) lines.push("output: false");
8
+ else if (step.output) lines.push(`output: ${step.output}`);
9
+ if (step.reads === false) lines.push("reads: false");
10
+ else if (Array.isArray(step.reads) && step.reads.length > 0) lines.push(`reads: ${step.reads.join(", ")}`);
11
+ if (step.model) lines.push(`model: ${step.model}`);
12
+ if (step.skills === false) lines.push("skills: false");
13
+ else if (Array.isArray(step.skills) && step.skills.length > 0) lines.push(`skills: ${step.skills.join(", ")}`);
14
+ if (step.progress !== undefined) lines.push(`progress: ${step.progress ? "true" : "false"}`);
15
+ if (step.worktree !== undefined) lines.push(`worktree: ${step.worktree ? "true" : "false"}`);
16
+ if (step.verify !== undefined) lines.push(`verify: ${step.verify ? "true" : "false"}`);
17
+ lines.push("", step.task.trim(), "");
18
+ return lines;
19
+ }
20
+
21
+ export function serializeWorkflow(workflow: WorkflowConfig): string {
22
+ const lines = [
23
+ "---",
24
+ `name: ${workflow.name}`,
25
+ `description: ${workflow.description}`,
26
+ "---",
27
+ "",
28
+ ...workflow.steps.flatMap(serializeStep),
29
+ ];
30
+ return lines.join("\n");
31
+ }
@@ -0,0 +1,69 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import type { TeamRunManifest } from "../state/types.ts";
5
+ import { writeArtifact } from "../state/artifact-store.ts";
6
+
7
+ export interface WorktreeCleanupResult {
8
+ removed: string[];
9
+ preserved: Array<{ path: string; reason: string }>;
10
+ artifactPaths: string[];
11
+ }
12
+
13
+ function git(cwd: string, args: string[]): string {
14
+ return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
15
+ }
16
+
17
+ function isDirty(worktreePath: string): boolean {
18
+ try {
19
+ return git(worktreePath, ["status", "--porcelain"]).trim().length > 0;
20
+ } catch {
21
+ return true;
22
+ }
23
+ }
24
+
25
+ function captureDiff(worktreePath: string): string {
26
+ try {
27
+ return [git(worktreePath, ["status", "--porcelain"]), "", git(worktreePath, ["diff", "--stat"]), "", git(worktreePath, ["diff"])].join("\n");
28
+ } catch (error) {
29
+ const message = error instanceof Error ? error.message : String(error);
30
+ return `Failed to capture cleanup diff for ${worktreePath}: ${message}`;
31
+ }
32
+ }
33
+
34
+ export function cleanupRunWorktrees(manifest: TeamRunManifest, options: { force?: boolean } = {}): WorktreeCleanupResult {
35
+ const worktreeRoot = path.join(manifest.cwd, ".pi", "teams", "worktrees", manifest.runId);
36
+ const result: WorktreeCleanupResult = { removed: [], preserved: [], artifactPaths: [] };
37
+ if (!fs.existsSync(worktreeRoot)) return result;
38
+
39
+ for (const entry of fs.readdirSync(worktreeRoot)) {
40
+ const worktreePath = path.join(worktreeRoot, entry);
41
+ if (!fs.statSync(worktreePath).isDirectory()) continue;
42
+ const dirty = isDirty(worktreePath);
43
+ if (dirty && !options.force) {
44
+ const artifact = writeArtifact(manifest.artifactsRoot, {
45
+ kind: "diff",
46
+ relativePath: `cleanup/${entry}.diff`,
47
+ content: captureDiff(worktreePath),
48
+ producer: "worktree-cleanup",
49
+ });
50
+ result.artifactPaths.push(artifact.path);
51
+ result.preserved.push({ path: worktreePath, reason: "dirty worktree preserved" });
52
+ continue;
53
+ }
54
+ try {
55
+ git(manifest.cwd, ["worktree", "remove", options.force ? "--force" : "", worktreePath].filter(Boolean));
56
+ result.removed.push(worktreePath);
57
+ } catch (error) {
58
+ const message = error instanceof Error ? error.message : String(error);
59
+ result.preserved.push({ path: worktreePath, reason: message });
60
+ }
61
+ }
62
+
63
+ try {
64
+ if (fs.existsSync(worktreeRoot) && fs.readdirSync(worktreeRoot).length === 0) fs.rmSync(worktreeRoot, { recursive: true, force: true });
65
+ } catch {
66
+ // Non-critical cleanup.
67
+ }
68
+ return result;
69
+ }
@@ -0,0 +1,60 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { loadConfig } from "../config/config.ts";
5
+ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
6
+
7
+ export interface PreparedTaskWorkspace {
8
+ cwd: string;
9
+ worktreePath?: string;
10
+ branch?: string;
11
+ reused?: boolean;
12
+ }
13
+
14
+ function git(cwd: string, args: string[]): string {
15
+ return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
16
+ }
17
+
18
+ function sanitizeBranchPart(value: string): string {
19
+ return value.toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").replace(/^-+|-+$/g, "") || "task";
20
+ }
21
+
22
+ export function findGitRoot(cwd: string): string {
23
+ return git(cwd, ["rev-parse", "--show-toplevel"]);
24
+ }
25
+
26
+ export function assertCleanLeader(repoRoot: string): void {
27
+ const status = git(repoRoot, ["status", "--porcelain"]);
28
+ if (status.trim()) {
29
+ throw new Error("Worktree mode requires a clean leader repository. Commit/stash changes or use workspaceMode: 'single'.");
30
+ }
31
+ }
32
+
33
+ export function prepareTaskWorkspace(manifest: TeamRunManifest, task: TeamTaskState): PreparedTaskWorkspace {
34
+ if (manifest.workspaceMode !== "worktree") return { cwd: task.cwd };
35
+ const repoRoot = findGitRoot(manifest.cwd);
36
+ const loadedConfig = loadConfig(manifest.cwd);
37
+ if (loadedConfig.config.requireCleanWorktreeLeader !== false) assertCleanLeader(repoRoot);
38
+ const worktreeRoot = path.join(repoRoot, ".pi", "teams", "worktrees", manifest.runId);
39
+ fs.mkdirSync(worktreeRoot, { recursive: true });
40
+ const worktreePath = path.join(worktreeRoot, task.id);
41
+ const branch = `pi-crew/${sanitizeBranchPart(manifest.runId)}/${sanitizeBranchPart(task.id)}`;
42
+ if (fs.existsSync(worktreePath)) {
43
+ const currentBranch = git(worktreePath, ["rev-parse", "--abbrev-ref", "HEAD"]);
44
+ if (currentBranch !== branch) {
45
+ throw new Error(`Existing worktree branch mismatch at ${worktreePath}: expected '${branch}', got '${currentBranch}'.`);
46
+ }
47
+ return { cwd: worktreePath, worktreePath, branch, reused: true };
48
+ }
49
+ git(repoRoot, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
50
+ return { cwd: worktreePath, worktreePath, branch, reused: false };
51
+ }
52
+
53
+ export function captureWorktreeDiff(worktreePath: string): string {
54
+ try {
55
+ return git(worktreePath, ["diff", "--stat"]) + "\n\n" + git(worktreePath, ["diff"]);
56
+ } catch (error) {
57
+ const message = error instanceof Error ? error.message : String(error);
58
+ return `Failed to capture worktree diff: ${message}`;
59
+ }
60
+ }
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: default
3
+ description: Balanced team for ordinary implementation tasks
4
+ defaultWorkflow: default
5
+ workspaceMode: single
6
+ maxConcurrency: 2
7
+ ---
8
+
9
+ - explorer: agent=explorer fast discovery
10
+ - planner: agent=planner plan the work
11
+ - executor: agent=executor implement changes
12
+ - verifier: agent=verifier verify completion
@@ -0,0 +1,11 @@
1
+ ---
2
+ name: fast-fix
3
+ description: Small team for quick bug fixes
4
+ defaultWorkflow: fast-fix
5
+ workspaceMode: single
6
+ maxConcurrency: 1
7
+ ---
8
+
9
+ - explorer: agent=explorer find the relevant files
10
+ - executor: agent=executor make the fix
11
+ - verifier: agent=verifier verify the fix
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: implementation
3
+ description: Full implementation team with analysis, critique, execution, review, and verification
4
+ defaultWorkflow: implementation
5
+ workspaceMode: single
6
+ maxConcurrency: 3
7
+ ---
8
+
9
+ - explorer: agent=explorer map the codebase
10
+ - analyst: agent=analyst clarify requirements and constraints
11
+ - planner: agent=planner create execution plan
12
+ - critic: agent=critic challenge the plan
13
+ - executor: agent=executor implement the plan
14
+ - reviewer: agent=reviewer review the implementation
15
+ - verifier: agent=verifier verify done
@@ -0,0 +1,11 @@
1
+ ---
2
+ name: research
3
+ description: Team for investigation and documentation
4
+ defaultWorkflow: research
5
+ workspaceMode: single
6
+ maxConcurrency: 2
7
+ ---
8
+
9
+ - explorer: agent=explorer gather codebase facts
10
+ - analyst: agent=analyst analyze findings
11
+ - writer: agent=writer produce final notes
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: review
3
+ description: Team for code review and security review
4
+ defaultWorkflow: review
5
+ workspaceMode: single
6
+ maxConcurrency: 2
7
+ ---
8
+
9
+ - explorer: agent=explorer understand changed areas
10
+ - reviewer: agent=reviewer review correctness and maintainability
11
+ - security-reviewer: agent=security-reviewer review security risks
12
+ - verifier: agent=verifier summarize pass/fail
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "noImplicitAny": true,
8
+ "exactOptionalPropertyTypes": false,
9
+ "skipLibCheck": true,
10
+ "allowImportingTsExtensions": true,
11
+ "noEmit": true,
12
+ "types": ["node"]
13
+ },
14
+ "include": [
15
+ "*.ts",
16
+ "src/**/*.ts",
17
+ "src/**/*.ts"
18
+ ]
19
+ }