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,25 @@
1
+ export interface ProcessLiveness {
2
+ pid?: number;
3
+ alive: boolean;
4
+ detail: string;
5
+ }
6
+
7
+ export function checkProcessLiveness(pid: number | undefined): ProcessLiveness {
8
+ if (pid === undefined || !Number.isInteger(pid) || pid <= 0) {
9
+ return { pid, alive: false, detail: "no pid recorded" };
10
+ }
11
+ try {
12
+ process.kill(pid, 0);
13
+ return { pid, alive: true, detail: "process is alive" };
14
+ } catch (error) {
15
+ const nodeError = error as NodeJS.ErrnoException;
16
+ if (nodeError.code === "EPERM") return { pid, alive: true, detail: "process exists but permission is denied" };
17
+ if (nodeError.code === "ESRCH") return { pid, alive: false, detail: "process does not exist" };
18
+ const message = error instanceof Error ? error.message : String(error);
19
+ return { pid, alive: false, detail: message };
20
+ }
21
+ }
22
+
23
+ export function isActiveRunStatus(status: string): boolean {
24
+ return status === "queued" || status === "planning" || status === "running";
25
+ }
@@ -0,0 +1,164 @@
1
+ import type { AgentConfig } from "../agents/agent-config.ts";
2
+ import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../state/types.ts";
3
+ import { writeArtifact } from "../state/artifact-store.ts";
4
+ import { appendEvent } from "../state/event-log.ts";
5
+ import { saveRunManifest, saveRunTasks } from "../state/state-store.ts";
6
+ import { createTaskClaim } from "../state/task-claims.ts";
7
+ import { createWorkerHeartbeat, touchWorkerHeartbeat } from "./worker-heartbeat.ts";
8
+ import type { WorkflowStep } from "../workflows/workflow-config.ts";
9
+ import { captureWorktreeDiff, prepareTaskWorkspace } from "../worktree/worktree-manager.ts";
10
+ import { buildModelCandidates, formatModelAttemptNote, isRetryableModelFailure, type ModelAttemptSummary } from "./model-fallback.ts";
11
+ import { parsePiJsonOutput, type ParsedPiJsonOutput } from "./pi-json-output.ts";
12
+ import { runChildPi } from "./child-pi.ts";
13
+
14
+ export interface TaskRunnerInput {
15
+ manifest: TeamRunManifest;
16
+ tasks: TeamTaskState[];
17
+ task: TeamTaskState;
18
+ step: WorkflowStep;
19
+ agent: AgentConfig;
20
+ signal?: AbortSignal;
21
+ executeWorkers: boolean;
22
+ }
23
+
24
+ function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: TeamTaskState): string {
25
+ return [
26
+ "# pi-crew Worker Runtime Context",
27
+ `Run ID: ${manifest.runId}`,
28
+ `Team: ${manifest.team}`,
29
+ `Workflow: ${manifest.workflow ?? "(none)"}`,
30
+ `State root: ${manifest.stateRoot}`,
31
+ `Artifacts root: ${manifest.artifactsRoot}`,
32
+ `Events path: ${manifest.eventsPath}`,
33
+ `Task ID: ${task.id}`,
34
+ `Task cwd: ${task.cwd}`,
35
+ `Workspace mode: ${manifest.workspaceMode}`,
36
+ "",
37
+ `Goal:\n${manifest.goal}`,
38
+ "",
39
+ `Step: ${step.id}`,
40
+ `Role: ${step.role}`,
41
+ "",
42
+ "Protocol:",
43
+ "- Stay within the task scope unless the prompt explicitly says otherwise.",
44
+ "- Report blockers and verification evidence in the final result.",
45
+ "- Do not claim completion without evidence.",
46
+ "",
47
+ "Task:",
48
+ step.task.replaceAll("{goal}", manifest.goal),
49
+ ].join("\n");
50
+ }
51
+
52
+ function updateTask(tasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
53
+ return tasks.map((task) => task.id === updated.id ? updated : task);
54
+ }
55
+
56
+ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
57
+ let manifest = input.manifest;
58
+ const workspace = prepareTaskWorkspace(manifest, input.task);
59
+ let task: TeamTaskState = {
60
+ ...input.task,
61
+ cwd: workspace.cwd,
62
+ worktree: workspace.worktreePath && workspace.branch ? { path: workspace.worktreePath, branch: workspace.branch, reused: workspace.reused ?? false } : input.task.worktree,
63
+ status: "running",
64
+ startedAt: new Date().toISOString(),
65
+ claim: createTaskClaim(`task-runner:${input.task.id}`),
66
+ heartbeat: createWorkerHeartbeat(input.task.id),
67
+ };
68
+ let tasks = updateTask(input.tasks, task);
69
+ saveRunTasks(manifest, tasks);
70
+ appendEvent(manifest.eventsPath, { type: "task.started", runId: manifest.runId, taskId: task.id, data: { role: task.role, agent: task.agent, cwd: task.cwd, worktreePath: workspace.worktreePath, worktreeBranch: workspace.branch, worktreeReused: workspace.reused } });
71
+
72
+ const prompt = renderTaskPrompt(manifest, input.step, task);
73
+ const promptArtifact = writeArtifact(manifest.artifactsRoot, {
74
+ kind: "prompt",
75
+ relativePath: `prompts/${task.id}.md`,
76
+ content: `${prompt}\n`,
77
+ producer: task.id,
78
+ });
79
+
80
+ let resultArtifact: ArtifactDescriptor;
81
+ let logArtifact: ArtifactDescriptor | undefined;
82
+ let exitCode: number | null = 0;
83
+ let error: string | undefined;
84
+ let modelAttempts: ModelAttemptSummary[] | undefined;
85
+ let parsedOutput: ParsedPiJsonOutput | undefined;
86
+
87
+ if (input.executeWorkers) {
88
+ const candidates = buildModelCandidates(input.step.model ?? input.agent.model, input.agent.fallbackModels, undefined);
89
+ const attemptModels = candidates.length > 0 ? candidates : [input.step.model ?? input.agent.model];
90
+ const logs: string[] = [];
91
+ let finalStdout = "";
92
+ let finalStderr = "";
93
+ modelAttempts = [];
94
+ for (let i = 0; i < attemptModels.length; i++) {
95
+ const model = attemptModels[i];
96
+ const childResult = await runChildPi({ cwd: task.cwd, task: prompt, agent: input.agent, model, signal: input.signal });
97
+ exitCode = childResult.exitCode;
98
+ finalStdout = childResult.stdout;
99
+ finalStderr = childResult.stderr;
100
+ parsedOutput = parsePiJsonOutput(childResult.stdout);
101
+ error = childResult.error || (childResult.exitCode && childResult.exitCode !== 0 ? childResult.stderr || `Child Pi exited with ${childResult.exitCode}` : undefined);
102
+ const attempt: ModelAttemptSummary = { model: model ?? "default", success: !error, exitCode, error };
103
+ modelAttempts.push(attempt);
104
+ logs.push(`MODEL ATTEMPT ${i + 1}: ${attempt.model}`, `success=${attempt.success}`, `exitCode=${attempt.exitCode ?? "null"}`, attempt.error ? `error=${attempt.error}` : "", "");
105
+ if (!error) break;
106
+ const nextModel = attemptModels[i + 1];
107
+ if (!nextModel || !isRetryableModelFailure(error)) break;
108
+ logs.push(formatModelAttemptNote(attempt, nextModel), "");
109
+ }
110
+ resultArtifact = writeArtifact(manifest.artifactsRoot, {
111
+ kind: "result",
112
+ relativePath: `results/${task.id}.txt`,
113
+ content: parsedOutput?.finalText || finalStdout || finalStderr || "(no output)",
114
+ producer: task.id,
115
+ });
116
+ logArtifact = writeArtifact(manifest.artifactsRoot, {
117
+ kind: "log",
118
+ relativePath: `logs/${task.id}.log`,
119
+ content: [...logs, `finalExitCode=${exitCode ?? "null"}`, `jsonEvents=${parsedOutput?.jsonEvents ?? 0}`, parsedOutput?.usage ? `usage=${JSON.stringify(parsedOutput.usage)}` : "", "", "STDOUT:", finalStdout, "", "STDERR:", finalStderr].join("\n"),
120
+ producer: task.id,
121
+ });
122
+ } else {
123
+ resultArtifact = writeArtifact(manifest.artifactsRoot, {
124
+ kind: "result",
125
+ relativePath: `results/${task.id}.md`,
126
+ content: [
127
+ `# ${task.id}`,
128
+ "",
129
+ "Worker execution is disabled in this scaffold-safe run.",
130
+ "The prompt artifact contains the exact task that will be sent to a child Pi worker when execution is enabled.",
131
+ ].join("\n"),
132
+ producer: task.id,
133
+ });
134
+ }
135
+
136
+ const diffArtifact = workspace.worktreePath ? writeArtifact(manifest.artifactsRoot, {
137
+ kind: "diff",
138
+ relativePath: `diffs/${task.id}.diff`,
139
+ content: captureWorktreeDiff(workspace.worktreePath),
140
+ producer: task.id,
141
+ }) : undefined;
142
+
143
+ task = {
144
+ ...task,
145
+ status: error ? "failed" : "completed",
146
+ finishedAt: new Date().toISOString(),
147
+ exitCode,
148
+ modelAttempts,
149
+ usage: parsedOutput?.usage,
150
+ jsonEvents: parsedOutput?.jsonEvents,
151
+ error,
152
+ promptArtifact,
153
+ resultArtifact,
154
+ claim: undefined,
155
+ heartbeat: touchWorkerHeartbeat(task.heartbeat ?? createWorkerHeartbeat(task.id), { alive: false }),
156
+ ...(logArtifact ? { logArtifact } : {}),
157
+ };
158
+ tasks = updateTask(tasks, task);
159
+ manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, promptArtifact, resultArtifact, ...(logArtifact ? [logArtifact] : []), ...(diffArtifact ? [diffArtifact] : [])] };
160
+ saveRunManifest(manifest);
161
+ saveRunTasks(manifest, tasks);
162
+ appendEvent(manifest.eventsPath, { type: error ? "task.failed" : "task.completed", runId: manifest.runId, taskId: task.id, message: error });
163
+ return { manifest, tasks };
164
+ }
@@ -0,0 +1,135 @@
1
+ import type { AgentConfig } from "../agents/agent-config.ts";
2
+ import { writeArtifact } from "../state/artifact-store.ts";
3
+ import type { TeamConfig } from "../teams/team-config.ts";
4
+ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
5
+ import { saveRunManifest, saveRunTasks, updateRunStatus } from "../state/state-store.ts";
6
+ import { aggregateUsage, formatUsage } from "../state/usage.ts";
7
+ import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
8
+ import { runTeamTask } from "./task-runner.ts";
9
+
10
+ export interface ExecuteTeamRunInput {
11
+ manifest: TeamRunManifest;
12
+ tasks: TeamTaskState[];
13
+ team: TeamConfig;
14
+ workflow: WorkflowConfig;
15
+ agents: AgentConfig[];
16
+ executeWorkers: boolean;
17
+ signal?: AbortSignal;
18
+ }
19
+
20
+ function findReadyTask(tasks: TeamTaskState[]): TeamTaskState | undefined {
21
+ const completedStepIds = new Set(tasks.filter((task) => task.status === "completed").map((task) => task.stepId).filter((id): id is string => id !== undefined));
22
+ return tasks.find((task) => task.status === "queued" && task.dependsOn.every((dep) => completedStepIds.has(dep)));
23
+ }
24
+
25
+ function findStep(workflow: WorkflowConfig, task: TeamTaskState): WorkflowStep {
26
+ const step = workflow.steps.find((candidate) => candidate.id === task.stepId);
27
+ if (!step) throw new Error(`Workflow step '${task.stepId}' not found for task '${task.id}'.`);
28
+ return step;
29
+ }
30
+
31
+ function findAgent(agents: AgentConfig[], task: TeamTaskState): AgentConfig {
32
+ const agent = agents.find((candidate) => candidate.name === task.agent);
33
+ if (!agent) throw new Error(`Agent '${task.agent}' not found for task '${task.id}'.`);
34
+ return agent;
35
+ }
36
+
37
+ function markBlocked(tasks: TeamTaskState[], reason: string): TeamTaskState[] {
38
+ return tasks.map((task) => task.status === "queued" ? { ...task, status: "skipped", error: reason, finishedAt: new Date().toISOString() } : task);
39
+ }
40
+
41
+ function writeProgress(manifest: TeamRunManifest, tasks: TeamTaskState[], producer: string): TeamRunManifest {
42
+ const counts = new Map<string, number>();
43
+ for (const task of tasks) counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
44
+ const progress = writeArtifact(manifest.artifactsRoot, {
45
+ kind: "progress",
46
+ relativePath: "progress.md",
47
+ producer,
48
+ content: [
49
+ `# pi-crew progress ${manifest.runId}`,
50
+ "",
51
+ `Status: ${manifest.status}`,
52
+ `Team: ${manifest.team}`,
53
+ `Workflow: ${manifest.workflow ?? "(none)"}`,
54
+ `Updated: ${new Date().toISOString()}`,
55
+ `Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
56
+ "",
57
+ "## Tasks",
58
+ ...tasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
59
+ "",
60
+ ].join("\n"),
61
+ });
62
+ return { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts.filter((artifact) => !(artifact.kind === "progress" && artifact.path === progress.path)), progress] };
63
+ }
64
+
65
+ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
66
+ let manifest = updateRunStatus(input.manifest, "running", input.executeWorkers ? "Executing team workflow." : "Creating workflow prompts and placeholder results.");
67
+ let tasks = input.tasks;
68
+ manifest = writeProgress(manifest, tasks, "team-runner");
69
+ saveRunManifest(manifest);
70
+
71
+ while (tasks.some((task) => task.status === "queued")) {
72
+ if (input.signal?.aborted) {
73
+ tasks = tasks.map((task) => task.status === "queued" || task.status === "running" ? { ...task, status: "cancelled", finishedAt: new Date().toISOString(), error: "Run cancelled." } : task);
74
+ saveRunTasks(manifest, tasks);
75
+ manifest = updateRunStatus(manifest, "cancelled", "Run cancelled.");
76
+ return { manifest, tasks };
77
+ }
78
+
79
+ const failed = tasks.find((task) => task.status === "failed");
80
+ if (failed) {
81
+ tasks = markBlocked(tasks, `Blocked by failed task '${failed.id}'.`);
82
+ saveRunTasks(manifest, tasks);
83
+ manifest = updateRunStatus(manifest, "failed", `Failed at task '${failed.id}'.`);
84
+ return { manifest, tasks };
85
+ }
86
+
87
+ const task = findReadyTask(tasks);
88
+ if (!task) {
89
+ tasks = markBlocked(tasks, "No ready queued task; dependency graph may be invalid.");
90
+ saveRunTasks(manifest, tasks);
91
+ manifest = updateRunStatus(manifest, "blocked", "No ready queued task.");
92
+ return { manifest, tasks };
93
+ }
94
+
95
+ const step = findStep(input.workflow, task);
96
+ const agent = findAgent(input.agents, task);
97
+ const result = await runTeamTask({ manifest, tasks, task, step, agent, signal: input.signal, executeWorkers: input.executeWorkers });
98
+ manifest = result.manifest;
99
+ tasks = result.tasks;
100
+ manifest = writeProgress(manifest, tasks, "team-runner");
101
+ saveRunManifest(manifest);
102
+ }
103
+
104
+ const failed = tasks.find((task) => task.status === "failed");
105
+ if (failed) {
106
+ manifest = updateRunStatus(manifest, "failed", `Failed at task '${failed.id}'.`);
107
+ } else {
108
+ manifest = updateRunStatus(manifest, "completed", input.executeWorkers ? "Team workflow completed." : "Team workflow scaffold completed without launching child workers.");
109
+ }
110
+ manifest = writeProgress(manifest, tasks, "team-runner");
111
+ saveRunManifest(manifest);
112
+ const usage = aggregateUsage(tasks);
113
+ const summaryArtifact = writeArtifact(manifest.artifactsRoot, {
114
+ kind: "summary",
115
+ relativePath: "summary.md",
116
+ producer: "team-runner",
117
+ content: [
118
+ `# pi-crew run ${manifest.runId}`,
119
+ "",
120
+ `Status: ${manifest.status}`,
121
+ `Team: ${manifest.team}`,
122
+ `Workflow: ${manifest.workflow ?? "(none)"}`,
123
+ `Goal: ${manifest.goal}`,
124
+ `Usage: ${formatUsage(usage)}`,
125
+ "",
126
+ "## Tasks",
127
+ ...tasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
128
+ "",
129
+ ].join("\n"),
130
+ });
131
+ manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, summaryArtifact] };
132
+ saveRunManifest(manifest);
133
+ saveRunTasks(manifest, tasks);
134
+ return { manifest, tasks };
135
+ }
@@ -0,0 +1,21 @@
1
+ export interface WorkerHeartbeatState {
2
+ workerId: string;
3
+ pid?: number;
4
+ lastSeenAt: string;
5
+ lastStdoutAt?: string;
6
+ lastEventAt?: string;
7
+ turnCount?: number;
8
+ alive?: boolean;
9
+ }
10
+
11
+ export function createWorkerHeartbeat(workerId: string, pid?: number, now = new Date()): WorkerHeartbeatState {
12
+ return { workerId, pid, lastSeenAt: now.toISOString(), alive: true };
13
+ }
14
+
15
+ export function touchWorkerHeartbeat(heartbeat: WorkerHeartbeatState, updates: Partial<Omit<WorkerHeartbeatState, "workerId">> = {}, now = new Date()): WorkerHeartbeatState {
16
+ return { ...heartbeat, ...updates, lastSeenAt: now.toISOString() };
17
+ }
18
+
19
+ export function isWorkerHeartbeatStale(heartbeat: WorkerHeartbeatState, staleMs: number, now = new Date()): boolean {
20
+ return now.getTime() - Date.parse(heartbeat.lastSeenAt) > staleMs;
21
+ }
@@ -0,0 +1,100 @@
1
+ import { Type } from "typebox";
2
+
3
+ const SkillOverride = Type.Unsafe({
4
+ type: ["string", "array", "boolean"],
5
+ items: { type: "string" },
6
+ description: "Skill name(s) to inject, array of skill names, or false to disable role defaults.",
7
+ });
8
+
9
+ export const TeamToolParams = Type.Object({
10
+ action: Type.Optional(Type.Union([
11
+ Type.Literal("run"),
12
+ Type.Literal("plan"),
13
+ Type.Literal("status"),
14
+ Type.Literal("list"),
15
+ Type.Literal("get"),
16
+ Type.Literal("cancel"),
17
+ Type.Literal("resume"),
18
+ Type.Literal("create"),
19
+ Type.Literal("update"),
20
+ Type.Literal("delete"),
21
+ Type.Literal("doctor"),
22
+ Type.Literal("cleanup"),
23
+ Type.Literal("events"),
24
+ Type.Literal("artifacts"),
25
+ Type.Literal("worktrees"),
26
+ Type.Literal("forget"),
27
+ Type.Literal("summary"),
28
+ Type.Literal("prune"),
29
+ Type.Literal("export"),
30
+ Type.Literal("import"),
31
+ Type.Literal("imports"),
32
+ Type.Literal("help"),
33
+ Type.Literal("validate"),
34
+ Type.Literal("config"),
35
+ Type.Literal("init"),
36
+ Type.Literal("recommend"),
37
+ Type.Literal("autonomy"),
38
+ Type.Literal("api"),
39
+ ], { description: "Team action. Defaults to 'list' when omitted." })),
40
+ resource: Type.Optional(Type.Union([
41
+ Type.Literal("agent"),
42
+ Type.Literal("team"),
43
+ Type.Literal("workflow"),
44
+ ], { description: "Resource kind for get/create/update/delete/list. Defaults to all for list." })),
45
+ team: Type.Optional(Type.String({ description: "Team name, e.g. default or implementation." })),
46
+ workflow: Type.Optional(Type.String({ description: "Workflow name, e.g. default or review." })),
47
+ role: Type.Optional(Type.String({ description: "Role name to run directly within a team." })),
48
+ agent: Type.Optional(Type.String({ description: "Agent name to inspect or run directly." })),
49
+ goal: Type.Optional(Type.String({ description: "High-level objective for a team run." })),
50
+ task: Type.Optional(Type.String({ description: "Concrete task text for direct role/agent execution." })),
51
+ runId: Type.Optional(Type.String({ description: "Run ID for status, cancel, or resume." })),
52
+ async: Type.Optional(Type.Boolean({ description: "Run in background when execution support is enabled." })),
53
+ workspaceMode: Type.Optional(Type.Union([
54
+ Type.Literal("single"),
55
+ Type.Literal("worktree"),
56
+ ], { description: "Workspace isolation mode. Worktree mode is planned after MVP." })),
57
+ context: Type.Optional(Type.Union([
58
+ Type.Literal("fresh"),
59
+ Type.Literal("fork"),
60
+ ], { description: "Child context mode for workers." })),
61
+ cwd: Type.Optional(Type.String({ description: "Working directory override." })),
62
+ model: Type.Optional(Type.String({ description: "Model override for direct runs." })),
63
+ skill: Type.Optional(SkillOverride),
64
+ scope: Type.Optional(Type.Union([
65
+ Type.Literal("user"),
66
+ Type.Literal("project"),
67
+ Type.Literal("both"),
68
+ ], { description: "Resource scope for discovery or management." })),
69
+ config: Type.Optional(Type.Unsafe({ description: "Resource config for management actions." })),
70
+ dryRun: Type.Optional(Type.Boolean({ description: "Preview a management mutation without writing files." })),
71
+ confirm: Type.Optional(Type.Boolean({ description: "Required for destructive management actions." })),
72
+ force: Type.Optional(Type.Boolean({ description: "Override reference checks for destructive management actions." })),
73
+ keep: Type.Optional(Type.Integer({ minimum: 0, description: "Number of finished runs to keep for prune." })),
74
+ updateReferences: Type.Optional(Type.Boolean({ description: "When renaming agents or workflows, update team references in the same project/user scope." })),
75
+ });
76
+
77
+ export interface TeamToolParamsValue {
78
+ action?: "run" | "plan" | "status" | "list" | "get" | "cancel" | "resume" | "create" | "update" | "delete" | "doctor" | "cleanup" | "events" | "artifacts" | "worktrees" | "forget" | "summary" | "prune" | "export" | "import" | "imports" | "help" | "validate" | "config" | "init" | "recommend" | "autonomy" | "api";
79
+ resource?: "agent" | "team" | "workflow";
80
+ team?: string;
81
+ workflow?: string;
82
+ role?: string;
83
+ agent?: string;
84
+ goal?: string;
85
+ task?: string;
86
+ runId?: string;
87
+ async?: boolean;
88
+ workspaceMode?: "single" | "worktree";
89
+ context?: "fresh" | "fork";
90
+ cwd?: string;
91
+ model?: string;
92
+ skill?: string | string[] | boolean;
93
+ scope?: "user" | "project" | "both";
94
+ config?: unknown;
95
+ dryRun?: boolean;
96
+ confirm?: boolean;
97
+ force?: boolean;
98
+ keep?: number;
99
+ updateReferences?: boolean;
100
+ }
@@ -0,0 +1,36 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import type { ArtifactDescriptor } from "./types.ts";
5
+ import { atomicWriteFile } from "./atomic-write.ts";
6
+
7
+ function hashContent(content: string): string {
8
+ return createHash("sha256").update(content).digest("hex");
9
+ }
10
+
11
+ export interface ArtifactWriteOptions {
12
+ kind: ArtifactDescriptor["kind"];
13
+ relativePath: string;
14
+ content: string;
15
+ producer: string;
16
+ retention?: ArtifactDescriptor["retention"];
17
+ }
18
+
19
+ export function writeArtifact(artifactsRoot: string, options: ArtifactWriteOptions): ArtifactDescriptor {
20
+ const normalizedRelativePath = options.relativePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
21
+ if (normalizedRelativePath.startsWith("../") || path.isAbsolute(normalizedRelativePath)) {
22
+ throw new Error(`Invalid artifact path: ${options.relativePath}`);
23
+ }
24
+ const filePath = path.join(artifactsRoot, normalizedRelativePath);
25
+ atomicWriteFile(filePath, options.content);
26
+ const stats = fs.statSync(filePath);
27
+ return {
28
+ kind: options.kind,
29
+ path: filePath,
30
+ createdAt: new Date().toISOString(),
31
+ producer: options.producer,
32
+ sizeBytes: stats.size,
33
+ contentHash: hashContent(options.content),
34
+ retention: options.retention ?? "run",
35
+ };
36
+ }
@@ -0,0 +1,18 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export function atomicWriteFile(filePath: string, content: string): void {
5
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
6
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
7
+ fs.writeFileSync(tempPath, content, "utf-8");
8
+ fs.renameSync(tempPath, filePath);
9
+ }
10
+
11
+ export function atomicWriteJson<T>(filePath: string, value: T): void {
12
+ atomicWriteFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
13
+ }
14
+
15
+ export function readJsonFile<T>(filePath: string): T | undefined {
16
+ if (!fs.existsSync(filePath)) return undefined;
17
+ return JSON.parse(fs.readFileSync(filePath, "utf-8")) as T;
18
+ }
@@ -0,0 +1,88 @@
1
+ export const TEAM_RUN_STATUSES = ["queued", "planning", "running", "blocked", "completed", "failed", "cancelled"] as const;
2
+ export type TeamRunStatus = typeof TEAM_RUN_STATUSES[number];
3
+
4
+ export const TEAM_TASK_STATUSES = ["queued", "running", "completed", "failed", "cancelled", "skipped"] as const;
5
+ export type TeamTaskStatus = typeof TEAM_TASK_STATUSES[number];
6
+
7
+ export const TEAM_TERMINAL_RUN_STATUSES: ReadonlySet<TeamRunStatus> = new Set(["blocked", "completed", "failed", "cancelled"]);
8
+ export const TEAM_TERMINAL_TASK_STATUSES: ReadonlySet<TeamTaskStatus> = new Set(["completed", "failed", "cancelled", "skipped"]);
9
+
10
+ export const TEAM_RUN_STATUS_TRANSITIONS: Readonly<Record<TeamRunStatus, readonly TeamRunStatus[]>> = {
11
+ queued: ["planning", "running", "cancelled", "failed"],
12
+ planning: ["running", "blocked", "cancelled", "failed"],
13
+ running: ["blocked", "completed", "failed", "cancelled"],
14
+ blocked: ["running", "cancelled", "failed"],
15
+ completed: ["running", "cancelled"],
16
+ failed: ["running", "cancelled"],
17
+ cancelled: ["running"],
18
+ };
19
+
20
+ export const TEAM_TASK_STATUS_TRANSITIONS: Readonly<Record<TeamTaskStatus, readonly TeamTaskStatus[]>> = {
21
+ queued: ["running", "cancelled", "skipped", "failed"],
22
+ running: ["completed", "failed", "cancelled", "queued"],
23
+ completed: ["queued"],
24
+ failed: ["queued", "cancelled"],
25
+ cancelled: ["queued"],
26
+ skipped: ["queued", "cancelled"],
27
+ };
28
+
29
+ export const TEAM_EVENT_TYPES = [
30
+ "run.created",
31
+ "run.queued",
32
+ "run.planning",
33
+ "run.running",
34
+ "run.blocked",
35
+ "run.completed",
36
+ "run.failed",
37
+ "run.cancelled",
38
+ "task.started",
39
+ "task.completed",
40
+ "task.failed",
41
+ "worktree.cleanup",
42
+ "async.spawned",
43
+ "async.started",
44
+ "async.completed",
45
+ "async.failed",
46
+ "async.stale",
47
+ ] as const;
48
+ export type TeamEventType = typeof TEAM_EVENT_TYPES[number];
49
+
50
+ export const TEAM_WAKEABLE_EVENT_TYPES: ReadonlySet<TeamEventType> = new Set([
51
+ "run.blocked",
52
+ "run.completed",
53
+ "run.failed",
54
+ "run.cancelled",
55
+ "task.completed",
56
+ "task.failed",
57
+ "async.completed",
58
+ "async.failed",
59
+ "async.stale",
60
+ ]);
61
+
62
+ export function isTeamRunStatus(value: unknown): value is TeamRunStatus {
63
+ return typeof value === "string" && TEAM_RUN_STATUSES.includes(value as TeamRunStatus);
64
+ }
65
+
66
+ export function isTeamTaskStatus(value: unknown): value is TeamTaskStatus {
67
+ return typeof value === "string" && TEAM_TASK_STATUSES.includes(value as TeamTaskStatus);
68
+ }
69
+
70
+ export function isTerminalRunStatus(status: TeamRunStatus): boolean {
71
+ return TEAM_TERMINAL_RUN_STATUSES.has(status);
72
+ }
73
+
74
+ export function isTerminalTaskStatus(status: TeamTaskStatus): boolean {
75
+ return TEAM_TERMINAL_TASK_STATUSES.has(status);
76
+ }
77
+
78
+ export function canTransitionRunStatus(from: TeamRunStatus, to: TeamRunStatus): boolean {
79
+ return from === to || (TEAM_RUN_STATUS_TRANSITIONS[from]?.includes(to) ?? false);
80
+ }
81
+
82
+ export function canTransitionTaskStatus(from: TeamTaskStatus, to: TeamTaskStatus): boolean {
83
+ return from === to || (TEAM_TASK_STATUS_TRANSITIONS[from]?.includes(to) ?? false);
84
+ }
85
+
86
+ export function isWakeableTeamEventType(type: TeamEventType): boolean {
87
+ return TEAM_WAKEABLE_EVENT_TYPES.has(type);
88
+ }
@@ -0,0 +1,27 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export interface TeamEvent {
5
+ time: string;
6
+ type: string;
7
+ runId: string;
8
+ taskId?: string;
9
+ message?: string;
10
+ data?: Record<string, unknown>;
11
+ }
12
+
13
+ export function appendEvent(eventsPath: string, event: Omit<TeamEvent, "time">): TeamEvent {
14
+ fs.mkdirSync(path.dirname(eventsPath), { recursive: true });
15
+ const fullEvent: TeamEvent = { time: new Date().toISOString(), ...event };
16
+ fs.appendFileSync(eventsPath, `${JSON.stringify(fullEvent)}\n`, "utf-8");
17
+ return fullEvent;
18
+ }
19
+
20
+ export function readEvents(eventsPath: string): TeamEvent[] {
21
+ if (!fs.existsSync(eventsPath)) return [];
22
+ return fs.readFileSync(eventsPath, "utf-8")
23
+ .split("\n")
24
+ .map((line) => line.trim())
25
+ .filter(Boolean)
26
+ .map((line) => JSON.parse(line) as TeamEvent);
27
+ }
@@ -0,0 +1,40 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { TeamRunManifest } from "./types.ts";
4
+
5
+ export interface RunLockOptions {
6
+ staleMs?: number;
7
+ }
8
+
9
+ const DEFAULT_STALE_MS = 30_000;
10
+
11
+ function lockPath(manifest: TeamRunManifest): string {
12
+ return path.join(manifest.stateRoot, "run.lock");
13
+ }
14
+
15
+ export function withRunLockSync<T>(manifest: TeamRunManifest, fn: () => T, options: RunLockOptions = {}): T {
16
+ const filePath = lockPath(manifest);
17
+ const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
18
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
19
+ try {
20
+ if (fs.existsSync(filePath)) {
21
+ const stat = fs.statSync(filePath);
22
+ if (Date.now() - stat.mtimeMs <= staleMs) {
23
+ throw new Error(`Run '${manifest.runId}' is locked by another operation.`);
24
+ }
25
+ fs.rmSync(filePath, { force: true });
26
+ }
27
+ fs.writeFileSync(filePath, JSON.stringify({ runId: manifest.runId, pid: process.pid, createdAt: new Date().toISOString() }, null, 2), { flag: "wx" });
28
+ return fn();
29
+ } finally {
30
+ try {
31
+ fs.rmSync(filePath, { force: true });
32
+ } catch {
33
+ // Best-effort lock cleanup.
34
+ }
35
+ }
36
+ }
37
+
38
+ export async function withRunLock<T>(manifest: TeamRunManifest, fn: () => Promise<T>, options: RunLockOptions = {}): Promise<T> {
39
+ return withRunLockSync(manifest, () => fn(), options);
40
+ }