@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,1759 @@
1
+ // @bun
2
+ // packages/server/src/inspector/provider-session.ts
3
+ function uniqueStrings(values) {
4
+ return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))];
5
+ }
6
+ function withSupport(defaultValue, support, key) {
7
+ return typeof support?.[key] === "boolean" ? Boolean(support[key]) : defaultValue;
8
+ }
9
+ function normalizeProviderSession(run) {
10
+ const sessionLogPaths = uniqueStrings([run.sessionLogPath]);
11
+ const artifactRoots = uniqueStrings([run.artifactRoot]);
12
+ const eventSources = uniqueStrings([run.source, ...run.rawSourceKinds, run.sessionPath ? "session-file" : null]);
13
+ const capabilityHints = uniqueStrings([
14
+ sessionLogPaths.length > 0 ? "logs" : null,
15
+ artifactRoots.length > 0 ? "artifacts" : null,
16
+ run.worktreePath ? "workspace" : null,
17
+ run.conflictState !== "none" ? "conflict" : null,
18
+ run.threadId ? "thread" : null
19
+ ]);
20
+ const observabilityConfidence = sessionLogPaths.length > 0 && artifactRoots.length > 0 && run.threadId ? "high" : sessionLogPaths.length > 0 || artifactRoots.length > 0 || Boolean(run.threadId) ? "medium" : "low";
21
+ return {
22
+ provider: run.provider,
23
+ runId: run.runId,
24
+ taskId: run.taskId,
25
+ workspaceId: run.workspaceId,
26
+ workspaceDir: run.worktreePath,
27
+ runtimeAdapter: run.runtimeAdapter,
28
+ status: run.status,
29
+ startedAt: run.startedAt,
30
+ updatedAt: run.updatedAt,
31
+ completedAt: run.completedAt,
32
+ threadId: run.threadId,
33
+ sessionLogPaths,
34
+ artifactRoots,
35
+ eventSources,
36
+ observabilityConfidence,
37
+ rawSourceKinds: [...run.rawSourceKinds],
38
+ capabilityHints
39
+ };
40
+ }
41
+ function deriveInspectorCapabilitySet(session, support = {}) {
42
+ return {
43
+ discoverRuns: withSupport(true, support, "discoverRuns"),
44
+ inspectRuns: withSupport(true, support, "inspectRuns"),
45
+ readLogs: session.sessionLogPaths.length > 0 && withSupport(true, support, "readLogs"),
46
+ readArtifacts: session.artifactRoots.length > 0 && withSupport(true, support, "readArtifacts"),
47
+ callServerApis: withSupport(true, support, "callServerApis"),
48
+ interruptRuns: withSupport(false, support, "interruptRuns"),
49
+ stopRuns: withSupport(false, support, "stopRuns"),
50
+ resumeRuns: withSupport(false, support, "resumeRuns"),
51
+ relaunchRuns: withSupport(false, support, "relaunchRuns"),
52
+ patchRepo: session.workspaceDir !== null && withSupport(true, support, "patchRepo"),
53
+ patchHarness: session.workspaceDir !== null && withSupport(true, support, "patchHarness"),
54
+ createTasks: withSupport(false, support, "createTasks"),
55
+ runReviewerAgents: withSupport(false, support, "runReviewerAgents"),
56
+ provisionSkills: withSupport(true, support, "provisionSkills"),
57
+ scanUpstream: withSupport(false, support, "scanUpstream"),
58
+ writeJournal: withSupport(true, support, "writeJournal"),
59
+ spawnSpecialists: withSupport(false, support, "spawnSpecialists")
60
+ };
61
+ }
62
+
63
+ // packages/server/src/inspector/tools.ts
64
+ import { randomUUID as randomUUID2 } from "crypto";
65
+
66
+ // packages/server/src/inspector/mission.ts
67
+ import { randomUUID } from "crypto";
68
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, renameSync, writeFileSync as writeFileSync2 } from "fs";
69
+ import { dirname as dirname2, join, resolve as resolve2 } from "path";
70
+
71
+ // packages/server/src/bootstrap.ts
72
+ import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "fs";
73
+ import { dirname, resolve } from "path";
74
+ import { RIG_DEFINITION_DIRNAME, resolveMonorepoRoot } from "@rig/runtime";
75
+ function normalizeOptionalString(value) {
76
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
77
+ }
78
+ function resolveRigServerPaths(projectRoot) {
79
+ const taskWorkspace = normalizeOptionalString(process.env.RIG_TASK_WORKSPACE);
80
+ const explicitStateDir = normalizeOptionalString(process.env.RIG_STATE_DIR);
81
+ const explicitLogsDir = normalizeOptionalString(process.env.RIG_LOGS_DIR);
82
+ const explicitSessionFile = normalizeOptionalString(process.env.RIG_SESSION_FILE);
83
+ const hostStateRoot = resolve(projectRoot, ".rig");
84
+ const monorepoRoot = resolveMonorepoRoot(projectRoot);
85
+ const monorepoStateRoot = resolve(monorepoRoot, ".rig");
86
+ const stateRoot = taskWorkspace ? resolve(taskWorkspace, ".rig") : explicitStateDir ? dirname(resolve(explicitStateDir)) : explicitLogsDir ? dirname(resolve(explicitLogsDir)) : explicitSessionFile ? dirname(dirname(resolve(explicitSessionFile))) : existsSync(hostStateRoot) ? hostStateRoot : monorepoStateRoot;
87
+ const stateDir = explicitStateDir ? resolve(explicitStateDir) : resolve(stateRoot, "state");
88
+ const logsDir = explicitLogsDir ? resolve(explicitLogsDir) : resolve(stateRoot, "logs");
89
+ 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");
90
+ return {
91
+ stateRoot,
92
+ stateDir,
93
+ logsDir,
94
+ controlPlaneEventsFile: resolve(logsDir, "control-plane.events.jsonl"),
95
+ taskConfigPath,
96
+ notificationsFile: resolve(projectRoot, "rig", "notifications", "targets.json"),
97
+ keybindingsPath: resolve(projectRoot, "rig", "keybindings.json")
98
+ };
99
+ }
100
+
101
+ // packages/server/src/inspector/mission.ts
102
+ function isJsonValue(value) {
103
+ if (value === null)
104
+ return true;
105
+ const valueType = typeof value;
106
+ if (valueType === "string" || valueType === "number" || valueType === "boolean") {
107
+ return Number.isFinite(value) || valueType !== "number";
108
+ }
109
+ if (Array.isArray(value)) {
110
+ return value.every(isJsonValue);
111
+ }
112
+ if (valueType === "object") {
113
+ for (const entry of Object.values(value)) {
114
+ if (!isJsonValue(entry))
115
+ return false;
116
+ }
117
+ return true;
118
+ }
119
+ return false;
120
+ }
121
+ function toJsonRecord(value) {
122
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
123
+ return null;
124
+ }
125
+ const record = {};
126
+ for (const [key, entry] of Object.entries(value)) {
127
+ if (isJsonValue(entry)) {
128
+ record[key] = entry;
129
+ }
130
+ }
131
+ return record;
132
+ }
133
+ function cloneJsonRecord(value) {
134
+ return JSON.parse(JSON.stringify(value));
135
+ }
136
+ function isRecord(value) {
137
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
138
+ }
139
+ function readJsonRecord(path) {
140
+ try {
141
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
142
+ if (!isRecord(parsed)) {
143
+ return { ok: false, error: `Mission file ${path} does not contain an object` };
144
+ }
145
+ return { ok: true, record: parsed };
146
+ } catch (error) {
147
+ const message = error instanceof Error ? error.message : String(error);
148
+ return { ok: false, error: message };
149
+ }
150
+ }
151
+ function parseMission(record) {
152
+ if (typeof record.missionId !== "string")
153
+ return { ok: false, error: "Mission is missing missionId" };
154
+ if (typeof record.owner !== "string")
155
+ return { ok: false, error: "Mission is missing owner" };
156
+ if (typeof record.status !== "string")
157
+ return { ok: false, error: "Mission is missing status" };
158
+ if (!isRecord(record.sourceTask))
159
+ return { ok: false, error: "Mission is missing sourceTask" };
160
+ if (!isJsonValue(record.sourceTask))
161
+ return { ok: false, error: "Mission sourceTask is not JSON-safe" };
162
+ const pendingEffects = Array.isArray(record.pendingEffects) ? record.pendingEffects.filter(isRecord).map((entry) => entry) : [];
163
+ const postedResults = Array.isArray(record.postedResults) ? record.postedResults.filter(isRecord).map((entry) => entry) : [];
164
+ const completionProof = isRecord(record.completionProof) ? record.completionProof : null;
165
+ return {
166
+ ok: true,
167
+ mission: {
168
+ missionId: record.missionId,
169
+ owner: record.owner,
170
+ sourceTask: cloneJsonRecord(record.sourceTask),
171
+ status: record.status,
172
+ summary: typeof record.summary === "string" ? record.summary : "Inspector mission",
173
+ pendingEffects,
174
+ postedResults,
175
+ completionProof,
176
+ createdAt: typeof record.createdAt === "string" ? record.createdAt : new Date(0).toISOString(),
177
+ updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : new Date(0).toISOString()
178
+ }
179
+ };
180
+ }
181
+ function sourceTaskId(sourceTask) {
182
+ for (const key of ["sourceTaskId", "taskId", "id"]) {
183
+ const value = sourceTask[key];
184
+ if (typeof value === "string" && value.trim().length > 0) {
185
+ return value;
186
+ }
187
+ }
188
+ return null;
189
+ }
190
+ function terminalStatus(status) {
191
+ return status === "accepted" || status === "rejected";
192
+ }
193
+ function latestEffectResults(mission) {
194
+ const latestByEffect = new Map;
195
+ for (const result of mission.postedResults) {
196
+ latestByEffect.set(result.effectId, result);
197
+ }
198
+ return [...latestByEffect.values()];
199
+ }
200
+ function blockingResults(mission) {
201
+ return latestEffectResults(mission).filter((result) => result.status !== "completed");
202
+ }
203
+ function pendingEffects(mission) {
204
+ return mission.pendingEffects.filter((effect) => effect.status === "pending");
205
+ }
206
+ function missionStatusAfterPost(mission) {
207
+ if (blockingResults(mission).length > 0)
208
+ return "blocked";
209
+ if (pendingEffects(mission).length > 0)
210
+ return "awaiting_effect";
211
+ return "open";
212
+ }
213
+ function missionActionDetails(mission) {
214
+ return {
215
+ missionId: mission.missionId,
216
+ status: mission.status,
217
+ sourceTaskId: sourceTaskId(mission.sourceTask)
218
+ };
219
+ }
220
+ function writeJsonFile(path, value) {
221
+ mkdirSync2(dirname2(path), { recursive: true });
222
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
223
+ writeFileSync2(tempPath, `${JSON.stringify(value, null, 2)}
224
+ `, "utf8");
225
+ renameSync(tempPath, path);
226
+ }
227
+ function resolveInspectorMissionPaths(projectRoot) {
228
+ const inspectorDir = resolve2(resolveRigServerPaths(projectRoot).stateDir, "inspector");
229
+ return {
230
+ inspectorDir,
231
+ missionsDir: join(inspectorDir, "missions"),
232
+ journalsDir: join(inspectorDir, "mission-journals")
233
+ };
234
+ }
235
+ function createInspectorMissionController(options) {
236
+ const paths = resolveInspectorMissionPaths(options.projectRoot);
237
+ mkdirSync2(paths.missionsDir, { recursive: true });
238
+ mkdirSync2(paths.journalsDir, { recursive: true });
239
+ const now = options.now ?? (() => new Date().toISOString());
240
+ const nextId = options.idGenerator ?? (() => `mission:${randomUUID()}`);
241
+ function missionPath(missionId) {
242
+ return join(paths.missionsDir, `${missionId}.json`);
243
+ }
244
+ function journalPath(missionId) {
245
+ return join(paths.journalsDir, `${missionId}.jsonl`);
246
+ }
247
+ function appendMissionJournal(entry) {
248
+ mkdirSync2(paths.journalsDir, { recursive: true });
249
+ appendFileSync(journalPath(entry.missionId), `${JSON.stringify(entry)}
250
+ `, "utf8");
251
+ }
252
+ function listMissionJournal(missionId) {
253
+ const path = journalPath(missionId);
254
+ if (!existsSync2(path))
255
+ return [];
256
+ return readFileSync(path, "utf8").split(`
257
+ `).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line)).filter(isRecord).map((entry) => ({
258
+ id: typeof entry.id === "string" ? entry.id : `journal:${randomUUID()}`,
259
+ missionId,
260
+ kind: typeof entry.kind === "string" ? entry.kind : "mission.event",
261
+ summary: typeof entry.summary === "string" ? entry.summary : "Mission event",
262
+ actor: typeof entry.actor === "string" ? entry.actor : null,
263
+ details: toJsonRecord(entry.details),
264
+ createdAt: typeof entry.createdAt === "string" ? entry.createdAt : new Date(0).toISOString()
265
+ }));
266
+ }
267
+ function saveMission(mission) {
268
+ writeJsonFile(missionPath(mission.missionId), mission);
269
+ }
270
+ function readMissionOnly(missionId) {
271
+ const path = missionPath(missionId);
272
+ if (!existsSync2(path)) {
273
+ return { ok: false, error: `Mission ${missionId} was not found` };
274
+ }
275
+ const read = readJsonRecord(path);
276
+ if (!read.ok)
277
+ return read;
278
+ return parseMission(read.record);
279
+ }
280
+ function recordDecision(mission, input) {
281
+ options.journal?.appendDecision({
282
+ id: `decision:${mission.missionId}:${input.type}:${randomUUID()}`,
283
+ runId: mission.missionId,
284
+ dedupeKey: null,
285
+ decisionType: input.type,
286
+ summary: input.summary,
287
+ rationale: input.rationale,
288
+ details: input.details ?? missionActionDetails(mission),
289
+ createdAt: input.at
290
+ });
291
+ }
292
+ function recordAction(mission, input) {
293
+ options.journal?.appendAction({
294
+ id: `action:${mission.missionId}:${input.type}:${randomUUID()}`,
295
+ runId: mission.missionId,
296
+ dedupeKey: null,
297
+ actionType: input.type,
298
+ status: input.status,
299
+ target: input.target ?? `mission:${mission.missionId}`,
300
+ input: input.input ?? missionActionDetails(mission),
301
+ result: input.result ?? missionActionDetails(mission),
302
+ startedAt: input.startedAt,
303
+ completedAt: input.completedAt ?? input.startedAt
304
+ });
305
+ }
306
+ function appendEvent(mission, input) {
307
+ appendMissionJournal({
308
+ id: `event:${mission.missionId}:${input.kind}:${randomUUID()}`,
309
+ missionId: mission.missionId,
310
+ kind: input.kind,
311
+ summary: input.summary,
312
+ actor: input.actor ?? null,
313
+ details: input.details ?? missionActionDetails(mission),
314
+ createdAt: input.at
315
+ });
316
+ }
317
+ return {
318
+ paths,
319
+ createMission(input) {
320
+ const source = cloneJsonRecord(input.sourceTask);
321
+ const missionId = nextId();
322
+ const path = missionPath(missionId);
323
+ if (existsSync2(path)) {
324
+ const existing = readMissionOnly(missionId);
325
+ if (!existing.ok)
326
+ return existing;
327
+ return { ok: true, mission: existing.mission, journal: listMissionJournal(missionId) };
328
+ }
329
+ const at = now();
330
+ const mission = {
331
+ missionId,
332
+ owner: input.owner ?? "inspector-agent",
333
+ sourceTask: source,
334
+ status: "open",
335
+ summary: input.summary ?? (typeof source.title === "string" ? source.title : "Inspector mission"),
336
+ pendingEffects: [],
337
+ postedResults: [],
338
+ completionProof: null,
339
+ createdAt: at,
340
+ updatedAt: at
341
+ };
342
+ saveMission(mission);
343
+ appendEvent(mission, {
344
+ kind: "mission.created",
345
+ summary: mission.summary,
346
+ actor: mission.owner,
347
+ details: { ...missionActionDetails(mission), sourceTask: source },
348
+ at
349
+ });
350
+ recordDecision(mission, {
351
+ type: "mission.create",
352
+ summary: mission.summary,
353
+ rationale: "Inspector-agent accepted agency/controller ownership for the source task contract.",
354
+ details: { ...missionActionDetails(mission), sourceTask: source },
355
+ at
356
+ });
357
+ recordAction(mission, {
358
+ type: "mission.create",
359
+ status: "completed",
360
+ result: missionActionDetails(mission),
361
+ startedAt: at
362
+ });
363
+ return { ok: true, mission, journal: listMissionJournal(missionId) };
364
+ },
365
+ listMissions() {
366
+ return readdirSync(paths.missionsDir).filter((entry) => entry.endsWith(".json")).map((entry) => readMissionOnly(entry.slice(0, -".json".length))).filter((entry) => entry.ok).map((entry) => entry.mission).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
367
+ },
368
+ readMission(missionId) {
369
+ const read = readMissionOnly(missionId);
370
+ if (!read.ok)
371
+ return read;
372
+ return { ok: true, mission: read.mission, journal: listMissionJournal(missionId) };
373
+ },
374
+ requestEffect(input) {
375
+ const read = readMissionOnly(input.missionId);
376
+ if (!read.ok)
377
+ return read;
378
+ const mission = read.mission;
379
+ if (terminalStatus(mission.status)) {
380
+ return { ok: false, error: `Mission ${mission.missionId} is terminal (${mission.status})` };
381
+ }
382
+ if (mission.pendingEffects.some((effect2) => effect2.effectId === input.effectId)) {
383
+ return { ok: false, error: `Mission ${mission.missionId} already has effect ${input.effectId}` };
384
+ }
385
+ const at = now();
386
+ const effect = {
387
+ effectId: input.effectId ?? `effect:${randomUUID()}`,
388
+ kind: input.kind,
389
+ executor: input.executor,
390
+ summary: input.summary,
391
+ input: input.input ?? null,
392
+ status: "pending",
393
+ requestedAt: at
394
+ };
395
+ mission.pendingEffects = [...mission.pendingEffects, effect];
396
+ mission.status = "awaiting_effect";
397
+ mission.updatedAt = at;
398
+ saveMission(mission);
399
+ appendEvent(mission, {
400
+ kind: "mission.effect.requested",
401
+ summary: input.summary,
402
+ actor: mission.owner,
403
+ details: { ...missionActionDetails(mission), effectId: effect.effectId, executor: effect.executor },
404
+ at
405
+ });
406
+ recordAction(mission, {
407
+ type: "mission.effect.request",
408
+ status: "completed",
409
+ target: effect.executor,
410
+ input: effect.input,
411
+ result: { ...missionActionDetails(mission), effectId: effect.effectId },
412
+ startedAt: at
413
+ });
414
+ return { ok: true, mission, journal: listMissionJournal(mission.missionId) };
415
+ },
416
+ postEffectResult(input) {
417
+ const read = readMissionOnly(input.missionId);
418
+ if (!read.ok)
419
+ return read;
420
+ const mission = read.mission;
421
+ if (terminalStatus(mission.status)) {
422
+ return { ok: false, error: `Mission ${mission.missionId} is terminal (${mission.status})` };
423
+ }
424
+ const effect = mission.pendingEffects.find((entry) => entry.effectId === input.effectId);
425
+ if (!effect) {
426
+ return { ok: false, error: `Mission ${mission.missionId} has no pending effect ${input.effectId}` };
427
+ }
428
+ if (effect.status !== "pending") {
429
+ const latestResult = mission.postedResults.filter((entry) => entry.effectId === input.effectId).at(-1);
430
+ if (!latestResult || latestResult.status !== "partial") {
431
+ return { ok: false, error: `Effect ${input.effectId} was already posted` };
432
+ }
433
+ }
434
+ const at = now();
435
+ effect.status = "posted";
436
+ const result = {
437
+ effectId: input.effectId,
438
+ status: input.status,
439
+ summary: input.summary,
440
+ result: input.result ?? null,
441
+ postedBy: input.postedBy ?? "provider-worker",
442
+ postedAt: at
443
+ };
444
+ mission.postedResults = [...mission.postedResults, result];
445
+ mission.status = missionStatusAfterPost(mission);
446
+ mission.updatedAt = at;
447
+ saveMission(mission);
448
+ appendEvent(mission, {
449
+ kind: "mission.effect.posted",
450
+ summary: input.summary,
451
+ actor: result.postedBy,
452
+ details: { ...missionActionDetails(mission), effectId: input.effectId, resultStatus: input.status },
453
+ at
454
+ });
455
+ recordAction(mission, {
456
+ type: "mission.effect.post",
457
+ status: input.status,
458
+ target: input.effectId,
459
+ input: { effectId: input.effectId },
460
+ result: result.result,
461
+ startedAt: at
462
+ });
463
+ if (input.status !== "completed") {
464
+ recordDecision(mission, {
465
+ type: "mission.block",
466
+ summary: input.summary,
467
+ rationale: "Provider-worker effect result was not complete; proof gate remains closed.",
468
+ details: { ...missionActionDetails(mission), effectId: input.effectId, resultStatus: input.status },
469
+ at
470
+ });
471
+ }
472
+ return { ok: true, mission, journal: listMissionJournal(mission.missionId) };
473
+ },
474
+ acceptMission(input) {
475
+ const read = readMissionOnly(input.missionId);
476
+ if (!read.ok)
477
+ return read;
478
+ const mission = read.mission;
479
+ const blockers = blockingResults(mission);
480
+ if (blockers.length > 0) {
481
+ return { ok: false, error: `Mission ${mission.missionId} has blocking effect result(s)` };
482
+ }
483
+ const pending = pendingEffects(mission);
484
+ if (pending.length > 0) {
485
+ return { ok: false, error: `Mission ${mission.missionId} has pending effect(s)` };
486
+ }
487
+ if (terminalStatus(mission.status)) {
488
+ return { ok: false, error: `Mission ${mission.missionId} is terminal (${mission.status})` };
489
+ }
490
+ const at = now();
491
+ const proof = {
492
+ proofId: `proof:${mission.missionId}:${randomUUID()}`,
493
+ missionId: mission.missionId,
494
+ sourceTaskId: sourceTaskId(mission.sourceTask),
495
+ status: "accepted",
496
+ acceptedBy: input.acceptedBy ?? "inspector-agent",
497
+ summary: input.summary,
498
+ evidence: input.evidence ?? [],
499
+ effectResultIds: latestEffectResults(mission).map((result) => result.effectId),
500
+ sourceTask: cloneJsonRecord(mission.sourceTask),
501
+ emittedAt: at
502
+ };
503
+ mission.status = "accepted";
504
+ mission.completionProof = proof;
505
+ mission.updatedAt = at;
506
+ saveMission(mission);
507
+ appendEvent(mission, {
508
+ kind: "mission.accepted",
509
+ summary: input.summary,
510
+ actor: proof.acceptedBy,
511
+ details: { ...missionActionDetails(mission), proofId: proof.proofId },
512
+ at
513
+ });
514
+ recordDecision(mission, {
515
+ type: "mission.accept",
516
+ summary: input.summary,
517
+ rationale: "All requested effects were posted as complete and the PR/source-task closeout readiness is accepted.",
518
+ details: { ...missionActionDetails(mission), proofId: proof.proofId, evidence: proof.evidence },
519
+ at
520
+ });
521
+ recordAction(mission, {
522
+ type: "mission.accept",
523
+ status: "completed",
524
+ result: { ...missionActionDetails(mission), proofId: proof.proofId },
525
+ startedAt: at
526
+ });
527
+ return { ok: true, mission, journal: listMissionJournal(mission.missionId), completionProof: proof };
528
+ },
529
+ rejectMission(input) {
530
+ const read = readMissionOnly(input.missionId);
531
+ if (!read.ok)
532
+ return read;
533
+ const mission = read.mission;
534
+ if (terminalStatus(mission.status)) {
535
+ return { ok: false, error: `Mission ${mission.missionId} is terminal (${mission.status})` };
536
+ }
537
+ const at = now();
538
+ mission.status = "rejected";
539
+ mission.completionProof = null;
540
+ mission.updatedAt = at;
541
+ saveMission(mission);
542
+ appendEvent(mission, {
543
+ kind: "mission.rejected",
544
+ summary: input.summary,
545
+ actor: input.rejectedBy ?? "inspector-agent",
546
+ details: { ...missionActionDetails(mission), rationale: input.rationale ?? "" },
547
+ at
548
+ });
549
+ recordDecision(mission, {
550
+ type: "mission.reject",
551
+ summary: input.summary,
552
+ rationale: input.rationale ?? "Inspector rejected mission closeout.",
553
+ details: missionActionDetails(mission),
554
+ at
555
+ });
556
+ recordAction(mission, {
557
+ type: "mission.reject",
558
+ status: "completed",
559
+ result: missionActionDetails(mission),
560
+ startedAt: at
561
+ });
562
+ return { ok: true, mission, journal: listMissionJournal(mission.missionId) };
563
+ },
564
+ blockMission(input) {
565
+ const read = readMissionOnly(input.missionId);
566
+ if (!read.ok)
567
+ return read;
568
+ const mission = read.mission;
569
+ if (terminalStatus(mission.status)) {
570
+ return { ok: false, error: `Mission ${mission.missionId} is terminal (${mission.status})` };
571
+ }
572
+ const at = now();
573
+ mission.status = "blocked";
574
+ mission.updatedAt = at;
575
+ saveMission(mission);
576
+ appendEvent(mission, {
577
+ kind: "mission.blocked",
578
+ summary: input.summary,
579
+ actor: input.blockedBy ?? "inspector-agent",
580
+ details: input.details ?? missionActionDetails(mission),
581
+ at
582
+ });
583
+ recordDecision(mission, {
584
+ type: "mission.block",
585
+ summary: input.summary,
586
+ rationale: "Inspector marked mission blocked; proof gate remains closed.",
587
+ details: input.details ?? missionActionDetails(mission),
588
+ at
589
+ });
590
+ return { ok: true, mission, journal: listMissionJournal(mission.missionId) };
591
+ },
592
+ completeMission(missionId) {
593
+ const read = readMissionOnly(missionId);
594
+ if (!read.ok)
595
+ return read;
596
+ const mission = read.mission;
597
+ if (mission.status !== "accepted" || !mission.completionProof) {
598
+ return { ok: false, error: `Mission ${missionId} must be accepted before completion proof can be completed` };
599
+ }
600
+ const at = now();
601
+ appendEvent(mission, {
602
+ kind: "mission.completed",
603
+ summary: "Mission completion proof read",
604
+ actor: mission.owner,
605
+ details: { ...missionActionDetails(mission), proofId: mission.completionProof.proofId },
606
+ at
607
+ });
608
+ recordAction(mission, {
609
+ type: "mission.complete",
610
+ status: "completed",
611
+ result: { ...missionActionDetails(mission), proofId: mission.completionProof.proofId },
612
+ startedAt: at
613
+ });
614
+ return {
615
+ ok: true,
616
+ mission,
617
+ journal: listMissionJournal(mission.missionId),
618
+ completionProof: mission.completionProof
619
+ };
620
+ }
621
+ };
622
+ }
623
+
624
+ // packages/server/src/inspector/review.ts
625
+ function buildLocalReviewRequest(options) {
626
+ return {
627
+ reviewerType: "local-reviewer",
628
+ runId: options.runId,
629
+ taskId: options.taskId,
630
+ focus: options.focus
631
+ };
632
+ }
633
+
634
+ // packages/server/src/inspector/skills.ts
635
+ var UNIVERSAL_REQUIRED_SKILLS = [
636
+ {
637
+ name: "test-driven-development",
638
+ rationale: "Write failing tests first for code and behavior changes unless a narrow exception is recorded."
639
+ }
640
+ ];
641
+ var INSPECTOR_REQUIRED_SKILLS = [
642
+ {
643
+ name: "verification-before-completion",
644
+ rationale: "Do not claim completion or stability without direct verification evidence."
645
+ },
646
+ {
647
+ name: "no-bs",
648
+ rationale: "Respect the machine no-bs gates, keep evidence up to date, and do not stop early."
649
+ }
650
+ ];
651
+ var UNIVERSAL_RECOMMENDED_SKILLS = [
652
+ {
653
+ name: "verification-before-completion",
654
+ rationale: "Gather direct evidence before claiming a repair, review result, or task completion."
655
+ },
656
+ {
657
+ name: "systematic-debugging",
658
+ rationale: "Use disciplined debugging when a run, harness path, or validator fails."
659
+ },
660
+ {
661
+ name: "dispatching-parallel-agents",
662
+ rationale: "Split independent analysis and review work when that shortens the critical path."
663
+ },
664
+ {
665
+ name: "subagent-driven-development",
666
+ rationale: "Delegate bounded work to specialist subagents while keeping one context owner."
667
+ },
668
+ {
669
+ name: "requesting-code-review",
670
+ rationale: "Run local review before landing meaningful implementation work."
671
+ },
672
+ {
673
+ name: "receiving-code-review",
674
+ rationale: "Handle review feedback rigorously instead of applying it blindly."
675
+ },
676
+ {
677
+ name: "using-git-worktrees",
678
+ rationale: "Use isolated worktrees for risky or parallel implementation branches."
679
+ },
680
+ {
681
+ name: "writing-plans",
682
+ rationale: "Plan multi-step changes explicitly when the work is broad or risky."
683
+ },
684
+ {
685
+ name: "executing-plans",
686
+ rationale: "Execute validated plans in a staged, reviewable way."
687
+ },
688
+ {
689
+ name: "writing-skills",
690
+ rationale: "Improve or add agent skills when the harness needs new reusable behavior."
691
+ },
692
+ {
693
+ name: "find-skills",
694
+ rationale: "Discover existing local skills before inventing new workflows."
695
+ },
696
+ {
697
+ name: "openai-docs",
698
+ rationale: "Use primary OpenAI documentation when the inspector needs current provider facts."
699
+ },
700
+ {
701
+ name: "linear",
702
+ rationale: "Create or update follow-up work in the task tracker when oversight detects needed action."
703
+ },
704
+ {
705
+ name: "playwright",
706
+ rationale: "Inspect browser flows directly when a run touches web behavior or UI verification."
707
+ },
708
+ {
709
+ name: "playwright-interactive",
710
+ rationale: "Keep a live browser session when iterative UI inspection is more efficient than restarts."
711
+ }
712
+ ];
713
+ var INSPECTOR_ONLY_RECOMMENDED_SKILLS = [
714
+ {
715
+ name: "vercel:agent-browser",
716
+ rationale: "Perform high-fidelity browser inspection when a run or preview needs interactive verification."
717
+ },
718
+ {
719
+ name: "vercel:agent-browser-verify",
720
+ rationale: "Run an automated visual gut-check against dev servers and previews."
721
+ },
722
+ {
723
+ name: "vercel:verification",
724
+ rationale: "Verify end-to-end browser to API behavior when the change spans multiple surfaces."
725
+ },
726
+ {
727
+ name: "vercel:investigation-mode",
728
+ rationale: "Triages broken or stuck flows systematically when the harness or a preview misbehaves."
729
+ }
730
+ ];
731
+ function uniqueSkills(skills) {
732
+ const seen = new Set;
733
+ const deduped = [];
734
+ for (const skill of skills) {
735
+ if (seen.has(skill.name)) {
736
+ continue;
737
+ }
738
+ seen.add(skill.name);
739
+ deduped.push(skill);
740
+ }
741
+ return deduped;
742
+ }
743
+ function buildInspectorSkillCatalog(options) {
744
+ const required = [...UNIVERSAL_REQUIRED_SKILLS];
745
+ const recommended = [...UNIVERSAL_RECOMMENDED_SKILLS];
746
+ if (options.role === "inspector") {
747
+ required.push(...INSPECTOR_REQUIRED_SKILLS);
748
+ recommended.push(...INSPECTOR_ONLY_RECOMMENDED_SKILLS);
749
+ } else if (options.role === "reviewer") {
750
+ recommended.push({
751
+ name: "receiving-code-review",
752
+ rationale: "Review lanes should reason about findings carefully and defensibly."
753
+ });
754
+ }
755
+ if (options.taskKind === "bugfix") {
756
+ recommended.push({
757
+ name: "systematic-debugging",
758
+ rationale: "Bugfix work should start from a disciplined failure-class analysis."
759
+ });
760
+ }
761
+ return {
762
+ required: uniqueSkills(required),
763
+ recommended: uniqueSkills(recommended)
764
+ };
765
+ }
766
+ function buildSkillProvisioningDecision(options) {
767
+ const exceptionRecorded = Boolean(options.exception);
768
+ const catalog = buildInspectorSkillCatalog({
769
+ role: options.role,
770
+ taskKind: options.taskKind
771
+ });
772
+ const requiredSkills = catalog.required.map((skill) => skill.name);
773
+ const preferredSkills = catalog.recommended.map((skill) => skill.name);
774
+ return {
775
+ role: options.role,
776
+ taskKind: options.taskKind,
777
+ requiredSkills,
778
+ preferredSkills,
779
+ tddMode: exceptionRecorded ? "exception-recorded" : "required",
780
+ exceptionRecorded,
781
+ exception: options.exception ?? null
782
+ };
783
+ }
784
+
785
+ // packages/server/src/inspector/tools.ts
786
+ function toJsonRecord2(value, fallbackKey = "value") {
787
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
788
+ if (value == null) {
789
+ return null;
790
+ }
791
+ return { [fallbackKey]: value };
792
+ }
793
+ return value;
794
+ }
795
+ function toMissionJsonRecord(value) {
796
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
797
+ return null;
798
+ }
799
+ const parsed = JSON.parse(JSON.stringify(value));
800
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
801
+ return null;
802
+ }
803
+ return parsed;
804
+ }
805
+ function toMissionJsonRecordOrNull(value) {
806
+ if (value == null)
807
+ return null;
808
+ return toMissionJsonRecord(value);
809
+ }
810
+ function missionFailureStatus(error) {
811
+ if (error.includes("was not found"))
812
+ return "missing";
813
+ return "blocked";
814
+ }
815
+ function activeRunStatuses() {
816
+ return new Set(["preparing", "running", "validating", "reviewing"]);
817
+ }
818
+ function stableFollowupDedupeKey(originRunId, summary) {
819
+ return `followup:${originRunId ?? "global"}:${summary.trim().toLowerCase()}`;
820
+ }
821
+ function createInspectorToolRegistry(options) {
822
+ const now = options.now ?? (() => new Date().toISOString());
823
+ const missionController = options.projectRoot ? createInspectorMissionController({
824
+ projectRoot: options.projectRoot,
825
+ journal: options.journal,
826
+ now,
827
+ idGenerator: options.missionIdGenerator
828
+ }) : null;
829
+ const descriptors = {
830
+ read_inspector_snapshot: {
831
+ enabled: Boolean(options.snapshotReader),
832
+ async handler() {
833
+ if (!options.snapshotReader) {
834
+ return {
835
+ status: "unavailable",
836
+ summary: "read_inspector_snapshot is not configured",
837
+ details: null
838
+ };
839
+ }
840
+ const snapshot = options.snapshotReader();
841
+ return {
842
+ status: "completed",
843
+ summary: "Read current inspector snapshot",
844
+ details: snapshot
845
+ };
846
+ }
847
+ },
848
+ discover_active_runs: {
849
+ enabled: true,
850
+ async handler() {
851
+ const runs = options.discoverRuns().filter((run) => activeRunStatuses().has(run.status));
852
+ return {
853
+ status: "completed",
854
+ summary: `Discovered ${runs.length} active run(s)`,
855
+ details: runs
856
+ };
857
+ }
858
+ },
859
+ inspect_run: {
860
+ enabled: true,
861
+ async handler(input) {
862
+ const runId = typeof input.runId === "string" ? input.runId : "";
863
+ const run = options.discoverRuns().find((entry) => entry.runId === runId) ?? null;
864
+ if (!run) {
865
+ return {
866
+ status: "missing",
867
+ summary: `Run ${runId} was not found`,
868
+ details: null
869
+ };
870
+ }
871
+ const session = normalizeProviderSession(run);
872
+ const findings = options.journal.listRecentFindings({ limit: 50 }).filter((entry) => entry.runId === runId);
873
+ return {
874
+ status: "completed",
875
+ summary: `Inspected run ${runId}`,
876
+ details: {
877
+ run,
878
+ session,
879
+ capabilities: deriveInspectorCapabilitySet(session, options.capabilitySupport),
880
+ observations: findings,
881
+ paths: {
882
+ workspaceDir: run.worktreePath,
883
+ artifactRoot: run.artifactRoot,
884
+ logRoot: run.logRoot,
885
+ sessionPath: run.sessionPath,
886
+ sessionLogPath: run.sessionLogPath
887
+ }
888
+ }
889
+ };
890
+ }
891
+ },
892
+ record_inspector_note: {
893
+ enabled: true,
894
+ async handler(input) {
895
+ const runId = typeof input.runId === "string" ? input.runId : null;
896
+ const summary = typeof input.summary === "string" ? input.summary : "Inspector note";
897
+ const details = toJsonRecord2(input.details);
898
+ const result = options.journal.appendObservation({
899
+ id: `note:${runId ?? "global"}:${randomUUID2()}`,
900
+ runId,
901
+ dedupeKey: null,
902
+ kind: "inspector.note",
903
+ severity: "info",
904
+ source: "inspector",
905
+ summary,
906
+ details,
907
+ createdAt: now()
908
+ });
909
+ return {
910
+ status: result.ok ? "completed" : "failed",
911
+ summary,
912
+ details: result.ok ? result.record : { error: result.error }
913
+ };
914
+ }
915
+ },
916
+ list_inspector_missions: {
917
+ enabled: Boolean(missionController),
918
+ async handler() {
919
+ if (!missionController) {
920
+ return { status: "unavailable", summary: "mission storage is not configured", details: null };
921
+ }
922
+ const missions = missionController.listMissions();
923
+ return {
924
+ status: "completed",
925
+ summary: `Listed ${missions.length} inspector mission(s)`,
926
+ details: { missions }
927
+ };
928
+ }
929
+ },
930
+ create_inspector_mission: {
931
+ enabled: Boolean(missionController),
932
+ async handler(input) {
933
+ if (!missionController) {
934
+ return { status: "unavailable", summary: "mission storage is not configured", details: null };
935
+ }
936
+ const sourceTask = toMissionJsonRecord(input.sourceTask);
937
+ if (!sourceTask) {
938
+ return { status: "failed", summary: "sourceTask must be a JSON object", details: null };
939
+ }
940
+ const result = missionController.createMission({
941
+ sourceTask,
942
+ owner: typeof input.owner === "string" ? input.owner : undefined,
943
+ summary: typeof input.summary === "string" ? input.summary : undefined
944
+ });
945
+ if (!result.ok) {
946
+ return { status: missionFailureStatus(result.error), summary: result.error, details: null };
947
+ }
948
+ return {
949
+ status: "completed",
950
+ summary: `Created inspector mission ${result.mission.missionId}`,
951
+ details: { ...result.mission, journal: result.journal }
952
+ };
953
+ }
954
+ },
955
+ read_inspector_mission: {
956
+ enabled: Boolean(missionController),
957
+ async handler(input) {
958
+ if (!missionController) {
959
+ return { status: "unavailable", summary: "mission storage is not configured", details: null };
960
+ }
961
+ const missionId = typeof input.missionId === "string" ? input.missionId : "";
962
+ const result = missionController.readMission(missionId);
963
+ if (!result.ok) {
964
+ return { status: missionFailureStatus(result.error), summary: result.error, details: null };
965
+ }
966
+ return {
967
+ status: "completed",
968
+ summary: `Read inspector mission ${missionId}`,
969
+ details: { ...result.mission, journal: result.journal }
970
+ };
971
+ }
972
+ },
973
+ request_inspector_effect: {
974
+ enabled: Boolean(missionController),
975
+ async handler(input) {
976
+ if (!missionController) {
977
+ return { status: "unavailable", summary: "mission storage is not configured", details: null };
978
+ }
979
+ const missionId = typeof input.missionId === "string" ? input.missionId : "";
980
+ const kind = typeof input.kind === "string" ? input.kind : "provider_worker";
981
+ const executor = typeof input.executor === "string" ? input.executor : "worker";
982
+ const summary = typeof input.summary === "string" ? input.summary : "Inspector effect requested";
983
+ const result = missionController.requestEffect({
984
+ missionId,
985
+ effectId: typeof input.effectId === "string" ? input.effectId : undefined,
986
+ kind,
987
+ executor,
988
+ summary,
989
+ input: toMissionJsonRecordOrNull(input.input)
990
+ });
991
+ if (!result.ok) {
992
+ return { status: missionFailureStatus(result.error), summary: result.error, details: null };
993
+ }
994
+ return {
995
+ status: "completed",
996
+ summary,
997
+ details: { ...result.mission, journal: result.journal }
998
+ };
999
+ }
1000
+ },
1001
+ post_inspector_effect_result: {
1002
+ enabled: Boolean(missionController),
1003
+ async handler(input) {
1004
+ if (!missionController) {
1005
+ return { status: "unavailable", summary: "mission storage is not configured", details: null };
1006
+ }
1007
+ const status = typeof input.status === "string" ? input.status : "failed";
1008
+ if (!["completed", "partial", "blocked", "failed"].includes(status)) {
1009
+ return { status: "failed", summary: `Unsupported effect result status: ${status}`, details: null };
1010
+ }
1011
+ const result = missionController.postEffectResult({
1012
+ missionId: typeof input.missionId === "string" ? input.missionId : "",
1013
+ effectId: typeof input.effectId === "string" ? input.effectId : "",
1014
+ status,
1015
+ summary: typeof input.summary === "string" ? input.summary : "Inspector effect result posted",
1016
+ result: toMissionJsonRecordOrNull(input.result),
1017
+ postedBy: typeof input.postedBy === "string" ? input.postedBy : undefined
1018
+ });
1019
+ if (!result.ok) {
1020
+ return { status: missionFailureStatus(result.error), summary: result.error, details: null };
1021
+ }
1022
+ return {
1023
+ status: result.mission.status === "blocked" ? "blocked" : "completed",
1024
+ summary: typeof input.summary === "string" ? input.summary : "Inspector effect result posted",
1025
+ details: { ...result.mission, journal: result.journal }
1026
+ };
1027
+ }
1028
+ },
1029
+ accept_inspector_mission: {
1030
+ enabled: Boolean(missionController),
1031
+ async handler(input) {
1032
+ if (!missionController) {
1033
+ return { status: "unavailable", summary: "mission storage is not configured", details: null };
1034
+ }
1035
+ const result = missionController.acceptMission({
1036
+ missionId: typeof input.missionId === "string" ? input.missionId : "",
1037
+ acceptedBy: typeof input.acceptedBy === "string" ? input.acceptedBy : undefined,
1038
+ summary: typeof input.summary === "string" ? input.summary : "Inspector mission accepted",
1039
+ evidence: Array.isArray(input.evidence) ? input.evidence.filter((entry) => typeof entry === "string") : undefined
1040
+ });
1041
+ if (!result.ok) {
1042
+ return { status: missionFailureStatus(result.error), summary: result.error, details: null };
1043
+ }
1044
+ return {
1045
+ status: "completed",
1046
+ summary: result.completionProof.summary,
1047
+ details: { ...result.mission, journal: result.journal, completionProof: result.completionProof }
1048
+ };
1049
+ }
1050
+ },
1051
+ reject_inspector_mission: {
1052
+ enabled: Boolean(missionController),
1053
+ async handler(input) {
1054
+ if (!missionController) {
1055
+ return { status: "unavailable", summary: "mission storage is not configured", details: null };
1056
+ }
1057
+ const result = missionController.rejectMission({
1058
+ missionId: typeof input.missionId === "string" ? input.missionId : "",
1059
+ rejectedBy: typeof input.rejectedBy === "string" ? input.rejectedBy : undefined,
1060
+ summary: typeof input.summary === "string" ? input.summary : "Inspector mission rejected",
1061
+ rationale: typeof input.rationale === "string" ? input.rationale : undefined
1062
+ });
1063
+ if (!result.ok) {
1064
+ return { status: missionFailureStatus(result.error), summary: result.error, details: null };
1065
+ }
1066
+ return {
1067
+ status: "completed",
1068
+ summary: result.mission.summary,
1069
+ details: { ...result.mission, journal: result.journal }
1070
+ };
1071
+ }
1072
+ },
1073
+ block_inspector_mission: {
1074
+ enabled: Boolean(missionController),
1075
+ async handler(input) {
1076
+ if (!missionController) {
1077
+ return { status: "unavailable", summary: "mission storage is not configured", details: null };
1078
+ }
1079
+ const result = missionController.blockMission({
1080
+ missionId: typeof input.missionId === "string" ? input.missionId : "",
1081
+ blockedBy: typeof input.blockedBy === "string" ? input.blockedBy : undefined,
1082
+ summary: typeof input.summary === "string" ? input.summary : "Inspector mission blocked",
1083
+ details: toMissionJsonRecordOrNull(input.details)
1084
+ });
1085
+ if (!result.ok) {
1086
+ return { status: missionFailureStatus(result.error), summary: result.error, details: null };
1087
+ }
1088
+ return {
1089
+ status: "blocked",
1090
+ summary: result.mission.summary,
1091
+ details: { ...result.mission, journal: result.journal }
1092
+ };
1093
+ }
1094
+ },
1095
+ complete_inspector_mission: {
1096
+ enabled: Boolean(missionController),
1097
+ async handler(input) {
1098
+ if (!missionController) {
1099
+ return { status: "unavailable", summary: "mission storage is not configured", details: null };
1100
+ }
1101
+ const missionId = typeof input.missionId === "string" ? input.missionId : "";
1102
+ const result = missionController.completeMission(missionId);
1103
+ if (!result.ok) {
1104
+ return { status: missionFailureStatus(result.error), summary: result.error, details: null };
1105
+ }
1106
+ return {
1107
+ status: "completed",
1108
+ summary: `Completed inspector mission ${missionId}`,
1109
+ details: { ...result.mission, journal: result.journal, completionProof: result.completionProof }
1110
+ };
1111
+ }
1112
+ },
1113
+ create_followup_task: {
1114
+ enabled: true,
1115
+ async handler(input) {
1116
+ const originRunId = typeof input.originRunId === "string" ? input.originRunId : null;
1117
+ const summary = typeof input.summary === "string" ? input.summary : "Inspector follow-up";
1118
+ const details = toJsonRecord2(input.details);
1119
+ const dedupeKey = typeof input.dedupeKey === "string" && input.dedupeKey.trim().length > 0 ? input.dedupeKey.trim() : stableFollowupDedupeKey(originRunId, summary);
1120
+ const createdAt = now();
1121
+ const record = {
1122
+ id: `followup:${randomUUID2()}`,
1123
+ originRunId,
1124
+ dedupeKey,
1125
+ taskId: null,
1126
+ kind: "followup-task",
1127
+ status: "proposed",
1128
+ summary,
1129
+ details,
1130
+ createdAt
1131
+ };
1132
+ const result = options.journal.appendFollowup(record);
1133
+ let createdTask = null;
1134
+ if (result.ok && result.changed && options.followupTaskRunner) {
1135
+ createdTask = await options.followupTaskRunner({
1136
+ ...input,
1137
+ summary,
1138
+ originRunId,
1139
+ dedupeKey
1140
+ });
1141
+ const taskDetails = toJsonRecord2(createdTask.details);
1142
+ const taskId = typeof taskDetails?.taskId === "string" ? taskDetails.taskId : null;
1143
+ if (taskId) {
1144
+ options.journal.attachFollowupTask({
1145
+ dedupeKey,
1146
+ taskId,
1147
+ status: "created",
1148
+ details: taskDetails
1149
+ });
1150
+ }
1151
+ }
1152
+ options.journal.appendAction({
1153
+ id: `action:followup:${randomUUID2()}`,
1154
+ runId: originRunId,
1155
+ dedupeKey: null,
1156
+ actionType: "create_followup_task",
1157
+ status: createdTask?.status ?? (result.ok ? "completed" : "failed"),
1158
+ target: originRunId ? `run:${originRunId}` : "global",
1159
+ input: {
1160
+ summary,
1161
+ dedupeKey
1162
+ },
1163
+ result: toJsonRecord2(createdTask?.details ?? record),
1164
+ startedAt: createdAt,
1165
+ completedAt: now()
1166
+ });
1167
+ return {
1168
+ status: createdTask?.status ?? (result.ok ? "completed" : "failed"),
1169
+ summary,
1170
+ details: {
1171
+ followup: result.ok ? record : { error: result.error },
1172
+ createdTask: createdTask?.details ?? null
1173
+ }
1174
+ };
1175
+ }
1176
+ },
1177
+ provision_skill: {
1178
+ enabled: true,
1179
+ async handler(input) {
1180
+ const role = typeof input.role === "string" ? input.role : "worker";
1181
+ const taskKind = typeof input.taskKind === "string" ? input.taskKind : "code-change";
1182
+ const exception = input.exception && typeof input.exception === "object" ? input.exception : null;
1183
+ const decision = buildSkillProvisioningDecision({
1184
+ role,
1185
+ taskKind,
1186
+ exception: exception && typeof exception.reason === "string" && typeof exception.category === "string" ? { reason: exception.reason, category: exception.category } : undefined
1187
+ });
1188
+ const createdAt = now();
1189
+ options.journal.appendDecision({
1190
+ id: `decision:skills:${randomUUID2()}`,
1191
+ runId: typeof input.runId === "string" ? input.runId : null,
1192
+ dedupeKey: null,
1193
+ decisionType: "skill-provisioning",
1194
+ summary: `Provisioned skills for ${role}`,
1195
+ rationale: "Inspector provisions role-aware skills before execution or review.",
1196
+ details: decision,
1197
+ createdAt
1198
+ });
1199
+ options.journal.appendAction({
1200
+ id: `action:skills:${randomUUID2()}`,
1201
+ runId: typeof input.runId === "string" ? input.runId : null,
1202
+ dedupeKey: null,
1203
+ actionType: "provision_skill",
1204
+ status: "completed",
1205
+ target: role,
1206
+ input: { role, taskKind },
1207
+ result: decision,
1208
+ startedAt: createdAt,
1209
+ completedAt: createdAt
1210
+ });
1211
+ return {
1212
+ status: "completed",
1213
+ summary: `Provisioned skills for ${role}`,
1214
+ details: decision
1215
+ };
1216
+ }
1217
+ },
1218
+ run_local_review: {
1219
+ enabled: Boolean(options.reviewRunner),
1220
+ async handler(input) {
1221
+ if (!options.reviewRunner) {
1222
+ return {
1223
+ status: "unavailable",
1224
+ summary: "run_local_review is not configured",
1225
+ details: null
1226
+ };
1227
+ }
1228
+ const request = buildLocalReviewRequest({
1229
+ runId: typeof input.runId === "string" ? input.runId : "",
1230
+ taskId: typeof input.taskId === "string" ? input.taskId : null,
1231
+ focus: Array.isArray(input.focus) ? input.focus.filter((entry) => typeof entry === "string") : []
1232
+ });
1233
+ const startedAt = now();
1234
+ const result = await options.reviewRunner(request);
1235
+ options.journal.appendReview({
1236
+ id: `review:${request.runId || "global"}:${randomUUID2()}`,
1237
+ runId: request.runId || null,
1238
+ dedupeKey: null,
1239
+ reviewerType: request.reviewerType,
1240
+ status: result.status,
1241
+ findings: {
1242
+ summary: result.summary,
1243
+ details: toJsonRecord2(result.details)
1244
+ },
1245
+ createdAt: startedAt
1246
+ });
1247
+ options.journal.appendAction({
1248
+ id: `action:review:${randomUUID2()}`,
1249
+ runId: request.runId || null,
1250
+ dedupeKey: null,
1251
+ actionType: "run_local_review",
1252
+ status: result.status,
1253
+ target: request.taskId ?? request.runId,
1254
+ input: request,
1255
+ result: {
1256
+ summary: result.summary,
1257
+ details: toJsonRecord2(result.details)
1258
+ },
1259
+ startedAt,
1260
+ completedAt: now()
1261
+ });
1262
+ return result;
1263
+ }
1264
+ },
1265
+ scan_upstream_drift: {
1266
+ enabled: Boolean(options.upstreamScanRunner),
1267
+ async handler(input) {
1268
+ if (!options.upstreamScanRunner) {
1269
+ return {
1270
+ status: "unavailable",
1271
+ summary: "scan_upstream_drift is not configured",
1272
+ details: null
1273
+ };
1274
+ }
1275
+ const startedAt = now();
1276
+ const result = await options.upstreamScanRunner(input);
1277
+ options.journal.appendAction({
1278
+ id: `action:upstream:${randomUUID2()}`,
1279
+ runId: typeof input.runId === "string" ? input.runId : null,
1280
+ dedupeKey: null,
1281
+ actionType: "scan_upstream_drift",
1282
+ status: result.status,
1283
+ target: "workspace",
1284
+ input: toJsonRecord2(input),
1285
+ result: {
1286
+ summary: result.summary,
1287
+ details: toJsonRecord2(result.details)
1288
+ },
1289
+ startedAt,
1290
+ completedAt: now()
1291
+ });
1292
+ return result;
1293
+ }
1294
+ },
1295
+ generate_analysis_report: {
1296
+ enabled: Boolean(options.analysisRunner),
1297
+ async handler(input) {
1298
+ if (!options.analysisRunner) {
1299
+ return {
1300
+ status: "unavailable",
1301
+ summary: "generate_analysis_report is not configured",
1302
+ details: null
1303
+ };
1304
+ }
1305
+ const startedAt = now();
1306
+ const result = await options.analysisRunner(input);
1307
+ options.journal.appendAction({
1308
+ id: `action:analysis:${randomUUID2()}`,
1309
+ runId: typeof input.runId === "string" ? input.runId : null,
1310
+ dedupeKey: null,
1311
+ actionType: "generate_analysis_report",
1312
+ status: result.status,
1313
+ target: typeof input.reportType === "string" ? input.reportType : "default",
1314
+ input: toJsonRecord2(input),
1315
+ result: {
1316
+ summary: result.summary,
1317
+ details: toJsonRecord2(result.details)
1318
+ },
1319
+ startedAt,
1320
+ completedAt: now()
1321
+ });
1322
+ return result;
1323
+ }
1324
+ }
1325
+ };
1326
+ return {
1327
+ list() {
1328
+ return Object.entries(descriptors).filter(([, descriptor]) => descriptor.enabled).map(([name]) => name).sort();
1329
+ },
1330
+ async invoke(name, input) {
1331
+ const descriptor = descriptors[name];
1332
+ if (!descriptor) {
1333
+ return {
1334
+ status: "missing",
1335
+ summary: `Unknown inspector tool: ${name}`,
1336
+ details: null
1337
+ };
1338
+ }
1339
+ if (!descriptor.enabled) {
1340
+ return {
1341
+ status: "unavailable",
1342
+ summary: `${name} is not configured`,
1343
+ details: null
1344
+ };
1345
+ }
1346
+ return descriptor.handler(input);
1347
+ }
1348
+ };
1349
+ }
1350
+
1351
+ // packages/server/src/inspector/discovery.ts
1352
+ import {
1353
+ runStatus
1354
+ } from "@rig/runtime/control-plane/native/run-ops";
1355
+ import {
1356
+ listAuthorityRuns,
1357
+ readAuthorityRun
1358
+ } from "@rig/runtime/control-plane/authority-files";
1359
+ function providerFromRuntimeAdapter(runtimeAdapter) {
1360
+ if (!runtimeAdapter) {
1361
+ return null;
1362
+ }
1363
+ return runtimeAdapter;
1364
+ }
1365
+ function uniqueStrings2(values) {
1366
+ return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))];
1367
+ }
1368
+ function preferString(...values) {
1369
+ return values.find((value) => typeof value === "string" && value.length > 0) ?? null;
1370
+ }
1371
+ function chooseStatus(surfaces) {
1372
+ const authority = surfaces.find((surface) => surface.source === "authority-record");
1373
+ if (authority?.status) {
1374
+ return authority.status;
1375
+ }
1376
+ return preferString(...surfaces.map((surface) => surface.status)) ?? "unknown";
1377
+ }
1378
+ function chooseUpdatedAt(surfaces) {
1379
+ const timestamps = uniqueStrings2(surfaces.map((surface) => surface.updatedAt)).sort();
1380
+ return timestamps.at(-1) ?? "";
1381
+ }
1382
+ function buildConflict(surfaces) {
1383
+ const statuses = uniqueStrings2(surfaces.map((surface) => surface.status));
1384
+ const titles = uniqueStrings2(surfaces.map((surface) => surface.title));
1385
+ const activeSurface = surfaces.find((surface) => new Set(["preparing", "running", "validating", "reviewing"]).has(surface.status));
1386
+ const authoritySurface = surfaces.find((surface) => surface.source === "authority-record");
1387
+ if (activeSurface && authoritySurface && new Set(["failed", "stopped", "completed", "done"]).has(authoritySurface.status)) {
1388
+ return {
1389
+ state: "stale-active",
1390
+ summary: `Run ${authoritySurface.runId} is still reported active by ${activeSurface.source} but authority says ${authoritySurface.status}.`
1391
+ };
1392
+ }
1393
+ if (statuses.length > 1 || titles.length > 1) {
1394
+ const sourceSummary = surfaces.map((surface) => `${surface.source}:${surface.status}:${surface.title}`).join(" | ");
1395
+ return {
1396
+ state: "source-disagreement",
1397
+ summary: `Conflicting run surfaces for ${surfaces[0]?.runId ?? "unknown"}: ${sourceSummary}`
1398
+ };
1399
+ }
1400
+ return { state: "none", summary: null };
1401
+ }
1402
+ function mergeRunSurfaces(surfaces) {
1403
+ const authority = surfaces.find((surface) => surface.source === "authority-record") ?? null;
1404
+ const conflict = buildConflict(surfaces);
1405
+ return {
1406
+ runId: surfaces[0].runId,
1407
+ workspaceId: preferString(authority?.workspaceId, ...surfaces.map((surface) => surface.workspaceId)),
1408
+ taskId: preferString(authority?.taskId, ...surfaces.map((surface) => surface.taskId)),
1409
+ provider: preferString(authority?.provider, ...surfaces.map((surface) => surface.provider)),
1410
+ runtimeAdapter: preferString(authority?.runtimeAdapter, ...surfaces.map((surface) => surface.runtimeAdapter)),
1411
+ threadId: preferString(authority?.threadId, ...surfaces.map((surface) => surface.threadId)),
1412
+ status: chooseStatus(surfaces),
1413
+ title: preferString(authority?.title, ...surfaces.map((surface) => surface.title)) ?? surfaces[0].runId,
1414
+ source: authority?.source ?? surfaces[0].source,
1415
+ rawSourceKinds: uniqueStrings2(surfaces.map((surface) => surface.source)),
1416
+ conflictState: conflict.state,
1417
+ conflictSummary: conflict.summary,
1418
+ worktreePath: preferString(authority?.worktreePath, ...surfaces.map((surface) => surface.worktreePath)),
1419
+ artifactRoot: preferString(authority?.artifactRoot, ...surfaces.map((surface) => surface.artifactRoot)),
1420
+ logRoot: preferString(authority?.logRoot, ...surfaces.map((surface) => surface.logRoot)),
1421
+ sessionPath: preferString(authority?.sessionPath, ...surfaces.map((surface) => surface.sessionPath)),
1422
+ sessionLogPath: preferString(authority?.sessionLogPath, ...surfaces.map((surface) => surface.sessionLogPath)),
1423
+ startedAt: preferString(authority?.startedAt, ...surfaces.map((surface) => surface.startedAt)),
1424
+ updatedAt: chooseUpdatedAt(surfaces),
1425
+ completedAt: preferString(authority?.completedAt, ...surfaces.map((surface) => surface.completedAt))
1426
+ };
1427
+ }
1428
+ function discoverInspectorRuns(options) {
1429
+ const summary = options.statusSummary ?? runStatus(options.projectRoot);
1430
+ const discovered = new Map;
1431
+ const pushSurface = (surface) => {
1432
+ const existing = discovered.get(surface.runId) ?? [];
1433
+ existing.push(surface);
1434
+ discovered.set(surface.runId, existing);
1435
+ };
1436
+ for (const authorityEntry of listAuthorityRuns(options.projectRoot)) {
1437
+ const run = readAuthorityRun(options.projectRoot, authorityEntry.runId);
1438
+ if (!run) {
1439
+ continue;
1440
+ }
1441
+ pushSurface({
1442
+ runId: run.runId,
1443
+ workspaceId: run.workspaceId ?? null,
1444
+ taskId: run.taskId ?? null,
1445
+ provider: providerFromRuntimeAdapter(run.runtimeAdapter ?? null),
1446
+ runtimeAdapter: run.runtimeAdapter ?? null,
1447
+ threadId: run.threadId ?? null,
1448
+ status: run.status ?? "unknown",
1449
+ title: run.title ?? run.taskId ?? run.runId,
1450
+ source: "authority-record",
1451
+ worktreePath: run.worktreePath ?? null,
1452
+ artifactRoot: run.artifactRoot ?? null,
1453
+ logRoot: run.logRoot ?? null,
1454
+ sessionPath: run.sessionPath ?? null,
1455
+ sessionLogPath: run.sessionLogPath ?? null,
1456
+ startedAt: run.startedAt ?? null,
1457
+ updatedAt: run.updatedAt,
1458
+ completedAt: run.completedAt ?? null
1459
+ });
1460
+ }
1461
+ for (const entry of [...summary.activeRuns, ...summary.recentRuns]) {
1462
+ pushSurface({
1463
+ runId: entry.runId,
1464
+ workspaceId: null,
1465
+ taskId: entry.taskId ?? null,
1466
+ provider: null,
1467
+ runtimeAdapter: null,
1468
+ threadId: null,
1469
+ status: entry.status,
1470
+ title: entry.title,
1471
+ source: "run-status",
1472
+ worktreePath: null,
1473
+ artifactRoot: null,
1474
+ logRoot: null,
1475
+ sessionPath: null,
1476
+ sessionLogPath: null,
1477
+ startedAt: null,
1478
+ updatedAt: "",
1479
+ completedAt: null
1480
+ });
1481
+ }
1482
+ return [...discovered.values()].map((surfaces) => mergeRunSurfaces(surfaces)).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
1483
+ }
1484
+
1485
+ // packages/server/src/inspector/reconcile.ts
1486
+ function decisionTypeForStatus(status) {
1487
+ switch (status) {
1488
+ case "failed":
1489
+ return "investigate-failure";
1490
+ case "stopped":
1491
+ return "inspect-stop";
1492
+ default:
1493
+ return null;
1494
+ }
1495
+ }
1496
+ function reconcileInspectorRuns(options) {
1497
+ const now = options.now ?? (() => new Date().toISOString());
1498
+ let observedCount = 0;
1499
+ let decisionCount = 0;
1500
+ let actionCount = 0;
1501
+ for (const run of options.runs) {
1502
+ options.journal.upsertRun({
1503
+ runId: run.runId,
1504
+ workspaceId: run.workspaceId,
1505
+ taskId: run.taskId,
1506
+ runtimeAdapter: run.runtimeAdapter,
1507
+ provider: run.provider,
1508
+ status: run.status,
1509
+ conflictState: run.conflictState,
1510
+ conflictSummary: run.conflictSummary,
1511
+ source: run.source,
1512
+ title: run.title,
1513
+ startedAt: run.startedAt,
1514
+ updatedAt: run.updatedAt,
1515
+ completedAt: run.completedAt
1516
+ });
1517
+ const observation = options.journal.appendObservation({
1518
+ id: `observation:${run.runId}:${options.reason}:${run.updatedAt}`,
1519
+ runId: run.runId,
1520
+ dedupeKey: `run:${run.runId}:status:${run.status}:updated:${run.updatedAt}`,
1521
+ kind: "run.observed",
1522
+ severity: run.status === "failed" ? "warn" : "info",
1523
+ source: run.source,
1524
+ summary: `Observed run ${run.title}`,
1525
+ details: {
1526
+ reason: options.reason,
1527
+ status: run.status,
1528
+ provider: run.provider,
1529
+ runtimeAdapter: run.runtimeAdapter,
1530
+ conflictState: run.conflictState,
1531
+ rawSourceKinds: run.rawSourceKinds
1532
+ },
1533
+ createdAt: now()
1534
+ });
1535
+ if (observation.ok && observation.changed) {
1536
+ observedCount += 1;
1537
+ }
1538
+ const action = options.journal.appendAction({
1539
+ id: `action:${run.runId}:${options.reason}:${run.updatedAt}`,
1540
+ runId: run.runId,
1541
+ dedupeKey: `action:${run.runId}:observe:${run.updatedAt}`,
1542
+ actionType: "observe",
1543
+ status: "completed",
1544
+ target: `run:${run.runId}`,
1545
+ input: { reason: options.reason },
1546
+ result: { status: run.status },
1547
+ startedAt: now(),
1548
+ completedAt: now()
1549
+ });
1550
+ if (action.ok && action.changed) {
1551
+ actionCount += 1;
1552
+ }
1553
+ const decisionType = decisionTypeForStatus(run.status);
1554
+ if (decisionType) {
1555
+ const decision = options.journal.appendDecision({
1556
+ id: `decision:${run.runId}:${decisionType}:${run.updatedAt}`,
1557
+ runId: run.runId,
1558
+ dedupeKey: `decision:${run.runId}:${decisionType}:${run.updatedAt}`,
1559
+ decisionType,
1560
+ summary: `Investigate ${run.title}`,
1561
+ rationale: `Run status ${run.status} requires inspector attention.`,
1562
+ details: {
1563
+ reason: options.reason,
1564
+ status: run.status
1565
+ },
1566
+ createdAt: now()
1567
+ });
1568
+ if (decision.ok && decision.changed) {
1569
+ decisionCount += 1;
1570
+ }
1571
+ }
1572
+ if (run.conflictState !== "none") {
1573
+ const conflictDecision = options.journal.appendDecision({
1574
+ id: `decision:${run.runId}:conflict:${run.updatedAt}`,
1575
+ runId: run.runId,
1576
+ dedupeKey: `decision:${run.runId}:conflict:${run.updatedAt}`,
1577
+ decisionType: "inspect-conflict",
1578
+ summary: `Resolve ${run.title} observation conflict`,
1579
+ rationale: run.conflictSummary ?? "Observed run surfaces disagree and require inspector review.",
1580
+ details: {
1581
+ reason: options.reason,
1582
+ conflictState: run.conflictState,
1583
+ conflictSummary: run.conflictSummary
1584
+ },
1585
+ createdAt: now()
1586
+ });
1587
+ if (conflictDecision.ok && conflictDecision.changed) {
1588
+ decisionCount += 1;
1589
+ }
1590
+ }
1591
+ }
1592
+ return {
1593
+ discoveredCount: options.runs.length,
1594
+ observedCount,
1595
+ decisionCount,
1596
+ actionCount
1597
+ };
1598
+ }
1599
+
1600
+ // packages/server/src/inspector/service.ts
1601
+ function buildCapabilitySupport(options) {
1602
+ return {
1603
+ discoverRuns: true,
1604
+ inspectRuns: true,
1605
+ readLogs: true,
1606
+ readArtifacts: true,
1607
+ callServerApis: true,
1608
+ createTasks: Boolean(options.followupTaskRunner),
1609
+ runReviewerAgents: Boolean(options.reviewRunner),
1610
+ provisionSkills: true,
1611
+ scanUpstream: Boolean(options.upstreamSyncRunner),
1612
+ writeJournal: true,
1613
+ spawnSpecialists: Boolean(options.reviewRunner || options.analysisRunner || options.upstreamSyncRunner)
1614
+ };
1615
+ }
1616
+ var ACTIVE_RUN_STATUSES = new Set(["preparing", "running", "validating", "reviewing"]);
1617
+ function rankRunRecency(run) {
1618
+ const updatedAt = Date.parse(run.updatedAt);
1619
+ if (Number.isFinite(updatedAt)) {
1620
+ return updatedAt;
1621
+ }
1622
+ const startedAt = run.startedAt ? Date.parse(run.startedAt) : Number.NaN;
1623
+ if (Number.isFinite(startedAt)) {
1624
+ return startedAt;
1625
+ }
1626
+ return Number.NEGATIVE_INFINITY;
1627
+ }
1628
+ function visibleActiveRunKey(run) {
1629
+ if (!run.taskId) {
1630
+ return run.runId;
1631
+ }
1632
+ return [
1633
+ run.workspaceId ?? "workspace:unknown",
1634
+ run.taskId,
1635
+ run.worktreePath ?? "worktree:unknown"
1636
+ ].join("::");
1637
+ }
1638
+ function selectVisibleActiveRuns(runs) {
1639
+ const latestByKey = new Map;
1640
+ for (const run of runs) {
1641
+ if (!ACTIVE_RUN_STATUSES.has(run.status) || run.conflictState === "stale-active") {
1642
+ continue;
1643
+ }
1644
+ const key = visibleActiveRunKey(run);
1645
+ const current = latestByKey.get(key);
1646
+ if (!current || rankRunRecency(run) >= rankRunRecency(current)) {
1647
+ latestByKey.set(key, run);
1648
+ }
1649
+ }
1650
+ return [...latestByKey.values()].sort((left, right) => rankRunRecency(right) - rankRunRecency(left));
1651
+ }
1652
+ function createGlobalInspectorService(options) {
1653
+ const discoverRuns = options.discoverRuns ?? (() => discoverInspectorRuns({ projectRoot: options.projectRoot }));
1654
+ const now = options.now ?? (() => new Date().toISOString());
1655
+ const pollMs = Math.max(10, options.pollMs ?? 1000);
1656
+ const upstreamSyncMs = options.upstreamSyncMs == null ? 24 * 60 * 60 * 1000 : options.upstreamSyncMs > 0 ? Math.max(1000, options.upstreamSyncMs) : null;
1657
+ const upstreamSyncOnStart = options.upstreamSyncOnStart ?? false;
1658
+ const analysisMs = Math.max(1000, options.analysisMs ?? 60 * 60 * 1000);
1659
+ const capabilitySupport = buildCapabilitySupport(options);
1660
+ const readSnapshot = () => {
1661
+ const activeRuns = selectVisibleActiveRuns(discoverRuns()).map((run) => {
1662
+ const session = normalizeProviderSession(run);
1663
+ return {
1664
+ run,
1665
+ session,
1666
+ capabilities: deriveInspectorCapabilitySet(session, capabilitySupport)
1667
+ };
1668
+ });
1669
+ return {
1670
+ activeRuns,
1671
+ recentFindings: options.journal.listRecentFindings({ limit: 20 }),
1672
+ followups: options.journal.listFollowups(),
1673
+ analysisReports: options.journal.listAnalysisReports(),
1674
+ availableTools: toolRegistry.list()
1675
+ };
1676
+ };
1677
+ const toolRegistry = createInspectorToolRegistry({
1678
+ journal: options.journal,
1679
+ projectRoot: options.projectRoot,
1680
+ discoverRuns,
1681
+ snapshotReader: readSnapshot,
1682
+ reviewRunner: options.reviewRunner,
1683
+ upstreamScanRunner: options.upstreamSyncRunner,
1684
+ analysisRunner: options.analysisRunner,
1685
+ followupTaskRunner: options.followupTaskRunner,
1686
+ capabilitySupport,
1687
+ now
1688
+ });
1689
+ let pollTimer = null;
1690
+ let upstreamTimer = null;
1691
+ let analysisTimer = null;
1692
+ const reconcileOnce = (reason) => {
1693
+ return reconcileInspectorRuns({
1694
+ journal: options.journal,
1695
+ runs: discoverRuns(),
1696
+ reason,
1697
+ now
1698
+ });
1699
+ };
1700
+ return {
1701
+ projectRoot: options.projectRoot,
1702
+ start() {
1703
+ if (pollTimer || upstreamTimer || analysisTimer) {
1704
+ return false;
1705
+ }
1706
+ pollTimer = setInterval(() => {
1707
+ try {
1708
+ reconcileOnce("poll");
1709
+ } catch {}
1710
+ }, pollMs);
1711
+ if (options.upstreamSyncRunner) {
1712
+ if (upstreamSyncOnStart) {
1713
+ Promise.resolve(options.upstreamSyncRunner({ trigger: "startup" })).catch(() => {});
1714
+ }
1715
+ if (upstreamSyncMs !== null) {
1716
+ upstreamTimer = setInterval(() => {
1717
+ Promise.resolve(options.upstreamSyncRunner?.({ trigger: "scheduled" })).catch(() => {});
1718
+ }, upstreamSyncMs);
1719
+ }
1720
+ }
1721
+ if (options.analysisRunner) {
1722
+ analysisTimer = setInterval(() => {
1723
+ Promise.resolve(options.analysisRunner?.({ trigger: "scheduled", reportType: "run-health" })).catch(() => {});
1724
+ }, analysisMs);
1725
+ }
1726
+ return true;
1727
+ },
1728
+ stop() {
1729
+ if (pollTimer)
1730
+ clearInterval(pollTimer);
1731
+ if (upstreamTimer)
1732
+ clearInterval(upstreamTimer);
1733
+ if (analysisTimer)
1734
+ clearInterval(analysisTimer);
1735
+ pollTimer = null;
1736
+ upstreamTimer = null;
1737
+ analysisTimer = null;
1738
+ },
1739
+ isRunning() {
1740
+ return pollTimer !== null || upstreamTimer !== null || analysisTimer !== null;
1741
+ },
1742
+ snapshot() {
1743
+ return readSnapshot();
1744
+ },
1745
+ async invokeTool(name, input) {
1746
+ return toolRegistry.invoke(name, input);
1747
+ },
1748
+ reconcileOnce,
1749
+ async runUpstreamSyncOnce(input = {}) {
1750
+ await options.upstreamSyncRunner?.(input);
1751
+ },
1752
+ async runAnalysisOnce(input = {}) {
1753
+ await options.analysisRunner?.(input);
1754
+ }
1755
+ };
1756
+ }
1757
+ export {
1758
+ createGlobalInspectorService
1759
+ };