pi-crew 0.1.3 → 0.1.5

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/README.md CHANGED
@@ -37,6 +37,7 @@ Current highlights:
37
37
  - retryable model fallback attempts per task
38
38
  - aggregate usage totals in status/summary
39
39
  - progress, summary, prompt, result, log, diff, patch, export artifacts
40
+ - task packets, verification/green-contract evidence, policy decision artifacts, and task graph metadata
40
41
  - opt-in git worktree isolation per task
41
42
  - worktree branch mismatch detection
42
43
  - dirty worktree preservation unless `force` is explicitly set
@@ -154,6 +155,14 @@ Supported config:
154
155
  "magicKeywords": {
155
156
  "review": ["review", "audit", "inspect"]
156
157
  }
158
+ },
159
+ "limits": {
160
+ "maxConcurrentWorkers": 3,
161
+ "maxTaskDepth": 2,
162
+ "maxChildrenPerTask": 5,
163
+ "maxRunMinutes": 60,
164
+ "maxRetriesPerTask": 1,
165
+ "heartbeatStaleMs": 60000
157
166
  }
158
167
  }
159
168
  ```
package/install.mjs CHANGED
@@ -7,7 +7,7 @@ const configDir = path.join(os.homedir(), ".pi", "agent", "extensions", "pi-crew
7
7
  const configPath = path.join(configDir, "config.json");
8
8
  fs.mkdirSync(configDir, { recursive: true });
9
9
  if (!fs.existsSync(configPath)) {
10
- fs.writeFileSync(configPath, `${JSON.stringify({ asyncByDefault: false, executeWorkers: false, notifierIntervalMs: 5000, requireCleanWorktreeLeader: true, autonomous: { enabled: true, injectPolicy: true, preferAsyncForLongTasks: false, allowWorktreeSuggestion: true } }, null, 2)}\n`, "utf-8");
10
+ fs.writeFileSync(configPath, `${JSON.stringify({ asyncByDefault: false, executeWorkers: false, notifierIntervalMs: 5000, requireCleanWorktreeLeader: true, autonomous: { enabled: true, injectPolicy: true, preferAsyncForLongTasks: false, allowWorktreeSuggestion: true }, limits: { maxConcurrentWorkers: 3, maxTaskDepth: 2, maxChildrenPerTask: 5, maxRunMinutes: 60, maxRetriesPerTask: 1, heartbeatStaleMs: 60000 } }, null, 2)}\n`, "utf-8");
11
11
  console.log(`Created default pi-crew config: ${configPath}`);
12
12
  } else {
13
13
  console.log(`pi-crew config already exists: ${configPath}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -89,5 +89,6 @@
89
89
  "@mariozechner/pi-tui": {
90
90
  "optional": true
91
91
  }
92
- }
92
+ },
93
+ "readmeFilename": "README.md"
93
94
  }
package/schema.json CHANGED
@@ -40,6 +40,20 @@
40
40
  }
41
41
  }
42
42
  }
43
+ },
44
+ "limits": {
45
+ "type": "object",
46
+ "additionalProperties": false,
47
+ "description": "Runtime safety limits for crew workers and policy decisions.",
48
+ "properties": {
49
+ "maxConcurrentWorkers": { "type": "integer", "minimum": 1 },
50
+ "maxTaskDepth": { "type": "integer", "minimum": 1 },
51
+ "maxChildrenPerTask": { "type": "integer", "minimum": 1 },
52
+ "maxRunMinutes": { "type": "integer", "minimum": 1 },
53
+ "maxRetriesPerTask": { "type": "integer", "minimum": 1 },
54
+ "maxTasksPerRun": { "type": "integer", "minimum": 1 },
55
+ "heartbeatStaleMs": { "type": "integer", "minimum": 1 }
56
+ }
43
57
  }
44
58
  }
45
59
  }
@@ -13,12 +13,23 @@ export interface PiTeamsAutonomousConfig {
13
13
  magicKeywords?: Record<string, string[]>;
14
14
  }
15
15
 
16
+ export interface CrewLimitsConfig {
17
+ maxConcurrentWorkers?: number;
18
+ maxTaskDepth?: number;
19
+ maxChildrenPerTask?: number;
20
+ maxRunMinutes?: number;
21
+ maxRetriesPerTask?: number;
22
+ maxTasksPerRun?: number;
23
+ heartbeatStaleMs?: number;
24
+ }
25
+
16
26
  export interface PiTeamsConfig {
17
27
  asyncByDefault?: boolean;
18
28
  executeWorkers?: boolean;
19
29
  notifierIntervalMs?: number;
20
30
  requireCleanWorktreeLeader?: boolean;
21
31
  autonomous?: PiTeamsAutonomousConfig;
32
+ limits?: CrewLimitsConfig;
22
33
  }
23
34
 
24
35
  export interface LoadedPiTeamsConfig {
@@ -60,6 +71,12 @@ function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTeamsConfi
60
71
  ...withoutUndefined((override.autonomous ?? {}) as Record<string, unknown>),
61
72
  };
62
73
  }
74
+ if (base.limits || override.limits) {
75
+ merged.limits = {
76
+ ...(base.limits ?? {}),
77
+ ...withoutUndefined((override.limits ?? {}) as Record<string, unknown>),
78
+ };
79
+ }
63
80
  return merged;
