@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,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
|
+
};
|