pi-crew 0.1.24 → 0.1.26

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 (73) hide show
  1. package/docs/refactor-tasks-phase3.md +394 -0
  2. package/docs/refactor-tasks-phase4.md +564 -0
  3. package/docs/refactor-tasks-phase5.md +402 -0
  4. package/docs/refactor-tasks.md +1484 -0
  5. package/package.json +98 -95
  6. package/src/agents/agent-config.ts +30 -30
  7. package/src/config/config.ts +153 -89
  8. package/src/config/defaults.ts +60 -0
  9. package/src/extension/autonomous-policy.ts +1 -1
  10. package/src/extension/help.ts +1 -0
  11. package/src/extension/management.ts +15 -2
  12. package/src/extension/register.ts +124 -170
  13. package/src/extension/registration/command-utils.ts +54 -0
  14. package/src/extension/registration/subagent-helpers.ts +70 -0
  15. package/src/extension/registration/viewers.ts +32 -0
  16. package/src/extension/result-watcher.ts +98 -89
  17. package/src/extension/team-tool/api.ts +276 -0
  18. package/src/extension/team-tool/config-patch.ts +36 -0
  19. package/src/extension/team-tool/context.ts +48 -0
  20. package/src/extension/team-tool/doctor.ts +178 -0
  21. package/src/extension/team-tool/run.ts +133 -0
  22. package/src/extension/team-tool-types.ts +6 -0
  23. package/src/extension/team-tool.ts +31 -623
  24. package/src/extension/tool-result.ts +16 -16
  25. package/src/runtime/async-runner.ts +42 -60
  26. package/src/runtime/child-pi.ts +434 -332
  27. package/src/runtime/concurrency.ts +50 -42
  28. package/src/runtime/crew-agent-records.ts +166 -156
  29. package/src/runtime/manifest-cache.ts +214 -0
  30. package/src/runtime/parallel-utils.ts +99 -0
  31. package/src/runtime/post-exit-stdio-guard.ts +86 -0
  32. package/src/runtime/runtime-resolver.ts +77 -74
  33. package/src/runtime/subagent-manager.ts +291 -236
  34. package/src/runtime/task-graph-scheduler.ts +122 -107
  35. package/src/runtime/team-runner.ts +46 -51
  36. package/src/schema/config-schema.ts +92 -0
  37. package/src/state/artifact-store.ts +108 -36
  38. package/src/state/atomic-write.ts +114 -49
  39. package/src/state/event-log.ts +189 -138
  40. package/src/state/jsonl-writer.ts +77 -0
  41. package/src/state/locks.ts +149 -40
  42. package/src/state/mailbox.ts +200 -188
  43. package/src/state/state-store.ts +104 -15
  44. package/src/teams/discover-teams.ts +94 -84
  45. package/src/teams/team-config.ts +26 -22
  46. package/src/ui/crew-footer.ts +101 -0
  47. package/src/ui/crew-select-list.ts +111 -0
  48. package/src/ui/crew-widget.ts +285 -219
  49. package/src/ui/dynamic-border.ts +25 -0
  50. package/src/ui/layout-primitives.ts +106 -0
  51. package/src/ui/live-run-sidebar.ts +163 -95
  52. package/src/ui/loaders.ts +158 -0
  53. package/src/ui/mascot.ts +441 -0
  54. package/src/ui/powerbar-publisher.ts +94 -71
  55. package/src/ui/render-diff.ts +119 -0
  56. package/src/ui/run-dashboard.ts +155 -120
  57. package/src/ui/status-colors.ts +54 -0
  58. package/src/ui/syntax-highlight.ts +116 -0
  59. package/src/ui/theme-adapter.ts +190 -0
  60. package/src/ui/transcript-viewer.ts +194 -111
  61. package/src/utils/completion-dedupe.ts +63 -0
  62. package/src/utils/file-coalescer.ts +84 -33
  63. package/src/utils/fs-watch.ts +31 -0
  64. package/src/utils/git.ts +262 -0
  65. package/src/utils/internal-error.ts +6 -0
  66. package/src/utils/paths.ts +33 -15
  67. package/src/utils/sleep.ts +32 -0
  68. package/src/utils/timings.ts +31 -0
  69. package/src/utils/visual.ts +159 -0
  70. package/src/workflows/discover-workflows.ts +109 -101
  71. package/src/workflows/workflow-config.ts +25 -24
  72. package/src/workflows/workflow-serializer.ts +32 -31
  73. package/tsconfig.json +19 -19