64
81
  }
65
82
 
@@ -110,6 +127,25 @@ function parseAutonomousConfig(value: unknown): PiTeamsAutonomousConfig | undefi
110
127
  };
111
128
  }
112
129
 
130
+ function parsePositiveInteger(value: unknown): number | undefined {
131
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
132
+ }
133
+
134
+ function parseLimitsConfig(value: unknown): CrewLimitsConfig | undefined {
135
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
136
+ const obj = value as Record<string, unknown>;
137
+ const limits: CrewLimitsConfig = {
138
+ maxConcurrentWorkers: parsePositiveInteger(obj.maxConcurrentWorkers),
139
+ maxTaskDepth: parsePositiveInteger(obj.maxTaskDepth),
140
+ maxChildrenPerTask: parsePositiveInteger(obj.maxChildrenPerTask),
141
+ maxRunMinutes: parsePositiveInteger(obj.maxRunMinutes),
142
+ maxRetriesPerTask: parsePositiveInteger(obj.maxRetriesPerTask),
143
+ maxTasksPerRun: parsePositiveInteger(obj.maxTasksPerRun),
144
+ heartbeatStaleMs: parsePositiveInteger(obj.heartbeatStaleMs),
145
+ };
146
+ return Object.values(limits).some((entry) => entry !== undefined) ? limits : undefined;
147
+ }
148
+
113
149
  function parseConfig(raw: unknown): PiTeamsConfig {
114
150
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
115
151
  const obj = raw as Record<string, unknown>;
@@ -119,6 +155,7 @@ function parseConfig(raw: unknown): PiTeamsConfig {
119
155
  notifierIntervalMs: typeof obj.notifierIntervalMs === "number" && Number.isFinite(obj.notifierIntervalMs) && obj.notifierIntervalMs >= 1000 ? obj.notifierIntervalMs : undefined,
120
156
  requireCleanWorktreeLeader: typeof obj.requireCleanWorktreeLeader === "boolean" ? obj.requireCleanWorktreeLeader : undefined,
121
157
  autonomous: parseAutonomousConfig(obj.autonomous),
158
+ limits: parseLimitsConfig(obj.limits),
122
159
  };
123
160
  }
124
161
 
@@ -256,7 +256,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
256
256
  }
257
257
 
258
258
  const executeWorkers = loadedConfig.config.executeWorkers === true || process.env.PI_TEAMS_EXECUTE_WORKERS === "1";
