@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,1143 @@
1
+ // @bun
2
+ var __require = import.meta.require;
3
+
4
+ // packages/server/src/server-helpers/snapshot-service.ts
5
+ import { listAgentRuntimes } from "@rig/runtime/control-plane/runtime/isolation";
6
+
7
+ // packages/server/src/server-helpers/snapshot-orchestrator.ts
8
+ import { dirname as dirname4, resolve as resolve5 } from "path";
9
+ import {
10
+ listAuthorityRuns as listAuthorityRuns3,
11
+ readAuthorityRun as readAuthorityRun2
12
+ } from "@rig/runtime/control-plane/authority-files";
13
+
14
+ // packages/server/src/server-helpers/normalizers.ts
15
+ function normalizeString(value) {
16
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
17
+ }
18
+ function normalizeSequence(value) {
19
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value, 10) : NaN;
20
+ return Number.isFinite(parsed) ? parsed : null;
21
+ }
22
+
23
+ // packages/server/src/server-helpers/summaries.ts
24
+ function toTaskSummary(workspaceId, task) {
25
+ const createdAt = task.createdAt ?? task.updatedAt ?? new Date().toISOString();
26
+ const updatedAt = task.updatedAt ?? task.createdAt ?? createdAt;
27
+ const description = task.description ?? task.acceptanceCriteria ?? "";
28
+ return {
29
+ id: task.id,
30
+ workspaceId,
31
+ graphId: null,
32
+ externalId: task.externalRef,
33
+ title: task.title,
34
+ description,
35
+ status: task.status,
36
+ priority: task.priority,
37
+ role: task.role,
38
+ scope: task.scope,
39
+ validationKeys: task.validation,
40
+ sourceIssueId: task.sourceIssueId,
41
+ dependencies: task.dependencies,
42
+ parentChildDeps: task.parentChildDeps,
43
+ metadata: {
44
+ acceptanceCriteria: task.acceptanceCriteria,
45
+ issueType: task.issueType,
46
+ sourceIssueId: task.sourceIssueId,
47
+ ...task.sourceStatus ? { sourceStatus: task.sourceStatus } : {},
48
+ dependencies: task.dependencies,
49
+ parentChildDeps: task.parentChildDeps,
50
+ labels: task.labels
51
+ },
52
+ createdAt,
53
+ updatedAt
54
+ };
55
+ }
56
+ function buildWorkspaceGraphSummary(workspace, tasks) {
57
+ const timestamps = [
58
+ workspace.updatedAt,
59
+ ...tasks.map((task) => task.updatedAt),
60
+ ...tasks.map((task) => task.createdAt)
61
+ ].filter((value) => typeof value === "string" && value.length > 0);
62
+ const updatedAt = timestamps.toSorted().at(-1) ?? workspace.updatedAt;
63
+ const taskCount = tasks.length;
64
+ return {
65
+ id: `graph:${workspace.id}`,
66
+ workspaceId: workspace.id,
67
+ title: `${workspace.title} graph`,
68
+ description: taskCount > 0 ? `${taskCount} imported task${taskCount === 1 ? "" : "s"} from the connected Rig workspace` : "No imported tasks are available yet for this workspace",
69
+ createdAt: workspace.createdAt,
70
+ updatedAt
71
+ };
72
+ }
73
+ function toApprovalSummary(record) {
74
+ const createdAt = normalizeString(record.record.createdAt) ?? normalizeString(record.record.requestedAt) ?? new Date().toISOString();
75
+ const resolvedAt = normalizeString(record.record.resolvedAt) ?? normalizeString(record.record.respondedAt) ?? null;
76
+ return {
77
+ id: record.requestId ?? `${record.runId}-approval-${createdAt}`,
78
+ runId: record.runId,
79
+ actionId: normalizeString(record.record.actionId),
80
+ requestKind: normalizeString(record.record.requestKind) ?? normalizeString(record.record.kind) ?? "approval",
81
+ status: record.status === "resolved" ? "resolved" : "pending",
82
+ payload: record.record,
83
+ createdAt,
84
+ resolvedAt
85
+ };
86
+ }
87
+ function toUserInputSummary(record) {
88
+ const createdAt = normalizeString(record.record.createdAt) ?? normalizeString(record.record.requestedAt) ?? new Date().toISOString();
89
+ const resolvedAt = normalizeString(record.record.resolvedAt) ?? normalizeString(record.record.respondedAt) ?? null;
90
+ return {
91
+ id: record.requestId ?? `${record.runId}-input-${createdAt}`,
92
+ runId: record.runId,
93
+ status: record.status === "resolved" ? "resolved" : "pending",
94
+ payload: record.record,
95
+ createdAt,
96
+ resolvedAt
97
+ };
98
+ }
99
+ function toRunSummary(run, approvals, userInputs) {
100
+ const pendingApprovalCount = approvals.filter((entry) => entry.status !== "resolved").length;
101
+ const pendingUserInputCount = userInputs.filter((entry) => entry.status !== "resolved").length;
102
+ 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";
103
+ const runMode = normalizeString(run.runMode) === "autonomous" ? "autonomous" : normalizeString(run.runMode) === "supervised" ? "supervised" : "interactive";
104
+ const runtimeMode = normalizeString(run.runtimeMode) ?? "approval-required";
105
+ const interactionMode = normalizeString(run.interactionMode) ?? "default";
106
+ return {
107
+ id: run.runId,
108
+ workspaceId: run.workspaceId,
109
+ taskId: run.taskId,
110
+ title: run.title,
111
+ runKind: run.taskId ? "task" : "adhoc",
112
+ mode: runMode,
113
+ runtimeMode,
114
+ interactionMode,
115
+ status: runStatus,
116
+ runtimeAdapter: run.runtimeAdapter,
117
+ model: normalizeString(run.model),
118
+ initialPrompt: normalizeString(run.initialPrompt),
119
+ executionTarget: run.mode === "remote" ? "remote" : "local",
120
+ remoteHostId: run.hostId,
121
+ remoteLeaseId: run.endpointId,
122
+ remoteLeaseClaimedAt: null,
123
+ activeRuntimeId: null,
124
+ latestMessageId: normalizeString(run.latestMessageId),
125
+ pendingApprovalCount,
126
+ pendingUserInputCount,
127
+ branch: normalizeString(run.branch),
128
+ worktreePath: run.worktreePath,
129
+ errorText: normalizeString(run.errorText),
130
+ createdAt: run.createdAt,
131
+ updatedAt: run.updatedAt,
132
+ startedAt: run.startedAt,
133
+ completedAt: run.completedAt
134
+ };
135
+ }
136
+
137
+ // packages/server/src/server-helpers/run-io.ts
138
+ import { dirname, resolve } from "path";
139
+ import { closeSync, existsSync, mkdirSync, openSync, readSync, statSync, writeFileSync } from "fs";
140
+ import {
141
+ listAuthorityRuns,
142
+ readAuthorityRun,
143
+ readJsonlFile,
144
+ resolveAuthorityRunDir
145
+ } from "@rig/runtime/control-plane/authority-files";
146
+ function matchesRunFilter(entry, filter) {
147
+ return (!filter.runId || entry.runId === filter.runId) && (!filter.taskId || entry.taskId === filter.taskId);
148
+ }
149
+ function readApprovalsForRuns(projectRoot, runs, filter = {}) {
150
+ return runs.filter((entry) => matchesRunFilter(entry, filter)).flatMap((entry) => readJsonlFile(resolve(resolveAuthorityRunDir(projectRoot, entry.runId), "approvals.jsonl")).flatMap((record) => {
151
+ if (!record || typeof record !== "object") {
152
+ return [];
153
+ }
154
+ const value = record;
155
+ return [
156
+ {
157
+ runId: entry.runId,
158
+ taskId: entry.taskId,
159
+ requestId: normalizeString(value.requestId) ?? normalizeString(value.id),
160
+ status: normalizeString(value.status),
161
+ record: value
162
+ }
163
+ ];
164
+ }));
165
+ }
166
+ function readUserInputsForRuns(projectRoot, runs, filter = {}) {
167
+ return runs.filter((entry) => matchesRunFilter(entry, filter)).flatMap((entry) => readJsonlFile(resolve(resolveAuthorityRunDir(projectRoot, entry.runId), "user-input.jsonl")).flatMap((record) => {
168
+ if (!record || typeof record !== "object") {
169
+ return [];
170
+ }
171
+ const value = record;
172
+ return [
173
+ {
174
+ runId: entry.runId,
175
+ taskId: entry.taskId,
176
+ requestId: normalizeString(value.requestId) ?? normalizeString(value.id),
177
+ status: normalizeString(value.status),
178
+ record: value
179
+ }
180
+ ];
181
+ }));
182
+ }
183
+ function runTimelinePath(projectRoot, runId) {
184
+ return resolve(resolveAuthorityRunDir(projectRoot, runId), "timeline.jsonl");
185
+ }
186
+ function runLogsPath(projectRoot, runId) {
187
+ return resolve(resolveAuthorityRunDir(projectRoot, runId), "logs.jsonl");
188
+ }
189
+ function parseJsonlRecords(lines) {
190
+ return lines.map((line) => line.trim()).filter(Boolean).flatMap((line) => {
191
+ try {
192
+ return [JSON.parse(line)];
193
+ } catch {
194
+ return [];
195
+ }
196
+ });
197
+ }
198
+ function readJsonlFileTail(path, options) {
199
+ const limit = Math.max(0, Math.trunc(options.limit));
200
+ if (limit === 0 || !existsSync(path))
201
+ return [];
202
+ const maxBytes = Math.max(1024, Math.trunc(options.maxBytes ?? 256 * 1024));
203
+ const size = statSync(path).size;
204
+ if (size === 0)
205
+ return [];
206
+ const start = Math.max(0, size - maxBytes);
207
+ const length = size - start;
208
+ const buffer = Buffer.alloc(length);
209
+ const fd = openSync(path, "r");
210
+ try {
211
+ readSync(fd, buffer, 0, length, start);
212
+ } finally {
213
+ closeSync(fd);
214
+ }
215
+ const text = buffer.toString("utf8");
216
+ const lines = text.split(/\r?\n/);
217
+ const completeLines = start > 0 ? lines.slice(1) : lines;
218
+ return parseJsonlRecords(completeLines.filter(Boolean).slice(-limit));
219
+ }
220
+ var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
221
+
222
+ // packages/server/src/server-helpers/queue-state.ts
223
+ import { resolve as resolve3 } from "path";
224
+ import { readJsonFile, writeJsonFile } from "@rig/runtime/control-plane/authority-files";
225
+
226
+ // packages/server/src/server-helpers/server-paths.ts
227
+ import { dirname as dirname2, resolve as resolve2 } from "path";
228
+ import { resolveMonorepoRoot } from "@rig/runtime/control-plane/native/utils";
229
+ function resolveServerAuthorityPaths(projectRoot) {
230
+ const taskWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
231
+ const explicitStateDir = process.env.RIG_STATE_DIR?.trim();
232
+ const explicitLogsDir = process.env.RIG_LOGS_DIR?.trim();
233
+ const explicitSessionFile = process.env.RIG_SESSION_FILE?.trim();
234
+ const monorepoRoot = resolveMonorepoRoot(projectRoot);
235
+ const stateRoot = taskWorkspace ? resolve2(taskWorkspace, ".rig") : explicitStateDir ? dirname2(resolve2(explicitStateDir)) : explicitLogsDir ? dirname2(resolve2(explicitLogsDir)) : explicitSessionFile ? dirname2(dirname2(resolve2(explicitSessionFile))) : resolve2(monorepoRoot, ".rig");
236
+ const stateDir = explicitStateDir ? resolve2(explicitStateDir) : resolve2(stateRoot, "state");
237
+ const logsDir = explicitLogsDir ? resolve2(explicitLogsDir) : resolve2(stateRoot, "logs");
238
+ const sessionFile = explicitSessionFile ? resolve2(explicitSessionFile) : resolve2(stateRoot, "session", "session.json");
239
+ const artifactsDir = taskWorkspace ? resolve2(taskWorkspace, "artifacts") : resolve2(monorepoRoot, "artifacts");
240
+ return {
241
+ stateRoot,
242
+ stateDir,
243
+ logsDir,
244
+ controlPlaneEventsFile: resolve2(logsDir, "control-plane.events.jsonl"),
245
+ sessionFile,
246
+ artifactsDir
247
+ };
248
+ }
249
+
250
+ // packages/server/src/server-helpers/queue-state.ts
251
+ function resolveQueueStatePath(projectRoot) {
252
+ return resolve3(resolveServerAuthorityPaths(projectRoot).stateDir, "task-queue.json");
253
+ }
254
+ function readQueueState(projectRoot) {
255
+ const queue = readJsonFile(resolveQueueStatePath(projectRoot), null);
256
+ if (!Array.isArray(queue))
257
+ return [];
258
+ return queue.filter((entry) => entry && typeof entry === "object").map((entry, index) => ({
259
+ taskId: normalizeString(entry.taskId),
260
+ score: typeof entry.score === "number" ? Math.max(0, Math.trunc(entry.score)) : 0,
261
+ unblockCount: typeof entry.unblockCount === "number" ? Math.max(0, Math.trunc(entry.unblockCount)) : 0,
262
+ position: typeof entry.position === "number" ? Math.max(0, Math.trunc(entry.position)) : index
263
+ })).filter((entry) => Boolean(entry.taskId)).sort((left, right) => right.score - left.score || left.position - right.position).map((entry, index) => ({ ...entry, position: index }));
264
+ }
265
+
266
+ // packages/server/src/server-helpers/remote-snapshots.ts
267
+ import { listAuthorityRemoteEndpoints } from "@rig/runtime/control-plane/authority-files";
268
+ function toRemoteConnectionStatus(status) {
269
+ switch (status) {
270
+ case "connected":
271
+ case "connecting":
272
+ case "authenticating":
273
+ case "reconnecting":
274
+ case "error":
275
+ case "disconnected":
276
+ return status;
277
+ case "ready":
278
+ case "busy":
279
+ case "degraded":
280
+ case "draining":
281
+ return "connected";
282
+ case "registering":
283
+ return "connecting";
284
+ case "offline":
285
+ case "quarantined":
286
+ return "error";
287
+ default:
288
+ return "disconnected";
289
+ }
290
+ }
291
+ function hostToRemoteEndpoint(host) {
292
+ let hostName = "127.0.0.1";
293
+ let port = 7890;
294
+ try {
295
+ const url = new URL(host.baseUrl);
296
+ hostName = url.hostname || hostName;
297
+ port = Number.parseInt(url.port || "80", 10) || port;
298
+ } catch {}
299
+ return {
300
+ id: host.hostId,
301
+ alias: host.name,
302
+ host: hostName,
303
+ port,
304
+ token: "",
305
+ tokenConfigured: false,
306
+ autoConnect: true,
307
+ addedAt: host.registeredAt,
308
+ lastConnectedAt: host.lastHeartbeatAt
309
+ };
310
+ }
311
+ function buildRemoteEndpointSnapshot(projectRoot, remoteHosts) {
312
+ const authorityEndpoints = listAuthorityRemoteEndpoints(projectRoot).map((entry) => ({
313
+ id: entry.id,
314
+ alias: entry.alias,
315
+ host: entry.host,
316
+ port: entry.port,
317
+ token: "",
318
+ tokenConfigured: entry.token.trim().length > 0,
319
+ autoConnect: entry.autoConnect,
320
+ addedAt: entry.addedAt,
321
+ lastConnectedAt: entry.lastConnectedAt
322
+ }));
323
+ const hostEndpoints = Array.from(remoteHosts.values()).map(hostToRemoteEndpoint);
324
+ const deduped = new Map;
325
+ for (const entry of [...authorityEndpoints, ...hostEndpoints]) {
326
+ deduped.set(entry.id, entry);
327
+ }
328
+ return Array.from(deduped.values()).sort((left, right) => left.alias.localeCompare(right.alias));
329
+ }
330
+ function buildRemoteConnectionSnapshot(remoteHosts, remoteConnections, endpoints) {
331
+ const connections = new Map;
332
+ for (const [endpointId, summary] of remoteConnections.entries()) {
333
+ connections.set(endpointId, summary);
334
+ }
335
+ for (const host of remoteHosts.values()) {
336
+ if (connections.has(host.hostId))
337
+ continue;
338
+ connections.set(host.hostId, {
339
+ endpointId: host.hostId,
340
+ status: toRemoteConnectionStatus(host.status),
341
+ error: host.status === "offline" || host.status === "quarantined" ? host.status : null,
342
+ connectedAt: host.lastHeartbeatAt,
343
+ tokenExpiresAt: null,
344
+ latencyMs: null,
345
+ subscribedEvents: []
346
+ });
347
+ }
348
+ for (const endpoint of endpoints) {
349
+ if (connections.has(endpoint.id))
350
+ continue;
351
+ connections.set(endpoint.id, {
352
+ endpointId: endpoint.id,
353
+ status: "disconnected",
354
+ error: null,
355
+ connectedAt: endpoint.lastConnectedAt,
356
+ tokenExpiresAt: null,
357
+ latencyMs: null,
358
+ subscribedEvents: []
359
+ });
360
+ }
361
+ return Array.from(connections.values()).sort((left, right) => String(left.endpointId).localeCompare(String(right.endpointId)));
362
+ }
363
+ function normalizeIsoTimestamp(value) {
364
+ const normalized = normalizeString(value);
365
+ if (!normalized)
366
+ return null;
367
+ const timestamp = Date.parse(normalized);
368
+ return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
369
+ }
370
+
371
+ // packages/server/src/server-helpers/conversation-snapshot.ts
372
+ import { existsSync as existsSync2, readdirSync, statSync as statSync2 } from "fs";
373
+ import { dirname as dirname3, resolve as resolve4 } from "path";
374
+ import {
375
+ listAuthorityRuns as listAuthorityRuns2,
376
+ readJsonlFile as readJsonlFile2
377
+ } from "@rig/runtime/control-plane/authority-files";
378
+ function readClaudeSessionRecords(run) {
379
+ if (!run.worktreePath)
380
+ return [];
381
+ const runtimeRoot = dirname3(run.worktreePath);
382
+ const projectsRoot = resolve4(runtimeRoot, "home", ".claude", "projects");
383
+ if (!existsSync2(projectsRoot))
384
+ return [];
385
+ const candidatePaths = [];
386
+ const visit = (dirPath, depth) => {
387
+ if (depth > 2)
388
+ return;
389
+ for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
390
+ const nextPath = resolve4(dirPath, entry.name);
391
+ if (entry.isDirectory()) {
392
+ visit(nextPath, depth + 1);
393
+ continue;
394
+ }
395
+ if (entry.isFile() && entry.name.endsWith(".jsonl")) {
396
+ candidatePaths.push(nextPath);
397
+ }
398
+ }
399
+ };
400
+ visit(projectsRoot, 0);
401
+ const entries = candidatePaths.map((path) => {
402
+ const records2 = readJsonlFile2(path).filter((raw) => !!raw && typeof raw === "object" && !Array.isArray(raw));
403
+ return {
404
+ path,
405
+ mtimeMs: statSync2(path).mtimeMs,
406
+ records: records2
407
+ };
408
+ }).filter((entry) => {
409
+ const normalizedWorktree = run.worktreePath.replaceAll("\\", "/");
410
+ return entry.records.some((record) => {
411
+ const cwd = normalizeString(record.cwd)?.replaceAll("\\", "/");
412
+ return cwd === normalizedWorktree;
413
+ });
414
+ }).sort((left, right) => left.mtimeMs - right.mtimeMs);
415
+ if (entries.length === 0)
416
+ return [];
417
+ const startedAt = Date.parse(run.startedAt ?? run.createdAt);
418
+ const completedAt = run.completedAt ? Date.parse(run.completedAt) : Number.POSITIVE_INFINITY;
419
+ const lookbackMs = 60000;
420
+ const lookaheadMs = 5 * 60000;
421
+ const records = [];
422
+ for (const entry of entries) {
423
+ for (const record of entry.records) {
424
+ const timestamp = Date.parse(normalizeIsoTimestamp(record.timestamp) ?? "");
425
+ if (Number.isFinite(startedAt) && Number.isFinite(timestamp)) {
426
+ if (timestamp < startedAt - lookbackMs)
427
+ continue;
428
+ if (Number.isFinite(completedAt) && timestamp > completedAt + lookaheadMs)
429
+ continue;
430
+ }
431
+ records.push(record);
432
+ }
433
+ }
434
+ return records.sort((left, right) => {
435
+ const leftAt = Date.parse(normalizeIsoTimestamp(left.timestamp) ?? "") || 0;
436
+ const rightAt = Date.parse(normalizeIsoTimestamp(right.timestamp) ?? "") || 0;
437
+ return leftAt - rightAt;
438
+ });
439
+ }
440
+ function buildClaudeStreamLogs(run) {
441
+ const sessionRecords = readClaudeSessionRecords(run);
442
+ if (sessionRecords.length === 0)
443
+ return [];
444
+ const logs = [];
445
+ const pendingToolUses = new Map;
446
+ for (const record of sessionRecords) {
447
+ const entryType = normalizeString(record.type);
448
+ const createdAt = normalizeIsoTimestamp(record.timestamp) ?? run.updatedAt;
449
+ if (entryType === "summary") {
450
+ continue;
451
+ }
452
+ if (entryType === "system" || entryType === "result") {
453
+ logs.push({
454
+ id: normalizeString(record.uuid) ?? `${run.runId}-claude-${logs.length + 1}`,
455
+ runId: run.runId,
456
+ title: entryType === "result" ? "Agent result" : "Agent session initialized",
457
+ detail: JSON.stringify(record),
458
+ tone: entryType === "result" && record.is_error === true ? "error" : "info",
459
+ status: run.completedAt ? "completed" : "running",
460
+ payload: record,
461
+ createdAt
462
+ });
463
+ continue;
464
+ }
465
+ const message = record.message;
466
+ if (!message || typeof message !== "object" || Array.isArray(message))
467
+ continue;
468
+ const messageRecord = message;
469
+ const role = normalizeString(messageRecord.role) ?? entryType ?? "assistant";
470
+ const content = Array.isArray(messageRecord.content) ? messageRecord.content : [];
471
+ for (const item of content) {
472
+ if (!item || typeof item !== "object" || Array.isArray(item))
473
+ continue;
474
+ const contentRecord = item;
475
+ const contentType = normalizeString(contentRecord.type);
476
+ if (contentType === "tool_use") {
477
+ const toolUseId = normalizeString(contentRecord.id);
478
+ if (!toolUseId)
479
+ continue;
480
+ pendingToolUses.set(toolUseId, {
481
+ id: toolUseId,
482
+ name: normalizeString(contentRecord.name),
483
+ input: contentRecord.input ?? {},
484
+ timestamp: createdAt,
485
+ record: contentRecord
486
+ });
487
+ continue;
488
+ }
489
+ if (contentType === "tool_result") {
490
+ const toolUseId = normalizeString(contentRecord.tool_use_id);
491
+ const pending = toolUseId ? pendingToolUses.get(toolUseId) ?? null : null;
492
+ if (pending && toolUseId) {
493
+ pendingToolUses.delete(toolUseId);
494
+ }
495
+ const detail2 = JSON.stringify({
496
+ type: "claude_tool_activity",
497
+ session_id: normalizeString(record.sessionId) ?? normalizeString(record.session_id),
498
+ uuid: normalizeString(record.uuid),
499
+ tool_use_id: toolUseId,
500
+ tool_name: pending?.name ?? null,
501
+ tool_input: pending?.input ?? null,
502
+ tool_result: contentRecord.content ?? contentRecord,
503
+ is_error: contentRecord.is_error === true,
504
+ raw: {
505
+ toolUse: pending?.record ?? null,
506
+ toolResult: contentRecord
507
+ }
508
+ });
509
+ logs.push({
510
+ id: toolUseId ? `${run.runId}-claude-tool-${toolUseId}` : normalizeString(record.uuid) ?? `${run.runId}-claude-${logs.length + 1}`,
511
+ runId: run.runId,
512
+ title: "Tool activity",
513
+ detail: detail2,
514
+ tone: contentRecord.is_error === true ? "error" : "tool",
515
+ status: run.completedAt ? "completed" : "running",
516
+ payload: {
517
+ toolUseId,
518
+ toolName: pending?.name ?? null,
519
+ toolInput: pending?.input ?? null,
520
+ content: contentRecord.content ?? null,
521
+ isError: contentRecord.is_error === true
522
+ },
523
+ createdAt: pending?.timestamp ?? createdAt
524
+ });
525
+ continue;
526
+ }
527
+ const detail = JSON.stringify({
528
+ type: role,
529
+ message: {
530
+ role,
531
+ content: [contentRecord]
532
+ },
533
+ session_id: normalizeString(record.sessionId) ?? normalizeString(record.session_id),
534
+ uuid: normalizeString(record.uuid)
535
+ });
536
+ logs.push({
537
+ id: normalizeString(record.uuid) ? `${normalizeString(record.uuid)}:${contentType ?? "content"}:${logs.length + 1}` : `${run.runId}-claude-${logs.length + 1}`,
538
+ runId: run.runId,
539
+ title: role === "assistant" ? "Assistant output" : role === "user" ? "Tool activity" : "Agent output",
540
+ detail,
541
+ tone: contentType === "text" ? "info" : "tool",
542
+ status: run.completedAt ? "completed" : "running",
543
+ payload: contentRecord,
544
+ createdAt
545
+ });
546
+ }
547
+ }
548
+ for (const pending of pendingToolUses.values()) {
549
+ logs.push({
550
+ id: `${run.runId}-claude-tool-${pending.id}`,
551
+ runId: run.runId,
552
+ title: "Tool activity",
553
+ detail: JSON.stringify({
554
+ type: "claude_tool_use",
555
+ tool_use_id: pending.id,
556
+ tool_name: pending.name,
557
+ tool_input: pending.input,
558
+ raw: pending.record
559
+ }),
560
+ tone: "tool",
561
+ status: "running",
562
+ payload: {
563
+ toolUseId: pending.id,
564
+ toolName: pending.name,
565
+ toolInput: pending.input
566
+ },
567
+ createdAt: pending.timestamp
568
+ });
569
+ }
570
+ return logs.sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt));
571
+ }
572
+ var snapshotCache = new Map;
573
+ var RUN_LOG_SNAPSHOT_TAIL_LIMIT = 200;
574
+ function runListSignature(runs) {
575
+ return runs.slice().sort((a, b) => a.runId.localeCompare(b.runId)).map((run) => `${run.runId}|${run.updatedAt ?? ""}|${run.completedAt ?? ""}`).join(";");
576
+ }
577
+ function invalidateConversationSnapshotCache(projectRoot) {
578
+ if (projectRoot) {
579
+ snapshotCache.delete(projectRoot);
580
+ return;
581
+ }
582
+ snapshotCache.clear();
583
+ }
584
+ function buildConversationSnapshot(projectRoot, options = {}) {
585
+ const runs = listAuthorityRuns2(projectRoot);
586
+ const signature = runListSignature(runs);
587
+ if (!options.force) {
588
+ const cached = snapshotCache.get(projectRoot);
589
+ if (cached && cached.signature === signature) {
590
+ return cached.snapshot;
591
+ }
592
+ }
593
+ const conversations = [];
594
+ const messages = [];
595
+ const logs = [];
596
+ const actions = [];
597
+ for (const run of runs) {
598
+ const conversationId = `conversation:${run.runId}`;
599
+ conversations.push({
600
+ id: conversationId,
601
+ runId: run.runId,
602
+ title: run.title,
603
+ createdAt: run.createdAt,
604
+ updatedAt: run.updatedAt
605
+ });
606
+ const timeline = readJsonlFile2(runTimelinePath(projectRoot, run.runId));
607
+ for (const raw of timeline) {
608
+ if (!raw || typeof raw !== "object")
609
+ continue;
610
+ const entry = raw;
611
+ const type = normalizeString(entry.type) ?? "system_message";
612
+ const createdAt = normalizeString(entry.createdAt) ?? run.updatedAt;
613
+ if (type === "user_message" || type === "assistant_message" || type === "system_message") {
614
+ const role = type === "assistant_message" ? "assistant" : type === "user_message" ? "user" : "system";
615
+ messages.push({
616
+ id: normalizeString(entry.id) ?? `${run.runId}-${messages.length + 1}`,
617
+ conversationId,
618
+ role,
619
+ text: typeof entry.text === "string" ? entry.text : "",
620
+ attachments: Array.isArray(entry.attachments) ? entry.attachments : [],
621
+ state: normalizeString(entry.state) === "streaming" ? "streaming" : normalizeString(entry.state) === "interrupted" ? "interrupted" : normalizeString(entry.state) === "errored" ? "errored" : "completed",
622
+ createdAt,
623
+ completedAt: normalizeString(entry.completedAt) ?? (normalizeString(entry.state) === "streaming" ? null : createdAt)
624
+ });
625
+ continue;
626
+ }
627
+ if (type === "action") {
628
+ actions.push({
629
+ id: normalizeString(entry.id) ?? `${run.runId}-action-${actions.length + 1}`,
630
+ runId: run.runId,
631
+ messageId: normalizeString(entry.messageId),
632
+ actionType: normalizeString(entry.actionType) ?? "task",
633
+ title: normalizeString(entry.title) ?? "Run step",
634
+ detail: normalizeString(entry.detail),
635
+ state: normalizeString(entry.state) ?? "completed",
636
+ payload: entry.payload ?? {},
637
+ startedAt: createdAt,
638
+ completedAt: normalizeString(entry.completedAt) ?? null
639
+ });
640
+ }
641
+ }
642
+ const runLogs = readJsonlFileTail(runLogsPath(projectRoot, run.runId), {
643
+ limit: RUN_LOG_SNAPSHOT_TAIL_LIMIT,
644
+ maxBytes: 512 * 1024
645
+ });
646
+ const claudeStreamLogs = buildClaudeStreamLogs(run);
647
+ const hasClaudeStreamLogs = claudeStreamLogs.length > 0;
648
+ for (const raw of runLogs) {
649
+ if (!raw || typeof raw !== "object")
650
+ continue;
651
+ const entry = raw;
652
+ const createdAt = normalizeString(entry.createdAt) ?? run.updatedAt;
653
+ const rawDetail = normalizeString(entry.detail);
654
+ if (hasClaudeStreamLogs && rawDetail) {
655
+ try {
656
+ const parsed = JSON.parse(rawDetail);
657
+ const parsedType = normalizeString(parsed.type);
658
+ if (parsedType === "assistant" || parsedType === "user" || parsedType === "system" || parsedType === "result") {
659
+ continue;
660
+ }
661
+ } catch {}
662
+ }
663
+ logs.push({
664
+ id: normalizeString(entry.id) ?? `${run.runId}-log-${logs.length + 1}`,
665
+ runId: run.runId,
666
+ title: normalizeString(entry.title) ?? "Run log",
667
+ detail: normalizeString(entry.detail),
668
+ tone: normalizeString(entry.tone) === "error" ? "error" : normalizeString(entry.tone) === "tool" ? "tool" : normalizeString(entry.tone) === "thinking" ? "thinking" : "info",
669
+ status: normalizeString(entry.status),
670
+ payload: entry.payload ?? {},
671
+ createdAt
672
+ });
673
+ }
674
+ logs.push(...claudeStreamLogs);
675
+ }
676
+ logs.sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt));
677
+ const snapshot = { conversations, messages, logs, actions };
678
+ snapshotCache.set(projectRoot, { signature, snapshot });
679
+ return snapshot;
680
+ }
681
+
682
+ // packages/server/src/server-helpers/snapshot-orchestrator.ts
683
+ async function buildTaskPrompt(projectRoot, taskId, readers) {
684
+ const task = (await readers.readWorkspaceTasks(projectRoot)).find((entry) => entry.id === taskId);
685
+ if (!task) {
686
+ return `Work on task ${taskId}.`;
687
+ }
688
+ const sections = [
689
+ `You are working on task ${task.id}: ${task.title}.`,
690
+ task.description ? `Description:
691
+ ${task.description}` : null,
692
+ task.acceptanceCriteria ? `Acceptance criteria:
693
+ ${task.acceptanceCriteria}` : null,
694
+ task.scope.length > 0 ? `Scope:
695
+ - ${task.scope.join(`
696
+ - `)}` : null,
697
+ task.validation.length > 0 ? `Validation:
698
+ - ${task.validation.join(`
699
+ - `)}` : null,
700
+ "Work directly in the assigned runtime workspace and leave the result in a reviewable state."
701
+ ];
702
+ return sections.filter((value) => Boolean(value)).join(`
703
+
704
+ `);
705
+ }
706
+ async function buildRigSnapshotPayload(projectRoot, readers) {
707
+ const workspace = await readers.readWorkspaceSummaryRecord(projectRoot);
708
+ const queue = readQueueState(projectRoot);
709
+ const queuedTaskIds = new Set(queue.map((entry) => entry.taskId));
710
+ const graph = buildWorkspaceGraphSummary(workspace, []);
711
+ const taskSummaries = (await readers.readWorkspaceTasks(projectRoot)).map((task) => ({
712
+ ...toTaskSummary(workspace.id, {
713
+ ...task,
714
+ status: queuedTaskIds.has(task.id) && (task.status === "open" || task.status === "ready" || task.status === "draft") ? "queued" : task.status
715
+ }),
716
+ graphId: graph.id
717
+ }));
718
+ const populatedGraph = buildWorkspaceGraphSummary(workspace, taskSummaries);
719
+ const authorityRuns = listAuthorityRuns3(projectRoot);
720
+ const approvalRecords = readApprovalsForRuns(projectRoot, authorityRuns);
721
+ const userInputRecords = readUserInputsForRuns(projectRoot, authorityRuns);
722
+ const approvals = approvalRecords.map(toApprovalSummary);
723
+ const userInputs = userInputRecords.map(toUserInputSummary);
724
+ const runs = authorityRuns.map((run) => toRunSummary(run, approvalRecords.filter((entry) => entry.runId === run.runId), userInputRecords.filter((entry) => entry.runId === run.runId)));
725
+ const artifacts = await readers.listArtifactSummaries(projectRoot, null, { knownRuns: authorityRuns });
726
+ return {
727
+ workspace,
728
+ snapshot: {
729
+ workspaces: [workspace],
730
+ graphs: [populatedGraph],
731
+ tasks: taskSummaries,
732
+ runs,
733
+ approvals,
734
+ userInputs,
735
+ artifacts,
736
+ queue,
737
+ updatedAt: workspace.updatedAt
738
+ },
739
+ approvals,
740
+ userInputs,
741
+ graphs: [populatedGraph],
742
+ tasks: taskSummaries,
743
+ runs,
744
+ artifacts,
745
+ queue
746
+ };
747
+ }
748
+ function buildRuntimeSnapshot(inputs) {
749
+ const { projectRoot, runProcesses, agentRuntimes } = inputs;
750
+ const runtimes = [];
751
+ const worktrees = [];
752
+ const runtimesById = new Map(agentRuntimes.map((runtime) => [runtime.id, runtime]));
753
+ for (const processState of runProcesses.values()) {
754
+ const run = readAuthorityRun2(projectRoot, processState.runId);
755
+ if (!run)
756
+ continue;
757
+ const runtimeId = normalizeString(run.branch);
758
+ const runtime = runtimeId ? runtimesById.get(runtimeId) ?? null : null;
759
+ const updatedAt = run.updatedAt;
760
+ const workspaceDir = runtime?.workspaceDir ?? run.worktreePath ?? null;
761
+ const logsDir = runtime?.logsDir ?? (workspaceDir ? resolve5(workspaceDir, ".rig", "logs") : run.logRoot);
762
+ const stateDir = runtime?.stateDir ?? (workspaceDir ? resolve5(workspaceDir, ".rig", "state") : null);
763
+ const sessionDir = runtime?.sessionDir ?? (workspaceDir ? resolve5(workspaceDir, ".rig", "session") : run.sessionPath ? dirname4(run.sessionPath) : null);
764
+ const sessionLogPath = runtime?.sessionDir || workspaceDir ? logsDir ? resolve5(logsDir, "session.log") : null : run.sessionLogPath;
765
+ runtimes.push({
766
+ id: runtimeId ?? `runtime:${run.runId}`,
767
+ workspaceId: run.workspaceId,
768
+ runId: run.runId,
769
+ adapterKind: run.runtimeAdapter,
770
+ executionTarget: run.mode === "remote" ? "remote" : "local",
771
+ remoteHostId: run.hostId,
772
+ status: run.status === "failed" ? "failed" : run.status === "stopped" ? "interrupted" : run.status === "running" ? "running" : run.status === "preparing" ? "starting" : "exited",
773
+ sandboxMode: normalizeString(run.runtimeMode) === "full-access" ? "danger-full-access" : "workspace-write",
774
+ isolationMode: "worktree",
775
+ workspaceDir,
776
+ homeDir: runtime?.homeDir ?? null,
777
+ tmpDir: runtime?.tmpDir ?? null,
778
+ cacheDir: runtime?.cacheDir ?? null,
779
+ logsDir,
780
+ stateDir,
781
+ sessionDir,
782
+ sessionLogPath,
783
+ pid: processState.child?.pid ?? null,
784
+ startedAt: run.startedAt,
785
+ updatedAt,
786
+ exitedAt: run.completedAt
787
+ });
788
+ if (workspaceDir) {
789
+ worktrees.push({
790
+ id: `worktree:${run.runId}`,
791
+ workspaceId: run.workspaceId,
792
+ runId: run.runId,
793
+ taskId: run.taskId,
794
+ branchName: runtimeId ?? `run:${run.runId}`,
795
+ path: workspaceDir,
796
+ status: run.completedAt ? "preserved" : "active",
797
+ createdAt: run.startedAt ?? run.createdAt,
798
+ cleanedAt: null
799
+ });
800
+ }
801
+ }
802
+ return { runtimes, worktrees };
803
+ }
804
+ async function buildEngineSnapshotPayload(inputs) {
805
+ const payload = await buildRigSnapshotPayload(inputs.projectRoot, inputs.readers);
806
+ const { conversations, messages, logs, actions } = buildConversationSnapshot(inputs.projectRoot);
807
+ const { runtimes, worktrees } = buildRuntimeSnapshot({
808
+ projectRoot: inputs.projectRoot,
809
+ runProcesses: inputs.runProcesses,
810
+ agentRuntimes: inputs.agentRuntimes
811
+ });
812
+ const remoteEndpoints = buildRemoteEndpointSnapshot(inputs.projectRoot, inputs.remoteHosts);
813
+ const remoteConnections = buildRemoteConnectionSnapshot(inputs.remoteHosts, inputs.remoteConnections, remoteEndpoints);
814
+ return {
815
+ snapshotSequence: normalizeSequence(inputs.sequence) ?? 0,
816
+ workspaces: payload.snapshot.workspaces,
817
+ graphs: payload.snapshot.graphs,
818
+ tasks: payload.snapshot.tasks,
819
+ runs: payload.snapshot.runs,
820
+ runtimes,
821
+ conversations,
822
+ messages,
823
+ actions,
824
+ logs,
825
+ approvals: payload.snapshot.approvals,
826
+ userInputs: payload.snapshot.userInputs,
827
+ validations: [],
828
+ reviews: [],
829
+ artifacts: payload.snapshot.artifacts,
830
+ policyDecisions: [],
831
+ queue: payload.queue,
832
+ worktrees,
833
+ remoteEndpoints,
834
+ remoteConnections,
835
+ remoteOrchestrations: [],
836
+ updatedAt: payload.snapshot.updatedAt
837
+ };
838
+ }
839
+
840
+ // packages/server/src/server-helpers/plugin-host-cache.ts
841
+ import { existsSync as existsSync3, statSync as statSync3 } from "fs";
842
+ import { resolve as resolve6 } from "path";
843
+ var contextCache = new Map;
844
+ var taskListCache = new Map;
845
+ var DEFAULT_TASK_LIST_TTL_MS = 2000;
846
+ function getPluginHostConfigMtime(projectRoot) {
847
+ for (const name of ["rig.config.ts", "rig.config.json"]) {
848
+ const path = resolve6(projectRoot, name);
849
+ if (existsSync3(path)) {
850
+ try {
851
+ return statSync3(path).mtimeMs;
852
+ } catch {
853
+ return null;
854
+ }
855
+ }
856
+ }
857
+ return null;
858
+ }
859
+ async function getCachedPluginHostContext(projectRoot) {
860
+ const mtimeMs = getPluginHostConfigMtime(projectRoot);
861
+ if (mtimeMs === null) {
862
+ contextCache.delete(projectRoot);
863
+ return null;
864
+ }
865
+ const cached = contextCache.get(projectRoot);
866
+ if (cached && cached.mtimeMs === mtimeMs && cached.ctx) {
867
+ return cached.ctx;
868
+ }
869
+ try {
870
+ const { buildPluginHostContext } = await import("@rig/runtime/control-plane/plugin-host-context");
871
+ const ctx = await buildPluginHostContext(projectRoot);
872
+ if (!ctx) {
873
+ contextCache.delete(projectRoot);
874
+ return null;
875
+ }
876
+ contextCache.set(projectRoot, { mtimeMs, ctx });
877
+ return ctx;
878
+ } catch (err) {
879
+ contextCache.delete(projectRoot);
880
+ throw err;
881
+ }
882
+ }
883
+ async function getCachedTaskSourceList(projectRoot, options = {}) {
884
+ const ctx = await getCachedPluginHostContext(projectRoot);
885
+ if (!ctx)
886
+ return null;
887
+ const sources = ctx.taskSourceRegistry.list();
888
+ if (sources.length === 0)
889
+ return null;
890
+ const source = sources[0];
891
+ const cacheKey = `${projectRoot}::${source.kind}`;
892
+ const ttl = Math.max(0, options.ttlMs ?? DEFAULT_TASK_LIST_TTL_MS);
893
+ const now = Date.now();
894
+ if (!options.force) {
895
+ const cached = taskListCache.get(cacheKey);
896
+ if (cached && cached.expiresAt > now) {
897
+ return { kind: source.kind, tasks: cached.tasks };
898
+ }
899
+ }
900
+ const tasks = await source.list();
901
+ taskListCache.set(cacheKey, { expiresAt: now + ttl, tasks });
902
+ return { kind: source.kind, tasks };
903
+ }
904
+ function invalidatePluginHostCache(projectRoot) {
905
+ contextCache.delete(projectRoot);
906
+ for (const key of taskListCache.keys()) {
907
+ if (key.startsWith(`${projectRoot}::`)) {
908
+ taskListCache.delete(key);
909
+ }
910
+ }
911
+ }
912
+
913
+ // packages/server/src/server-helpers/snapshot-service.ts
914
+ var DEFAULT_TASK_SOURCE_TTL_MS = 2000;
915
+ function errorMessage(error) {
916
+ return error instanceof Error ? error.message : String(error);
917
+ }
918
+ function errorName(error) {
919
+ return error instanceof Error ? error.name : null;
920
+ }
921
+ function toTaskSourceError(input) {
922
+ return {
923
+ source: input.source,
924
+ reason: input.reason,
925
+ projectRoot: input.projectRoot,
926
+ kind: input.kind,
927
+ message: errorMessage(input.error),
928
+ causeName: errorName(input.error)
929
+ };
930
+ }
931
+ function normalizeTaskRecord(task, now) {
932
+ if (!task || typeof task !== "object" || Array.isArray(task)) {
933
+ throw new Error("task source returned a non-object task record");
934
+ }
935
+ const t = task;
936
+ const idValue = typeof t.id === "string" ? t.id : "";
937
+ const titleValue = typeof t.title === "string" ? t.title : idValue;
938
+ const statusValue = typeof t.status === "string" ? t.status : "open";
939
+ const depsInput = Array.isArray(t.deps) ? t.deps : Array.isArray(t.dependencies) ? t.dependencies : [];
940
+ const depsValue = depsInput.filter((dependency) => typeof dependency === "string");
941
+ const parentChildInput = Array.isArray(t.parentChildDeps) ? t.parentChildDeps : [];
942
+ const parentChildDepsValue = parentChildInput.filter((dependency) => typeof dependency === "string");
943
+ const labelsValue = Array.isArray(t.labels) ? t.labels.filter((label) => typeof label === "string") : [];
944
+ const scopeValue = Array.isArray(t.scope) ? t.scope.filter((scope) => typeof scope === "string") : [];
945
+ const description = typeof t.description === "string" ? t.description : typeof t.body === "string" ? t.body : null;
946
+ const validationValue = Array.isArray(t.validators) ? t.validators.filter((validator) => typeof validator === "string") : Array.isArray(t.validation) ? t.validation.filter((validator) => typeof validator === "string") : [];
947
+ return {
948
+ id: idValue,
949
+ title: titleValue,
950
+ description,
951
+ acceptanceCriteria: typeof t.acceptanceCriteria === "string" ? t.acceptanceCriteria : null,
952
+ status: statusValue,
953
+ sourceStatus: null,
954
+ priority: typeof t.priority === "number" ? t.priority : null,
955
+ issueType: typeof t.issueType === "string" ? t.issueType : null,
956
+ role: typeof t.role === "string" ? t.role : null,
957
+ externalRef: typeof t.externalRef === "string" ? t.externalRef : typeof t.url === "string" ? t.url : null,
958
+ sourceIssueId: typeof t.sourceIssueId === "string" ? t.sourceIssueId : null,
959
+ dependencies: depsValue.slice(),
960
+ parentChildDeps: parentChildDepsValue.slice(),
961
+ scope: scopeValue,
962
+ validation: validationValue,
963
+ createdAt: typeof t.createdAt === "string" ? t.createdAt : now,
964
+ updatedAt: typeof t.updatedAt === "string" ? t.updatedAt : now,
965
+ labels: labelsValue,
966
+ ...t.raw && typeof t.raw === "object" && !Array.isArray(t.raw) ? { raw: t.raw } : {},
967
+ ...typeof t.issueNodeId === "string" ? { issueNodeId: t.issueNodeId } : {},
968
+ ...typeof t.nodeId === "string" ? { nodeId: t.nodeId } : {},
969
+ ...typeof t.node_id === "string" ? { node_id: t.node_id } : {}
970
+ };
971
+ }
972
+ function buildEmptyTaskSourceHint(kind) {
973
+ if (kind === "github-issues") {
974
+ return `Verify (1) "gh auth status" is signed in, (2) the configured owner/repo has issues with the configured labels (default: "task"), and (3) the gh CLI can list them: "gh issue list --repo <owner>/<repo> --label task".`;
975
+ }
976
+ if (kind === "files") {
977
+ return "Check the configured taskSource.path directory exists and contains JSON files matching the pattern.";
978
+ }
979
+ return "The configured task source returned no tasks. Check the plugin's factory logs for details.";
980
+ }
981
+ function createSnapshotService(options) {
982
+ const { projectRoot, readers } = options;
983
+ const deps = options.deps ?? {};
984
+ const readConfigMtime = deps.readConfigMtime ?? getPluginHostConfigMtime;
985
+ const readTaskSourceList = deps.readTaskSourceList ?? ((root, readOptions) => getCachedTaskSourceList(root, readOptions));
986
+ const readPluginHostContext = deps.readPluginHostContext ?? getCachedPluginHostContext;
987
+ const invalidateHostCache = deps.invalidatePluginHostCache ?? invalidatePluginHostCache;
988
+ const invalidateConversationCache = deps.invalidateConversationSnapshotCache ?? invalidateConversationSnapshotCache;
989
+ const readAgentRuntimes = deps.listAgentRuntimes ?? listAgentRuntimes;
990
+ const nowMs = deps.nowMs ?? Date.now;
991
+ const nowIso = deps.nowIso ?? (() => new Date().toISOString());
992
+ const logger = deps.logger ?? console;
993
+ const taskSourceTtlMs = Math.max(0, deps.taskSourceTtlMs ?? DEFAULT_TASK_SOURCE_TTL_MS);
994
+ let pluginTaskCache = null;
995
+ let taskSourceErrors = [];
996
+ let lastEmptyTaskSourceWarning = null;
997
+ function setOutcome(outcome, configMtime) {
998
+ pluginTaskCache = {
999
+ configMtime,
1000
+ expiresAt: nowMs() + taskSourceTtlMs,
1001
+ outcome
1002
+ };
1003
+ taskSourceErrors = outcome.errors;
1004
+ return outcome;
1005
+ }
1006
+ async function readPluginHostTasks() {
1007
+ const configMtime = readConfigMtime(projectRoot);
1008
+ const now = nowMs();
1009
+ if (pluginTaskCache && pluginTaskCache.configMtime === configMtime && pluginTaskCache.expiresAt > now) {
1010
+ taskSourceErrors = pluginTaskCache.outcome.errors;
1011
+ return pluginTaskCache.outcome;
1012
+ }
1013
+ let cacheResult;
1014
+ try {
1015
+ cacheResult = await readTaskSourceList(projectRoot, { force: true, ttlMs: 0 });
1016
+ } catch (error) {
1017
+ const kind2 = await readConfiguredTaskSourceKind();
1018
+ const structured = toTaskSourceError({
1019
+ source: "task-source",
1020
+ reason: "read-failed",
1021
+ projectRoot,
1022
+ kind: kind2,
1023
+ error
1024
+ });
1025
+ logger.warn(`[server] plugin-host task source read failed: ${structured.message}`);
1026
+ return setOutcome({ kind: kind2, tasks: null, errors: [structured] }, configMtime);
1027
+ }
1028
+ if (!cacheResult) {
1029
+ const ctx = await readPluginHostContext(projectRoot).catch((error) => {
1030
+ const structured = toTaskSourceError({
1031
+ source: "plugin-host",
1032
+ reason: "read-failed",
1033
+ projectRoot,
1034
+ kind: null,
1035
+ error
1036
+ });
1037
+ taskSourceErrors = [structured];
1038
+ logger.warn(`[server] plugin-host context read failed: ${structured.message}`);
1039
+ return null;
1040
+ });
1041
+ if (ctx && ctx.taskSourceRegistry.list().length === 0) {
1042
+ const key = `${projectRoot}::no-source`;
1043
+ if (lastEmptyTaskSourceWarning !== key) {
1044
+ logger.warn(`[server] rig.config loaded but no task source registered \u2014 check that the configured plugin contributes a factory for kind "${ctx.config.taskSource.kind}".`);
1045
+ lastEmptyTaskSourceWarning = key;
1046
+ }
1047
+ }
1048
+ return setOutcome({ kind: null, tasks: null, errors: taskSourceErrors }, configMtime);
1049
+ }
1050
+ const { kind, tasks } = cacheResult;
1051
+ if (tasks.length === 0) {
1052
+ const key = `${projectRoot}::${kind}::empty`;
1053
+ if (lastEmptyTaskSourceWarning !== key) {
1054
+ logger.warn(`[server] task source "${kind}" returned no tasks for ${projectRoot}. ${buildEmptyTaskSourceHint(kind)}`);
1055
+ lastEmptyTaskSourceWarning = key;
1056
+ }
1057
+ return setOutcome({ kind, tasks: null, errors: [] }, configMtime);
1058
+ }
1059
+ lastEmptyTaskSourceWarning = null;
1060
+ try {
1061
+ return setOutcome({ kind, tasks: tasks.map((task) => normalizeTaskRecord(task, nowIso())), errors: [] }, configMtime);
1062
+ } catch (error) {
1063
+ const structured = toTaskSourceError({
1064
+ source: "task-source",
1065
+ reason: "map-failed",
1066
+ projectRoot,
1067
+ kind,
1068
+ error
1069
+ });
1070
+ logger.warn(`[server] plugin-host task source read failed: ${structured.message}`);
1071
+ return setOutcome({ kind, tasks: null, errors: [structured] }, configMtime);
1072
+ }
1073
+ }
1074
+ async function readConfiguredTaskSourceKind() {
1075
+ try {
1076
+ return (await readPluginHostContext(projectRoot))?.config.taskSource.kind ?? null;
1077
+ } catch {
1078
+ return null;
1079
+ }
1080
+ }
1081
+ async function readWorkspaceTasks() {
1082
+ const fromPluginHost = await readPluginHostTasks();
1083
+ if (fromPluginHost.tasks && fromPluginHost.tasks.length > 0) {
1084
+ return fromPluginHost.tasks;
1085
+ }
1086
+ if (fromPluginHost.kind === "github-issues") {
1087
+ return [];
1088
+ }
1089
+ return readers.readLegacyWorkspaceTasks(projectRoot);
1090
+ }
1091
+ function makeWorkspaceReaders() {
1092
+ let taskMemo = null;
1093
+ const memoizedTasks = () => {
1094
+ taskMemo ??= readWorkspaceTasks();
1095
+ return taskMemo;
1096
+ };
1097
+ return {
1098
+ readWorkspaceTasks: () => memoizedTasks(),
1099
+ readWorkspaceSummaryRecord: (root) => readers.readWorkspaceSummaryRecord(root),
1100
+ listArtifactSummaries: async (root, taskId, options2) => {
1101
+ const knownTaskIds = taskId ? undefined : (await memoizedTasks()).map((task) => task.id);
1102
+ return readers.listArtifactSummaries(root, taskId, knownTaskIds, options2?.knownRuns);
1103
+ }
1104
+ };
1105
+ }
1106
+ return {
1107
+ getWorkspaceTasks: readWorkspaceTasks,
1108
+ getRigSnapshot: () => buildRigSnapshotPayload(projectRoot, makeWorkspaceReaders()),
1109
+ async getRuntimeSnapshot(input) {
1110
+ const agentRuntimes = await readAgentRuntimes(projectRoot);
1111
+ return buildRuntimeSnapshot({
1112
+ projectRoot,
1113
+ runProcesses: input.runProcesses,
1114
+ agentRuntimes
1115
+ });
1116
+ },
1117
+ async getEngineSnapshot(input) {
1118
+ const agentRuntimes = await readAgentRuntimes(projectRoot);
1119
+ return buildEngineSnapshotPayload({
1120
+ projectRoot,
1121
+ remoteHosts: input.remoteHosts,
1122
+ remoteConnections: input.remoteConnections,
1123
+ sequence: input.sequence,
1124
+ runProcesses: input.runProcesses,
1125
+ agentRuntimes,
1126
+ readers: makeWorkspaceReaders()
1127
+ });
1128
+ },
1129
+ getTaskPrompt: (taskId) => buildTaskPrompt(projectRoot, taskId, makeWorkspaceReaders()),
1130
+ getTaskSourceErrors: () => taskSourceErrors,
1131
+ invalidate(reason) {
1132
+ pluginTaskCache = null;
1133
+ taskSourceErrors = [];
1134
+ invalidateConversationCache(projectRoot);
1135
+ if (reason.toLowerCase().includes("config")) {
1136
+ invalidateHostCache(projectRoot);
1137
+ }
1138
+ }
1139
+ };
1140
+ }
1141
+ export {
1142
+ createSnapshotService
1143
+ };