@@ -0,0 +1,70 @@
1
+ import * as fs from "node:fs";
2
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
3
+ import { loadRunManifestById } from "../../state/state-store.ts";
4
+ import { savePersistedSubagentRecord, type SubagentRecord, type SubagentSpawnOptions } from "../../runtime/subagent-manager.ts";
5
+
6
+ export function sendFollowUp(pi: ExtensionAPI, content: string): void {
7
+ const sender = (pi as unknown as { sendMessage?: (message: unknown, options?: unknown) => void }).sendMessage;
8
+ if (typeof sender !== "function") return;
9
+ sender.call(pi, { customType: "pi-crew-subagent-notification", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
10
+ }
11
+
12
+ export function refreshPersistedSubagentRecord(ctx: ExtensionContext | ExtensionCommandContext, record: SubagentRecord): SubagentRecord {
13
+ if (!record.runId) return record;
14
+ const loaded = loadRunManifestById(ctx.cwd, record.runId);
15
+ if (!loaded) return record;
16
+ if (loaded.manifest.status === "completed" || loaded.manifest.status === "failed" || loaded.manifest.status === "cancelled" || loaded.manifest.status === "blocked") {
17
+ const refreshed = {
18
+ ...record,
19
+ status: loaded.manifest.status,
20
+ error: loaded.manifest.status === "completed" || loaded.manifest.status === "blocked" ? undefined : loaded.manifest.summary,
21
+ completedAt: loaded.manifest.status === "blocked" ? undefined : record.completedAt ?? Date.now(),
22
+ };
23
+ savePersistedSubagentRecord(ctx.cwd, refreshed);
24
+ return refreshed;
25
+ }
26
+ return record;
27
+ }
28
+
29
+ export function formatSubagentRecord(record: SubagentRecord): string {
30
+ const duration = record.completedAt ? `${Math.round((record.completedAt - record.startedAt) / 1000)}s` : "running";
31
+ return [
32
+ `Agent: ${record.id}`,
33
+ `Type: ${record.type}`,
34
+ `Status: ${record.status}`,
35
+ record.runId ? `Run: ${record.runId}` : undefined,
36
+ `Description: ${record.description}`,
37
+ record.model ? `Model: ${record.model}` : undefined,
38
+ `Duration: ${duration}`,
39
+ record.error ? `Error: ${record.error}` : undefined,
40
+ ].filter((line): line is string => Boolean(line)).join("\n");
41
+ }
42
+
43
+ export function readSubagentRunResult(ctx: ExtensionContext | ExtensionCommandContext, record: SubagentRecord): string | undefined {
44
+ if (!record.runId) return record.result;
45
+ const loaded = loadRunManifestById(ctx.cwd, record.runId);
46
+ const task = loaded?.tasks.find((item) => item.resultArtifact) ?? loaded?.tasks[0];
47
+ const path = task?.resultArtifact?.path;
48
+ if (!path) return undefined;
49
+ try {
50
+ return fs.readFileSync(path, "utf-8").trim();
51
+ } catch {
52
+ return undefined;
53
+ }
54
+ }
55
+
56
+ export function subagentToolResult(text: string, details: Record<string, unknown> = {}, isError = false) {
57
+ return { content: [{ type: "text" as const, text }], details, isError };
58
+ }
59
+
60
+ export function __test__subagentSpawnParams(params: Record<string, unknown>, ctx: Pick<ExtensionContext, "cwd">): SubagentSpawnOptions {
61
+ return {
62
+ cwd: ctx.cwd,
63
+ type: typeof params.subagent_type === "string" && params.subagent_type.trim() ? params.subagent_type.trim() : "executor",
64
+ description: typeof params.description === "string" && params.description.trim() ? params.description.trim() : "pi-crew subagent",
65
+ prompt: typeof params.prompt === "string" ? params.prompt : "",
66
+ background: params.run_in_background === true,
67
+ model: typeof params.model === "string" && params.model.trim() ? params.model.trim() : undefined,
68
+ maxTurns: typeof params.max_turns === "number" && Number.isFinite(params.max_turns) ? params.max_turns : undefined,
69
+ };
70
+ }
@@ -0,0 +1,32 @@
1
+ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ import { loadRunManifestById } from "../../state/state-store.ts";
3
+ import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
4
+ import { DurableTranscriptViewer } from "../../ui/transcript-viewer.ts";
5
+
6
+ export async function selectAgentTask(ctx: ExtensionCommandContext, runId: string | undefined, taskId?: string): Promise<{ runId: string; taskId?: string } | undefined> {
7
+ if (!runId) return undefined;
8
+ if (taskId) return { runId, taskId };
9
+ const loaded = loadRunManifestById(ctx.cwd, runId);
10
+ if (!loaded) return { runId };
11
+ const agents = readCrewAgents(loaded.manifest);
12
+ if (ctx.hasUI && agents.length > 1) {
13
+ const choice = await ctx.ui.select("Select pi-crew agent", agents.map((agent) => `${agent.taskId} ${agent.role}→${agent.agent} [${agent.status}]`));
14
+ return { runId, taskId: choice?.split(" ")[0] };
15
+ }
16
+ return { runId, taskId: agents[0]?.taskId };
17
+ }
18
+
19
+ export async function openTranscriptViewer(ctx: ExtensionCommandContext, initialRunId: string | undefined, initialTaskId?: string): Promise<boolean> {
20
+ const selected = await selectAgentTask(ctx, initialRunId, initialTaskId);
21
+ if (!selected) return false;
22
+ const runId = selected.runId;
23
+ const taskId = selected.taskId;
24
+ if (!runId || !ctx.hasUI) return false;
25
+ const loaded = loadRunManifestById(ctx.cwd, runId);
26
+ if (!loaded) return false;
27
+ await ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new DurableTranscriptViewer(loaded.manifest, theme, done, taskId), {
28
+ overlay: true,
29
+ overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" },
30
+ });
31
+ return true;
32
+ }
@@ -1,89 +1,98 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { createFileCoalescer } from "../utils/file-coalescer.ts";
4
-
5
- export interface ResultWatcherEvents {
6
- emit(event: string, data: unknown): void;
7
- }
8
-
9
- export interface ResultWatcherHandle {
10
- start(): void;
11
- prime(): void;
12
- stop(): void;
13
- }
14
-
15
- export interface ResultWatcherOptions {
16
- eventName?: string;
17
- completionTtlMs?: number;
18
- }
19
-
20
- function readJson(filePath: string): unknown | undefined {
21
- try {
22
- return JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
23
- } catch {
24
- return undefined;
25
- }
26
- }
27
-
28
- function completionKey(payload: unknown, file: string): string {
29
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) return `file:${file}`;
30
- const obj = payload as Record<string, unknown>;
31
- const id = [obj.runId, obj.sessionId, obj.id, obj.status].filter((entry): entry is string => typeof entry === "string" && entry.length > 0).join(":");
32
- return id || `file:${file}`;
33
- }
34
-
35
- export function createResultWatcher(events: ResultWatcherEvents, resultsDir: string, eventNameOrOptions: string | ResultWatcherOptions = "pi-crew:run-result"): ResultWatcherHandle {
36
- const options = typeof eventNameOrOptions === "string" ? { eventName: eventNameOrOptions } : eventNameOrOptions;
37
- const eventName = options.eventName ?? "pi-crew:run-result";
38
- const completionTtlMs = options.completionTtlMs ?? 5 * 60_000;
39
- const seen = new Map<string, number>();
40
- let watcher: fs.FSWatcher | undefined;
41
- let restartTimer: ReturnType<typeof setTimeout> | undefined;
42
- const coalescer = createFileCoalescer((file) => {
43
- const filePath = path.join(resultsDir, file);
44
- if (!file.endsWith(".json") || !fs.existsSync(filePath)) return;
45
- const payload = readJson(filePath);
46
- if (payload !== undefined) {
47
- const now = Date.now();
48
- for (const [key, expiresAt] of seen) if (expiresAt <= now) seen.delete(key);
49
- const key = completionKey(payload, file);
50
- if (!seen.has(key)) {
51
- seen.set(key, now + completionTtlMs);
52
- events.emit(eventName, payload);
53
- }
54
- }
55
- try { fs.unlinkSync(filePath); } catch {}
56
- }, 50);
57
- const scheduleRestart = () => {
58
- if (restartTimer) clearTimeout(restartTimer);
59
- restartTimer = setTimeout(() => {
60
- restartTimer = undefined;
61
- try { handle.start(); } catch {}
62
- }, 3000);
63
- restartTimer.unref?.();
64
- };
65
- const handle: ResultWatcherHandle = {
66
- start() {
67
- fs.mkdirSync(resultsDir, { recursive: true });
68
- watcher?.close();
69
- watcher = fs.watch(resultsDir, (event, file) => {
70
- if (event !== "rename" || !file) return;
71
- coalescer.schedule(file.toString());
72
- });
73
- watcher.on("error", scheduleRestart);
74
- watcher.unref?.();
75
- },
76
- prime() {
77
- if (!fs.existsSync(resultsDir)) return;
78
- for (const file of fs.readdirSync(resultsDir).filter((entry) => entry.endsWith(".json"))) coalescer.schedule(file, 0);
79
- },
80
- stop() {
81
- watcher?.close();
82
- watcher = undefined;
83
- if (restartTimer) clearTimeout(restartTimer);
84
- restartTimer = undefined;
85
- coalescer.clear();
86
- },
87
- };
88
- return handle;
89
- }
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { buildCompletionKey, getGlobalSeenMap, markSeenWithTtl } from "../utils/completion-dedupe.ts";
4
+ import { closeWatcher, watchWithErrorHandler } from "../utils/fs-watch.ts";
5
+ import { createFileCoalescer } from "../utils/file-coalescer.ts";
6
+ import { logInternalError } from "../utils/internal-error.ts";
7
+
8
+ export interface ResultWatcherEvents {
9
+ emit(event: string, data: unknown): void;
10
+ }
11
+
12
+ export interface ResultWatcherHandle {
13
+ start(): void;
14
+ prime(): void;
15
+ stop(): void;
16
+ }
17
+
18
+ interface ResultWatcherDependencies {
19
+ watch?: typeof watchWithErrorHandler;
20
+ }
21
+
22
+ export interface ResultWatcherOptions extends ResultWatcherDependencies {
23
+ eventName?: string;
24
+ completionTtlMs?: number;
25
+ }
26
+
27
+ const RESULT_WATCHER_RESTART_MS = 3000;
28
+
29
+ function readJson(filePath: string): unknown | undefined {
30
+ try {
31
+ return JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
32
+ } catch (error) {
33
+ logInternalError("result-watcher.parse", error, `filePath=${filePath}`);
34
+ return undefined;
35
+ }
36
+ }
37
+
38
+ export function createResultWatcher(events: ResultWatcherEvents, resultsDir: string, eventNameOrOptions: string | ResultWatcherOptions = "pi-crew:run-result"): ResultWatcherHandle {
39
+ const options: ResultWatcherOptions = typeof eventNameOrOptions === "string" ? { eventName: eventNameOrOptions } : eventNameOrOptions;
40
+ const eventName = options.eventName ?? "pi-crew:run-result";
41
+ const completionTtlMs = options.completionTtlMs ?? 5 * 60_000;
42
+ const watch = options.watch ?? watchWithErrorHandler;
43
+ const seen = getGlobalSeenMap("pi-crew.result-watcher");
44
+ let watcher: fs.FSWatcher | null | undefined;
45
+ let restartTimer: ReturnType<typeof setTimeout> | undefined;
46
+ const coalescer = createFileCoalescer((file) => {
47
+ const filePath = path.join(resultsDir, file);
48
+ if (!file.endsWith(".json") || !fs.existsSync(filePath)) return;
49
+ const payload = readJson(filePath);
50
+ if (payload !== undefined) {
51
+ const key = buildCompletionKey(payload as Record<string, unknown>, `file:${file}`);
52
+ if (!markSeenWithTtl(seen, key, Date.now(), completionTtlMs)) {
53
+ events.emit(eventName, payload);
54
+ }
55
+ }
56
+ try {
57
+ fs.unlinkSync(filePath);
58
+ } catch (error) {
59
+ logInternalError("result-watcher.unlink", error, `filePath=${filePath}`);
60
+ }
61
+ }, 50);
62
+ const scheduleRestart = () => {
63
+ if (restartTimer) clearTimeout(restartTimer);
64
+ restartTimer = setTimeout(() => {
65
+ restartTimer = undefined;
66
+ try {
67
+ fs.mkdirSync(resultsDir, { recursive: true });
68
+ handle.start();
69
+ } catch (error) {
70
+ logInternalError("result-watcher.restart", error, `resultsDir=${resultsDir}`);
71
+ }
72
+ }, RESULT_WATCHER_RESTART_MS);
73
+ restartTimer.unref?.();
74
+ };
75
+ const handle: ResultWatcherHandle = {
76
+ start() {
77
+ fs.mkdirSync(resultsDir, { recursive: true });
78
+ if (watcher) closeWatcher(watcher);
79
+ watcher = watch(resultsDir, (event, fileName) => {
80
+ if (event !== "rename" || !fileName) return;
81
+ coalescer.schedule(fileName.toString());
82
+ }, scheduleRestart);
83
+ watcher?.unref?.();
84
+ },
85
+ prime() {
86
+ if (!fs.existsSync(resultsDir)) return;
87
+ for (const file of fs.readdirSync(resultsDir).filter((entry) => entry.endsWith(".json"))) coalescer.schedule(file, 0);
88
+ },
89
+ stop() {
90
+ if (restartTimer) clearTimeout(restartTimer);
91
+ restartTimer = undefined;
92
+ closeWatcher(watcher);
93
+ watcher = undefined;
94
+ coalescer.clear();
95
+ },
96
+ };
97
+ return handle;
98
+ }
@@ -0,0 +1,276 @@
1
+ import * as fs from "node:fs";
2
+ import { loadConfig } from "../../config/config.ts";
3
+ import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
4
+ import { saveRunTasks, loadRunManifestById } from "../../state/state-store.ts";
5
+ import { withRunLockSync } from "../../state/locks.ts";
6
+ import { canTransitionTaskStatus, isTeamTaskStatus } from "../../state/contracts.ts";
7
+ import { claimTask, releaseTaskClaim, transitionClaimedTaskStatus } from "../../state/task-claims.ts";
8
+ import { acknowledgeMailboxMessage, appendMailboxMessage, readDeliveryState, readMailbox, validateMailbox, type MailboxDirection } from "../../state/mailbox.ts";
9
+ import { appendEvent, readEvents, readEventsCursor } from "../../state/event-log.ts";
10
+ import { resolveCrewRuntime } from "../../runtime/runtime-resolver.ts";
11
+ import { probeLiveSessionRuntime } from "../../runtime/live-session-runtime.ts";
12
+ import { touchWorkerHeartbeat } from "../../runtime/worker-heartbeat.ts";
13
+ import { agentEventsPath, agentOutputPath, readCrewAgentEvents, readCrewAgentEventsCursor, readCrewAgentStatus, readCrewAgents } from "../../runtime/crew-agent-records.ts";
14
+ import { buildAgentDashboard, readAgentOutput } from "../../runtime/agent-observability.ts";
15
+ import { readForegroundControlStatus, writeForegroundInterruptRequest } from "../../runtime/foreground-control.ts";
16
+ import { listLiveAgents, resumeLiveAgent, steerLiveAgent, stopLiveAgent } from "../../runtime/live-agent-manager.ts";
17
+ import { appendLiveAgentControlRequest } from "../../runtime/live-agent-control.ts";
18
+ import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../../runtime/live-control-realtime.ts";
19
+ import type { PiTeamsToolResult } from "../tool-result.ts";
20
+ import { configRecord, result, type TeamContext } from "./context.ts";
21
+
22
+ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
23
+ if (!params.runId) return result("API requires runId.", { action: "api", status: "error" }, true);
24
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
25
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "api", status: "error" }, true);
26
+ const cfg = configRecord(params.config);
27
+ const operation = typeof cfg.operation === "string" ? cfg.operation : "read-manifest";
28
+ if (operation === "read-manifest") {
29
+ return result(JSON.stringify(loaded.manifest, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
30
+ }
31
+ if (operation === "list-tasks") {
32
+ return result(JSON.stringify(loaded.tasks, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
33
+ }
34
+ if (operation === "read-task") {
35
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
36
+ const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
37
+ if (!task) return result("API read-task requires config.taskId matching a task id or step id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
38
+ return result(JSON.stringify(task, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
39
+ }
40
+ if (operation === "read-events") {
41
+ const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
42
+ const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
43
+ const payload = sinceSeq !== undefined || limit !== undefined
44
+ ? readEventsCursor(loaded.manifest.eventsPath, { sinceSeq, limit })
45
+ : { events: readEvents(loaded.manifest.eventsPath), nextSeq: undefined, total: undefined };
46
+ return result(JSON.stringify(payload, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
47
+ }
48
+ if (operation === "runtime-capabilities") {
49
+ const loadedConfig = loadConfig(ctx.cwd);
50
+ return result(JSON.stringify(await resolveCrewRuntime(loadedConfig.config), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
51
+ }
52
+ if (operation === "probe-live-session") {
53
+ return result(JSON.stringify(await probeLiveSessionRuntime(), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
54
+ }
55
+ if (operation === "list-agents") {
56
+ return result(JSON.stringify(readCrewAgents(loaded.manifest), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
57
+ }
58
+ if (operation === "get-agent-result") {
59
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
60
+ const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
61
+ if (!agent) return result("API get-agent-result requires config.agentId matching an agent id or task id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
62
+ const task = loaded.tasks.find((item) => item.id === agent.taskId);
63
+ const text = task?.resultArtifact && fs.existsSync(task.resultArtifact.path) ? fs.readFileSync(task.resultArtifact.path, "utf-8") : JSON.stringify(agent, null, 2);
64
+ return result(text, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
65
+ }
66
+ if (operation === "read-agent-status") {
67
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
68
+ const agent = agentId ? readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId) : undefined;
69
+ const status = agent ? readCrewAgentStatus(loaded.manifest, agent.taskId) ?? agent : undefined;
70
+ if (!status) return result("API read-agent-status requires config.agentId matching an agent id or task id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
71
+ return result(JSON.stringify(status, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
72
+ }
73
+ if (operation === "read-agent-events") {
74
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
75
+ const agents = readCrewAgents(loaded.manifest);
76
+ const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
77
+ if (!agent) return result("API read-agent-events requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
78
+ const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
79
+ const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
80
+ const payload = sinceSeq !== undefined || limit !== undefined
81
+ ? readCrewAgentEventsCursor(loaded.manifest, agent.taskId, { sinceSeq, limit })
82
+ : { path: agentEventsPath(loaded.manifest, agent.taskId), events: readCrewAgentEvents(loaded.manifest, agent.taskId) };
83
+ return result(JSON.stringify(payload, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
84
+ }
85
+ if (operation === "read-agent-transcript") {
86
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
87
+ const agents = readCrewAgents(loaded.manifest);
88
+ const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
89
+ if (!agent) return result("API read-agent-transcript requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
90
+ const transcriptPath = agent.transcriptPath && fs.existsSync(agent.transcriptPath) ? agent.transcriptPath : agentOutputPath(loaded.manifest, agent.taskId);
91
+ const text = fs.existsSync(transcriptPath) ? fs.readFileSync(transcriptPath, "utf-8") : "";
92
+ return result(text || `(no transcript at ${transcriptPath})`, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
93
+ }
94
+ if (operation === "read-agent-output") {
95
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
96
+ const agents = readCrewAgents(loaded.manifest);
97
+ const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
98
+ if (!agent) return result("API read-agent-output requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
99
+ const maxBytes = typeof cfg.maxBytes === "number" ? cfg.maxBytes : undefined;
100
+ return result(JSON.stringify(readAgentOutput(loaded.manifest, agent.taskId, maxBytes), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
101
+ }
102
+ if (operation === "agent-dashboard") {
103
+ return result(buildAgentDashboard(loaded.manifest).text, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
104
+ }
105
+ if (operation === "foreground-status") {
106
+ return result(JSON.stringify(readForegroundControlStatus(loaded.manifest, loaded.tasks), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
107
+ }
108
+ if (operation === "foreground-interrupt") {
109
+ const reason = typeof cfg.reason === "string" && cfg.reason.trim() ? cfg.reason.trim() : undefined;
110
+ return result(JSON.stringify(writeForegroundInterruptRequest(loaded.manifest, reason), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
111
+ }
112
+ if (operation === "nudge-agent") {
113
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
114
+ const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
115
+ if (!agent) return result("API nudge-agent requires config.agentId matching an agent id or task id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
116
+ const messageText = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : "Please report your current status, blocker, or smallest next step.";
117
+ const message = appendMailboxMessage(loaded.manifest, { direction: "inbox", from: "leader", to: agent.taskId, taskId: agent.taskId, body: messageText });
118
+ appendEvent(loaded.manifest.eventsPath, { type: "agent.nudged", runId: loaded.manifest.runId, taskId: agent.taskId, message: messageText, data: { agentId: agent.id, mailboxMessageId: message.id } });
119
+ return result(JSON.stringify({ agentId: agent.id, mailboxMessage: message }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
120
+ }
121
+ if (operation === "list-live-agents") {
122
+ return result(JSON.stringify(listLiveAgents().filter((agent) => agent.runId === loaded.manifest.runId), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
123
+ }
124
+ if (operation === "steer-agent" || operation === "stop-agent" || operation === "resume-agent" || operation === "interrupt-agent") {
125
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
126
+ if (!agentId) return result(`API ${operation} requires config.agentId.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
127
+ const message = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : undefined;
128
+ const prompt = typeof cfg.prompt === "string" && cfg.prompt.trim() ? cfg.prompt.trim() : message;
129
+ try {
130
+ if (operation === "steer-agent") return result(JSON.stringify(await steerLiveAgent(agentId, message ?? "Please report current status and wrap up if possible."), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
131
+ if (operation === "resume-agent") {
132
+ if (!prompt) return result("API resume-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
133
+ return result(JSON.stringify(await resumeLiveAgent(agentId, prompt), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
134
+ }
135
+ return result(JSON.stringify(await stopLiveAgent(agentId), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
136
+ } catch (error) {
137
+ const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
138
+ if (!agent) {
139
+ const err = error instanceof Error ? error.message : String(error);
140
+ return result(err, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
141
+ }
142
+ if (operation === "resume-agent" && !prompt) return result("API resume-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
143
+ const request = appendLiveAgentControlRequest(loaded.manifest, { taskId: agent.taskId, agentId: agent.id, operation: operation === "resume-agent" ? "resume" : operation === "steer-agent" ? "steer" : "stop", message: operation === "resume-agent" ? prompt : message });
144
+ publishLiveControlRealtime(request);
145
+ ctx.events?.emit?.("pi-crew:live-control", liveControlRealtimeMessage(request));
146
+ appendEvent(loaded.manifest.eventsPath, { type: "agent.control.queued", runId: loaded.manifest.runId, taskId: agent.taskId, message: `Queued ${request.operation} control request for live agent.`, data: { request, realtime: true } });
147
+ return result(JSON.stringify({ queued: true, request }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
148
+ }
149
+ }
150
+ if (operation === "read-mailbox") {
151
+ const direction = cfg.direction === "inbox" || cfg.direction === "outbox" ? cfg.direction as MailboxDirection : undefined;
152
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
153
+ return result(JSON.stringify(readMailbox(loaded.manifest, direction, taskId), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
154
+ }
155
+ if (operation === "validate-mailbox") {
156
+ const report = validateMailbox(loaded.manifest, { repair: cfg.repair === true });
157
+ return result(JSON.stringify(report, null, 2), { action: "api", status: report.issues.some((issue) => issue.level === "error") && cfg.repair !== true ? "error" : "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot }, report.issues.some((issue) => issue.level === "error") && cfg.repair !== true);
158
+ }
159
+ if (operation === "read-delivery") {
160
+ return result(JSON.stringify(readDeliveryState(loaded.manifest), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
161
+ }
162
+ if (operation === "send-message") {
163
+ const direction = cfg.direction === "outbox" ? "outbox" : "inbox";
164
+ const from = typeof cfg.from === "string" && cfg.from.trim() ? cfg.from.trim() : "api";
165
+ const to = typeof cfg.to === "string" && cfg.to.trim() ? cfg.to.trim() : "leader";
166
+ const body = typeof cfg.body === "string" && cfg.body.trim() ? cfg.body : undefined;
167
+ const taskId = typeof cfg.taskId === "string" && cfg.taskId.trim() ? cfg.taskId.trim() : undefined;
168
+ if (!body) return result("API send-message requires config.body.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
169
+ try {
170
+ return withRunLockSync(loaded.manifest, () => {
171
+ const message = appendMailboxMessage(loaded.manifest, { direction, from, to, body, taskId });
172
+ appendEvent(loaded.manifest.eventsPath, { type: "mailbox.message", runId: loaded.manifest.runId, data: { id: message.id, direction, from, to } });
173
+ return result(JSON.stringify(message, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
174
+ });
175
+ } catch (error) {
176
+ const message = error instanceof Error ? error.message : String(error);
177
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
178
+ }
179
+ }
180
+ if (operation === "ack-message") {
181
+ const messageId = typeof cfg.messageId === "string" ? cfg.messageId : undefined;
182
+ if (!messageId) return result("API ack-message requires config.messageId.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
183
+ try {
184
+ return withRunLockSync(loaded.manifest, () => {
185
+ const delivery = acknowledgeMailboxMessage(loaded.manifest, messageId);
186
+ appendEvent(loaded.manifest.eventsPath, { type: "mailbox.acknowledged", runId: loaded.manifest.runId, data: { messageId } });
187
+ return result(JSON.stringify(delivery, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
188
+ });
189
+ } catch (error) {
190
+ const message = error instanceof Error ? error.message : String(error);
191
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
192
+ }
193
+ }
194
+ if (operation === "read-heartbeat") {
195
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
196
+ const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
197
+ if (!task) return result("API read-heartbeat requires config.taskId matching a task id or step id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
198
+ return result(JSON.stringify(task.heartbeat ?? null, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
199
+ }
200
+ if (operation === "claim-task") {
201
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
202
+ const owner = typeof cfg.owner === "string" ? cfg.owner : "api";
203
+ const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
204
+ if (!task) return result("API claim-task requires config.taskId matching a task id or step id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
205
+ try {
206
+ return withRunLockSync(loaded.manifest, () => {
207
+ const updatedTask = claimTask(task, owner);
208
+ const tasks = loaded.tasks.map((item) => item.id === task.id ? updatedTask : item);
209
+ saveRunTasks(loaded.manifest, tasks);
210
+ appendEvent(loaded.manifest.eventsPath, { type: "task.claimed", runId: loaded.manifest.runId, taskId: task.id, data: { owner, token: updatedTask.claim?.token, leasedUntil: updatedTask.claim?.leasedUntil } });
211
+ return result(JSON.stringify(updatedTask.claim, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
212
+ });
213
+ } catch (error) {
214
+ const message = error instanceof Error ? error.message : String(error);
215
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
216
+ }
217
+ }
218
+ if (operation === "release-task-claim") {
219
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
220
+ const owner = typeof cfg.owner === "string" ? cfg.owner : undefined;
221
+ const token = typeof cfg.token === "string" ? cfg.token : undefined;
222
+ const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
223
+ if (!task || !owner || !token) return result("API release-task-claim requires config.taskId, config.owner, and config.token.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
224
+ try {
225
+ return withRunLockSync(loaded.manifest, () => {
226
+ const updatedTask = releaseTaskClaim(task, owner, token);
227
+ const tasks = loaded.tasks.map((item) => item.id === task.id ? updatedTask : item);
228
+ saveRunTasks(loaded.manifest, tasks);
229
+ appendEvent(loaded.manifest.eventsPath, { type: "task.claim_released", runId: loaded.manifest.runId, taskId: task.id, data: { owner } });
230
+ return result(JSON.stringify(updatedTask, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
231
+ });
232
+ } catch (error) {
233
+ const message = error instanceof Error ? error.message : String(error);
234
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
235
+ }
236
+ }
237
+ if (operation === "transition-task-status") {
238
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
239
+ const owner = typeof cfg.owner === "string" ? cfg.owner : undefined;
240
+ const token = typeof cfg.token === "string" ? cfg.token : undefined;
241
+ const to = cfg.status;
242
+ const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
243
+ if (!task || !owner || !token || !isTeamTaskStatus(to)) return result("API transition-task-status requires config.taskId, config.owner, config.token, and valid config.status.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
244
+ if (!canTransitionTaskStatus(task.status, to)) return result(`Invalid task status transition: ${task.status} -> ${to}`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
245
+ try {
246
+ return withRunLockSync(loaded.manifest, () => {
247
+ const updatedTask = transitionClaimedTaskStatus(task, owner, token, to);
248
+ const tasks = loaded.tasks.map((item) => item.id === task.id ? updatedTask : item);
249
+ saveRunTasks(loaded.manifest, tasks);
250
+ appendEvent(loaded.manifest.eventsPath, { type: "task.status_transitioned", runId: loaded.manifest.runId, taskId: task.id, data: { owner, status: to } });
251
+ return result(JSON.stringify(updatedTask, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
252
+ });
253
+ } catch (error) {
254
+ const message = error instanceof Error ? error.message : String(error);
255
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
256
+ }
257
+ }
258
+ if (operation === "write-heartbeat") {
259
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
260
+ const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
261
+ if (!task) return result("API write-heartbeat requires config.taskId matching a task id or step id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
262
+ try {
263
+ return withRunLockSync(loaded.manifest, () => {
264
+ const heartbeat = touchWorkerHeartbeat(task.heartbeat ?? { workerId: task.id, lastSeenAt: new Date().toISOString() }, { alive: typeof cfg.alive === "boolean" ? cfg.alive : undefined });
265
+ const tasks = loaded.tasks.map((item) => item.id === task.id ? { ...item, heartbeat } : item);
266
+ saveRunTasks(loaded.manifest, tasks);
267
+ appendEvent(loaded.manifest.eventsPath, { type: "worker.heartbeat", runId: loaded.manifest.runId, taskId: task.id, data: { ...heartbeat } });
268
+ return result(JSON.stringify(heartbeat, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
269
+ });
270
+ } catch (error) {
271
+ const message = error instanceof Error ? error.message : String(error);
272
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
273
+ }
274
+ }
275
+ return result(`Unknown API operation: ${operation}`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
276
+ }
@@ -0,0 +1,36 @@
1
+ import { effectiveAutonomousConfig, parseConfig, type PiTeamsAutonomousConfig, type PiTeamsConfig } from "../../config/config.ts";
2
+
3
+ export function autonomousPatchFromConfig(config: unknown): PiTeamsAutonomousConfig {
4
+ const rootPatch = parseConfig(config).autonomous;
5
+ if (rootPatch) return rootPatch;
6
+ return parseConfig({ autonomous: config }).autonomous ?? {};
7
+ }
8
+
9
+ export function configPatchFromConfig(config: unknown): PiTeamsConfig {
10
+ return parseConfig(config);
11
+ }
12
+
13
+ export function effectiveRunConfig(base: PiTeamsConfig, rawOverride: unknown): PiTeamsConfig {
14
+ const patch = parseConfig(rawOverride);
15
+ return {
16
+ ...base,
17
+ ...patch,
18
+ limits: patch.limits ? { ...(base.limits ?? {}), ...patch.limits } : base.limits,
19
+ runtime: patch.runtime ? { ...(base.runtime ?? {}), ...patch.runtime } : base.runtime,
20
+ control: patch.control ? { ...(base.control ?? {}), ...patch.control } : base.control,
21
+ worktree: patch.worktree ? { ...(base.worktree ?? {}), ...patch.worktree } : base.worktree,
22
+ };
23
+ }
24
+
25
+ export function formatAutonomyStatus(config: PiTeamsAutonomousConfig | undefined, pathValue: string, updated: boolean): string {
26
+ const effective = effectiveAutonomousConfig(config);
27
+ return [
28
+ updated ? "Updated pi-crew autonomous mode." : "pi-crew autonomous mode:",
29
+ `Path: ${pathValue}`,
30
+ `Profile: ${effective.profile}`,
31
+ `Enabled: ${effective.enabled}`,
32
+ `Inject policy: ${effective.injectPolicy}`,
33
+ `Prefer async for long tasks: ${effective.preferAsyncForLongTasks}`,
34
+ `Allow worktree suggestion: ${effective.allowWorktreeSuggestion}`,
35
+ ].join("\n");
36
+ }