259
- const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers });
259
+ const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: loadedConfig.config.limits });
260
260
  const text = [
261
261
  `Created pi-crew run ${executed.manifest.runId}.`,
262
262
  `Team: ${team.name}`,
@@ -306,8 +306,10 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
306
306
  `Artifacts: ${manifest.artifactsRoot}`,
307
307
  ...(asyncLivenessLine ? [asyncLivenessLine] : []),
308
308
  "Tasks:",
309
- ...(tasks.length ? tasks.map((task) => `- ${task.id} [${task.status}] ${task.role} -> ${task.agent}${task.modelAttempts?.length ? ` attempts=${task.modelAttempts.length}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${task.error}` : ""}`) : ["- (none)"]),
309
+ ...(tasks.length ? tasks.map((task) => `- ${task.id} [${task.status}] ${task.role} -> ${task.agent}${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.modelAttempts?.length ? ` attempts=${task.modelAttempts.length}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${task.error}` : ""}`) : ["- (none)"]),
310
310
  `Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
311
+ "Policy decisions:",
312
+ ...(manifest.policyDecisions?.length ? manifest.policyDecisions.map((item) => `- ${item.action} (${item.reason})${item.taskId ? ` ${item.taskId}` : ""}: ${item.message}`) : ["- (none)"]),
311
313
  `Total usage: ${formatUsage(totalUsage)}`,
312
314
  "",
313
315
  "Recent artifacts:",
@@ -369,7 +371,7 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
369
371
  appendEvent(loaded.manifest.eventsPath, { type: "run.resume_requested", runId: loaded.manifest.runId });
370
372
  const loadedConfig = loadConfig(ctx.cwd);
371
373
  const executeWorkers = loadedConfig.config.executeWorkers === true || process.env.PI_TEAMS_EXECUTE_WORKERS === "1";
372
- const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers });
374
+ const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers, limits: loadedConfig.config.limits });
373
375
  return result([`Resumed run ${executed.manifest.runId}.`, `Status: ${executed.manifest.status}`, `Tasks: ${executed.tasks.length}`, `Artifacts: ${executed.manifest.artifactsRoot}`].join("\n"), { action: "resume", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
374
376
  });
375
377
  }
@@ -3,6 +3,7 @@ import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
3
3
  import { appendEvent } from "../state/event-log.ts";
4
4
  import { loadRunManifestById, updateRunStatus } from "../state/state-store.ts";
5
5
  import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
6
+ import { loadConfig } from "../config/config.ts";
6
7
  import { executeTeamRun } from "./team-runner.ts";
7
8
 
8
9
  function argValue(name: string): string | undefined {
@@ -27,8 +28,9 @@ async function main(): Promise<void> {
27
28
  const workflow = allWorkflows(discoverWorkflows(cwd)).find((candidate) => candidate.name === manifest.workflow);
28
29
  if (!workflow) throw new Error(`Workflow '${manifest.workflow ?? ""}' not found.`);
29
30
  const agents = allAgents(discoverAgents(cwd));
30
- const executeWorkers = process.env.PI_TEAMS_EXECUTE_WORKERS === "1";
31
- const result = await executeTeamRun({ manifest, tasks, team, workflow, agents, executeWorkers });
31
+ const loadedConfig = loadConfig(cwd);
32
+ const executeWorkers = loadedConfig.config.executeWorkers === true || process.env.PI_TEAMS_EXECUTE_WORKERS === "1";
33
+ const result = await executeTeamRun({ manifest, tasks, team, workflow, agents, executeWorkers, limits: loadedConfig.config.limits });
32
34
  manifest = result.manifest;
33
35
  tasks = result.tasks;
34
36
  appendEvent(manifest.eventsPath, { type: "async.completed", runId: manifest.runId, data: { status: manifest.status, tasks: tasks.length } });
@@ -0,0 +1,46 @@
1
+ import type { GreenLevel, VerificationContract, VerificationEvidence } from "../state/types.ts";
2
+
3
+ const GREEN_ORDER: Record<GreenLevel, number> = {
4
+ none: 0,
5
+ targeted: 1,
6
+ package: 2,
7
+ workspace: 3,
8
+ merge_ready: 4,
9
+ };
10
+
11
+ export interface GreenContractOutcome {
12
+ requiredGreenLevel: GreenLevel;
13
+ observedGreenLevel: GreenLevel;
14
+ satisfied: boolean;
15
+ }
16
+
17
+ export function greenLevelSatisfies(observed: GreenLevel, required: GreenLevel): boolean {
18
+ return GREEN_ORDER[observed] >= GREEN_ORDER[required];
19
+ }
20
+
21
+ export function evaluateGreenContract(contract: VerificationContract, evidence?: VerificationEvidence): GreenContractOutcome {
22
+ const observedGreenLevel = evidence?.observedGreenLevel ?? "none";
23
+ return {
24
+ requiredGreenLevel: contract.requiredGreenLevel,
25
+ observedGreenLevel,
26
+ satisfied: greenLevelSatisfies(observedGreenLevel, contract.requiredGreenLevel),
27
+ };
28
+ }
29
+
30
+ export function inferGreenLevelFromTask(success: boolean, contract: VerificationContract): GreenLevel {
31
+ if (!success) return "none";
32
+ if (contract.requiredGreenLevel === "none") return "none";
33
+ return contract.allowManualEvidence ? contract.requiredGreenLevel : "targeted";
34
+ }
35
+
36
+ export function createVerificationEvidence(contract: VerificationContract, success: boolean, notes: string): VerificationEvidence {
37
+ const observedGreenLevel = inferGreenLevelFromTask(success, contract);
38
+ const outcome = evaluateGreenContract(contract, { requiredGreenLevel: contract.requiredGreenLevel, observedGreenLevel, satisfied: false, commands: [], notes });
39
+ return {
40
+ requiredGreenLevel: contract.requiredGreenLevel,
41
+ observedGreenLevel,
42
+ satisfied: outcome.satisfied,
43
+ commands: contract.commands.map((cmd) => ({ cmd, status: "not_run" as const })),
44
+ notes,
45
+ };
46
+ }
@@ -0,0 +1,55 @@
1
+ import type { CrewLimitsConfig } from "../config/config.ts";
2
+ import type { PolicyDecision, PolicyDecisionAction, PolicyDecisionReason, TeamRunManifest, TeamTaskState } from "../state/types.ts";
3
+ import { evaluateGreenContract } from "./green-contract.ts";
4
+ import { isWorkerHeartbeatStale } from "./worker-heartbeat.ts";
5
+
6
+ export interface PolicyEngineInput {
7
+ manifest: TeamRunManifest;
8
+ tasks: TeamTaskState[];
9
+ limits?: CrewLimitsConfig;
10
+ now?: Date;
11
+ }
12
+
13
+ function decision(action: PolicyDecisionAction, reason: PolicyDecisionReason, message: string, taskId?: string): PolicyDecision {
14
+ return {
15
+ action,
16
+ reason,
17
+ message,
18
+ taskId,
19
+ createdAt: new Date().toISOString(),
20
+ };
21
+ }
22
+
23
+ export function evaluateCrewPolicy(input: PolicyEngineInput): PolicyDecision[] {
24
+ const decisions: PolicyDecision[] = [];
25
+ const maxTasksPerRun = input.limits?.maxTasksPerRun;
26
+ if (maxTasksPerRun !== undefined && input.tasks.length > maxTasksPerRun) {
27
+ decisions.push(decision("block", "limit_exceeded", `Run has ${input.tasks.length} tasks, exceeding maxTasksPerRun=${maxTasksPerRun}.`));
28
+ }
29
+
30
+ for (const task of input.tasks) {
31
+ if (task.status === "failed") {
32
+ const retryCount = task.policy?.retryCount ?? 0;
33
+ const maxRetries = input.limits?.maxRetriesPerTask ?? 0;
34
+ decisions.push(decision(retryCount < maxRetries ? "retry" : "escalate", "task_failed", task.error ? `Task failed: ${task.error}` : "Task failed.", task.id));
35
+ }
36
+ if (task.heartbeat && isWorkerHeartbeatStale(task.heartbeat, input.limits?.heartbeatStaleMs ?? 60_000, input.now)) {
37
+ decisions.push(decision("escalate", "worker_stale", "Worker heartbeat is stale.", task.id));
38
+ }
39
+ if (task.taskPacket?.verification) {
40
+ const outcome = evaluateGreenContract(task.taskPacket.verification, task.verification);
41
+ if (!outcome.satisfied && task.status === "completed") {
42
+ decisions.push(decision("block", "green_unsatisfied", `Green contract unsatisfied: required=${outcome.requiredGreenLevel}, observed=${outcome.observedGreenLevel}.`, task.id));
43
+ }
44
+ }
45
+ }
46
+
47
+ if (decisions.length === 0 && input.tasks.length > 0 && input.tasks.every((task) => task.status === "completed")) {
48
+ decisions.push(decision("closeout", "run_complete", "All tasks completed and no policy blockers were found."));
49
+ }
50
+ return decisions;
51
+ }
52
+
53
+ export function summarizePolicyDecisions(decisions: PolicyDecision[]): string[] {
54
+ return decisions.map((item) => `- ${item.action} (${item.reason})${item.taskId ? ` ${item.taskId}` : ""}: ${item.message}`);
55
+ }
@@ -0,0 +1,84 @@
1
+ import * as path from "node:path";
2
+ import type { TeamRunManifest, TaskPacket, TaskScope, VerificationContract } from "../state/types.ts";
3
+ import type { WorkflowStep } from "../workflows/workflow-config.ts";
4
+
5
+ export interface BuildTaskPacketInput {
6
+ manifest: TeamRunManifest;
7
+ step: WorkflowStep;
8
+ taskId: string;
9
+ cwd: string;
10
+ worktreePath?: string;
11
+ }
12
+
13
+ export interface TaskPacketValidationResult {
14
+ valid: boolean;
15
+ errors: string[];
16
+ }
17
+
18
+ export function inferTaskScope(step: WorkflowStep): TaskScope {
19
+ const reads = step.reads === false ? [] : step.reads ?? [];
20
+ if (reads.length === 1) return "single_file";
21
+ if (reads.length > 1) return "module";
22
+ return "workspace";
23
+ }
24
+
25
+ export function defaultVerificationContract(step: WorkflowStep): VerificationContract {
26
+ return {
27
+ requiredGreenLevel: step.verify ? "targeted" : "none",
28
+ commands: [],
29
+ allowManualEvidence: true,
30
+ };
31
+ }
32
+
33
+ export function buildTaskPacket(input: BuildTaskPacketInput): TaskPacket {
34
+ const scope = inferTaskScope(input.step);
35
+ const reads = input.step.reads === false ? [] : input.step.reads ?? [];
36
+ const scopePath = reads.length === 1 ? reads[0] : reads.length > 1 ? reads.join(", ") : undefined;
37
+ return {
38
+ objective: input.step.task.replaceAll("{goal}", input.manifest.goal),
39
+ scope,
40
+ scopePath,
41
+ repo: path.basename(input.manifest.cwd) || input.manifest.cwd,
42
+ worktree: input.worktreePath,
43
+ branchPolicy: input.manifest.workspaceMode === "worktree" ? "Use the assigned task worktree and avoid modifying the leader checkout." : "Use the current checkout; do not create branches unless explicitly requested.",
44
+ acceptanceTests: [],
45
+ commitPolicy: "Do not commit unless explicitly requested by the user or workflow.",
46
+ reportingContract: "Report changed files, verification evidence, blockers, and next recommended action.",
47
+ escalationPolicy: "Stop and report if scope is ambiguous, destructive action is needed, permissions are missing, or verification cannot be completed.",
48
+ constraints: [
49
+ "Stay within the assigned task scope.",
50
+ "Do not claim completion without verification evidence.",
51
+ "Use mailbox/API state for coordination when available.",
52
+ ],
53
+ expectedArtifacts: ["prompt", "result", "verification"],
54
+ verification: defaultVerificationContract(input.step),
55
+ };
56
+ }
57
+
58
+ export function validateTaskPacket(packet: TaskPacket): TaskPacketValidationResult {
59
+ const errors: string[] = [];
60
+ if (!packet.objective.trim()) errors.push("objective must not be empty");
61
+ if (!packet.repo.trim()) errors.push("repo must not be empty");
62
+ if (!packet.branchPolicy.trim()) errors.push("branchPolicy must not be empty");
63
+ if (!packet.commitPolicy.trim()) errors.push("commitPolicy must not be empty");
64
+ if (!packet.reportingContract.trim()) errors.push("reportingContract must not be empty");
65
+ if (!packet.escalationPolicy.trim()) errors.push("escalationPolicy must not be empty");
66
+ if ((packet.scope === "module" || packet.scope === "single_file" || packet.scope === "custom") && !packet.scopePath?.trim()) {
67
+ errors.push(`scopePath is required for scope '${packet.scope}'`);
68
+ }
69
+ for (const [index, test] of packet.acceptanceTests.entries()) {
70
+ if (!test.trim()) errors.push(`acceptanceTests contains an empty value at index ${index}`);
71
+ }
72
+ return { valid: errors.length === 0, errors };
73
+ }
74
+
75
+ export function renderTaskPacket(packet: TaskPacket): string {
76
+ return [
77
+ "# Task Packet",
78
+ "",
79
+ "```json",
80
+ JSON.stringify(packet, null, 2),
81
+ "```",
82
+ "",
83
+ ].join("\n");
84
+ }
@@ -10,6 +10,8 @@ import { captureWorktreeDiff, prepareTaskWorkspace } from "../worktree/worktree-
10
10
  import { buildModelCandidates, formatModelAttemptNote, isRetryableModelFailure, type ModelAttemptSummary } from "./model-fallback.ts";
