@h-rig/server 0.0.6-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +14 -0
  2. package/dist/src/bootstrap.js +161 -0
  3. package/dist/src/index.js +13153 -0
  4. package/dist/src/inspector/agent-runtime.js +1077 -0
  5. package/dist/src/inspector/analysis.js +41 -0
  6. package/dist/src/inspector/discovery.js +137 -0
  7. package/dist/src/inspector/journal.js +518 -0
  8. package/dist/src/inspector/mission.js +562 -0
  9. package/dist/src/inspector/prompt.js +97 -0
  10. package/dist/src/inspector/provider-session.js +65 -0
  11. package/dist/src/inspector/reconcile.js +118 -0
  12. package/dist/src/inspector/review.js +13 -0
  13. package/dist/src/inspector/service.js +1759 -0
  14. package/dist/src/inspector/skills.js +155 -0
  15. package/dist/src/inspector/tools.js +1592 -0
  16. package/dist/src/inspector/types.js +1 -0
  17. package/dist/src/inspector/upstream-sync.js +479 -0
  18. package/dist/src/orchestration.js +402 -0
  19. package/dist/src/remote.js +123 -0
  20. package/dist/src/scheduler.js +84 -0
  21. package/dist/src/server-helpers/broadcasters.js +161 -0
  22. package/dist/src/server-helpers/conversation-snapshot.js +382 -0
  23. package/dist/src/server-helpers/event-emitter.js +41 -0
  24. package/dist/src/server-helpers/github-auth-store.js +155 -0
  25. package/dist/src/server-helpers/github-credentials.js +38 -0
  26. package/dist/src/server-helpers/github-project-status-sync.js +196 -0
  27. package/dist/src/server-helpers/github-projects.js +147 -0
  28. package/dist/src/server-helpers/github-reconciler.js +89 -0
  29. package/dist/src/server-helpers/http-router.js +3781 -0
  30. package/dist/src/server-helpers/http-utils.js +135 -0
  31. package/dist/src/server-helpers/inspector-agent-lifecycle.js +104 -0
  32. package/dist/src/server-helpers/inspector-jobs.js +4145 -0
  33. package/dist/src/server-helpers/issue-analysis.js +362 -0
  34. package/dist/src/server-helpers/normalizers.js +31 -0
  35. package/dist/src/server-helpers/notifications.js +96 -0
  36. package/dist/src/server-helpers/orchestration-ops.js +287 -0
  37. package/dist/src/server-helpers/orchestration.js +39 -0
  38. package/dist/src/server-helpers/plugin-host-cache.js +86 -0
  39. package/dist/src/server-helpers/project-fs-ops.js +194 -0
  40. package/dist/src/server-helpers/project-registry.js +124 -0
  41. package/dist/src/server-helpers/queue-state.js +78 -0
  42. package/dist/src/server-helpers/remote-checkout.js +140 -0
  43. package/dist/src/server-helpers/remote-snapshots.js +119 -0
  44. package/dist/src/server-helpers/run-io.js +262 -0
  45. package/dist/src/server-helpers/run-mutations.js +1784 -0
  46. package/dist/src/server-helpers/run-steering.js +176 -0
  47. package/dist/src/server-helpers/run-writers.js +75 -0
  48. package/dist/src/server-helpers/server-paths.js +27 -0
  49. package/dist/src/server-helpers/snapshot-orchestrator.js +832 -0
  50. package/dist/src/server-helpers/snapshot-service.js +1143 -0
  51. package/dist/src/server-helpers/summaries.js +126 -0
  52. package/dist/src/server-helpers/task-config.js +50 -0
  53. package/dist/src/server-helpers/task-projection.js +98 -0
  54. package/dist/src/server-helpers/terminal-runtime.js +156 -0
  55. package/dist/src/server-helpers/terminal-sessions.js +22 -0
  56. package/dist/src/server-helpers/validation-failure.js +31 -0
  57. package/dist/src/server-helpers/ws-router.js +1308 -0
  58. package/dist/src/server.js +12628 -0
  59. package/dist/src/websocket.js +63 -0
  60. package/package.json +33 -0
