@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,4145 @@
1
+ // @bun
2
+ var __require = import.meta.require;
3
+
4
+ // packages/server/src/server-helpers/inspector-jobs.ts
5
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
6
+ import { dirname as dirname5, resolve as resolve5 } from "path";
7
+ import { readJsonFile } from "@rig/runtime/control-plane/authority-files";
8
+ import { resolveMonorepoRoot as resolveMonorepoRoot3 } from "@rig/runtime/control-plane/native/utils";
9
+ import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
10
+
11
+ // packages/server/src/inspector/agent-runtime.ts
12
+ import { spawn } from "child_process";
13
+ import { createHash } from "crypto";
14
+ import { accessSync, constants, realpathSync } from "fs";
15
+ import { createInterface } from "readline";
16
+
17
+ // packages/server/src/inspector/prompt.ts
18
+ function renderSkills(skills) {
19
+ return skills.map((skill) => `- ${skill.name}: ${skill.rationale}`).join(`
20
+ `);
21
+ }
22
+ function stableJson(value) {
23
+ return JSON.stringify(value, null, 2);
24
+ }
25
+ function compactSnapshot(snapshot) {
26
+ const recentFindings = Array.isArray(snapshot.recentFindings) ? snapshot.recentFindings.filter((finding) => {
27
+ const kind = typeof finding.kind === "string" ? finding.kind : "";
28
+ const severity = typeof finding.severity === "string" ? finding.severity : "";
29
+ const source = typeof finding.source === "string" ? finding.source : "";
30
+ if (source === "inspector-agent") {
31
+ return false;
32
+ }
33
+ return kind !== "run.observed" || severity === "warn" || severity === "error";
34
+ }) : [];
35
+ return {
36
+ activeRuns: snapshot.activeRuns ?? [],
37
+ recentFindings,
38
+ followups: snapshot.followups ?? [],
39
+ analysisReports: snapshot.analysisReports ?? [],
40
+ availableTools: snapshot.availableTools ?? []
41
+ };
42
+ }
43
+ function buildInspectorBaseInstructions(options) {
44
+ return [
45
+ "You are Rig Global Inspector, the permanent supervisor for this workspace.",
46
+ "You operate as an out-of-band overlay around Rig execution, not inside the worker execution flow.",
47
+ "The lower runtime and provider layers are inspector-blind. Keep them that way.",
48
+ "App-server turns are transport boundaries only. They are not your operating model.",
49
+ "Stay broad, autonomous, and continuous in your judgment. Use turns only as a way to receive updates and emit progress.",
50
+ "Use full contextual judgment. There is no fixed intervention ladder.",
51
+ "You may patch task code or Rig itself, interrupt runs, relaunch runs, create follow-up tasks, run reviews, and improve the harness when confidence is high.",
52
+ "Be decisive, but stay surgical. Prefer the smallest action that materially improves the current situation.",
53
+ "Stay aware of all observable runs, the harness state, upstream drift tasks, and repeated failure patterns.",
54
+ "Document important findings and decisions through the inspector journal tools so the system retains a durable memory of what happened.",
55
+ "Treat TDD as the default for code and behavior changes unless a narrow recorded exception applies.",
56
+ "Before landing meaningful implementation work, run local review or another verification path that produces direct evidence.",
57
+ "When a human is not needed, do not wait. When a human is needed, create the right follow-up or journal note instead of stalling silently.",
58
+ "",
59
+ `Project root: ${options.projectRoot}`,
60
+ "",
61
+ "Required skills:",
62
+ renderSkills(options.requiredSkills),
63
+ "",
64
+ "Recommended skills:",
65
+ renderSkills(options.recommendedSkills)
66
+ ].join(`
67
+ `);
68
+ }
69
+ function buildInspectorDeveloperInstructions(options) {
70
+ return [
71
+ "Operate pragmatically and keep momentum.",
72
+ "Use the native Codex capabilities and the semantic inspector tools together.",
73
+ "You are free to inspect, edit, test, relaunch, review, and continue as far as the current situation warrants.",
74
+ "Yield when you reach a useful checkpoint, need fresher external state, or detect that continued looping would be wasteful.",
75
+ "Prefer semantic inspector tools for journal, follow-up task, upstream-sync, review, and run-inspection operations.",
76
+ "Use shell, read, write, edit, and git directly when that is the fastest reliable way to diagnose or repair the system.",
77
+ "When you create or modify code, keep the red-green-refactor discipline visible in your reasoning and actions.",
78
+ "Before claiming something is fixed, verify it directly.",
79
+ "Spawn specialist child agents when parallel work is useful, but remain the full-context owner and take over when they are not doing the job well enough.",
80
+ "You are responsible both for task oversight and for Rig self-improvement when a clear win-win fix is available.",
81
+ "",
82
+ `Workspace root: ${options.projectRoot}`,
83
+ "",
84
+ "Skill catalog in scope:",
85
+ renderSkills([...options.requiredSkills, ...options.recommendedSkills])
86
+ ].join(`
87
+ `);
88
+ }
89
+ function buildInspectorTurnPrompt(options) {
90
+ const compact = compactSnapshot(options.snapshot);
91
+ return [
92
+ "Rig inspector update.",
93
+ `reason: ${options.reason}`,
94
+ `timestamp: ${options.now}`,
95
+ "Review the current state and use broad judgment about whether intervention is warranted.",
96
+ "You may take multiple actions inside this turn if that is the most effective way to improve the situation.",
97
+ "Do not become noisy or repetitive when nothing material changed.",
98
+ "When you reach a useful checkpoint, emit one compact operator update in this form:",
99
+ "STATUS: <observed|acted|escalated|waiting>",
100
+ "NOTE: <one or two concise sentences>",
101
+ "Then yield so the server can deliver the next update when needed.",
102
+ "",
103
+ "Current snapshot:",
104
+ stableJson(compact)
105
+ ].join(`
106
+ `);
107
+ }
108
+
109
+ // packages/server/src/inspector/tools.ts
110
+ import { randomUUID as randomUUID2 } from "crypto";
111
+
112
+ // packages/server/src/inspector/mission.ts
113
+ import { randomUUID } from "crypto";
114
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, renameSync, writeFileSync as writeFileSync2 } from "fs";
115
+ import { dirname as dirname2, join, resolve as resolve2 } from "path";
116
+
117
+ // packages/server/src/bootstrap.ts
118
+ import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "fs";
119
+ import { dirname, resolve } from "path";
120
+ import { RIG_DEFINITION_DIRNAME, resolveMonorepoRoot } from "@rig/runtime";
121
+ function normalizeOptionalString(value) {
122
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
123
+ }
124
+ function resolveRigServerPaths(projectRoot) {
125
+ const taskWorkspace = normalizeOptionalString(process.env.RIG_TASK_WORKSPACE);
126
+ const explicitStateDir = normalizeOptionalString(process.env.RIG_STATE_DIR);
127
+ const explicitLogsDir = normalizeOptionalString(process.env.RIG_LOGS_DIR);
128
+ const explicitSessionFile = normalizeOptionalString(process.env.RIG_SESSION_FILE);
129
+ const hostStateRoot = resolve(projectRoot, ".rig");
130
+ const monorepoRoot = resolveMonorepoRoot(projectRoot);
131
+ const monorepoStateRoot = resolve(monorepoRoot, ".rig");
132
+ const stateRoot = taskWorkspace ? resolve(taskWorkspace, ".rig") : explicitStateDir ? dirname(resolve(explicitStateDir)) : explicitLogsDir ? dirname(resolve(explicitLogsDir)) : explicitSessionFile ? dirname(dirname(resolve(explicitSessionFile))) : existsSync(hostStateRoot) ? hostStateRoot : monorepoStateRoot;
133
+ const stateDir = explicitStateDir ? resolve(explicitStateDir) : resolve(stateRoot, "state");
134
+ const logsDir = explicitLogsDir ? resolve(explicitLogsDir) : resolve(stateRoot, "logs");
135
+ 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");
136
+ return {
137
+ stateRoot,
138
+ stateDir,
139
+ logsDir,
140
+ controlPlaneEventsFile: resolve(logsDir, "control-plane.events.jsonl"),
141
+ taskConfigPath,
142
+ notificationsFile: resolve(projectRoot, "rig", "notifications", "targets.json"),
143
+ keybindingsPath: resolve(projectRoot, "rig", "keybindings.json")
144
+ };
145
+ }
146
+
147
+ // packages/server/src/inspector/mission.ts
148
+ function isJsonValue(value) {
149
+ if (value === null)
150
+ return true;
151
+ const valueType = typeof value;
152
+ if (valueType === "string" || valueType === "number" || valueType === "boolean") {
153
+ return Number.isFinite(value) || valueType !== "number";
154
+ }
155
+ if (Array.isArray(value)) {
156
+ return value.every(isJsonValue);
157
+ }
158
+ if (valueType === "object") {
159
+ for (const entry of Object.values(value)) {
160
+ if (!isJsonValue(entry))
161
+ return false;
162
+ }
163
+ return true;
164
+ }
165
+ return false;
166
+ }
167
+ function toJsonRecord(value) {
168
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
169
+ return null;
170
+ }
171
+ const record = {};
172
+ for (const [key, entry] of Object.entries(value)) {
173
+ if (isJsonValue(entry)) {
174
+ record[key] = entry;
175
+ }
176
+ }
177
+ return record;
178
+ }
179
+ function cloneJsonRecord(value) {
180
+ return JSON.parse(JSON.stringify(value));
181
+ }
182
+ function isRecord(value) {
183
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
184
+ }
185
+ function readJsonRecord(path) {
186
+ try {
187
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
188
+ if (!isRecord(parsed)) {
189
+ return { ok: false, error: `Mission file ${path} does not contain an object` };
190
+ }
191
+ return { ok: true, record: parsed };
192
+ } catch (error) {
193
+ const message = error instanceof Error ? error.message : String(error);
194
+ return { ok: false, error: message };
195
+ }
196
+ }
197
+ function parseMission(record) {
198
+ if (typeof record.missionId !== "string")
199
+ return { ok: false, error: "Mission is missing missionId" };
200
+ if (typeof record.owner !== "string")
201
+ return { ok: false, error: "Mission is missing owner" };
202
+ if (typeof record.status !== "string")
203
+ return { ok: false, error: "Mission is missing status" };
204
+ if (!isRecord(record.sourceTask))
205
+ return { ok: false, error: "Mission is missing sourceTask" };
206
+ if (!isJsonValue(record.sourceTask))
207
+ return { ok: false, error: "Mission sourceTask is not JSON-safe" };
208
+ const pendingEffects = Array.isArray(record.pendingEffects) ? record.pendingEffects.filter(isRecord).map((entry) => entry) : [];
209
+ const postedResults = Array.isArray(record.postedResults) ? record.postedResults.filter(isRecord).map((entry) => entry) : [];
210
+ const completionProof = isRecord(record.completionProof) ? record.completionProof : null;
211
+ return {
212
+ ok: true,
213
+ mission: {
214
+ missionId: record.missionId,
215
+ owner: record.owner,
216
+ sourceTask: cloneJsonRecord(record.sourceTask),
217
+ status: record.status,
218
+ summary: typeof record.summary === "string" ? record.summary : "Inspector mission",
219
+ pendingEffects,
220
+ postedResults,
221
+ completionProof,
222
+ createdAt: typeof record.createdAt === "string" ? record.createdAt : new Date(0).toISOString(),
223
+ updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : new Date(0).toISOString()
224
+ }
225
+ };
226
+ }
227
+ function sourceTaskId(sourceTask) {
228
+ for (const key of ["sourceTaskId", "taskId", "id"]) {
229
+ const value = sourceTask[key];
230
+ if (typeof value === "string" && value.trim().length > 0) {
231
+ return value;
232
+ }
233
+ }
234
+ return null;
235
+ }
236
+ function terminalStatus(status) {
237
+ return status === "accepted" || status === "rejected";
238
+ }
239
+ function latestEffectResults(mission) {
240
+ const latestByEffect = new Map;
241
+ for (const result of mission.postedResults) {
242
+ latestByEffect.set(result.effectId, result);
243
+ }
244
+ return [...latestByEffect.values()];
245
+ }
246
+ function blockingResults(mission) {
247
+ return latestEffectResults(mission).filter((result) => result.status !== "completed");
248
+ }
249
+ function pendingEffects(mission) {
250
+ return mission.pendingEffects.filter((effect) => effect.status === "pending");
251
+ }
252
+ function missionStatusAfterPost(mission) {
253
+ if (blockingResults(mission).length > 0)
254
+ return "blocked";
255
+ if (pendingEffects(mission).length > 0)
256
+ return "awaiting_effect";
257
+ return "open";
258
+ }
259
+ function missionActionDetails(mission) {
260
+ return {
261
+ missionId: mission.missionId,
262
+ status: mission.status,
263
+ sourceTaskId: sourceTaskId(mission.sourceTask)
264
+ };
265
+ }
266
+ function writeJsonFile(path, value) {
267
+ mkdirSync2(dirname2(path), { recursive: true });
268
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
269
+ writeFileSync2(tempPath, `${JSON.stringify(value, null, 2)}
270
+ `, "utf8");
271
+ renameSync(tempPath, path);
272
+ }
273
+ function resolveInspectorMissionPaths(projectRoot) {
274
+ const inspectorDir = resolve2(resolveRigServerPaths(projectRoot).stateDir, "inspector");
275
+ return {
276
+ inspectorDir,
277
+ missionsDir: join(inspectorDir, "missions"),
278
+ journalsDir: join(inspectorDir, "mission-journals")
279
+ };
280
+ }
281
+ function createInspectorMissionController(options) {
282
+ const paths = resolveInspectorMissionPaths(options.projectRoot);
283
+ mkdirSync2(paths.missionsDir, { recursive: true });
284
+ mkdirSync2(paths.journalsDir, { recursive: true });
285
+ const now = options.now ?? (() => new Date().toISOString());
286
+ const nextId = options.idGenerator ?? (() => `mission:${randomUUID()}`);
287
+ function missionPath(missionId) {
288
+ return join(paths.missionsDir, `${missionId}.json`);
289
+ }
290
+ function journalPath(missionId) {
291
+ return join(paths.journalsDir, `${missionId}.jsonl`);
292
+ }
293
+ function appendMissionJournal(entry) {
294
+ mkdirSync2(paths.journalsDir, { recursive: true });
295
+ appendFileSync(journalPath(entry.missionId), `${JSON.stringify(entry)}
296
+ `, "utf8");
297
+ }
298
+ function listMissionJournal(missionId) {
299
+ const path = journalPath(missionId);
300
+ if (!existsSync2(path))
301
+ return [];
302
+ return readFileSync(path, "utf8").split(`
303
+ `).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line)).filter(isRecord).map((entry) => ({
304
+ id: typeof entry.id === "string" ? entry.id : `journal:${randomUUID()}`,
305
+ missionId,
306
+ kind: typeof entry.kind === "string" ? entry.kind : "mission.event",
307
+ summary: typeof entry.summary === "string" ? entry.summary : "Mission event",
308
+ actor: typeof entry.actor === "string" ? entry.actor : null,
309
+ details: toJsonRecord(entry.details),
310
+ createdAt: typeof entry.createdAt === "string" ? entry.createdAt : new Date(0).toISOString()
311
+ }));
312
+ }
313
+ function saveMission(mission) {
314
+ writeJsonFile(missionPath(mission.missionId), mission);
315
+ }
316
+ function readMissionOnly(missionId) {
317
+ const path = missionPath(missionId);
318
+ if (!existsSync2(path)) {
319
+ return { ok: false, error: `Mission ${missionId} was not found` };
320
+ }
321
+ const read = readJsonRecord(path);
322
+ if (!read.ok)
323
+ return read;
324
+ return parseMission(read.record);
325
+ }
326
+ function recordDecision(mission, input) {
327
+ options.journal?.appendDecision({
328
+ id: `decision:${mission.missionId}:${input.type}:${randomUUID()}`,
329
+ runId: mission.missionId,
330
+ dedupeKey: null,
331
+ decisionType: input.type,
332
+ summary: input.summary,
333
+ rationale: input.rationale,
334
+ details: input.details ?? missionActionDetails(mission),
335
+ createdAt: input.at
336
+ });
337
+ }
338
+ function recordAction(mission, input) {
339
+ options.journal?.appendAction({
340
+ id: `action:${mission.missionId}:${input.type}:${randomUUID()}`,
341
+ runId: mission.missionId,
342
+ dedupeKey: null,
343
+ actionType: input.type,
344
+ status: input.status,
345
+ target: input.target ?? `mission:${mission.missionId}`,
346
+ input: input.input ?? missionActionDetails(mission),
347
+ result: input.result ?? missionActionDetails(mission),
348
+ startedAt: input.startedAt,
349
+ completedAt: input.completedAt ?? input.startedAt
350
+ });
351
+ }
352
+ function appendEvent(mission, input) {
353
+ appendMissionJournal({
354
+ id: `event:${mission.missionId}:${input.kind}:${randomUUID()}`,
355
+ missionId: mission.missionId,
356
+ kind: input.kind,
357
+ summary: input.summary,
358
+ actor: input.actor ?? null,
359
+ details: input.details ?? missionActionDetails(mission),
360
+ createdAt: input.at
361
+ });
362
+ }
363
+ return {
364
+ paths,
365
+ createMission(input) {
366
+ const source = cloneJsonRecord(input.sourceTask);
367
+ const missionId = nextId();
368
+ const path = missionPath(missionId);
369
+ if (existsSync2(path)) {
370
+ const existing = readMissionOnly(missionId);
371
+ if (!existing.ok)
372
+ return existing;
373
+ return { ok: true, mission: existing.mission, journal: listMissionJournal(missionId) };
374
+ }
375
+ const at = now();
376
+ const mission = {
377
+ missionId,
378
+ owner: input.owner ?? "inspector-agent",
379
+ sourceTask: source,
380
+ status: "open",
381
+ summary: input.summary ?? (typeof source.title === "string" ? source.title : "Inspector mission"),
382
+ pendingEffects: [],
383
+ postedResults: [],
384
+ completionProof: null,
385
+ createdAt: at,
386
+ updatedAt: at
387
+ };
388
+ saveMission(mission);
389
+ appendEvent(mission, {
390
+ kind: "mission.created",
391
+ summary: mission.summary,
392
+ actor: mission.owner,
393
+ details: { ...missionActionDetails(mission), sourceTask: source },
394
+ at
395
+ });
396
+ recordDecision(mission, {
397
+ type: "mission.create",
398
+ summary: mission.summary,
399
+ rationale: "Inspector-agent accepted agency/controller ownership for the source task contract.",
400
+ details: { ...missionActionDetails(mission), sourceTask: source },
401
+ at
402
+ });
403
+ recordAction(mission, {
404
+ type: "mission.create",
405
+ status: "completed",
406
+ result: missionActionDetails(mission),
407
+ startedAt: at
408
+ });
409
+ return { ok: true, mission, journal: listMissionJournal(missionId) };
410
+ },
411
+ listMissions() {
412
+ 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));
413
+ },
414
+ readMission(missionId) {
415
+ const read = readMissionOnly(missionId);
416
+ if (!read.ok)
417
+ return read;
418
+ return { ok: true, mission: read.mission, journal: listMissionJournal(missionId) };
419
+ },
420
+ requestEffect(input) {
421
+ const read = readMissionOnly(input.missionId);
422
+ if (!read.ok)
423
+ return read;
424
+ const mission = read.mission;
425
+ if (terminalStatus(mission.status)) {
426
+ return { ok: false, error: `Mission ${mission.missionId} is terminal (${mission.status})` };
427
+ }
428
+ if (mission.pendingEffects.some((effect2) => effect2.effectId === input.effectId)) {
429
+ return { ok: false, error: `Mission ${mission.missionId} already has effect ${input.effectId}` };
430
+ }
431
+ const at = now();
432
+ const effect = {
433
+ effectId: input.effectId ?? `effect:${randomUUID()}`,
434
+ kind: input.kind,
435
+ executor: input.executor,
436
+ summary: input.summary,
437
+ input: input.input ?? null,
438
+ status: "pending",
439
+ requestedAt: at
440
+ };
441
+ mission.pendingEffects = [...mission.pendingEffects, effect];
442
+ mission.status = "awaiting_effect";
443
+ mission.updatedAt = at;
444
+ saveMission(mission);
445
+ appendEvent(mission, {
446
+ kind: "mission.effect.requested",
447
+ summary: input.summary,
448
+ actor: mission.owner,
449
+ details: { ...missionActionDetails(mission), effectId: effect.effectId, executor: effect.executor },
450
+ at
451
+ });
452
+ recordAction(mission, {
453
+ type: "mission.effect.request",
454
+ status: "completed",
455
+ target: effect.executor,
456
+ input: effect.input,
457
+ result: { ...missionActionDetails(mission), effectId: effect.effectId },
458
+ startedAt: at
459
+ });
460
+ return { ok: true, mission, journal: listMissionJournal(mission.missionId) };
461
+ },
462
+ postEffectResult(input) {
463
+ const read = readMissionOnly(input.missionId);
464
+ if (!read.ok)
465
+ return read;
466
+ const mission = read.mission;
467
+ if (terminalStatus(mission.status)) {
468
+ return { ok: false, error: `Mission ${mission.missionId} is terminal (${mission.status})` };
469
+ }
470
+ const effect = mission.pendingEffects.find((entry) => entry.effectId === input.effectId);
471
+ if (!effect) {
472
+ return { ok: false, error: `Mission ${mission.missionId} has no pending effect ${input.effectId}` };
473
+ }
474
+ if (effect.status !== "pending") {
475
+ const latestResult = mission.postedResults.filter((entry) => entry.effectId === input.effectId).at(-1);
476
+ if (!latestResult || latestResult.status !== "partial") {
477
+ return { ok: false, error: `Effect ${input.effectId} was already posted` };
478
+ }
479
+ }
480
+ const at = now();
481
+ effect.status = "posted";
482
+ const result = {
483
+ effectId: input.effectId,
484
+ status: input.status,
485
+ summary: input.summary,
486
+ result: input.result ?? null,
487
+ postedBy: input.postedBy ?? "provider-worker",
488
+ postedAt: at
489
+ };
490
+ mission.postedResults = [...mission.postedResults, result];
491
+ mission.status = missionStatusAfterPost(mission);
492
+ mission.updatedAt = at;
493
+ saveMission(mission);
494
+ appendEvent(mission, {
495
+ kind: "mission.effect.posted",
496
+ summary: input.summary,
497
+ actor: result.postedBy,
498
+ details: { ...missionActionDetails(mission), effectId: input.effectId, resultStatus: input.status },
499
+ at
500
+ });
501
+ recordAction(mission, {
502
+ type: "mission.effect.post",
503
+ status: input.status,
504
+ target: input.effectId,
505
+ input: { effectId: input.effectId },
506
+ result: result.result,
507
+ startedAt: at
508
+ });
509
+ if (input.status !== "completed") {
510
+ recordDecision(mission, {
511
+ type: "mission.block",
512
+ summary: input.summary,
513
+ rationale: "Provider-worker effect result was not complete; proof gate remains closed.",
514
+ details: { ...missionActionDetails(mission), effectId: input.effectId, resultStatus: input.status },
515
+ at
516
+ });
517
+ }
518
+ return { ok: true, mission, journal: listMissionJournal(mission.missionId) };
519
+ },
520
+ acceptMission(input) {
521
+ const read = readMissionOnly(input.missionId);
522
+ if (!read.ok)
523
+ return read;
524
+ const mission = read.mission;
525
+ const blockers = blockingResults(mission);
526
+ if (blockers.length > 0) {
527
+ return { ok: false, error: `Mission ${mission.missionId} has blocking effect result(s)` };
528
+ }
529
+ const pending = pendingEffects(mission);
530
+ if (pending.length > 0) {
531
+ return { ok: false, error: `Mission ${mission.missionId} has pending effect(s)` };
532
+ }
533
+ if (terminalStatus(mission.status)) {
534
+ return { ok: false, error: `Mission ${mission.missionId} is terminal (${mission.status})` };
535
+ }
536
+ const at = now();
537
+ const proof = {
538
+ proofId: `proof:${mission.missionId}:${randomUUID()}`,
539
+ missionId: mission.missionId,
540
+ sourceTaskId: sourceTaskId(mission.sourceTask),
541
+ status: "accepted",
542
+ acceptedBy: input.acceptedBy ?? "inspector-agent",
543
+ summary: input.summary,
544
+ evidence: input.evidence ?? [],
545
+ effectResultIds: latestEffectResults(mission).map((result) => result.effectId),
546
+ sourceTask: cloneJsonRecord(mission.sourceTask),
547
+ emittedAt: at
548
+ };
549
+ mission.status = "accepted";
550
+ mission.completionProof = proof;
551
+ mission.updatedAt = at;
552
+ saveMission(mission);
553
+ appendEvent(mission, {
554
+ kind: "mission.accepted",
555
+ summary: input.summary,
556
+ actor: proof.acceptedBy,
557
+ details: { ...missionActionDetails(mission), proofId: proof.proofId },
558
+ at
559
+ });
560
+ recordDecision(mission, {
561
+ type: "mission.accept",
562
+ summary: input.summary,
563
+ rationale: "All requested effects were posted as complete and the PR/source-task closeout readiness is accepted.",
564
+ details: { ...missionActionDetails(mission), proofId: proof.proofId, evidence: proof.evidence },
565
+ at
566
+ });
567
+ recordAction(mission, {
568
+ type: "mission.accept",
569
+ status: "completed",
570
+ result: { ...missionActionDetails(mission), proofId: proof.proofId },
571
+ startedAt: at
572
+ });
573
+ return { ok: true, mission, journal: listMissionJournal(mission.missionId), completionProof: proof };
574
+ },
575
+ rejectMission(input) {
576
+ const read = readMissionOnly(input.missionId);
577
+ if (!read.ok)
578
+ return read;
579
+ const mission = read.mission;
580
+ if (terminalStatus(mission.status)) {
581
+ return { ok: false, error: `Mission ${mission.missionId} is terminal (${mission.status})` };
582
+ }
583
+ const at = now();
584
+ mission.status = "rejected";
585
+ mission.completionProof = null;
586
+ mission.updatedAt = at;
587
+ saveMission(mission);
588
+ appendEvent(mission, {
589
+ kind: "mission.rejected",
590
+ summary: input.summary,
591
+ actor: input.rejectedBy ?? "inspector-agent",
592
+ details: { ...missionActionDetails(mission), rationale: input.rationale ?? "" },
593
+ at
594
+ });
595
+ recordDecision(mission, {
596
+ type: "mission.reject",
597
+ summary: input.summary,
598
+ rationale: input.rationale ?? "Inspector rejected mission closeout.",
599
+ details: missionActionDetails(mission),
600
+ at
601
+ });
602
+ recordAction(mission, {
603
+ type: "mission.reject",
604
+ status: "completed",
605
+ result: missionActionDetails(mission),
606
+ startedAt: at
607
+ });
608
+ return { ok: true, mission, journal: listMissionJournal(mission.missionId) };
609
+ },
610
+ blockMission(input) {
611
+ const read = readMissionOnly(input.missionId);
612
+ if (!read.ok)
613
+ return read;
614
+ const mission = read.mission;
615
+ if (terminalStatus(mission.status)) {
616
+ return { ok: false, error: `Mission ${mission.missionId} is terminal (${mission.status})` };
617
+ }
618
+ const at = now();
619
+ mission.status = "blocked";
620
+ mission.updatedAt = at;
621
+ saveMission(mission);
622
+ appendEvent(mission, {
623
+ kind: "mission.blocked",
624
+ summary: input.summary,
625
+ actor: input.blockedBy ?? "inspector-agent",
626
+ details: input.details ?? missionActionDetails(mission),
627
+ at
628
+ });
629
+ recordDecision(mission, {
630
+ type: "mission.block",
631
+ summary: input.summary,
632
+ rationale: "Inspector marked mission blocked; proof gate remains closed.",
633
+ details: input.details ?? missionActionDetails(mission),
634
+ at
635
+ });
636
+ return { ok: true, mission, journal: listMissionJournal(mission.missionId) };
637
+ },
638
+ completeMission(missionId) {
639
+ const read = readMissionOnly(missionId);
640
+ if (!read.ok)
641
+ return read;
642
+ const mission = read.mission;
643
+ if (mission.status !== "accepted" || !mission.completionProof) {
644
+ return { ok: false, error: `Mission ${missionId} must be accepted before completion proof can be completed` };
645
+ }
646
+ const at = now();
647
+ appendEvent(mission, {
648
+ kind: "mission.completed",
649
+ summary: "Mission completion proof read",
650
+ actor: mission.owner,
651
+ details: { ...missionActionDetails(mission), proofId: mission.completionProof.proofId },
652
+ at
653
+ });
654
+ recordAction(mission, {
655
+ type: "mission.complete",
656
+ status: "completed",
657
+ result: { ...missionActionDetails(mission), proofId: mission.completionProof.proofId },
658
+ startedAt: at
659
+ });
660
+ return {
661
+ ok: true,
662
+ mission,
663
+ journal: listMissionJournal(mission.missionId),
664
+ completionProof: mission.completionProof
665
+ };
666
+ }
667
+ };
668
+ }
669
+
670
+ // packages/server/src/inspector/provider-session.ts
671
+ function uniqueStrings(values) {
672
+ return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))];
673
+ }
674
+ function withSupport(defaultValue, support, key) {
675
+ return typeof support?.[key] === "boolean" ? Boolean(support[key]) : defaultValue;
676
+ }
677
+ function normalizeProviderSession(run) {
678
+ const sessionLogPaths = uniqueStrings([run.sessionLogPath]);
679
+ const artifactRoots = uniqueStrings([run.artifactRoot]);
680
+ const eventSources = uniqueStrings([run.source, ...run.rawSourceKinds, run.sessionPath ? "session-file" : null]);
681
+ const capabilityHints = uniqueStrings([
682
+ sessionLogPaths.length > 0 ? "logs" : null,
683
+ artifactRoots.length > 0 ? "artifacts" : null,
684
+ run.worktreePath ? "workspace" : null,
685
+ run.conflictState !== "none" ? "conflict" : null,
686
+ run.threadId ? "thread" : null
687
+ ]);
688
+ const observabilityConfidence = sessionLogPaths.length > 0 && artifactRoots.length > 0 && run.threadId ? "high" : sessionLogPaths.length > 0 || artifactRoots.length > 0 || Boolean(run.threadId) ? "medium" : "low";
689
+ return {
690
+ provider: run.provider,
691
+ runId: run.runId,
692
+ taskId: run.taskId,
693
+ workspaceId: run.workspaceId,
694
+ workspaceDir: run.worktreePath,
695
+ runtimeAdapter: run.runtimeAdapter,
696
+ status: run.status,
697
+ startedAt: run.startedAt,
698
+ updatedAt: run.updatedAt,
699
+ completedAt: run.completedAt,
700
+ threadId: run.threadId,
701
+ sessionLogPaths,
702
+ artifactRoots,
703
+ eventSources,
704
+ observabilityConfidence,
705
+ rawSourceKinds: [...run.rawSourceKinds],
706
+ capabilityHints
707
+ };
708
+ }
709
+ function deriveInspectorCapabilitySet(session, support = {}) {
710
+ return {
711
+ discoverRuns: withSupport(true, support, "discoverRuns"),
712
+ inspectRuns: withSupport(true, support, "inspectRuns"),
713
+ readLogs: session.sessionLogPaths.length > 0 && withSupport(true, support, "readLogs"),
714
+ readArtifacts: session.artifactRoots.length > 0 && withSupport(true, support, "readArtifacts"),
715
+ callServerApis: withSupport(true, support, "callServerApis"),
716
+ interruptRuns: withSupport(false, support, "interruptRuns"),
717
+ stopRuns: withSupport(false, support, "stopRuns"),
718
+ resumeRuns: withSupport(false, support, "resumeRuns"),
719
+ relaunchRuns: withSupport(false, support, "relaunchRuns"),
720
+ patchRepo: session.workspaceDir !== null && withSupport(true, support, "patchRepo"),
721
+ patchHarness: session.workspaceDir !== null && withSupport(true, support, "patchHarness"),
722
+ createTasks: withSupport(false, support, "createTasks"),
723
+ runReviewerAgents: withSupport(false, support, "runReviewerAgents"),
724
+ provisionSkills: withSupport(true, support, "provisionSkills"),
725
+ scanUpstream: withSupport(false, support, "scanUpstream"),
726
+ writeJournal: withSupport(true, support, "writeJournal"),
727
+ spawnSpecialists: withSupport(false, support, "spawnSpecialists")
728
+ };
729
+ }
730
+
731
+ // packages/server/src/inspector/review.ts
732
+ function buildLocalReviewRequest(options) {
733
+ return {
734
+ reviewerType: "local-reviewer",
735
+ runId: options.runId,
736
+ taskId: options.taskId,
737
+ focus: options.focus
738
+ };
739
+ }
740
+
741
+ // packages/server/src/inspector/skills.ts
742
+ var UNIVERSAL_REQUIRED_SKILLS = [
743
+ {
744
+ name: "test-driven-development",
745
+ rationale: "Write failing tests first for code and behavior changes unless a narrow exception is recorded."
746
+ }
747
+ ];
748
+ var INSPECTOR_REQUIRED_SKILLS = [
749
+ {
750
+ name: "verification-before-completion",
751
+ rationale: "Do not claim completion or stability without direct verification evidence."
752
+ },
753
+ {
754
+ name: "no-bs",
755
+ rationale: "Respect the machine no-bs gates, keep evidence up to date, and do not stop early."
756
+ }
757
+ ];
758
+ var UNIVERSAL_RECOMMENDED_SKILLS = [
759
+ {
760
+ name: "verification-before-completion",
761
+ rationale: "Gather direct evidence before claiming a repair, review result, or task completion."
762
+ },
763
+ {
764
+ name: "systematic-debugging",
765
+ rationale: "Use disciplined debugging when a run, harness path, or validator fails."
766
+ },
767
+ {
768
+ name: "dispatching-parallel-agents",
769
+ rationale: "Split independent analysis and review work when that shortens the critical path."
770
+ },
771
+ {
772
+ name: "subagent-driven-development",
773
+ rationale: "Delegate bounded work to specialist subagents while keeping one context owner."
774
+ },
775
+ {
776
+ name: "requesting-code-review",
777
+ rationale: "Run local review before landing meaningful implementation work."
778
+ },
779
+ {
780
+ name: "receiving-code-review",
781
+ rationale: "Handle review feedback rigorously instead of applying it blindly."
782
+ },
783
+ {
784
+ name: "using-git-worktrees",
785
+ rationale: "Use isolated worktrees for risky or parallel implementation branches."
786
+ },
787
+ {
788
+ name: "writing-plans",
789
+ rationale: "Plan multi-step changes explicitly when the work is broad or risky."
790
+ },
791
+ {
792
+ name: "executing-plans",
793
+ rationale: "Execute validated plans in a staged, reviewable way."
794
+ },
795
+ {
796
+ name: "writing-skills",
797
+ rationale: "Improve or add agent skills when the harness needs new reusable behavior."
798
+ },
799
+ {
800
+ name: "find-skills",
801
+ rationale: "Discover existing local skills before inventing new workflows."
802
+ },
803
+ {
804
+ name: "openai-docs",
805
+ rationale: "Use primary OpenAI documentation when the inspector needs current provider facts."
806
+ },
807
+ {
808
+ name: "linear",
809
+ rationale: "Create or update follow-up work in the task tracker when oversight detects needed action."
810
+ },
811
+ {
812
+ name: "playwright",
813
+ rationale: "Inspect browser flows directly when a run touches web behavior or UI verification."
814
+ },
815
+ {
816
+ name: "playwright-interactive",
817
+ rationale: "Keep a live browser session when iterative UI inspection is more efficient than restarts."
818
+ }
819
+ ];
820
+ var INSPECTOR_ONLY_RECOMMENDED_SKILLS = [
821
+ {
822
+ name: "vercel:agent-browser",
823
+ rationale: "Perform high-fidelity browser inspection when a run or preview needs interactive verification."
824
+ },
825
+ {
826
+ name: "vercel:agent-browser-verify",
827
+ rationale: "Run an automated visual gut-check against dev servers and previews."
828
+ },
829
+ {
830
+ name: "vercel:verification",
831
+ rationale: "Verify end-to-end browser to API behavior when the change spans multiple surfaces."
832
+ },
833
+ {
834
+ name: "vercel:investigation-mode",
835
+ rationale: "Triages broken or stuck flows systematically when the harness or a preview misbehaves."
836
+ }
837
+ ];
838
+ function uniqueSkills(skills) {
839
+ const seen = new Set;
840
+ const deduped = [];
841
+ for (const skill of skills) {
842
+ if (seen.has(skill.name)) {
843
+ continue;
844
+ }
845
+ seen.add(skill.name);
846
+ deduped.push(skill);
847
+ }
848
+ return deduped;
849
+ }
850
+ function buildInspectorSkillCatalog(options) {
851
+ const required = [...UNIVERSAL_REQUIRED_SKILLS];
852
+ const recommended = [...UNIVERSAL_RECOMMENDED_SKILLS];
853
+ if (options.role === "inspector") {
854
+ required.push(...INSPECTOR_REQUIRED_SKILLS);
855
+ recommended.push(...INSPECTOR_ONLY_RECOMMENDED_SKILLS);
856
+ } else if (options.role === "reviewer") {
857
+ recommended.push({
858
+ name: "receiving-code-review",
859
+ rationale: "Review lanes should reason about findings carefully and defensibly."
860
+ });
861
+ }
862
+ if (options.taskKind === "bugfix") {
863
+ recommended.push({
864
+ name: "systematic-debugging",
865
+ rationale: "Bugfix work should start from a disciplined failure-class analysis."
866
+ });
867
+ }
868
+ return {
869
+ required: uniqueSkills(required),
870
+ recommended: uniqueSkills(recommended)
871
+ };
872
+ }
873
+ function buildSkillProvisioningDecision(options) {
874
+ const exceptionRecorded = Boolean(options.exception);
875
+ const catalog = buildInspectorSkillCatalog({
876
+ role: options.role,
877
+ taskKind: options.taskKind
878
+ });
879
+ const requiredSkills = catalog.required.map((skill) => skill.name);
880
+ const preferredSkills = catalog.recommended.map((skill) => skill.name);
881
+ return {
882
+ role: options.role,
883
+ taskKind: options.taskKind,
884
+ requiredSkills,
885
+ preferredSkills,
886
+ tddMode: exceptionRecorded ? "exception-recorded" : "required",
887
+ exceptionRecorded,
888
+ exception: options.exception ?? null
889
+ };
890
+ }
891
+
892
+ // packages/server/src/inspector/tools.ts
893
+ function toJsonRecord2(value, fallbackKey = "value") {
894
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
895
+ if (value == null) {
896
+ return null;
897
+ }
898
+ return { [fallbackKey]: value };
899
+ }
900
+ return value;
901
+ }
902
+ function toMissionJsonRecord(value) {
903
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
904
+ return null;
905
+ }
906
+ const parsed = JSON.parse(JSON.stringify(value));
907
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
908
+ return null;
909
+ }
910
+ return parsed;
911
+ }
912
+ function toMissionJsonRecordOrNull(value) {
913
+ if (value == null)
914
+ return null;
915
+ return toMissionJsonRecord(value);
916
+ }
917
+ function missionFailureStatus(error) {
918
+ if (error.includes("was not found"))
919
+ return "missing";
920
+ return "blocked";
921
+ }
922
+ function activeRunStatuses() {
923
+ return new Set(["preparing", "running", "validating", "reviewing"]);
924
+ }
925
+ function stableFollowupDedupeKey(originRunId, summary) {
926
+ return `followup:${originRunId ?? "global"}:${summary.trim().toLowerCase()}`;
927
+ }
928
+ var INSPECTOR_TOOL_DEFINITIONS = [
929
+ {
930
+ name: "read_inspector_snapshot",
931
+ description: "Read the current global inspector snapshot including active runs, recent findings, follow-ups, reports, and available semantic tools.",
932
+ inputSchema: {
933
+ type: "object",
934
+ properties: {},
935
+ additionalProperties: false
936
+ }
937
+ },
938
+ {
939
+ name: "discover_active_runs",
940
+ description: "List currently active Rig runs known to the inspector overlay.",
941
+ inputSchema: {
942
+ type: "object",
943
+ properties: {},
944
+ additionalProperties: false
945
+ }
946
+ },
947
+ {
948
+ name: "inspect_run",
949
+ description: "Inspect one run in detail, including session paths, capabilities, and recent journal observations.",
950
+ inputSchema: {
951
+ type: "object",
952
+ properties: {
953
+ runId: { type: "string" }
954
+ },
955
+ required: ["runId"],
956
+ additionalProperties: false
957
+ }
958
+ },
959
+ {
960
+ name: "record_inspector_note",
961
+ description: "Append a durable note to the inspector journal.",
962
+ inputSchema: {
963
+ type: "object",
964
+ properties: {
965
+ runId: { type: "string" },
966
+ summary: { type: "string" },
967
+ details: { type: "object" }
968
+ },
969
+ required: ["summary"],
970
+ additionalProperties: true
971
+ }
972
+ },
973
+ {
974
+ name: "list_inspector_missions",
975
+ description: "List durable inspector missions with status, pending effects, posted results, and completion proof state.",
976
+ inputSchema: {
977
+ type: "object",
978
+ properties: {},
979
+ additionalProperties: false
980
+ }
981
+ },
982
+ {
983
+ name: "create_inspector_mission",
984
+ description: "Create a durable inspector-owned mission for a source task contract. The sourceTask is preserved in the mission JSON state.",
985
+ inputSchema: {
986
+ type: "object",
987
+ properties: {
988
+ sourceTask: { type: "object" },
989
+ owner: { type: "string" },
990
+ summary: { type: "string" }
991
+ },
992
+ required: ["sourceTask"],
993
+ additionalProperties: true
994
+ }
995
+ },
996
+ {
997
+ name: "read_inspector_mission",
998
+ description: "Read a durable inspector mission, including sourceTask, pending effects, posted results, proof, and mission journal.",
999
+ inputSchema: {
1000
+ type: "object",
1001
+ properties: {
1002
+ missionId: { type: "string" }
1003
+ },
1004
+ required: ["missionId"],
1005
+ additionalProperties: false
1006
+ }
1007
+ },
1008
+ {
1009
+ name: "request_inspector_effect",
1010
+ description: "Request a provider-worker effect for an inspector mission. Pending effects gate acceptance until posted.",
1011
+ inputSchema: {
1012
+ type: "object",
1013
+ properties: {
1014
+ missionId: { type: "string" },
1015
+ effectId: { type: "string" },
1016
+ kind: { type: "string" },
1017
+ executor: { type: "string" },
1018
+ summary: { type: "string" },
1019
+ input: { type: "object" }
1020
+ },
1021
+ required: ["missionId", "kind", "executor", "summary"],
1022
+ additionalProperties: true
1023
+ }
1024
+ },
1025
+ {
1026
+ name: "post_inspector_effect_result",
1027
+ description: "Post a provider-worker effect result. Partial, blocked, or failed results block completion proof.",
1028
+ inputSchema: {
1029
+ type: "object",
1030
+ properties: {
1031
+ missionId: { type: "string" },
1032
+ effectId: { type: "string" },
1033
+ status: { type: "string", enum: ["completed", "partial", "blocked", "failed"] },
1034
+ summary: { type: "string" },
1035
+ result: { type: "object" },
1036
+ postedBy: { type: "string" }
1037
+ },
1038
+ required: ["missionId", "effectId", "status", "summary"],
1039
+ additionalProperties: true
1040
+ }
1041
+ },
1042
+ {
1043
+ name: "accept_inspector_mission",
1044
+ description: "Accept a mission closeout and emit completionProof when no pending/blocking effects remain.",
1045
+ inputSchema: {
1046
+ type: "object",
1047
+ properties: {
1048
+ missionId: { type: "string" },
1049
+ acceptedBy: { type: "string" },
1050
+ summary: { type: "string" },
1051
+ evidence: { type: "array", items: { type: "string" } }
1052
+ },
1053
+ required: ["missionId", "summary"],
1054
+ additionalProperties: true
1055
+ }
1056
+ },
1057
+ {
1058
+ name: "reject_inspector_mission",
1059
+ description: "Reject a mission closeout without emitting completionProof.",
1060
+ inputSchema: {
1061
+ type: "object",
1062
+ properties: {
1063
+ missionId: { type: "string" },
1064
+ rejectedBy: { type: "string" },
1065
+ summary: { type: "string" },
1066
+ rationale: { type: "string" }
1067
+ },
1068
+ required: ["missionId", "summary"],
1069
+ additionalProperties: true
1070
+ }
1071
+ },
1072
+ {
1073
+ name: "block_inspector_mission",
1074
+ description: "Mark a mission blocked without emitting completionProof.",
1075
+ inputSchema: {
1076
+ type: "object",
1077
+ properties: {
1078
+ missionId: { type: "string" },
1079
+ blockedBy: { type: "string" },
1080
+ summary: { type: "string" },
1081
+ details: { type: "object" }
1082
+ },
1083
+ required: ["missionId", "summary"],
1084
+ additionalProperties: true
1085
+ }
1086
+ },
1087
+ {
1088
+ name: "complete_inspector_mission",
1089
+ description: "Return the accepted mission completionProof. This fails until the mission has been accepted.",
1090
+ inputSchema: {
1091
+ type: "object",
1092
+ properties: {
1093
+ missionId: { type: "string" }
1094
+ },
1095
+ required: ["missionId"],
1096
+ additionalProperties: false
1097
+ }
1098
+ },
1099
+ {
1100
+ name: "create_followup_task",
1101
+ description: "Create or dedupe a structured follow-up task for work the inspector discovered.",
1102
+ inputSchema: {
1103
+ type: "object",
1104
+ properties: {
1105
+ originRunId: { type: "string" },
1106
+ summary: { type: "string" },
1107
+ dedupeKey: { type: "string" },
1108
+ details: { type: "object" }
1109
+ },
1110
+ required: ["summary"],
1111
+ additionalProperties: true
1112
+ }
1113
+ },
1114
+ {
1115
+ name: "provision_skill",
1116
+ description: "Return the role-aware Rig skill set that should be in force for an agent or specialist lane.",
1117
+ inputSchema: {
1118
+ type: "object",
1119
+ properties: {
1120
+ runId: { type: "string" },
1121
+ role: { type: "string" },
1122
+ taskKind: { type: "string" },
1123
+ exception: { type: "object" }
1124
+ },
1125
+ additionalProperties: true
1126
+ }
1127
+ },
1128
+ {
1129
+ name: "run_local_review",
1130
+ description: "Run the local Rig reviewer/validator flow for a task.",
1131
+ inputSchema: {
1132
+ type: "object",
1133
+ properties: {
1134
+ runId: { type: "string" },
1135
+ taskId: { type: "string" },
1136
+ focus: { type: "array", items: { type: "string" } }
1137
+ },
1138
+ required: ["taskId"],
1139
+ additionalProperties: false
1140
+ }
1141
+ },
1142
+ {
1143
+ name: "scan_upstream_drift",
1144
+ description: "Run the separate upstream-sync workflow and create follow-up tasks for needed ports.",
1145
+ inputSchema: {
1146
+ type: "object",
1147
+ properties: {
1148
+ scanId: { type: "string" }
1149
+ },
1150
+ additionalProperties: true
1151
+ }
1152
+ },
1153
+ {
1154
+ name: "generate_analysis_report",
1155
+ description: "Generate an inspector analysis report from journaled run and oversight data.",
1156
+ inputSchema: {
1157
+ type: "object",
1158
+ properties: {
1159
+ reportId: { type: "string" },
1160
+ reportType: { type: "string" },
1161
+ runId: { type: "string" }
1162
+ },
1163
+ additionalProperties: true
1164
+ }
1165
+ }
1166
+ ];
1167
+ function createInspectorToolRegistry(options) {
1168
+ const now = options.now ?? (() => new Date().toISOString());
1169
+ const missionController = options.projectRoot ? createInspectorMissionController({
1170
+ projectRoot: options.projectRoot,
1171
+ journal: options.journal,
1172
+ now,
1173
+ idGenerator: options.missionIdGenerator
1174
+ }) : null;
1175
+ const descriptors = {
1176
+ read_inspector_snapshot: {
1177
+ enabled: Boolean(options.snapshotReader),
1178
+ async handler() {
1179
+ if (!options.snapshotReader) {
1180
+ return {
1181
+ status: "unavailable",
1182
+ summary: "read_inspector_snapshot is not configured",
1183
+ details: null
1184
+ };
1185
+ }
1186
+ const snapshot = options.snapshotReader();
1187
+ return {
1188
+ status: "completed",
1189
+ summary: "Read current inspector snapshot",
1190
+ details: snapshot
1191
+ };
1192
+ }
1193
+ },
1194
+ discover_active_runs: {
1195
+ enabled: true,
1196
+ async handler() {
1197
+ const runs = options.discoverRuns().filter((run) => activeRunStatuses().has(run.status));
1198
+ return {
1199
+ status: "completed",
1200
+ summary: `Discovered ${runs.length} active run(s)`,
1201
+ details: runs
1202
+ };
1203
+ }
1204
+ },
1205
+ inspect_run: {
1206
+ enabled: true,
1207
+ async handler(input) {
1208
+ const runId = typeof input.runId === "string" ? input.runId : "";
1209
+ const run = options.discoverRuns().find((entry) => entry.runId === runId) ?? null;
1210
+ if (!run) {
1211
+ return {
1212
+ status: "missing",
1213
+ summary: `Run ${runId} was not found`,
1214
+ details: null
1215
+ };
1216
+ }
1217
+ const session = normalizeProviderSession(run);
1218
+ const findings = options.journal.listRecentFindings({ limit: 50 }).filter((entry) => entry.runId === runId);
1219
+ return {
1220
+ status: "completed",
1221
+ summary: `Inspected run ${runId}`,
1222
+ details: {
1223
+ run,
1224
+ session,
1225
+ capabilities: deriveInspectorCapabilitySet(session, options.capabilitySupport),
1226
+ observations: findings,
1227
+ paths: {
1228
+ workspaceDir: run.worktreePath,
1229
+ artifactRoot: run.artifactRoot,
1230
+ logRoot: run.logRoot,
1231
+ sessionPath: run.sessionPath,
1232
+ sessionLogPath: run.sessionLogPath
1233
+ }
1234
+ }
1235
+ };
1236
+ }
1237
+ },
1238
+ record_inspector_note: {
1239
+ enabled: true,
1240
+ async handler(input) {
1241
+ const runId = typeof input.runId === "string" ? input.runId : null;
1242
+ const summary = typeof input.summary === "string" ? input.summary : "Inspector note";
1243
+ const details = toJsonRecord2(input.details);
1244
+ const result = options.journal.appendObservation({
1245
+ id: `note:${runId ?? "global"}:${randomUUID2()}`,
1246
+ runId,
1247
+ dedupeKey: null,
1248
+ kind: "inspector.note",
1249
+ severity: "info",
1250
+ source: "inspector",
1251
+ summary,
1252
+ details,
1253
+ createdAt: now()
1254
+ });
1255
+ return {
1256
+ status: result.ok ? "completed" : "failed",
1257
+ summary,
1258
+ details: result.ok ? result.record : { error: result.error }
1259
+ };
1260
+ }
1261
+ },
1262
+ list_inspector_missions: {
1263
+ enabled: Boolean(missionController),
1264
+ async handler() {
1265
+ if (!missionController) {
1266
+ return { status: "unavailable", summary: "mission storage is not configured", details: null };
1267
+ }
1268
+ const missions = missionController.listMissions();
1269
+ return {
1270
+ status: "completed",
1271
+ summary: `Listed ${missions.length} inspector mission(s)`,
1272
+ details: { missions }
1273
+ };
1274
+ }
1275
+ },
1276
+ create_inspector_mission: {
1277
+ enabled: Boolean(missionController),
1278
+ async handler(input) {
1279
+ if (!missionController) {
1280
+ return { status: "unavailable", summary: "mission storage is not configured", details: null };
1281
+ }
1282
+ const sourceTask = toMissionJsonRecord(input.sourceTask);
1283
+ if (!sourceTask) {
1284
+ return { status: "failed", summary: "sourceTask must be a JSON object", details: null };
1285
+ }
1286
+ const result = missionController.createMission({
1287
+ sourceTask,
1288
+ owner: typeof input.owner === "string" ? input.owner : undefined,
1289
+ summary: typeof input.summary === "string" ? input.summary : undefined
1290
+ });
1291
+ if (!result.ok) {
1292
+ return { status: missionFailureStatus(result.error), summary: result.error, details: null };
1293
+ }
1294
+ return {
1295
+ status: "completed",
1296
+ summary: `Created inspector mission ${result.mission.missionId}`,
1297
+ details: { ...result.mission, journal: result.journal }
1298
+ };
1299
+ }
1300
+ },
1301
+ read_inspector_mission: {
1302
+ enabled: Boolean(missionController),
1303
+ async handler(input) {
1304
+ if (!missionController) {
1305
+ return { status: "unavailable", summary: "mission storage is not configured", details: null };
1306
+ }
1307
+ const missionId = typeof input.missionId === "string" ? input.missionId : "";
1308
+ const result = missionController.readMission(missionId);
1309
+ if (!result.ok) {
1310
+ return { status: missionFailureStatus(result.error), summary: result.error, details: null };
1311
+ }
1312
+ return {
1313
+ status: "completed",
1314
+ summary: `Read inspector mission ${missionId}`,
1315
+ details: { ...result.mission, journal: result.journal }
1316
+ };
1317
+ }
1318
+ },
1319
+ request_inspector_effect: {
1320
+ enabled: Boolean(missionController),
1321
+ async handler(input) {
1322
+ if (!missionController) {
1323
+ return { status: "unavailable", summary: "mission storage is not configured", details: null };
1324
+ }
1325
+ const missionId = typeof input.missionId === "string" ? input.missionId : "";
1326
+ const kind = typeof input.kind === "string" ? input.kind : "provider_worker";
1327
+ const executor = typeof input.executor === "string" ? input.executor : "worker";
1328
+ const summary = typeof input.summary === "string" ? input.summary : "Inspector effect requested";
1329
+ const result = missionController.requestEffect({
1330
+ missionId,
1331
+ effectId: typeof input.effectId === "string" ? input.effectId : undefined,
1332
+ kind,
1333
+ executor,
1334
+ summary,
1335
+ input: toMissionJsonRecordOrNull(input.input)
1336
+ });
1337
+ if (!result.ok) {
1338
+ return { status: missionFailureStatus(result.error), summary: result.error, details: null };
1339
+ }
1340
+ return {
1341
+ status: "completed",
1342
+ summary,
1343
+ details: { ...result.mission, journal: result.journal }
1344
+ };
1345
+ }
1346
+ },
1347
+ post_inspector_effect_result: {
1348
+ enabled: Boolean(missionController),
1349
+ async handler(input) {
1350
+ if (!missionController) {
1351
+ return { status: "unavailable", summary: "mission storage is not configured", details: null };
1352
+ }
1353
+ const status = typeof input.status === "string" ? input.status : "failed";
1354
+ if (!["completed", "partial", "blocked", "failed"].includes(status)) {
1355
+ return { status: "failed", summary: `Unsupported effect result status: ${status}`, details: null };
1356
+ }
1357
+ const result = missionController.postEffectResult({
1358
+ missionId: typeof input.missionId === "string" ? input.missionId : "",
1359
+ effectId: typeof input.effectId === "string" ? input.effectId : "",
1360
+ status,
1361
+ summary: typeof input.summary === "string" ? input.summary : "Inspector effect result posted",
1362
+ result: toMissionJsonRecordOrNull(input.result),
1363
+ postedBy: typeof input.postedBy === "string" ? input.postedBy : undefined
1364
+ });
1365
+ if (!result.ok) {
1366
+ return { status: missionFailureStatus(result.error), summary: result.error, details: null };
1367
+ }
1368
+ return {
1369
+ status: result.mission.status === "blocked" ? "blocked" : "completed",
1370
+ summary: typeof input.summary === "string" ? input.summary : "Inspector effect result posted",
1371
+ details: { ...result.mission, journal: result.journal }
1372
+ };
1373
+ }
1374
+ },
1375
+ accept_inspector_mission: {
1376
+ enabled: Boolean(missionController),
1377
+ async handler(input) {
1378
+ if (!missionController) {
1379
+ return { status: "unavailable", summary: "mission storage is not configured", details: null };
1380
+ }
1381
+ const result = missionController.acceptMission({
1382
+ missionId: typeof input.missionId === "string" ? input.missionId : "",
1383
+ acceptedBy: typeof input.acceptedBy === "string" ? input.acceptedBy : undefined,
1384
+ summary: typeof input.summary === "string" ? input.summary : "Inspector mission accepted",
1385
+ evidence: Array.isArray(input.evidence) ? input.evidence.filter((entry) => typeof entry === "string") : undefined
1386
+ });
1387
+ if (!result.ok) {
1388
+ return { status: missionFailureStatus(result.error), summary: result.error, details: null };
1389
+ }
1390
+ return {
1391
+ status: "completed",
1392
+ summary: result.completionProof.summary,
1393
+ details: { ...result.mission, journal: result.journal, completionProof: result.completionProof }
1394
+ };
1395
+ }
1396
+ },
1397
+ reject_inspector_mission: {
1398
+ enabled: Boolean(missionController),
1399
+ async handler(input) {
1400
+ if (!missionController) {
1401
+ return { status: "unavailable", summary: "mission storage is not configured", details: null };
1402
+ }
1403
+ const result = missionController.rejectMission({
1404
+ missionId: typeof input.missionId === "string" ? input.missionId : "",
1405
+ rejectedBy: typeof input.rejectedBy === "string" ? input.rejectedBy : undefined,
1406
+ summary: typeof input.summary === "string" ? input.summary : "Inspector mission rejected",
1407
+ rationale: typeof input.rationale === "string" ? input.rationale : undefined
1408
+ });
1409
+ if (!result.ok) {
1410
+ return { status: missionFailureStatus(result.error), summary: result.error, details: null };
1411
+ }
1412
+ return {
1413
+ status: "completed",
1414
+ summary: result.mission.summary,
1415
+ details: { ...result.mission, journal: result.journal }
1416
+ };
1417
+ }
1418
+ },
1419
+ block_inspector_mission: {
1420
+ enabled: Boolean(missionController),
1421
+ async handler(input) {
1422
+ if (!missionController) {
1423
+ return { status: "unavailable", summary: "mission storage is not configured", details: null };
1424
+ }
1425
+ const result = missionController.blockMission({
1426
+ missionId: typeof input.missionId === "string" ? input.missionId : "",
1427
+ blockedBy: typeof input.blockedBy === "string" ? input.blockedBy : undefined,
1428
+ summary: typeof input.summary === "string" ? input.summary : "Inspector mission blocked",
1429
+ details: toMissionJsonRecordOrNull(input.details)
1430
+ });
1431
+ if (!result.ok) {
1432
+ return { status: missionFailureStatus(result.error), summary: result.error, details: null };
1433
+ }
1434
+ return {
1435
+ status: "blocked",
1436
+ summary: result.mission.summary,
1437
+ details: { ...result.mission, journal: result.journal }
1438
+ };
1439
+ }
1440
+ },
1441
+ complete_inspector_mission: {
1442
+ enabled: Boolean(missionController),
1443
+ async handler(input) {
1444
+ if (!missionController) {
1445
+ return { status: "unavailable", summary: "mission storage is not configured", details: null };
1446
+ }
1447
+ const missionId = typeof input.missionId === "string" ? input.missionId : "";
1448
+ const result = missionController.completeMission(missionId);
1449
+ if (!result.ok) {
1450
+ return { status: missionFailureStatus(result.error), summary: result.error, details: null };
1451
+ }
1452
+ return {
1453
+ status: "completed",
1454
+ summary: `Completed inspector mission ${missionId}`,
1455
+ details: { ...result.mission, journal: result.journal, completionProof: result.completionProof }
1456
+ };
1457
+ }
1458
+ },
1459
+ create_followup_task: {
1460
+ enabled: true,
1461
+ async handler(input) {
1462
+ const originRunId = typeof input.originRunId === "string" ? input.originRunId : null;
1463
+ const summary = typeof input.summary === "string" ? input.summary : "Inspector follow-up";
1464
+ const details = toJsonRecord2(input.details);
1465
+ const dedupeKey = typeof input.dedupeKey === "string" && input.dedupeKey.trim().length > 0 ? input.dedupeKey.trim() : stableFollowupDedupeKey(originRunId, summary);
1466
+ const createdAt = now();
1467
+ const record = {
1468
+ id: `followup:${randomUUID2()}`,
1469
+ originRunId,
1470
+ dedupeKey,
1471
+ taskId: null,
1472
+ kind: "followup-task",
1473
+ status: "proposed",
1474
+ summary,
1475
+ details,
1476
+ createdAt
1477
+ };
1478
+ const result = options.journal.appendFollowup(record);
1479
+ let createdTask = null;
1480
+ if (result.ok && result.changed && options.followupTaskRunner) {
1481
+ createdTask = await options.followupTaskRunner({
1482
+ ...input,
1483
+ summary,
1484
+ originRunId,
1485
+ dedupeKey
1486
+ });
1487
+ const taskDetails = toJsonRecord2(createdTask.details);
1488
+ const taskId = typeof taskDetails?.taskId === "string" ? taskDetails.taskId : null;
1489
+ if (taskId) {
1490
+ options.journal.attachFollowupTask({
1491
+ dedupeKey,
1492
+ taskId,
1493
+ status: "created",
1494
+ details: taskDetails
1495
+ });
1496
+ }
1497
+ }
1498
+ options.journal.appendAction({
1499
+ id: `action:followup:${randomUUID2()}`,
1500
+ runId: originRunId,
1501
+ dedupeKey: null,
1502
+ actionType: "create_followup_task",
1503
+ status: createdTask?.status ?? (result.ok ? "completed" : "failed"),
1504
+ target: originRunId ? `run:${originRunId}` : "global",
1505
+ input: {
1506
+ summary,
1507
+ dedupeKey
1508
+ },
1509
+ result: toJsonRecord2(createdTask?.details ?? record),
1510
+ startedAt: createdAt,
1511
+ completedAt: now()
1512
+ });
1513
+ return {
1514
+ status: createdTask?.status ?? (result.ok ? "completed" : "failed"),
1515
+ summary,
1516
+ details: {
1517
+ followup: result.ok ? record : { error: result.error },
1518
+ createdTask: createdTask?.details ?? null
1519
+ }
1520
+ };
1521
+ }
1522
+ },
1523
+ provision_skill: {
1524
+ enabled: true,
1525
+ async handler(input) {
1526
+ const role = typeof input.role === "string" ? input.role : "worker";
1527
+ const taskKind = typeof input.taskKind === "string" ? input.taskKind : "code-change";
1528
+ const exception = input.exception && typeof input.exception === "object" ? input.exception : null;
1529
+ const decision = buildSkillProvisioningDecision({
1530
+ role,
1531
+ taskKind,
1532
+ exception: exception && typeof exception.reason === "string" && typeof exception.category === "string" ? { reason: exception.reason, category: exception.category } : undefined
1533
+ });
1534
+ const createdAt = now();
1535
+ options.journal.appendDecision({
1536
+ id: `decision:skills:${randomUUID2()}`,
1537
+ runId: typeof input.runId === "string" ? input.runId : null,
1538
+ dedupeKey: null,
1539
+ decisionType: "skill-provisioning",
1540
+ summary: `Provisioned skills for ${role}`,
1541
+ rationale: "Inspector provisions role-aware skills before execution or review.",
1542
+ details: decision,
1543
+ createdAt
1544
+ });
1545
+ options.journal.appendAction({
1546
+ id: `action:skills:${randomUUID2()}`,
1547
+ runId: typeof input.runId === "string" ? input.runId : null,
1548
+ dedupeKey: null,
1549
+ actionType: "provision_skill",
1550
+ status: "completed",
1551
+ target: role,
1552
+ input: { role, taskKind },
1553
+ result: decision,
1554
+ startedAt: createdAt,
1555
+ completedAt: createdAt
1556
+ });
1557
+ return {
1558
+ status: "completed",
1559
+ summary: `Provisioned skills for ${role}`,
1560
+ details: decision
1561
+ };
1562
+ }
1563
+ },
1564
+ run_local_review: {
1565
+ enabled: Boolean(options.reviewRunner),
1566
+ async handler(input) {
1567
+ if (!options.reviewRunner) {
1568
+ return {
1569
+ status: "unavailable",
1570
+ summary: "run_local_review is not configured",
1571
+ details: null
1572
+ };
1573
+ }
1574
+ const request = buildLocalReviewRequest({
1575
+ runId: typeof input.runId === "string" ? input.runId : "",
1576
+ taskId: typeof input.taskId === "string" ? input.taskId : null,
1577
+ focus: Array.isArray(input.focus) ? input.focus.filter((entry) => typeof entry === "string") : []
1578
+ });
1579
+ const startedAt = now();
1580
+ const result = await options.reviewRunner(request);
1581
+ options.journal.appendReview({
1582
+ id: `review:${request.runId || "global"}:${randomUUID2()}`,
1583
+ runId: request.runId || null,
1584
+ dedupeKey: null,
1585
+ reviewerType: request.reviewerType,
1586
+ status: result.status,
1587
+ findings: {
1588
+ summary: result.summary,
1589
+ details: toJsonRecord2(result.details)
1590
+ },
1591
+ createdAt: startedAt
1592
+ });
1593
+ options.journal.appendAction({
1594
+ id: `action:review:${randomUUID2()}`,
1595
+ runId: request.runId || null,
1596
+ dedupeKey: null,
1597
+ actionType: "run_local_review",
1598
+ status: result.status,
1599
+ target: request.taskId ?? request.runId,
1600
+ input: request,
1601
+ result: {
1602
+ summary: result.summary,
1603
+ details: toJsonRecord2(result.details)
1604
+ },
1605
+ startedAt,
1606
+ completedAt: now()
1607
+ });
1608
+ return result;
1609
+ }
1610
+ },
1611
+ scan_upstream_drift: {
1612
+ enabled: Boolean(options.upstreamScanRunner),
1613
+ async handler(input) {
1614
+ if (!options.upstreamScanRunner) {
1615
+ return {
1616
+ status: "unavailable",
1617
+ summary: "scan_upstream_drift is not configured",
1618
+ details: null
1619
+ };
1620
+ }
1621
+ const startedAt = now();
1622
+ const result = await options.upstreamScanRunner(input);
1623
+ options.journal.appendAction({
1624
+ id: `action:upstream:${randomUUID2()}`,
1625
+ runId: typeof input.runId === "string" ? input.runId : null,
1626
+ dedupeKey: null,
1627
+ actionType: "scan_upstream_drift",
1628
+ status: result.status,
1629
+ target: "workspace",
1630
+ input: toJsonRecord2(input),
1631
+ result: {
1632
+ summary: result.summary,
1633
+ details: toJsonRecord2(result.details)
1634
+ },
1635
+ startedAt,
1636
+ completedAt: now()
1637
+ });
1638
+ return result;
1639
+ }
1640
+ },
1641
+ generate_analysis_report: {
1642
+ enabled: Boolean(options.analysisRunner),
1643
+ async handler(input) {
1644
+ if (!options.analysisRunner) {
1645
+ return {
1646
+ status: "unavailable",
1647
+ summary: "generate_analysis_report is not configured",
1648
+ details: null
1649
+ };
1650
+ }
1651
+ const startedAt = now();
1652
+ const result = await options.analysisRunner(input);
1653
+ options.journal.appendAction({
1654
+ id: `action:analysis:${randomUUID2()}`,
1655
+ runId: typeof input.runId === "string" ? input.runId : null,
1656
+ dedupeKey: null,
1657
+ actionType: "generate_analysis_report",
1658
+ status: result.status,
1659
+ target: typeof input.reportType === "string" ? input.reportType : "default",
1660
+ input: toJsonRecord2(input),
1661
+ result: {
1662
+ summary: result.summary,
1663
+ details: toJsonRecord2(result.details)
1664
+ },
1665
+ startedAt,
1666
+ completedAt: now()
1667
+ });
1668
+ return result;
1669
+ }
1670
+ }
1671
+ };
1672
+ return {
1673
+ list() {
1674
+ return Object.entries(descriptors).filter(([, descriptor]) => descriptor.enabled).map(([name]) => name).sort();
1675
+ },
1676
+ async invoke(name, input) {
1677
+ const descriptor = descriptors[name];
1678
+ if (!descriptor) {
1679
+ return {
1680
+ status: "missing",
1681
+ summary: `Unknown inspector tool: ${name}`,
1682
+ details: null
1683
+ };
1684
+ }
1685
+ if (!descriptor.enabled) {
1686
+ return {
1687
+ status: "unavailable",
1688
+ summary: `${name} is not configured`,
1689
+ details: null
1690
+ };
1691
+ }
1692
+ return descriptor.handler(input);
1693
+ }
1694
+ };
1695
+ }
1696
+
1697
+ // packages/server/src/inspector/agent-runtime.ts
1698
+ var DEFAULT_CLIENT_INFO = {
1699
+ name: "rig_global_inspector",
1700
+ title: "Rig Global Inspector",
1701
+ version: "0.1.0"
1702
+ };
1703
+ function createInspectorAgentRuntime(options) {
1704
+ const now = options.now ?? (() => new Date().toISOString());
1705
+ const pollMs = Math.max(100, options.pollMs ?? 2000);
1706
+ const startTimeoutMs = Math.max(1000, options.startTimeoutMs ?? 20000);
1707
+ const model = options.model ?? process.env.RIG_INSPECTOR_MODEL ?? null;
1708
+ const skills = [...options.requiredSkills, ...options.recommendedSkills];
1709
+ const transport = options.transportFactory?.() ?? createCodexInspectorTransport({
1710
+ model
1711
+ });
1712
+ const baseInstructions = buildInspectorBaseInstructions({
1713
+ projectRoot: options.projectRoot,
1714
+ requiredSkills: options.requiredSkills,
1715
+ recommendedSkills: options.recommendedSkills
1716
+ });
1717
+ const developerInstructions = buildInspectorDeveloperInstructions({
1718
+ projectRoot: options.projectRoot,
1719
+ requiredSkills: options.requiredSkills,
1720
+ recommendedSkills: options.recommendedSkills
1721
+ });
1722
+ let pollTimer = null;
1723
+ let status = "stopped";
1724
+ let threadId = null;
1725
+ let processId = null;
1726
+ let lastFingerprint = null;
1727
+ let lastPromptAt = null;
1728
+ let lastTurnCompletedAt = null;
1729
+ let lastAssistantMessage = null;
1730
+ let lastError = null;
1731
+ let inFlight = null;
1732
+ let pendingTurn = null;
1733
+ const dispatchPrompt = async (turn) => {
1734
+ status = "running";
1735
+ lastPromptAt = now();
1736
+ try {
1737
+ const result = await transport.sendTurn(turn.prompt);
1738
+ lastAssistantMessage = result.assistantMessage;
1739
+ lastTurnCompletedAt = now();
1740
+ lastError = result.error;
1741
+ lastFingerprint = turn.fingerprint;
1742
+ status = result.status === "completed" ? "idle" : "errored";
1743
+ if (options.journal) {
1744
+ if (result.assistantMessage) {
1745
+ options.journal.appendObservation({
1746
+ id: `inspector-agent-message:${Date.now()}`,
1747
+ runId: null,
1748
+ dedupeKey: null,
1749
+ kind: "agent.message",
1750
+ severity: "info",
1751
+ source: "inspector-agent",
1752
+ summary: result.assistantMessage.split(`
1753
+ `)[0].slice(0, 240),
1754
+ details: {
1755
+ status: result.status,
1756
+ message: result.assistantMessage
1757
+ },
1758
+ createdAt: now()
1759
+ });
1760
+ }
1761
+ if (result.error) {
1762
+ options.journal.appendObservation({
1763
+ id: `inspector-agent-error:${Date.now()}`,
1764
+ runId: null,
1765
+ dedupeKey: null,
1766
+ kind: "agent.error",
1767
+ severity: "warn",
1768
+ source: "inspector-agent",
1769
+ summary: result.error,
1770
+ details: {
1771
+ status: result.status
1772
+ },
1773
+ createdAt: now()
1774
+ });
1775
+ }
1776
+ }
1777
+ } catch (error) {
1778
+ lastError = error instanceof Error ? error.message : String(error);
1779
+ status = "errored";
1780
+ if (options.journal) {
1781
+ options.journal.appendObservation({
1782
+ id: `inspector-agent-crash:${Date.now()}`,
1783
+ runId: null,
1784
+ dedupeKey: null,
1785
+ kind: "agent.error",
1786
+ severity: "warn",
1787
+ source: "inspector-agent",
1788
+ summary: lastError,
1789
+ details: null,
1790
+ createdAt: now()
1791
+ });
1792
+ }
1793
+ } finally {
1794
+ inFlight = null;
1795
+ if (pendingTurn) {
1796
+ const next = pendingTurn;
1797
+ pendingTurn = null;
1798
+ if (next.fingerprint === lastFingerprint) {
1799
+ return;
1800
+ }
1801
+ inFlight = dispatchPrompt(next);
1802
+ await inFlight;
1803
+ }
1804
+ }
1805
+ };
1806
+ const queuePrompt = async (turn) => {
1807
+ if (inFlight) {
1808
+ pendingTurn = turn;
1809
+ return false;
1810
+ }
1811
+ inFlight = dispatchPrompt(turn);
1812
+ await inFlight;
1813
+ return true;
1814
+ };
1815
+ const tickNow = async (reason) => {
1816
+ if (status === "stopped" || status === "starting") {
1817
+ return false;
1818
+ }
1819
+ const snapshot = options.snapshotProvider();
1820
+ const prompt = buildInspectorTurnPrompt({
1821
+ reason,
1822
+ snapshot,
1823
+ now: now()
1824
+ });
1825
+ const fingerprint = hashSnapshot(snapshot);
1826
+ if (reason !== "startup" && fingerprint === lastFingerprint) {
1827
+ return false;
1828
+ }
1829
+ return queuePrompt({
1830
+ fingerprint,
1831
+ prompt
1832
+ });
1833
+ };
1834
+ return {
1835
+ async start() {
1836
+ if (status === "starting" || status === "idle" || status === "running") {
1837
+ return false;
1838
+ }
1839
+ if (status === "errored") {
1840
+ await transport.stop().catch(() => {});
1841
+ threadId = null;
1842
+ processId = null;
1843
+ }
1844
+ status = "starting";
1845
+ try {
1846
+ const started = await withTimeout(transport.startSession({
1847
+ projectRoot: options.projectRoot,
1848
+ model,
1849
+ baseInstructions,
1850
+ developerInstructions,
1851
+ dynamicTools: INSPECTOR_TOOL_DEFINITIONS,
1852
+ invokeDynamicTool: options.invokeDynamicTool
1853
+ }), startTimeoutMs, `Inspector agent start timed out after ${startTimeoutMs}ms`);
1854
+ threadId = started.threadId;
1855
+ processId = started.processId;
1856
+ status = "idle";
1857
+ pollTimer = setInterval(() => {
1858
+ tickNow("poll").catch((error) => {
1859
+ lastError = error instanceof Error ? error.message : String(error);
1860
+ status = "errored";
1861
+ });
1862
+ }, pollMs);
1863
+ await tickNow("startup");
1864
+ return true;
1865
+ } catch (error) {
1866
+ lastError = error instanceof Error ? error.message : String(error);
1867
+ status = "errored";
1868
+ throw error;
1869
+ }
1870
+ },
1871
+ async stop() {
1872
+ if (pollTimer) {
1873
+ clearInterval(pollTimer);
1874
+ }
1875
+ pollTimer = null;
1876
+ pendingTurn = null;
1877
+ await transport.stop();
1878
+ status = "stopped";
1879
+ threadId = null;
1880
+ processId = null;
1881
+ },
1882
+ isRunning() {
1883
+ return status !== "stopped";
1884
+ },
1885
+ tickNow,
1886
+ snapshot() {
1887
+ return {
1888
+ ...transport.snapshot(),
1889
+ status,
1890
+ threadId,
1891
+ processId,
1892
+ queueDepth: pendingTurn ? 1 : 0,
1893
+ lastFingerprint,
1894
+ lastPromptAt,
1895
+ lastTurnCompletedAt,
1896
+ lastAssistantMessage,
1897
+ lastError,
1898
+ model,
1899
+ skills
1900
+ };
1901
+ }
1902
+ };
1903
+ }
1904
+ function hashPrompt(prompt) {
1905
+ return createHash("sha1").update(prompt).digest("hex");
1906
+ }
1907
+ function hashSnapshot(snapshot) {
1908
+ const compact = {
1909
+ activeRuns: snapshot.activeRuns ?? [],
1910
+ recentFindings: (snapshot.recentFindings ?? []).filter((finding) => {
1911
+ const kind = typeof finding.kind === "string" ? finding.kind : "";
1912
+ const severity = typeof finding.severity === "string" ? finding.severity : "";
1913
+ const source = typeof finding.source === "string" ? finding.source : "";
1914
+ if (source === "inspector-agent") {
1915
+ return false;
1916
+ }
1917
+ return kind !== "run.observed" || severity === "warn" || severity === "error";
1918
+ }),
1919
+ followups: snapshot.followups ?? [],
1920
+ analysisReports: snapshot.analysisReports ?? [],
1921
+ availableTools: snapshot.availableTools ?? []
1922
+ };
1923
+ return hashPrompt(JSON.stringify(compact));
1924
+ }
1925
+ function createCodexInspectorTransport(options) {
1926
+ const turnTimeoutMs = Math.max(5000, options.turnTimeoutMs ?? 45000);
1927
+ let child = null;
1928
+ let threadId = null;
1929
+ let processId = null;
1930
+ let status = "stopped";
1931
+ let lastAssistantMessage = null;
1932
+ let lastError = null;
1933
+ const pendingResponses = new Map;
1934
+ const assistantMessageText = new Map;
1935
+ const inFlightToolCalls = new Set;
1936
+ const recentProtocolEvents = [];
1937
+ let nextRequestId = 1;
1938
+ let sendQueue = Promise.resolve();
1939
+ let currentTurn = null;
1940
+ let invokeDynamicTool = null;
1941
+ const rememberProtocolEvent = (event) => {
1942
+ recentProtocolEvents.push(event);
1943
+ if (recentProtocolEvents.length > 25) {
1944
+ recentProtocolEvents.splice(0, recentProtocolEvents.length - 25);
1945
+ }
1946
+ };
1947
+ const sendMessage = async (payload) => {
1948
+ if (!child) {
1949
+ throw new Error("Inspector agent session is not started.");
1950
+ }
1951
+ const activeChild = child;
1952
+ if (activeChild.stdin.destroyed || !activeChild.stdin.writable) {
1953
+ throw new Error(lastError ?? "Inspector agent transport is not writable.");
1954
+ }
1955
+ rememberProtocolEvent({
1956
+ at: new Date().toISOString(),
1957
+ direction: "send",
1958
+ kind: payload.method ? "request" : "response",
1959
+ method: typeof payload.method === "string" ? payload.method : `response:${String(payload.id ?? "unknown")}`
1960
+ });
1961
+ const line = `${JSON.stringify(payload)}
1962
+ `;
1963
+ sendQueue = sendQueue.then(() => writeChildLine(activeChild, line));
1964
+ await sendQueue;
1965
+ };
1966
+ const sendRequest = async (method, params) => {
1967
+ const id = nextRequestId;
1968
+ nextRequestId += 1;
1969
+ const response = new Promise((resolve3, reject) => {
1970
+ pendingResponses.set(id, { resolve: resolve3, reject });
1971
+ });
1972
+ response.catch(() => {});
1973
+ try {
1974
+ await sendMessage({ id, method, params });
1975
+ } catch (error) {
1976
+ pendingResponses.delete(id);
1977
+ throw error;
1978
+ }
1979
+ return response;
1980
+ };
1981
+ const sendResponse = async (id, result) => {
1982
+ await sendMessage({ id, result });
1983
+ };
1984
+ const sendError = async (id, message) => {
1985
+ await sendMessage({
1986
+ id,
1987
+ error: {
1988
+ code: -32603,
1989
+ message
1990
+ }
1991
+ });
1992
+ };
1993
+ const trackToolCall = (call) => {
1994
+ inFlightToolCalls.add(call);
1995
+ call.finally(() => inFlightToolCalls.delete(call)).catch((err) => {
1996
+ console.warn("[inspector/agent-runtime] in-flight tool call failure:", err);
1997
+ });
1998
+ };
1999
+ const handleServerRequest = (message) => {
2000
+ const method = message.method;
2001
+ const requestId = message.id;
2002
+ if (!method || requestId === undefined) {
2003
+ return;
2004
+ }
2005
+ if (method === "item/tool/call") {
2006
+ const params = message.params ?? {};
2007
+ const toolName = normalizeString(params.tool);
2008
+ const toolArgs = asRecord(params.arguments) ?? {};
2009
+ const toolCall = (async () => {
2010
+ if (!toolName || !invokeDynamicTool) {
2011
+ rememberProtocolEvent({
2012
+ at: new Date().toISOString(),
2013
+ direction: "recv",
2014
+ kind: "request",
2015
+ method: "item/tool/call:malformed"
2016
+ });
2017
+ await sendError(requestId, "Malformed inspector dynamic tool call.");
2018
+ return;
2019
+ }
2020
+ try {
2021
+ const invocation = await invokeDynamicTool(toolName, toolArgs);
2022
+ await sendResponse(requestId, {
2023
+ contentItems: [
2024
+ {
2025
+ type: "inputText",
2026
+ text: typeof invocation.details === "string" ? invocation.details : JSON.stringify({
2027
+ status: invocation.status,
2028
+ summary: invocation.summary,
2029
+ details: invocation.details
2030
+ }, null, 2)
2031
+ }
2032
+ ],
2033
+ success: invocation.status !== "failed" && invocation.status !== "missing"
2034
+ });
2035
+ } catch (error) {
2036
+ const messageText = error instanceof Error ? error.message : String(error);
2037
+ await sendResponse(requestId, {
2038
+ contentItems: [{ type: "inputText", text: messageText }],
2039
+ success: false
2040
+ });
2041
+ }
2042
+ })();
2043
+ trackToolCall(toolCall);
2044
+ return;
2045
+ }
2046
+ if (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval") {
2047
+ trackToolCall(sendResponse(requestId, { decision: "approved_for_session" }));
2048
+ return;
2049
+ }
2050
+ if (method === "item/permissions/requestApproval") {
2051
+ trackToolCall(sendResponse(requestId, {
2052
+ permissions: asRecord(message.params)?.permissions ?? {},
2053
+ scope: "session"
2054
+ }));
2055
+ return;
2056
+ }
2057
+ if (method === "item/tool/requestUserInput") {
2058
+ trackToolCall(sendResponse(requestId, { answers: {} }));
2059
+ return;
2060
+ }
2061
+ if (method === "mcpServer/elicitation/request") {
2062
+ trackToolCall(sendResponse(requestId, { action: "decline" }));
2063
+ return;
2064
+ }
2065
+ if (method === "applyPatchApproval" || method === "execCommandApproval") {
2066
+ trackToolCall(sendResponse(requestId, { decision: "approved_for_session" }));
2067
+ return;
2068
+ }
2069
+ trackToolCall(sendError(requestId, `Unsupported inspector app-server request: ${method}`));
2070
+ };
2071
+ const handleNotification = (message) => {
2072
+ const method = message.method;
2073
+ const params = message.params ?? {};
2074
+ if (!method) {
2075
+ return;
2076
+ }
2077
+ if (method === "thread/started") {
2078
+ const thread = asRecord(params.thread);
2079
+ threadId = normalizeString(thread?.id);
2080
+ status = "idle";
2081
+ return;
2082
+ }
2083
+ if (method === "turn/started") {
2084
+ status = "running";
2085
+ currentTurn?.events.push({ method, params });
2086
+ return;
2087
+ }
2088
+ if (method === "item/agentMessage/delta") {
2089
+ const itemId = normalizeString(params.itemId);
2090
+ const delta = typeof params.delta === "string" ? params.delta : "";
2091
+ if (itemId && delta) {
2092
+ assistantMessageText.set(itemId, `${assistantMessageText.get(itemId) ?? ""}${delta}`);
2093
+ }
2094
+ return;
2095
+ }
2096
+ if (method === "item/completed") {
2097
+ const item = asRecord(params.item);
2098
+ const itemType = normalizeString(item?.type);
2099
+ const itemId = normalizeString(item?.id);
2100
+ currentTurn?.events.push({ method, params });
2101
+ if (itemType === "agentMessage" && itemId) {
2102
+ const finalText = normalizeString(item?.text) ?? assistantMessageText.get(itemId) ?? "";
2103
+ assistantMessageText.delete(itemId);
2104
+ if (finalText.trim()) {
2105
+ lastAssistantMessage = finalText.trim();
2106
+ }
2107
+ }
2108
+ return;
2109
+ }
2110
+ if (method === "error") {
2111
+ lastError = normalizeString(params.message) ?? JSON.stringify(params);
2112
+ currentTurn?.events.push({ method, params });
2113
+ return;
2114
+ }
2115
+ if (method === "turn/completed") {
2116
+ const turn = asRecord(params.turn);
2117
+ const error = asRecord(turn?.error);
2118
+ const completion = {
2119
+ status: normalizeString(turn?.status) ?? "failed",
2120
+ error: normalizeString(error?.message),
2121
+ assistantMessage: lastAssistantMessage,
2122
+ events: currentTurn?.events ?? []
2123
+ };
2124
+ status = completion.status === "completed" ? "idle" : "errored";
2125
+ currentTurn?.resolve(completion);
2126
+ currentTurn = null;
2127
+ return;
2128
+ }
2129
+ };
2130
+ const bootstrapChild = (config) => {
2131
+ const codexExecutable = resolveCodexExecutable();
2132
+ child = spawn(codexExecutable, ["app-server"], {
2133
+ cwd: config.projectRoot,
2134
+ env: process.env,
2135
+ stdio: ["pipe", "pipe", "pipe"]
2136
+ });
2137
+ processId = child.pid ?? null;
2138
+ const stdout = createInterface({ input: child.stdout });
2139
+ const stderr = createInterface({ input: child.stderr });
2140
+ stdout.on("line", (line) => {
2141
+ const trimmed = line.trim();
2142
+ if (!trimmed) {
2143
+ return;
2144
+ }
2145
+ let message;
2146
+ try {
2147
+ message = JSON.parse(trimmed);
2148
+ } catch {
2149
+ return;
2150
+ }
2151
+ if (message.method) {
2152
+ if (message.id !== undefined) {
2153
+ rememberProtocolEvent({
2154
+ at: new Date().toISOString(),
2155
+ direction: "recv",
2156
+ kind: "request",
2157
+ method: message.method
2158
+ });
2159
+ handleServerRequest(message);
2160
+ } else {
2161
+ rememberProtocolEvent({
2162
+ at: new Date().toISOString(),
2163
+ direction: "recv",
2164
+ kind: "notification",
2165
+ method: message.method
2166
+ });
2167
+ handleNotification(message);
2168
+ }
2169
+ return;
2170
+ }
2171
+ if (message.id !== undefined) {
2172
+ rememberProtocolEvent({
2173
+ at: new Date().toISOString(),
2174
+ direction: "recv",
2175
+ kind: "response",
2176
+ method: `response:${String(message.id)}`
2177
+ });
2178
+ const pending = pendingResponses.get(message.id);
2179
+ if (!pending) {
2180
+ return;
2181
+ }
2182
+ pendingResponses.delete(message.id);
2183
+ if (message.error) {
2184
+ pending.reject(new Error(formatJsonRpcError(message.error)));
2185
+ } else {
2186
+ pending.resolve(message.result ?? {});
2187
+ }
2188
+ }
2189
+ });
2190
+ stderr.on("line", (line) => {
2191
+ if (line.trim()) {
2192
+ rememberProtocolEvent({
2193
+ at: new Date().toISOString(),
2194
+ direction: "recv",
2195
+ kind: "stderr",
2196
+ method: line.trim().slice(0, 240)
2197
+ });
2198
+ lastError = line.trim();
2199
+ }
2200
+ });
2201
+ child.once("error", (error) => {
2202
+ const message = error instanceof Error ? error.message : String(error);
2203
+ lastError = message;
2204
+ status = "errored";
2205
+ for (const [id, pending] of pendingResponses.entries()) {
2206
+ pending.reject(new Error(message));
2207
+ pendingResponses.delete(id);
2208
+ }
2209
+ if (currentTurn) {
2210
+ currentTurn.reject(new Error(message));
2211
+ currentTurn = null;
2212
+ }
2213
+ threadId = null;
2214
+ processId = null;
2215
+ child = null;
2216
+ });
2217
+ child.once("close", (code, signal) => {
2218
+ const exitMessage = `Inspector app-server exited (${String(code ?? signal ?? "unknown")})`;
2219
+ for (const [id, pending] of pendingResponses.entries()) {
2220
+ pending.reject(new Error(exitMessage));
2221
+ pendingResponses.delete(id);
2222
+ }
2223
+ if (currentTurn) {
2224
+ currentTurn.reject(new Error(exitMessage));
2225
+ currentTurn = null;
2226
+ }
2227
+ status = "stopped";
2228
+ lastError = exitMessage;
2229
+ threadId = null;
2230
+ processId = null;
2231
+ child = null;
2232
+ });
2233
+ return child;
2234
+ };
2235
+ return {
2236
+ async startSession(config) {
2237
+ if (child) {
2238
+ return { threadId, processId };
2239
+ }
2240
+ invokeDynamicTool = config.invokeDynamicTool;
2241
+ status = "starting";
2242
+ const activeChild = bootstrapChild(config);
2243
+ await waitForChildSpawn(activeChild);
2244
+ await sendRequest("initialize", {
2245
+ clientInfo: DEFAULT_CLIENT_INFO,
2246
+ capabilities: {
2247
+ experimentalApi: true
2248
+ }
2249
+ });
2250
+ const started = await sendRequest("thread/start", {
2251
+ model: config.model ?? options.model ?? null,
2252
+ cwd: config.projectRoot,
2253
+ approvalPolicy: "never",
2254
+ sandbox: "danger-full-access",
2255
+ serviceName: "rig_global_inspector",
2256
+ personality: "pragmatic",
2257
+ ephemeral: false,
2258
+ baseInstructions: config.baseInstructions,
2259
+ developerInstructions: config.developerInstructions,
2260
+ dynamicTools: config.dynamicTools
2261
+ });
2262
+ const thread = asRecord(started.thread);
2263
+ threadId = normalizeString(thread?.id) ?? threadId;
2264
+ if (!threadId) {
2265
+ const preview = JSON.stringify(started).slice(0, 400);
2266
+ throw new Error(`Codex thread/start response did not include thread.id (got: ${preview}). ` + `Likely codex protocol version mismatch \u2014 current Rig speaks codex 0.130 schema.`);
2267
+ }
2268
+ status = "idle";
2269
+ return { threadId, processId };
2270
+ },
2271
+ async sendTurn(prompt) {
2272
+ if (!child || !threadId) {
2273
+ throw new Error("Inspector agent session is not ready.");
2274
+ }
2275
+ if (currentTurn) {
2276
+ throw new Error("Inspector agent turn already in progress.");
2277
+ }
2278
+ lastAssistantMessage = null;
2279
+ lastError = null;
2280
+ const turnResult = new Promise((resolve3, reject) => {
2281
+ currentTurn = {
2282
+ resolve: resolve3,
2283
+ reject,
2284
+ events: []
2285
+ };
2286
+ });
2287
+ try {
2288
+ await sendRequest("turn/start", {
2289
+ threadId,
2290
+ approvalPolicy: "never",
2291
+ input: [
2292
+ {
2293
+ type: "text",
2294
+ text: prompt,
2295
+ text_elements: []
2296
+ }
2297
+ ]
2298
+ });
2299
+ } catch (error) {
2300
+ currentTurn = null;
2301
+ throw error;
2302
+ }
2303
+ let result;
2304
+ try {
2305
+ result = await withTimeout(turnResult, turnTimeoutMs, `Inspector agent turn timed out after ${turnTimeoutMs}ms`);
2306
+ } catch (error) {
2307
+ currentTurn = null;
2308
+ if (child) {
2309
+ terminateChild(child);
2310
+ child = null;
2311
+ }
2312
+ throw error;
2313
+ }
2314
+ if (inFlightToolCalls.size > 0) {
2315
+ await Promise.allSettled(Array.from(inFlightToolCalls));
2316
+ }
2317
+ return result;
2318
+ },
2319
+ snapshot() {
2320
+ return {
2321
+ status,
2322
+ threadId,
2323
+ processId,
2324
+ queueDepth: 0,
2325
+ lastAssistantMessage,
2326
+ lastError,
2327
+ recentProtocolEvents: [...recentProtocolEvents]
2328
+ };
2329
+ },
2330
+ async stop() {
2331
+ if (child) {
2332
+ terminateChild(child);
2333
+ child = null;
2334
+ }
2335
+ status = "stopped";
2336
+ threadId = null;
2337
+ processId = null;
2338
+ }
2339
+ };
2340
+ }
2341
+ function writeChildLine(child, line) {
2342
+ return new Promise((resolve3, reject) => {
2343
+ child.stdin.write(line, (error) => {
2344
+ if (error) {
2345
+ reject(error);
2346
+ return;
2347
+ }
2348
+ resolve3();
2349
+ });
2350
+ });
2351
+ }
2352
+ function terminateChild(child) {
2353
+ if (child.killed) {
2354
+ return;
2355
+ }
2356
+ try {
2357
+ child.kill("SIGTERM");
2358
+ } catch {}
2359
+ }
2360
+ async function waitForChildSpawn(child) {
2361
+ await new Promise((resolve3, reject) => {
2362
+ const onSpawn = () => {
2363
+ cleanup();
2364
+ resolve3();
2365
+ };
2366
+ const onError = (error) => {
2367
+ cleanup();
2368
+ reject(error);
2369
+ };
2370
+ const cleanup = () => {
2371
+ child.off("spawn", onSpawn);
2372
+ child.off("error", onError);
2373
+ };
2374
+ child.once("spawn", onSpawn);
2375
+ child.once("error", onError);
2376
+ });
2377
+ }
2378
+ function resolveCodexExecutable() {
2379
+ const candidate = process.env.RIG_INSPECTOR_CODEX_BIN ?? Bun.which("codex");
2380
+ if (!candidate) {
2381
+ throw new Error("Codex CLI is not available on PATH for the inspector app-server.");
2382
+ }
2383
+ let resolvedCandidate;
2384
+ try {
2385
+ resolvedCandidate = realpathSync(candidate);
2386
+ } catch {
2387
+ throw new Error(`Codex CLI is not executable for the inspector app-server: ${candidate}`);
2388
+ }
2389
+ try {
2390
+ accessSync(resolvedCandidate, constants.X_OK);
2391
+ } catch {
2392
+ throw new Error(`Codex CLI is not executable for the inspector app-server: ${candidate}`);
2393
+ }
2394
+ return resolvedCandidate;
2395
+ }
2396
+ function asRecord(value) {
2397
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
2398
+ }
2399
+ function normalizeString(value) {
2400
+ if (typeof value !== "string") {
2401
+ return null;
2402
+ }
2403
+ const trimmed = value.trim();
2404
+ return trimmed.length > 0 ? trimmed : null;
2405
+ }
2406
+ function formatJsonRpcError(error) {
2407
+ if (!error) {
2408
+ return "Unknown app-server error";
2409
+ }
2410
+ const parts = [error.message, error.data ? JSON.stringify(error.data) : null].filter(Boolean);
2411
+ return parts.join(" ");
2412
+ }
2413
+ async function withTimeout(promise, timeoutMs, message) {
2414
+ let timer = null;
2415
+ try {
2416
+ return await Promise.race([
2417
+ promise,
2418
+ new Promise((_, reject) => {
2419
+ timer = setTimeout(() => reject(new Error(message)), timeoutMs);
2420
+ })
2421
+ ]);
2422
+ } finally {
2423
+ if (timer) {
2424
+ clearTimeout(timer);
2425
+ }
2426
+ }
2427
+ }
2428
+
2429
+ // packages/server/src/inspector/analysis.ts
2430
+ function summarizeInspectorJournal(options) {
2431
+ const now = options.now ?? (() => new Date().toISOString());
2432
+ const counts = options.journal.getCounts();
2433
+ const activeRuns = options.journal.listActiveRuns();
2434
+ const recentFindings = options.journal.listRecentFindings({ limit: 50 });
2435
+ const severityCounts = recentFindings.reduce((acc, finding) => {
2436
+ acc[finding.severity] = (acc[finding.severity] ?? 0) + 1;
2437
+ return acc;
2438
+ }, {});
2439
+ const conflictCount = activeRuns.filter((run) => run.conflictState !== "none").length;
2440
+ const summary = `Inspector journal summary: observations=${counts.observations}, decisions=${counts.decisions}, ` + `actions=${counts.actions}, followups=${counts.followups}, activeRuns=${activeRuns.length}, conflicts=${conflictCount}`;
2441
+ options.journal.appendAnalysisReport({
2442
+ id: options.reportId,
2443
+ dedupeKey: `${options.reportType}:${options.reportId}`,
2444
+ reportType: options.reportType,
2445
+ status: "completed",
2446
+ summary,
2447
+ details: {
2448
+ ...counts,
2449
+ activeRuns: activeRuns.length,
2450
+ conflictCount,
2451
+ severityCounts
2452
+ },
2453
+ createdAt: now()
2454
+ });
2455
+ return {
2456
+ status: "completed",
2457
+ summary,
2458
+ details: {
2459
+ ...counts,
2460
+ activeRuns: activeRuns.length,
2461
+ conflictCount,
2462
+ severityCounts
2463
+ }
2464
+ };
2465
+ }
2466
+
2467
+ // packages/server/src/inspector/journal.ts
2468
+ import { Database } from "bun:sqlite";
2469
+ import { mkdirSync as mkdirSync3 } from "fs";
2470
+ import { dirname as dirname3, resolve as resolve3 } from "path";
2471
+ var SCHEMA = [
2472
+ `CREATE TABLE IF NOT EXISTS inspector_runs (
2473
+ run_id TEXT PRIMARY KEY,
2474
+ workspace_id TEXT,
2475
+ task_id TEXT,
2476
+ runtime_adapter TEXT,
2477
+ provider TEXT,
2478
+ status TEXT NOT NULL,
2479
+ conflict_state TEXT NOT NULL DEFAULT 'none',
2480
+ conflict_summary TEXT,
2481
+ source TEXT,
2482
+ title TEXT,
2483
+ started_at TEXT,
2484
+ updated_at TEXT NOT NULL,
2485
+ completed_at TEXT
2486
+ )`,
2487
+ `CREATE TABLE IF NOT EXISTS inspector_observations (
2488
+ id TEXT PRIMARY KEY,
2489
+ run_id TEXT,
2490
+ dedupe_key TEXT,
2491
+ kind TEXT NOT NULL,
2492
+ severity TEXT NOT NULL,
2493
+ source TEXT NOT NULL,
2494
+ summary TEXT NOT NULL,
2495
+ details_json TEXT,
2496
+ created_at TEXT NOT NULL
2497
+ )`,
2498
+ `CREATE TABLE IF NOT EXISTS inspector_decisions (
2499
+ id TEXT PRIMARY KEY,
2500
+ run_id TEXT,
2501
+ dedupe_key TEXT,
2502
+ decision_type TEXT NOT NULL,
2503
+ summary TEXT NOT NULL,
2504
+ rationale TEXT NOT NULL,
2505
+ details_json TEXT,
2506
+ created_at TEXT NOT NULL
2507
+ )`,
2508
+ `CREATE TABLE IF NOT EXISTS inspector_actions (
2509
+ id TEXT PRIMARY KEY,
2510
+ run_id TEXT,
2511
+ dedupe_key TEXT,
2512
+ action_type TEXT NOT NULL,
2513
+ status TEXT NOT NULL,
2514
+ target TEXT,
2515
+ input_json TEXT,
2516
+ result_json TEXT,
2517
+ started_at TEXT NOT NULL,
2518
+ completed_at TEXT
2519
+ )`,
2520
+ `CREATE TABLE IF NOT EXISTS inspector_reviews (
2521
+ id TEXT PRIMARY KEY,
2522
+ run_id TEXT,
2523
+ dedupe_key TEXT,
2524
+ reviewer_type TEXT NOT NULL,
2525
+ status TEXT NOT NULL,
2526
+ findings_json TEXT,
2527
+ created_at TEXT NOT NULL
2528
+ )`,
2529
+ `CREATE TABLE IF NOT EXISTS inspector_followups (
2530
+ id TEXT PRIMARY KEY,
2531
+ origin_run_id TEXT,
2532
+ dedupe_key TEXT,
2533
+ task_id TEXT,
2534
+ kind TEXT NOT NULL,
2535
+ status TEXT NOT NULL,
2536
+ summary TEXT NOT NULL,
2537
+ details_json TEXT,
2538
+ created_at TEXT NOT NULL
2539
+ )`,
2540
+ `CREATE TABLE IF NOT EXISTS upstream_sync_scans (
2541
+ id TEXT PRIMARY KEY,
2542
+ dedupe_key TEXT,
2543
+ started_at TEXT NOT NULL,
2544
+ completed_at TEXT,
2545
+ status TEXT NOT NULL,
2546
+ summary TEXT NOT NULL,
2547
+ details_json TEXT
2548
+ )`,
2549
+ `CREATE TABLE IF NOT EXISTS analysis_reports (
2550
+ id TEXT PRIMARY KEY,
2551
+ dedupe_key TEXT,
2552
+ report_type TEXT NOT NULL,
2553
+ status TEXT NOT NULL,
2554
+ summary TEXT NOT NULL,
2555
+ details_json TEXT,
2556
+ created_at TEXT NOT NULL
2557
+ )`,
2558
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_inspector_observations_dedupe
2559
+ ON inspector_observations(dedupe_key) WHERE dedupe_key IS NOT NULL`,
2560
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_inspector_decisions_dedupe
2561
+ ON inspector_decisions(dedupe_key) WHERE dedupe_key IS NOT NULL`,
2562
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_inspector_actions_dedupe
2563
+ ON inspector_actions(dedupe_key) WHERE dedupe_key IS NOT NULL`,
2564
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_inspector_reviews_dedupe
2565
+ ON inspector_reviews(dedupe_key) WHERE dedupe_key IS NOT NULL`,
2566
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_inspector_followups_dedupe
2567
+ ON inspector_followups(dedupe_key) WHERE dedupe_key IS NOT NULL`,
2568
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_upstream_sync_scans_dedupe
2569
+ ON upstream_sync_scans(dedupe_key) WHERE dedupe_key IS NOT NULL`,
2570
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_analysis_reports_dedupe
2571
+ ON analysis_reports(dedupe_key) WHERE dedupe_key IS NOT NULL`,
2572
+ `CREATE INDEX IF NOT EXISTS idx_inspector_runs_status_updated
2573
+ ON inspector_runs(status, updated_at DESC)`,
2574
+ `CREATE INDEX IF NOT EXISTS idx_inspector_observations_run_created
2575
+ ON inspector_observations(run_id, created_at DESC)`
2576
+ ];
2577
+ function jsonText(value) {
2578
+ return value == null ? null : JSON.stringify(value);
2579
+ }
2580
+ function parseJsonRecord(value) {
2581
+ if (typeof value !== "string" || value.length === 0) {
2582
+ return null;
2583
+ }
2584
+ return JSON.parse(value);
2585
+ }
2586
+ function normalizeError(error) {
2587
+ return error instanceof Error ? error.message : String(error);
2588
+ }
2589
+ function safeWrite(sqlite, record, fn) {
2590
+ try {
2591
+ fn();
2592
+ const row = sqlite.prepare("SELECT changes() AS count").get();
2593
+ return { ok: true, record, changed: Number(row?.count ?? 0) > 0 };
2594
+ } catch (error) {
2595
+ return { ok: false, error: normalizeError(error) };
2596
+ }
2597
+ }
2598
+ function resolveInspectorJournalPath(projectRoot) {
2599
+ return resolve3(resolveRigServerPaths(projectRoot).stateDir, "inspector", "journal.sqlite");
2600
+ }
2601
+ function createInspectorJournal(options) {
2602
+ const dbPath = resolve3(options.dbPath ?? resolveInspectorJournalPath(options.projectRoot));
2603
+ mkdirSync3(dirname3(dbPath), { recursive: true });
2604
+ const sqlite = new Database(dbPath, { create: true, strict: true });
2605
+ sqlite.exec("PRAGMA journal_mode = WAL");
2606
+ sqlite.exec("PRAGMA busy_timeout = 50");
2607
+ for (const statement of SCHEMA) {
2608
+ sqlite.exec(statement);
2609
+ }
2610
+ const upsertRunStatement = sqlite.prepare(`
2611
+ INSERT INTO inspector_runs (
2612
+ run_id, workspace_id, task_id, runtime_adapter, provider, status, conflict_state, conflict_summary,
2613
+ source, title, started_at, updated_at, completed_at
2614
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2615
+ ON CONFLICT(run_id) DO UPDATE SET
2616
+ workspace_id = excluded.workspace_id,
2617
+ task_id = excluded.task_id,
2618
+ runtime_adapter = excluded.runtime_adapter,
2619
+ provider = excluded.provider,
2620
+ status = excluded.status,
2621
+ conflict_state = excluded.conflict_state,
2622
+ conflict_summary = excluded.conflict_summary,
2623
+ source = excluded.source,
2624
+ title = excluded.title,
2625
+ started_at = excluded.started_at,
2626
+ updated_at = excluded.updated_at,
2627
+ completed_at = excluded.completed_at
2628
+ `);
2629
+ const observationStatement = sqlite.prepare(`
2630
+ INSERT OR IGNORE INTO inspector_observations (
2631
+ id, run_id, dedupe_key, kind, severity, source, summary, details_json, created_at
2632
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
2633
+ `);
2634
+ const decisionStatement = sqlite.prepare(`
2635
+ INSERT OR IGNORE INTO inspector_decisions (
2636
+ id, run_id, dedupe_key, decision_type, summary, rationale, details_json, created_at
2637
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
2638
+ `);
2639
+ const actionStatement = sqlite.prepare(`
2640
+ INSERT OR IGNORE INTO inspector_actions (
2641
+ id, run_id, dedupe_key, action_type, status, target, input_json, result_json, started_at, completed_at
2642
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2643
+ `);
2644
+ const reviewStatement = sqlite.prepare(`
2645
+ INSERT OR IGNORE INTO inspector_reviews (
2646
+ id, run_id, dedupe_key, reviewer_type, status, findings_json, created_at
2647
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
2648
+ `);
2649
+ const followupStatement = sqlite.prepare(`
2650
+ INSERT OR IGNORE INTO inspector_followups (
2651
+ id, origin_run_id, dedupe_key, task_id, kind, status, summary, details_json, created_at
2652
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
2653
+ `);
2654
+ const scanStatement = sqlite.prepare(`
2655
+ INSERT OR IGNORE INTO upstream_sync_scans (
2656
+ id, dedupe_key, started_at, completed_at, status, summary, details_json
2657
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
2658
+ `);
2659
+ const reportStatement = sqlite.prepare(`
2660
+ INSERT OR IGNORE INTO analysis_reports (
2661
+ id, dedupe_key, report_type, status, summary, details_json, created_at
2662
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
2663
+ `);
2664
+ const attachFollowupTaskStatement = sqlite.prepare(`
2665
+ UPDATE inspector_followups
2666
+ SET task_id = ?, status = ?, details_json = ?
2667
+ WHERE dedupe_key = ?
2668
+ `);
2669
+ const getFollowupByDedupeKeyStatement = sqlite.prepare(`
2670
+ SELECT id, origin_run_id, dedupe_key, task_id, kind, status, summary, details_json, created_at
2671
+ FROM inspector_followups
2672
+ WHERE dedupe_key = ?
2673
+ LIMIT 1
2674
+ `);
2675
+ const listActiveRunsStatement = sqlite.prepare(`
2676
+ SELECT
2677
+ run_id,
2678
+ workspace_id,
2679
+ task_id,
2680
+ runtime_adapter,
2681
+ provider,
2682
+ status,
2683
+ conflict_state,
2684
+ conflict_summary,
2685
+ source,
2686
+ title,
2687
+ started_at,
2688
+ updated_at,
2689
+ completed_at
2690
+ FROM inspector_runs
2691
+ WHERE status IN ('preparing', 'running', 'validating', 'reviewing')
2692
+ ORDER BY updated_at DESC
2693
+ `);
2694
+ const listRecentFindingsStatement = sqlite.prepare(`
2695
+ SELECT id, run_id, kind, severity, source, summary, details_json, created_at
2696
+ FROM inspector_observations
2697
+ ORDER BY created_at DESC
2698
+ LIMIT ?
2699
+ `);
2700
+ const listRecentDecisionsStatement = sqlite.prepare(`
2701
+ SELECT id, run_id, decision_type, summary, rationale, details_json, created_at
2702
+ FROM inspector_decisions
2703
+ ORDER BY created_at DESC
2704
+ LIMIT ?
2705
+ `);
2706
+ const listRecentActionsStatement = sqlite.prepare(`
2707
+ SELECT id, run_id, action_type, status, target, input_json, result_json, started_at, completed_at
2708
+ FROM inspector_actions
2709
+ ORDER BY started_at DESC
2710
+ LIMIT ?
2711
+ `);
2712
+ function mapFollowupRow(row) {
2713
+ return {
2714
+ id: String(row.id),
2715
+ originRunId: row.origin_run_id == null ? null : String(row.origin_run_id),
2716
+ dedupeKey: row.dedupe_key == null ? null : String(row.dedupe_key),
2717
+ taskId: row.task_id == null ? null : String(row.task_id),
2718
+ kind: String(row.kind),
2719
+ status: String(row.status),
2720
+ summary: String(row.summary),
2721
+ details: parseJsonRecord(row.details_json),
2722
+ createdAt: String(row.created_at)
2723
+ };
2724
+ }
2725
+ return {
2726
+ path: dbPath,
2727
+ upsertRun(record) {
2728
+ return safeWrite(sqlite, record, () => {
2729
+ upsertRunStatement.run(record.runId, record.workspaceId, record.taskId, record.runtimeAdapter, record.provider, record.status, record.conflictState, record.conflictSummary, record.source, record.title, record.startedAt, record.updatedAt, record.completedAt);
2730
+ });
2731
+ },
2732
+ appendObservation(record) {
2733
+ return safeWrite(sqlite, record, () => {
2734
+ observationStatement.run(record.id, record.runId, record.dedupeKey ?? null, record.kind, record.severity, record.source, record.summary, jsonText(record.details), record.createdAt);
2735
+ });
2736
+ },
2737
+ appendDecision(record) {
2738
+ return safeWrite(sqlite, record, () => {
2739
+ decisionStatement.run(record.id, record.runId, record.dedupeKey ?? null, record.decisionType, record.summary, record.rationale, jsonText(record.details), record.createdAt);
2740
+ });
2741
+ },
2742
+ appendAction(record) {
2743
+ return safeWrite(sqlite, record, () => {
2744
+ actionStatement.run(record.id, record.runId, record.dedupeKey ?? null, record.actionType, record.status, record.target, jsonText(record.input), jsonText(record.result), record.startedAt, record.completedAt);
2745
+ });
2746
+ },
2747
+ appendReview(record) {
2748
+ return safeWrite(sqlite, record, () => {
2749
+ reviewStatement.run(record.id, record.runId, record.dedupeKey ?? null, record.reviewerType, record.status, jsonText(record.findings), record.createdAt);
2750
+ });
2751
+ },
2752
+ appendFollowup(record) {
2753
+ return safeWrite(sqlite, record, () => {
2754
+ followupStatement.run(record.id, record.originRunId, record.dedupeKey ?? null, record.taskId, record.kind, record.status, record.summary, jsonText(record.details), record.createdAt);
2755
+ });
2756
+ },
2757
+ attachFollowupTask(input) {
2758
+ const record = {
2759
+ dedupeKey: input.dedupeKey,
2760
+ taskId: input.taskId,
2761
+ status: input.status,
2762
+ details: input.details ?? null
2763
+ };
2764
+ return safeWrite(sqlite, record, () => {
2765
+ attachFollowupTaskStatement.run(input.taskId, input.status, jsonText(input.details), input.dedupeKey);
2766
+ });
2767
+ },
2768
+ getFollowupByDedupeKey(dedupeKey) {
2769
+ const row = getFollowupByDedupeKeyStatement.get(dedupeKey);
2770
+ return row ? mapFollowupRow(row) : null;
2771
+ },
2772
+ appendUpstreamSyncScan(record) {
2773
+ return safeWrite(sqlite, record, () => {
2774
+ scanStatement.run(record.id, record.dedupeKey ?? null, record.startedAt, record.completedAt, record.status, record.summary, jsonText(record.details));
2775
+ });
2776
+ },
2777
+ appendAnalysisReport(record) {
2778
+ return safeWrite(sqlite, record, () => {
2779
+ reportStatement.run(record.id, record.dedupeKey ?? null, record.reportType, record.status, record.summary, jsonText(record.details), record.createdAt);
2780
+ });
2781
+ },
2782
+ listActiveRuns() {
2783
+ return listActiveRunsStatement.all().map((row) => ({
2784
+ runId: String(row.run_id),
2785
+ workspaceId: row.workspace_id == null ? null : String(row.workspace_id),
2786
+ taskId: row.task_id == null ? null : String(row.task_id),
2787
+ runtimeAdapter: row.runtime_adapter == null ? null : String(row.runtime_adapter),
2788
+ provider: row.provider == null ? null : String(row.provider),
2789
+ status: String(row.status),
2790
+ conflictState: String(row.conflict_state),
2791
+ conflictSummary: row.conflict_summary == null ? null : String(row.conflict_summary),
2792
+ source: row.source == null ? null : String(row.source),
2793
+ title: row.title == null ? null : String(row.title),
2794
+ startedAt: row.started_at == null ? null : String(row.started_at),
2795
+ updatedAt: String(row.updated_at),
2796
+ completedAt: row.completed_at == null ? null : String(row.completed_at)
2797
+ }));
2798
+ },
2799
+ listRecentFindings(options2 = {}) {
2800
+ const limit = options2.limit ?? 10;
2801
+ return listRecentFindingsStatement.all(limit).map((row) => ({
2802
+ id: String(row.id),
2803
+ runId: row.run_id == null ? null : String(row.run_id),
2804
+ kind: String(row.kind),
2805
+ severity: String(row.severity),
2806
+ source: String(row.source),
2807
+ summary: String(row.summary),
2808
+ details: parseJsonRecord(row.details_json),
2809
+ createdAt: String(row.created_at)
2810
+ }));
2811
+ },
2812
+ listRecentDecisions(options2 = {}) {
2813
+ const limit = options2.limit ?? 10;
2814
+ return listRecentDecisionsStatement.all(limit).map((row) => ({
2815
+ id: String(row.id),
2816
+ runId: row.run_id == null ? null : String(row.run_id),
2817
+ decisionType: String(row.decision_type),
2818
+ summary: String(row.summary),
2819
+ rationale: String(row.rationale),
2820
+ details: parseJsonRecord(row.details_json),
2821
+ createdAt: String(row.created_at)
2822
+ }));
2823
+ },
2824
+ listRecentActions(options2 = {}) {
2825
+ const limit = options2.limit ?? 10;
2826
+ return listRecentActionsStatement.all(limit).map((row) => ({
2827
+ id: String(row.id),
2828
+ runId: row.run_id == null ? null : String(row.run_id),
2829
+ actionType: String(row.action_type),
2830
+ status: String(row.status),
2831
+ target: row.target == null ? null : String(row.target),
2832
+ input: parseJsonRecord(row.input_json),
2833
+ result: parseJsonRecord(row.result_json),
2834
+ startedAt: String(row.started_at),
2835
+ completedAt: row.completed_at == null ? null : String(row.completed_at)
2836
+ }));
2837
+ },
2838
+ listDecisions(runId) {
2839
+ return sqlite.prepare(`
2840
+ SELECT id, run_id, decision_type, summary, rationale, details_json, created_at
2841
+ FROM inspector_decisions
2842
+ WHERE run_id = ?
2843
+ ORDER BY created_at DESC
2844
+ `).all(runId).map((row) => ({
2845
+ id: String(row.id),
2846
+ runId: row.run_id == null ? null : String(row.run_id),
2847
+ decisionType: String(row.decision_type),
2848
+ summary: String(row.summary),
2849
+ rationale: String(row.rationale),
2850
+ details: parseJsonRecord(row.details_json),
2851
+ createdAt: String(row.created_at)
2852
+ }));
2853
+ },
2854
+ listActions(runId) {
2855
+ return sqlite.prepare(`
2856
+ SELECT id, run_id, action_type, status, target, input_json, result_json, started_at, completed_at
2857
+ FROM inspector_actions
2858
+ WHERE run_id = ?
2859
+ ORDER BY started_at DESC
2860
+ `).all(runId).map((row) => ({
2861
+ id: String(row.id),
2862
+ runId: row.run_id == null ? null : String(row.run_id),
2863
+ actionType: String(row.action_type),
2864
+ status: String(row.status),
2865
+ target: row.target == null ? null : String(row.target),
2866
+ input: parseJsonRecord(row.input_json),
2867
+ result: parseJsonRecord(row.result_json),
2868
+ startedAt: String(row.started_at),
2869
+ completedAt: row.completed_at == null ? null : String(row.completed_at)
2870
+ }));
2871
+ },
2872
+ listReviews(runId) {
2873
+ return sqlite.prepare(`
2874
+ SELECT id, run_id, reviewer_type, status, findings_json, created_at
2875
+ FROM inspector_reviews
2876
+ WHERE run_id = ?
2877
+ ORDER BY created_at DESC
2878
+ `).all(runId).map((row) => ({
2879
+ id: String(row.id),
2880
+ runId: row.run_id == null ? null : String(row.run_id),
2881
+ reviewerType: String(row.reviewer_type),
2882
+ status: String(row.status),
2883
+ findings: parseJsonRecord(row.findings_json),
2884
+ createdAt: String(row.created_at)
2885
+ }));
2886
+ },
2887
+ listFollowups() {
2888
+ return sqlite.prepare(`
2889
+ SELECT id, origin_run_id, dedupe_key, task_id, kind, status, summary, details_json, created_at
2890
+ FROM inspector_followups
2891
+ ORDER BY created_at DESC
2892
+ `).all().map(mapFollowupRow);
2893
+ },
2894
+ listUpstreamSyncScans() {
2895
+ return sqlite.prepare(`
2896
+ SELECT id, started_at, completed_at, status, summary, details_json
2897
+ FROM upstream_sync_scans
2898
+ ORDER BY started_at DESC
2899
+ `).all().map((row) => ({
2900
+ id: String(row.id),
2901
+ startedAt: String(row.started_at),
2902
+ completedAt: row.completed_at == null ? null : String(row.completed_at),
2903
+ status: String(row.status),
2904
+ summary: String(row.summary),
2905
+ details: parseJsonRecord(row.details_json)
2906
+ }));
2907
+ },
2908
+ listAnalysisReports() {
2909
+ return sqlite.prepare(`
2910
+ SELECT id, report_type, status, summary, details_json, created_at
2911
+ FROM analysis_reports
2912
+ ORDER BY created_at DESC
2913
+ `).all().map((row) => ({
2914
+ id: String(row.id),
2915
+ reportType: String(row.report_type),
2916
+ status: String(row.status),
2917
+ summary: String(row.summary),
2918
+ details: parseJsonRecord(row.details_json),
2919
+ createdAt: String(row.created_at)
2920
+ }));
2921
+ },
2922
+ getCounts() {
2923
+ const row = sqlite.prepare(`
2924
+ SELECT
2925
+ (SELECT COUNT(*) FROM inspector_observations) AS observations,
2926
+ (SELECT COUNT(*) FROM inspector_decisions) AS decisions,
2927
+ (SELECT COUNT(*) FROM inspector_actions) AS actions,
2928
+ (SELECT COUNT(*) FROM inspector_followups) AS followups
2929
+ `).get();
2930
+ return {
2931
+ observations: Number(row?.observations ?? 0),
2932
+ decisions: Number(row?.decisions ?? 0),
2933
+ actions: Number(row?.actions ?? 0),
2934
+ followups: Number(row?.followups ?? 0)
2935
+ };
2936
+ },
2937
+ beginExclusiveTransaction() {
2938
+ sqlite.exec("BEGIN EXCLUSIVE");
2939
+ },
2940
+ rollbackTransaction() {
2941
+ sqlite.exec("ROLLBACK");
2942
+ },
2943
+ close() {
2944
+ sqlite.close();
2945
+ }
2946
+ };
2947
+ }
2948
+
2949
+ // packages/server/src/inspector/discovery.ts
2950
+ import {
2951
+ runStatus
2952
+ } from "@rig/runtime/control-plane/native/run-ops";
2953
+ import {
2954
+ listAuthorityRuns,
2955
+ readAuthorityRun
2956
+ } from "@rig/runtime/control-plane/authority-files";
2957
+ function providerFromRuntimeAdapter(runtimeAdapter) {
2958
+ if (!runtimeAdapter) {
2959
+ return null;
2960
+ }
2961
+ return runtimeAdapter;
2962
+ }
2963
+ function uniqueStrings2(values) {
2964
+ return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))];
2965
+ }
2966
+ function preferString(...values) {
2967
+ return values.find((value) => typeof value === "string" && value.length > 0) ?? null;
2968
+ }
2969
+ function chooseStatus(surfaces) {
2970
+ const authority = surfaces.find((surface) => surface.source === "authority-record");
2971
+ if (authority?.status) {
2972
+ return authority.status;
2973
+ }
2974
+ return preferString(...surfaces.map((surface) => surface.status)) ?? "unknown";
2975
+ }
2976
+ function chooseUpdatedAt(surfaces) {
2977
+ const timestamps = uniqueStrings2(surfaces.map((surface) => surface.updatedAt)).sort();
2978
+ return timestamps.at(-1) ?? "";
2979
+ }
2980
+ function buildConflict(surfaces) {
2981
+ const statuses = uniqueStrings2(surfaces.map((surface) => surface.status));
2982
+ const titles = uniqueStrings2(surfaces.map((surface) => surface.title));
2983
+ const activeSurface = surfaces.find((surface) => new Set(["preparing", "running", "validating", "reviewing"]).has(surface.status));
2984
+ const authoritySurface = surfaces.find((surface) => surface.source === "authority-record");
2985
+ if (activeSurface && authoritySurface && new Set(["failed", "stopped", "completed", "done"]).has(authoritySurface.status)) {
2986
+ return {
2987
+ state: "stale-active",
2988
+ summary: `Run ${authoritySurface.runId} is still reported active by ${activeSurface.source} but authority says ${authoritySurface.status}.`
2989
+ };
2990
+ }
2991
+ if (statuses.length > 1 || titles.length > 1) {
2992
+ const sourceSummary = surfaces.map((surface) => `${surface.source}:${surface.status}:${surface.title}`).join(" | ");
2993
+ return {
2994
+ state: "source-disagreement",
2995
+ summary: `Conflicting run surfaces for ${surfaces[0]?.runId ?? "unknown"}: ${sourceSummary}`
2996
+ };
2997
+ }
2998
+ return { state: "none", summary: null };
2999
+ }
3000
+ function mergeRunSurfaces(surfaces) {
3001
+ const authority = surfaces.find((surface) => surface.source === "authority-record") ?? null;
3002
+ const conflict = buildConflict(surfaces);
3003
+ return {
3004
+ runId: surfaces[0].runId,
3005
+ workspaceId: preferString(authority?.workspaceId, ...surfaces.map((surface) => surface.workspaceId)),
3006
+ taskId: preferString(authority?.taskId, ...surfaces.map((surface) => surface.taskId)),
3007
+ provider: preferString(authority?.provider, ...surfaces.map((surface) => surface.provider)),
3008
+ runtimeAdapter: preferString(authority?.runtimeAdapter, ...surfaces.map((surface) => surface.runtimeAdapter)),
3009
+ threadId: preferString(authority?.threadId, ...surfaces.map((surface) => surface.threadId)),
3010
+ status: chooseStatus(surfaces),
3011
+ title: preferString(authority?.title, ...surfaces.map((surface) => surface.title)) ?? surfaces[0].runId,
3012
+ source: authority?.source ?? surfaces[0].source,
3013
+ rawSourceKinds: uniqueStrings2(surfaces.map((surface) => surface.source)),
3014
+ conflictState: conflict.state,
3015
+ conflictSummary: conflict.summary,
3016
+ worktreePath: preferString(authority?.worktreePath, ...surfaces.map((surface) => surface.worktreePath)),
3017
+ artifactRoot: preferString(authority?.artifactRoot, ...surfaces.map((surface) => surface.artifactRoot)),
3018
+ logRoot: preferString(authority?.logRoot, ...surfaces.map((surface) => surface.logRoot)),
3019
+ sessionPath: preferString(authority?.sessionPath, ...surfaces.map((surface) => surface.sessionPath)),
3020
+ sessionLogPath: preferString(authority?.sessionLogPath, ...surfaces.map((surface) => surface.sessionLogPath)),
3021
+ startedAt: preferString(authority?.startedAt, ...surfaces.map((surface) => surface.startedAt)),
3022
+ updatedAt: chooseUpdatedAt(surfaces),
3023
+ completedAt: preferString(authority?.completedAt, ...surfaces.map((surface) => surface.completedAt))
3024
+ };
3025
+ }
3026
+ function discoverInspectorRuns(options) {
3027
+ const summary = options.statusSummary ?? runStatus(options.projectRoot);
3028
+ const discovered = new Map;
3029
+ const pushSurface = (surface) => {
3030
+ const existing = discovered.get(surface.runId) ?? [];
3031
+ existing.push(surface);
3032
+ discovered.set(surface.runId, existing);
3033
+ };
3034
+ for (const authorityEntry of listAuthorityRuns(options.projectRoot)) {
3035
+ const run = readAuthorityRun(options.projectRoot, authorityEntry.runId);
3036
+ if (!run) {
3037
+ continue;
3038
+ }
3039
+ pushSurface({
3040
+ runId: run.runId,
3041
+ workspaceId: run.workspaceId ?? null,
3042
+ taskId: run.taskId ?? null,
3043
+ provider: providerFromRuntimeAdapter(run.runtimeAdapter ?? null),
3044
+ runtimeAdapter: run.runtimeAdapter ?? null,
3045
+ threadId: run.threadId ?? null,
3046
+ status: run.status ?? "unknown",
3047
+ title: run.title ?? run.taskId ?? run.runId,
3048
+ source: "authority-record",
3049
+ worktreePath: run.worktreePath ?? null,
3050
+ artifactRoot: run.artifactRoot ?? null,
3051
+ logRoot: run.logRoot ?? null,
3052
+ sessionPath: run.sessionPath ?? null,
3053
+ sessionLogPath: run.sessionLogPath ?? null,
3054
+ startedAt: run.startedAt ?? null,
3055
+ updatedAt: run.updatedAt,
3056
+ completedAt: run.completedAt ?? null
3057
+ });
3058
+ }
3059
+ for (const entry of [...summary.activeRuns, ...summary.recentRuns]) {
3060
+ pushSurface({
3061
+ runId: entry.runId,
3062
+ workspaceId: null,
3063
+ taskId: entry.taskId ?? null,
3064
+ provider: null,
3065
+ runtimeAdapter: null,
3066
+ threadId: null,
3067
+ status: entry.status,
3068
+ title: entry.title,
3069
+ source: "run-status",
3070
+ worktreePath: null,
3071
+ artifactRoot: null,
3072
+ logRoot: null,
3073
+ sessionPath: null,
3074
+ sessionLogPath: null,
3075
+ startedAt: null,
3076
+ updatedAt: "",
3077
+ completedAt: null
3078
+ });
3079
+ }
3080
+ return [...discovered.values()].map((surfaces) => mergeRunSurfaces(surfaces)).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
3081
+ }
3082
+
3083
+ // packages/server/src/inspector/reconcile.ts
3084
+ function decisionTypeForStatus(status) {
3085
+ switch (status) {
3086
+ case "failed":
3087
+ return "investigate-failure";
3088
+ case "stopped":
3089
+ return "inspect-stop";
3090
+ default:
3091
+ return null;
3092
+ }
3093
+ }
3094
+ function reconcileInspectorRuns(options) {
3095
+ const now = options.now ?? (() => new Date().toISOString());
3096
+ let observedCount = 0;
3097
+ let decisionCount = 0;
3098
+ let actionCount = 0;
3099
+ for (const run of options.runs) {
3100
+ options.journal.upsertRun({
3101
+ runId: run.runId,
3102
+ workspaceId: run.workspaceId,
3103
+ taskId: run.taskId,
3104
+ runtimeAdapter: run.runtimeAdapter,
3105
+ provider: run.provider,
3106
+ status: run.status,
3107
+ conflictState: run.conflictState,
3108
+ conflictSummary: run.conflictSummary,
3109
+ source: run.source,
3110
+ title: run.title,
3111
+ startedAt: run.startedAt,
3112
+ updatedAt: run.updatedAt,
3113
+ completedAt: run.completedAt
3114
+ });
3115
+ const observation = options.journal.appendObservation({
3116
+ id: `observation:${run.runId}:${options.reason}:${run.updatedAt}`,
3117
+ runId: run.runId,
3118
+ dedupeKey: `run:${run.runId}:status:${run.status}:updated:${run.updatedAt}`,
3119
+ kind: "run.observed",
3120
+ severity: run.status === "failed" ? "warn" : "info",
3121
+ source: run.source,
3122
+ summary: `Observed run ${run.title}`,
3123
+ details: {
3124
+ reason: options.reason,
3125
+ status: run.status,
3126
+ provider: run.provider,
3127
+ runtimeAdapter: run.runtimeAdapter,
3128
+ conflictState: run.conflictState,
3129
+ rawSourceKinds: run.rawSourceKinds
3130
+ },
3131
+ createdAt: now()
3132
+ });
3133
+ if (observation.ok && observation.changed) {
3134
+ observedCount += 1;
3135
+ }
3136
+ const action = options.journal.appendAction({
3137
+ id: `action:${run.runId}:${options.reason}:${run.updatedAt}`,
3138
+ runId: run.runId,
3139
+ dedupeKey: `action:${run.runId}:observe:${run.updatedAt}`,
3140
+ actionType: "observe",
3141
+ status: "completed",
3142
+ target: `run:${run.runId}`,
3143
+ input: { reason: options.reason },
3144
+ result: { status: run.status },
3145
+ startedAt: now(),
3146
+ completedAt: now()
3147
+ });
3148
+ if (action.ok && action.changed) {
3149
+ actionCount += 1;
3150
+ }
3151
+ const decisionType = decisionTypeForStatus(run.status);
3152
+ if (decisionType) {
3153
+ const decision = options.journal.appendDecision({
3154
+ id: `decision:${run.runId}:${decisionType}:${run.updatedAt}`,
3155
+ runId: run.runId,
3156
+ dedupeKey: `decision:${run.runId}:${decisionType}:${run.updatedAt}`,
3157
+ decisionType,
3158
+ summary: `Investigate ${run.title}`,
3159
+ rationale: `Run status ${run.status} requires inspector attention.`,
3160
+ details: {
3161
+ reason: options.reason,
3162
+ status: run.status
3163
+ },
3164
+ createdAt: now()
3165
+ });
3166
+ if (decision.ok && decision.changed) {
3167
+ decisionCount += 1;
3168
+ }
3169
+ }
3170
+ if (run.conflictState !== "none") {
3171
+ const conflictDecision = options.journal.appendDecision({
3172
+ id: `decision:${run.runId}:conflict:${run.updatedAt}`,
3173
+ runId: run.runId,
3174
+ dedupeKey: `decision:${run.runId}:conflict:${run.updatedAt}`,
3175
+ decisionType: "inspect-conflict",
3176
+ summary: `Resolve ${run.title} observation conflict`,
3177
+ rationale: run.conflictSummary ?? "Observed run surfaces disagree and require inspector review.",
3178
+ details: {
3179
+ reason: options.reason,
3180
+ conflictState: run.conflictState,
3181
+ conflictSummary: run.conflictSummary
3182
+ },
3183
+ createdAt: now()
3184
+ });
3185
+ if (conflictDecision.ok && conflictDecision.changed) {
3186
+ decisionCount += 1;
3187
+ }
3188
+ }
3189
+ }
3190
+ return {
3191
+ discoveredCount: options.runs.length,
3192
+ observedCount,
3193
+ decisionCount,
3194
+ actionCount
3195
+ };
3196
+ }
3197
+
3198
+ // packages/server/src/inspector/service.ts
3199
+ function buildCapabilitySupport(options) {
3200
+ return {
3201
+ discoverRuns: true,
3202
+ inspectRuns: true,
3203
+ readLogs: true,
3204
+ readArtifacts: true,
3205
+ callServerApis: true,
3206
+ createTasks: Boolean(options.followupTaskRunner),
3207
+ runReviewerAgents: Boolean(options.reviewRunner),
3208
+ provisionSkills: true,
3209
+ scanUpstream: Boolean(options.upstreamSyncRunner),
3210
+ writeJournal: true,
3211
+ spawnSpecialists: Boolean(options.reviewRunner || options.analysisRunner || options.upstreamSyncRunner)
3212
+ };
3213
+ }
3214
+ var ACTIVE_RUN_STATUSES = new Set(["preparing", "running", "validating", "reviewing"]);
3215
+ function rankRunRecency(run) {
3216
+ const updatedAt = Date.parse(run.updatedAt);
3217
+ if (Number.isFinite(updatedAt)) {
3218
+ return updatedAt;
3219
+ }
3220
+ const startedAt = run.startedAt ? Date.parse(run.startedAt) : Number.NaN;
3221
+ if (Number.isFinite(startedAt)) {
3222
+ return startedAt;
3223
+ }
3224
+ return Number.NEGATIVE_INFINITY;
3225
+ }
3226
+ function visibleActiveRunKey(run) {
3227
+ if (!run.taskId) {
3228
+ return run.runId;
3229
+ }
3230
+ return [
3231
+ run.workspaceId ?? "workspace:unknown",
3232
+ run.taskId,
3233
+ run.worktreePath ?? "worktree:unknown"
3234
+ ].join("::");
3235
+ }
3236
+ function selectVisibleActiveRuns(runs) {
3237
+ const latestByKey = new Map;
3238
+ for (const run of runs) {
3239
+ if (!ACTIVE_RUN_STATUSES.has(run.status) || run.conflictState === "stale-active") {
3240
+ continue;
3241
+ }
3242
+ const key = visibleActiveRunKey(run);
3243
+ const current = latestByKey.get(key);
3244
+ if (!current || rankRunRecency(run) >= rankRunRecency(current)) {
3245
+ latestByKey.set(key, run);
3246
+ }
3247
+ }
3248
+ return [...latestByKey.values()].sort((left, right) => rankRunRecency(right) - rankRunRecency(left));
3249
+ }
3250
+ function createGlobalInspectorService(options) {
3251
+ const discoverRuns = options.discoverRuns ?? (() => discoverInspectorRuns({ projectRoot: options.projectRoot }));
3252
+ const now = options.now ?? (() => new Date().toISOString());
3253
+ const pollMs = Math.max(10, options.pollMs ?? 1000);
3254
+ const upstreamSyncMs = options.upstreamSyncMs == null ? 24 * 60 * 60 * 1000 : options.upstreamSyncMs > 0 ? Math.max(1000, options.upstreamSyncMs) : null;
3255
+ const upstreamSyncOnStart = options.upstreamSyncOnStart ?? false;
3256
+ const analysisMs = Math.max(1000, options.analysisMs ?? 60 * 60 * 1000);
3257
+ const capabilitySupport = buildCapabilitySupport(options);
3258
+ const readSnapshot = () => {
3259
+ const activeRuns = selectVisibleActiveRuns(discoverRuns()).map((run) => {
3260
+ const session = normalizeProviderSession(run);
3261
+ return {
3262
+ run,
3263
+ session,
3264
+ capabilities: deriveInspectorCapabilitySet(session, capabilitySupport)
3265
+ };
3266
+ });
3267
+ return {
3268
+ activeRuns,
3269
+ recentFindings: options.journal.listRecentFindings({ limit: 20 }),
3270
+ followups: options.journal.listFollowups(),
3271
+ analysisReports: options.journal.listAnalysisReports(),
3272
+ availableTools: toolRegistry.list()
3273
+ };
3274
+ };
3275
+ const toolRegistry = createInspectorToolRegistry({
3276
+ journal: options.journal,
3277
+ projectRoot: options.projectRoot,
3278
+ discoverRuns,
3279
+ snapshotReader: readSnapshot,
3280
+ reviewRunner: options.reviewRunner,
3281
+ upstreamScanRunner: options.upstreamSyncRunner,
3282
+ analysisRunner: options.analysisRunner,
3283
+ followupTaskRunner: options.followupTaskRunner,
3284
+ capabilitySupport,
3285
+ now
3286
+ });
3287
+ let pollTimer = null;
3288
+ let upstreamTimer = null;
3289
+ let analysisTimer = null;
3290
+ const reconcileOnce = (reason) => {
3291
+ return reconcileInspectorRuns({
3292
+ journal: options.journal,
3293
+ runs: discoverRuns(),
3294
+ reason,
3295
+ now
3296
+ });
3297
+ };
3298
+ return {
3299
+ projectRoot: options.projectRoot,
3300
+ start() {
3301
+ if (pollTimer || upstreamTimer || analysisTimer) {
3302
+ return false;
3303
+ }
3304
+ pollTimer = setInterval(() => {
3305
+ try {
3306
+ reconcileOnce("poll");
3307
+ } catch {}
3308
+ }, pollMs);
3309
+ if (options.upstreamSyncRunner) {
3310
+ if (upstreamSyncOnStart) {
3311
+ Promise.resolve(options.upstreamSyncRunner({ trigger: "startup" })).catch(() => {});
3312
+ }
3313
+ if (upstreamSyncMs !== null) {
3314
+ upstreamTimer = setInterval(() => {
3315
+ Promise.resolve(options.upstreamSyncRunner?.({ trigger: "scheduled" })).catch(() => {});
3316
+ }, upstreamSyncMs);
3317
+ }
3318
+ }
3319
+ if (options.analysisRunner) {
3320
+ analysisTimer = setInterval(() => {
3321
+ Promise.resolve(options.analysisRunner?.({ trigger: "scheduled", reportType: "run-health" })).catch(() => {});
3322
+ }, analysisMs);
3323
+ }
3324
+ return true;
3325
+ },
3326
+ stop() {
3327
+ if (pollTimer)
3328
+ clearInterval(pollTimer);
3329
+ if (upstreamTimer)
3330
+ clearInterval(upstreamTimer);
3331
+ if (analysisTimer)
3332
+ clearInterval(analysisTimer);
3333
+ pollTimer = null;
3334
+ upstreamTimer = null;
3335
+ analysisTimer = null;
3336
+ },
3337
+ isRunning() {
3338
+ return pollTimer !== null || upstreamTimer !== null || analysisTimer !== null;
3339
+ },
3340
+ snapshot() {
3341
+ return readSnapshot();
3342
+ },
3343
+ async invokeTool(name, input) {
3344
+ return toolRegistry.invoke(name, input);
3345
+ },
3346
+ reconcileOnce,
3347
+ async runUpstreamSyncOnce(input = {}) {
3348
+ await options.upstreamSyncRunner?.(input);
3349
+ },
3350
+ async runAnalysisOnce(input = {}) {
3351
+ await options.analysisRunner?.(input);
3352
+ }
3353
+ };
3354
+ }
3355
+
3356
+ // packages/server/src/inspector/upstream-sync.ts
3357
+ import { spawnSync } from "child_process";
3358
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
3359
+ import { dirname as dirname4, resolve as resolve4 } from "path";
3360
+ import { resolveMonorepoRoot as resolveMonorepoRoot2 } from "@rig/runtime/control-plane/native/utils";
3361
+ var UPSTREAM_VALIDATION_DESCRIPTIONS = {
3362
+ "integration:hg-auth-backport": "Preserves the upstream auth hardening cluster: nonce-backed node-client login, signature-aware JWT verification, shared-token onboarding semantics, and regression coverage.",
3363
+ "integration:hg-core-security-backport": "Preserves the vendored core-app and backend security fixes for OTP validation, NoSQL/operator hardening, AES/encryption behavior, and rate-limit or redirect-state protections.",
3364
+ "integration:hg-boundary-hardening": "Preserves boundary-safe CORS behavior and credential lookup error hygiene across the vendored backend and the extracted credentials-service analogue.",
3365
+ "integration:hg-uudl-runtime": "Preserves the upstream uudl runtime/package fixes: production dependency placement, path-resolution/runtime bootstrap behavior, AWS/MSK environment detection, KMS coverage, and a compiling UUDL tree.",
3366
+ "boundary:hg-auth-triage": "Requires a durable keep/adapt/reject triage record for the post-import auth policy and mobile-wallet SIWE commits so the watcher does not keep rediscovering the same ambiguity.",
3367
+ "boundary:hg-upstream-triage": "Requires a durable keep/adapt/reject triage record for service-relevant upstream commits that do not match the current portable catalog, including commit hashes, touched paths, and follow-up routing decisions."
3368
+ };
3369
+ var CLUSTERS = {
3370
+ "auth-hardening": {
3371
+ classification: "portable-now",
3372
+ title: "[HG-001] Preserve upstream auth and shared-token hardening across extracted auth work",
3373
+ description: "Backport clearly portable auth changes introduced upstream after imported revision 58b56e15: 3196d5c (node-client login nonce), bb5b74f (JWT signature validation), and 92011ead (shared token verification for wallet signup onboarding). Apply them where they belong in this repo: hp-auth-service, hp-gateway if verification semantics surface there, and the upstream monorepo auth owner flows for the remaining wallet-login/shared-token work. This task must preserve the security behavior, not just copy code. It should explicitly reconcile with bd-k0y-10, bd-k0y-11, bd-k0y-12, and bd-k0y-13.",
3374
+ acceptanceCriteria: "Map upstream commits 3196d5c, bb5b74f, and 92011ead to exact local targets; preserve nonce-backed node-client login, JWT signature validation, and shared-token verification behavior; add automated tests that fail without the hardening; document any deliberate non-ports.",
3375
+ role: "extractor",
3376
+ scope: [
3377
+ "repos/spliter-monorepo/microservices/hp-auth-service/**",
3378
+ "repos/spliter-monorepo/microservices/hp-gateway/**",
3379
+ "repos/spliter-monorepo/humoongate/moongate/core-app/**"
3380
+ ],
3381
+ validation: ["integration:hg-auth-backport", "boundary:changed-files"],
3382
+ validationDescriptions: {
3383
+ "integration:hg-auth-backport": UPSTREAM_VALIDATION_DESCRIPTIONS["integration:hg-auth-backport"]
3384
+ },
3385
+ labels: ["upstream:monorepo", "kind:backport", "cluster:auth"]
3386
+ },
3387
+ "core-security": {
3388
+ classification: "portable-now",
3389
+ title: "[HG-002] Backport core-app and backend security fixes from post-import upstream",
3390
+ description: "Backport the clearly portable security fixes from efdae709 (OTP validation bypass), f63830d (NoSQL operator injection hardening), d7f6919 (AES encryption), and af86ac16 (three critical/high vulnerabilities). Preserve the behavior in the vendored auth owner and the touched backend query surfaces instead of treating these as optional cleanups. The high-value touched files from af86ac16 must be reviewed and ported or consciously waived with rationale.",
3391
+ acceptanceCriteria: "Review and port the exact security behaviors from efdae709, f63830d, d7f6919, and af86ac16; cover the listed core-app and backend touched files; add regression tests for OTP bypass, NoSQL operator injection, AES/encryption behavior, and rate-limit or redirect-state hardening; document any consciously waived upstream changes.",
3392
+ role: "mechanic",
3393
+ scope: [
3394
+ "repos/spliter-monorepo/humoongate/moongate/core-app/**",
3395
+ "repos/spliter-monorepo/humoongate/humanity/hp-backend-ts/**"
3396
+ ],
3397
+ validation: ["integration:hg-core-security-backport", "boundary:changed-files"],
3398
+ validationDescriptions: {
3399
+ "integration:hg-core-security-backport": UPSTREAM_VALIDATION_DESCRIPTIONS["integration:hg-core-security-backport"]
3400
+ },
3401
+ labels: ["upstream:monorepo", "kind:backport", "cluster:security"]
3402
+ },
3403
+ "boundary-hardening": {
3404
+ classification: "portable-now",
3405
+ title: "[HG-003] Backport request-boundary hardening for CORS and credential lookup flows",
3406
+ description: "Backport the clearly portable boundary hardening from 083bbe4 (CORS config) and 36d52fba (credential lookup error hygiene). Preserve request-boundary behavior in the vendored backend and any analogous extracted credential-facing service seams so attackers cannot learn internals from credential lookup failures or exploit inconsistent CORS policy.",
3407
+ acceptanceCriteria: "Port the exact request-boundary behavior from 083bbe4 and 36d52fba; preserve safe CORS policy and credential lookup error hygiene; add tests showing callers do not receive overly detailed credential lookup failures; document any extracted-service analogue that must mirror the behavior.",
3408
+ role: "mechanic",
3409
+ scope: [
3410
+ "repos/spliter-monorepo/humoongate/humanity/hp-backend-ts/**",
3411
+ "repos/spliter-monorepo/microservices/hp-credentials-service/**"
3412
+ ],
3413
+ validation: ["integration:hg-boundary-hardening", "boundary:changed-files"],
3414
+ validationDescriptions: {
3415
+ "integration:hg-boundary-hardening": UPSTREAM_VALIDATION_DESCRIPTIONS["integration:hg-boundary-hardening"]
3416
+ },
3417
+ labels: ["upstream:monorepo", "kind:backport", "cluster:boundary"]
3418
+ },
3419
+ "uudl-runtime": {
3420
+ classification: "portable-now",
3421
+ title: "[HG-004] Backport uudl runtime and packaging fixes from upstream",
3422
+ description: "Backport the clearly portable uudl runtime fixes from 2141b4c, 1fa56d7, a9a0a99, 9fcfa6e, and ea5cb03. Preserve the runtime behavior rather than cherry-picking filenames: production dependency placement, tsconfig-paths resolution, aggregator cycle removal, AWS/MSK environment detection, and local KMS wiring all need to be reflected in whichever UUDL runtime this repo still owns.",
3423
+ acceptanceCriteria: "Port the runtime behaviors from 2141b4c, 1fa56d7, a9a0a99, 9fcfa6e, and ea5cb03; preserve production dependency placement, tsconfig-paths runtime resolution, aggregator cycle removal, AWS/MSK environment detection, and local KMS configuration; add runtime-focused tests or smoke coverage for each preserved behavior.",
3424
+ role: "extractor",
3425
+ scope: ["repos/spliter-monorepo/humoongate/humanity/hp-uudl/**"],
3426
+ validation: ["integration:hg-uudl-runtime", "boundary:changed-files"],
3427
+ validationDescriptions: {
3428
+ "integration:hg-uudl-runtime": UPSTREAM_VALIDATION_DESCRIPTIONS["integration:hg-uudl-runtime"]
3429
+ },
3430
+ labels: ["upstream:monorepo", "kind:backport", "cluster:uudl"]
3431
+ },
3432
+ "auth-triage": {
3433
+ classification: "needs-human-triage",
3434
+ title: "[HG-005] Triage post-import auth policy deltas and mobile-wallet SIWE changes",
3435
+ description: "Review the upstream changes that are relevant but not safely auto-portable: 1262b75 (JWT_EXPIRES_IN 1d -> 7d), 15036424 (unlink-wallet endpoint and Wallet.default fix), and d4a47015 (unlink-wallet error handling). Produce an explicit keep/adapt/reject decision for each commit with rationale tied to the extracted auth plan, core-app ownership, and current threat model, and record the result in artifacts/bd-2ztk.5/decision-log.md so future agents do not silently re-litigate these decisions.",
3436
+ acceptanceCriteria: "For 1262b75, 15036424, and d4a47015, produce explicit keep/adapt/reject decisions with rationale tied to auth ownership and threat model; record the decision in artifacts/bd-2ztk.5/decision-log.md; identify any follow-on implementation tasks if the answer is keep or adapt.",
3437
+ role: "architect",
3438
+ scope: ["artifacts/bd-2ztk.5/**", "docs/research/**"],
3439
+ validation: ["boundary:hg-auth-triage"],
3440
+ validationDescriptions: {
3441
+ "boundary:hg-auth-triage": UPSTREAM_VALIDATION_DESCRIPTIONS["boundary:hg-auth-triage"]
3442
+ },
3443
+ labels: ["upstream:monorepo", "kind:backport", "cluster:triage"]
3444
+ },
3445
+ "generic-triage": {
3446
+ classification: "needs-human-triage",
3447
+ title: "[HG-007] Triage uncatalogued service-relevant upstream commits",
3448
+ description: "Review future upstream commits that touch service-relevant paths but do not match the current portable-now catalog. For each commit or batch, produce keep/adapt/reject decisions with rationale, cite the touched paths and why they matter here, and either route them into an existing backport task or open a new follow-up task when they are worth preserving.",
3449
+ acceptanceCriteria: "For each uncatalogued service-relevant upstream commit or batch, record keep/adapt/reject decisions with rationale in artifacts/bd-2ztk.7/decision-log.md; cite the commit hashes and touched paths; and either attach the work to an existing backport task or open a new follow-up task when preservation is warranted.",
3450
+ role: "architect",
3451
+ scope: ["artifacts/bd-2ztk.7/**", "docs/research/**"],
3452
+ validation: ["boundary:hg-upstream-triage"],
3453
+ validationDescriptions: {
3454
+ "boundary:hg-upstream-triage": UPSTREAM_VALIDATION_DESCRIPTIONS["boundary:hg-upstream-triage"]
3455
+ },
3456
+ labels: ["upstream:monorepo", "kind:backport", "cluster:triage"]
3457
+ }
3458
+ };
3459
+ var RELEVANT_PREFIXES = [
3460
+ "humanity/hp-backend-ts/",
3461
+ "humanity/hp-dev-api/",
3462
+ "humanity/hp-uudl/",
3463
+ "moongate/core-app/",
3464
+ "moongate/models/",
3465
+ "packages/",
3466
+ "shared-migrations/"
3467
+ ];
3468
+ var IGNORE_PATTERNS = [
3469
+ /hp-next|dashboard|frontend/i,
3470
+ /CODEOWNERS|CodeRabbit|security-workflow|workflow housekeeping/i,
3471
+ /npm registry|workspace packages to npm/i,
3472
+ /bulk event|organizer/i
3473
+ ];
3474
+ function parseImportedUpstreamRevision(doc, upstreamName = "upstream") {
3475
+ const sectionPattern = new RegExp(`##\\s+${upstreamName}\\b([\\s\\S]*?)(?:\\n##\\s+|$)`, "i");
3476
+ const sectionMatch = doc.match(sectionPattern);
3477
+ if (!sectionMatch) {
3478
+ return null;
3479
+ }
3480
+ const revisionMatch = sectionMatch[1]?.match(/Imported revision:\s*`([0-9a-f]{40})`/i);
3481
+ return revisionMatch?.[1] ?? null;
3482
+ }
3483
+ function normalizeGitResult(result) {
3484
+ return {
3485
+ exitCode: result.status ?? 1,
3486
+ stdout: typeof result.stdout === "string" ? result.stdout : result.stdout?.toString("utf8") ?? "",
3487
+ stderr: typeof result.stderr === "string" ? result.stderr : result.stderr?.toString("utf8") ?? ""
3488
+ };
3489
+ }
3490
+ function defaultGitRunner(repoRoot, args) {
3491
+ return normalizeGitResult(spawnSync("git", ["-C", repoRoot, ...args], {
3492
+ encoding: "utf8"
3493
+ }));
3494
+ }
3495
+ function upstreamStatePath(projectRoot, override) {
3496
+ if (override) {
3497
+ return resolve4(override);
3498
+ }
3499
+ return resolve4(resolveRigServerPaths(projectRoot).stateDir, "inspector", "upstream-sync.json");
3500
+ }
3501
+ function readUpstreamState(projectRoot, statePath) {
3502
+ const path = upstreamStatePath(projectRoot, statePath);
3503
+ if (!existsSync3(path)) {
3504
+ return null;
3505
+ }
3506
+ try {
3507
+ return JSON.parse(readFileSync2(path, "utf-8"));
3508
+ } catch {
3509
+ return null;
3510
+ }
3511
+ }
3512
+ function writeUpstreamState(projectRoot, state, statePath) {
3513
+ const path = upstreamStatePath(projectRoot, statePath);
3514
+ mkdirSync4(dirname4(path), { recursive: true });
3515
+ writeFileSync3(path, `${JSON.stringify(state, null, 2)}
3516
+ `, "utf8");
3517
+ }
3518
+ function readImportedRevision(projectRoot, upstreamsDocPath) {
3519
+ const monorepoRoot = resolveMonorepoRoot2(projectRoot);
3520
+ const docPath = upstreamsDocPath ? resolve4(upstreamsDocPath) : resolve4(monorepoRoot, "docs", "UPSTREAMS.md");
3521
+ if (!existsSync3(docPath)) {
3522
+ throw new Error(`UPSTREAMS.md not found at ${docPath}`);
3523
+ }
3524
+ const docContent = readFileSync2(docPath, "utf-8");
3525
+ const revision = parseImportedUpstreamRevision(docContent, "upstream") ?? parseImportedUpstreamRevision(docContent, "humoongate");
3526
+ if (!revision) {
3527
+ throw new Error(`Failed to parse upstream imported revision from ${docPath}`);
3528
+ }
3529
+ return revision;
3530
+ }
3531
+ function resolveRemoteBranch(repoRoot, remote, gitRunner) {
3532
+ const symbolic = gitRunner(repoRoot, ["symbolic-ref", `refs/remotes/${remote}/HEAD`]);
3533
+ const symbolicRef = symbolic.stdout.trim();
3534
+ if (symbolic.exitCode === 0 && symbolicRef.length > 0) {
3535
+ return symbolicRef.replace(/^refs\/remotes\//, "");
3536
+ }
3537
+ for (const fallback of [`${remote}/main`, `${remote}/master`]) {
3538
+ const showRef = gitRunner(repoRoot, ["show-ref", "--verify", `refs/remotes/${fallback}`]);
3539
+ if (showRef.exitCode === 0) {
3540
+ return fallback;
3541
+ }
3542
+ }
3543
+ return null;
3544
+ }
3545
+ function isGitCheckout(path, gitRunner) {
3546
+ if (!existsSync3(resolve4(path, ".git"))) {
3547
+ return false;
3548
+ }
3549
+ const result = gitRunner(path, ["rev-parse", "--is-inside-work-tree"]);
3550
+ return result.exitCode === 0 && result.stdout.trim() === "true";
3551
+ }
3552
+ function resolveUpstreamCheckout(projectRoot, explicitCheckout, gitRunner) {
3553
+ const monorepoRoot = resolveMonorepoRoot2(projectRoot);
3554
+ const candidates = [
3555
+ explicitCheckout ? resolve4(explicitCheckout) : "",
3556
+ process.env.UPSTREAM_CHECKOUT?.trim() ? resolve4(process.env.UPSTREAM_CHECKOUT.trim()) : "",
3557
+ process.env.HUMOONGATE_UPSTREAM_CHECKOUT?.trim() ? resolve4(process.env.HUMOONGATE_UPSTREAM_CHECKOUT.trim()) : "",
3558
+ resolve4(projectRoot, "..", "humoongate"),
3559
+ resolve4(monorepoRoot, "..", "humoongate"),
3560
+ resolve4(monorepoRoot, "humoongate")
3561
+ ].filter(Boolean);
3562
+ for (const candidate of candidates) {
3563
+ if (isGitCheckout(candidate, gitRunner)) {
3564
+ return candidate;
3565
+ }
3566
+ }
3567
+ throw new Error(`Unable to locate an authoritative upstream checkout. Checked: ${candidates.join(", ")}`);
3568
+ }
3569
+ function parseCommitLog(output) {
3570
+ const commits = [];
3571
+ let current = null;
3572
+ for (const rawLine of output.split(/\r?\n/)) {
3573
+ const line = rawLine.trim();
3574
+ if (line === "__COMMIT__") {
3575
+ if (current) {
3576
+ commits.push(current);
3577
+ }
3578
+ current = null;
3579
+ continue;
3580
+ }
3581
+ if (!line) {
3582
+ continue;
3583
+ }
3584
+ if (!current) {
3585
+ const [sha, date, ...rest] = line.split("\t");
3586
+ if (!sha || !date) {
3587
+ continue;
3588
+ }
3589
+ current = {
3590
+ sha,
3591
+ date,
3592
+ summary: rest.join("\t").trim(),
3593
+ paths: []
3594
+ };
3595
+ continue;
3596
+ }
3597
+ current.paths.push(line);
3598
+ }
3599
+ if (current) {
3600
+ commits.push(current);
3601
+ }
3602
+ return commits;
3603
+ }
3604
+ function relevantPaths(paths) {
3605
+ return paths.filter((path) => RELEVANT_PREFIXES.some((prefix) => path.startsWith(prefix)));
3606
+ }
3607
+ function classifyCommit(commit) {
3608
+ const summary = commit.summary.toLowerCase();
3609
+ const paths = relevantPaths(commit.paths);
3610
+ const pathText = paths.join(" ").toLowerCase();
3611
+ const signature = `${summary}
3612
+ ${pathText}`;
3613
+ if (IGNORE_PATTERNS.some((pattern) => pattern.test(signature))) {
3614
+ return { classification: "ignore", clusterKey: null };
3615
+ }
3616
+ if (/jwt_expires_in|unlink-wallet|mobile wallet|siwe endpoint/i.test(signature)) {
3617
+ return { classification: "needs-human-triage", clusterKey: "auth-triage" };
3618
+ }
3619
+ if (/node-client login use nonce|jwt signature validation|shared token verification|wallet signup onboarding/i.test(signature)) {
3620
+ return { classification: "portable-now", clusterKey: "auth-hardening" };
3621
+ }
3622
+ if (/otp validation bypass|otp queries|aes encryption|critical\/high vulnerabilities|redirect jwt steal/i.test(signature)) {
3623
+ return { classification: "portable-now", clusterKey: "core-security" };
3624
+ }
3625
+ if (/cors config|credential lookup errors/i.test(signature)) {
3626
+ return { classification: "portable-now", clusterKey: "boundary-hardening" };
3627
+ }
3628
+ if (paths.some((path) => path.startsWith("humanity/hp-uudl/")) && /tsconfig-paths|circular dependency|msk|kms port|runtime|production dependenc/i.test(signature)) {
3629
+ return { classification: "portable-now", clusterKey: "uudl-runtime" };
3630
+ }
3631
+ if (paths.length > 0) {
3632
+ return { classification: "needs-human-triage", clusterKey: "generic-triage" };
3633
+ }
3634
+ return { classification: "ignore", clusterKey: null };
3635
+ }
3636
+ function scanUpstream(options) {
3637
+ const now = options.now ?? (() => new Date().toISOString());
3638
+ const gitRunner = options.gitRunner ?? defaultGitRunner;
3639
+ const remote = options.remote ?? "origin";
3640
+ const baselineSha = options.baselineSha ?? readImportedRevision(options.projectRoot, options.upstreamsDocPath);
3641
+ const checkoutPath = resolveUpstreamCheckout(options.projectRoot, options.explicitCheckout, gitRunner);
3642
+ if (options.fetch !== false) {
3643
+ const fetchResult = gitRunner(checkoutPath, ["fetch", remote]);
3644
+ if (fetchResult.exitCode !== 0) {
3645
+ throw new Error(`Failed to fetch ${remote} in ${checkoutPath}: ${fetchResult.stderr || fetchResult.stdout}`);
3646
+ }
3647
+ }
3648
+ const branch = options.branch ?? resolveRemoteBranch(checkoutPath, remote, gitRunner);
3649
+ if (!branch) {
3650
+ throw new Error(`Failed to resolve tracked branch for remote ${remote} in ${checkoutPath}`);
3651
+ }
3652
+ const headResult = gitRunner(checkoutPath, ["rev-parse", branch]);
3653
+ if (headResult.exitCode !== 0 || !headResult.stdout.trim()) {
3654
+ throw new Error(`Failed to resolve latest upstream head for ${branch}: ${headResult.stderr || headResult.stdout}`);
3655
+ }
3656
+ const latestHeadSha = headResult.stdout.trim();
3657
+ const state = readUpstreamState(options.projectRoot, options.statePath);
3658
+ let sinceSha = state?.lastProcessedCommit || baselineSha;
3659
+ const ancestorCheck = gitRunner(checkoutPath, ["merge-base", "--is-ancestor", sinceSha, branch]);
3660
+ if (ancestorCheck.exitCode !== 0) {
3661
+ sinceSha = baselineSha;
3662
+ }
3663
+ const logResult = gitRunner(checkoutPath, [
3664
+ "log",
3665
+ "--reverse",
3666
+ "--date=short",
3667
+ "--name-only",
3668
+ "--pretty=format:__COMMIT__%n%H%x09%cs%x09%s",
3669
+ `${sinceSha}..${branch}`
3670
+ ]);
3671
+ if (logResult.exitCode !== 0) {
3672
+ throw new Error(`Failed to collect upstream commits: ${logResult.stderr || logResult.stdout}`);
3673
+ }
3674
+ const commits = parseCommitLog(logResult.stdout);
3675
+ const grouped = new Map;
3676
+ for (const commit of commits) {
3677
+ const classification = classifyCommit(commit);
3678
+ if (!classification.clusterKey) {
3679
+ continue;
3680
+ }
3681
+ const existing = grouped.get(classification.clusterKey) ?? [];
3682
+ existing.push(commit);
3683
+ grouped.set(classification.clusterKey, existing);
3684
+ }
3685
+ const findings = Array.from(grouped.entries()).map(([clusterKey, clusterCommits]) => {
3686
+ const definition = CLUSTERS[clusterKey];
3687
+ const newestCommit = clusterCommits[clusterCommits.length - 1];
3688
+ return {
3689
+ key: `upstream:${definition.classification}:${clusterKey}:${newestCommit.sha}`,
3690
+ summary: definition.title,
3691
+ details: {
3692
+ remote,
3693
+ branch,
3694
+ checkoutPath,
3695
+ baselineSha,
3696
+ sinceSha,
3697
+ latestHeadSha,
3698
+ classification: definition.classification,
3699
+ clusterKey,
3700
+ description: definition.description,
3701
+ acceptanceCriteria: definition.acceptanceCriteria,
3702
+ role: definition.role,
3703
+ scope: definition.scope,
3704
+ validation: definition.validation,
3705
+ validationDescriptions: definition.validationDescriptions,
3706
+ labels: definition.labels,
3707
+ commits: clusterCommits.map((commit) => ({
3708
+ sha: commit.sha,
3709
+ date: commit.date,
3710
+ summary: commit.summary,
3711
+ paths: relevantPaths(commit.paths)
3712
+ }))
3713
+ }
3714
+ };
3715
+ });
3716
+ const groupedCommitCount = Array.from(grouped.values()).reduce((sum, clusterCommits) => sum + clusterCommits.length, 0);
3717
+ return {
3718
+ baselineSha,
3719
+ sinceSha,
3720
+ latestHeadSha,
3721
+ checkoutPath,
3722
+ remote,
3723
+ branch,
3724
+ scannedCommitCount: commits.length,
3725
+ ignoredCommitCount: Math.max(0, commits.length - groupedCommitCount),
3726
+ findings,
3727
+ statePath: upstreamStatePath(options.projectRoot, options.statePath)
3728
+ };
3729
+ }
3730
+ async function runUpstreamSyncScan(options) {
3731
+ const now = options.now ?? (() => new Date().toISOString());
3732
+ const startedAt = now();
3733
+ let followupCount = 0;
3734
+ const createdTaskIds = [];
3735
+ for (const finding of options.findings) {
3736
+ const existingFollowup = options.journal.getFollowupByDedupeKey(finding.key);
3737
+ if (!existingFollowup) {
3738
+ const result = options.journal.appendFollowup({
3739
+ id: `upstream-followup:${finding.key}`,
3740
+ originRunId: null,
3741
+ dedupeKey: finding.key,
3742
+ taskId: null,
3743
+ kind: "upstream-drift",
3744
+ status: "proposed",
3745
+ summary: finding.summary,
3746
+ details: finding.details ?? null,
3747
+ createdAt: startedAt
3748
+ });
3749
+ if (!result.ok) {
3750
+ throw new Error(`Failed to record upstream follow-up ${finding.key}: ${result.error}`);
3751
+ }
3752
+ if (result.changed) {
3753
+ followupCount += 1;
3754
+ }
3755
+ }
3756
+ const followup = options.journal.getFollowupByDedupeKey(finding.key);
3757
+ if (options.createTask && followup?.taskId == null) {
3758
+ const created = await options.createTask(finding);
3759
+ if (created.taskId) {
3760
+ createdTaskIds.push(created.taskId);
3761
+ const attachment = options.journal.attachFollowupTask({
3762
+ dedupeKey: finding.key,
3763
+ taskId: created.taskId,
3764
+ status: "created",
3765
+ details: finding.details && created.details ? { ...finding.details, followupTask: created.details } : created.details ?? finding.details ?? null
3766
+ });
3767
+ if (!attachment.ok) {
3768
+ throw new Error(`Failed to attach task ${created.taskId} to upstream follow-up ${finding.key}: ${attachment.error}`);
3769
+ }
3770
+ }
3771
+ }
3772
+ }
3773
+ const summary = options.findings.length === 0 ? "No upstream drift findings" : `Detected ${options.findings.length} upstream drift finding(s)`;
3774
+ options.journal.appendUpstreamSyncScan({
3775
+ id: options.scanId,
3776
+ dedupeKey: options.scanId,
3777
+ startedAt,
3778
+ completedAt: startedAt,
3779
+ status: "completed",
3780
+ summary,
3781
+ details: {
3782
+ findingKeys: options.findings.map((finding) => finding.key),
3783
+ followupCount,
3784
+ createdTaskIds
3785
+ }
3786
+ });
3787
+ return {
3788
+ status: "completed",
3789
+ summary,
3790
+ followupCount,
3791
+ createdTaskIds
3792
+ };
3793
+ }
3794
+
3795
+ // packages/server/src/server-helpers/normalizers.ts
3796
+ function normalizeString2(value) {
3797
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
3798
+ }
3799
+ function normalizeStringArray(value) {
3800
+ return Array.isArray(value) ? value.map((entry) => normalizeString2(entry)).filter((entry) => entry !== null) : [];
3801
+ }
3802
+
3803
+ // packages/server/src/server-helpers/task-config.ts
3804
+ import { existsSync as existsSync4 } from "fs";
3805
+ async function readTaskConfig(projectRoot) {
3806
+ const taskConfigPath = resolveRigServerPaths(projectRoot).taskConfigPath;
3807
+ if (!existsSync4(taskConfigPath)) {
3808
+ return {};
3809
+ }
3810
+ try {
3811
+ const parsed = await Bun.file(taskConfigPath).json();
3812
+ return parsed ?? {};
3813
+ } catch {
3814
+ return {};
3815
+ }
3816
+ }
3817
+
3818
+ // packages/server/src/server-helpers/inspector-jobs.ts
3819
+ function nextInspectorTaskId(existingIds, timestamp) {
3820
+ const datePart = timestamp.slice(0, 10).replace(/-/g, "");
3821
+ let sequence = 1;
3822
+ while (true) {
3823
+ const candidate = `bd-inspector-${datePart}-${String(sequence).padStart(2, "0")}`;
3824
+ if (!existingIds.has(candidate)) {
3825
+ return candidate;
3826
+ }
3827
+ sequence += 1;
3828
+ }
3829
+ }
3830
+ function normalizeValidationDescriptions(value) {
3831
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3832
+ return {};
3833
+ }
3834
+ const entries = Object.entries(value).filter((entry) => typeof entry[0] === "string" && typeof entry[1] === "string").map(([key, description]) => [key.trim(), description.trim()]).filter(([key, description]) => key.length > 0 && description.length > 0);
3835
+ return Object.fromEntries(entries);
3836
+ }
3837
+ function resolveFollowupSourceCommit(input) {
3838
+ const details = input.details && typeof input.details === "object" && !Array.isArray(input.details) ? input.details : null;
3839
+ return normalizeString2(input.sourceCommit) ?? normalizeString2(details?.latestHeadSha) ?? normalizeString2(details?.latestSha) ?? normalizeString2(details?.sourceCommit) ?? normalizeString2(details?.headSha) ?? undefined;
3840
+ }
3841
+ async function createInspectorFollowupTask(projectRoot, input) {
3842
+ const monorepoRoot = resolveMonorepoRoot3(projectRoot);
3843
+ const issuesPath = resolve5(monorepoRoot, ".beads", "issues.jsonl");
3844
+ const taskStatePath = resolve5(monorepoRoot, ".beads", "task-state.json");
3845
+ const taskConfigPath = resolve5(monorepoRoot, ".rig", "task-config.json");
3846
+ mkdirSync5(dirname5(issuesPath), { recursive: true });
3847
+ mkdirSync5(dirname5(taskConfigPath), { recursive: true });
3848
+ const summary = normalizeString2(input.summary) ?? "Inspector follow-up";
3849
+ const description = normalizeString2(input.description) ?? normalizeString2(input.details?.description) ?? `Created by the global inspector: ${summary}`;
3850
+ const acceptanceCriteria = normalizeString2(input.acceptanceCriteria) ?? "Investigate the detected drift and port the relevant changes into Rig.";
3851
+ const role = normalizeString2(input.role) ?? "mechanic";
3852
+ const scope = normalizeStringArray(input.scope).concat(normalizeStringArray(input.details?.scope)).filter(Boolean);
3853
+ const validation = normalizeStringArray(input.validation).concat(normalizeStringArray(input.details?.validation)).filter(Boolean);
3854
+ const validationDescriptions = {
3855
+ ...normalizeValidationDescriptions(input.validationDescriptions),
3856
+ ...normalizeValidationDescriptions(input.details?.validationDescriptions)
3857
+ };
3858
+ const labels = [
3859
+ "origin:inspector",
3860
+ "workflow:inspector-followup",
3861
+ ...normalizeStringArray(input.labels)
3862
+ ];
3863
+ const sourceKey = normalizeString2(input.sourceKey) ?? normalizeString2(input.details?.sourceKey);
3864
+ const createdAt = normalizeString2(input.createdAt) ?? new Date().toISOString();
3865
+ const status = normalizeTaskLifecycleStatus(normalizeString2(input.status) ?? "open") ?? "open";
3866
+ const existingIssueLines = existsSync5(issuesPath) ? readFileSync3(issuesPath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : [];
3867
+ const existingIssues = existingIssueLines.map((line) => {
3868
+ try {
3869
+ return JSON.parse(line);
3870
+ } catch {
3871
+ return null;
3872
+ }
3873
+ }).filter((value) => value !== null);
3874
+ const existingIds = new Set(existingIssues.map((issue) => typeof issue.id === "string" ? issue.id : null).filter((value) => value !== null));
3875
+ const rawTaskState = existsSync5(taskStatePath) ? readJsonFile(taskStatePath, {}) : {};
3876
+ const tasks = rawTaskState.tasks && typeof rawTaskState.tasks === "object" && !Array.isArray(rawTaskState.tasks) ? rawTaskState.tasks : {};
3877
+ const existingTaskIdFromSourceKey = sourceKey == null ? null : Object.entries(tasks).find(([, metadata]) => {
3878
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
3879
+ return false;
3880
+ }
3881
+ return normalizeString2(metadata.sourceKey) === sourceKey;
3882
+ })?.[0] ?? null;
3883
+ const existingIssue = (existingTaskIdFromSourceKey ? existingIssues.find((issue) => normalizeString2(issue.id) === existingTaskIdFromSourceKey && normalizeString2(issue.status) !== "closed") : null) ?? existingIssues.find((issue) => normalizeString2(issue.title) === summary && normalizeString2(issue.status) !== "closed");
3884
+ const taskId = normalizeString2(existingIssue?.id) ?? existingTaskIdFromSourceKey ?? nextInspectorTaskId(existingIds, createdAt);
3885
+ const mergedLabels = Array.from(new Set([
3886
+ ...normalizeStringArray(existingIssue?.labels),
3887
+ ...labels
3888
+ ]));
3889
+ if (!existingIssue) {
3890
+ const issueRecord = {
3891
+ id: taskId,
3892
+ title: summary,
3893
+ description,
3894
+ acceptance_criteria: acceptanceCriteria,
3895
+ status,
3896
+ issue_type: "task",
3897
+ created_at: createdAt,
3898
+ updated_at: createdAt,
3899
+ labels: mergedLabels
3900
+ };
3901
+ writeFileSync4(issuesPath, existingIssueLines.length > 0 ? `${existingIssueLines.join(`
3902
+ `)}
3903
+ ${JSON.stringify(issueRecord)}
3904
+ ` : `${JSON.stringify(issueRecord)}
3905
+ `, "utf8");
3906
+ } else {
3907
+ const updatedIssues = existingIssues.map((issue) => {
3908
+ if (normalizeString2(issue.id) !== taskId) {
3909
+ return issue;
3910
+ }
3911
+ return {
3912
+ ...issue,
3913
+ description,
3914
+ acceptance_criteria: acceptanceCriteria,
3915
+ updated_at: createdAt,
3916
+ labels: mergedLabels
3917
+ };
3918
+ });
3919
+ writeFileSync4(issuesPath, `${updatedIssues.map((issue) => JSON.stringify(issue)).join(`
3920
+ `)}
3921
+ `, "utf8");
3922
+ }
3923
+ const taskConfig = await readTaskConfig(projectRoot);
3924
+ const meta = taskConfig._meta && typeof taskConfig._meta === "object" && !Array.isArray(taskConfig._meta) ? taskConfig._meta : {};
3925
+ const existingDescriptions = normalizeValidationDescriptions(meta.validation_descriptions);
3926
+ taskConfig[taskId] = {
3927
+ role,
3928
+ description,
3929
+ acceptance_criteria: acceptanceCriteria,
3930
+ scope: scope.length > 0 ? scope : [`artifacts/${taskId}/**`],
3931
+ validation: validation.length > 0 ? validation : ["boundary:changed-files"]
3932
+ };
3933
+ if (Object.keys({ ...existingDescriptions, ...validationDescriptions }).length > 0) {
3934
+ taskConfig._meta = {
3935
+ ...meta,
3936
+ validation_descriptions: {
3937
+ ...existingDescriptions,
3938
+ ...validationDescriptions
3939
+ }
3940
+ };
3941
+ }
3942
+ writeFileSync4(taskConfigPath, `${JSON.stringify(taskConfig, null, 2)}
3943
+ `, "utf8");
3944
+ tasks[taskId] = {
3945
+ status,
3946
+ sourceCommit: resolveFollowupSourceCommit(input),
3947
+ ...sourceKey ? { sourceKey } : {}
3948
+ };
3949
+ writeFileSync4(taskStatePath, `${JSON.stringify({
3950
+ schemaVersion: 1,
3951
+ baseTrackerCommit: typeof rawTaskState.baseTrackerCommit === "string" ? rawTaskState.baseTrackerCommit : null,
3952
+ tasks
3953
+ }, null, 2)}
3954
+ `, "utf8");
3955
+ return {
3956
+ status: "completed",
3957
+ summary: `${existingIssue ? "Reused" : "Created"} inspector follow-up task ${taskId}`,
3958
+ details: {
3959
+ taskId,
3960
+ status,
3961
+ role,
3962
+ scope: taskConfig[taskId]?.scope ?? [],
3963
+ validation: taskConfig[taskId]?.validation ?? []
3964
+ }
3965
+ };
3966
+ }
3967
+ async function runInspectorLocalReview(projectRoot, input) {
3968
+ const taskId = normalizeString2(input.taskId);
3969
+ if (!taskId) {
3970
+ return {
3971
+ status: "unavailable",
3972
+ summary: "Local review requires a taskId",
3973
+ details: null
3974
+ };
3975
+ }
3976
+ const [{ RuntimeEventBus }, { PluginManager }, { verifyTask }] = await Promise.all([
3977
+ import("@rig/runtime/control-plane/runtime/events"),
3978
+ import("@rig/runtime/control-plane/runtime/plugins"),
3979
+ import("@rig/runtime/control-plane/native/verifier")
3980
+ ]);
3981
+ const eventBus = new RuntimeEventBus({ projectRoot, runId: `inspector-review:${taskId}` });
3982
+ const plugins = await PluginManager.load({
3983
+ projectRoot,
3984
+ runId: `inspector-review:${taskId}`,
3985
+ eventBus
3986
+ });
3987
+ const outcome = await verifyTask({
3988
+ projectRoot,
3989
+ taskId,
3990
+ plugins,
3991
+ skipAiReview: true
3992
+ });
3993
+ return {
3994
+ status: outcome.approved ? "completed" : "failed",
3995
+ summary: outcome.approved ? `Local review approved for ${taskId}` : `Local review rejected for ${taskId}`,
3996
+ details: outcome
3997
+ };
3998
+ }
3999
+ async function runInspectorUpstreamSync(projectRoot, journal, input) {
4000
+ const scanId = normalizeString2(input.scanId) ?? `scheduled:${new Date().toISOString().slice(0, 10)}`;
4001
+ try {
4002
+ const scan = scanUpstream({ projectRoot });
4003
+ const result = await runUpstreamSyncScan({
4004
+ journal,
4005
+ scanId,
4006
+ findings: scan.findings,
4007
+ createTask: (finding) => {
4008
+ const details = finding.details ?? {};
4009
+ const commitSummary = typeof details.commits === "object" && Array.isArray(details.commits) ? [
4010
+ `Upstream remote: ${String(details.remote ?? "")}`,
4011
+ `Tracked branch: ${String(details.branch ?? "")}`,
4012
+ `Imported baseline: ${String(details.baselineSha ?? "")}`,
4013
+ `Scanned since: ${String(details.sinceSha ?? "")}`,
4014
+ "",
4015
+ "Recent upstream commits:",
4016
+ ...details.commits.map((commit) => `- ${(commit.sha ?? "").slice(0, 12)} ${commit.date ?? ""} ${commit.summary ?? ""}`.trim())
4017
+ ].join(`
4018
+ `) : "";
4019
+ return createInspectorFollowupTask(projectRoot, {
4020
+ summary: finding.summary,
4021
+ description: [normalizeString2(details.description) ?? "", commitSummary].filter(Boolean).join(`
4022
+
4023
+ `),
4024
+ acceptanceCriteria: normalizeString2(details.acceptanceCriteria),
4025
+ sourceCommit: normalizeString2(details.latestHeadSha),
4026
+ sourceKey: finding.key,
4027
+ details,
4028
+ scope: Array.isArray(details.scope) ? details.scope : [],
4029
+ validation: Array.isArray(details.validation) ? details.validation : [],
4030
+ validationDescriptions: details.validationDescriptions && typeof details.validationDescriptions === "object" && !Array.isArray(details.validationDescriptions) ? details.validationDescriptions : undefined,
4031
+ labels: [
4032
+ ...normalizeStringArray(details.labels),
4033
+ `remote:${String(details.remote ?? "unknown")}`,
4034
+ `classification:${String(details.classification ?? "unknown")}`,
4035
+ "kind:upstream-drift"
4036
+ ]
4037
+ }).then((taskResult) => ({
4038
+ taskId: taskResult.details && typeof taskResult.details === "object" && !Array.isArray(taskResult.details) && typeof taskResult.details.taskId === "string" ? taskResult.details.taskId : null,
4039
+ details: taskResult.details && typeof taskResult.details === "object" && !Array.isArray(taskResult.details) ? taskResult.details : null
4040
+ }));
4041
+ }
4042
+ });
4043
+ writeUpstreamState(projectRoot, {
4044
+ baselineSha: scan.baselineSha,
4045
+ lastProcessedCommit: scan.latestHeadSha,
4046
+ latestHeadSha: scan.latestHeadSha,
4047
+ checkoutPath: scan.checkoutPath,
4048
+ branch: scan.branch,
4049
+ updatedAt: new Date().toISOString()
4050
+ }, scan.statePath);
4051
+ return {
4052
+ status: result.status,
4053
+ summary: result.summary,
4054
+ details: {
4055
+ baselineSha: scan.baselineSha,
4056
+ sinceSha: scan.sinceSha,
4057
+ latestHeadSha: scan.latestHeadSha,
4058
+ checkoutPath: scan.checkoutPath,
4059
+ branch: scan.branch,
4060
+ scannedCommitCount: scan.scannedCommitCount,
4061
+ ignoredCommitCount: scan.ignoredCommitCount,
4062
+ followupCount: result.followupCount,
4063
+ createdTaskIds: result.createdTaskIds
4064
+ }
4065
+ };
4066
+ } catch (error) {
4067
+ const message = error instanceof Error ? error.message : String(error);
4068
+ if (/Unable to locate an authoritative upstream checkout/i.test(message)) {
4069
+ return {
4070
+ status: "completed",
4071
+ summary: "No upstream drift findings",
4072
+ details: {
4073
+ checkoutMissing: true,
4074
+ reason: message,
4075
+ followupCount: 0,
4076
+ createdTaskIds: []
4077
+ }
4078
+ };
4079
+ }
4080
+ return {
4081
+ status: "failed",
4082
+ summary: `Upstream sync failed: ${message}`,
4083
+ details: null
4084
+ };
4085
+ }
4086
+ }
4087
+ function createServerInspector(projectRoot, scheduleOptions = {}) {
4088
+ try {
4089
+ const journal = createInspectorJournal({ projectRoot });
4090
+ const service = createGlobalInspectorService({
4091
+ projectRoot,
4092
+ journal,
4093
+ upstreamSyncMs: scheduleOptions.upstreamSyncMs,
4094
+ upstreamSyncOnStart: scheduleOptions.upstreamSyncOnStart,
4095
+ reviewRunner: (input) => runInspectorLocalReview(projectRoot, input),
4096
+ followupTaskRunner: (input) => createInspectorFollowupTask(projectRoot, input),
4097
+ upstreamSyncRunner: (input) => runInspectorUpstreamSync(projectRoot, journal, input),
4098
+ analysisRunner: async (input) => summarizeInspectorJournal({
4099
+ journal,
4100
+ reportId: typeof input.reportId === "string" && input.reportId.trim().length > 0 ? input.reportId : `analysis:${new Date().toISOString()}`,
4101
+ reportType: typeof input.reportType === "string" && input.reportType.trim().length > 0 ? input.reportType : "run-health"
4102
+ })
4103
+ });
4104
+ service.reconcileOnce("startup");
4105
+ return service;
4106
+ } catch (error) {
4107
+ const message = error instanceof Error ? error.message : String(error);
4108
+ console.error(`[server] inspector bootstrap failed: ${message}`);
4109
+ return null;
4110
+ }
4111
+ }
4112
+ function createServerInspectorAgent(projectRoot, inspector) {
4113
+ if (!inspector) {
4114
+ return null;
4115
+ }
4116
+ try {
4117
+ const journal = createInspectorJournal({ projectRoot });
4118
+ const catalog = buildInspectorSkillCatalog({
4119
+ role: "inspector",
4120
+ taskKind: "code-change"
4121
+ });
4122
+ return createInspectorAgentRuntime({
4123
+ projectRoot,
4124
+ journal,
4125
+ snapshotProvider: () => inspector.snapshot(),
4126
+ invokeDynamicTool: (name, input) => inspector.invokeTool(name, input),
4127
+ requiredSkills: catalog.required,
4128
+ recommendedSkills: catalog.recommended
4129
+ });
4130
+ } catch (error) {
4131
+ const message = error instanceof Error ? error.message : String(error);
4132
+ console.error(`[server] inspector agent bootstrap failed: ${message}`);
4133
+ return null;
4134
+ }
4135
+ }
4136
+ export {
4137
+ runInspectorUpstreamSync,
4138
+ runInspectorLocalReview,
4139
+ resolveFollowupSourceCommit,
4140
+ normalizeValidationDescriptions,
4141
+ nextInspectorTaskId,
4142
+ createServerInspectorAgent,
4143
+ createServerInspector,
4144
+ createInspectorFollowupTask
4145
+ };