11
11
  import { parsePiJsonOutput, type ParsedPiJsonOutput } from "./pi-json-output.ts";
12
12
  import { runChildPi } from "./child-pi.ts";
13
+ import { buildTaskPacket, renderTaskPacket } from "./task-packet.ts";
14
+ import { createVerificationEvidence } from "./green-contract.ts";
13
15
 
14
16
  export interface TaskRunnerInput {
15
17
  manifest: TeamRunManifest;
@@ -43,7 +45,9 @@ function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: T
43
45
  "- Stay within the task scope unless the prompt explicitly says otherwise.",
44
46
  "- Report blockers and verification evidence in the final result.",
45
47
  "- Do not claim completion without evidence.",
48
+ "- Follow the Task Packet contract below; escalate if any contract field is impossible to satisfy.",
46
49
  "",
50
+ task.taskPacket ? renderTaskPacket(task.taskPacket) : "",
47
51
  "Task:",
48
52
  step.task.replaceAll("{goal}", manifest.goal),
49
53
  ].join("\n");
@@ -56,10 +60,13 @@ function updateTask(tasks: TeamTaskState[], updated: TeamTaskState): TeamTaskSta
56
60
  export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
57
61
  let manifest = input.manifest;
58
62
  const workspace = prepareTaskWorkspace(manifest, input.task);
