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