@fusionkit/ensemble 0.1.3 → 0.1.5
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/dist/agent.js +6 -1
- package/dist/cursor.d.ts +42 -0
- package/dist/cursor.js +440 -0
- package/dist/dashboard.d.ts +1 -1
- package/dist/dashboard.js +48 -74
- package/dist/test/cursor.test.d.ts +1 -0
- package/dist/test/cursor.test.js +97 -0
- package/dist/test/dashboard.test.js +6 -6
- package/dist/unified.js +12 -2
- package/package.json +7 -7
package/dist/agent.js
CHANGED
|
@@ -34,6 +34,11 @@ function deriveVerification(steps) {
|
|
|
34
34
|
}
|
|
35
35
|
export function createAgentHarness(options) {
|
|
36
36
|
const id = options.id ?? "agent";
|
|
37
|
+
// The base URL is shared across panel models (one `fusionkit serve` router),
|
|
38
|
+
// which routes by the request `model` field. So the request model is the panel
|
|
39
|
+
// *endpoint id* (what the router's passthrough matches), not the provider model
|
|
40
|
+
// name. With a dedicated per-model endpoint the id is ignored, so this is safe
|
|
41
|
+
// either way.
|
|
37
42
|
return {
|
|
38
43
|
id,
|
|
39
44
|
harnessKind: "generic",
|
|
@@ -82,7 +87,7 @@ export function createAgentHarness(options) {
|
|
|
82
87
|
worktree: root,
|
|
83
88
|
prompt: descriptor.prompt,
|
|
84
89
|
baseUrl,
|
|
85
|
-
model: model.
|
|
90
|
+
model: model.id,
|
|
86
91
|
abortSignal: AbortSignal.timeout(modelTimeoutMs),
|
|
87
92
|
...(options.turn !== undefined ? { turn: options.turn } : {}),
|
|
88
93
|
...(options.apiKey !== undefined ? { apiKey: options.apiKey } : {}),
|
package/dist/cursor.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { EnsembleModel, HarnessAdapter } from "./harness.js";
|
|
2
|
+
export type CursorRunMode = "ask" | "agent";
|
|
3
|
+
export type CursorExecInput = {
|
|
4
|
+
prompt: string;
|
|
5
|
+
cwd: string;
|
|
6
|
+
fusionBackendUrl: string;
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
model: EnsembleModel;
|
|
9
|
+
cursorKitDir: string;
|
|
10
|
+
command: string;
|
|
11
|
+
modelName: string;
|
|
12
|
+
providerModel: string;
|
|
13
|
+
mode: CursorRunMode;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
env: Record<string, string>;
|
|
16
|
+
};
|
|
17
|
+
export type CursorExecResult = {
|
|
18
|
+
status: "succeeded" | "failed";
|
|
19
|
+
transcript: string;
|
|
20
|
+
diff?: string;
|
|
21
|
+
toolEvents: number;
|
|
22
|
+
exitCode?: number;
|
|
23
|
+
reason?: string;
|
|
24
|
+
};
|
|
25
|
+
export type CursorExecRunner = (input: CursorExecInput) => Promise<CursorExecResult> | CursorExecResult;
|
|
26
|
+
export type CursorHarnessOptions = {
|
|
27
|
+
id?: string;
|
|
28
|
+
command?: string;
|
|
29
|
+
cursorKitDir?: string;
|
|
30
|
+
fusionBackendUrl?: string;
|
|
31
|
+
apiKey?: string;
|
|
32
|
+
modelName?: string;
|
|
33
|
+
providerModel?: string;
|
|
34
|
+
mode?: CursorRunMode;
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
env?: Record<string, string | undefined>;
|
|
37
|
+
runner?: CursorExecRunner;
|
|
38
|
+
skipWhenUnavailable?: boolean;
|
|
39
|
+
};
|
|
40
|
+
export declare function cursorHarnessUnavailableReason(env?: Record<string, string | undefined>, options?: Pick<CursorHarnessOptions, "command" | "cursorKitDir">): string | undefined;
|
|
41
|
+
export declare function createCursorHarness(options?: CursorHarnessOptions): HarnessAdapter;
|
|
42
|
+
export declare const cursorHarness: typeof createCursorHarness;
|
package/dist/cursor.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { delimiter, join } from "node:path";
|
|
4
|
+
import { artifactHash } from "@fusionkit/protocol";
|
|
5
|
+
const DEFAULT_CURSOR_COMMAND = "cursor-agent";
|
|
6
|
+
const DEFAULT_BRIDGE_MODEL_NAME = "local-fusion";
|
|
7
|
+
const DEFAULT_BRIDGE_PROVIDER_MODEL = "fusion-panel";
|
|
8
|
+
const BRIDGE_START_TIMEOUT_MS = 20_000;
|
|
9
|
+
function definedEnv(env) {
|
|
10
|
+
const result = {};
|
|
11
|
+
for (const [key, value] of Object.entries(env)) {
|
|
12
|
+
if (value !== undefined)
|
|
13
|
+
result[key] = value;
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
function normalizeModelBaseUrl(fusionBackendUrl) {
|
|
18
|
+
const trimmed = fusionBackendUrl.replace(/\/+$/, "");
|
|
19
|
+
return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
|
|
20
|
+
}
|
|
21
|
+
function commandOnPath(command, env) {
|
|
22
|
+
if (command.includes("/")) {
|
|
23
|
+
return existsSync(command);
|
|
24
|
+
}
|
|
25
|
+
const pathValue = env.PATH ?? process.env.PATH ?? "";
|
|
26
|
+
return pathValue
|
|
27
|
+
.split(delimiter)
|
|
28
|
+
.filter((entry) => entry.length > 0)
|
|
29
|
+
.some((dir) => existsSync(join(dir, command)));
|
|
30
|
+
}
|
|
31
|
+
function resolveCursorKitDir(options, env) {
|
|
32
|
+
return (options.cursorKitDir ??
|
|
33
|
+
env.WARRANT_CURSORKIT_DIR ??
|
|
34
|
+
env.FUSIONKIT_CURSORKIT_DIR);
|
|
35
|
+
}
|
|
36
|
+
function resolveAvailability(options, env) {
|
|
37
|
+
const command = options.command ?? DEFAULT_CURSOR_COMMAND;
|
|
38
|
+
if (options.runner !== undefined) {
|
|
39
|
+
return {
|
|
40
|
+
available: true,
|
|
41
|
+
cursorKitDir: resolveCursorKitDir(options, env) ?? ".",
|
|
42
|
+
command
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const cursorKitDir = resolveCursorKitDir(options, env);
|
|
46
|
+
if (cursorKitDir === undefined) {
|
|
47
|
+
return {
|
|
48
|
+
available: false,
|
|
49
|
+
reason: "Cursorkit checkout is not configured; set WARRANT_CURSORKIT_DIR or pass cursorKitDir."
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (!existsSync(join(cursorKitDir, "dist/src/cli.js"))) {
|
|
53
|
+
return {
|
|
54
|
+
available: false,
|
|
55
|
+
reason: `Cursorkit bridge build was not found at ${join(cursorKitDir, "dist/src/cli.js")}; run pnpm build in the Cursorkit checkout.`
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (!commandOnPath(command, env)) {
|
|
59
|
+
return {
|
|
60
|
+
available: false,
|
|
61
|
+
reason: `Cursor CLI "${command}" was not found on PATH; install the Cursor CLI (https://cursor.com/cli) and log in.`
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { available: true, cursorKitDir, command };
|
|
65
|
+
}
|
|
66
|
+
export function cursorHarnessUnavailableReason(env = process.env, options = {}) {
|
|
67
|
+
const availability = resolveAvailability(options, definedEnv(env));
|
|
68
|
+
return availability.available ? undefined : availability.reason;
|
|
69
|
+
}
|
|
70
|
+
function modeFor(descriptor, override) {
|
|
71
|
+
if (override !== undefined)
|
|
72
|
+
return override;
|
|
73
|
+
switch (descriptor.policy.sideEffects) {
|
|
74
|
+
case "none":
|
|
75
|
+
case "read_only":
|
|
76
|
+
return "ask";
|
|
77
|
+
case "writes_workspace":
|
|
78
|
+
case "network":
|
|
79
|
+
case "tool_execution":
|
|
80
|
+
case "unknown":
|
|
81
|
+
return "agent";
|
|
82
|
+
default: {
|
|
83
|
+
const exhausted = descriptor.policy.sideEffects;
|
|
84
|
+
throw new Error(`unsupported side effects policy: ${String(exhausted)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function skippedCandidate(input) {
|
|
89
|
+
const transcript = `Cursor adapter skipped: ${input.reason}`;
|
|
90
|
+
const hash = artifactHash(transcript);
|
|
91
|
+
return {
|
|
92
|
+
candidateId: `${input.descriptor.id}_${input.model.id}_${input.ordinal}`,
|
|
93
|
+
model: input.model,
|
|
94
|
+
status: "skipped",
|
|
95
|
+
transcript,
|
|
96
|
+
log: transcript,
|
|
97
|
+
artifacts: [
|
|
98
|
+
{
|
|
99
|
+
artifact_id: `artifact_${input.descriptor.id}_${input.model.id}_cursor_skip`,
|
|
100
|
+
kind: "log",
|
|
101
|
+
hash,
|
|
102
|
+
redaction_status: "synthetic"
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
verification: {
|
|
106
|
+
status: "skipped",
|
|
107
|
+
evidence: [input.reason]
|
|
108
|
+
},
|
|
109
|
+
error: {
|
|
110
|
+
kind: "capability_missing",
|
|
111
|
+
message: input.reason,
|
|
112
|
+
retryable: false
|
|
113
|
+
},
|
|
114
|
+
metadata: {
|
|
115
|
+
adapter: "cursor",
|
|
116
|
+
skip_reason: input.reason
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Drives the real cursor-agent CLI in ACP mode against a freshly spawned
|
|
122
|
+
* Cursorkit bridge whose local-model backend points at the fusion gateway.
|
|
123
|
+
* The bridge runs with BRIDGE_AGENT_TOOL_POLICY=all so Cursor can read, edit
|
|
124
|
+
* (apply_patch/write_file), and run shell commands inside the worktree.
|
|
125
|
+
*/
|
|
126
|
+
async function defaultCursorRunner(input) {
|
|
127
|
+
const bridgePort = 9700 + Math.floor(Math.random() * 250);
|
|
128
|
+
const bridgeEnv = { ...input.env };
|
|
129
|
+
for (const key of Object.keys(bridgeEnv)) {
|
|
130
|
+
if (key.startsWith("BRIDGE_") ||
|
|
131
|
+
key.startsWith("MODEL_") ||
|
|
132
|
+
key.startsWith("CURSOR_UPSTREAM")) {
|
|
133
|
+
delete bridgeEnv[key];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
Object.assign(bridgeEnv, {
|
|
137
|
+
BRIDGE_PORT: String(bridgePort),
|
|
138
|
+
BRIDGE_ROUTE_INVENTORY: "true",
|
|
139
|
+
BRIDGE_AGENT_TOOL_POLICY: "all",
|
|
140
|
+
BRIDGE_AGENT_TOOL_MAX_ITERATIONS: "24",
|
|
141
|
+
CURSOR_UPSTREAM_BASE_URL: "https://api2.cursor.sh",
|
|
142
|
+
MODEL_BASE_URL: normalizeModelBaseUrl(input.fusionBackendUrl),
|
|
143
|
+
MODEL_API_KEY: input.apiKey ?? "local",
|
|
144
|
+
MODEL_NAME: input.modelName,
|
|
145
|
+
MODEL_PROVIDER_MODEL: input.providerModel,
|
|
146
|
+
MODEL_CONTEXT_TOKEN_LIMIT: "128000"
|
|
147
|
+
});
|
|
148
|
+
let bridgeOut = "";
|
|
149
|
+
const bridge = spawn(process.execPath, ["dist/src/cli.js", "serve"], {
|
|
150
|
+
cwd: input.cursorKitDir,
|
|
151
|
+
env: bridgeEnv,
|
|
152
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
153
|
+
});
|
|
154
|
+
bridge.stdout.on("data", (chunk) => {
|
|
155
|
+
bridgeOut += chunk.toString("utf8");
|
|
156
|
+
});
|
|
157
|
+
bridge.stderr.on("data", (chunk) => {
|
|
158
|
+
bridgeOut += chunk.toString("utf8");
|
|
159
|
+
});
|
|
160
|
+
const timeoutMs = input.timeoutMs ?? 180_000;
|
|
161
|
+
try {
|
|
162
|
+
const deadline = Date.now() + BRIDGE_START_TIMEOUT_MS;
|
|
163
|
+
while (!/bridge listening/.test(bridgeOut) && Date.now() < deadline) {
|
|
164
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
165
|
+
}
|
|
166
|
+
if (!/bridge listening/.test(bridgeOut)) {
|
|
167
|
+
return {
|
|
168
|
+
status: "failed",
|
|
169
|
+
transcript: bridgeOut,
|
|
170
|
+
toolEvents: 0,
|
|
171
|
+
reason: "Cursorkit bridge did not start in time."
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const printResult = await driveCursorAgentPrint({
|
|
175
|
+
command: input.command,
|
|
176
|
+
bridgePort,
|
|
177
|
+
modelName: input.modelName,
|
|
178
|
+
mode: input.mode,
|
|
179
|
+
cwd: input.cwd,
|
|
180
|
+
prompt: input.prompt,
|
|
181
|
+
timeoutMs
|
|
182
|
+
});
|
|
183
|
+
const diff = captureWorktreeDiff(input.cwd);
|
|
184
|
+
return {
|
|
185
|
+
status: printResult.status,
|
|
186
|
+
transcript: printResult.transcript,
|
|
187
|
+
toolEvents: diff !== undefined && diff.length > 0 ? 1 : 0,
|
|
188
|
+
...(printResult.exitCode !== undefined ? { exitCode: printResult.exitCode } : {}),
|
|
189
|
+
...(diff !== undefined ? { diff } : {}),
|
|
190
|
+
...(printResult.reason !== undefined ? { reason: printResult.reason } : {})
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
bridge.kill("SIGTERM");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Drives cursor-agent in headless print mode (`-p`), which "has access to all
|
|
199
|
+
* tools, including write and shell". The bridge runs the Cursor tool loop over
|
|
200
|
+
* the SSE/BidiAppend transport, so the agent can read, apply_patch/write, and
|
|
201
|
+
* run shell inside the worktree. `--trust` skips the workspace-trust prompt and
|
|
202
|
+
* `--force` auto-approves tool actions. For read-only tasks we pass `--mode ask`.
|
|
203
|
+
*/
|
|
204
|
+
async function driveCursorAgentPrint(input) {
|
|
205
|
+
const args = [
|
|
206
|
+
"-p",
|
|
207
|
+
"--force",
|
|
208
|
+
"--trust",
|
|
209
|
+
"--output-format",
|
|
210
|
+
"text",
|
|
211
|
+
"--model",
|
|
212
|
+
input.modelName,
|
|
213
|
+
"--endpoint",
|
|
214
|
+
`http://127.0.0.1:${input.bridgePort}`
|
|
215
|
+
];
|
|
216
|
+
if (input.mode === "ask") {
|
|
217
|
+
args.push("--mode", "ask");
|
|
218
|
+
}
|
|
219
|
+
args.push(input.prompt);
|
|
220
|
+
return await new Promise((resolve) => {
|
|
221
|
+
const child = spawn(input.command, args, {
|
|
222
|
+
cwd: input.cwd,
|
|
223
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
224
|
+
});
|
|
225
|
+
let stdout = "";
|
|
226
|
+
let stderr = "";
|
|
227
|
+
let timedOut = false;
|
|
228
|
+
const timer = setTimeout(() => {
|
|
229
|
+
timedOut = true;
|
|
230
|
+
child.kill("SIGTERM");
|
|
231
|
+
}, input.timeoutMs);
|
|
232
|
+
child.stdout.on("data", (chunk) => {
|
|
233
|
+
stdout += chunk.toString("utf8");
|
|
234
|
+
});
|
|
235
|
+
child.stderr.on("data", (chunk) => {
|
|
236
|
+
stderr += chunk.toString("utf8");
|
|
237
|
+
});
|
|
238
|
+
child.on("error", (error) => {
|
|
239
|
+
clearTimeout(timer);
|
|
240
|
+
resolve({
|
|
241
|
+
status: "failed",
|
|
242
|
+
transcript: stdout,
|
|
243
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
child.on("exit", (code) => {
|
|
247
|
+
clearTimeout(timer);
|
|
248
|
+
const transcript = [stdout, stderr].filter(Boolean).join("\n");
|
|
249
|
+
if (timedOut) {
|
|
250
|
+
resolve({
|
|
251
|
+
status: "failed",
|
|
252
|
+
transcript,
|
|
253
|
+
reason: "cursor-agent timed out"
|
|
254
|
+
});
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
resolve({
|
|
258
|
+
status: code === 0 ? "succeeded" : "failed",
|
|
259
|
+
transcript,
|
|
260
|
+
exitCode: code ?? 0,
|
|
261
|
+
...(code === 0 ? {} : { reason: stderr.slice(0, 500) })
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function captureWorktreeDiff(cwd) {
|
|
267
|
+
try {
|
|
268
|
+
const result = spawnSync("git", ["-C", cwd, "diff"], { encoding: "utf8" });
|
|
269
|
+
const stdout = result.stdout ?? "";
|
|
270
|
+
return result.status === 0 && stdout.length > 0 ? stdout : undefined;
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
export function createCursorHarness(options = {}) {
|
|
277
|
+
const id = options.id ?? "cursor";
|
|
278
|
+
const runner = options.runner ?? defaultCursorRunner;
|
|
279
|
+
const skipWhenUnavailable = options.skipWhenUnavailable ?? true;
|
|
280
|
+
return {
|
|
281
|
+
id,
|
|
282
|
+
harnessKind: "cursor",
|
|
283
|
+
prepare: () => {
|
|
284
|
+
const env = definedEnv(options.env ?? process.env);
|
|
285
|
+
return { env, availability: resolveAvailability(options, env) };
|
|
286
|
+
},
|
|
287
|
+
capabilities: () => {
|
|
288
|
+
const env = definedEnv(options.env ?? process.env);
|
|
289
|
+
const available = resolveAvailability(options, env).available;
|
|
290
|
+
const status = available ? "supported" : "degraded";
|
|
291
|
+
return {
|
|
292
|
+
workspace_read: status,
|
|
293
|
+
workspace_write: status,
|
|
294
|
+
apply_patch: status,
|
|
295
|
+
tool_call_loop: status,
|
|
296
|
+
tool_records: status,
|
|
297
|
+
verification: status,
|
|
298
|
+
route_observation: "supported",
|
|
299
|
+
adapter_available: available ? "supported" : "unsupported"
|
|
300
|
+
};
|
|
301
|
+
},
|
|
302
|
+
verificationProfile: () => ({
|
|
303
|
+
id: `${id}-verification`,
|
|
304
|
+
requiredEvidence: [
|
|
305
|
+
"cursor-agent transcript",
|
|
306
|
+
"session status",
|
|
307
|
+
"worktree diff or skip reason"
|
|
308
|
+
]
|
|
309
|
+
}),
|
|
310
|
+
run: async ({ descriptor, model, ordinal, prepared, worktree }) => {
|
|
311
|
+
const state = prepared;
|
|
312
|
+
if (!state.availability.available) {
|
|
313
|
+
if (!skipWhenUnavailable) {
|
|
314
|
+
throw new Error(state.availability.reason);
|
|
315
|
+
}
|
|
316
|
+
return skippedCandidate({
|
|
317
|
+
descriptor,
|
|
318
|
+
model,
|
|
319
|
+
ordinal,
|
|
320
|
+
reason: state.availability.reason
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
const fusionBackendUrl = options.fusionBackendUrl ?? state.env.FUSIONKIT_BASE_URL;
|
|
324
|
+
if (fusionBackendUrl === undefined || fusionBackendUrl.length === 0) {
|
|
325
|
+
return skippedCandidate({
|
|
326
|
+
descriptor,
|
|
327
|
+
model,
|
|
328
|
+
ordinal,
|
|
329
|
+
reason: "Fusion backend URL is not configured for the Cursor harness."
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const cwd = worktree?.path ?? descriptor.workspace ?? process.cwd();
|
|
333
|
+
let result;
|
|
334
|
+
try {
|
|
335
|
+
result = await runner({
|
|
336
|
+
prompt: descriptor.prompt,
|
|
337
|
+
cwd,
|
|
338
|
+
fusionBackendUrl,
|
|
339
|
+
...(options.apiKey !== undefined ? { apiKey: options.apiKey } : {}),
|
|
340
|
+
model,
|
|
341
|
+
cursorKitDir: state.availability.cursorKitDir,
|
|
342
|
+
command: state.availability.command,
|
|
343
|
+
modelName: options.modelName ?? DEFAULT_BRIDGE_MODEL_NAME,
|
|
344
|
+
providerModel: options.providerModel ?? model.model ?? DEFAULT_BRIDGE_PROVIDER_MODEL,
|
|
345
|
+
mode: modeFor(descriptor, options.mode),
|
|
346
|
+
...(options.timeoutMs !== undefined
|
|
347
|
+
? { timeoutMs: options.timeoutMs }
|
|
348
|
+
: descriptor.policy.timeoutMs !== undefined
|
|
349
|
+
? { timeoutMs: descriptor.policy.timeoutMs }
|
|
350
|
+
: {}),
|
|
351
|
+
env: state.env
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
return skippedCandidate({
|
|
356
|
+
descriptor,
|
|
357
|
+
model,
|
|
358
|
+
ordinal,
|
|
359
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
const transcript = result.transcript;
|
|
363
|
+
const outputHash = artifactHash(transcript.length > 0 ? transcript : `cursor:${descriptor.id}`);
|
|
364
|
+
const status = result.status;
|
|
365
|
+
const candidateId = `${descriptor.id}_${model.id}_${ordinal}`;
|
|
366
|
+
const artifacts = [
|
|
367
|
+
{
|
|
368
|
+
artifact_id: `artifact_${descriptor.id}_${model.id}_cursor_transcript`,
|
|
369
|
+
kind: "transcript",
|
|
370
|
+
hash: outputHash,
|
|
371
|
+
redaction_status: "synthetic"
|
|
372
|
+
}
|
|
373
|
+
];
|
|
374
|
+
if (result.diff !== undefined && result.diff.length > 0) {
|
|
375
|
+
artifacts.push({
|
|
376
|
+
artifact_id: `artifact_${descriptor.id}_${model.id}_cursor_patch`,
|
|
377
|
+
kind: "patch",
|
|
378
|
+
hash: artifactHash(result.diff),
|
|
379
|
+
redaction_status: "synthetic"
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
candidateId,
|
|
384
|
+
model,
|
|
385
|
+
status,
|
|
386
|
+
...(worktree
|
|
387
|
+
? { branchName: worktree.branchName, worktreePath: worktree.path }
|
|
388
|
+
: {}),
|
|
389
|
+
transcript,
|
|
390
|
+
...(result.diff !== undefined ? { diff: result.diff } : {}),
|
|
391
|
+
log: transcript,
|
|
392
|
+
artifacts,
|
|
393
|
+
toolRecords: [
|
|
394
|
+
{
|
|
395
|
+
execution_id: `exec_${candidateId}_cursor`,
|
|
396
|
+
plan_id: `plan_${candidateId}_cursor`,
|
|
397
|
+
status,
|
|
398
|
+
output_hash: outputHash,
|
|
399
|
+
...(status === "failed"
|
|
400
|
+
? {
|
|
401
|
+
error: {
|
|
402
|
+
kind: "provider_error",
|
|
403
|
+
message: result.reason ?? "Cursor run failed.",
|
|
404
|
+
retryable: false
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
: {})
|
|
408
|
+
}
|
|
409
|
+
],
|
|
410
|
+
verification: {
|
|
411
|
+
status,
|
|
412
|
+
evidence: [
|
|
413
|
+
`tool_events=${result.toolEvents}`,
|
|
414
|
+
outputHash,
|
|
415
|
+
...(result.diff !== undefined ? ["worktree_diff"] : [])
|
|
416
|
+
],
|
|
417
|
+
...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {})
|
|
418
|
+
},
|
|
419
|
+
...(status === "failed"
|
|
420
|
+
? {
|
|
421
|
+
error: {
|
|
422
|
+
kind: "provider_error",
|
|
423
|
+
message: result.reason ?? "Cursor run failed.",
|
|
424
|
+
retryable: false
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
: {}),
|
|
428
|
+
metadata: {
|
|
429
|
+
adapter: "cursor",
|
|
430
|
+
mode: modeFor(descriptor, options.mode),
|
|
431
|
+
tool_events: result.toolEvents,
|
|
432
|
+
has_diff: result.diff !== undefined && result.diff.length > 0
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
},
|
|
436
|
+
collectArtifacts: () => [],
|
|
437
|
+
cleanup: () => undefined
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
export const cursorHarness = createCursorHarness;
|
package/dist/dashboard.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HarnessRunResultV1, ModelFusionHarnessKind } from "@fusionkit/protocol";
|
|
2
2
|
import type { HarnessAdapter, HarnessCapabilities } from "./harness.js";
|
|
3
|
-
declare const LIVE_SMOKE_TARGETS: readonly ["claude-code", "codex"];
|
|
3
|
+
declare const LIVE_SMOKE_TARGETS: readonly ["claude-code", "codex", "cursor"];
|
|
4
4
|
export type HarnessCapabilityTarget = "cursor" | "claude-code" | "codex" | "command" | "mock";
|
|
5
5
|
export type HarnessAvailability = "available" | "credential_gated" | "missing";
|
|
6
6
|
export type HarnessLiveSmokeTarget = (typeof LIVE_SMOKE_TARGETS)[number];
|
package/dist/dashboard.js
CHANGED
|
@@ -5,6 +5,7 @@ import { gitText } from "@fusionkit/workspace";
|
|
|
5
5
|
import { claudeCodeHarness, claudeCodeHarnessCredentialSkipReason } from "./claude-code.js";
|
|
6
6
|
import { createCommandHarness } from "./command.js";
|
|
7
7
|
import { codexHarness, codexHarnessCredentialSkipReason } from "./codex.js";
|
|
8
|
+
import { createCursorHarness, cursorHarnessUnavailableReason } from "./cursor.js";
|
|
8
9
|
import { createMockHarness } from "./mock.js";
|
|
9
10
|
import { runEnsemble } from "./run.js";
|
|
10
11
|
const PRODUCER_GIT_SHA = "0".repeat(40);
|
|
@@ -18,10 +19,12 @@ const DEFAULT_COMMAND_FAILURE = "exit 7";
|
|
|
18
19
|
const DEFAULT_OUTPUT_DIR = ".warrant/ensemble-dashboard";
|
|
19
20
|
const CLAUDE_LIVE_SMOKE_ENV = "WARRANT_CLAUDE_SMOKE";
|
|
20
21
|
const CODEX_LIVE_SMOKE_ENV = "WARRANT_CODEX_SMOKE";
|
|
22
|
+
const CURSOR_LIVE_SMOKE_ENV = "WARRANT_CURSOR_SMOKE";
|
|
21
23
|
const ALL_LIVE_SMOKE_ENV = "WARRANT_ENSEMBLE_LIVE_SMOKE";
|
|
22
|
-
const LIVE_SMOKE_TARGETS = ["claude-code", "codex"];
|
|
24
|
+
const LIVE_SMOKE_TARGETS = ["claude-code", "codex", "cursor"];
|
|
23
25
|
const CLAUDE_LIVE_SMOKE_PROMPT = "Read README.md if present, then reply exactly CLAUDE_LIVE_SMOKE_OK. Do not modify files.";
|
|
24
26
|
const CODEX_LIVE_SMOKE_PROMPT = "Read README.md if present, then reply exactly CODEX_LIVE_SMOKE_OK. Do not modify files.";
|
|
27
|
+
const CURSOR_LIVE_SMOKE_PROMPT = "Read README.md if present, then reply exactly CURSOR_LIVE_SMOKE_OK. Do not modify files.";
|
|
25
28
|
function metadata(schema, createdAt) {
|
|
26
29
|
return {
|
|
27
30
|
schema,
|
|
@@ -51,6 +54,8 @@ function liveSmokeEnvName(target) {
|
|
|
51
54
|
return CLAUDE_LIVE_SMOKE_ENV;
|
|
52
55
|
case "codex":
|
|
53
56
|
return CODEX_LIVE_SMOKE_ENV;
|
|
57
|
+
case "cursor":
|
|
58
|
+
return CURSOR_LIVE_SMOKE_ENV;
|
|
54
59
|
default: {
|
|
55
60
|
const exhausted = target;
|
|
56
61
|
throw new Error(`unsupported live smoke target: ${String(exhausted)}`);
|
|
@@ -95,29 +100,18 @@ function displayNameFor(target) {
|
|
|
95
100
|
return assertNever(target);
|
|
96
101
|
}
|
|
97
102
|
}
|
|
98
|
-
function cursorCapabilities() {
|
|
99
|
-
return {
|
|
100
|
-
workspace_read: "degraded",
|
|
101
|
-
workspace_write: "degraded",
|
|
102
|
-
apply_patch: "degraded",
|
|
103
|
-
tool_records: "degraded",
|
|
104
|
-
verification: "degraded",
|
|
105
|
-
proprietary_harness: "unsupported",
|
|
106
|
-
adapter_available: "unsupported"
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
103
|
function dashboardCapabilitiesFor(target) {
|
|
110
104
|
switch (target) {
|
|
111
105
|
case "cursor":
|
|
112
106
|
return {
|
|
113
|
-
model_override: "
|
|
114
|
-
transcript_capture: "
|
|
115
|
-
diff_capture: "
|
|
116
|
-
tool_loop_capture: "
|
|
117
|
-
patch_apply_visibility: "
|
|
118
|
-
route_model_observation: "
|
|
119
|
-
verification_hint: "
|
|
120
|
-
replay_support: "
|
|
107
|
+
model_override: "supported",
|
|
108
|
+
transcript_capture: "supported",
|
|
109
|
+
diff_capture: "supported",
|
|
110
|
+
tool_loop_capture: "supported",
|
|
111
|
+
patch_apply_visibility: "supported",
|
|
112
|
+
route_model_observation: "supported",
|
|
113
|
+
verification_hint: "supported",
|
|
114
|
+
replay_support: "degraded"
|
|
121
115
|
};
|
|
122
116
|
case "claude-code":
|
|
123
117
|
return {
|
|
@@ -209,9 +203,11 @@ export function createHarnessCapabilityMatrix(options = {}) {
|
|
|
209
203
|
const rows = [
|
|
210
204
|
matrixRow({
|
|
211
205
|
harnessId: "cursor",
|
|
212
|
-
availability: "
|
|
213
|
-
capabilities: matrixCapabilities("cursor",
|
|
214
|
-
notes: [
|
|
206
|
+
availability: "credential_gated",
|
|
207
|
+
capabilities: matrixCapabilities("cursor", adapterCapabilities(createCursorHarness({ env }))),
|
|
208
|
+
notes: [
|
|
209
|
+
"Credential-gated; requires a logged-in Cursor CLI and a built Cursorkit checkout (WARRANT_CURSORKIT_DIR)."
|
|
210
|
+
]
|
|
215
211
|
}),
|
|
216
212
|
matrixRow({
|
|
217
213
|
harnessId: "claude-code",
|
|
@@ -272,33 +268,6 @@ function smokeDescriptor(input) {
|
|
|
272
268
|
outputRoot: join(input.outputRoot, "runs", input.id)
|
|
273
269
|
};
|
|
274
270
|
}
|
|
275
|
-
function unsupportedCursorResult(input) {
|
|
276
|
-
const result = {
|
|
277
|
-
...metadata("harness-run-result.v1", input.createdAt),
|
|
278
|
-
result_id: `ensemble_result_${input.taskId}`,
|
|
279
|
-
request_id: `ensemble_req_${input.taskId}`,
|
|
280
|
-
harness_kind: "cursor",
|
|
281
|
-
status: "unsupported",
|
|
282
|
-
candidate_ids: [],
|
|
283
|
-
output_summary: "Cursor harness unavailable in CI-safe package context.",
|
|
284
|
-
capabilities: cursorCapabilities(),
|
|
285
|
-
started_at: input.createdAt,
|
|
286
|
-
finished_at: input.createdAt,
|
|
287
|
-
errors: [
|
|
288
|
-
{
|
|
289
|
-
kind: "capability_missing",
|
|
290
|
-
message: "Cursor proprietary harness is not available from @fusionkit/ensemble.",
|
|
291
|
-
retryable: false
|
|
292
|
-
}
|
|
293
|
-
],
|
|
294
|
-
metadata: {
|
|
295
|
-
dashboard_outcome: "missing",
|
|
296
|
-
harness_id: "cursor"
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
assertHarnessRunResultV1(result);
|
|
300
|
-
return result;
|
|
301
|
-
}
|
|
302
271
|
function liveSmokePreflightFailureResult(input) {
|
|
303
272
|
const result = {
|
|
304
273
|
...metadata("harness-run-result.v1", input.createdAt),
|
|
@@ -382,21 +351,6 @@ async function runSmokeTask(input) {
|
|
|
382
351
|
resultPath
|
|
383
352
|
};
|
|
384
353
|
}
|
|
385
|
-
if (input.run.harnessId === "cursor") {
|
|
386
|
-
const result = unsupportedCursorResult({
|
|
387
|
-
createdAt: input.createdAt,
|
|
388
|
-
taskId: input.run.taskId
|
|
389
|
-
});
|
|
390
|
-
const resultPath = writeRunResult(input.outputRoot, input.run.taskId, result);
|
|
391
|
-
return {
|
|
392
|
-
taskId: input.run.taskId,
|
|
393
|
-
harnessId: input.run.harnessId,
|
|
394
|
-
purpose: input.run.purpose,
|
|
395
|
-
outcome: input.run.outcome,
|
|
396
|
-
result,
|
|
397
|
-
resultPath
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
354
|
const descriptor = smokeDescriptor({
|
|
401
355
|
id: input.run.taskId,
|
|
402
356
|
harness: input.run.harness,
|
|
@@ -476,14 +430,14 @@ function smokeRuns(options) {
|
|
|
476
430
|
allowedTools: ["read_file", "apply_patch"]
|
|
477
431
|
},
|
|
478
432
|
{
|
|
479
|
-
taskId: "cursor-
|
|
433
|
+
taskId: "cursor-skipped",
|
|
480
434
|
harnessId: "cursor",
|
|
481
|
-
purpose: "
|
|
482
|
-
outcome: "
|
|
483
|
-
harness:
|
|
484
|
-
model: { id: "cursor", model: "
|
|
435
|
+
purpose: "credential-skip",
|
|
436
|
+
outcome: "skipped",
|
|
437
|
+
harness: createCursorHarness({ env: {} }),
|
|
438
|
+
model: { id: "cursor", model: "fusion-panel" },
|
|
485
439
|
sideEffects: "writes_workspace",
|
|
486
|
-
allowedTools: ["read_file", "write_file", "apply_patch"]
|
|
440
|
+
allowedTools: ["read_file", "write_file", "apply_patch", "run_shell"]
|
|
487
441
|
}
|
|
488
442
|
];
|
|
489
443
|
}
|
|
@@ -526,6 +480,25 @@ function liveSmokeRuns(options) {
|
|
|
526
480
|
preflightFailureReason: codexHarnessCredentialSkipReason(options.env)
|
|
527
481
|
});
|
|
528
482
|
}
|
|
483
|
+
if (options.targets.includes("cursor")) {
|
|
484
|
+
const harness = options.harnesses?.cursor ??
|
|
485
|
+
createCursorHarness({ env: options.env, skipWhenUnavailable: false });
|
|
486
|
+
runs.push({
|
|
487
|
+
taskId: "cursor-live",
|
|
488
|
+
harnessId: "cursor",
|
|
489
|
+
purpose: "live",
|
|
490
|
+
outcome: "success",
|
|
491
|
+
harness,
|
|
492
|
+
model: {
|
|
493
|
+
id: "cursor",
|
|
494
|
+
model: options.env.WARRANT_CURSOR_SMOKE_MODEL ?? "fusion-panel"
|
|
495
|
+
},
|
|
496
|
+
sideEffects: "read_only",
|
|
497
|
+
allowedTools: ["read_file"],
|
|
498
|
+
prompt: CURSOR_LIVE_SMOKE_PROMPT,
|
|
499
|
+
preflightFailureReason: cursorHarnessUnavailableReason(options.env)
|
|
500
|
+
});
|
|
501
|
+
}
|
|
529
502
|
return runs;
|
|
530
503
|
}
|
|
531
504
|
function capabilityCell(capabilities, capability) {
|
|
@@ -592,7 +565,9 @@ function credentialStateFor(harnessId, env) {
|
|
|
592
565
|
case "mock":
|
|
593
566
|
return "not required";
|
|
594
567
|
case "cursor":
|
|
595
|
-
return
|
|
568
|
+
return cursorHarnessUnavailableReason(env) === undefined
|
|
569
|
+
? "credentials available"
|
|
570
|
+
: "credentials missing/skipped";
|
|
596
571
|
default:
|
|
597
572
|
return assertNever(harnessId);
|
|
598
573
|
}
|
|
@@ -604,9 +579,8 @@ function contractReadinessFor(harnessId) {
|
|
|
604
579
|
return "contract/mock ready";
|
|
605
580
|
case "claude-code":
|
|
606
581
|
case "codex":
|
|
607
|
-
return "contract/mock ready";
|
|
608
582
|
case "cursor":
|
|
609
|
-
return "
|
|
583
|
+
return "contract/mock ready";
|
|
610
584
|
default:
|
|
611
585
|
return assertNever(harnessId);
|
|
612
586
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import { cursorHarness } from "../cursor.js";
|
|
7
|
+
import { createMockHarness } from "../mock.js";
|
|
8
|
+
import { ensemble } from "../run.js";
|
|
9
|
+
function tempOutputRoot() {
|
|
10
|
+
const outputRoot = mkdtempSync(join(tmpdir(), "ensemble-cursor-out-"));
|
|
11
|
+
return {
|
|
12
|
+
outputRoot,
|
|
13
|
+
cleanup: () => rmSync(outputRoot, { recursive: true, force: true })
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function descriptor(outputRoot, overrides = {}) {
|
|
17
|
+
return {
|
|
18
|
+
id: "cursor_ensemble_test",
|
|
19
|
+
harness: createMockHarness(),
|
|
20
|
+
models: [{ id: "cursor", model: "fusion-panel" }],
|
|
21
|
+
runtime: { id: "local" },
|
|
22
|
+
judge: { id: "judge", model: "fake-judge" },
|
|
23
|
+
policy: {
|
|
24
|
+
id: "policy",
|
|
25
|
+
allowedTools: ["read_file", "apply_patch", "run_shell"],
|
|
26
|
+
sideEffects: "writes_workspace",
|
|
27
|
+
timeoutMs: 1_000
|
|
28
|
+
},
|
|
29
|
+
prompt: "Fix the failing test in the repo.",
|
|
30
|
+
sourceRepo: "handoffkit",
|
|
31
|
+
baseGitSha: "b".repeat(40),
|
|
32
|
+
outputRoot,
|
|
33
|
+
...overrides
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
test("cursor adapter skips clearly when Cursorkit is not configured", async () => {
|
|
37
|
+
const { outputRoot, cleanup } = tempOutputRoot();
|
|
38
|
+
try {
|
|
39
|
+
const result = await ensemble.run(descriptor(outputRoot, {
|
|
40
|
+
harness: cursorHarness({ env: {} })
|
|
41
|
+
}));
|
|
42
|
+
assert.equal(result.harnessRunResult.status, "skipped");
|
|
43
|
+
assert.equal(result.candidates[0]?.status, "skipped");
|
|
44
|
+
assert.equal(result.candidates[0]?.error?.kind, "capability_missing");
|
|
45
|
+
assert.match(result.candidates[0]?.error?.message ?? "", /Cursorkit checkout is not configured/);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
cleanup();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
test("cursor adapter produces a real candidate with a diff via the injected runner", async () => {
|
|
52
|
+
const { outputRoot, cleanup } = tempOutputRoot();
|
|
53
|
+
let observedMode;
|
|
54
|
+
let observedBackend;
|
|
55
|
+
const runner = (input) => {
|
|
56
|
+
observedMode = input.mode;
|
|
57
|
+
observedBackend = input.fusionBackendUrl;
|
|
58
|
+
return {
|
|
59
|
+
status: "succeeded",
|
|
60
|
+
transcript: "Applied the fix and verified the tests pass.",
|
|
61
|
+
diff: "--- a/calc.ts\n+++ b/calc.ts\n@@ -1,1 +1,1 @@\n-return a - b;\n+return a + b;",
|
|
62
|
+
toolEvents: 3
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
const result = await ensemble.run(descriptor(outputRoot, {
|
|
67
|
+
harness: cursorHarness({
|
|
68
|
+
fusionBackendUrl: "http://127.0.0.1:9999",
|
|
69
|
+
runner
|
|
70
|
+
})
|
|
71
|
+
}));
|
|
72
|
+
assert.equal(observedMode, "agent");
|
|
73
|
+
assert.equal(observedBackend, "http://127.0.0.1:9999");
|
|
74
|
+
assert.equal(result.harnessRunResult.status, "succeeded");
|
|
75
|
+
const candidate = result.candidates[0];
|
|
76
|
+
assert.equal(candidate?.status, "succeeded");
|
|
77
|
+
assert.equal(candidate?.metadata?.adapter, "cursor");
|
|
78
|
+
assert.equal(candidate?.metadata?.tool_events, 3);
|
|
79
|
+
assert.equal(candidate?.metadata?.has_diff, true);
|
|
80
|
+
assert.ok(result.artifacts.some((artifact) => artifact.kind === "patch"), "a patch artifact should be captured");
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
cleanup();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
test("cursor adapter capabilities report supported when available", () => {
|
|
87
|
+
const runner = () => ({
|
|
88
|
+
status: "succeeded",
|
|
89
|
+
transcript: "ok",
|
|
90
|
+
toolEvents: 0
|
|
91
|
+
});
|
|
92
|
+
const harness = cursorHarness({ runner, fusionBackendUrl: "http://x/v1" });
|
|
93
|
+
const capabilities = harness.capabilities({});
|
|
94
|
+
assert.equal(capabilities.apply_patch, "supported");
|
|
95
|
+
assert.equal(capabilities.tool_call_loop, "supported");
|
|
96
|
+
assert.equal(capabilities.adapter_available, "supported");
|
|
97
|
+
});
|
|
@@ -37,7 +37,7 @@ test("capability matrix covers Cursor, Claude Code, Codex, command, and mock", (
|
|
|
37
37
|
assert.ok(matrix.capabilities.includes("replay_support"));
|
|
38
38
|
assert.ok(matrix.capabilities.includes("workspace_read"));
|
|
39
39
|
assert.ok(matrix.capabilities.includes("verification"));
|
|
40
|
-
assert.equal(matrix.rows.find((row) => row.harnessId === "cursor")?.availability, "
|
|
40
|
+
assert.equal(matrix.rows.find((row) => row.harnessId === "cursor")?.availability, "credential_gated");
|
|
41
41
|
assert.equal(matrix.rows.find((row) => row.harnessId === "claude-code")?.harnessKind, "claude_code");
|
|
42
42
|
assert.equal(matrix.rows.find((row) => row.harnessId === "codex")?.harnessKind, "codex");
|
|
43
43
|
});
|
|
@@ -63,15 +63,15 @@ test("smoke dashboard writes schema-valid success, failure, skipped, and missing
|
|
|
63
63
|
"failed",
|
|
64
64
|
"skipped",
|
|
65
65
|
"skipped",
|
|
66
|
+
"skipped",
|
|
66
67
|
"succeeded",
|
|
67
|
-
"succeeded"
|
|
68
|
-
"unsupported"
|
|
68
|
+
"succeeded"
|
|
69
69
|
]);
|
|
70
70
|
assert.equal(dashboard.records.find((record) => record.taskId === "claude-code-skipped")?.result
|
|
71
71
|
.harness_kind, "claude_code");
|
|
72
72
|
assert.equal(dashboard.records.find((record) => record.taskId === "codex-skipped")?.result.harness_kind, "codex");
|
|
73
|
-
assert.equal(dashboard.records.find((record) => record.taskId === "cursor-
|
|
74
|
-
|
|
73
|
+
assert.equal(dashboard.records.find((record) => record.taskId === "cursor-skipped")?.result.harness_kind, "cursor");
|
|
74
|
+
assert.equal(dashboard.records.find((record) => record.taskId === "cursor-skipped")?.result.status, "skipped");
|
|
75
75
|
const markdown = readFileSync(dashboard.dashboardPath, "utf8");
|
|
76
76
|
assert.match(markdown, /# HandoffKit Harness Smoke Dashboard/);
|
|
77
77
|
assert.match(markdown, /## Capability Matrix/);
|
|
@@ -80,7 +80,7 @@ test("smoke dashboard writes schema-valid success, failure, skipped, and missing
|
|
|
80
80
|
assert.match(markdown, /credentials missing\/skipped/);
|
|
81
81
|
assert.match(markdown, /live smoke not requested/);
|
|
82
82
|
assert.match(markdown, /command-failure/);
|
|
83
|
-
assert.match(markdown, /cursor-
|
|
83
|
+
assert.match(markdown, /cursor-skipped/);
|
|
84
84
|
assert.match(markdown, /harness-run-results\/mock-success\.json/);
|
|
85
85
|
assert.equal(dashboard.readiness.length, 5);
|
|
86
86
|
}
|
package/dist/unified.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createAgentHarness } from "./agent.js";
|
|
|
7
7
|
import { claudeCodeHarness } from "./claude-code.js";
|
|
8
8
|
import { createCommandHarness } from "./command.js";
|
|
9
9
|
import { codexHarness } from "./codex.js";
|
|
10
|
+
import { createCursorHarness } from "./cursor.js";
|
|
10
11
|
import { createMockHarness } from "./mock.js";
|
|
11
12
|
import { runEnsemble } from "./run.js";
|
|
12
13
|
function normalizeFusionBackendUrl(value) {
|
|
@@ -84,7 +85,13 @@ function harnessAdapter(kind, options) {
|
|
|
84
85
|
return claudeCodeHarness({ timeoutMs: options.timeoutMs });
|
|
85
86
|
case "cursor-acp":
|
|
86
87
|
case "cursor-desktop":
|
|
87
|
-
|
|
88
|
+
return createCursorHarness({
|
|
89
|
+
id: kind,
|
|
90
|
+
fusionBackendUrl: normalizeFusionBackendUrl(options.fusionBackendUrl),
|
|
91
|
+
...(options.fusionApiKey ? { apiKey: options.fusionApiKey } : {}),
|
|
92
|
+
...(options.cursorKitDir !== undefined ? { cursorKitDir: options.cursorKitDir } : {}),
|
|
93
|
+
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {})
|
|
94
|
+
});
|
|
88
95
|
default: {
|
|
89
96
|
const exhausted = kind;
|
|
90
97
|
throw new Error(`unsupported unified harness: ${String(exhausted)}`);
|
|
@@ -385,7 +392,10 @@ export async function runUnifiedHarnessE2E(options) {
|
|
|
385
392
|
mkdirSync(outputRoot, { recursive: true });
|
|
386
393
|
const results = [];
|
|
387
394
|
for (const kind of options.harnesses) {
|
|
388
|
-
if (kind === "cursor-acp" || kind === "cursor-desktop")
|
|
395
|
+
if ((kind === "cursor-acp" || kind === "cursor-desktop") &&
|
|
396
|
+
options.cursorRunner !== undefined) {
|
|
397
|
+
// Explicit probe runner: drive the Cursorkit harness suite and record a
|
|
398
|
+
// route/transcript probe instead of producing real ensemble candidates.
|
|
389
399
|
results.push(await runCursorHarness(kind, options));
|
|
390
400
|
continue;
|
|
391
401
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusionkit/ensemble",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.5",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/velum-labs/handoffkit.git",
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
"provenance": true
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@fusionkit/
|
|
29
|
-
"@fusionkit/
|
|
30
|
-
"@fusionkit/
|
|
31
|
-
"@fusionkit/
|
|
32
|
-
"@fusionkit/
|
|
33
|
-
"@fusionkit/
|
|
28
|
+
"@fusionkit/adapter-ai-sdk": "0.1.5",
|
|
29
|
+
"@fusionkit/model-gateway": "0.1.5",
|
|
30
|
+
"@fusionkit/runner": "0.1.5",
|
|
31
|
+
"@fusionkit/session-harness": "0.1.5",
|
|
32
|
+
"@fusionkit/workspace": "0.1.5",
|
|
33
|
+
"@fusionkit/protocol": "0.1.5"
|
|
34
34
|
}
|
|
35
35
|
}
|