63
+ const worktree = workspace.worktreePath && workspace.branch ? { path: workspace.worktreePath, branch: workspace.branch, reused: workspace.reused ?? false } : input.task.worktree;
64
+ const taskPacket = buildTaskPacket({ manifest, step: input.step, taskId: input.task.id, cwd: workspace.cwd, worktreePath: worktree?.path });
59
65
  let task: TeamTaskState = {
60
66
  ...input.task,
61
67
  cwd: workspace.cwd,
62
- worktree: workspace.worktreePath && workspace.branch ? { path: workspace.worktreePath, branch: workspace.branch, reused: workspace.reused ?? false } : input.task.worktree,
68
+ worktree,
69
+ taskPacket,
63
70
  status: "running",
64
71
  startedAt: new Date().toISOString(),
65
72
  claim: createTaskClaim(`task-runner:${input.task.id}`),
@@ -149,6 +156,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
149
156
  usage: parsedOutput?.usage,
150
157
  jsonEvents: parsedOutput?.jsonEvents,
151
158
  error,
159
+ verification: createVerificationEvidence(taskPacket.verification, !error, error ? `Task failed: ${error}` : input.executeWorkers ? "Worker finished without reporting a verification failure." : "Safe scaffold mode; verification commands were not executed."),
152
160
  promptArtifact,
153
161
  resultArtifact,
154
162
  claim: undefined,
@@ -156,7 +164,19 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
156
164
  ...(logArtifact ? { logArtifact } : {}),
157
165
  };
158
166
  tasks = updateTask(tasks, task);
159
- manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, promptArtifact, resultArtifact, ...(logArtifact ? [logArtifact] : []), ...(diffArtifact ? [diffArtifact] : [])] };
167
+ const packetArtifact = writeArtifact(manifest.artifactsRoot, {
168
+ kind: "metadata",
169
+ relativePath: `metadata/${task.id}.task-packet.json`,
170
+ content: `${JSON.stringify(task.taskPacket, null, 2)}\n`,
171
+ producer: task.id,
172
+ });
173
+ const verificationArtifact = writeArtifact(manifest.artifactsRoot, {
174
+ kind: "metadata",
175
+ relativePath: `metadata/${task.id}.verification.json`,
176
+ content: `${JSON.stringify(task.verification, null, 2)}\n`,
177
+ producer: task.id,
178
+ });
179
+ manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, promptArtifact, resultArtifact, packetArtifact, verificationArtifact, ...(logArtifact ? [logArtifact] : []), ...(diffArtifact ? [diffArtifact] : [])] };
160
180
  saveRunManifest(manifest);
161
181
  saveRunTasks(manifest, tasks);
162
182
  appendEvent(manifest.eventsPath, { type: error ? "task.failed" : "task.completed", runId: manifest.runId, taskId: task.id, message: error });