@@ -0,0 +1,126 @@
1
+ // @bun
2
+ // packages/server/src/server-helpers/normalizers.ts
3
+ function normalizeString(value) {
4
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
5
+ }
6
+
7
+ // packages/server/src/server-helpers/summaries.ts
8
+ function toTaskSummary(workspaceId, task) {
9
+ const createdAt = task.createdAt ?? task.updatedAt ?? new Date().toISOString();
10
+ const updatedAt = task.updatedAt ?? task.createdAt ?? createdAt;
11
+ const description = task.description ?? task.acceptanceCriteria ?? "";
12
+ return {
13
+ id: task.id,
14
+ workspaceId,
15
+ graphId: null,
16
+ externalId: task.externalRef,
17
+ title: task.title,
18
+ description,
19
+ status: task.status,
20
+ priority: task.priority,
21
+ role: task.role,
22
+ scope: task.scope,
23
+ validationKeys: task.validation,
24
+ sourceIssueId: task.sourceIssueId,
25
+ dependencies: task.dependencies,
26
+ parentChildDeps: task.parentChildDeps,
27
+ metadata: {
28
+ acceptanceCriteria: task.acceptanceCriteria,
29
+ issueType: task.issueType,
30
+ sourceIssueId: task.sourceIssueId,
31
+ ...task.sourceStatus ? { sourceStatus: task.sourceStatus } : {},
32
+ dependencies: task.dependencies,
33
+ parentChildDeps: task.parentChildDeps,
34
+ labels: task.labels
35
+ },
36
+ createdAt,
37
+ updatedAt
38
+ };
39
+ }
40
+ function buildWorkspaceGraphSummary(workspace, tasks) {
41
+ const timestamps = [
42
+ workspace.updatedAt,
43
+ ...tasks.map((task) => task.updatedAt),
44
+ ...tasks.map((task) => task.createdAt)
45
+ ].filter((value) => typeof value === "string" && value.length > 0);
46
+ const updatedAt = timestamps.toSorted().at(-1) ?? workspace.updatedAt;
47
+ const taskCount = tasks.length;
48
+ return {
49
+ id: `graph:${workspace.id}`,
50
+ workspaceId: workspace.id,
51
+ title: `${workspace.title} graph`,
52
+ description: taskCount > 0 ? `${taskCount} imported task${taskCount === 1 ? "" : "s"} from the connected Rig workspace` : "No imported tasks are available yet for this workspace",
53
+ createdAt: workspace.createdAt,
54
+ updatedAt
55
+ };
56
+ }
57
+ function toApprovalSummary(record) {
58
+ const createdAt = normalizeString(record.record.createdAt) ?? normalizeString(record.record.requestedAt) ?? new Date().toISOString();
59
+ const resolvedAt = normalizeString(record.record.resolvedAt) ?? normalizeString(record.record.respondedAt) ?? null;
60
+ return {
61
+ id: record.requestId ?? `${record.runId}-approval-${createdAt}`,
62
+ runId: record.runId,
63
+ actionId: normalizeString(record.record.actionId),
64
+ requestKind: normalizeString(record.record.requestKind) ?? normalizeString(record.record.kind) ?? "approval",
65
+ status: record.status === "resolved" ? "resolved" : "pending",
66
+ payload: record.record,
67
+ createdAt,
68
+ resolvedAt
69
+ };
70
+ }
71
+ function toUserInputSummary(record) {
72
+ const createdAt = normalizeString(record.record.createdAt) ?? normalizeString(record.record.requestedAt) ?? new Date().toISOString();
73
+ const resolvedAt = normalizeString(record.record.resolvedAt) ?? normalizeString(record.record.respondedAt) ?? null;
74
+ return {
75
+ id: record.requestId ?? `${record.runId}-input-${createdAt}`,
76
+ runId: record.runId,
77
+ status: record.status === "resolved" ? "resolved" : "pending",
78
+ payload: record.record,
79
+ createdAt,
80
+ resolvedAt
81
+ };
82
+ }
83
+ function toRunSummary(run, approvals, userInputs) {
84
+ const pendingApprovalCount = approvals.filter((entry) => entry.status !== "resolved").length;
85
+ const pendingUserInputCount = userInputs.filter((entry) => entry.status !== "resolved").length;
86
+ const runStatus = pendingApprovalCount > 0 ? "waiting-approval" : pendingUserInputCount > 0 ? "waiting-user-input" : run.status === "waiting" ? "queued" : run.status === "preparing" ? "preparing" : run.status === "stopped" ? "cancelled" : run.status === "done" || run.status === "completed" ? "completed" : run.status === "error" || run.status === "failed" ? "failed" : run.status === "running" ? "running" : "created";
87
+ const runMode = normalizeString(run.runMode) === "autonomous" ? "autonomous" : normalizeString(run.runMode) === "supervised" ? "supervised" : "interactive";
88
+ const runtimeMode = normalizeString(run.runtimeMode) ?? "approval-required";
89
+ const interactionMode = normalizeString(run.interactionMode) ?? "default";
90
+ return {
91
+ id: run.runId,
92
+ workspaceId: run.workspaceId,
93
+ taskId: run.taskId,
94
+ title: run.title,
95
+ runKind: run.taskId ? "task" : "adhoc",
96
+ mode: runMode,
97
+ runtimeMode,
98
+ interactionMode,
99
+ status: runStatus,
100
+ runtimeAdapter: run.runtimeAdapter,
101
+ model: normalizeString(run.model),
102
+ initialPrompt: normalizeString(run.initialPrompt),
103
+ executionTarget: run.mode === "remote" ? "remote" : "local",
104
+ remoteHostId: run.hostId,
105
+ remoteLeaseId: run.endpointId,
106
+ remoteLeaseClaimedAt: null,
107
+ activeRuntimeId: null,
108
+ latestMessageId: normalizeString(run.latestMessageId),
109
+ pendingApprovalCount,
110
+ pendingUserInputCount,
111
+ branch: normalizeString(run.branch),
112
+ worktreePath: run.worktreePath,
113
+ errorText: normalizeString(run.errorText),
114
+ createdAt: run.createdAt,
115
+ updatedAt: run.updatedAt,
116
+ startedAt: run.startedAt,
117
+ completedAt: run.completedAt
118
+ };
119
+ }
120
+ export {
121
+ toUserInputSummary,
122
+ toTaskSummary,
123
+ toRunSummary,
124
+ toApprovalSummary,
125
+ buildWorkspaceGraphSummary
126
+ };
@@ -0,0 +1,50 @@
1
+ // @bun
2
+ // packages/server/src/server-helpers/task-config.ts
3
+ import { existsSync as existsSync2 } from "fs";
4
+
5
+ // packages/server/src/bootstrap.ts
6
+ import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "fs";
7
+ import { dirname, resolve } from "path";
8
+ import { RIG_DEFINITION_DIRNAME, resolveMonorepoRoot } from "@rig/runtime";
9
+ function normalizeOptionalString(value) {
10
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
11
+ }
12
+ function resolveRigServerPaths(projectRoot) {
13
+ const taskWorkspace = normalizeOptionalString(process.env.RIG_TASK_WORKSPACE);
14
+ const explicitStateDir = normalizeOptionalString(process.env.RIG_STATE_DIR);
15
+ const explicitLogsDir = normalizeOptionalString(process.env.RIG_LOGS_DIR);
16
+ const explicitSessionFile = normalizeOptionalString(process.env.RIG_SESSION_FILE);
17
+ const hostStateRoot = resolve(projectRoot, ".rig");
18
+ const monorepoRoot = resolveMonorepoRoot(projectRoot);
19
+ const monorepoStateRoot = resolve(monorepoRoot, ".rig");
20
+ const stateRoot = taskWorkspace ? resolve(taskWorkspace, ".rig") : explicitStateDir ? dirname(resolve(explicitStateDir)) : explicitLogsDir ? dirname(resolve(explicitLogsDir)) : explicitSessionFile ? dirname(dirname(resolve(explicitSessionFile))) : existsSync(hostStateRoot) ? hostStateRoot : monorepoStateRoot;
21
+ const stateDir = explicitStateDir ? resolve(explicitStateDir) : resolve(stateRoot, "state");
22
+ const logsDir = explicitLogsDir ? resolve(explicitLogsDir) : resolve(stateRoot, "logs");
23
+ const taskConfigPath = taskWorkspace ? resolve(taskWorkspace, ".rig", "task-config.json") : existsSync(resolve(projectRoot, ".rig", "task-config.json")) ? resolve(projectRoot, ".rig", "task-config.json") : resolve(monorepoStateRoot, "task-config.json");
24
+ return {
25
+ stateRoot,
26
+ stateDir,
27
+ logsDir,
28
+ controlPlaneEventsFile: resolve(logsDir, "control-plane.events.jsonl"),
29
+ taskConfigPath,
30
+ notificationsFile: resolve(projectRoot, "rig", "notifications", "targets.json"),
31
+ keybindingsPath: resolve(projectRoot, "rig", "keybindings.json")
32
+ };
33
+ }
34
+
35
+ // packages/server/src/server-helpers/task-config.ts
36
+ async function readTaskConfig(projectRoot) {
37
+ const taskConfigPath = resolveRigServerPaths(projectRoot).taskConfigPath;
38
+ if (!existsSync2(taskConfigPath)) {
39
+ return {};
40
+ }
41
+ try {
42
+ const parsed = await Bun.file(taskConfigPath).json();
43
+ return parsed ?? {};
44
+ } catch {
45
+ return {};
46
+ }
47
+ }
48
+ export {
49
+ readTaskConfig
50
+ };
@@ -0,0 +1,98 @@
1
+ // @bun
2
+ // packages/server/src/server-helpers/task-projection.ts
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
+ import { resolve } from "path";
5
+ function projectionPath(projectRoot) {
6
+ return resolve(projectRoot, ".rig", "state", "task-projection.json");
7
+ }
8
+ function stateDir(projectRoot) {
9
+ return resolve(projectRoot, ".rig", "state");
10
+ }
11
+ function cloneTask(task) {
12
+ return { ...task };
13
+ }
14
+ function writeTaskProjection(projectRoot, input) {
15
+ const activeByTask = new Map((input.activeRuns ?? []).map((run) => [run.taskId, run]));
16
+ const tasks = input.tasks.map((task) => {
17
+ const projected = cloneTask(task);
18
+ const active = activeByTask.get(task.id);
19
+ if (active) {
20
+ projected.status = "in_progress";
21
+ projected.activeRun = {
22
+ runId: active.runId,
23
+ ...active.status ? { status: active.status } : {},
24
+ ...active.stage ? { stage: active.stage } : {}
25
+ };
26
+ }
27
+ return projected;
28
+ });
29
+ const snapshot = {
30
+ version: 1,
31
+ source: input.source,
32
+ reason: input.reason,
33
+ refreshedAt: input.refreshedAt ?? new Date().toISOString(),
34
+ tasks
35
+ };
36
+ mkdirSync(stateDir(projectRoot), { recursive: true });
37
+ writeFileSync(projectionPath(projectRoot), JSON.stringify(snapshot, null, 2), "utf8");
38
+ return snapshot;
39
+ }
40
+ function readTaskProjection(projectRoot) {
41
+ const file = projectionPath(projectRoot);
42
+ if (!existsSync(file))
43
+ return null;
44
+ try {
45
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
46
+ return parsed && parsed.version === 1 && Array.isArray(parsed.tasks) ? parsed : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+ async function refreshTaskProjection(projectRoot, input) {
52
+ const tasks = typeof input.tasks === "function" ? await input.tasks() : input.tasks;
53
+ const activeRuns = typeof input.activeRuns === "function" ? await input.activeRuns() : input.activeRuns;
54
+ return writeTaskProjection(projectRoot, {
55
+ source: input.source,
56
+ reason: input.reason ?? "refresh",
57
+ tasks,
58
+ activeRuns
59
+ });
60
+ }
61
+ function taskAssignees(task) {
62
+ const raw = task.assignees;
63
+ if (!Array.isArray(raw))
64
+ return [];
65
+ return raw.flatMap((value) => {
66
+ if (typeof value === "string")
67
+ return [value];
68
+ if (value && typeof value === "object" && typeof value.login === "string")
69
+ return [value.login];
70
+ return [];
71
+ });
72
+ }
73
+ function filterProjectedTasks(tasks, filter) {
74
+ const status = filter.status?.trim();
75
+ const assignedTo = filter.assignedTo?.trim();
76
+ const search = filter.search?.trim().toLowerCase();
77
+ return tasks.filter((task) => {
78
+ if (status && task.status !== status)
79
+ return false;
80
+ if (assignedTo && !taskAssignees(task).includes(assignedTo))
81
+ return false;
82
+ if (search) {
83
+ const title = typeof task.title === "string" ? task.title : "";
84
+ const body = typeof task.body === "string" ? task.body : "";
85
+ if (!`${task.id}
86
+ ${title}
87
+ ${body}`.toLowerCase().includes(search))
88
+ return false;
89
+ }
90
+ return true;
91
+ });
92
+ }
93
+ export {
94
+ writeTaskProjection,
95
+ refreshTaskProjection,
96
+ readTaskProjection,
97
+ filterProjectedTasks
98
+ };
@@ -0,0 +1,156 @@
1
+ // @bun
2
+ // packages/server/src/server-helpers/terminal-runtime.ts
3
+ import { spawn } from "child_process";
4
+ import { WS_CHANNELS as WS_CHANNELS2 } from "@rig/contracts";
5
+
6
+ // packages/server/src/server-helpers/broadcasters.ts
7
+ import { RIG_WS_CHANNELS } from "@rig/contracts";
8
+
9
+ // packages/server/src/websocket.ts
10
+ import {
11
+ WS_CHANNELS
12
+ } from "@rig/contracts";
13
+ function encodeWebSocketPayload(payload) {
14
+ return JSON.stringify(payload);
15
+ }
16
+
17
+ // packages/server/src/server-helpers/run-writers.ts
18
+ import {
19
+ appendJsonlRecord,
20
+ readAuthorityRun as readAuthorityRun2,
21
+ resolveAuthorityRunDir as resolveAuthorityRunDir2,
22
+ writeJsonFile
23
+ } from "@rig/runtime/control-plane/authority-files";
24
+
25
+ // packages/server/src/server-helpers/run-io.ts
26
+ import {
27
+ listAuthorityRuns,
28
+ readAuthorityRun,
29
+ readJsonlFile,
30
+ resolveAuthorityRunDir
31
+ } from "@rig/runtime/control-plane/authority-files";
32
+ var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
33
+
34
+ // packages/server/src/server-helpers/remote-snapshots.ts
35
+ import { listAuthorityRemoteEndpoints } from "@rig/runtime/control-plane/authority-files";
36
+
37
+ // packages/server/src/server-helpers/broadcasters.ts
38
+ function broadcastPush(state, payload) {
39
+ const encoded = encodeWebSocketPayload(payload);
40
+ for (const socket of state.sockets) {
41
+ try {
42
+ socket.send(encoded);
43
+ } catch {
44
+ state.sockets.delete(socket);
45
+ }
46
+ }
47
+ }
48
+
49
+ // packages/server/src/server-helpers/terminal-sessions.ts
50
+ function terminalSessionKey(threadId, terminalId) {
51
+ return `${threadId}::${terminalId}`;
52
+ }
53
+ function terminalSnapshot(session) {
54
+ return {
55
+ threadId: session.threadId,
56
+ terminalId: session.terminalId,
57
+ cwd: session.cwd,
58
+ status: session.status,
59
+ pid: session.pid,
60
+ history: session.history,
61
+ exitCode: session.exitCode,
62
+ exitSignal: session.exitSignal,
63
+ updatedAt: session.updatedAt
64
+ };
65
+ }
66
+
67
+ // packages/server/src/server-helpers/terminal-runtime.ts
68
+ var DEFAULT_TERMINAL_SHELL = process.env.SHELL || "/bin/zsh";
69
+ function pushTerminalEvent(state, event) {
70
+ broadcastPush(state, {
71
+ type: "push",
72
+ channel: WS_CHANNELS2.terminalEvent,
73
+ data: event
74
+ });
75
+ }
76
+ function createTerminalSession(state, input) {
77
+ const startedAt = new Date().toISOString();
78
+ const child = spawn(DEFAULT_TERMINAL_SHELL, [], {
79
+ cwd: input.cwd,
80
+ env: { ...process.env, ...input.env ?? {} },
81
+ stdio: "pipe"
82
+ });
83
+ const session = {
84
+ threadId: input.threadId,
85
+ terminalId: input.terminalId,
86
+ cwd: input.cwd,
87
+ history: "",
88
+ status: "running",
89
+ pid: child.pid ?? null,
90
+ exitCode: null,
91
+ exitSignal: null,
92
+ updatedAt: startedAt,
93
+ child
94
+ };
95
+ const sessionKey = terminalSessionKey(input.threadId, input.terminalId);
96
+ state.terminalSessions.set(sessionKey, session);
97
+ const appendOutput = (data) => {
98
+ const chunk = Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
99
+ session.history += chunk;
100
+ session.updatedAt = new Date().toISOString();
101
+ pushTerminalEvent(state, {
102
+ type: "output",
103
+ threadId: session.threadId,
104
+ terminalId: session.terminalId,
105
+ createdAt: session.updatedAt,
106
+ data: chunk
107
+ });
108
+ };
109
+ child.stdout.on("data", appendOutput);
110
+ child.stderr.on("data", appendOutput);
111
+ child.on("exit", (code, signal) => {
112
+ session.status = "exited";
113
+ session.exitCode = typeof code === "number" ? code : null;
114
+ session.exitSignal = signal ?? null;
115
+ session.updatedAt = new Date().toISOString();
116
+ pushTerminalEvent(state, {
117
+ type: "exited",
118
+ threadId: session.threadId,
119
+ terminalId: session.terminalId,
120
+ createdAt: session.updatedAt,
121
+ exitCode: session.exitCode,
122
+ exitSignal: session.exitSignal
123
+ });
124
+ });
125
+ child.on("error", (error) => {
126
+ session.status = "error";
127
+ session.updatedAt = new Date().toISOString();
128
+ pushTerminalEvent(state, {
129
+ type: "error",
130
+ threadId: session.threadId,
131
+ terminalId: session.terminalId,
132
+ createdAt: session.updatedAt,
133
+ message: error.message
134
+ });
135
+ });
136
+ pushTerminalEvent(state, {
137
+ type: "started",
138
+ threadId: session.threadId,
139
+ terminalId: session.terminalId,
140
+ createdAt: startedAt,
141
+ snapshot: terminalSnapshot(session)
142
+ });
143
+ return session;
144
+ }
145
+ function getOrCreateTerminalSession(state, input) {
146
+ const existing = state.terminalSessions.get(terminalSessionKey(input.threadId, input.terminalId));
147
+ if (existing && existing.status !== "exited" && existing.status !== "error") {
148
+ return existing;
149
+ }
150
+ return createTerminalSession(state, input);
151
+ }
152
+ export {
153
+ pushTerminalEvent,
154
+ getOrCreateTerminalSession,
155
+ createTerminalSession
156
+ };
@@ -0,0 +1,22 @@
1
+ // @bun
2
+ // packages/server/src/server-helpers/terminal-sessions.ts
3
+ function terminalSessionKey(threadId, terminalId) {
4
+ return `${threadId}::${terminalId}`;
5
+ }
6
+ function terminalSnapshot(session) {
7
+ return {
8
+ threadId: session.threadId,
9
+ terminalId: session.terminalId,
10
+ cwd: session.cwd,
11
+ status: session.status,
12
+ pid: session.pid,
13
+ history: session.history,
14
+ exitCode: session.exitCode,
15
+ exitSignal: session.exitSignal,
16
+ updatedAt: session.updatedAt
17
+ };
18
+ }
19
+ export {
20
+ terminalSnapshot,
21
+ terminalSessionKey
22
+ };
@@ -0,0 +1,31 @@
1
+ // @bun
2
+ // packages/server/src/server-helpers/validation-failure.ts
3
+ import { resolve } from "path";
4
+ import {
5
+ readJsonFile,
6
+ resolveTaskArtifactDirs
7
+ } from "@rig/runtime/control-plane/authority-files";
8
+ function summarizeRunValidationFailure(projectRoot, run) {
9
+ const artifactRoots = run.taskId ? resolveTaskArtifactDirs(projectRoot, run.taskId) : [];
10
+ const preferredRoots = run.artifactRoot ? [run.artifactRoot, ...artifactRoots] : artifactRoots;
11
+ const seen = new Set;
12
+ for (const artifactRoot of preferredRoots) {
13
+ if (!artifactRoot || seen.has(artifactRoot)) {
14
+ continue;
15
+ }
16
+ seen.add(artifactRoot);
17
+ const summary = readJsonFile(resolve(artifactRoot, "validation-summary.json"), null);
18
+ if (!summary || summary.status !== "fail") {
19
+ continue;
20
+ }
21
+ const failedCategories = Array.isArray(summary.categories) ? summary.categories.filter((entry) => String(entry?.status ?? "") !== "pass").map((entry) => String(entry?.category ?? "").trim()).filter(Boolean) : [];
22
+ if (failedCategories.length > 0) {
23
+ return `Task validation failed: ${failedCategories.join(", ")}`;
24
+ }
25
+ return "Task validation failed.";
26
+ }
27
+ return null;
28
+ }
29
+ export {
30
+ summarizeRunValidationFailure
31
+ };