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