@@ -1,135 +1,162 @@
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
- }
1
+ import type { AgentConfig } from "../agents/agent-config.ts";
2
+ import type { CrewLimitsConfig } from "../config/config.ts";
3
+ import { writeArtifact } from "../state/artifact-store.ts";
4
+ import { appendEvent } from "../state/event-log.ts";
5
+ import type { TeamConfig } from "../teams/team-config.ts";
6
+ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
7
+ import { saveRunManifest, saveRunTasks, updateRunStatus } from "../state/state-store.ts";
8
+ import { aggregateUsage, formatUsage } from "../state/usage.ts";
9
+ import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
10
+ import { evaluateCrewPolicy, summarizePolicyDecisions } from "./policy-engine.ts";
11
+ import { runTeamTask } from "./task-runner.ts";
12
+
13
+ export interface ExecuteTeamRunInput {
14
+ manifest: TeamRunManifest;
15
+ tasks: TeamTaskState[];
16
+ team: TeamConfig;
17
+ workflow: WorkflowConfig;
18
+ agents: AgentConfig[];
19
+ executeWorkers: boolean;
20
+ limits?: CrewLimitsConfig;
21
+ signal?: AbortSignal;
22
+ }
23
+
24
+ function findReadyTask(tasks: TeamTaskState[]): TeamTaskState | undefined {
25
+ const completedStepIds = new Set(tasks.filter((task) => task.status === "completed").map((task) => task.stepId).filter((id): id is string => id !== undefined));
26
+ return tasks.find((task) => task.status === "queued" && task.dependsOn.every((dep) => completedStepIds.has(dep)));
27
+ }
28
+
29
+ function findStep(workflow: WorkflowConfig, task: TeamTaskState): WorkflowStep {
30
+ const step = workflow.steps.find((candidate) => candidate.id === task.stepId);
31
+ if (!step) throw new Error(`Workflow step '${task.stepId}' not found for task '${task.id}'.`);
32
+ return step;
33
+ }
34
+
35
+ function findAgent(agents: AgentConfig[], task: TeamTaskState): AgentConfig {
36
+ const agent = agents.find((candidate) => candidate.name === task.agent);
37
+ if (!agent) throw new Error(`Agent '${task.agent}' not found for task '${task.id}'.`);
38
+ return agent;
39
+ }
40
+
41
+ function markBlocked(tasks: TeamTaskState[], reason: string): TeamTaskState[] {
42
+ return tasks.map((task) => task.status === "queued" ? { ...task, status: "skipped", error: reason, finishedAt: new Date().toISOString(), graph: task.graph ? { ...task.graph, queue: "blocked" } : undefined } : task);
43
+ }
44
+
45
+ function formatTaskProgress(task: TeamTaskState): string {
46
+ return `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.error ? ` - ${task.error}` : ""}`;
47
+ }
48
+
49
+ function writeProgress(manifest: TeamRunManifest, tasks: TeamTaskState[], producer: string): TeamRunManifest {
50
+ const counts = new Map<string, number>();
51
+ for (const task of tasks) counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
52
+ const progress = writeArtifact(manifest.artifactsRoot, {
53
+ kind: "progress",
54
+ relativePath: "progress.md",
55
+ producer,
56
+ content: [
57
+ `# pi-crew progress ${manifest.runId}`,
58
+ "",
59
+ `Status: ${manifest.status}`,
60
+ `Team: ${manifest.team}`,
61
+ `Workflow: ${manifest.workflow ?? "(none)"}`,
62
+ `Updated: ${new Date().toISOString()}`,
63
+ `Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
64
+ "",
65
+ "## Tasks",
66
+ ...tasks.map(formatTaskProgress),
67
+ "",
68
+ ].join("\n"),
69
+ });
70
+ return { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts.filter((artifact) => !(artifact.kind === "progress" && artifact.path === progress.path)), progress] };
71
+ }
72
+
73
+ function applyPolicy(manifest: TeamRunManifest, tasks: TeamTaskState[], limits?: CrewLimitsConfig): TeamRunManifest {
74
+ const decisions = evaluateCrewPolicy({ manifest, tasks, limits });
75
+ const policyArtifact = writeArtifact(manifest.artifactsRoot, {
76
+ kind: "metadata",
77
+ relativePath: "policy-decisions.json",
78
+ producer: "policy-engine",
79
+ content: `${JSON.stringify(decisions, null, 2)}\n`,
80
+ });
81
+ for (const item of decisions) appendEvent(manifest.eventsPath, { type: item.action === "escalate" ? "policy.escalated" : "policy.action", runId: manifest.runId, taskId: item.taskId, message: item.message, data: { action: item.action, reason: item.reason } });
82
+ return { ...manifest, updatedAt: new Date().toISOString(), policyDecisions: decisions, artifacts: [...manifest.artifacts.filter((artifact) => !(artifact.kind === "metadata" && artifact.path.endsWith("policy-decisions.json"))), policyArtifact] };
83
+ }
84
+
85
+ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
86
+ let manifest = updateRunStatus(input.manifest, "running", input.executeWorkers ? "Executing team workflow." : "Creating workflow prompts and placeholder results.");
87
+ let tasks = input.tasks;
88
+ manifest = writeProgress(manifest, tasks, "team-runner");
89
+ saveRunManifest(manifest);
90
+
91
+ while (tasks.some((task) => task.status === "queued")) {
92
+ if (input.signal?.aborted) {
93
+ tasks = tasks.map((task) => task.status === "queued" || task.status === "running" ? { ...task, status: "cancelled", finishedAt: new Date().toISOString(), error: "Run cancelled." } : task);
94
+ saveRunTasks(manifest, tasks);
95
+ manifest = updateRunStatus(manifest, "cancelled", "Run cancelled.");
96
+ return { manifest, tasks };
97
+ }
98
+
99
+ const failed = tasks.find((task) => task.status === "failed");
100
+ if (failed) {
101
+ tasks = markBlocked(tasks, `Blocked by failed task '${failed.id}'.`);
102
+ saveRunTasks(manifest, tasks);
103
+ manifest = updateRunStatus(manifest, "failed", `Failed at task '${failed.id}'.`);
104
+ return { manifest, tasks };
105
+ }
106
+
107
+ const task = findReadyTask(tasks);
108
+ if (!task) {
109
+ tasks = markBlocked(tasks, "No ready queued task; dependency graph may be invalid.");
110
+ saveRunTasks(manifest, tasks);
111
+ manifest = updateRunStatus(manifest, "blocked", "No ready queued task.");
112
+ return { manifest, tasks };
113
+ }
114
+
115
+ const step = findStep(input.workflow, task);
116
+ const agent = findAgent(input.agents, task);
117
+ const result = await runTeamTask({ manifest, tasks, task, step, agent, signal: input.signal, executeWorkers: input.executeWorkers });
118
+ manifest = result.manifest;
119
+ tasks = result.tasks;
120
+ manifest = writeProgress(manifest, tasks, "team-runner");
121
+ saveRunManifest(manifest);
122
+ }
123
+
124
+ const failed = tasks.find((task) => task.status === "failed");
125
+ manifest = applyPolicy(manifest, tasks, input.limits);
126
+ const blockingDecision = manifest.policyDecisions?.find((item) => item.action === "block" || item.action === "escalate");
127
+ if (failed) {
128
+ manifest = updateRunStatus(manifest, "failed", `Failed at task '${failed.id}'.`);
129
+ } else if (blockingDecision) {
130
+ manifest = updateRunStatus(manifest, "blocked", blockingDecision.message);
131
+ } else {
132
+ manifest = updateRunStatus(manifest, "completed", input.executeWorkers ? "Team workflow completed." : "Team workflow scaffold completed without launching child workers.");
133
+ }
134
+ manifest = writeProgress(manifest, tasks, "team-runner");
135
+ saveRunManifest(manifest);
136
+ const usage = aggregateUsage(tasks);
137
+ const summaryArtifact = writeArtifact(manifest.artifactsRoot, {
138
+ kind: "summary",
139
+ relativePath: "summary.md",
140
+ producer: "team-runner",
141
+ content: [
142
+ `# pi-crew run ${manifest.runId}`,
143
+ "",
144
+ `Status: ${manifest.status}`,
145
+ `Team: ${manifest.team}`,
146
+ `Workflow: ${manifest.workflow ?? "(none)"}`,
147
+ `Goal: ${manifest.goal}`,
148
+ `Usage: ${formatUsage(usage)}`,
149
+ "",
150
+ "## Tasks",
151
+ ...tasks.map(formatTaskProgress),
152
+ "",
153
+ "## Policy decisions",
154
+ ...(manifest.policyDecisions?.length ? summarizePolicyDecisions(manifest.policyDecisions) : ["- (none)"]),
155
+ "",
156
+ ].join("\n"),
157
+ });
158
+ manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, summaryArtifact] };
159
+ saveRunManifest(manifest);
160
+ saveRunTasks(manifest, tasks);
161
+ return { manifest, tasks };
162
+ }
@@ -36,9 +36,19 @@ export const TEAM_EVENT_TYPES = [
36
36
  "run.failed",
37
37
  "run.cancelled",
38
38
  "task.started",
39
+ "task.progress",
40
+ "task.blocked",
41
+ "task.green",
42
+ "task.red",
39
43
  "task.completed",
40
44
  "task.failed",
45
+ "review.approved",
46
+ "review.rejected",
47
+ "policy.action",
48
+ "policy.escalated",
49
+ "mailbox.timeout",
41
50
  "worktree.cleanup",
51
+ "worktree.dirty",
42
52
  "async.spawned",
43
53
  "async.started",
44
54
  "async.completed",
@@ -39,18 +39,29 @@ export function createRunPaths(cwd: string, runId = createRunId()): RunPaths {
39
39
  }
40
40
 
41
41
  export function createTasksFromWorkflow(runId: string, workflow: WorkflowConfig, team: TeamConfig, cwd: string): TeamTaskState[] {
42
+ const stepToTaskId = new Map(workflow.steps.map((step, index) => [step.id, createTaskId(step.id, index)]));
42
43
  return workflow.steps.map((step, index) => {
43
44
  const role = team.roles.find((candidate) => candidate.name === step.role);
45
+ const id = stepToTaskId.get(step.id) ?? createTaskId(step.id, index);
46
+ const dependencies = step.dependsOn ?? [];
47
+ const children = workflow.steps.filter((candidate) => candidate.dependsOn?.includes(step.id)).map((candidate) => stepToTaskId.get(candidate.id)).filter((childId): childId is string => childId !== undefined);
44
48
  return {
45
- id: createTaskId(step.id, index),
49
+ id,
46
50
  runId,
47
51
  stepId: step.id,
48
52
  role: step.role,
49
53
  agent: role?.agent ?? step.role,
50
54
  title: step.id,
51
55
  status: "queued",
52
- dependsOn: step.dependsOn ?? [],
56
+ dependsOn: dependencies,
53
57
  cwd,
58
+ graph: {
59
+ taskId: id,
60
+ parentId: dependencies[0] ? stepToTaskId.get(dependencies[0]) : undefined,
61
+ children,
62
+ dependencies: dependencies.map((dep) => stepToTaskId.get(dep) ?? dep),
63
+ queue: dependencies.length ? "blocked" : "ready",
64
+ },
54
65
  };
55
66
  });
56
67
  }
@@ -14,6 +14,66 @@ export interface ArtifactDescriptor {
14
14
  expiresAt?: string;
15
15
  }
16
16
 
17
+ export type TaskScope = "workspace" | "module" | "single_file" | "custom";
18
+ export type GreenLevel = "none" | "targeted" | "package" | "workspace" | "merge_ready";
19
+
20
+ export interface VerificationCommandResult {
21
+ cmd: string;
22
+ status: "passed" | "failed" | "not_run";
23
+ exitCode?: number | null;
24
+ outputArtifact?: ArtifactDescriptor;
25
+ }
26
+
27
+ export interface VerificationContract {
28
+ requiredGreenLevel: GreenLevel;
29
+ commands: string[];
30
+ allowManualEvidence: boolean;
31
+ }
32
+
33
+ export interface VerificationEvidence {
34
+ requiredGreenLevel: GreenLevel;
35
+ observedGreenLevel: GreenLevel;
36
+ satisfied: boolean;
37
+ commands: VerificationCommandResult[];
38
+ notes?: string;
39
+ }
40
+
41
+ export interface TaskPacket {
42
+ objective: string;
43
+ scope: TaskScope;
44
+ scopePath?: string;
45
+ repo: string;
46
+ worktree?: string;
47
+ branchPolicy: string;
48
+ acceptanceTests: string[];
49
+ commitPolicy: string;
50
+ reportingContract: string;
51
+ escalationPolicy: string;
52
+ constraints: string[];
53
+ expectedArtifacts: string[];
54
+ verification: VerificationContract;
55
+ }
56
+
57
+ export type PolicyDecisionAction = "retry" | "reassign" | "escalate" | "block" | "notify" | "cleanup" | "closeout";
58
+ export type PolicyDecisionReason = "task_failed" | "worker_stale" | "green_unsatisfied" | "limit_exceeded" | "run_complete" | "mailbox_timeout" | "review_rejected" | "branch_stale" | "scope_mismatch";
59
+
60
+ export interface PolicyDecision {
61
+ action: PolicyDecisionAction;
62
+ reason: PolicyDecisionReason;
63
+ message: string;
64
+ taskId?: string;
65
+ createdAt: string;
66
+ }
67
+
68
+ export interface TaskGraphNode {
69
+ taskId: string;
70
+ parentId?: string;
71
+ children: string[];
72
+ dependencies: string[];
73
+ queue: "ready" | "blocked" | "running" | "done";
74
+ sessionForkFrom?: string;
75
+ }
76
+
17
77
  export interface AsyncRunState {
18
78
  pid?: number;
19
79
  logPath: string;
@@ -38,6 +98,7 @@ export interface TeamRunManifest {
38
98
  artifacts: ArtifactDescriptor[];
39
99
  async?: AsyncRunState;
40
100
  summary?: string;
101
+ policyDecisions?: PolicyDecision[];
41
102
  }
42
103
 
43
104
  export interface UsageState {
@@ -85,4 +146,11 @@ export interface TeamTaskState {
85
146
  error?: string;
86
147
  claim?: TaskClaimState;
87
148
  heartbeat?: WorkerHeartbeatState;
149
+ taskPacket?: TaskPacket;
150
+ verification?: VerificationEvidence;
151
+ graph?: TaskGraphNode;
152
+ policy?: {
153
+ retryCount?: number;
154
+ lastDecision?: PolicyDecision;
155
+ };
88
156
  }
@@ -1,7 +1,12 @@
1
1
  import * as fs from "node:fs";
2
- import type { Component } from "@mariozechner/pi-tui";
3
2
  import type { TeamRunManifest } from "../state/types.ts";
4
3
 
4
+ interface DashboardComponent {
5
+ invalidate(): void;
6
+ render(width: number): string[];
7
+ handleInput(data: string): void;
8
+ }
9
+
5
10
  export type RunDashboardAction = "status" | "summary" | "artifacts" | "api" | "reload";
6
11
  export interface RunDashboardSelection {
7
12
  runId: string;
@@ -41,7 +46,7 @@ function countByStatus(runs: TeamRunManifest[]): string {
41
46
  return [...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none";
42
47
  }
43
48
 
44
- export class RunDashboard implements Component {
49
+ export class RunDashboard implements DashboardComponent {
45
50
  private selected = 0;
46
51
  private showFullProgress = false;
47
52
  private readonly runs: TeamRunManifest[];