@revealui/harnesses 0.1.7 → 0.1.9
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/LICENSE +104 -17
- package/README.md +6 -6
- package/dist/{chunk-DGUM43GV.js → chunk-3RG5ZIWI.js} +0 -1
- package/dist/chunk-ANX4L2PF.js +651 -0
- package/dist/{chunk-JG6CAG4A.js → chunk-Y4FFO3TO.js} +29 -8
- package/dist/chunk-YYAYTCRM.js +3016 -0
- package/dist/{chunk-XXEKWC6F.js → chunk-ZNIQELKZ.js} +189 -345
- package/dist/cli.js +44 -9
- package/dist/content/index.d.ts +1 -1
- package/dist/content/index.js +2 -3
- package/dist/index-w5ashbfb.d.ts +266 -0
- package/dist/index.d.ts +770 -85
- package/dist/index.js +39 -10
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +9 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js +0 -1
- package/dist/workboard/index.d.ts +26 -14
- package/dist/workboard/index.js +2 -3
- package/package.json +30 -7
- package/LICENSE.commercial +0 -111
- package/dist/chunk-DGUM43GV.js.map +0 -1
- package/dist/chunk-JG6CAG4A.js.map +0 -1
- package/dist/chunk-XLIKSLM3.js +0 -1105
- package/dist/chunk-XLIKSLM3.js.map +0 -1
- package/dist/chunk-XXEKWC6F.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/content/index.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/workboard/index.js.map +0 -1
|
@@ -0,0 +1,3016 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DaemonStore
|
|
3
|
+
} from "./chunk-ANX4L2PF.js";
|
|
4
|
+
import {
|
|
5
|
+
WorkboardManager,
|
|
6
|
+
deriveSessionId,
|
|
7
|
+
detectSessionType
|
|
8
|
+
} from "./chunk-Y4FFO3TO.js";
|
|
9
|
+
import {
|
|
10
|
+
__require
|
|
11
|
+
} from "./chunk-3RG5ZIWI.js";
|
|
12
|
+
|
|
13
|
+
// src/index.ts
|
|
14
|
+
import { isFeatureEnabled } from "@revealui/core/features";
|
|
15
|
+
import { initializeLicense } from "@revealui/core/license";
|
|
16
|
+
import { logger } from "@revealui/core/observability/logger";
|
|
17
|
+
|
|
18
|
+
// src/config/config-sync.ts
|
|
19
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "fs";
|
|
20
|
+
import { dirname } from "path";
|
|
21
|
+
|
|
22
|
+
// src/config/harness-config-paths.ts
|
|
23
|
+
import { homedir } from "os";
|
|
24
|
+
import { join } from "path";
|
|
25
|
+
var HOME = homedir();
|
|
26
|
+
var REVEALUI_ROOT = process.env.REVEALUI_ROOT ?? join(HOME, ".revealui");
|
|
27
|
+
var LOCAL_CONFIG_PATHS = {
|
|
28
|
+
"claude-code": join(HOME, ".claude", "settings.json"),
|
|
29
|
+
cursor: join(HOME, ".cursor", "settings.json"),
|
|
30
|
+
copilot: join(HOME, ".config", "github-copilot", "hosts.json")
|
|
31
|
+
};
|
|
32
|
+
var ROOT_CONFIG_FILES = {
|
|
33
|
+
"claude-code": "settings.json",
|
|
34
|
+
cursor: "settings.json",
|
|
35
|
+
copilot: "hosts.json"
|
|
36
|
+
};
|
|
37
|
+
function getLocalConfigPath(harnessId) {
|
|
38
|
+
return LOCAL_CONFIG_PATHS[harnessId];
|
|
39
|
+
}
|
|
40
|
+
function getRootConfigPath(harnessId, root = REVEALUI_ROOT) {
|
|
41
|
+
const file = ROOT_CONFIG_FILES[harnessId];
|
|
42
|
+
if (!file) return void 0;
|
|
43
|
+
return join(root, "harness-configs", harnessId, file);
|
|
44
|
+
}
|
|
45
|
+
function getConfigurableHarnesses() {
|
|
46
|
+
return Object.keys(LOCAL_CONFIG_PATHS);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/config/config-sync.ts
|
|
50
|
+
function syncConfig(harnessId, direction, root) {
|
|
51
|
+
const localPath = getLocalConfigPath(harnessId);
|
|
52
|
+
const rootPath = getRootConfigPath(harnessId, root);
|
|
53
|
+
if (!(localPath && rootPath)) {
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
harnessId,
|
|
57
|
+
direction,
|
|
58
|
+
message: `No config path known for harness: ${harnessId}`
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
if (direction === "pull") {
|
|
63
|
+
if (!existsSync(rootPath)) {
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
harnessId,
|
|
67
|
+
direction,
|
|
68
|
+
message: `Root config not found: ${rootPath}`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
mkdirSync(dirname(localPath), { recursive: true });
|
|
72
|
+
backupIfExists(localPath);
|
|
73
|
+
copyFileSync(rootPath, localPath);
|
|
74
|
+
return { success: true, harnessId, direction, message: `Pulled ${rootPath} \u2192 ${localPath}` };
|
|
75
|
+
} else {
|
|
76
|
+
if (!existsSync(localPath)) {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
harnessId,
|
|
80
|
+
direction,
|
|
81
|
+
message: `Local config not found: ${localPath}`
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
mkdirSync(dirname(rootPath), { recursive: true });
|
|
85
|
+
backupIfExists(rootPath);
|
|
86
|
+
copyFileSync(localPath, rootPath);
|
|
87
|
+
return { success: true, harnessId, direction, message: `Pushed ${localPath} \u2192 ${rootPath}` };
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
harnessId,
|
|
93
|
+
direction,
|
|
94
|
+
message: err instanceof Error ? err.message : String(err)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function diffConfig(harnessId, root) {
|
|
99
|
+
const localPath = getLocalConfigPath(harnessId);
|
|
100
|
+
const rootPath = getRootConfigPath(harnessId, root);
|
|
101
|
+
const localExists = !!localPath && existsSync(localPath);
|
|
102
|
+
const ssdExists = !!rootPath && existsSync(rootPath);
|
|
103
|
+
if (!(localExists && ssdExists)) {
|
|
104
|
+
return { harnessId, localExists, ssdExists, identical: false };
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const localContent = readFileSync(localPath, "utf8");
|
|
108
|
+
const ssdContent = readFileSync(rootPath, "utf8");
|
|
109
|
+
return { harnessId, localExists, ssdExists, identical: localContent === ssdContent };
|
|
110
|
+
} catch {
|
|
111
|
+
return { harnessId, localExists, ssdExists, identical: false };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function syncAllConfigs(direction, root) {
|
|
115
|
+
return getConfigurableHarnesses().map((id) => syncConfig(id, direction, root));
|
|
116
|
+
}
|
|
117
|
+
function diffAllConfigs(root) {
|
|
118
|
+
return getConfigurableHarnesses().map((id) => diffConfig(id, root));
|
|
119
|
+
}
|
|
120
|
+
function validateConfigJson(harnessId) {
|
|
121
|
+
const localPath = getLocalConfigPath(harnessId);
|
|
122
|
+
if (!localPath) return `No config path known for harness: ${harnessId}`;
|
|
123
|
+
if (!existsSync(localPath)) return `Config file not found: ${localPath}`;
|
|
124
|
+
try {
|
|
125
|
+
const content = readFileSync(localPath, "utf8");
|
|
126
|
+
JSON.parse(content);
|
|
127
|
+
return null;
|
|
128
|
+
} catch (err) {
|
|
129
|
+
return err instanceof Error ? err.message : String(err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function backupIfExists(filePath) {
|
|
133
|
+
try {
|
|
134
|
+
if (existsSync(filePath)) {
|
|
135
|
+
copyFileSync(filePath, `${filePath}.bak`);
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/coordinator.ts
|
|
142
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
143
|
+
import { join as join4 } from "path";
|
|
144
|
+
|
|
145
|
+
// src/coordination/ci-feedback.ts
|
|
146
|
+
var MAX_RETRIES = 3;
|
|
147
|
+
var CIFeedback = class {
|
|
148
|
+
constructor(store) {
|
|
149
|
+
this.store = store;
|
|
150
|
+
}
|
|
151
|
+
store;
|
|
152
|
+
/**
|
|
153
|
+
* Handle a CI report. Called when GitHub Actions posts results
|
|
154
|
+
* to the daemon HTTP gateway.
|
|
155
|
+
*/
|
|
156
|
+
async report(params) {
|
|
157
|
+
const mr = await this.findMergeRequest(params);
|
|
158
|
+
if (!mr) {
|
|
159
|
+
return { action: "no_merge_request" };
|
|
160
|
+
}
|
|
161
|
+
await this.store.logEvent({
|
|
162
|
+
agentId: mr.agent_id,
|
|
163
|
+
eventType: params.success ? "ci-passed" : "ci-failed",
|
|
164
|
+
payload: {
|
|
165
|
+
mergeId: mr.id,
|
|
166
|
+
prNumber: params.prNumber,
|
|
167
|
+
failedJob: params.failedJob,
|
|
168
|
+
runUrl: params.runUrl
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
if (params.success) {
|
|
172
|
+
return this.handleSuccess(mr);
|
|
173
|
+
}
|
|
174
|
+
return this.handleFailure(mr, params);
|
|
175
|
+
}
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Private - success path
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
async handleSuccess(mr) {
|
|
180
|
+
await this.store.updateMergeRequest(mr.id, { status: "merged" });
|
|
181
|
+
if (mr.task_id) {
|
|
182
|
+
await this.store.completeTask(mr.task_id, mr.agent_id);
|
|
183
|
+
}
|
|
184
|
+
await this.store.updateWorktreeStatus(mr.agent_id, "merged");
|
|
185
|
+
await this.store.sendMessage({
|
|
186
|
+
fromAgent: "ci-feedback",
|
|
187
|
+
toAgent: mr.agent_id,
|
|
188
|
+
subject: "CI passed - PR ready to merge",
|
|
189
|
+
body: `PR ${mr.pr_url ?? `#${mr.pr_number}`} passed CI. Merge request ${mr.id} is complete.`
|
|
190
|
+
});
|
|
191
|
+
await this.store.storeMemory({
|
|
192
|
+
agentId: mr.agent_id,
|
|
193
|
+
memoryType: "result",
|
|
194
|
+
content: `CI passed for ${mr.source_branch}. PR ${mr.pr_number ?? "(unknown)"} is ready.`,
|
|
195
|
+
metadata: { mergeId: mr.id, prNumber: mr.pr_number }
|
|
196
|
+
});
|
|
197
|
+
return { action: "merged", mergeRequestId: mr.id };
|
|
198
|
+
}
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Private - failure path
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
async handleFailure(mr, params) {
|
|
203
|
+
const retryCount = await this.store.incrementMergeRetry(mr.id);
|
|
204
|
+
await this.store.updateMergeRequest(mr.id, {
|
|
205
|
+
status: "ci_failed",
|
|
206
|
+
ciOutput: params.output?.slice(0, 1e4),
|
|
207
|
+
errorMessage: `CI failed (attempt ${retryCount}/${MAX_RETRIES}): ${params.failedJob ?? "unknown job"}`
|
|
208
|
+
});
|
|
209
|
+
if (retryCount >= MAX_RETRIES) {
|
|
210
|
+
return this.escalate(mr, retryCount, params);
|
|
211
|
+
}
|
|
212
|
+
const category = this.categorizeFailure(params);
|
|
213
|
+
const taskId = `fix-${mr.id}-${retryCount}`;
|
|
214
|
+
const description = this.buildFixTaskDescription(mr, category, params);
|
|
215
|
+
await this.store.createTask({ id: taskId, description });
|
|
216
|
+
await this.store.claimTask(taskId, mr.agent_id);
|
|
217
|
+
await this.store.sendMessage({
|
|
218
|
+
fromAgent: "ci-feedback",
|
|
219
|
+
toAgent: mr.agent_id,
|
|
220
|
+
subject: `CI failed \u2014 fix task created (attempt ${retryCount}/${MAX_RETRIES})`,
|
|
221
|
+
body: description
|
|
222
|
+
});
|
|
223
|
+
await this.store.storeMemory({
|
|
224
|
+
agentId: mr.agent_id,
|
|
225
|
+
memoryType: "action",
|
|
226
|
+
content: `CI failed on ${mr.source_branch}: ${category} error. Fix task ${taskId} created. Attempt ${retryCount}/${MAX_RETRIES}.`,
|
|
227
|
+
metadata: {
|
|
228
|
+
mergeId: mr.id,
|
|
229
|
+
category,
|
|
230
|
+
failedJob: params.failedJob,
|
|
231
|
+
retryCount
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
return {
|
|
235
|
+
action: "fix_task_created",
|
|
236
|
+
mergeRequestId: mr.id,
|
|
237
|
+
taskId,
|
|
238
|
+
retryCount
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/** Escalate to human after max retries. */
|
|
242
|
+
async escalate(mr, retryCount, params) {
|
|
243
|
+
await this.store.updateMergeRequest(mr.id, { status: "escalated" });
|
|
244
|
+
await this.store.broadcastMessage({
|
|
245
|
+
fromAgent: "ci-feedback",
|
|
246
|
+
subject: `ESCALATION: ${mr.source_branch} failed CI ${retryCount} times`,
|
|
247
|
+
body: [
|
|
248
|
+
`Merge request ${mr.id} has failed CI ${retryCount} times and needs human intervention.`,
|
|
249
|
+
`Agent: ${mr.agent_id}`,
|
|
250
|
+
`Branch: ${mr.source_branch} \u2192 ${mr.base_branch}`,
|
|
251
|
+
`PR: ${mr.pr_url ?? `#${mr.pr_number}`}`,
|
|
252
|
+
`Last failure: ${params.failedJob ?? "unknown"}`,
|
|
253
|
+
`Run: ${params.runUrl ?? "no URL"}`
|
|
254
|
+
].join("\n")
|
|
255
|
+
});
|
|
256
|
+
await this.store.logEvent({
|
|
257
|
+
agentId: mr.agent_id,
|
|
258
|
+
eventType: "merge-escalated",
|
|
259
|
+
payload: {
|
|
260
|
+
mergeId: mr.id,
|
|
261
|
+
retryCount,
|
|
262
|
+
failedJob: params.failedJob,
|
|
263
|
+
runUrl: params.runUrl
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
action: "escalated",
|
|
268
|
+
mergeRequestId: mr.id,
|
|
269
|
+
retryCount
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// Private - helpers
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
/** Find the merge request associated with this CI run. */
|
|
276
|
+
async findMergeRequest(params) {
|
|
277
|
+
if (params.prNumber) {
|
|
278
|
+
const mr = await this.store.getMergeRequestByPr(params.prNumber);
|
|
279
|
+
if (mr) return mr;
|
|
280
|
+
}
|
|
281
|
+
if (params.branch) {
|
|
282
|
+
return this.store.getMergeRequestByBranch(params.branch);
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
/** Categorize the CI failure from the job name or output. */
|
|
287
|
+
categorizeFailure(params) {
|
|
288
|
+
const job = params.failedJob?.toLowerCase() ?? "";
|
|
289
|
+
const output = params.output?.toLowerCase() ?? "";
|
|
290
|
+
if (job.includes("lint") || job.includes("quality")) return "lint";
|
|
291
|
+
if (job.includes("typecheck") || job.includes("type")) return "typecheck";
|
|
292
|
+
if (job.includes("test") && !job.includes("e2e")) return "test";
|
|
293
|
+
if (job.includes("build")) return "build";
|
|
294
|
+
if (job.includes("e2e") || job.includes("playwright")) return "e2e";
|
|
295
|
+
if (output.includes("biome") || output.includes("lint")) return "lint";
|
|
296
|
+
if (output.includes("ts2") || output.includes("type error")) return "typecheck";
|
|
297
|
+
if (output.includes("vitest") || output.includes("test failed")) return "test";
|
|
298
|
+
if (output.includes("build failed") || output.includes("esbuild")) return "build";
|
|
299
|
+
return "unknown";
|
|
300
|
+
}
|
|
301
|
+
/** Build a descriptive fix task for the agent. */
|
|
302
|
+
buildFixTaskDescription(mr, category, params) {
|
|
303
|
+
const lines = [`Fix CI ${category} failure on branch ${mr.source_branch}.`];
|
|
304
|
+
switch (category) {
|
|
305
|
+
case "lint":
|
|
306
|
+
lines.push("Run `pnpm lint:fix` and commit the changes.");
|
|
307
|
+
break;
|
|
308
|
+
case "typecheck":
|
|
309
|
+
lines.push("Run `pnpm typecheck:all` locally and fix type errors.");
|
|
310
|
+
break;
|
|
311
|
+
case "test":
|
|
312
|
+
lines.push("Run `pnpm test` locally and fix failing tests.");
|
|
313
|
+
break;
|
|
314
|
+
case "build":
|
|
315
|
+
lines.push("Run `pnpm build` locally and fix build errors.");
|
|
316
|
+
break;
|
|
317
|
+
case "e2e":
|
|
318
|
+
lines.push("Check Playwright E2E test output and fix the failing scenarios.");
|
|
319
|
+
break;
|
|
320
|
+
default:
|
|
321
|
+
lines.push("Review the CI output and fix the issue.");
|
|
322
|
+
}
|
|
323
|
+
if (params.runUrl) {
|
|
324
|
+
lines.push(`CI run: ${params.runUrl}`);
|
|
325
|
+
}
|
|
326
|
+
if (params.output) {
|
|
327
|
+
const truncated = params.output.slice(0, 2e3);
|
|
328
|
+
lines.push("", "CI output (truncated):", "```", truncated, "```");
|
|
329
|
+
}
|
|
330
|
+
return lines.join("\n");
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// src/coordination/merge-pipeline.ts
|
|
335
|
+
import { execFile } from "child_process";
|
|
336
|
+
import { promisify } from "util";
|
|
337
|
+
var execFileAsync = promisify(execFile);
|
|
338
|
+
var GIT_TIMEOUT = 3e4;
|
|
339
|
+
var GH_TIMEOUT = 15e3;
|
|
340
|
+
async function getWorkspaceDependencyOrder(repoRoot) {
|
|
341
|
+
try {
|
|
342
|
+
const { stdout } = await execFileAsync("pnpm", ["ls", "--depth", "0", "--json", "-r"], {
|
|
343
|
+
cwd: repoRoot,
|
|
344
|
+
timeout: GIT_TIMEOUT
|
|
345
|
+
});
|
|
346
|
+
const packages = JSON.parse(stdout);
|
|
347
|
+
return packages.map((p) => p.name);
|
|
348
|
+
} catch {
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
var MergePipeline = class {
|
|
353
|
+
constructor(store, config) {
|
|
354
|
+
this.store = store;
|
|
355
|
+
this.repoRoot = config.repoRoot;
|
|
356
|
+
this.baseBranch = config.baseBranch ?? "test";
|
|
357
|
+
this.remote = config.remote ?? "origin";
|
|
358
|
+
}
|
|
359
|
+
store;
|
|
360
|
+
repoRoot;
|
|
361
|
+
baseBranch;
|
|
362
|
+
remote;
|
|
363
|
+
/**
|
|
364
|
+
* Request a merge for an agent's branch into the base branch.
|
|
365
|
+
* Creates a merge_request record and attempts the merge.
|
|
366
|
+
*/
|
|
367
|
+
async requestMerge(params) {
|
|
368
|
+
const mergeId = `merge-${params.agentId}-${Date.now()}`;
|
|
369
|
+
const baseBranch = params.baseBranch ?? this.baseBranch;
|
|
370
|
+
await this.store.createMergeRequest({
|
|
371
|
+
id: mergeId,
|
|
372
|
+
agentId: params.agentId,
|
|
373
|
+
taskId: params.taskId,
|
|
374
|
+
sourceBranch: params.sourceBranch,
|
|
375
|
+
baseBranch
|
|
376
|
+
});
|
|
377
|
+
await this.store.logEvent({
|
|
378
|
+
agentId: params.agentId,
|
|
379
|
+
eventType: "merge-requested",
|
|
380
|
+
payload: { mergeId, sourceBranch: params.sourceBranch, baseBranch }
|
|
381
|
+
});
|
|
382
|
+
return this.processMerge(mergeId);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Process a pending merge request: fetch, check for conflicts,
|
|
386
|
+
* fast-forward merge, and create a PR.
|
|
387
|
+
*/
|
|
388
|
+
async processMerge(mergeId) {
|
|
389
|
+
const mr = await this.store.getMergeRequest(mergeId);
|
|
390
|
+
if (!mr) {
|
|
391
|
+
return { mergeRequestId: mergeId, status: "pending", error: "Merge request not found" };
|
|
392
|
+
}
|
|
393
|
+
await this.store.updateMergeRequest(mergeId, { status: "merging" });
|
|
394
|
+
try {
|
|
395
|
+
await this.git(["fetch", this.remote, mr.base_branch, mr.source_branch]);
|
|
396
|
+
const conflictCheck = await this.checkConflicts(mr.source_branch, mr.base_branch);
|
|
397
|
+
if (!conflictCheck.clean) {
|
|
398
|
+
await this.handleConflict(mr, conflictCheck.conflictingFiles);
|
|
399
|
+
return {
|
|
400
|
+
mergeRequestId: mergeId,
|
|
401
|
+
status: "conflict",
|
|
402
|
+
conflictingFiles: conflictCheck.conflictingFiles,
|
|
403
|
+
error: `Merge conflict in: ${conflictCheck.conflictingFiles.join(", ")}`
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const prResult = await this.createPR(mr);
|
|
407
|
+
await this.store.updateMergeRequest(mergeId, {
|
|
408
|
+
status: "pr_created",
|
|
409
|
+
prNumber: prResult.number,
|
|
410
|
+
prUrl: prResult.url
|
|
411
|
+
});
|
|
412
|
+
await this.store.logEvent({
|
|
413
|
+
agentId: mr.agent_id,
|
|
414
|
+
eventType: "pr-created",
|
|
415
|
+
payload: { mergeId, prNumber: prResult.number, prUrl: prResult.url }
|
|
416
|
+
});
|
|
417
|
+
return {
|
|
418
|
+
mergeRequestId: mergeId,
|
|
419
|
+
status: "pr_created",
|
|
420
|
+
prUrl: prResult.url,
|
|
421
|
+
prNumber: prResult.number
|
|
422
|
+
};
|
|
423
|
+
} catch (err) {
|
|
424
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
425
|
+
await this.store.updateMergeRequest(mergeId, {
|
|
426
|
+
status: "ci_failed",
|
|
427
|
+
errorMessage: message
|
|
428
|
+
});
|
|
429
|
+
return { mergeRequestId: mergeId, status: "ci_failed", error: message };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/** Get the current status of a merge request. */
|
|
433
|
+
async getStatus(mergeId) {
|
|
434
|
+
const mr = await this.store.getMergeRequest(mergeId);
|
|
435
|
+
if (!mr) {
|
|
436
|
+
return { mergeRequestId: mergeId, status: "pending", error: "Not found" };
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
mergeRequestId: mergeId,
|
|
440
|
+
status: mr.status,
|
|
441
|
+
prUrl: mr.pr_url ?? void 0,
|
|
442
|
+
prNumber: mr.pr_number ?? void 0,
|
|
443
|
+
error: mr.error_message ?? void 0
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
/** Retry a failed or conflicted merge request. */
|
|
447
|
+
async resolve(mergeId) {
|
|
448
|
+
const mr = await this.store.getMergeRequest(mergeId);
|
|
449
|
+
if (!mr) {
|
|
450
|
+
return { mergeRequestId: mergeId, status: "pending", error: "Not found" };
|
|
451
|
+
}
|
|
452
|
+
if (mr.status !== "conflict" && mr.status !== "ci_failed") {
|
|
453
|
+
return {
|
|
454
|
+
mergeRequestId: mergeId,
|
|
455
|
+
status: mr.status,
|
|
456
|
+
error: `Cannot resolve: status is ${mr.status}`
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
await this.store.updateMergeRequest(mergeId, { status: "pending", errorMessage: "" });
|
|
460
|
+
return this.processMerge(mergeId);
|
|
461
|
+
}
|
|
462
|
+
/** List all active (non-terminal) merge requests. */
|
|
463
|
+
async listActive() {
|
|
464
|
+
const all = await this.store.listMergeRequests();
|
|
465
|
+
return all.filter((mr) => mr.status !== "merged" && mr.status !== "escalated");
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Get the workspace dependency order for merge sequencing.
|
|
469
|
+
* Packages that are dependencies of others should merge first.
|
|
470
|
+
*/
|
|
471
|
+
async getDependencyOrder() {
|
|
472
|
+
return getWorkspaceDependencyOrder(this.repoRoot);
|
|
473
|
+
}
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
// Private helpers
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
/** Run a git command in the repo root. */
|
|
478
|
+
async git(args) {
|
|
479
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
480
|
+
cwd: this.repoRoot,
|
|
481
|
+
timeout: GIT_TIMEOUT
|
|
482
|
+
});
|
|
483
|
+
return stdout.trim();
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Check whether source branch merges cleanly into base branch.
|
|
487
|
+
* Uses `git merge-tree` (available in Git 2.38+) for a tree-only check
|
|
488
|
+
* that doesn't touch the working directory.
|
|
489
|
+
*/
|
|
490
|
+
async checkConflicts(sourceBranch, baseBranch) {
|
|
491
|
+
try {
|
|
492
|
+
await execFileAsync(
|
|
493
|
+
"git",
|
|
494
|
+
[
|
|
495
|
+
"merge-tree",
|
|
496
|
+
"--write-tree",
|
|
497
|
+
`${this.remote}/${baseBranch}`,
|
|
498
|
+
`${this.remote}/${sourceBranch}`
|
|
499
|
+
],
|
|
500
|
+
{ cwd: this.repoRoot, timeout: GIT_TIMEOUT }
|
|
501
|
+
);
|
|
502
|
+
return { clean: true, conflictingFiles: [] };
|
|
503
|
+
} catch (err) {
|
|
504
|
+
const output = err instanceof Error && "stdout" in err ? String(err.stdout) : "";
|
|
505
|
+
const conflicting = [];
|
|
506
|
+
for (const line of output.split("\n")) {
|
|
507
|
+
if (line.startsWith("CONFLICT")) {
|
|
508
|
+
const parts = line.split(" ");
|
|
509
|
+
const lastPart = parts[parts.length - 1];
|
|
510
|
+
if (lastPart) conflicting.push(lastPart);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return { clean: false, conflictingFiles: conflicting };
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/** Notify conflicting agents via the messaging system. */
|
|
517
|
+
async handleConflict(mr, conflictingFiles) {
|
|
518
|
+
await this.store.updateMergeRequest(mr.id, {
|
|
519
|
+
status: "conflict",
|
|
520
|
+
errorMessage: `Conflict in: ${conflictingFiles.join(", ")}`
|
|
521
|
+
});
|
|
522
|
+
const notifiedAgents = /* @__PURE__ */ new Set();
|
|
523
|
+
for (const file of conflictingFiles) {
|
|
524
|
+
const reservation = await this.store.checkReservation(file);
|
|
525
|
+
if (reservation && reservation.agent_id !== mr.agent_id) {
|
|
526
|
+
notifiedAgents.add(reservation.agent_id);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
for (const agentId of notifiedAgents) {
|
|
530
|
+
await this.store.sendMessage({
|
|
531
|
+
fromAgent: "merge-pipeline",
|
|
532
|
+
toAgent: agentId,
|
|
533
|
+
subject: `Merge conflict with ${mr.agent_id}`,
|
|
534
|
+
body: `Branch ${mr.source_branch} conflicts with your work on: ${conflictingFiles.join(", ")}. Please coordinate resolution.`
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
await this.store.sendMessage({
|
|
538
|
+
fromAgent: "merge-pipeline",
|
|
539
|
+
toAgent: mr.agent_id,
|
|
540
|
+
subject: "Merge conflict detected",
|
|
541
|
+
body: `Your branch ${mr.source_branch} has conflicts with ${mr.base_branch} in: ${conflictingFiles.join(", ")}. Resolve and call merge.resolve to retry.`
|
|
542
|
+
});
|
|
543
|
+
await this.store.logEvent({
|
|
544
|
+
agentId: mr.agent_id,
|
|
545
|
+
eventType: "merge-conflict",
|
|
546
|
+
payload: {
|
|
547
|
+
mergeId: mr.id,
|
|
548
|
+
conflictingFiles,
|
|
549
|
+
notifiedAgents: [...notifiedAgents]
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
/** Create a GitHub PR using the `gh` CLI. */
|
|
554
|
+
async createPR(mr) {
|
|
555
|
+
const title = `agent/${mr.agent_id}: ${mr.task_id ?? mr.source_branch}`;
|
|
556
|
+
const body = [
|
|
557
|
+
"## Automated Agent PR",
|
|
558
|
+
"",
|
|
559
|
+
`**Agent**: ${mr.agent_id}`,
|
|
560
|
+
`**Branch**: ${mr.source_branch} \u2192 ${mr.base_branch}`,
|
|
561
|
+
mr.task_id ? `**Task**: ${mr.task_id}` : "",
|
|
562
|
+
"",
|
|
563
|
+
"This PR was created automatically by the merge pipeline.",
|
|
564
|
+
"CI must pass before merge."
|
|
565
|
+
].filter(Boolean).join("\n");
|
|
566
|
+
const { stdout } = await execFileAsync(
|
|
567
|
+
"gh",
|
|
568
|
+
[
|
|
569
|
+
"pr",
|
|
570
|
+
"create",
|
|
571
|
+
"--base",
|
|
572
|
+
mr.base_branch,
|
|
573
|
+
"--head",
|
|
574
|
+
mr.source_branch,
|
|
575
|
+
"--title",
|
|
576
|
+
title,
|
|
577
|
+
"--body",
|
|
578
|
+
body
|
|
579
|
+
],
|
|
580
|
+
{ cwd: this.repoRoot, timeout: GH_TIMEOUT }
|
|
581
|
+
);
|
|
582
|
+
const url = stdout.trim();
|
|
583
|
+
const parts = url.split("/");
|
|
584
|
+
const number = Number.parseInt(parts[parts.length - 1] ?? "0", 10);
|
|
585
|
+
return { number, url };
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// src/vaughn/capabilities.ts
|
|
590
|
+
function createDefaultCapabilities() {
|
|
591
|
+
return {
|
|
592
|
+
dispatch: {
|
|
593
|
+
generateCode: false,
|
|
594
|
+
analyzeCode: false,
|
|
595
|
+
applyEdit: false,
|
|
596
|
+
executeCommand: false
|
|
597
|
+
},
|
|
598
|
+
readWorkboard: false,
|
|
599
|
+
writeWorkboard: false,
|
|
600
|
+
claimTasks: false,
|
|
601
|
+
reportConflicts: false,
|
|
602
|
+
headless: false,
|
|
603
|
+
resumable: false,
|
|
604
|
+
forkable: false,
|
|
605
|
+
backgroundable: false,
|
|
606
|
+
hooks: { supported: false, granularity: "none", canBlock: false },
|
|
607
|
+
sandbox: { supported: false, modes: [] },
|
|
608
|
+
supportsWorktrees: false,
|
|
609
|
+
supportsSkills: false,
|
|
610
|
+
supportsMcp: false,
|
|
611
|
+
memory: { supported: false, backend: "none" },
|
|
612
|
+
maxContextTokens: 0,
|
|
613
|
+
lifecycleEvents: []
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
var TOOL_PROFILES = {
|
|
617
|
+
"claude-code": {
|
|
618
|
+
dispatch: {
|
|
619
|
+
generateCode: false,
|
|
620
|
+
analyzeCode: false,
|
|
621
|
+
applyEdit: false,
|
|
622
|
+
executeCommand: false
|
|
623
|
+
},
|
|
624
|
+
readWorkboard: true,
|
|
625
|
+
writeWorkboard: true,
|
|
626
|
+
claimTasks: true,
|
|
627
|
+
reportConflicts: true,
|
|
628
|
+
headless: true,
|
|
629
|
+
resumable: false,
|
|
630
|
+
forkable: false,
|
|
631
|
+
backgroundable: true,
|
|
632
|
+
hooks: { supported: true, granularity: "all-tools", canBlock: true },
|
|
633
|
+
sandbox: { supported: false, modes: [] },
|
|
634
|
+
supportsWorktrees: true,
|
|
635
|
+
supportsSkills: true,
|
|
636
|
+
supportsMcp: true,
|
|
637
|
+
memory: { supported: false, backend: "none" },
|
|
638
|
+
maxContextTokens: 2e5,
|
|
639
|
+
lifecycleEvents: [
|
|
640
|
+
"session.start",
|
|
641
|
+
"session.stop",
|
|
642
|
+
"prompt.submit",
|
|
643
|
+
"tool.before",
|
|
644
|
+
"tool.after",
|
|
645
|
+
"tool.blocked"
|
|
646
|
+
]
|
|
647
|
+
},
|
|
648
|
+
codex: {
|
|
649
|
+
dispatch: {
|
|
650
|
+
generateCode: false,
|
|
651
|
+
analyzeCode: false,
|
|
652
|
+
applyEdit: false,
|
|
653
|
+
executeCommand: false
|
|
654
|
+
},
|
|
655
|
+
readWorkboard: true,
|
|
656
|
+
writeWorkboard: true,
|
|
657
|
+
claimTasks: true,
|
|
658
|
+
reportConflicts: false,
|
|
659
|
+
headless: true,
|
|
660
|
+
resumable: true,
|
|
661
|
+
forkable: true,
|
|
662
|
+
backgroundable: true,
|
|
663
|
+
hooks: { supported: true, granularity: "bash-only", canBlock: true },
|
|
664
|
+
sandbox: { supported: true, modes: ["read-only", "workspace-write", "full-access"] },
|
|
665
|
+
supportsWorktrees: false,
|
|
666
|
+
supportsSkills: true,
|
|
667
|
+
supportsMcp: true,
|
|
668
|
+
memory: { supported: true, backend: "sqlite" },
|
|
669
|
+
maxContextTokens: 2e5,
|
|
670
|
+
lifecycleEvents: [
|
|
671
|
+
"session.start",
|
|
672
|
+
"session.stop",
|
|
673
|
+
"prompt.submit",
|
|
674
|
+
"tool.before",
|
|
675
|
+
"tool.after",
|
|
676
|
+
"tool.blocked"
|
|
677
|
+
]
|
|
678
|
+
},
|
|
679
|
+
cursor: {
|
|
680
|
+
dispatch: {
|
|
681
|
+
generateCode: false,
|
|
682
|
+
analyzeCode: false,
|
|
683
|
+
applyEdit: false,
|
|
684
|
+
executeCommand: false
|
|
685
|
+
},
|
|
686
|
+
readWorkboard: false,
|
|
687
|
+
writeWorkboard: false,
|
|
688
|
+
claimTasks: false,
|
|
689
|
+
reportConflicts: false,
|
|
690
|
+
headless: false,
|
|
691
|
+
resumable: false,
|
|
692
|
+
forkable: false,
|
|
693
|
+
backgroundable: false,
|
|
694
|
+
hooks: { supported: false, granularity: "none", canBlock: false },
|
|
695
|
+
sandbox: { supported: false, modes: [] },
|
|
696
|
+
supportsWorktrees: false,
|
|
697
|
+
supportsSkills: false,
|
|
698
|
+
supportsMcp: false,
|
|
699
|
+
memory: { supported: false, backend: "none" },
|
|
700
|
+
maxContextTokens: 128e3,
|
|
701
|
+
lifecycleEvents: []
|
|
702
|
+
},
|
|
703
|
+
"revealui-agent": {
|
|
704
|
+
dispatch: {
|
|
705
|
+
generateCode: true,
|
|
706
|
+
analyzeCode: true,
|
|
707
|
+
applyEdit: true,
|
|
708
|
+
executeCommand: true
|
|
709
|
+
},
|
|
710
|
+
readWorkboard: true,
|
|
711
|
+
writeWorkboard: true,
|
|
712
|
+
claimTasks: true,
|
|
713
|
+
reportConflicts: true,
|
|
714
|
+
headless: true,
|
|
715
|
+
resumable: false,
|
|
716
|
+
forkable: false,
|
|
717
|
+
backgroundable: true,
|
|
718
|
+
hooks: { supported: true, granularity: "all-tools", canBlock: true },
|
|
719
|
+
sandbox: { supported: false, modes: [] },
|
|
720
|
+
supportsWorktrees: true,
|
|
721
|
+
supportsSkills: true,
|
|
722
|
+
supportsMcp: true,
|
|
723
|
+
memory: { supported: true, backend: "crdt" },
|
|
724
|
+
maxContextTokens: 2e5,
|
|
725
|
+
lifecycleEvents: [
|
|
726
|
+
"session.start",
|
|
727
|
+
"session.stop",
|
|
728
|
+
"session.crash",
|
|
729
|
+
"prompt.submit",
|
|
730
|
+
"tool.before",
|
|
731
|
+
"tool.after",
|
|
732
|
+
"tool.blocked",
|
|
733
|
+
"task.claimed",
|
|
734
|
+
"task.completed",
|
|
735
|
+
"agent.heartbeat"
|
|
736
|
+
]
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
// src/vaughn/degradation-strategies.ts
|
|
741
|
+
var DEGRADATION_TABLE = {
|
|
742
|
+
"claude-code": {
|
|
743
|
+
"session.crash": "polyfill",
|
|
744
|
+
"task.claimed": "polyfill",
|
|
745
|
+
"task.completed": "polyfill",
|
|
746
|
+
"agent.heartbeat": "polyfill"
|
|
747
|
+
},
|
|
748
|
+
codex: {
|
|
749
|
+
"session.crash": "polyfill",
|
|
750
|
+
"task.claimed": "polyfill",
|
|
751
|
+
"task.completed": "polyfill",
|
|
752
|
+
"agent.heartbeat": "polyfill"
|
|
753
|
+
},
|
|
754
|
+
cursor: {
|
|
755
|
+
"session.start": "absent",
|
|
756
|
+
"session.stop": "absent",
|
|
757
|
+
"session.crash": "polyfill",
|
|
758
|
+
"prompt.submit": "absent",
|
|
759
|
+
"tool.before": "absent",
|
|
760
|
+
"tool.after": "absent",
|
|
761
|
+
"tool.blocked": "absent",
|
|
762
|
+
"task.claimed": "absent",
|
|
763
|
+
"task.completed": "absent",
|
|
764
|
+
"agent.heartbeat": "polyfill"
|
|
765
|
+
},
|
|
766
|
+
"revealui-agent": {
|
|
767
|
+
// RevealUI Agent natively supports all 10 events; no degradation needed.
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
function getDegradationStrategy(toolName, event) {
|
|
771
|
+
const toolDegradations = DEGRADATION_TABLE[toolName];
|
|
772
|
+
if (!toolDegradations) return "absent";
|
|
773
|
+
return toolDegradations[event];
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/vaughn/event-envelope.ts
|
|
777
|
+
import { z } from "zod";
|
|
778
|
+
var VAUGHN_VERSION = "0.1.0";
|
|
779
|
+
var VAUGHN_EVENTS = [
|
|
780
|
+
"session.start",
|
|
781
|
+
"session.stop",
|
|
782
|
+
"session.crash",
|
|
783
|
+
"prompt.submit",
|
|
784
|
+
"tool.before",
|
|
785
|
+
"tool.after",
|
|
786
|
+
"tool.blocked",
|
|
787
|
+
"task.claimed",
|
|
788
|
+
"task.completed",
|
|
789
|
+
"agent.heartbeat"
|
|
790
|
+
];
|
|
791
|
+
var vaughnEventSchema = z.enum([
|
|
792
|
+
"session.start",
|
|
793
|
+
"session.stop",
|
|
794
|
+
"session.crash",
|
|
795
|
+
"prompt.submit",
|
|
796
|
+
"tool.before",
|
|
797
|
+
"tool.after",
|
|
798
|
+
"tool.blocked",
|
|
799
|
+
"task.claimed",
|
|
800
|
+
"task.completed",
|
|
801
|
+
"agent.heartbeat"
|
|
802
|
+
]);
|
|
803
|
+
var vaughnEventEnvelopeSchema = z.object({
|
|
804
|
+
version: z.literal(VAUGHN_VERSION),
|
|
805
|
+
event: vaughnEventSchema,
|
|
806
|
+
timestamp: z.string().min(1),
|
|
807
|
+
agentId: z.string().min(1),
|
|
808
|
+
toolName: z.string().min(1),
|
|
809
|
+
sessionId: z.string().min(1),
|
|
810
|
+
payload: z.record(z.string(), z.unknown())
|
|
811
|
+
});
|
|
812
|
+
function createEventEnvelope(event, agentId, toolName, sessionId, payload = {}) {
|
|
813
|
+
return {
|
|
814
|
+
version: VAUGHN_VERSION,
|
|
815
|
+
event,
|
|
816
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
817
|
+
agentId,
|
|
818
|
+
toolName,
|
|
819
|
+
sessionId,
|
|
820
|
+
payload
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/vaughn/event-normalizer.ts
|
|
825
|
+
var VaughnEventNormalizer = class {
|
|
826
|
+
constructor(toolName, agentId, sessionId) {
|
|
827
|
+
this.toolName = toolName;
|
|
828
|
+
this.agentId = agentId;
|
|
829
|
+
this.sessionId = sessionId;
|
|
830
|
+
}
|
|
831
|
+
toolName;
|
|
832
|
+
agentId;
|
|
833
|
+
sessionId;
|
|
834
|
+
/**
|
|
835
|
+
* Map a HarnessEvent type to its canonical VAUGHN event.
|
|
836
|
+
* Returns null if the event has no VAUGHN mapping.
|
|
837
|
+
*/
|
|
838
|
+
mapEventType(event) {
|
|
839
|
+
switch (event.type) {
|
|
840
|
+
case "harness-connected":
|
|
841
|
+
return "session.start";
|
|
842
|
+
case "harness-disconnected":
|
|
843
|
+
return "session.stop";
|
|
844
|
+
case "generation-started":
|
|
845
|
+
return "tool.before";
|
|
846
|
+
case "generation-completed":
|
|
847
|
+
return "tool.after";
|
|
848
|
+
case "error":
|
|
849
|
+
return "session.crash";
|
|
850
|
+
default:
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
/** Extract event-specific payload fields. */
|
|
855
|
+
extractPayload(event) {
|
|
856
|
+
switch (event.type) {
|
|
857
|
+
case "harness-connected":
|
|
858
|
+
return { harnessId: event.harnessId };
|
|
859
|
+
case "harness-disconnected":
|
|
860
|
+
return { harnessId: event.harnessId };
|
|
861
|
+
case "generation-started":
|
|
862
|
+
return { taskId: event.taskId };
|
|
863
|
+
case "generation-completed":
|
|
864
|
+
return { taskId: event.taskId, output: event.output };
|
|
865
|
+
case "error":
|
|
866
|
+
return { harnessId: event.harnessId, message: event.message };
|
|
867
|
+
default:
|
|
868
|
+
return {};
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Normalize a HarnessEvent into a VaughnEventEnvelope.
|
|
873
|
+
*
|
|
874
|
+
* Returns null if:
|
|
875
|
+
* - The event has no VAUGHN mapping
|
|
876
|
+
* - The degradation strategy for this tool/event is 'absent'
|
|
877
|
+
*/
|
|
878
|
+
normalize(event) {
|
|
879
|
+
const vaughnEvent = this.mapEventType(event);
|
|
880
|
+
if (!vaughnEvent) return null;
|
|
881
|
+
const degradation = getDegradationStrategy(this.toolName, vaughnEvent);
|
|
882
|
+
if (degradation === "absent") return null;
|
|
883
|
+
const payload = this.extractPayload(event);
|
|
884
|
+
if (degradation) {
|
|
885
|
+
payload.degraded = true;
|
|
886
|
+
payload.degradationStrategy = degradation;
|
|
887
|
+
}
|
|
888
|
+
const envelope = createEventEnvelope(
|
|
889
|
+
vaughnEvent,
|
|
890
|
+
this.agentId,
|
|
891
|
+
this.toolName,
|
|
892
|
+
this.sessionId,
|
|
893
|
+
payload
|
|
894
|
+
);
|
|
895
|
+
return { envelope, degradation };
|
|
896
|
+
}
|
|
897
|
+
/** Convenience: normalize and return just the envelope (or null). */
|
|
898
|
+
normalizeToEnvelope(event) {
|
|
899
|
+
return this.normalize(event)?.envelope ?? null;
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
// src/adapters/revealui-agent-adapter.ts
|
|
904
|
+
var DEFAULT_CONFIG = {
|
|
905
|
+
projectRoot: process.cwd(),
|
|
906
|
+
maxIterations: 10,
|
|
907
|
+
timeoutMs: 12e4
|
|
908
|
+
};
|
|
909
|
+
var RevealUIAgentAdapter = class {
|
|
910
|
+
id = "revealui-agent";
|
|
911
|
+
name = "RevealUI Agent";
|
|
912
|
+
config;
|
|
913
|
+
eventHandlers = /* @__PURE__ */ new Set();
|
|
914
|
+
vaughnEventHandlers = /* @__PURE__ */ new Set();
|
|
915
|
+
vaughnNormalizer = null;
|
|
916
|
+
constructor(config) {
|
|
917
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
918
|
+
}
|
|
919
|
+
getCapabilities() {
|
|
920
|
+
return {
|
|
921
|
+
generateCode: true,
|
|
922
|
+
analyzeCode: true,
|
|
923
|
+
applyEdit: true,
|
|
924
|
+
applyConfig: false,
|
|
925
|
+
readWorkboard: (this.config.workboardPath ?? process.env.REVEALUI_WORKBOARD_PATH) !== void 0,
|
|
926
|
+
writeWorkboard: (this.config.workboardPath ?? process.env.REVEALUI_WORKBOARD_PATH) !== void 0
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
async getInfo() {
|
|
930
|
+
return {
|
|
931
|
+
id: this.id,
|
|
932
|
+
name: this.name,
|
|
933
|
+
version: "0.1.0",
|
|
934
|
+
capabilities: this.getCapabilities()
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
/** Get the VAUGHN capability profile for this adapter. */
|
|
938
|
+
getVaughnCapabilities() {
|
|
939
|
+
return TOOL_PROFILES["revealui-agent"];
|
|
940
|
+
}
|
|
941
|
+
/** Subscribe to VAUGHN-normalized events. */
|
|
942
|
+
onVaughnEvent(handler) {
|
|
943
|
+
this.vaughnEventHandlers.add(handler);
|
|
944
|
+
if (!this.vaughnNormalizer) {
|
|
945
|
+
this.vaughnNormalizer = new VaughnEventNormalizer(
|
|
946
|
+
"revealui-agent",
|
|
947
|
+
this.id,
|
|
948
|
+
`session-${Date.now()}`
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
return () => this.vaughnEventHandlers.delete(handler);
|
|
952
|
+
}
|
|
953
|
+
/** Generate tool-native config files from canonical VAUGHN config (stub). */
|
|
954
|
+
async generateConfig(_config) {
|
|
955
|
+
return { files: /* @__PURE__ */ new Map() };
|
|
956
|
+
}
|
|
957
|
+
/** Read tool-native config into canonical form (stub). */
|
|
958
|
+
async readConfig() {
|
|
959
|
+
return {};
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* The RevealUI agent is available if at least one LLM provider is reachable.
|
|
963
|
+
* Checks in order: Ollama (localhost), Groq (API key).
|
|
964
|
+
*/
|
|
965
|
+
async isAvailable() {
|
|
966
|
+
const ollamaUrl = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434";
|
|
967
|
+
try {
|
|
968
|
+
const res = await fetch(`${ollamaUrl}/api/tags`, {
|
|
969
|
+
signal: AbortSignal.timeout(2e3)
|
|
970
|
+
});
|
|
971
|
+
if (res.ok) return true;
|
|
972
|
+
} catch {
|
|
973
|
+
}
|
|
974
|
+
if (process.env.GROQ_API_KEY) return true;
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
notifyRegistered() {
|
|
978
|
+
this.emit({ type: "harness-connected", harnessId: this.id });
|
|
979
|
+
}
|
|
980
|
+
notifyUnregistering() {
|
|
981
|
+
this.emit({ type: "harness-disconnected", harnessId: this.id });
|
|
982
|
+
}
|
|
983
|
+
async execute(command) {
|
|
984
|
+
try {
|
|
985
|
+
return await this.executeInner(command);
|
|
986
|
+
} catch (err) {
|
|
987
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
988
|
+
this.emit({ type: "error", harnessId: this.id, message });
|
|
989
|
+
return { success: false, command: command.type, message };
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
async executeInner(command) {
|
|
993
|
+
switch (command.type) {
|
|
994
|
+
case "get-status": {
|
|
995
|
+
const available = await this.isAvailable();
|
|
996
|
+
return {
|
|
997
|
+
success: true,
|
|
998
|
+
command: command.type,
|
|
999
|
+
data: {
|
|
1000
|
+
available,
|
|
1001
|
+
provider: this.config.provider ?? "auto",
|
|
1002
|
+
model: this.config.model ?? "auto",
|
|
1003
|
+
projectRoot: this.config.projectRoot
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
case "headless-prompt": {
|
|
1008
|
+
return this.runHeadlessPrompt(command.prompt, command.maxTurns, command.timeoutMs);
|
|
1009
|
+
}
|
|
1010
|
+
case "generate-code": {
|
|
1011
|
+
return this.runHeadlessPrompt(
|
|
1012
|
+
`Generate code: ${command.prompt}${command.language ? ` (language: ${command.language})` : ""}${command.context ? `
|
|
1013
|
+
|
|
1014
|
+
Context:
|
|
1015
|
+
${command.context}` : ""}`
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
case "analyze-code": {
|
|
1019
|
+
const question = command.question ?? "Analyze this file and explain what it does.";
|
|
1020
|
+
return this.runHeadlessPrompt(
|
|
1021
|
+
`Read the file at ${command.filePath} and answer: ${question}`
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
case "apply-edit": {
|
|
1025
|
+
return this.runHeadlessPrompt(
|
|
1026
|
+
`Apply the following diff to ${command.filePath}:
|
|
1027
|
+
|
|
1028
|
+
${command.diff}`
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
case "apply-config":
|
|
1032
|
+
case "sync-config":
|
|
1033
|
+
case "diff-config": {
|
|
1034
|
+
return {
|
|
1035
|
+
success: false,
|
|
1036
|
+
command: command.type,
|
|
1037
|
+
message: "Config sync is not applicable - RevealUI agent uses the content layer directly"
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
case "read-workboard":
|
|
1041
|
+
case "update-workboard": {
|
|
1042
|
+
return {
|
|
1043
|
+
success: false,
|
|
1044
|
+
command: command.type,
|
|
1045
|
+
message: "Workboard support not yet wired - use WorkboardManager directly"
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
default: {
|
|
1049
|
+
return {
|
|
1050
|
+
success: false,
|
|
1051
|
+
command: command.type,
|
|
1052
|
+
message: `Command not supported by ${this.name}`
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Run a headless prompt through the coding agent.
|
|
1059
|
+
* Lazy-imports @revealui/ai to avoid hard dependency at module load time.
|
|
1060
|
+
* Types are inferred from the dynamic imports - no compile-time @revealui/ai dependency.
|
|
1061
|
+
*/
|
|
1062
|
+
async runHeadlessPrompt(prompt, maxTurns, timeoutMs) {
|
|
1063
|
+
const aiRuntimePath = "@revealui/ai/orchestration/streaming-runtime";
|
|
1064
|
+
const aiClientPath = "@revealui/ai/llm/client";
|
|
1065
|
+
const aiToolsPath = "@revealui/ai/tools/coding";
|
|
1066
|
+
let runtimeMod;
|
|
1067
|
+
let clientMod;
|
|
1068
|
+
let toolsMod;
|
|
1069
|
+
try {
|
|
1070
|
+
[runtimeMod, clientMod, toolsMod] = await Promise.all([
|
|
1071
|
+
import(aiRuntimePath),
|
|
1072
|
+
import(aiClientPath),
|
|
1073
|
+
import(aiToolsPath)
|
|
1074
|
+
]);
|
|
1075
|
+
} catch {
|
|
1076
|
+
return {
|
|
1077
|
+
success: false,
|
|
1078
|
+
command: "headless-prompt",
|
|
1079
|
+
message: "@revealui/ai is not installed. Install it to use the RevealUI agent: npm install @revealui/ai"
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
const StreamingAgentRuntime = runtimeMod.StreamingAgentRuntime;
|
|
1083
|
+
const createCodingTools = toolsMod.createCodingTools;
|
|
1084
|
+
const projectRoot = this.config.projectRoot ?? process.cwd();
|
|
1085
|
+
const tools = createCodingTools({ projectRoot });
|
|
1086
|
+
let llmClient;
|
|
1087
|
+
if (this.config.provider) {
|
|
1088
|
+
const LLMClient = clientMod.LLMClient;
|
|
1089
|
+
llmClient = new LLMClient({
|
|
1090
|
+
provider: this.config.provider,
|
|
1091
|
+
model: this.config.model,
|
|
1092
|
+
baseURL: process.env.INFERENCE_SNAPS_BASE_URL ?? process.env.OLLAMA_BASE_URL,
|
|
1093
|
+
apiKey: process.env.GROQ_API_KEY ?? "not-needed"
|
|
1094
|
+
});
|
|
1095
|
+
} else {
|
|
1096
|
+
const createLLMClientFromEnv = clientMod.createLLMClientFromEnv;
|
|
1097
|
+
llmClient = createLLMClientFromEnv();
|
|
1098
|
+
}
|
|
1099
|
+
const agent = {
|
|
1100
|
+
id: "revealui-coding-agent",
|
|
1101
|
+
name: "RevealUI Coding Agent",
|
|
1102
|
+
instructions: this.buildInstructions(),
|
|
1103
|
+
tools,
|
|
1104
|
+
config: {},
|
|
1105
|
+
getContext: () => ({ projectRoot, workingDirectory: projectRoot })
|
|
1106
|
+
};
|
|
1107
|
+
const task = {
|
|
1108
|
+
id: `task-${Date.now()}`,
|
|
1109
|
+
type: "headless-prompt",
|
|
1110
|
+
description: prompt
|
|
1111
|
+
};
|
|
1112
|
+
const runtime = new StreamingAgentRuntime({
|
|
1113
|
+
maxIterations: maxTurns ?? this.config.maxIterations ?? DEFAULT_CONFIG.maxIterations,
|
|
1114
|
+
timeout: timeoutMs ?? this.config.timeoutMs ?? DEFAULT_CONFIG.timeoutMs
|
|
1115
|
+
});
|
|
1116
|
+
const taskId = task.id;
|
|
1117
|
+
this.emit({ type: "generation-started", taskId });
|
|
1118
|
+
const outputParts = [];
|
|
1119
|
+
try {
|
|
1120
|
+
for await (const chunk of runtime.streamTask(agent, task, llmClient)) {
|
|
1121
|
+
switch (chunk.type) {
|
|
1122
|
+
case "text":
|
|
1123
|
+
if (chunk.content) outputParts.push(chunk.content);
|
|
1124
|
+
break;
|
|
1125
|
+
case "tool_call_result":
|
|
1126
|
+
if (chunk.toolResult?.content) {
|
|
1127
|
+
outputParts.push(`[tool: ${chunk.toolCall?.name}] ${chunk.toolResult.content}`);
|
|
1128
|
+
}
|
|
1129
|
+
break;
|
|
1130
|
+
case "error":
|
|
1131
|
+
if (chunk.error) outputParts.push(`[error] ${chunk.error}`);
|
|
1132
|
+
break;
|
|
1133
|
+
case "done":
|
|
1134
|
+
break;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
} finally {
|
|
1138
|
+
await runtime.cleanup();
|
|
1139
|
+
}
|
|
1140
|
+
const output = outputParts.join("\n");
|
|
1141
|
+
this.emit({ type: "generation-completed", taskId, output });
|
|
1142
|
+
return {
|
|
1143
|
+
success: true,
|
|
1144
|
+
command: "headless-prompt",
|
|
1145
|
+
message: output,
|
|
1146
|
+
data: { taskId, output }
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Build system instructions from the content layer.
|
|
1151
|
+
* Loads rules from .claude/rules/ as a baseline (they are the canonical content).
|
|
1152
|
+
*/
|
|
1153
|
+
buildInstructions() {
|
|
1154
|
+
const lines = [
|
|
1155
|
+
"You are the RevealUI coding agent. You help with software development tasks in this project.",
|
|
1156
|
+
"Use the available tools to read, write, edit, search, and execute commands.",
|
|
1157
|
+
"Always read files before modifying them. Prefer surgical edits over full rewrites.",
|
|
1158
|
+
"Follow project conventions discovered via the project_context tool.",
|
|
1159
|
+
""
|
|
1160
|
+
];
|
|
1161
|
+
try {
|
|
1162
|
+
const { readdirSync, readFileSync: readFileSync2 } = __require("fs");
|
|
1163
|
+
const { join: join5 } = __require("path");
|
|
1164
|
+
const projectRoot = this.config.projectRoot ?? process.cwd();
|
|
1165
|
+
const rulesDir = join5(projectRoot, ".claude", "rules");
|
|
1166
|
+
const ruleFiles = readdirSync(rulesDir);
|
|
1167
|
+
for (const file of ruleFiles) {
|
|
1168
|
+
if (file.endsWith(".md")) {
|
|
1169
|
+
const content = readFileSync2(join5(rulesDir, file), "utf8");
|
|
1170
|
+
lines.push(`## ${file.replace(".md", "")}`, content, "");
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
} catch {
|
|
1174
|
+
}
|
|
1175
|
+
return lines.join("\n");
|
|
1176
|
+
}
|
|
1177
|
+
onEvent(handler) {
|
|
1178
|
+
this.eventHandlers.add(handler);
|
|
1179
|
+
return () => this.eventHandlers.delete(handler);
|
|
1180
|
+
}
|
|
1181
|
+
async dispose() {
|
|
1182
|
+
this.eventHandlers.clear();
|
|
1183
|
+
this.vaughnEventHandlers.clear();
|
|
1184
|
+
this.vaughnNormalizer = null;
|
|
1185
|
+
}
|
|
1186
|
+
emit(event) {
|
|
1187
|
+
for (const handler of this.eventHandlers) {
|
|
1188
|
+
try {
|
|
1189
|
+
handler(event);
|
|
1190
|
+
} catch {
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (this.vaughnNormalizer && this.vaughnEventHandlers.size > 0) {
|
|
1194
|
+
const envelope = this.vaughnNormalizer.normalizeToEnvelope(event);
|
|
1195
|
+
if (envelope) {
|
|
1196
|
+
for (const handler of this.vaughnEventHandlers) {
|
|
1197
|
+
try {
|
|
1198
|
+
handler(envelope);
|
|
1199
|
+
} catch {
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
// src/detection/auto-detector.ts
|
|
1208
|
+
async function autoDetectHarnesses(registry) {
|
|
1209
|
+
const candidates = [new RevealUIAgentAdapter()];
|
|
1210
|
+
const registered = [];
|
|
1211
|
+
await Promise.all(
|
|
1212
|
+
candidates.map(async (adapter) => {
|
|
1213
|
+
try {
|
|
1214
|
+
if (await adapter.isAvailable()) {
|
|
1215
|
+
registry.register(adapter);
|
|
1216
|
+
registered.push(adapter.id);
|
|
1217
|
+
} else {
|
|
1218
|
+
await adapter.dispose();
|
|
1219
|
+
}
|
|
1220
|
+
} catch {
|
|
1221
|
+
await adapter.dispose();
|
|
1222
|
+
}
|
|
1223
|
+
})
|
|
1224
|
+
);
|
|
1225
|
+
return registered;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// src/registry/harness-registry.ts
|
|
1229
|
+
var HarnessRegistry = class {
|
|
1230
|
+
adapters = /* @__PURE__ */ new Map();
|
|
1231
|
+
/** Register an adapter. Throws if an adapter with the same id already exists. */
|
|
1232
|
+
register(adapter) {
|
|
1233
|
+
if (this.adapters.has(adapter.id)) {
|
|
1234
|
+
throw new Error(`Harness adapter already registered: ${adapter.id}`);
|
|
1235
|
+
}
|
|
1236
|
+
this.adapters.set(adapter.id, adapter);
|
|
1237
|
+
adapter.notifyRegistered?.();
|
|
1238
|
+
}
|
|
1239
|
+
/** Unregister an adapter, disposing it in the process. */
|
|
1240
|
+
async unregister(id) {
|
|
1241
|
+
const adapter = this.adapters.get(id);
|
|
1242
|
+
if (adapter) {
|
|
1243
|
+
adapter.notifyUnregistering?.();
|
|
1244
|
+
await adapter.dispose();
|
|
1245
|
+
this.adapters.delete(id);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
/** Retrieve an adapter by id. */
|
|
1249
|
+
get(id) {
|
|
1250
|
+
return this.adapters.get(id);
|
|
1251
|
+
}
|
|
1252
|
+
/** List all registered adapter ids. */
|
|
1253
|
+
listAll() {
|
|
1254
|
+
return Array.from(this.adapters.keys());
|
|
1255
|
+
}
|
|
1256
|
+
/** List ids of adapters that report isAvailable() === true. */
|
|
1257
|
+
async listAvailable() {
|
|
1258
|
+
const results = await Promise.all(
|
|
1259
|
+
Array.from(this.adapters.entries()).map(async ([id, adapter]) => ({
|
|
1260
|
+
id,
|
|
1261
|
+
available: await adapter.isAvailable()
|
|
1262
|
+
}))
|
|
1263
|
+
);
|
|
1264
|
+
return results.filter((r) => r.available).map((r) => r.id);
|
|
1265
|
+
}
|
|
1266
|
+
/** Dispose all adapters and clear the registry. */
|
|
1267
|
+
async disposeAll() {
|
|
1268
|
+
await Promise.all(Array.from(this.adapters.values()).map((a) => a.dispose()));
|
|
1269
|
+
this.adapters.clear();
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
// src/server/http-gateway.ts
|
|
1274
|
+
import { randomBytes } from "crypto";
|
|
1275
|
+
import { createReadStream, existsSync as existsSync2, statSync } from "fs";
|
|
1276
|
+
import { createServer } from "http";
|
|
1277
|
+
import { extname, join as join2, normalize } from "path";
|
|
1278
|
+
var MIME_TYPES = {
|
|
1279
|
+
".html": "text/html; charset=utf-8",
|
|
1280
|
+
".js": "application/javascript; charset=utf-8",
|
|
1281
|
+
".css": "text/css; charset=utf-8",
|
|
1282
|
+
".json": "application/json; charset=utf-8",
|
|
1283
|
+
".png": "image/png",
|
|
1284
|
+
".jpg": "image/jpeg",
|
|
1285
|
+
".jpeg": "image/jpeg",
|
|
1286
|
+
".svg": "image/svg+xml",
|
|
1287
|
+
".ico": "image/x-icon",
|
|
1288
|
+
".woff": "font/woff",
|
|
1289
|
+
".woff2": "font/woff2",
|
|
1290
|
+
".ttf": "font/ttf",
|
|
1291
|
+
".wasm": "application/wasm"
|
|
1292
|
+
};
|
|
1293
|
+
var HttpGateway = class {
|
|
1294
|
+
server;
|
|
1295
|
+
config;
|
|
1296
|
+
/** 6-digit pairing code (regenerated on each start) */
|
|
1297
|
+
pairingCode;
|
|
1298
|
+
/** Active session tokens (bearer tokens granted after pairing) */
|
|
1299
|
+
sessionTokens = /* @__PURE__ */ new Set();
|
|
1300
|
+
/** Whether pairing has been completed at least once */
|
|
1301
|
+
paired = false;
|
|
1302
|
+
constructor(config) {
|
|
1303
|
+
this.config = config;
|
|
1304
|
+
this.pairingCode = generatePairingCode();
|
|
1305
|
+
this.server = createServer((req, res) => this.handleRequest(req, res));
|
|
1306
|
+
}
|
|
1307
|
+
/** The current pairing code (display this in Studio/terminal) */
|
|
1308
|
+
getPairingCode() {
|
|
1309
|
+
return this.pairingCode;
|
|
1310
|
+
}
|
|
1311
|
+
/** Regenerate the pairing code (invalidates previous code) */
|
|
1312
|
+
regeneratePairingCode() {
|
|
1313
|
+
this.pairingCode = generatePairingCode();
|
|
1314
|
+
return this.pairingCode;
|
|
1315
|
+
}
|
|
1316
|
+
async start() {
|
|
1317
|
+
return new Promise((resolve, reject) => {
|
|
1318
|
+
this.server.listen(this.config.port, this.config.host, () => resolve());
|
|
1319
|
+
this.server.once("error", reject);
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
async stop() {
|
|
1323
|
+
return new Promise((resolve) => this.server.close(() => resolve()));
|
|
1324
|
+
}
|
|
1325
|
+
handleRequest(req, res) {
|
|
1326
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
1327
|
+
const path = url.pathname;
|
|
1328
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1329
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1330
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
1331
|
+
if (req.method === "OPTIONS") {
|
|
1332
|
+
res.writeHead(204);
|
|
1333
|
+
res.end();
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
if (path === "/api/pair" && req.method === "POST") {
|
|
1337
|
+
this.handlePair(req, res);
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (path === "/rpc" || path.startsWith("/api/")) {
|
|
1341
|
+
if (!this.checkAuth(req, res)) return;
|
|
1342
|
+
if (path === "/rpc" && req.method === "POST") {
|
|
1343
|
+
this.handleRpc(req, res);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
if (path === "/api/pair" && req.method === "GET") {
|
|
1347
|
+
this.handlePairStatus(res);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
if (path === "/api/status") {
|
|
1351
|
+
this.handleStatus(res);
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
if (path.startsWith("/api/stream") && req.method === "GET") {
|
|
1355
|
+
const sessionFilter = path.split("/")[3] ?? null;
|
|
1356
|
+
this.handleStream(req, res, sessionFilter);
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
jsonResponse(res, 404, { error: "Not found" });
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
this.handleStatic(path, res);
|
|
1363
|
+
}
|
|
1364
|
+
/** Verify the Authorization: Bearer <token> header */
|
|
1365
|
+
checkAuth(req, res) {
|
|
1366
|
+
if (this.sessionTokens.size === 0 && !this.paired) {
|
|
1367
|
+
return true;
|
|
1368
|
+
}
|
|
1369
|
+
const authHeader = req.headers.authorization ?? "";
|
|
1370
|
+
if (!authHeader.startsWith("Bearer ")) {
|
|
1371
|
+
jsonResponse(res, 401, { error: "Authorization required", paired: this.paired });
|
|
1372
|
+
return false;
|
|
1373
|
+
}
|
|
1374
|
+
const token = authHeader.slice(7);
|
|
1375
|
+
if (!this.sessionTokens.has(token)) {
|
|
1376
|
+
jsonResponse(res, 403, { error: "Invalid token" });
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
return true;
|
|
1380
|
+
}
|
|
1381
|
+
/** POST /api/pair - submit pairing code, receive session token */
|
|
1382
|
+
handlePair(req, res) {
|
|
1383
|
+
let body = "";
|
|
1384
|
+
req.on("data", (chunk) => {
|
|
1385
|
+
body += chunk.toString();
|
|
1386
|
+
if (body.length > 1024) {
|
|
1387
|
+
res.writeHead(413);
|
|
1388
|
+
res.end();
|
|
1389
|
+
req.destroy();
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
req.on("end", () => {
|
|
1393
|
+
try {
|
|
1394
|
+
const { code } = JSON.parse(body);
|
|
1395
|
+
if (code !== this.pairingCode) {
|
|
1396
|
+
jsonResponse(res, 403, { error: "Invalid pairing code" });
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
const token = randomBytes(32).toString("hex");
|
|
1400
|
+
this.sessionTokens.add(token);
|
|
1401
|
+
this.paired = true;
|
|
1402
|
+
this.pairingCode = generatePairingCode();
|
|
1403
|
+
jsonResponse(res, 200, { token, expires: null });
|
|
1404
|
+
} catch {
|
|
1405
|
+
jsonResponse(res, 400, { error: "Invalid JSON" });
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
/** GET /api/pair - check pairing status */
|
|
1410
|
+
handlePairStatus(res) {
|
|
1411
|
+
jsonResponse(res, 200, {
|
|
1412
|
+
paired: this.paired,
|
|
1413
|
+
activeSessions: this.sessionTokens.size
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
/** GET /api/status - daemon status summary */
|
|
1417
|
+
handleStatus(res) {
|
|
1418
|
+
jsonResponse(res, 200, {
|
|
1419
|
+
daemon: "revdev-harness",
|
|
1420
|
+
pid: process.pid,
|
|
1421
|
+
uptime: process.uptime(),
|
|
1422
|
+
paired: this.paired,
|
|
1423
|
+
activeSessions: this.sessionTokens.size
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
/** POST /rpc - proxy JSON-RPC to the daemon's dispatch */
|
|
1427
|
+
handleRpc(req, res) {
|
|
1428
|
+
let body = "";
|
|
1429
|
+
req.on("data", (chunk) => {
|
|
1430
|
+
body += chunk.toString();
|
|
1431
|
+
if (body.length > 1048576) {
|
|
1432
|
+
res.writeHead(413);
|
|
1433
|
+
res.end();
|
|
1434
|
+
req.destroy();
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
req.on("end", () => {
|
|
1438
|
+
this.config.rpcDispatch.dispatchHttp(body, (response) => {
|
|
1439
|
+
jsonResponse(res, 200, response);
|
|
1440
|
+
});
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
/** GET /api/stream[/:sessionId] - SSE for agent output and exit events */
|
|
1444
|
+
handleStream(req, res, sessionFilter) {
|
|
1445
|
+
const spawner = this.config.spawner;
|
|
1446
|
+
if (!spawner) {
|
|
1447
|
+
jsonResponse(res, 503, { error: "Spawner not available" });
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
res.writeHead(200, {
|
|
1451
|
+
"Content-Type": "text/event-stream",
|
|
1452
|
+
"Cache-Control": "no-cache",
|
|
1453
|
+
Connection: "keep-alive"
|
|
1454
|
+
});
|
|
1455
|
+
res.write(": connected\n\n");
|
|
1456
|
+
const onOutput = (evt) => {
|
|
1457
|
+
if (sessionFilter && evt.sessionId !== sessionFilter) return;
|
|
1458
|
+
res.write(`event: output
|
|
1459
|
+
data: ${JSON.stringify(evt)}
|
|
1460
|
+
|
|
1461
|
+
`);
|
|
1462
|
+
};
|
|
1463
|
+
const onExit = (evt) => {
|
|
1464
|
+
if (sessionFilter && evt.sessionId !== sessionFilter) return;
|
|
1465
|
+
res.write(`event: exit
|
|
1466
|
+
data: ${JSON.stringify(evt)}
|
|
1467
|
+
|
|
1468
|
+
`);
|
|
1469
|
+
};
|
|
1470
|
+
spawner.on("output", onOutput);
|
|
1471
|
+
spawner.on("exit", onExit);
|
|
1472
|
+
const keepalive = setInterval(() => {
|
|
1473
|
+
res.write(": keepalive\n\n");
|
|
1474
|
+
}, 3e4);
|
|
1475
|
+
req.on("close", () => {
|
|
1476
|
+
clearInterval(keepalive);
|
|
1477
|
+
spawner.off("output", onOutput);
|
|
1478
|
+
spawner.off("exit", onExit);
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
/** Serve static files from the Studio build directory */
|
|
1482
|
+
handleStatic(urlPath, res) {
|
|
1483
|
+
if (!this.config.staticDir) {
|
|
1484
|
+
jsonResponse(res, 404, { error: "Static serving not configured" });
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
const filePath = urlPath === "/" ? "/index.html" : urlPath;
|
|
1488
|
+
const normalized = normalize(filePath);
|
|
1489
|
+
if (normalized.includes("..")) {
|
|
1490
|
+
res.writeHead(400);
|
|
1491
|
+
res.end("Bad request");
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
const fullPath = join2(this.config.staticDir, normalized);
|
|
1495
|
+
const targetPath = existsSync2(fullPath) && statSync(fullPath).isFile() ? fullPath : join2(this.config.staticDir, "index.html");
|
|
1496
|
+
if (!existsSync2(targetPath)) {
|
|
1497
|
+
res.writeHead(404);
|
|
1498
|
+
res.end("Not found");
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
const ext = extname(targetPath);
|
|
1502
|
+
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
1503
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
1504
|
+
createReadStream(targetPath).pipe(res);
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
function generatePairingCode() {
|
|
1508
|
+
const max = Math.floor(4294967296 / 1e6) * 1e6;
|
|
1509
|
+
let num;
|
|
1510
|
+
do {
|
|
1511
|
+
const bytes = randomBytes(4);
|
|
1512
|
+
num = bytes.readUInt32BE(0);
|
|
1513
|
+
} while (num >= max);
|
|
1514
|
+
return (num % 1e6).toString().padStart(6, "0");
|
|
1515
|
+
}
|
|
1516
|
+
function jsonResponse(res, status, body) {
|
|
1517
|
+
const json = JSON.stringify(body);
|
|
1518
|
+
res.writeHead(status, {
|
|
1519
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
1520
|
+
"Content-Length": Buffer.byteLength(json)
|
|
1521
|
+
});
|
|
1522
|
+
res.end(json);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// src/server/inference-service.ts
|
|
1526
|
+
import { execFile as execFile2 } from "child_process";
|
|
1527
|
+
import { promisify as promisify2 } from "util";
|
|
1528
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
1529
|
+
var KNOWN_SNAPS = [
|
|
1530
|
+
["nemotron-3-nano", "General (reasoning + non-reasoning) - free tier default"],
|
|
1531
|
+
["gemma3", "General + vision - image understanding, multimodal"],
|
|
1532
|
+
["deepseek-r1", "Reasoning - complex analysis, chain-of-thought"],
|
|
1533
|
+
["qwen-vl", "Vision-language - document parsing, visual Q&A"]
|
|
1534
|
+
];
|
|
1535
|
+
async function commandExists(cmd) {
|
|
1536
|
+
try {
|
|
1537
|
+
await execFileAsync2("which", [cmd]);
|
|
1538
|
+
return true;
|
|
1539
|
+
} catch {
|
|
1540
|
+
return false;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
var ALLOWED_COMMANDS = /* @__PURE__ */ new Set(["ollama", "snap", "pkill", "which"]);
|
|
1544
|
+
async function run(cmd, args) {
|
|
1545
|
+
if (!ALLOWED_COMMANDS.has(cmd)) {
|
|
1546
|
+
throw new Error(`Command not allowed: ${cmd}`);
|
|
1547
|
+
}
|
|
1548
|
+
return execFileAsync2(cmd, args, { timeout: 3e4 });
|
|
1549
|
+
}
|
|
1550
|
+
var InferenceService = class {
|
|
1551
|
+
// ── Ollama ──────────────────────────────────────────────────────
|
|
1552
|
+
async ollamaStatus() {
|
|
1553
|
+
const installed = await commandExists("ollama");
|
|
1554
|
+
if (!installed) return { installed: false, running: false, version: null };
|
|
1555
|
+
let version = null;
|
|
1556
|
+
try {
|
|
1557
|
+
const { stdout } = await run("ollama", ["--version"]);
|
|
1558
|
+
version = stdout.trim() || null;
|
|
1559
|
+
} catch {
|
|
1560
|
+
}
|
|
1561
|
+
let running = false;
|
|
1562
|
+
try {
|
|
1563
|
+
await run("ollama", ["list"]);
|
|
1564
|
+
running = true;
|
|
1565
|
+
} catch {
|
|
1566
|
+
}
|
|
1567
|
+
return { installed, running, version };
|
|
1568
|
+
}
|
|
1569
|
+
async ollamaModels() {
|
|
1570
|
+
const { stdout } = await run("ollama", ["list"]);
|
|
1571
|
+
const models = [];
|
|
1572
|
+
const lines = stdout.split("\n");
|
|
1573
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1574
|
+
const line = lines[i]?.trim();
|
|
1575
|
+
if (!line) continue;
|
|
1576
|
+
const parts = line.split(/\s+/);
|
|
1577
|
+
if (parts.length >= 3) {
|
|
1578
|
+
models.push({
|
|
1579
|
+
name: parts[0] ?? "",
|
|
1580
|
+
size: parts[2] ?? "",
|
|
1581
|
+
modified: parts.slice(3).join(" ")
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return models;
|
|
1586
|
+
}
|
|
1587
|
+
async ollamaPull(modelName) {
|
|
1588
|
+
if (!/^[\w./:@-]+$/.test(modelName)) {
|
|
1589
|
+
return { success: false, message: `Invalid model name: ${modelName}` };
|
|
1590
|
+
}
|
|
1591
|
+
try {
|
|
1592
|
+
const { stdout, stderr } = await execFileAsync2("ollama", ["pull", modelName], {
|
|
1593
|
+
timeout: 6e5
|
|
1594
|
+
// 10 min for large models
|
|
1595
|
+
});
|
|
1596
|
+
return { success: true, message: stdout || stderr };
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
return { success: false, message: err instanceof Error ? err.message : String(err) };
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
async ollamaDelete(modelName) {
|
|
1602
|
+
if (!/^[\w./:@-]+$/.test(modelName)) {
|
|
1603
|
+
throw new Error(`Invalid model name: ${modelName}`);
|
|
1604
|
+
}
|
|
1605
|
+
await run("ollama", ["rm", modelName]);
|
|
1606
|
+
}
|
|
1607
|
+
async ollamaStart() {
|
|
1608
|
+
const { spawn } = await import("child_process");
|
|
1609
|
+
const child = spawn("ollama", ["serve"], {
|
|
1610
|
+
stdio: "ignore",
|
|
1611
|
+
detached: true
|
|
1612
|
+
});
|
|
1613
|
+
child.unref();
|
|
1614
|
+
}
|
|
1615
|
+
async ollamaStop() {
|
|
1616
|
+
try {
|
|
1617
|
+
await run("pkill", ["-f", "ollama serve"]);
|
|
1618
|
+
} catch {
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
// ── Inference Snaps ─────────────────────────────────────────────
|
|
1622
|
+
async snapList() {
|
|
1623
|
+
const results = [];
|
|
1624
|
+
for (const [name, description] of KNOWN_SNAPS) {
|
|
1625
|
+
let installed = false;
|
|
1626
|
+
try {
|
|
1627
|
+
await run("snap", ["list", name]);
|
|
1628
|
+
installed = true;
|
|
1629
|
+
} catch {
|
|
1630
|
+
}
|
|
1631
|
+
results.push({ name, description, installed });
|
|
1632
|
+
}
|
|
1633
|
+
return results;
|
|
1634
|
+
}
|
|
1635
|
+
async snapStatus(snapName) {
|
|
1636
|
+
const known = KNOWN_SNAPS.some(([name]) => name === snapName);
|
|
1637
|
+
if (!known) throw new Error(`Unknown inference snap: ${snapName}`);
|
|
1638
|
+
let installed = false;
|
|
1639
|
+
let version = null;
|
|
1640
|
+
try {
|
|
1641
|
+
const { stdout } = await run("snap", ["list", snapName]);
|
|
1642
|
+
installed = true;
|
|
1643
|
+
const secondLine = stdout.split("\n")[1];
|
|
1644
|
+
if (secondLine) {
|
|
1645
|
+
version = secondLine.split(/\s+/)[1] ?? null;
|
|
1646
|
+
}
|
|
1647
|
+
} catch {
|
|
1648
|
+
return { installed: false, running: false, snapName, endpoint: null, version: null };
|
|
1649
|
+
}
|
|
1650
|
+
let running = false;
|
|
1651
|
+
try {
|
|
1652
|
+
await run(snapName, ["status"]);
|
|
1653
|
+
running = true;
|
|
1654
|
+
} catch {
|
|
1655
|
+
}
|
|
1656
|
+
const endpoint = running ? "http://localhost:9090/v1" : null;
|
|
1657
|
+
return { installed, running, snapName, endpoint, version };
|
|
1658
|
+
}
|
|
1659
|
+
async snapInstall(snapName) {
|
|
1660
|
+
const known = KNOWN_SNAPS.some(([name]) => name === snapName);
|
|
1661
|
+
if (!known) throw new Error(`Unknown inference snap: ${snapName}`);
|
|
1662
|
+
try {
|
|
1663
|
+
const { stdout, stderr } = await execFileAsync2("sudo", ["snap", "install", snapName], {
|
|
1664
|
+
timeout: 3e5
|
|
1665
|
+
// 5 min for large snaps
|
|
1666
|
+
});
|
|
1667
|
+
return { success: true, message: stdout || stderr };
|
|
1668
|
+
} catch (err) {
|
|
1669
|
+
return { success: false, message: err instanceof Error ? err.message : String(err) };
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
async snapRemove(snapName) {
|
|
1673
|
+
await execFileAsync2("sudo", ["snap", "remove", snapName], { timeout: 6e4 });
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
// src/server/rpc-server.ts
|
|
1678
|
+
import { existsSync as existsSync3, unlinkSync } from "fs";
|
|
1679
|
+
import { createServer as createServer2 } from "net";
|
|
1680
|
+
|
|
1681
|
+
// src/detection/process-detector.ts
|
|
1682
|
+
import { execFile as execFile3 } from "child_process";
|
|
1683
|
+
import { readdir } from "fs/promises";
|
|
1684
|
+
import { join as join3 } from "path";
|
|
1685
|
+
import { promisify as promisify3 } from "util";
|
|
1686
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
1687
|
+
async function findProcesses(pattern) {
|
|
1688
|
+
try {
|
|
1689
|
+
const { stdout } = await execFileAsync3("pgrep", ["-a", pattern], { timeout: 3e3 });
|
|
1690
|
+
return stdout.trim().split("\n").filter(Boolean).map((line) => {
|
|
1691
|
+
const spaceIdx = line.indexOf(" ");
|
|
1692
|
+
const pid = parseInt(line.slice(0, spaceIdx), 10);
|
|
1693
|
+
const command = line.slice(spaceIdx + 1);
|
|
1694
|
+
return { pid, command };
|
|
1695
|
+
});
|
|
1696
|
+
} catch {
|
|
1697
|
+
return [];
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
var HARNESS_PROCESS_PATTERNS = {
|
|
1701
|
+
"claude-code": ["claude"],
|
|
1702
|
+
cursor: ["cursor", "Cursor"],
|
|
1703
|
+
copilot: ["copilot"]
|
|
1704
|
+
};
|
|
1705
|
+
async function findHarnessProcesses(harnessId) {
|
|
1706
|
+
const patterns = HARNESS_PROCESS_PATTERNS[harnessId];
|
|
1707
|
+
if (!patterns) return [];
|
|
1708
|
+
const results = await Promise.all(patterns.map((p) => findProcesses(p)));
|
|
1709
|
+
return results.flat().map((p) => ({ ...p, harnessId }));
|
|
1710
|
+
}
|
|
1711
|
+
async function findAllHarnessProcesses() {
|
|
1712
|
+
const results = await Promise.all(
|
|
1713
|
+
Object.keys(HARNESS_PROCESS_PATTERNS).map((id) => findHarnessProcesses(id))
|
|
1714
|
+
);
|
|
1715
|
+
return results.flat();
|
|
1716
|
+
}
|
|
1717
|
+
async function findClaudeCodeSockets() {
|
|
1718
|
+
const dirs = [
|
|
1719
|
+
"/tmp",
|
|
1720
|
+
process.env.XDG_RUNTIME_DIR,
|
|
1721
|
+
join3(process.env.HOME ?? "/tmp", ".claude")
|
|
1722
|
+
].filter(Boolean);
|
|
1723
|
+
const sockets = [];
|
|
1724
|
+
for (const dir of dirs) {
|
|
1725
|
+
try {
|
|
1726
|
+
const entries = await readdir(dir);
|
|
1727
|
+
for (const entry of entries) {
|
|
1728
|
+
if (/^claude.*\.sock$/.test(entry)) {
|
|
1729
|
+
sockets.push(join3(dir, entry));
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
} catch {
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
return sockets;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// src/vaughn/config-normalizer.ts
|
|
1739
|
+
var MCP_SERVER_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
1740
|
+
var FORBIDDEN_MCP_SERVER_NAMES = /* @__PURE__ */ new Set([
|
|
1741
|
+
"__proto__",
|
|
1742
|
+
"constructor",
|
|
1743
|
+
"prototype",
|
|
1744
|
+
"hasOwnProperty",
|
|
1745
|
+
"isPrototypeOf",
|
|
1746
|
+
"propertyIsEnumerable",
|
|
1747
|
+
"toLocaleString",
|
|
1748
|
+
"toString",
|
|
1749
|
+
"valueOf"
|
|
1750
|
+
]);
|
|
1751
|
+
function isSafeMcpServerName(name) {
|
|
1752
|
+
return typeof name === "string" && MCP_SERVER_NAME_PATTERN.test(name) && !FORBIDDEN_MCP_SERVER_NAMES.has(name);
|
|
1753
|
+
}
|
|
1754
|
+
function vaughnConfigToClaudeSettings(config) {
|
|
1755
|
+
const settings = {};
|
|
1756
|
+
if (config.permissions.autoApprove.length > 0 || config.permissions.deny.length > 0) {
|
|
1757
|
+
settings.permissions = {};
|
|
1758
|
+
if (config.permissions.autoApprove.length > 0) {
|
|
1759
|
+
settings.permissions.allow = config.permissions.autoApprove;
|
|
1760
|
+
}
|
|
1761
|
+
if (config.permissions.deny.length > 0) {
|
|
1762
|
+
settings.permissions.deny = config.permissions.deny;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
if (Object.keys(config.environment.variables).length > 0) {
|
|
1766
|
+
settings.env = { ...config.environment.variables };
|
|
1767
|
+
}
|
|
1768
|
+
if (config.environment.mcpServers.length > 0) {
|
|
1769
|
+
const servers = {};
|
|
1770
|
+
for (const server of config.environment.mcpServers) {
|
|
1771
|
+
if (!isSafeMcpServerName(server.name)) continue;
|
|
1772
|
+
servers[server.name] = {
|
|
1773
|
+
command: server.command,
|
|
1774
|
+
...server.args && { args: server.args },
|
|
1775
|
+
...server.env && { env: server.env }
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
if (Object.keys(servers).length > 0) {
|
|
1779
|
+
settings.mcpServers = servers;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
return settings;
|
|
1783
|
+
}
|
|
1784
|
+
function claudeSettingsToVaughnConfig(settings) {
|
|
1785
|
+
const config = {};
|
|
1786
|
+
if (settings.permissions) {
|
|
1787
|
+
config.permissions = {
|
|
1788
|
+
autoApprove: settings.permissions.allow ?? [],
|
|
1789
|
+
deny: settings.permissions.deny ?? []
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
const mcpServers = settings.mcpServers ? Object.entries(settings.mcpServers).filter(([name]) => isSafeMcpServerName(name)).map(([name, server]) => ({
|
|
1793
|
+
name,
|
|
1794
|
+
command: server.command,
|
|
1795
|
+
...server.args && { args: server.args },
|
|
1796
|
+
...server.env && { env: server.env }
|
|
1797
|
+
})) : [];
|
|
1798
|
+
config.environment = {
|
|
1799
|
+
variables: settings.env ?? {},
|
|
1800
|
+
mcpServers
|
|
1801
|
+
};
|
|
1802
|
+
return config;
|
|
1803
|
+
}
|
|
1804
|
+
function vaughnConfigToCursorrules(config) {
|
|
1805
|
+
const lines = [];
|
|
1806
|
+
lines.push("# Project Rules");
|
|
1807
|
+
lines.push("");
|
|
1808
|
+
lines.push("## Identity");
|
|
1809
|
+
lines.push(`- Name: ${config.identity.name}`);
|
|
1810
|
+
if (config.identity.role) {
|
|
1811
|
+
lines.push(`- Role: ${config.identity.role}`);
|
|
1812
|
+
}
|
|
1813
|
+
lines.push("");
|
|
1814
|
+
if (config.rules.length > 0) {
|
|
1815
|
+
lines.push("## Rules");
|
|
1816
|
+
lines.push("");
|
|
1817
|
+
for (const rule of config.rules) {
|
|
1818
|
+
lines.push(`### ${rule.id}`);
|
|
1819
|
+
lines.push("");
|
|
1820
|
+
lines.push(rule.description);
|
|
1821
|
+
lines.push("");
|
|
1822
|
+
lines.push(renderRuleContent(rule));
|
|
1823
|
+
lines.push("");
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
if (config.skills.length > 0) {
|
|
1827
|
+
lines.push("## Skills");
|
|
1828
|
+
lines.push("");
|
|
1829
|
+
for (const skill of config.skills) {
|
|
1830
|
+
lines.push(`### ${skill.name}`);
|
|
1831
|
+
lines.push("");
|
|
1832
|
+
lines.push(skill.description);
|
|
1833
|
+
lines.push("");
|
|
1834
|
+
lines.push(skill.instructions);
|
|
1835
|
+
lines.push("");
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return lines.join("\n");
|
|
1839
|
+
}
|
|
1840
|
+
function vaughnConfigToAgentsMd(config) {
|
|
1841
|
+
const lines = [];
|
|
1842
|
+
lines.push("# AGENTS.md");
|
|
1843
|
+
lines.push("");
|
|
1844
|
+
lines.push(`> Generated by VAUGHN protocol v${VAUGHN_VERSION}`);
|
|
1845
|
+
lines.push("");
|
|
1846
|
+
lines.push("## Identity");
|
|
1847
|
+
lines.push("");
|
|
1848
|
+
lines.push(`- Name: ${config.identity.name}`);
|
|
1849
|
+
lines.push(`- Email: ${config.identity.email}`);
|
|
1850
|
+
if (config.identity.role) {
|
|
1851
|
+
lines.push(`- Role: ${config.identity.role}`);
|
|
1852
|
+
}
|
|
1853
|
+
lines.push("");
|
|
1854
|
+
if (config.permissions.deny.length > 0) {
|
|
1855
|
+
lines.push("## Denied Operations");
|
|
1856
|
+
lines.push("");
|
|
1857
|
+
for (const d of config.permissions.deny) {
|
|
1858
|
+
lines.push(`- ${d}`);
|
|
1859
|
+
}
|
|
1860
|
+
lines.push("");
|
|
1861
|
+
}
|
|
1862
|
+
if (config.rules.length > 0) {
|
|
1863
|
+
lines.push("## Rules");
|
|
1864
|
+
lines.push("");
|
|
1865
|
+
for (const rule of config.rules) {
|
|
1866
|
+
lines.push(`### ${rule.id}`);
|
|
1867
|
+
lines.push("");
|
|
1868
|
+
lines.push(rule.description);
|
|
1869
|
+
lines.push("");
|
|
1870
|
+
if (rule.appliesTo.length > 0) {
|
|
1871
|
+
lines.push(`Applies to: ${rule.appliesTo.join(", ")}`);
|
|
1872
|
+
lines.push("");
|
|
1873
|
+
}
|
|
1874
|
+
lines.push(renderRuleContent(rule));
|
|
1875
|
+
lines.push("");
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
if (config.commands.length > 0) {
|
|
1879
|
+
lines.push("## Commands");
|
|
1880
|
+
lines.push("");
|
|
1881
|
+
for (const cmd of config.commands) {
|
|
1882
|
+
lines.push(`### /${cmd.id}`);
|
|
1883
|
+
lines.push("");
|
|
1884
|
+
lines.push(cmd.description);
|
|
1885
|
+
lines.push("");
|
|
1886
|
+
if (cmd.steps.length > 0) {
|
|
1887
|
+
lines.push("Steps:");
|
|
1888
|
+
for (let i = 0; i < cmd.steps.length; i++) {
|
|
1889
|
+
lines.push(`${i + 1}. ${cmd.steps[i]}`);
|
|
1890
|
+
}
|
|
1891
|
+
lines.push("");
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
return lines.join("\n");
|
|
1896
|
+
}
|
|
1897
|
+
function generateAllConfigs(config) {
|
|
1898
|
+
const files = /* @__PURE__ */ new Map();
|
|
1899
|
+
files.set(".claude/settings.json", JSON.stringify(vaughnConfigToClaudeSettings(config), null, 2));
|
|
1900
|
+
files.set(".cursorrules", vaughnConfigToCursorrules(config));
|
|
1901
|
+
files.set("AGENTS.md", vaughnConfigToAgentsMd(config));
|
|
1902
|
+
for (const rule of config.rules) {
|
|
1903
|
+
const frontmatter = [
|
|
1904
|
+
"---",
|
|
1905
|
+
`description: ${rule.description}`,
|
|
1906
|
+
...rule.appliesTo.length > 0 ? [`globs: ${rule.appliesTo.join(", ")}`] : [],
|
|
1907
|
+
"---"
|
|
1908
|
+
].join("\n");
|
|
1909
|
+
files.set(`.claude/rules/${rule.id}.md`, `${frontmatter}
|
|
1910
|
+
|
|
1911
|
+
${renderRuleContent(rule)}
|
|
1912
|
+
`);
|
|
1913
|
+
}
|
|
1914
|
+
return { files };
|
|
1915
|
+
}
|
|
1916
|
+
function renderRuleContent(rule) {
|
|
1917
|
+
let content = rule.content;
|
|
1918
|
+
for (const [key, value] of Object.entries(rule.variables)) {
|
|
1919
|
+
content = content.split(`{{${key}}}`).join(value);
|
|
1920
|
+
}
|
|
1921
|
+
return content;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// src/server/rpc-server.ts
|
|
1925
|
+
var ERR_PARSE = -32700;
|
|
1926
|
+
var ERR_INVALID_PARAMS = -32602;
|
|
1927
|
+
var ERR_METHOD_NOT_FOUND = -32601;
|
|
1928
|
+
var ERR_INTERNAL = -32603;
|
|
1929
|
+
var RpcServer = class _RpcServer {
|
|
1930
|
+
constructor(registry, socketPath, store) {
|
|
1931
|
+
this.registry = registry;
|
|
1932
|
+
this.socketPath = socketPath;
|
|
1933
|
+
this.store = store;
|
|
1934
|
+
this.server.on("connection", (socket) => {
|
|
1935
|
+
let buffer = "";
|
|
1936
|
+
socket.on("data", (chunk) => {
|
|
1937
|
+
buffer += chunk.toString();
|
|
1938
|
+
const lines = buffer.split("\n");
|
|
1939
|
+
buffer = lines.pop() ?? "";
|
|
1940
|
+
for (const line of lines) {
|
|
1941
|
+
this.handleLine(line.trim(), (response) => {
|
|
1942
|
+
socket.write(`${JSON.stringify(response)}
|
|
1943
|
+
`);
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
registry;
|
|
1950
|
+
socketPath;
|
|
1951
|
+
store;
|
|
1952
|
+
server = createServer2();
|
|
1953
|
+
healthCheckFn = null;
|
|
1954
|
+
spawner = null;
|
|
1955
|
+
inference = null;
|
|
1956
|
+
mergePipeline = null;
|
|
1957
|
+
ciFeedback = null;
|
|
1958
|
+
vaughnDispatchFn = null;
|
|
1959
|
+
vaughnEventQueue = [];
|
|
1960
|
+
static MAX_VAUGHN_EVENTS = 100;
|
|
1961
|
+
handleLine(line, reply) {
|
|
1962
|
+
let req;
|
|
1963
|
+
try {
|
|
1964
|
+
req = JSON.parse(line);
|
|
1965
|
+
} catch {
|
|
1966
|
+
reply({ jsonrpc: "2.0", id: null, error: { code: ERR_PARSE, message: "Parse error" } });
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
this.dispatch(req).then(reply).catch((err) => {
|
|
1970
|
+
reply({
|
|
1971
|
+
jsonrpc: "2.0",
|
|
1972
|
+
id: req.id,
|
|
1973
|
+
error: { code: ERR_INTERNAL, message: err instanceof Error ? err.message : String(err) }
|
|
1974
|
+
});
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
async dispatch(req) {
|
|
1978
|
+
const { id, method, params } = req;
|
|
1979
|
+
const p = params ?? {};
|
|
1980
|
+
switch (method) {
|
|
1981
|
+
case "harness.list": {
|
|
1982
|
+
const ids = await this.registry.listAvailable();
|
|
1983
|
+
const infos = await Promise.all(ids.map((id2) => this.registry.get(id2)?.getInfo()));
|
|
1984
|
+
return { jsonrpc: "2.0", id, result: infos };
|
|
1985
|
+
}
|
|
1986
|
+
case "harness.info": {
|
|
1987
|
+
const harnessId = p.harnessId;
|
|
1988
|
+
if (!harnessId) {
|
|
1989
|
+
return {
|
|
1990
|
+
jsonrpc: "2.0",
|
|
1991
|
+
id,
|
|
1992
|
+
error: { code: ERR_INVALID_PARAMS, message: "harnessId required" }
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
const adapter = this.registry.get(harnessId);
|
|
1996
|
+
if (!adapter) {
|
|
1997
|
+
return {
|
|
1998
|
+
jsonrpc: "2.0",
|
|
1999
|
+
id,
|
|
2000
|
+
error: { code: ERR_INVALID_PARAMS, message: `Harness not found: ${harnessId}` }
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
return { jsonrpc: "2.0", id, result: await adapter.getInfo() };
|
|
2004
|
+
}
|
|
2005
|
+
case "harness.execute": {
|
|
2006
|
+
const harnessId = p.harnessId;
|
|
2007
|
+
const command = p.command;
|
|
2008
|
+
if (!(harnessId && command)) {
|
|
2009
|
+
return {
|
|
2010
|
+
jsonrpc: "2.0",
|
|
2011
|
+
id,
|
|
2012
|
+
error: { code: ERR_INVALID_PARAMS, message: "harnessId and command required" }
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
const adapter = this.registry.get(harnessId);
|
|
2016
|
+
if (!adapter) {
|
|
2017
|
+
return {
|
|
2018
|
+
jsonrpc: "2.0",
|
|
2019
|
+
id,
|
|
2020
|
+
error: { code: ERR_INVALID_PARAMS, message: `Harness not found: ${harnessId}` }
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
const result = await adapter.execute(command);
|
|
2024
|
+
return { jsonrpc: "2.0", id, result };
|
|
2025
|
+
}
|
|
2026
|
+
case "harness.syncConfig": {
|
|
2027
|
+
const harnessId = p.harnessId;
|
|
2028
|
+
const direction = p.direction;
|
|
2029
|
+
if (!(harnessId && direction)) {
|
|
2030
|
+
return {
|
|
2031
|
+
jsonrpc: "2.0",
|
|
2032
|
+
id,
|
|
2033
|
+
error: { code: ERR_INVALID_PARAMS, message: "harnessId and direction required" }
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
return { jsonrpc: "2.0", id, result: syncConfig(harnessId, direction) };
|
|
2037
|
+
}
|
|
2038
|
+
case "harness.diffConfig": {
|
|
2039
|
+
const harnessId = p.harnessId;
|
|
2040
|
+
if (!harnessId) {
|
|
2041
|
+
return {
|
|
2042
|
+
jsonrpc: "2.0",
|
|
2043
|
+
id,
|
|
2044
|
+
error: { code: ERR_INVALID_PARAMS, message: "harnessId required" }
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
return { jsonrpc: "2.0", id, result: diffConfig(harnessId) };
|
|
2048
|
+
}
|
|
2049
|
+
case "harness.listRunning": {
|
|
2050
|
+
const harnessId = p.harnessId;
|
|
2051
|
+
if (!harnessId) {
|
|
2052
|
+
return {
|
|
2053
|
+
jsonrpc: "2.0",
|
|
2054
|
+
id,
|
|
2055
|
+
error: { code: ERR_INVALID_PARAMS, message: "harnessId required" }
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
const processes = await findHarnessProcesses(harnessId);
|
|
2059
|
+
return { jsonrpc: "2.0", id, result: processes };
|
|
2060
|
+
}
|
|
2061
|
+
case "ping": {
|
|
2062
|
+
return { jsonrpc: "2.0", id, result: { status: "ok", pid: process.pid } };
|
|
2063
|
+
}
|
|
2064
|
+
case "harness.health": {
|
|
2065
|
+
if (!this.healthCheckFn) {
|
|
2066
|
+
return {
|
|
2067
|
+
jsonrpc: "2.0",
|
|
2068
|
+
id,
|
|
2069
|
+
error: { code: ERR_INTERNAL, message: "Health check not configured" }
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
const health = await this.healthCheckFn();
|
|
2073
|
+
return { jsonrpc: "2.0", id, result: health };
|
|
2074
|
+
}
|
|
2075
|
+
// -----------------------------------------------------------------------
|
|
2076
|
+
// Session management (PGlite-backed)
|
|
2077
|
+
// -----------------------------------------------------------------------
|
|
2078
|
+
case "session.register": {
|
|
2079
|
+
if (!this.store) return this.noStore(id);
|
|
2080
|
+
const agentId = p.agentId;
|
|
2081
|
+
const env = p.env;
|
|
2082
|
+
if (!agentId) return this.missingParam(id, "agentId");
|
|
2083
|
+
const session = await this.store.registerSession({
|
|
2084
|
+
id: agentId,
|
|
2085
|
+
env: env ?? agentId,
|
|
2086
|
+
task: p.task,
|
|
2087
|
+
pid: p.pid
|
|
2088
|
+
});
|
|
2089
|
+
return { jsonrpc: "2.0", id, result: { session } };
|
|
2090
|
+
}
|
|
2091
|
+
case "session.update": {
|
|
2092
|
+
if (!this.store) return this.noStore(id);
|
|
2093
|
+
const agentId = p.agentId;
|
|
2094
|
+
if (!agentId) return this.missingParam(id, "agentId");
|
|
2095
|
+
const session = await this.store.updateSession(agentId, {
|
|
2096
|
+
task: p.task,
|
|
2097
|
+
files: p.files
|
|
2098
|
+
});
|
|
2099
|
+
return { jsonrpc: "2.0", id, result: session };
|
|
2100
|
+
}
|
|
2101
|
+
case "session.end": {
|
|
2102
|
+
if (!this.store) return this.noStore(id);
|
|
2103
|
+
const agentId = p.agentId;
|
|
2104
|
+
if (!agentId) return this.missingParam(id, "agentId");
|
|
2105
|
+
await this.store.endSession(agentId, p.exitSummary);
|
|
2106
|
+
await this.store.releaseAllReservations(agentId);
|
|
2107
|
+
await this.store.logEvent({
|
|
2108
|
+
agentId,
|
|
2109
|
+
eventType: "session-end",
|
|
2110
|
+
payload: { exitSummary: p.exitSummary }
|
|
2111
|
+
});
|
|
2112
|
+
return { jsonrpc: "2.0", id, result: { ok: true } };
|
|
2113
|
+
}
|
|
2114
|
+
case "session.list": {
|
|
2115
|
+
if (!this.store) return this.noStore(id);
|
|
2116
|
+
const sessions = await this.store.getActiveSessions();
|
|
2117
|
+
return { jsonrpc: "2.0", id, result: sessions };
|
|
2118
|
+
}
|
|
2119
|
+
case "session.history": {
|
|
2120
|
+
if (!this.store) return this.noStore(id);
|
|
2121
|
+
const agentId = p.agentId;
|
|
2122
|
+
if (!agentId) return this.missingParam(id, "agentId");
|
|
2123
|
+
const limit = p.limit ?? 10;
|
|
2124
|
+
const history = await this.store.getSessionHistory(agentId, limit);
|
|
2125
|
+
return { jsonrpc: "2.0", id, result: history };
|
|
2126
|
+
}
|
|
2127
|
+
// -----------------------------------------------------------------------
|
|
2128
|
+
// Inter-agent messaging (PGlite-backed)
|
|
2129
|
+
// -----------------------------------------------------------------------
|
|
2130
|
+
case "mail.send": {
|
|
2131
|
+
if (!this.store) return this.noStore(id);
|
|
2132
|
+
const fromAgent = p.fromAgent;
|
|
2133
|
+
const toAgent = p.toAgent;
|
|
2134
|
+
const subject = p.subject;
|
|
2135
|
+
if (!(fromAgent && toAgent && subject)) {
|
|
2136
|
+
return this.missingParam(id, "fromAgent, toAgent, subject");
|
|
2137
|
+
}
|
|
2138
|
+
const msg = await this.store.sendMessage({
|
|
2139
|
+
fromAgent,
|
|
2140
|
+
toAgent,
|
|
2141
|
+
subject,
|
|
2142
|
+
body: p.body
|
|
2143
|
+
});
|
|
2144
|
+
return { jsonrpc: "2.0", id, result: msg };
|
|
2145
|
+
}
|
|
2146
|
+
case "mail.broadcast": {
|
|
2147
|
+
if (!this.store) return this.noStore(id);
|
|
2148
|
+
const fromAgent = p.fromAgent;
|
|
2149
|
+
const subject = p.subject;
|
|
2150
|
+
if (!(fromAgent && subject)) return this.missingParam(id, "fromAgent, subject");
|
|
2151
|
+
const sent = await this.store.broadcastMessage({
|
|
2152
|
+
fromAgent,
|
|
2153
|
+
subject,
|
|
2154
|
+
body: p.body
|
|
2155
|
+
});
|
|
2156
|
+
return { jsonrpc: "2.0", id, result: { sent } };
|
|
2157
|
+
}
|
|
2158
|
+
case "mail.inbox": {
|
|
2159
|
+
if (!this.store) return this.noStore(id);
|
|
2160
|
+
const agentId = p.agentId;
|
|
2161
|
+
if (!agentId) return this.missingParam(id, "agentId");
|
|
2162
|
+
const unreadOnly = p.unreadOnly ?? true;
|
|
2163
|
+
const messages = await this.store.getInbox(agentId, unreadOnly);
|
|
2164
|
+
return { jsonrpc: "2.0", id, result: messages };
|
|
2165
|
+
}
|
|
2166
|
+
case "mail.markRead": {
|
|
2167
|
+
if (!this.store) return this.noStore(id);
|
|
2168
|
+
const messageIds = p.messageIds;
|
|
2169
|
+
if (!messageIds) return this.missingParam(id, "messageIds");
|
|
2170
|
+
await this.store.markRead(messageIds);
|
|
2171
|
+
return { jsonrpc: "2.0", id, result: { ok: true } };
|
|
2172
|
+
}
|
|
2173
|
+
// -----------------------------------------------------------------------
|
|
2174
|
+
// File reservations (PGlite-backed)
|
|
2175
|
+
// -----------------------------------------------------------------------
|
|
2176
|
+
case "files.reserve": {
|
|
2177
|
+
if (!this.store) return this.noStore(id);
|
|
2178
|
+
const filePath = p.filePath;
|
|
2179
|
+
const agentId = p.agentId;
|
|
2180
|
+
if (!(filePath && agentId)) return this.missingParam(id, "filePath, agentId");
|
|
2181
|
+
const ttlSeconds = p.ttlSeconds ?? 3600;
|
|
2182
|
+
const reservation = await this.store.reserveFile({
|
|
2183
|
+
filePath,
|
|
2184
|
+
agentId,
|
|
2185
|
+
ttlSeconds,
|
|
2186
|
+
reason: p.reason
|
|
2187
|
+
});
|
|
2188
|
+
return { jsonrpc: "2.0", id, result: reservation };
|
|
2189
|
+
}
|
|
2190
|
+
case "files.check": {
|
|
2191
|
+
if (!this.store) return this.noStore(id);
|
|
2192
|
+
const filePath = p.filePath;
|
|
2193
|
+
if (!filePath) return this.missingParam(id, "filePath");
|
|
2194
|
+
const reservation = await this.store.checkReservation(filePath);
|
|
2195
|
+
return { jsonrpc: "2.0", id, result: reservation };
|
|
2196
|
+
}
|
|
2197
|
+
case "files.release": {
|
|
2198
|
+
if (!this.store) return this.noStore(id);
|
|
2199
|
+
const agentId = p.agentId;
|
|
2200
|
+
if (!agentId) return this.missingParam(id, "agentId");
|
|
2201
|
+
const released = await this.store.releaseAllReservations(agentId);
|
|
2202
|
+
return { jsonrpc: "2.0", id, result: { released } };
|
|
2203
|
+
}
|
|
2204
|
+
case "files.list": {
|
|
2205
|
+
if (!this.store) return this.noStore(id);
|
|
2206
|
+
const agentId = p.agentId;
|
|
2207
|
+
if (!agentId) return this.missingParam(id, "agentId");
|
|
2208
|
+
const reservations = await this.store.getReservations(agentId);
|
|
2209
|
+
return { jsonrpc: "2.0", id, result: reservations };
|
|
2210
|
+
}
|
|
2211
|
+
// -----------------------------------------------------------------------
|
|
2212
|
+
// Tasks (PGlite-backed)
|
|
2213
|
+
// -----------------------------------------------------------------------
|
|
2214
|
+
case "tasks.create": {
|
|
2215
|
+
if (!this.store) return this.noStore(id);
|
|
2216
|
+
const taskId = p.taskId;
|
|
2217
|
+
const description = p.description;
|
|
2218
|
+
if (!taskId) return this.missingParam(id, "taskId");
|
|
2219
|
+
const task = await this.store.createTask({ id: taskId, description: description ?? "" });
|
|
2220
|
+
return { jsonrpc: "2.0", id, result: task };
|
|
2221
|
+
}
|
|
2222
|
+
case "tasks.claim": {
|
|
2223
|
+
if (!this.store) return this.noStore(id);
|
|
2224
|
+
const taskId = p.taskId;
|
|
2225
|
+
const agentId = p.agentId;
|
|
2226
|
+
if (!(taskId && agentId)) return this.missingParam(id, "taskId, agentId");
|
|
2227
|
+
const claim = await this.store.claimTask(taskId, agentId);
|
|
2228
|
+
return { jsonrpc: "2.0", id, result: claim };
|
|
2229
|
+
}
|
|
2230
|
+
case "tasks.complete": {
|
|
2231
|
+
if (!this.store) return this.noStore(id);
|
|
2232
|
+
const taskId = p.taskId;
|
|
2233
|
+
const agentId = p.agentId;
|
|
2234
|
+
if (!(taskId && agentId)) return this.missingParam(id, "taskId, agentId");
|
|
2235
|
+
const completed = await this.store.completeTask(taskId, agentId);
|
|
2236
|
+
return { jsonrpc: "2.0", id, result: { ok: completed } };
|
|
2237
|
+
}
|
|
2238
|
+
case "tasks.release": {
|
|
2239
|
+
if (!this.store) return this.noStore(id);
|
|
2240
|
+
const taskId = p.taskId;
|
|
2241
|
+
const agentId = p.agentId;
|
|
2242
|
+
if (!(taskId && agentId)) return this.missingParam(id, "taskId, agentId");
|
|
2243
|
+
const released = await this.store.releaseTask(taskId, agentId);
|
|
2244
|
+
return { jsonrpc: "2.0", id, result: { ok: released } };
|
|
2245
|
+
}
|
|
2246
|
+
case "tasks.list": {
|
|
2247
|
+
if (!this.store) return this.noStore(id);
|
|
2248
|
+
const tasks = await this.store.listTasks({
|
|
2249
|
+
status: p.status,
|
|
2250
|
+
owner: p.owner
|
|
2251
|
+
});
|
|
2252
|
+
return { jsonrpc: "2.0", id, result: tasks };
|
|
2253
|
+
}
|
|
2254
|
+
// -----------------------------------------------------------------------
|
|
2255
|
+
// Events (PGlite-backed)
|
|
2256
|
+
// -----------------------------------------------------------------------
|
|
2257
|
+
case "events.log": {
|
|
2258
|
+
if (!this.store) return this.noStore(id);
|
|
2259
|
+
const agentId = p.agentId;
|
|
2260
|
+
const eventType = p.eventType;
|
|
2261
|
+
if (!(agentId && eventType)) return this.missingParam(id, "agentId, eventType");
|
|
2262
|
+
const event = await this.store.logEvent({
|
|
2263
|
+
agentId,
|
|
2264
|
+
eventType,
|
|
2265
|
+
payload: p.payload
|
|
2266
|
+
});
|
|
2267
|
+
return { jsonrpc: "2.0", id, result: event };
|
|
2268
|
+
}
|
|
2269
|
+
case "events.recent": {
|
|
2270
|
+
if (!this.store) return this.noStore(id);
|
|
2271
|
+
const limit = p.limit ?? 50;
|
|
2272
|
+
const events = await this.store.getRecentEvents(limit);
|
|
2273
|
+
return { jsonrpc: "2.0", id, result: events };
|
|
2274
|
+
}
|
|
2275
|
+
// -----------------------------------------------------------------------
|
|
2276
|
+
// Worktrees (PGlite-backed)
|
|
2277
|
+
// -----------------------------------------------------------------------
|
|
2278
|
+
case "worktree.create": {
|
|
2279
|
+
if (!this.store) return this.noStore(id);
|
|
2280
|
+
const agentId = p.agentId;
|
|
2281
|
+
const branch = p.branch;
|
|
2282
|
+
const worktreePath = p.worktreePath;
|
|
2283
|
+
if (!(agentId && branch && worktreePath))
|
|
2284
|
+
return this.missingParam(id, "agentId, branch, worktreePath");
|
|
2285
|
+
const wt = await this.store.registerWorktree({
|
|
2286
|
+
agentId,
|
|
2287
|
+
branch,
|
|
2288
|
+
worktreePath,
|
|
2289
|
+
baseBranch: p.baseBranch
|
|
2290
|
+
});
|
|
2291
|
+
return { jsonrpc: "2.0", id, result: wt };
|
|
2292
|
+
}
|
|
2293
|
+
case "worktree.get": {
|
|
2294
|
+
if (!this.store) return this.noStore(id);
|
|
2295
|
+
const agentId = p.agentId;
|
|
2296
|
+
if (!agentId) return this.missingParam(id, "agentId");
|
|
2297
|
+
const wt = await this.store.getWorktree(agentId);
|
|
2298
|
+
return { jsonrpc: "2.0", id, result: wt };
|
|
2299
|
+
}
|
|
2300
|
+
case "worktree.list": {
|
|
2301
|
+
if (!this.store) return this.noStore(id);
|
|
2302
|
+
const worktrees = await this.store.getActiveWorktrees();
|
|
2303
|
+
return { jsonrpc: "2.0", id, result: worktrees };
|
|
2304
|
+
}
|
|
2305
|
+
case "worktree.status": {
|
|
2306
|
+
if (!this.store) return this.noStore(id);
|
|
2307
|
+
const agentId = p.agentId;
|
|
2308
|
+
const status = p.status;
|
|
2309
|
+
if (!(agentId && status)) return this.missingParam(id, "agentId, status");
|
|
2310
|
+
const ok = await this.store.updateWorktreeStatus(agentId, status);
|
|
2311
|
+
return { jsonrpc: "2.0", id, result: { success: ok } };
|
|
2312
|
+
}
|
|
2313
|
+
case "worktree.remove": {
|
|
2314
|
+
if (!this.store) return this.noStore(id);
|
|
2315
|
+
const agentId = p.agentId;
|
|
2316
|
+
if (!agentId) return this.missingParam(id, "agentId");
|
|
2317
|
+
const ok = await this.store.removeWorktree(agentId);
|
|
2318
|
+
return { jsonrpc: "2.0", id, result: { success: ok } };
|
|
2319
|
+
}
|
|
2320
|
+
// -----------------------------------------------------------------------
|
|
2321
|
+
// Agent Memory (PGlite-backed)
|
|
2322
|
+
// -----------------------------------------------------------------------
|
|
2323
|
+
case "memory.store": {
|
|
2324
|
+
if (!this.store) return this.noStore(id);
|
|
2325
|
+
const agentId = p.agentId;
|
|
2326
|
+
const memoryType = p.memoryType;
|
|
2327
|
+
const content = p.content;
|
|
2328
|
+
if (!(agentId && memoryType && content))
|
|
2329
|
+
return this.missingParam(id, "agentId, memoryType, content");
|
|
2330
|
+
const entry = await this.store.storeMemory({
|
|
2331
|
+
agentId,
|
|
2332
|
+
memoryType,
|
|
2333
|
+
content,
|
|
2334
|
+
metadata: p.metadata
|
|
2335
|
+
});
|
|
2336
|
+
return { jsonrpc: "2.0", id, result: entry };
|
|
2337
|
+
}
|
|
2338
|
+
case "memory.recall": {
|
|
2339
|
+
if (!this.store) return this.noStore(id);
|
|
2340
|
+
const entries = await this.store.recallMemory({
|
|
2341
|
+
agentId: p.agentId,
|
|
2342
|
+
memoryType: p.memoryType,
|
|
2343
|
+
keyword: p.keyword,
|
|
2344
|
+
limit: p.limit
|
|
2345
|
+
});
|
|
2346
|
+
return { jsonrpc: "2.0", id, result: entries };
|
|
2347
|
+
}
|
|
2348
|
+
case "memory.summarize": {
|
|
2349
|
+
if (!this.store) return this.noStore(id);
|
|
2350
|
+
const agentId = p.agentId;
|
|
2351
|
+
if (!agentId) return this.missingParam(id, "agentId");
|
|
2352
|
+
const perType = p.perType ?? 5;
|
|
2353
|
+
const entries = await this.store.summarizeMemory(agentId, perType);
|
|
2354
|
+
return { jsonrpc: "2.0", id, result: entries };
|
|
2355
|
+
}
|
|
2356
|
+
// -----------------------------------------------------------------------
|
|
2357
|
+
// Agent spawner (process management)
|
|
2358
|
+
// -----------------------------------------------------------------------
|
|
2359
|
+
case "agent.spawn": {
|
|
2360
|
+
if (!this.spawner) return this.noService(id, "spawner");
|
|
2361
|
+
const name = p.name;
|
|
2362
|
+
const backend = p.backend;
|
|
2363
|
+
const model = p.model;
|
|
2364
|
+
const prompt = p.prompt;
|
|
2365
|
+
if (!(name && backend && model && prompt)) {
|
|
2366
|
+
return this.missingParam(id, "name, backend, model, prompt");
|
|
2367
|
+
}
|
|
2368
|
+
const sessionId = this.spawner.spawn(name, backend, model, prompt);
|
|
2369
|
+
return { jsonrpc: "2.0", id, result: { sessionId } };
|
|
2370
|
+
}
|
|
2371
|
+
case "agent.stop": {
|
|
2372
|
+
if (!this.spawner) return this.noService(id, "spawner");
|
|
2373
|
+
const sessionId = p.sessionId;
|
|
2374
|
+
if (!sessionId) return this.missingParam(id, "sessionId");
|
|
2375
|
+
this.spawner.stop(sessionId);
|
|
2376
|
+
return { jsonrpc: "2.0", id, result: { ok: true } };
|
|
2377
|
+
}
|
|
2378
|
+
case "agent.list": {
|
|
2379
|
+
if (!this.spawner) return this.noService(id, "spawner");
|
|
2380
|
+
return { jsonrpc: "2.0", id, result: this.spawner.list() };
|
|
2381
|
+
}
|
|
2382
|
+
case "agent.remove": {
|
|
2383
|
+
if (!this.spawner) return this.noService(id, "spawner");
|
|
2384
|
+
const sessionId = p.sessionId;
|
|
2385
|
+
if (!sessionId) return this.missingParam(id, "sessionId");
|
|
2386
|
+
this.spawner.remove(sessionId);
|
|
2387
|
+
return { jsonrpc: "2.0", id, result: { ok: true } };
|
|
2388
|
+
}
|
|
2389
|
+
case "agent.input":
|
|
2390
|
+
case "agent.resize": {
|
|
2391
|
+
return {
|
|
2392
|
+
jsonrpc: "2.0",
|
|
2393
|
+
id,
|
|
2394
|
+
error: { code: -32601, message: "PTY interaction not supported for current backends" }
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
// -----------------------------------------------------------------------
|
|
2398
|
+
// Inference management (Ollama, Snaps)
|
|
2399
|
+
// -----------------------------------------------------------------------
|
|
2400
|
+
case "inference.ollama.status": {
|
|
2401
|
+
if (!this.inference) return this.noService(id, "inference");
|
|
2402
|
+
return { jsonrpc: "2.0", id, result: await this.inference.ollamaStatus() };
|
|
2403
|
+
}
|
|
2404
|
+
case "inference.ollama.models": {
|
|
2405
|
+
if (!this.inference) return this.noService(id, "inference");
|
|
2406
|
+
return { jsonrpc: "2.0", id, result: await this.inference.ollamaModels() };
|
|
2407
|
+
}
|
|
2408
|
+
case "inference.ollama.pull": {
|
|
2409
|
+
if (!this.inference) return this.noService(id, "inference");
|
|
2410
|
+
const modelName = p.modelName;
|
|
2411
|
+
if (!modelName) return this.missingParam(id, "modelName");
|
|
2412
|
+
return { jsonrpc: "2.0", id, result: await this.inference.ollamaPull(modelName) };
|
|
2413
|
+
}
|
|
2414
|
+
case "inference.ollama.delete": {
|
|
2415
|
+
if (!this.inference) return this.noService(id, "inference");
|
|
2416
|
+
const modelName = p.modelName;
|
|
2417
|
+
if (!modelName) return this.missingParam(id, "modelName");
|
|
2418
|
+
await this.inference.ollamaDelete(modelName);
|
|
2419
|
+
return { jsonrpc: "2.0", id, result: { ok: true } };
|
|
2420
|
+
}
|
|
2421
|
+
case "inference.ollama.start": {
|
|
2422
|
+
if (!this.inference) return this.noService(id, "inference");
|
|
2423
|
+
await this.inference.ollamaStart();
|
|
2424
|
+
return { jsonrpc: "2.0", id, result: { ok: true } };
|
|
2425
|
+
}
|
|
2426
|
+
case "inference.ollama.stop": {
|
|
2427
|
+
if (!this.inference) return this.noService(id, "inference");
|
|
2428
|
+
await this.inference.ollamaStop();
|
|
2429
|
+
return { jsonrpc: "2.0", id, result: { ok: true } };
|
|
2430
|
+
}
|
|
2431
|
+
case "inference.snap.list": {
|
|
2432
|
+
if (!this.inference) return this.noService(id, "inference");
|
|
2433
|
+
return { jsonrpc: "2.0", id, result: await this.inference.snapList() };
|
|
2434
|
+
}
|
|
2435
|
+
case "inference.snap.status": {
|
|
2436
|
+
if (!this.inference) return this.noService(id, "inference");
|
|
2437
|
+
const snapName = p.snapName;
|
|
2438
|
+
if (!snapName) return this.missingParam(id, "snapName");
|
|
2439
|
+
return { jsonrpc: "2.0", id, result: await this.inference.snapStatus(snapName) };
|
|
2440
|
+
}
|
|
2441
|
+
case "inference.snap.install": {
|
|
2442
|
+
if (!this.inference) return this.noService(id, "inference");
|
|
2443
|
+
const snapName = p.snapName;
|
|
2444
|
+
if (!snapName) return this.missingParam(id, "snapName");
|
|
2445
|
+
return { jsonrpc: "2.0", id, result: await this.inference.snapInstall(snapName) };
|
|
2446
|
+
}
|
|
2447
|
+
case "inference.snap.remove": {
|
|
2448
|
+
if (!this.inference) return this.noService(id, "inference");
|
|
2449
|
+
const snapName = p.snapName;
|
|
2450
|
+
if (!snapName) return this.missingParam(id, "snapName");
|
|
2451
|
+
await this.inference.snapRemove(snapName);
|
|
2452
|
+
return { jsonrpc: "2.0", id, result: { ok: true } };
|
|
2453
|
+
}
|
|
2454
|
+
// -----------------------------------------------------------------------
|
|
2455
|
+
// Merge Pipeline
|
|
2456
|
+
// -----------------------------------------------------------------------
|
|
2457
|
+
case "merge.request": {
|
|
2458
|
+
if (!this.mergePipeline) return this.noService(id, "merge-pipeline");
|
|
2459
|
+
const agentId = p.agentId;
|
|
2460
|
+
const sourceBranch = p.sourceBranch;
|
|
2461
|
+
if (!(agentId && sourceBranch)) return this.missingParam(id, "agentId, sourceBranch");
|
|
2462
|
+
const result = await this.mergePipeline.requestMerge({
|
|
2463
|
+
agentId,
|
|
2464
|
+
sourceBranch,
|
|
2465
|
+
taskId: p.taskId,
|
|
2466
|
+
baseBranch: p.baseBranch
|
|
2467
|
+
});
|
|
2468
|
+
return { jsonrpc: "2.0", id, result };
|
|
2469
|
+
}
|
|
2470
|
+
case "merge.status": {
|
|
2471
|
+
if (!this.mergePipeline) return this.noService(id, "merge-pipeline");
|
|
2472
|
+
const mergeId = p.mergeId;
|
|
2473
|
+
if (!mergeId) return this.missingParam(id, "mergeId");
|
|
2474
|
+
const result = await this.mergePipeline.getStatus(mergeId);
|
|
2475
|
+
return { jsonrpc: "2.0", id, result };
|
|
2476
|
+
}
|
|
2477
|
+
case "merge.resolve": {
|
|
2478
|
+
if (!this.mergePipeline) return this.noService(id, "merge-pipeline");
|
|
2479
|
+
const mergeId = p.mergeId;
|
|
2480
|
+
if (!mergeId) return this.missingParam(id, "mergeId");
|
|
2481
|
+
const result = await this.mergePipeline.resolve(mergeId);
|
|
2482
|
+
return { jsonrpc: "2.0", id, result };
|
|
2483
|
+
}
|
|
2484
|
+
case "merge.list": {
|
|
2485
|
+
if (!this.mergePipeline) return this.noService(id, "merge-pipeline");
|
|
2486
|
+
const result = await this.mergePipeline.listActive();
|
|
2487
|
+
return { jsonrpc: "2.0", id, result };
|
|
2488
|
+
}
|
|
2489
|
+
// -----------------------------------------------------------------------
|
|
2490
|
+
// CI Feedback
|
|
2491
|
+
// -----------------------------------------------------------------------
|
|
2492
|
+
case "ci.report": {
|
|
2493
|
+
if (!this.ciFeedback) return this.noService(id, "ci-feedback");
|
|
2494
|
+
const result = await this.ciFeedback.report({
|
|
2495
|
+
prNumber: p.prNumber,
|
|
2496
|
+
branch: p.branch,
|
|
2497
|
+
success: p.success,
|
|
2498
|
+
output: p.output,
|
|
2499
|
+
runUrl: p.runUrl,
|
|
2500
|
+
failedJob: p.failedJob
|
|
2501
|
+
});
|
|
2502
|
+
return { jsonrpc: "2.0", id, result };
|
|
2503
|
+
}
|
|
2504
|
+
// -----------------------------------------------------------------------
|
|
2505
|
+
// VAUGHN Protocol
|
|
2506
|
+
// -----------------------------------------------------------------------
|
|
2507
|
+
case "vaughn.capabilities": {
|
|
2508
|
+
const result = [];
|
|
2509
|
+
for (const adapterId of this.registry.listAll()) {
|
|
2510
|
+
const caps = TOOL_PROFILES[adapterId];
|
|
2511
|
+
if (caps) result.push({ id: adapterId, capabilities: caps });
|
|
2512
|
+
}
|
|
2513
|
+
return { jsonrpc: "2.0", id, result };
|
|
2514
|
+
}
|
|
2515
|
+
case "vaughn.dispatch": {
|
|
2516
|
+
if (!this.vaughnDispatchFn) return this.noService(id, "vaughn-dispatch");
|
|
2517
|
+
const description = p.description;
|
|
2518
|
+
if (!description) return this.missingParam(id, "description");
|
|
2519
|
+
const requirements = p.requirements ?? {};
|
|
2520
|
+
const adapterId = this.vaughnDispatchFn(requirements, description);
|
|
2521
|
+
return { jsonrpc: "2.0", id, result: { adapterId } };
|
|
2522
|
+
}
|
|
2523
|
+
case "vaughn.events": {
|
|
2524
|
+
const limit = p.limit ?? 50;
|
|
2525
|
+
const events = this.vaughnEventQueue.slice(-limit);
|
|
2526
|
+
return { jsonrpc: "2.0", id, result: events };
|
|
2527
|
+
}
|
|
2528
|
+
case "vaughn.config.sync": {
|
|
2529
|
+
const config = p.config;
|
|
2530
|
+
if (!config) return this.missingParam(id, "config");
|
|
2531
|
+
const generated = generateAllConfigs(config);
|
|
2532
|
+
const files = {};
|
|
2533
|
+
for (const [path, content] of generated.files) {
|
|
2534
|
+
files[path] = content;
|
|
2535
|
+
}
|
|
2536
|
+
return { jsonrpc: "2.0", id, result: { files } };
|
|
2537
|
+
}
|
|
2538
|
+
default:
|
|
2539
|
+
return {
|
|
2540
|
+
jsonrpc: "2.0",
|
|
2541
|
+
id,
|
|
2542
|
+
error: { code: ERR_METHOD_NOT_FOUND, message: `Method not found: ${method}` }
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
/** Helper: store not configured error. */
|
|
2547
|
+
noStore(id) {
|
|
2548
|
+
return {
|
|
2549
|
+
jsonrpc: "2.0",
|
|
2550
|
+
id,
|
|
2551
|
+
error: { code: ERR_INTERNAL, message: "Daemon store not initialized" }
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
/** Helper: missing parameter error. */
|
|
2555
|
+
missingParam(id, param) {
|
|
2556
|
+
return {
|
|
2557
|
+
jsonrpc: "2.0",
|
|
2558
|
+
id,
|
|
2559
|
+
error: { code: ERR_INVALID_PARAMS, message: `Missing required parameter: ${param}` }
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
/** Helper: service not configured error. */
|
|
2563
|
+
noService(id, service) {
|
|
2564
|
+
return {
|
|
2565
|
+
jsonrpc: "2.0",
|
|
2566
|
+
id,
|
|
2567
|
+
error: { code: ERR_INTERNAL, message: `${service} service not initialized` }
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
/**
|
|
2571
|
+
* Dispatch an HTTP request body (JSON-RPC) and call the reply callback.
|
|
2572
|
+
* Used by HttpGateway to proxy requests without going through a socket.
|
|
2573
|
+
*/
|
|
2574
|
+
dispatchHttp(body, reply) {
|
|
2575
|
+
this.handleLine(body.trim(), reply);
|
|
2576
|
+
}
|
|
2577
|
+
/** Set the health check function (called by coordinator after construction). */
|
|
2578
|
+
setHealthCheck(fn) {
|
|
2579
|
+
this.healthCheckFn = fn;
|
|
2580
|
+
}
|
|
2581
|
+
/** Attach the spawner service (called by coordinator after construction). */
|
|
2582
|
+
setSpawner(spawner) {
|
|
2583
|
+
this.spawner = spawner;
|
|
2584
|
+
}
|
|
2585
|
+
/** Attach the inference service (called by coordinator after construction). */
|
|
2586
|
+
setInference(inference) {
|
|
2587
|
+
this.inference = inference;
|
|
2588
|
+
}
|
|
2589
|
+
/** Attach the merge pipeline (called by coordinator after construction). */
|
|
2590
|
+
setMergePipeline(pipeline) {
|
|
2591
|
+
this.mergePipeline = pipeline;
|
|
2592
|
+
}
|
|
2593
|
+
/** Attach the CI feedback handler (called by coordinator after construction). */
|
|
2594
|
+
setCIFeedback(feedback) {
|
|
2595
|
+
this.ciFeedback = feedback;
|
|
2596
|
+
}
|
|
2597
|
+
/** Attach the VAUGHN dispatch function (called by coordinator after construction). */
|
|
2598
|
+
setVaughnDispatch(fn) {
|
|
2599
|
+
this.vaughnDispatchFn = fn;
|
|
2600
|
+
}
|
|
2601
|
+
/** Push a VAUGHN event into the recent event queue (capped at 100). */
|
|
2602
|
+
pushVaughnEvent(event) {
|
|
2603
|
+
this.vaughnEventQueue.push(event);
|
|
2604
|
+
if (this.vaughnEventQueue.length > _RpcServer.MAX_VAUGHN_EVENTS) {
|
|
2605
|
+
this.vaughnEventQueue.shift();
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
/** Get the spawner service (used by HTTP gateway for SSE). */
|
|
2609
|
+
getSpawner() {
|
|
2610
|
+
return this.spawner;
|
|
2611
|
+
}
|
|
2612
|
+
start() {
|
|
2613
|
+
if (existsSync3(this.socketPath)) {
|
|
2614
|
+
unlinkSync(this.socketPath);
|
|
2615
|
+
}
|
|
2616
|
+
return new Promise((resolve, reject) => {
|
|
2617
|
+
this.server.listen(this.socketPath, () => resolve());
|
|
2618
|
+
this.server.once("error", reject);
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
stop() {
|
|
2622
|
+
return new Promise((resolve) => this.server.close(() => resolve()));
|
|
2623
|
+
}
|
|
2624
|
+
};
|
|
2625
|
+
|
|
2626
|
+
// src/server/spawner-service.ts
|
|
2627
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
2628
|
+
import { randomUUID } from "crypto";
|
|
2629
|
+
import { EventEmitter } from "events";
|
|
2630
|
+
var DEFAULT_CONFIG2 = {
|
|
2631
|
+
snapEndpoint: "http://localhost:9090",
|
|
2632
|
+
maxSessions: 8
|
|
2633
|
+
};
|
|
2634
|
+
var SpawnerService = class extends EventEmitter {
|
|
2635
|
+
sessions = /* @__PURE__ */ new Map();
|
|
2636
|
+
config;
|
|
2637
|
+
constructor(overrides) {
|
|
2638
|
+
super();
|
|
2639
|
+
this.config = { ...DEFAULT_CONFIG2, ...overrides };
|
|
2640
|
+
}
|
|
2641
|
+
/** Spawn a new agent process. Returns the session ID. */
|
|
2642
|
+
spawn(name, backend, model, prompt) {
|
|
2643
|
+
if (this.sessions.size >= this.config.maxSessions) {
|
|
2644
|
+
throw new Error(`Max sessions (${this.config.maxSessions}) reached`);
|
|
2645
|
+
}
|
|
2646
|
+
const sessionId = randomUUID();
|
|
2647
|
+
let child;
|
|
2648
|
+
switch (backend) {
|
|
2649
|
+
case "Snap": {
|
|
2650
|
+
const body = JSON.stringify({
|
|
2651
|
+
model,
|
|
2652
|
+
messages: [{ role: "user", content: prompt }],
|
|
2653
|
+
stream: false
|
|
2654
|
+
});
|
|
2655
|
+
child = nodeSpawn(
|
|
2656
|
+
"curl",
|
|
2657
|
+
[
|
|
2658
|
+
"-s",
|
|
2659
|
+
"-X",
|
|
2660
|
+
"POST",
|
|
2661
|
+
`${this.config.snapEndpoint}/v1/chat/completions`,
|
|
2662
|
+
"-H",
|
|
2663
|
+
"Content-Type: application/json",
|
|
2664
|
+
"-d",
|
|
2665
|
+
body
|
|
2666
|
+
],
|
|
2667
|
+
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
2668
|
+
);
|
|
2669
|
+
break;
|
|
2670
|
+
}
|
|
2671
|
+
case "Ollama": {
|
|
2672
|
+
child = nodeSpawn("ollama", ["run", model, prompt], {
|
|
2673
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2674
|
+
});
|
|
2675
|
+
break;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
const proc = {
|
|
2679
|
+
name,
|
|
2680
|
+
model,
|
|
2681
|
+
backend,
|
|
2682
|
+
prompt,
|
|
2683
|
+
child,
|
|
2684
|
+
status: "running"
|
|
2685
|
+
};
|
|
2686
|
+
this.sessions.set(sessionId, proc);
|
|
2687
|
+
child.stdout?.on("data", (chunk) => {
|
|
2688
|
+
const data = chunk.toString();
|
|
2689
|
+
if (data.length > 0) {
|
|
2690
|
+
this.emit("output", { sessionId, stream: "stdout", data });
|
|
2691
|
+
}
|
|
2692
|
+
});
|
|
2693
|
+
child.stderr?.on("data", (chunk) => {
|
|
2694
|
+
const data = chunk.toString();
|
|
2695
|
+
if (data.length > 0) {
|
|
2696
|
+
this.emit("output", { sessionId, stream: "stderr", data });
|
|
2697
|
+
}
|
|
2698
|
+
});
|
|
2699
|
+
child.on("close", (code) => {
|
|
2700
|
+
proc.status = code === 0 ? "stopped" : "errored";
|
|
2701
|
+
this.emit("exit", { sessionId, code });
|
|
2702
|
+
});
|
|
2703
|
+
child.on("error", () => {
|
|
2704
|
+
proc.status = "errored";
|
|
2705
|
+
this.emit("exit", { sessionId, code: null });
|
|
2706
|
+
});
|
|
2707
|
+
return sessionId;
|
|
2708
|
+
}
|
|
2709
|
+
/** Stop a running agent by killing its process. */
|
|
2710
|
+
stop(sessionId) {
|
|
2711
|
+
const proc = this.sessions.get(sessionId);
|
|
2712
|
+
if (!proc) throw new Error(`No agent session: ${sessionId}`);
|
|
2713
|
+
if (proc.status !== "running") throw new Error(`Agent is not running (${proc.status})`);
|
|
2714
|
+
proc.child.kill("SIGTERM");
|
|
2715
|
+
proc.status = "stopped";
|
|
2716
|
+
}
|
|
2717
|
+
/** List all agent sessions. */
|
|
2718
|
+
list() {
|
|
2719
|
+
const result = [];
|
|
2720
|
+
for (const [id, proc] of this.sessions) {
|
|
2721
|
+
result.push({
|
|
2722
|
+
id,
|
|
2723
|
+
name: proc.name,
|
|
2724
|
+
model: proc.model,
|
|
2725
|
+
backend: proc.backend,
|
|
2726
|
+
prompt: proc.prompt,
|
|
2727
|
+
status: proc.status,
|
|
2728
|
+
pid: proc.child.pid ?? null
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
return result;
|
|
2732
|
+
}
|
|
2733
|
+
/** Remove a stopped/errored session. */
|
|
2734
|
+
remove(sessionId) {
|
|
2735
|
+
const proc = this.sessions.get(sessionId);
|
|
2736
|
+
if (!proc) throw new Error(`No agent session: ${sessionId}`);
|
|
2737
|
+
if (proc.status === "running")
|
|
2738
|
+
throw new Error("Cannot remove a running agent - stop it first");
|
|
2739
|
+
this.sessions.delete(sessionId);
|
|
2740
|
+
}
|
|
2741
|
+
/** Kill all running agents (called on daemon shutdown). */
|
|
2742
|
+
stopAll() {
|
|
2743
|
+
for (const [, proc] of this.sessions) {
|
|
2744
|
+
if (proc.status === "running") {
|
|
2745
|
+
proc.child.kill("SIGTERM");
|
|
2746
|
+
proc.status = "stopped";
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
};
|
|
2751
|
+
|
|
2752
|
+
// src/coordinator.ts
|
|
2753
|
+
var HarnessCoordinator = class {
|
|
2754
|
+
constructor(options) {
|
|
2755
|
+
this.options = options;
|
|
2756
|
+
const workboardPath = join4(options.projectRoot, ".claude", "workboard.md");
|
|
2757
|
+
this.workboard = new WorkboardManager(workboardPath);
|
|
2758
|
+
}
|
|
2759
|
+
options;
|
|
2760
|
+
registry = new HarnessRegistry();
|
|
2761
|
+
vaughnCapabilities = /* @__PURE__ */ new Map();
|
|
2762
|
+
rpcServer = null;
|
|
2763
|
+
httpGateway = null;
|
|
2764
|
+
store = null;
|
|
2765
|
+
spawner = null;
|
|
2766
|
+
inference = null;
|
|
2767
|
+
mergePipeline = null;
|
|
2768
|
+
ciFeedback = null;
|
|
2769
|
+
sessionId = null;
|
|
2770
|
+
workboard;
|
|
2771
|
+
async start() {
|
|
2772
|
+
await autoDetectHarnesses(this.registry);
|
|
2773
|
+
const type = detectSessionType();
|
|
2774
|
+
const state = this.workboard.read();
|
|
2775
|
+
const existingIds = state.agents.map((a) => a.id);
|
|
2776
|
+
this.sessionId = deriveSessionId(type, existingIds);
|
|
2777
|
+
const envLabels = {
|
|
2778
|
+
zed: "Zed/ACP",
|
|
2779
|
+
cursor: "Cursor",
|
|
2780
|
+
terminal: "WSL/bash"
|
|
2781
|
+
};
|
|
2782
|
+
this.workboard.registerAgent({
|
|
2783
|
+
id: this.sessionId,
|
|
2784
|
+
env: envLabels[type] ?? type,
|
|
2785
|
+
started: `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}Z`,
|
|
2786
|
+
task: this.options.task ?? "Harness coordination active",
|
|
2787
|
+
files: "",
|
|
2788
|
+
updated: `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}Z`
|
|
2789
|
+
});
|
|
2790
|
+
const dataDir = join4(process.env.HOME ?? "/tmp", ".local", "share", "revealui", "harness.db");
|
|
2791
|
+
mkdirSync2(dataDir, { recursive: true });
|
|
2792
|
+
this.store = new DaemonStore({ dataDir });
|
|
2793
|
+
await this.store.init();
|
|
2794
|
+
const socketPath = this.options.socketPath ?? join4(process.env.HOME ?? "/tmp", ".local", "share", "revealui", "harness.sock");
|
|
2795
|
+
this.rpcServer = new RpcServer(this.registry, socketPath, this.store);
|
|
2796
|
+
this.rpcServer.setHealthCheck(() => this.healthCheck());
|
|
2797
|
+
this.rpcServer.setVaughnDispatch((req, desc) => this.dispatchTask(req, desc));
|
|
2798
|
+
this.spawner = new SpawnerService();
|
|
2799
|
+
this.inference = new InferenceService();
|
|
2800
|
+
this.rpcServer.setSpawner(this.spawner);
|
|
2801
|
+
this.rpcServer.setInference(this.inference);
|
|
2802
|
+
this.mergePipeline = new MergePipeline(this.store, {
|
|
2803
|
+
repoRoot: this.options.projectRoot
|
|
2804
|
+
});
|
|
2805
|
+
this.ciFeedback = new CIFeedback(this.store);
|
|
2806
|
+
this.rpcServer.setMergePipeline(this.mergePipeline);
|
|
2807
|
+
this.rpcServer.setCIFeedback(this.ciFeedback);
|
|
2808
|
+
await this.rpcServer.start();
|
|
2809
|
+
if (this.options.httpPort) {
|
|
2810
|
+
this.httpGateway = new HttpGateway({
|
|
2811
|
+
port: this.options.httpPort,
|
|
2812
|
+
host: this.options.httpHost ?? "0.0.0.0",
|
|
2813
|
+
staticDir: this.options.httpStaticDir,
|
|
2814
|
+
rpcDispatch: this.rpcServer,
|
|
2815
|
+
spawner: this.spawner
|
|
2816
|
+
});
|
|
2817
|
+
await this.httpGateway.start();
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
async stop() {
|
|
2821
|
+
if (this.sessionId) {
|
|
2822
|
+
this.workboard.unregisterAgent(this.sessionId);
|
|
2823
|
+
this.workboard.addLogEntry(
|
|
2824
|
+
this.sessionId,
|
|
2825
|
+
`Session ended \u2014 ${this.options.task ?? "harness coordination"}`
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
if (this.httpGateway) {
|
|
2829
|
+
await this.httpGateway.stop();
|
|
2830
|
+
this.httpGateway = null;
|
|
2831
|
+
}
|
|
2832
|
+
if (this.spawner) {
|
|
2833
|
+
await this.spawner.stopAll();
|
|
2834
|
+
this.spawner = null;
|
|
2835
|
+
}
|
|
2836
|
+
this.inference = null;
|
|
2837
|
+
this.mergePipeline = null;
|
|
2838
|
+
this.ciFeedback = null;
|
|
2839
|
+
if (this.rpcServer) {
|
|
2840
|
+
await this.rpcServer.stop();
|
|
2841
|
+
this.rpcServer = null;
|
|
2842
|
+
}
|
|
2843
|
+
if (this.store) {
|
|
2844
|
+
await this.store.close();
|
|
2845
|
+
this.store = null;
|
|
2846
|
+
}
|
|
2847
|
+
await this.registry.disposeAll();
|
|
2848
|
+
}
|
|
2849
|
+
/** The registry of detected harnesses. Available after start(). */
|
|
2850
|
+
getRegistry() {
|
|
2851
|
+
return this.registry;
|
|
2852
|
+
}
|
|
2853
|
+
/** The workboard manager. */
|
|
2854
|
+
getWorkboard() {
|
|
2855
|
+
return this.workboard;
|
|
2856
|
+
}
|
|
2857
|
+
/** The daemon persistent store (available after start()). */
|
|
2858
|
+
getStore() {
|
|
2859
|
+
return this.store;
|
|
2860
|
+
}
|
|
2861
|
+
/** Register a custom adapter (must be called before start()). */
|
|
2862
|
+
registerAdapter(adapter) {
|
|
2863
|
+
this.registry.register(adapter);
|
|
2864
|
+
}
|
|
2865
|
+
/** Register explicit VAUGHN capabilities for an adapter. */
|
|
2866
|
+
registerVaughnCapabilities(adapterId, capabilities) {
|
|
2867
|
+
this.vaughnCapabilities.set(adapterId, capabilities);
|
|
2868
|
+
}
|
|
2869
|
+
/**
|
|
2870
|
+
* Dispatch a task to the best-matching adapter based on VAUGHN capability requirements.
|
|
2871
|
+
*
|
|
2872
|
+
* Returns the selected adapter ID, or null if no adapter matches.
|
|
2873
|
+
* Prefers adapters with hooks.canBlock for safety-critical dispatch.
|
|
2874
|
+
*/
|
|
2875
|
+
dispatchTask(requirements, _description) {
|
|
2876
|
+
const candidates = [];
|
|
2877
|
+
for (const id of this.registry.listAll()) {
|
|
2878
|
+
const caps = this.vaughnCapabilities.get(id) ?? TOOL_PROFILES[id];
|
|
2879
|
+
if (!caps) continue;
|
|
2880
|
+
if (this.matchesRequirements(caps, requirements)) {
|
|
2881
|
+
candidates.push({ id, caps });
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
if (candidates.length === 0) return null;
|
|
2885
|
+
const blocking = candidates.filter((c) => c.caps.hooks.canBlock);
|
|
2886
|
+
const best = blocking.length > 0 ? blocking[0] : candidates[0];
|
|
2887
|
+
return best?.id ?? null;
|
|
2888
|
+
}
|
|
2889
|
+
/** Check whether capabilities satisfy requirements. */
|
|
2890
|
+
matchesRequirements(caps, req) {
|
|
2891
|
+
if (req.dispatch) {
|
|
2892
|
+
if (req.dispatch.generateCode && !caps.dispatch.generateCode) return false;
|
|
2893
|
+
if (req.dispatch.analyzeCode && !caps.dispatch.analyzeCode) return false;
|
|
2894
|
+
if (req.dispatch.applyEdit && !caps.dispatch.applyEdit) return false;
|
|
2895
|
+
if (req.dispatch.executeCommand && !caps.dispatch.executeCommand) return false;
|
|
2896
|
+
}
|
|
2897
|
+
if (req.headless && !caps.headless) return false;
|
|
2898
|
+
if (req.resumable && !caps.resumable) return false;
|
|
2899
|
+
if (req.forkable && !caps.forkable) return false;
|
|
2900
|
+
if (req.backgroundable && !caps.backgroundable) return false;
|
|
2901
|
+
if (req.readWorkboard && !caps.readWorkboard) return false;
|
|
2902
|
+
if (req.writeWorkboard && !caps.writeWorkboard) return false;
|
|
2903
|
+
if (req.claimTasks && !caps.claimTasks) return false;
|
|
2904
|
+
if (req.reportConflicts && !caps.reportConflicts) return false;
|
|
2905
|
+
if (req.supportsWorktrees && !caps.supportsWorktrees) return false;
|
|
2906
|
+
if (req.supportsSkills && !caps.supportsSkills) return false;
|
|
2907
|
+
if (req.supportsMcp && !caps.supportsMcp) return false;
|
|
2908
|
+
if (req.hooks) {
|
|
2909
|
+
if (req.hooks.supported && !caps.hooks.supported) return false;
|
|
2910
|
+
if (req.hooks.canBlock && !caps.hooks.canBlock) return false;
|
|
2911
|
+
}
|
|
2912
|
+
if (req.sandbox?.supported && !caps.sandbox.supported) return false;
|
|
2913
|
+
if (req.memory?.supported && !caps.memory.supported) return false;
|
|
2914
|
+
return true;
|
|
2915
|
+
}
|
|
2916
|
+
/** The HTTP gateway (available after start() if httpPort was set). */
|
|
2917
|
+
getHttpGateway() {
|
|
2918
|
+
return this.httpGateway;
|
|
2919
|
+
}
|
|
2920
|
+
/** Run a health check across all registered harnesses and the workboard. */
|
|
2921
|
+
async healthCheck() {
|
|
2922
|
+
const diagnostics = [];
|
|
2923
|
+
const StaleMs = 4 * 60 * 60 * 1e3;
|
|
2924
|
+
const allIds = this.registry.listAll();
|
|
2925
|
+
const registeredHarnesses = await Promise.all(
|
|
2926
|
+
allIds.map(async (harnessId) => {
|
|
2927
|
+
const adapter = this.registry.get(harnessId);
|
|
2928
|
+
let available = false;
|
|
2929
|
+
try {
|
|
2930
|
+
available = adapter ? await adapter.isAvailable() : false;
|
|
2931
|
+
} catch (err) {
|
|
2932
|
+
diagnostics.push(
|
|
2933
|
+
`${harnessId}: availability check failed \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
2934
|
+
);
|
|
2935
|
+
}
|
|
2936
|
+
if (!available) diagnostics.push(`${harnessId}: not available`);
|
|
2937
|
+
return { harnessId, available };
|
|
2938
|
+
})
|
|
2939
|
+
);
|
|
2940
|
+
let readable = false;
|
|
2941
|
+
let sessionCount = 0;
|
|
2942
|
+
const staleSessionIds = [];
|
|
2943
|
+
try {
|
|
2944
|
+
const state = this.workboard.read();
|
|
2945
|
+
readable = true;
|
|
2946
|
+
sessionCount = state.agents.length;
|
|
2947
|
+
const now = Date.now();
|
|
2948
|
+
for (const s of state.agents) {
|
|
2949
|
+
const ts = Date.parse(s.updated);
|
|
2950
|
+
if (!Number.isNaN(ts) && now - ts > StaleMs) {
|
|
2951
|
+
staleSessionIds.push(s.id);
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
if (staleSessionIds.length > 0) {
|
|
2955
|
+
diagnostics.push(`Stale sessions: ${staleSessionIds.join(", ")}`);
|
|
2956
|
+
}
|
|
2957
|
+
} catch (err) {
|
|
2958
|
+
diagnostics.push(`Workboard unreadable: ${err instanceof Error ? err.message : String(err)}`);
|
|
2959
|
+
}
|
|
2960
|
+
const healthy = registeredHarnesses.some((h) => h.available) && readable && staleSessionIds.length === 0;
|
|
2961
|
+
return {
|
|
2962
|
+
healthy,
|
|
2963
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2964
|
+
registeredHarnesses,
|
|
2965
|
+
workboard: { readable, sessionCount, staleSessionIds },
|
|
2966
|
+
diagnostics
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
};
|
|
2970
|
+
|
|
2971
|
+
// src/index.ts
|
|
2972
|
+
async function checkHarnessesLicense() {
|
|
2973
|
+
await initializeLicense();
|
|
2974
|
+
if (!isFeatureEnabled("ai")) {
|
|
2975
|
+
logger.warn(
|
|
2976
|
+
"[@revealui/harnesses] AI harness integration requires a Pro or Enterprise license. Visit https://revealui.com/pricing for details.",
|
|
2977
|
+
{ feature: "ai" }
|
|
2978
|
+
);
|
|
2979
|
+
return false;
|
|
2980
|
+
}
|
|
2981
|
+
return true;
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
export {
|
|
2985
|
+
createDefaultCapabilities,
|
|
2986
|
+
TOOL_PROFILES,
|
|
2987
|
+
getDegradationStrategy,
|
|
2988
|
+
VAUGHN_VERSION,
|
|
2989
|
+
VAUGHN_EVENTS,
|
|
2990
|
+
vaughnEventSchema,
|
|
2991
|
+
vaughnEventEnvelopeSchema,
|
|
2992
|
+
createEventEnvelope,
|
|
2993
|
+
VaughnEventNormalizer,
|
|
2994
|
+
autoDetectHarnesses,
|
|
2995
|
+
HarnessRegistry,
|
|
2996
|
+
getLocalConfigPath,
|
|
2997
|
+
getRootConfigPath,
|
|
2998
|
+
getConfigurableHarnesses,
|
|
2999
|
+
syncConfig,
|
|
3000
|
+
diffConfig,
|
|
3001
|
+
syncAllConfigs,
|
|
3002
|
+
diffAllConfigs,
|
|
3003
|
+
validateConfigJson,
|
|
3004
|
+
findProcesses,
|
|
3005
|
+
findHarnessProcesses,
|
|
3006
|
+
findAllHarnessProcesses,
|
|
3007
|
+
findClaudeCodeSockets,
|
|
3008
|
+
vaughnConfigToClaudeSettings,
|
|
3009
|
+
claudeSettingsToVaughnConfig,
|
|
3010
|
+
vaughnConfigToCursorrules,
|
|
3011
|
+
vaughnConfigToAgentsMd,
|
|
3012
|
+
generateAllConfigs,
|
|
3013
|
+
RpcServer,
|
|
3014
|
+
HarnessCoordinator,
|
|
3015
|
+
checkHarnessesLicense
|
|
3016
|
+
};
|