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.
- package/AGENTS.md +32 -0
- package/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/NOTICE.md +15 -0
- package/README.md +703 -0
- package/agents/analyst.md +11 -0
- package/agents/critic.md +11 -0
- package/agents/executor.md +11 -0
- package/agents/explorer.md +11 -0
- package/agents/planner.md +11 -0
- package/agents/reviewer.md +11 -0
- package/agents/security-reviewer.md +11 -0
- package/agents/test-engineer.md +11 -0
- package/agents/verifier.md +11 -0
- package/agents/writer.md +11 -0
- package/docs/architecture.md +92 -0
- package/docs/live-mailbox-runtime.md +36 -0
- package/docs/publishing.md +65 -0
- package/docs/resource-formats.md +131 -0
- package/docs/usage.md +203 -0
- package/index.ts +6 -0
- package/install.mjs +19 -0
- package/package.json +79 -0
- package/schema.json +45 -0
- package/skills/.gitkeep +0 -0
- package/src/agents/agent-config.ts +27 -0
- package/src/agents/agent-serializer.ts +34 -0
- package/src/agents/discover-agents.ts +73 -0
- package/src/config/config.ts +193 -0
- package/src/extension/async-notifier.ts +36 -0
- package/src/extension/autonomous-policy.ts +122 -0
- package/src/extension/help.ts +43 -0
- package/src/extension/import-index.ts +52 -0
- package/src/extension/management.ts +335 -0
- package/src/extension/project-init.ts +74 -0
- package/src/extension/register.ts +349 -0
- package/src/extension/run-bundle-schema.ts +85 -0
- package/src/extension/run-export.ts +59 -0
- package/src/extension/run-import.ts +46 -0
- package/src/extension/run-index.ts +28 -0
- package/src/extension/run-maintenance.ts +24 -0
- package/src/extension/session-summary.ts +8 -0
- package/src/extension/team-manager-command.ts +86 -0
- package/src/extension/team-recommendation.ts +174 -0
- package/src/extension/team-tool.ts +783 -0
- package/src/extension/tool-result.ts +16 -0
- package/src/extension/validate-resources.ts +77 -0
- package/src/prompt/prompt-runtime.ts +58 -0
- package/src/runtime/async-runner.ts +26 -0
- package/src/runtime/background-runner.ts +43 -0
- package/src/runtime/child-pi.ts +75 -0
- package/src/runtime/model-fallback.ts +101 -0
- package/src/runtime/pi-args.ts +81 -0
- package/src/runtime/pi-json-output.ts +110 -0
- package/src/runtime/pi-spawn.ts +96 -0
- package/src/runtime/process-status.ts +25 -0
- package/src/runtime/task-runner.ts +164 -0
- package/src/runtime/team-runner.ts +135 -0
- package/src/runtime/worker-heartbeat.ts +21 -0
- package/src/schema/team-tool-schema.ts +100 -0
- package/src/state/artifact-store.ts +36 -0
- package/src/state/atomic-write.ts +18 -0
- package/src/state/contracts.ts +88 -0
- package/src/state/event-log.ts +27 -0
- package/src/state/locks.ts +40 -0
- package/src/state/mailbox.ts +188 -0
- package/src/state/state-store.ts +119 -0
- package/src/state/task-claims.ts +42 -0
- package/src/state/types.ts +88 -0
- package/src/state/usage.ts +29 -0
- package/src/teams/discover-teams.ts +84 -0
- package/src/teams/team-config.ts +22 -0
- package/src/teams/team-serializer.ts +36 -0
- package/src/ui/run-dashboard.ts +138 -0
- package/src/utils/frontmatter.ts +36 -0
- package/src/utils/ids.ts +12 -0
- package/src/utils/names.ts +26 -0
- package/src/utils/paths.ts +15 -0
- package/src/workflows/discover-workflows.ts +101 -0
- package/src/workflows/validate-workflow.ts +40 -0
- package/src/workflows/workflow-config.ts +24 -0
- package/src/workflows/workflow-serializer.ts +31 -0
- package/src/worktree/cleanup.ts +69 -0
- package/src/worktree/worktree-manager.ts +60 -0
- package/teams/default.team.md +12 -0
- package/teams/fast-fix.team.md +11 -0
- package/teams/implementation.team.md +15 -0
- package/teams/research.team.md +11 -0
- package/teams/review.team.md +12 -0
- package/tsconfig.json +19 -0
- package/workflows/default.workflow.md +29 -0
- package/workflows/fast-fix.workflow.md +22 -0
- package/workflows/implementation.workflow.md +47 -0
- package/workflows/research.workflow.md +22 -0
- package/workflows/review.workflow.md +30 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { TeamRunManifest } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export type MailboxDirection = "inbox" | "outbox";
|
|
6
|
+
export type MailboxMessageStatus = "queued" | "delivered" | "acknowledged";
|
|
7
|
+
|
|
8
|
+
export interface MailboxMessage {
|
|
9
|
+
id: string;
|
|
10
|
+
runId: string;
|
|
11
|
+
direction: MailboxDirection;
|
|
12
|
+
from: string;
|
|
13
|
+
to: string;
|
|
14
|
+
body: string;
|
|
15
|
+
createdAt: string;
|
|
16
|
+
status: MailboxMessageStatus;
|
|
17
|
+
taskId?: string;
|
|
18
|
+
acknowledgedAt?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MailboxDeliveryState {
|
|
22
|
+
messages: Record<string, MailboxMessageStatus>;
|
|
23
|
+
updatedAt: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MailboxValidationIssue {
|
|
27
|
+
level: "error" | "warning";
|
|
28
|
+
path: string;
|
|
29
|
+
message: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface MailboxValidationReport {
|
|
33
|
+
issues: MailboxValidationIssue[];
|
|
34
|
+
repaired: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function mailboxDir(manifest: TeamRunManifest): string {
|
|
38
|
+
return path.join(manifest.stateRoot, "mailbox");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function taskMailboxDir(manifest: TeamRunManifest, taskId: string): string {
|
|
42
|
+
return path.join(mailboxDir(manifest), "tasks", taskId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function mailboxPath(manifest: TeamRunManifest, direction: MailboxDirection, taskId?: string): string {
|
|
46
|
+
return taskId ? path.join(taskMailboxDir(manifest, taskId), `${direction}.jsonl`) : path.join(mailboxDir(manifest), `${direction}.jsonl`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function deliveryPath(manifest: TeamRunManifest): string {
|
|
50
|
+
return path.join(mailboxDir(manifest), "delivery.json");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ensureMailbox(manifest: TeamRunManifest, taskId?: string): void {
|
|
54
|
+
fs.mkdirSync(taskId ? taskMailboxDir(manifest, taskId) : mailboxDir(manifest), { recursive: true });
|
|
55
|
+
for (const direction of ["inbox", "outbox"] as const) {
|
|
56
|
+
const filePath = mailboxPath(manifest, direction, taskId);
|
|
57
|
+
if (!fs.existsSync(filePath)) fs.writeFileSync(filePath, "", "utf-8");
|
|
58
|
+
}
|
|
59
|
+
fs.mkdirSync(mailboxDir(manifest), { recursive: true });
|
|
60
|
+
const delivery = deliveryPath(manifest);
|
|
61
|
+
if (!fs.existsSync(delivery)) fs.writeFileSync(delivery, `${JSON.stringify({ messages: {}, updatedAt: new Date().toISOString() }, null, 2)}\n`, "utf-8");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isDirection(value: unknown): value is MailboxDirection {
|
|
65
|
+
return value === "inbox" || value === "outbox";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isStatus(value: unknown): value is MailboxMessageStatus {
|
|
69
|
+
return value === "queued" || value === "delivered" || value === "acknowledged";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseMailboxMessage(raw: unknown, expectedDirection: MailboxDirection): MailboxMessage | undefined {
|
|
73
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
74
|
+
const obj = raw as Record<string, unknown>;
|
|
75
|
+
if (typeof obj.id !== "string" || typeof obj.runId !== "string" || !isDirection(obj.direction) || typeof obj.from !== "string" || typeof obj.to !== "string" || typeof obj.body !== "string" || typeof obj.createdAt !== "string" || !isStatus(obj.status)) return undefined;
|
|
76
|
+
if (obj.direction !== expectedDirection) return undefined;
|
|
77
|
+
return { id: obj.id, runId: obj.runId, direction: obj.direction, from: obj.from, to: obj.to, body: obj.body, createdAt: obj.createdAt, status: obj.status, taskId: typeof obj.taskId === "string" ? obj.taskId : undefined, acknowledgedAt: typeof obj.acknowledgedAt === "string" ? obj.acknowledgedAt : undefined };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readMailboxFile(filePath: string, direction: MailboxDirection): MailboxMessage[] {
|
|
81
|
+
if (!fs.existsSync(filePath)) return [];
|
|
82
|
+
const messages: MailboxMessage[] = [];
|
|
83
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
84
|
+
for (const line of raw.split(/\r?\n/).filter(Boolean)) {
|
|
85
|
+
try {
|
|
86
|
+
const message = parseMailboxMessage(JSON.parse(line) as unknown, direction);
|
|
87
|
+
if (message) messages.push(message);
|
|
88
|
+
} catch {
|
|
89
|
+
// Invalid mailbox lines are reported by validateMailbox().
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return messages;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function readMailbox(manifest: TeamRunManifest, direction?: MailboxDirection, taskId?: string): MailboxMessage[] {
|
|
96
|
+
ensureMailbox(manifest, taskId);
|
|
97
|
+
const directions = direction ? [direction] : ["inbox", "outbox"] as const;
|
|
98
|
+
const messages = directions.flatMap((item) => readMailboxFile(mailboxPath(manifest, item, taskId), item));
|
|
99
|
+
return messages.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function readDeliveryState(manifest: TeamRunManifest): MailboxDeliveryState {
|
|
103
|
+
ensureMailbox(manifest);
|
|
104
|
+
try {
|
|
105
|
+
const raw = JSON.parse(fs.readFileSync(deliveryPath(manifest), "utf-8")) as unknown;
|
|
106
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Invalid delivery state.");
|
|
107
|
+
const obj = raw as Record<string, unknown>;
|
|
108
|
+
const messages: Record<string, MailboxMessageStatus> = {};
|
|
109
|
+
if (obj.messages && typeof obj.messages === "object" && !Array.isArray(obj.messages)) {
|
|
110
|
+
for (const [id, status] of Object.entries(obj.messages)) if (isStatus(status)) messages[id] = status;
|
|
111
|
+
}
|
|
112
|
+
return { messages, updatedAt: typeof obj.updatedAt === "string" ? obj.updatedAt : new Date().toISOString() };
|
|
113
|
+
} catch {
|
|
114
|
+
return { messages: {}, updatedAt: new Date().toISOString() };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function writeDeliveryState(manifest: TeamRunManifest, state: MailboxDeliveryState): void {
|
|
119
|
+
ensureMailbox(manifest);
|
|
120
|
+
fs.writeFileSync(deliveryPath(manifest), `${JSON.stringify(state, null, 2)}\n`, "utf-8");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function appendMailboxMessage(manifest: TeamRunManifest, message: Omit<MailboxMessage, "id" | "runId" | "createdAt" | "status"> & { id?: string; status?: MailboxMessageStatus }): MailboxMessage {
|
|
124
|
+
ensureMailbox(manifest, message.taskId);
|
|
125
|
+
const createdAt = new Date().toISOString();
|
|
126
|
+
const complete: MailboxMessage = {
|
|
127
|
+
id: message.id ?? `msg_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
128
|
+
runId: manifest.runId,
|
|
129
|
+
direction: message.direction,
|
|
130
|
+
from: message.from,
|
|
131
|
+
to: message.to,
|
|
132
|
+
body: message.body,
|
|
133
|
+
createdAt,
|
|
134
|
+
status: message.status ?? "queued",
|
|
135
|
+
taskId: message.taskId,
|
|
136
|
+
};
|
|
137
|
+
fs.appendFileSync(mailboxPath(manifest, complete.direction, complete.taskId), `${JSON.stringify(complete)}\n`, "utf-8");
|
|
138
|
+
const delivery = readDeliveryState(manifest);
|
|
139
|
+
delivery.messages[complete.id] = complete.status;
|
|
140
|
+
delivery.updatedAt = createdAt;
|
|
141
|
+
writeDeliveryState(manifest, delivery);
|
|
142
|
+
return complete;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function acknowledgeMailboxMessage(manifest: TeamRunManifest, messageId: string): MailboxDeliveryState {
|
|
146
|
+
const delivery = readDeliveryState(manifest);
|
|
147
|
+
if (!delivery.messages[messageId]) throw new Error(`Mailbox message '${messageId}' not found.`);
|
|
148
|
+
delivery.messages[messageId] = "acknowledged";
|
|
149
|
+
delivery.updatedAt = new Date().toISOString();
|
|
150
|
+
writeDeliveryState(manifest, delivery);
|
|
151
|
+
return delivery;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function validateMailbox(manifest: TeamRunManifest, options: { repair?: boolean } = {}): MailboxValidationReport {
|
|
155
|
+
ensureMailbox(manifest);
|
|
156
|
+
const issues: MailboxValidationIssue[] = [];
|
|
157
|
+
const repaired: string[] = [];
|
|
158
|
+
for (const direction of ["inbox", "outbox"] as const) {
|
|
159
|
+
const filePath = mailboxPath(manifest, direction);
|
|
160
|
+
const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/).filter(Boolean);
|
|
161
|
+
const validLines: string[] = [];
|
|
162
|
+
for (const line of lines) {
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(line) as unknown;
|
|
165
|
+
const message = parseMailboxMessage(parsed, direction);
|
|
166
|
+
if (!message) throw new Error("invalid message schema");
|
|
167
|
+
validLines.push(JSON.stringify(message));
|
|
168
|
+
} catch (error) {
|
|
169
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
170
|
+
issues.push({ level: "error", path: filePath, message });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (options.repair && validLines.length !== lines.length) {
|
|
174
|
+
fs.writeFileSync(filePath, `${validLines.join("\n")}${validLines.length ? "\n" : ""}`, "utf-8");
|
|
175
|
+
repaired.push(filePath);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const delivery = readDeliveryState(manifest);
|
|
179
|
+
const allMessages = readMailbox(manifest);
|
|
180
|
+
for (const message of allMessages) if (!delivery.messages[message.id]) issues.push({ level: "warning", path: deliveryPath(manifest), message: `Missing delivery entry for ${message.id}.` });
|
|
181
|
+
if (options.repair) {
|
|
182
|
+
for (const message of allMessages) delivery.messages[message.id] ??= message.status;
|
|
183
|
+
delivery.updatedAt = new Date().toISOString();
|
|
184
|
+
writeDeliveryState(manifest, delivery);
|
|
185
|
+
repaired.push(deliveryPath(manifest));
|
|
186
|
+
}
|
|
187
|
+
return { issues, repaired };
|
|
188
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { TeamRunManifest, TeamTaskState } from "./types.ts";
|
|
4
|
+
import { canTransitionRunStatus } from "./contracts.ts";
|
|
5
|
+
import { atomicWriteJson, readJsonFile } from "./atomic-write.ts";
|
|
6
|
+
import { appendEvent } from "./event-log.ts";
|
|
7
|
+
import { createRunId, createTaskId } from "../utils/ids.ts";
|
|
8
|
+
import { projectPiRoot, userPiRoot } from "../utils/paths.ts";
|
|
9
|
+
import type { TeamConfig } from "../teams/team-config.ts";
|
|
10
|
+
import type { WorkflowConfig } from "../workflows/workflow-config.ts";
|
|
11
|
+
|
|
12
|
+
export interface RunPaths {
|
|
13
|
+
runId: string;
|
|
14
|
+
stateRoot: string;
|
|
15
|
+
artifactsRoot: string;
|
|
16
|
+
manifestPath: string;
|
|
17
|
+
tasksPath: string;
|
|
18
|
+
eventsPath: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function useProjectState(cwd: string): boolean {
|
|
22
|
+
return fs.existsSync(path.join(cwd, ".pi")) || fs.existsSync(path.join(cwd, ".git"));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createRunPaths(cwd: string, runId = createRunId()): RunPaths {
|
|
26
|
+
const baseRoot = useProjectState(cwd)
|
|
27
|
+
? path.join(projectPiRoot(cwd), "teams")
|
|
28
|
+
: path.join(userPiRoot(), "extensions", "pi-crew", "runs");
|
|
29
|
+
const stateRoot = path.join(baseRoot, "state", "runs", runId);
|
|
30
|
+
const artifactsRoot = path.join(baseRoot, "artifacts", runId);
|
|
31
|
+
return {
|
|
32
|
+
runId,
|
|
33
|
+
stateRoot,
|
|
34
|
+
artifactsRoot,
|
|
35
|
+
manifestPath: path.join(stateRoot, "manifest.json"),
|
|
36
|
+
tasksPath: path.join(stateRoot, "tasks.json"),
|
|
37
|
+
eventsPath: path.join(stateRoot, "events.jsonl"),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createTasksFromWorkflow(runId: string, workflow: WorkflowConfig, team: TeamConfig, cwd: string): TeamTaskState[] {
|
|
42
|
+
return workflow.steps.map((step, index) => {
|
|
43
|
+
const role = team.roles.find((candidate) => candidate.name === step.role);
|
|
44
|
+
return {
|
|
45
|
+
id: createTaskId(step.id, index),
|
|
46
|
+
runId,
|
|
47
|
+
stepId: step.id,
|
|
48
|
+
role: step.role,
|
|
49
|
+
agent: role?.agent ?? step.role,
|
|
50
|
+
title: step.id,
|
|
51
|
+
status: "queued",
|
|
52
|
+
dependsOn: step.dependsOn ?? [],
|
|
53
|
+
cwd,
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createRunManifest(params: {
|
|
59
|
+
cwd: string;
|
|
60
|
+
team: TeamConfig;
|
|
61
|
+
workflow?: WorkflowConfig;
|
|
62
|
+
goal: string;
|
|
63
|
+
workspaceMode?: "single" | "worktree";
|
|
64
|
+
}): { manifest: TeamRunManifest; tasks: TeamTaskState[]; paths: RunPaths } {
|
|
65
|
+
const paths = createRunPaths(params.cwd);
|
|
66
|
+
const now = new Date().toISOString();
|
|
67
|
+
const tasks = params.workflow ? createTasksFromWorkflow(paths.runId, params.workflow, params.team, params.cwd) : [];
|
|
68
|
+
const manifest: TeamRunManifest = {
|
|
69
|
+
schemaVersion: 1,
|
|
70
|
+
runId: paths.runId,
|
|
71
|
+
team: params.team.name,
|
|
72
|
+
workflow: params.workflow?.name,
|
|
73
|
+
goal: params.goal,
|
|
74
|
+
status: "queued",
|
|
75
|
+
workspaceMode: params.workspaceMode ?? params.team.workspaceMode ?? "single",
|
|
76
|
+
createdAt: now,
|
|
77
|
+
updatedAt: now,
|
|
78
|
+
cwd: params.cwd,
|
|
79
|
+
stateRoot: paths.stateRoot,
|
|
80
|
+
artifactsRoot: paths.artifactsRoot,
|
|
81
|
+
tasksPath: paths.tasksPath,
|
|
82
|
+
eventsPath: paths.eventsPath,
|
|
83
|
+
artifacts: [],
|
|
84
|
+
};
|
|
85
|
+
fs.mkdirSync(paths.stateRoot, { recursive: true });
|
|
86
|
+
fs.mkdirSync(paths.artifactsRoot, { recursive: true });
|
|
87
|
+
atomicWriteJson(paths.manifestPath, manifest);
|
|
88
|
+
atomicWriteJson(paths.tasksPath, tasks);
|
|
89
|
+
appendEvent(paths.eventsPath, { type: "run.created", runId: paths.runId, data: { team: params.team.name, workflow: params.workflow?.name } });
|
|
90
|
+
return { manifest, tasks, paths };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function saveRunManifest(manifest: TeamRunManifest): void {
|
|
94
|
+
atomicWriteJson(path.join(manifest.stateRoot, "manifest.json"), manifest);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function saveRunTasks(manifest: TeamRunManifest, tasks: TeamTaskState[]): void {
|
|
98
|
+
atomicWriteJson(manifest.tasksPath, tasks);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function updateRunStatus(manifest: TeamRunManifest, status: TeamRunManifest["status"], summary?: string): TeamRunManifest {
|
|
102
|
+
if (!canTransitionRunStatus(manifest.status, status)) {
|
|
103
|
+
throw new Error(`Invalid run status transition: ${manifest.status} -> ${status}`);
|
|
104
|
+
}
|
|
105
|
+
const updated: TeamRunManifest = { ...manifest, status, updatedAt: new Date().toISOString(), summary: summary ?? manifest.summary };
|
|
106
|
+
saveRunManifest(updated);
|
|
107
|
+
appendEvent(updated.eventsPath, { type: `run.${status}`, runId: updated.runId, message: summary });
|
|
108
|
+
return updated;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function loadRunManifestById(cwd: string, runId: string): { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined {
|
|
112
|
+
const projectPath = path.join(projectPiRoot(cwd), "teams", "state", "runs", runId);
|
|
113
|
+
const userPath = path.join(userPiRoot(), "extensions", "pi-crew", "runs", "state", "runs", runId);
|
|
114
|
+
const stateRoot = fs.existsSync(projectPath) ? projectPath : userPath;
|
|
115
|
+
const manifest = readJsonFile<TeamRunManifest>(path.join(stateRoot, "manifest.json"));
|
|
116
|
+
if (!manifest) return undefined;
|
|
117
|
+
const tasks = readJsonFile<TeamTaskState[]>(path.join(stateRoot, "tasks.json")) ?? [];
|
|
118
|
+
return { manifest, tasks };
|
|
119
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { TeamTaskState } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export interface TaskClaimState {
|
|
5
|
+
owner: string;
|
|
6
|
+
token: string;
|
|
7
|
+
leasedUntil: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createTaskClaim(owner: string, leaseMs = 5 * 60_000, now = new Date()): TaskClaimState {
|
|
11
|
+
return { owner, token: randomUUID(), leasedUntil: new Date(now.getTime() + leaseMs).toISOString() };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isTaskClaimExpired(claim: TaskClaimState | undefined, now = new Date()): boolean {
|
|
15
|
+
if (!claim) return false;
|
|
16
|
+
return Date.parse(claim.leasedUntil) <= now.getTime();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function canUseTaskClaim(task: Pick<TeamTaskState, "claim">, owner: string, token: string, now = new Date()): boolean {
|
|
20
|
+
return task.claim?.owner === owner && task.claim.token === token && !isTaskClaimExpired(task.claim, now);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function claimTask<T extends TeamTaskState>(task: T, owner: string, leaseMs?: number, now = new Date()): T {
|
|
24
|
+
if (task.claim && !isTaskClaimExpired(task.claim, now)) {
|
|
25
|
+
throw new Error(`Task '${task.id}' is already claimed by '${task.claim.owner}'.`);
|
|
26
|
+
}
|
|
27
|
+
return { ...task, claim: createTaskClaim(owner, leaseMs, now) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function releaseTaskClaim<T extends TeamTaskState>(task: T, owner: string, token: string, now = new Date()): T {
|
|
31
|
+
if (!canUseTaskClaim(task, owner, token, now)) {
|
|
32
|
+
throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
|
|
33
|
+
}
|
|
34
|
+
return { ...task, claim: undefined };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function transitionClaimedTaskStatus<T extends TeamTaskState>(task: T, owner: string, token: string, status: T["status"], now = new Date()): T {
|
|
38
|
+
if (!canUseTaskClaim(task, owner, token, now)) {
|
|
39
|
+
throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
|
|
40
|
+
}
|
|
41
|
+
return { ...task, status };
|
|
42
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { TeamRunStatus, TeamTaskStatus } from "./contracts.ts";
|
|
2
|
+
import type { TaskClaimState } from "./task-claims.ts";
|
|
3
|
+
import type { WorkerHeartbeatState } from "../runtime/worker-heartbeat.ts";
|
|
4
|
+
export type { TeamRunStatus, TeamTaskStatus } from "./contracts.ts";
|
|
5
|
+
|
|
6
|
+
export interface ArtifactDescriptor {
|
|
7
|
+
kind: "plan" | "prompt" | "result" | "summary" | "log" | "diff" | "patch" | "progress" | "notepad" | "metadata";
|
|
8
|
+
path: string;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
producer: string;
|
|
11
|
+
sizeBytes?: number;
|
|
12
|
+
contentHash?: string;
|
|
13
|
+
retention: "run" | "project" | "temporary";
|
|
14
|
+
expiresAt?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AsyncRunState {
|
|
18
|
+
pid?: number;
|
|
19
|
+
logPath: string;
|
|
20
|
+
spawnedAt: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TeamRunManifest {
|
|
24
|
+
schemaVersion: 1;
|
|
25
|
+
runId: string;
|
|
26
|
+
team: string;
|
|
27
|
+
workflow?: string;
|
|
28
|
+
goal: string;
|
|
29
|
+
status: TeamRunStatus;
|
|
30
|
+
workspaceMode: "single" | "worktree";
|
|
31
|
+
createdAt: string;
|
|
32
|
+
updatedAt: string;
|
|
33
|
+
cwd: string;
|
|
34
|
+
stateRoot: string;
|
|
35
|
+
artifactsRoot: string;
|
|
36
|
+
tasksPath: string;
|
|
37
|
+
eventsPath: string;
|
|
38
|
+
artifacts: ArtifactDescriptor[];
|
|
39
|
+
async?: AsyncRunState;
|
|
40
|
+
summary?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface UsageState {
|
|
44
|
+
input?: number;
|
|
45
|
+
output?: number;
|
|
46
|
+
cacheRead?: number;
|
|
47
|
+
cacheWrite?: number;
|
|
48
|
+
cost?: number;
|
|
49
|
+
turns?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ModelAttemptState {
|
|
53
|
+
model: string;
|
|
54
|
+
success: boolean;
|
|
55
|
+
exitCode?: number | null;
|
|
56
|
+
error?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface TaskWorktreeState {
|
|
60
|
+
path: string;
|
|
61
|
+
branch: string;
|
|
62
|
+
reused: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface TeamTaskState {
|
|
66
|
+
id: string;
|
|
67
|
+
runId: string;
|
|
68
|
+
stepId?: string;
|
|
69
|
+
role: string;
|
|
70
|
+
agent: string;
|
|
71
|
+
title: string;
|
|
72
|
+
status: TeamTaskStatus;
|
|
73
|
+
dependsOn: string[];
|
|
74
|
+
cwd: string;
|
|
75
|
+
worktree?: TaskWorktreeState;
|
|
76
|
+
promptArtifact?: ArtifactDescriptor;
|
|
77
|
+
resultArtifact?: ArtifactDescriptor;
|
|
78
|
+
logArtifact?: ArtifactDescriptor;
|
|
79
|
+
startedAt?: string;
|
|
80
|
+
finishedAt?: string;
|
|
81
|
+
exitCode?: number | null;
|
|
82
|
+
modelAttempts?: ModelAttemptState[];
|
|
83
|
+
usage?: UsageState;
|
|
84
|
+
jsonEvents?: number;
|
|
85
|
+
error?: string;
|
|
86
|
+
claim?: TaskClaimState;
|
|
87
|
+
heartbeat?: WorkerHeartbeatState;
|
|
88
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { TeamTaskState, UsageState } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export function aggregateUsage(tasks: TeamTaskState[]): UsageState | undefined {
|
|
4
|
+
const total: UsageState = {};
|
|
5
|
+
let found = false;
|
|
6
|
+
for (const task of tasks) {
|
|
7
|
+
if (!task.usage) continue;
|
|
8
|
+
found = true;
|
|
9
|
+
total.input = (total.input ?? 0) + (task.usage.input ?? 0);
|
|
10
|
+
total.output = (total.output ?? 0) + (task.usage.output ?? 0);
|
|
11
|
+
total.cacheRead = (total.cacheRead ?? 0) + (task.usage.cacheRead ?? 0);
|
|
12
|
+
total.cacheWrite = (total.cacheWrite ?? 0) + (task.usage.cacheWrite ?? 0);
|
|
13
|
+
total.cost = (total.cost ?? 0) + (task.usage.cost ?? 0);
|
|
14
|
+
total.turns = (total.turns ?? 0) + (task.usage.turns ?? 0);
|
|
15
|
+
}
|
|
16
|
+
return found ? total : undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatUsage(usage: UsageState | undefined): string {
|
|
20
|
+
if (!usage) return "(none)";
|
|
21
|
+
const parts: string[] = [];
|
|
22
|
+
if (usage.input !== undefined) parts.push(`input=${usage.input}`);
|
|
23
|
+
if (usage.output !== undefined) parts.push(`output=${usage.output}`);
|
|
24
|
+
if (usage.cacheRead !== undefined) parts.push(`cacheRead=${usage.cacheRead}`);
|
|
25
|
+
if (usage.cacheWrite !== undefined) parts.push(`cacheWrite=${usage.cacheWrite}`);
|
|
26
|
+
if (usage.cost !== undefined) parts.push(`cost=${usage.cost.toFixed(6)}`);
|
|
27
|
+
if (usage.turns !== undefined) parts.push(`turns=${usage.turns}`);
|
|
28
|
+
return parts.join(", ") || "(none)";
|
|
29
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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 type { TeamConfig, TeamRole } from "./team-config.ts";
|
|
5
|
+
import { parseCsv, parseFrontmatter } from "../utils/frontmatter.ts";
|
|
6
|
+
import { packageRoot, projectPiRoot, userPiRoot } from "../utils/paths.ts";
|
|
7
|
+
|
|
8
|
+
export interface TeamDiscoveryResult {
|
|
9
|
+
builtin: TeamConfig[];
|
|
10
|
+
user: TeamConfig[];
|
|
11
|
+
project: TeamConfig[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseRoleLine(line: string): TeamRole | undefined {
|
|
15
|
+
const trimmed = line.trim();
|
|
16
|
+
if (!trimmed.startsWith("-")) return undefined;
|
|
17
|
+
const value = trimmed.slice(1).trim();
|
|
18
|
+
if (!value) return undefined;
|
|
19
|
+
const [namePart, restPart] = value.split(":", 2);
|
|
20
|
+
const name = namePart?.trim();
|
|
21
|
+
if (!name) return undefined;
|
|
22
|
+
const agentMatch = restPart?.match(/agent\s*=\s*([\w-]+)/);
|
|
23
|
+
return {
|
|
24
|
+
name,
|
|
25
|
+
agent: agentMatch?.[1] ?? name,
|
|
26
|
+
description: restPart?.replace(/agent\s*=\s*[\w-]+/, "").trim() || undefined,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseCost(value: string | undefined): "free" | "cheap" | "expensive" | undefined {
|
|
31
|
+
return value === "free" || value === "cheap" || value === "expensive" ? value : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseTeamFile(filePath: string, source: ResourceSource): TeamConfig | undefined {
|
|
35
|
+
try {
|
|
36
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
37
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
38
|
+
const name = frontmatter.name?.trim() || path.basename(filePath, ".team.md");
|
|
39
|
+
const roles = body.split("\n").map(parseRoleLine).filter((role): role is TeamRole => role !== undefined);
|
|
40
|
+
const triggers = parseCsv(frontmatter.triggers ?? frontmatter.trigger);
|
|
41
|
+
const useWhen = parseCsv(frontmatter.useWhen);
|
|
42
|
+
const avoidWhen = parseCsv(frontmatter.avoidWhen);
|
|
43
|
+
const cost = parseCost(frontmatter.cost);
|
|
44
|
+
const category = frontmatter.category?.trim() || undefined;
|
|
45
|
+
return {
|
|
46
|
+
name,
|
|
47
|
+
description: frontmatter.description?.trim() || "No description provided.",
|
|
48
|
+
source,
|
|
49
|
+
filePath,
|
|
50
|
+
roles,
|
|
51
|
+
defaultWorkflow: frontmatter.defaultWorkflow || frontmatter.workflow || undefined,
|
|
52
|
+
workspaceMode: frontmatter.workspaceMode === "worktree" ? "worktree" : "single",
|
|
53
|
+
maxConcurrency: frontmatter.maxConcurrency ? Number.parseInt(frontmatter.maxConcurrency, 10) : undefined,
|
|
54
|
+
routing: triggers || useWhen || avoidWhen || cost || category ? { triggers, useWhen, avoidWhen, cost, category } : undefined,
|
|
55
|
+
};
|
|
56
|
+
} catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readTeamDir(dir: string, source: ResourceSource): TeamConfig[] {
|
|
62
|
+
if (!fs.existsSync(dir)) return [];
|
|
63
|
+
return fs.readdirSync(dir)
|
|
64
|
+
.filter((entry) => entry.endsWith(".team.md"))
|
|
65
|
+
.map((entry) => parseTeamFile(path.join(dir, entry), source))
|
|
66
|
+
.filter((team): team is TeamConfig => team !== undefined)
|
|
67
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function discoverTeams(cwd: string): TeamDiscoveryResult {
|
|
71
|
+
return {
|
|
72
|
+
builtin: readTeamDir(path.join(packageRoot(), "teams"), "builtin"),
|
|
73
|
+
user: readTeamDir(path.join(userPiRoot(), "teams"), "user"),
|
|
74
|
+
project: readTeamDir(path.join(projectPiRoot(cwd), "teams"), "project"),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function allTeams(discovery: TeamDiscoveryResult): TeamConfig[] {
|
|
79
|
+
const byName = new Map<string, TeamConfig>();
|
|
80
|
+
for (const team of [...discovery.builtin, ...discovery.user, ...discovery.project]) {
|
|
81
|
+
byName.set(team.name, team);
|
|
82
|
+
}
|
|
83
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
84
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ResourceSource, RoutingMetadata } from "../agents/agent-config.ts";
|
|
2
|
+
|
|
3
|
+
export interface TeamRole {
|
|
4
|
+
name: string;
|
|
5
|
+
agent: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
skills?: string[] | false;
|
|
9
|
+
maxConcurrency?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TeamConfig {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
source: ResourceSource;
|
|
16
|
+
filePath: string;
|
|
17
|
+
roles: TeamRole[];
|
|
18
|
+
defaultWorkflow?: string;
|
|
19
|
+
workspaceMode?: "single" | "worktree";
|
|
20
|
+
maxConcurrency?: number;
|
|
21
|
+
routing?: RoutingMetadata;
|
|
22
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { TeamConfig, TeamRole } from "./team-config.ts";
|
|
2
|
+
|
|
3
|
+
function line(key: string, value: string | string[] | undefined): string | undefined {
|
|
4
|
+
if (value === undefined) return undefined;
|
|
5
|
+
if (Array.isArray(value)) return `${key}: ${value.join(", ")}`;
|
|
6
|
+
return `${key}: ${value}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function serializeRole(role: TeamRole): string {
|
|
10
|
+
const parts = [`agent=${role.agent}`];
|
|
11
|
+
if (role.model) parts.push(`model=${role.model}`);
|
|
12
|
+
if (role.maxConcurrency !== undefined) parts.push(`maxConcurrency=${role.maxConcurrency}`);
|
|
13
|
+
if (role.description) parts.push(role.description);
|
|
14
|
+
return `- ${role.name}: ${parts.join(" ")}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function serializeTeam(team: TeamConfig): string {
|
|
18
|
+
const lines = [
|
|
19
|
+
"---",
|
|
20
|
+
`name: ${team.name}`,
|
|
21
|
+
`description: ${team.description}`,
|
|
22
|
+
team.defaultWorkflow ? `defaultWorkflow: ${team.defaultWorkflow}` : undefined,
|
|
23
|
+
team.workspaceMode ? `workspaceMode: ${team.workspaceMode}` : undefined,
|
|
24
|
+
team.maxConcurrency !== undefined ? `maxConcurrency: ${team.maxConcurrency}` : undefined,
|
|
25
|
+
line("triggers", team.routing?.triggers),
|
|
26
|
+
line("useWhen", team.routing?.useWhen),
|
|
27
|
+
line("avoidWhen", team.routing?.avoidWhen),
|
|
28
|
+
line("cost", team.routing?.cost),
|
|
29
|
+
line("category", team.routing?.category),
|
|
30
|
+
"---",
|
|
31
|
+
"",
|
|
32
|
+
...team.roles.map(serializeRole),
|
|
33
|
+
"",
|
|
34
|
+
].filter((entry): entry is string => entry !== undefined);
|
|
35
|
+
return lines.join("\n");
|
|
36
|
+
}
|