@fusionkit/cli 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +4 -17
- package/dist/commands/ensemble-gateway.js +0 -2
- package/dist/commands/ensemble-records.d.ts +2 -1
- package/dist/commands/ensemble-records.js +3 -1
- package/dist/commands/ensemble.js +3 -4
- package/dist/commands/fusion.js +14 -15
- package/dist/commands/local.js +3 -3
- package/dist/cursor-acp.d.ts +18 -0
- package/dist/cursor-acp.js +206 -0
- package/dist/dashboard.d.ts +65 -0
- package/dist/dashboard.js +587 -0
- package/dist/fusion/env.d.ts +108 -0
- package/dist/fusion/env.js +98 -0
- package/dist/fusion/observability.d.ts +39 -0
- package/dist/fusion/observability.js +227 -0
- package/dist/fusion/preflight.d.ts +12 -0
- package/dist/fusion/preflight.js +42 -0
- package/dist/fusion/stack.d.ts +62 -0
- package/dist/fusion/stack.js +295 -0
- package/dist/fusion-config.d.ts +0 -1
- package/dist/fusion-config.js +0 -6
- package/dist/fusion-init.js +2 -11
- package/dist/fusion-quickstart.d.ts +11 -222
- package/dist/fusion-quickstart.js +57 -759
- package/dist/gateway.d.ts +0 -2
- package/dist/gateway.js +12 -2
- package/dist/local.d.ts +10 -17
- package/dist/local.js +50 -116
- package/dist/shared/options.d.ts +2 -1
- package/dist/shared/options.js +13 -19
- package/dist/shared/proc.d.ts +4 -70
- package/dist/shared/proc.js +3 -228
- package/dist/test/cli.test.js +32 -142
- package/dist/test/dashboard.test.d.ts +1 -0
- package/dist/test/dashboard.test.js +214 -0
- package/dist/test/gateway-e2e.test.js +13 -10
- package/dist/test/local.test.js +4 -4
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +25 -0
- package/package.json +14 -9
- package/scope/.next/BUILD_ID +1 -1
- package/scope/.next/app-build-manifest.json +12 -12
- package/scope/.next/app-path-routes-manifest.json +3 -3
- package/scope/.next/build-manifest.json +2 -2
- package/scope/.next/prerender-manifest.json +16 -16
- package/scope/.next/server/app/_not-found.html +1 -1
- package/scope/.next/server/app/_not-found.rsc +1 -1
- package/scope/.next/server/app/environments.html +1 -1
- package/scope/.next/server/app/environments.rsc +1 -1
- package/scope/.next/server/app/index.html +1 -1
- package/scope/.next/server/app/index.rsc +1 -1
- package/scope/.next/server/app/models.html +1 -1
- package/scope/.next/server/app/models.rsc +1 -1
- package/scope/.next/server/app-paths-manifest.json +3 -3
- package/scope/.next/server/functions-config-manifest.json +2 -2
- package/scope/.next/server/pages/404.html +1 -1
- package/scope/.next/server/pages/500.html +1 -1
- package/scope/.next/server/server-reference-manifest.json +1 -1
- package/dist/commands/init.d.ts +0 -2
- package/dist/commands/init.js +0 -24
- package/dist/commands/lifecycle.d.ts +0 -2
- package/dist/commands/lifecycle.js +0 -124
- package/dist/commands/plane.d.ts +0 -2
- package/dist/commands/plane.js +0 -38
- package/dist/commands/run.d.ts +0 -2
- package/dist/commands/run.js +0 -149
- package/dist/commands/runner.d.ts +0 -2
- package/dist/commands/runner.js +0 -33
- package/dist/commands/secrets.d.ts +0 -2
- package/dist/commands/secrets.js +0 -21
- /package/scope/.next/static/{5tnFLuvnSbNZNtqRgoot8 → x7wPUCpgS31-5ZHJkcKsU}/_buildManifest.js +0 -0
- /package/scope/.next/static/{5tnFLuvnSbNZNtqRgoot8 → x7wPUCpgS31-5ZHJkcKsU}/_ssgManifest.js +0 -0
package/dist/test/cli.test.js
CHANGED
|
@@ -6,16 +6,19 @@ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync
|
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import {
|
|
9
|
+
import { test } from "node:test";
|
|
10
10
|
import { MODEL_FUSION_SCHEMA_BUNDLE_HASH } from "@fusionkit/protocol";
|
|
11
|
-
import { makeRepo as makeStackRepo, mockRunRequest, startStack, uploadWorkspace } from "@fusionkit/testkit";
|
|
12
11
|
const CLI = fileURLToPath(new URL("../index.js", import.meta.url));
|
|
13
12
|
const SMOKE_ENV_KEYS = [
|
|
13
|
+
"FUSIONKIT_CLAUDE_SMOKE",
|
|
14
|
+
"FUSIONKIT_CODEX_SMOKE",
|
|
15
|
+
"FUSIONKIT_CURSOR_SMOKE",
|
|
16
|
+
"FUSIONKIT_ENSEMBLE_LIVE_SMOKE",
|
|
14
17
|
"WARRANT_CLAUDE_SMOKE",
|
|
15
18
|
"WARRANT_CODEX_SMOKE",
|
|
19
|
+
"WARRANT_CURSOR_SMOKE",
|
|
16
20
|
"WARRANT_ENSEMBLE_LIVE_SMOKE"
|
|
17
21
|
];
|
|
18
|
-
let home;
|
|
19
22
|
async function readBody(req) {
|
|
20
23
|
const chunks = [];
|
|
21
24
|
for await (const chunk of req)
|
|
@@ -100,7 +103,7 @@ function warrant(args, options = {}) {
|
|
|
100
103
|
else
|
|
101
104
|
env[key] = value;
|
|
102
105
|
}
|
|
103
|
-
const result = spawnSync(process.execPath, [CLI,
|
|
106
|
+
const result = spawnSync(process.execPath, [CLI, ...args], {
|
|
104
107
|
encoding: "utf8",
|
|
105
108
|
env,
|
|
106
109
|
input: options.input
|
|
@@ -122,7 +125,7 @@ async function warrantAsync(args, options = {}) {
|
|
|
122
125
|
env[key] = value;
|
|
123
126
|
}
|
|
124
127
|
return await new Promise((resolve) => {
|
|
125
|
-
const child = spawn(process.execPath, [CLI,
|
|
128
|
+
const child = spawn(process.execPath, [CLI, ...args], {
|
|
126
129
|
env,
|
|
127
130
|
stdio: ["pipe", "pipe", "pipe"]
|
|
128
131
|
});
|
|
@@ -145,18 +148,11 @@ async function warrantAsync(args, options = {}) {
|
|
|
145
148
|
}
|
|
146
149
|
});
|
|
147
150
|
}
|
|
148
|
-
before(() => {
|
|
149
|
-
home = mkdtempSync(join(tmpdir(), "warrant-cli-test-"));
|
|
150
|
-
rmSync(home, { recursive: true, force: true });
|
|
151
|
-
});
|
|
152
|
-
after(() => {
|
|
153
|
-
rmSync(home, { recursive: true, force: true });
|
|
154
|
-
});
|
|
155
151
|
test("help prints usage and lists the top-level commands", () => {
|
|
156
152
|
const result = warrant(["help"]);
|
|
157
153
|
assert.equal(result.status, 0);
|
|
158
154
|
assert.match(result.stdout, /real model fusion behind your coding agent/);
|
|
159
|
-
for (const command of ["
|
|
155
|
+
for (const command of ["init", "ensemble", "local", "fusion", "codex", "claude", "cursor", "serve"]) {
|
|
160
156
|
assert.match(result.stdout, new RegExp(`\\b${command}\\b`));
|
|
161
157
|
}
|
|
162
158
|
});
|
|
@@ -186,12 +182,12 @@ test("gateway acp-registry rejects an unknown action", () => {
|
|
|
186
182
|
test("local without a tool prints usage and fails", () => {
|
|
187
183
|
const result = warrant(["local"]);
|
|
188
184
|
assert.equal(result.status, 1);
|
|
189
|
-
assert.match(result.stderr, /usage:
|
|
185
|
+
assert.match(result.stderr, /usage: fusionkit local </);
|
|
190
186
|
});
|
|
191
187
|
test("local rejects an unknown tool", () => {
|
|
192
188
|
const result = warrant(["local", "frobnicate"]);
|
|
193
189
|
assert.equal(result.status, 1);
|
|
194
|
-
assert.match(result.stderr, /usage:
|
|
190
|
+
assert.match(result.stderr, /usage: fusionkit local </);
|
|
195
191
|
});
|
|
196
192
|
test("local help documents the flags-before-tool contract", () => {
|
|
197
193
|
const result = warrant(["local", "--help"]);
|
|
@@ -203,78 +199,29 @@ test("fusion help documents the flags-before-tool contract", () => {
|
|
|
203
199
|
assert.equal(result.status, 0);
|
|
204
200
|
assert.match(result.stdout, /must precede the tool name/);
|
|
205
201
|
});
|
|
206
|
-
test("init
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
assert.match(again.stderr, /already initialized/);
|
|
225
|
-
});
|
|
226
|
-
test("secrets are stored encrypted and listed by name only", () => {
|
|
227
|
-
const set = warrant(["secrets", "set", "NPM_TOKEN", "super-secret-value"]);
|
|
228
|
-
assert.equal(set.status, 0, set.stderr);
|
|
229
|
-
assert.match(set.stdout, /encrypted at rest/);
|
|
230
|
-
const list = warrant(["secrets", "list"]);
|
|
231
|
-
assert.equal(list.status, 0);
|
|
232
|
-
assert.equal(list.stdout.trim(), "NPM_TOKEN");
|
|
233
|
-
const stored = readFileSync(join(home, "secrets.enc"), "utf8");
|
|
234
|
-
assert.ok(!stored.includes("super-secret-value"), "value must be encrypted");
|
|
235
|
-
});
|
|
236
|
-
test("ui prints the control panel address and login token", () => {
|
|
237
|
-
const result = warrant(["ui"]);
|
|
238
|
-
assert.equal(result.status, 0);
|
|
239
|
-
assert.match(result.stdout, /control panel: http:\/\/127\.0\.0\.1:7172\/ui\//);
|
|
240
|
-
assert.match(result.stdout, /login token: {3}\S+/);
|
|
202
|
+
test("init scaffolds a fusionkit.json and refuses to clobber without --force", () => {
|
|
203
|
+
const fixture = makeRepo();
|
|
204
|
+
try {
|
|
205
|
+
const result = warrant(["init", "--repo", fixture.repo]);
|
|
206
|
+
assert.equal(result.status, 0, result.stderr);
|
|
207
|
+
const configPath = join(fixture.repo, "fusionkit.json");
|
|
208
|
+
assert.ok(existsSync(configPath));
|
|
209
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
210
|
+
assert.equal(config.version, "fusionkit.fusion.v1");
|
|
211
|
+
const again = warrant(["init", "--repo", fixture.repo]);
|
|
212
|
+
assert.equal(again.status, 1);
|
|
213
|
+
assert.match(again.stderr, /already exists/);
|
|
214
|
+
const forced = warrant(["init", "--repo", fixture.repo, "--force"]);
|
|
215
|
+
assert.equal(forced.status, 0, forced.stderr);
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
fixture.cleanup();
|
|
219
|
+
}
|
|
241
220
|
});
|
|
242
|
-
test("unknown commands
|
|
221
|
+
test("unknown commands fail with guidance", () => {
|
|
243
222
|
const unknown = warrant(["frobnicate"]);
|
|
244
223
|
assert.equal(unknown.status, 1);
|
|
245
224
|
assert.match(unknown.stderr, /unknown command/);
|
|
246
|
-
const missingAgent = warrant(["run", "do things"]);
|
|
247
|
-
assert.equal(missingAgent.status, 1);
|
|
248
|
-
assert.match(missingAgent.stderr, /--agent is required/);
|
|
249
|
-
const missingTask = warrant(["continue", "--agent", "mock"]);
|
|
250
|
-
assert.equal(missingTask.status, 1);
|
|
251
|
-
assert.match(missingTask.stderr, /task prompt is required/);
|
|
252
|
-
const badAgent = warrant(["continue", "--agent", "nonsense", "task"]);
|
|
253
|
-
assert.equal(badAgent.status, 1);
|
|
254
|
-
assert.match(badAgent.stderr, /unknown agent kind/);
|
|
255
|
-
});
|
|
256
|
-
test("verify fails closed on a tampered bundle file", () => {
|
|
257
|
-
const path = join(home, "garbage.bundle.json");
|
|
258
|
-
const fake = {
|
|
259
|
-
version: "warrant.bundle.v1",
|
|
260
|
-
contract: { signatures: [], workspace: { baseRef: "x" } },
|
|
261
|
-
receipt: {
|
|
262
|
-
contractHash: "0".repeat(64),
|
|
263
|
-
signatures: [],
|
|
264
|
-
status: "completed",
|
|
265
|
-
workspaceIn: { baseRef: "y", manifestHash: "z" },
|
|
266
|
-
workspaceOut: { diffHash: "", artifactHashes: [] },
|
|
267
|
-
secretsReleased: [],
|
|
268
|
-
eventsHead: "",
|
|
269
|
-
eventCount: 0
|
|
270
|
-
},
|
|
271
|
-
events: [],
|
|
272
|
-
keys: { planePublicKeyPem: "", runnerPublicKeyPem: "" }
|
|
273
|
-
};
|
|
274
|
-
writeFileSync(path, JSON.stringify(fake));
|
|
275
|
-
const result = warrant(["verify", path]);
|
|
276
|
-
assert.equal(result.status, 1);
|
|
277
|
-
assert.match(result.stderr, /VERIFICATION FAILED/);
|
|
278
225
|
});
|
|
279
226
|
function makeRepo() {
|
|
280
227
|
const root = mkdtempSync(join(tmpdir(), "warrant-ensemble-cli-"));
|
|
@@ -630,7 +577,7 @@ test("ensemble dashboard writes markdown and run-result records", () => {
|
|
|
630
577
|
assert.match(result.stdout, /records: 6/);
|
|
631
578
|
assert.ok(existsSync(join(fixture.output, "dashboard.md")));
|
|
632
579
|
assert.ok(existsSync(join(fixture.output, "harness-run-results", "mock-success.json")));
|
|
633
|
-
assert.ok(existsSync(join(fixture.output, "harness-run-results", "cursor-
|
|
580
|
+
assert.ok(existsSync(join(fixture.output, "harness-run-results", "cursor-skipped.json")));
|
|
634
581
|
const dashboard = readFileSync(join(fixture.output, "dashboard.md"), "utf8");
|
|
635
582
|
assert.match(dashboard, /Capability Matrix/);
|
|
636
583
|
assert.match(dashboard, /command-failure/);
|
|
@@ -678,7 +625,7 @@ test("ensemble dashboard rejects unknown live-smoke targets", () => {
|
|
|
678
625
|
"--out",
|
|
679
626
|
fixture.output,
|
|
680
627
|
"--live-smoke",
|
|
681
|
-
"
|
|
628
|
+
"bogus"
|
|
682
629
|
]);
|
|
683
630
|
assert.equal(result.status, 1);
|
|
684
631
|
assert.match(result.stderr, /--live-smoke must be/);
|
|
@@ -808,60 +755,3 @@ test("ensemble gateway test runs the unified front-door acceptance suite", async
|
|
|
808
755
|
fixture.cleanup();
|
|
809
756
|
}
|
|
810
757
|
});
|
|
811
|
-
test("lifecycle commands read a real run from a live plane", async () => {
|
|
812
|
-
const stack = await startStack({
|
|
813
|
-
policy: (policy) => {
|
|
814
|
-
policy.agents.allow = ["mock"];
|
|
815
|
-
}
|
|
816
|
-
});
|
|
817
|
-
const repo = makeStackRepo({ files: { "README.md": "# cli lifecycle\n" } });
|
|
818
|
-
const liveHome = mkdtempSync(join(tmpdir(), "warrant-cli-live-"));
|
|
819
|
-
rmSync(liveHome, { recursive: true, force: true });
|
|
820
|
-
try {
|
|
821
|
-
// The plane runs in this test process, so every CLI call must use the async
|
|
822
|
-
// spawner: a synchronous spawn would block the event loop and deadlock the
|
|
823
|
-
// in-process plane.
|
|
824
|
-
const init = await warrantAsync(["init"], { dir: liveHome });
|
|
825
|
-
assert.equal(init.status, 0, init.stderr);
|
|
826
|
-
// Point the freshly initialized home at the in-process test stack.
|
|
827
|
-
const configPath = join(liveHome, "config.json");
|
|
828
|
-
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
829
|
-
config.planeUrl = stack.planeUrl;
|
|
830
|
-
config.adminToken = stack.adminToken;
|
|
831
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
832
|
-
// Create one completed run through the SDK so the CLI has something to read.
|
|
833
|
-
const captured = await uploadWorkspace(stack.client, repo);
|
|
834
|
-
const created = await stack.client.requestRun(mockRunRequest({ prompt: "lifecycle probe", pool: stack.pool, workspace: captured.manifest }));
|
|
835
|
-
if (created.status === "awaiting_approval") {
|
|
836
|
-
await stack.client.approve(created.runId, { kind: "human", id: "cli-tester" });
|
|
837
|
-
}
|
|
838
|
-
assert.ok(await stack.runOnce());
|
|
839
|
-
const runs = await warrantAsync(["runs"], { dir: liveHome });
|
|
840
|
-
assert.equal(runs.status, 0, runs.stderr);
|
|
841
|
-
assert.match(runs.stdout, new RegExp(created.runId));
|
|
842
|
-
const receipt = await warrantAsync(["receipt", created.runId], { dir: liveHome });
|
|
843
|
-
assert.equal(receipt.status, 0, receipt.stderr);
|
|
844
|
-
const bundlePath = join(liveHome, "out.bundle.json");
|
|
845
|
-
const bundle = await warrantAsync(["bundle", created.runId, "--out", bundlePath], {
|
|
846
|
-
dir: liveHome
|
|
847
|
-
});
|
|
848
|
-
assert.equal(bundle.status, 0, bundle.stderr);
|
|
849
|
-
assert.match(bundle.stdout, /bundle written to/);
|
|
850
|
-
assert.ok(existsSync(bundlePath));
|
|
851
|
-
// The CLI round-trips its own bundle through offline verification.
|
|
852
|
-
const verify = await warrantAsync(["verify", bundlePath], { dir: liveHome });
|
|
853
|
-
assert.equal(verify.status, 0, verify.stderr);
|
|
854
|
-
assert.match(verify.stdout, /VERIFIED/);
|
|
855
|
-
const exported = await warrantAsync(["export"], { dir: liveHome });
|
|
856
|
-
assert.equal(exported.status, 0, exported.stderr);
|
|
857
|
-
assert.match(exported.stdout, new RegExp(created.runId));
|
|
858
|
-
const pull = await warrantAsync(["pull", created.runId, "--repo", repo], { dir: liveHome });
|
|
859
|
-
assert.equal(pull.status, 0, pull.stderr);
|
|
860
|
-
assert.match(pull.stdout, /applied|nothing to pull|branch/);
|
|
861
|
-
}
|
|
862
|
-
finally {
|
|
863
|
-
await stack.stop();
|
|
864
|
-
rmSync(repo, { recursive: true, force: true });
|
|
865
|
-
rmSync(liveHome, { recursive: true, force: true });
|
|
866
|
-
}
|
|
867
|
-
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import { assertHarnessRunResultV1 } from "@fusionkit/protocol";
|
|
7
|
+
import { gitText } from "@fusionkit/workspace";
|
|
8
|
+
import { createMockHarness } from "@fusionkit/ensemble";
|
|
9
|
+
import { createHarnessCapabilityMatrix, runHarnessSmokeDashboard } from "../dashboard.js";
|
|
10
|
+
function makeRepo() {
|
|
11
|
+
const root = mkdtempSync(join(tmpdir(), "ensemble-dashboard-"));
|
|
12
|
+
const repo = join(root, "repo");
|
|
13
|
+
mkdirSync(repo);
|
|
14
|
+
gitText(repo, ["init", "--quiet", "--initial-branch=main"]);
|
|
15
|
+
gitText(repo, ["config", "user.email", "dashboard@warrant.local"]);
|
|
16
|
+
gitText(repo, ["config", "user.name", "dashboard"]);
|
|
17
|
+
writeFileSync(join(repo, "README.md"), "# dashboard\n");
|
|
18
|
+
gitText(repo, ["add", "-A"]);
|
|
19
|
+
gitText(repo, ["commit", "--quiet", "-m", "init"]);
|
|
20
|
+
return {
|
|
21
|
+
repo,
|
|
22
|
+
outputRoot: join(root, "dashboard-out"),
|
|
23
|
+
cleanup: () => rmSync(root, { recursive: true, force: true })
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
test("capability matrix covers Cursor, Claude Code, Codex, command, and mock", () => {
|
|
27
|
+
const matrix = createHarnessCapabilityMatrix({ env: {} });
|
|
28
|
+
const harnessIds = matrix.rows.map((row) => row.harnessId);
|
|
29
|
+
assert.deepEqual(harnessIds, ["codex", "claude-code", "cursor", "command", "mock"]);
|
|
30
|
+
assert.ok(matrix.capabilities.includes("model_override"));
|
|
31
|
+
assert.ok(matrix.capabilities.includes("transcript_capture"));
|
|
32
|
+
assert.ok(matrix.capabilities.includes("diff_capture"));
|
|
33
|
+
assert.ok(matrix.capabilities.includes("tool_loop_capture"));
|
|
34
|
+
assert.ok(matrix.capabilities.includes("patch_apply_visibility"));
|
|
35
|
+
assert.ok(matrix.capabilities.includes("route_model_observation"));
|
|
36
|
+
assert.ok(matrix.capabilities.includes("verification_hint"));
|
|
37
|
+
assert.ok(matrix.capabilities.includes("replay_support"));
|
|
38
|
+
assert.ok(matrix.capabilities.includes("workspace_read"));
|
|
39
|
+
assert.ok(matrix.capabilities.includes("verification"));
|
|
40
|
+
assert.equal(matrix.rows.find((row) => row.harnessId === "cursor")?.availability, "credential_gated");
|
|
41
|
+
assert.equal(matrix.rows.find((row) => row.harnessId === "claude-code")?.harnessKind, "claude_code");
|
|
42
|
+
assert.equal(matrix.rows.find((row) => row.harnessId === "codex")?.harnessKind, "codex");
|
|
43
|
+
});
|
|
44
|
+
test("smoke dashboard writes schema-valid success, failure, skipped, and missing records", async () => {
|
|
45
|
+
const fixture = makeRepo();
|
|
46
|
+
try {
|
|
47
|
+
const dashboard = await runHarnessSmokeDashboard({
|
|
48
|
+
repo: fixture.repo,
|
|
49
|
+
outputRoot: fixture.outputRoot,
|
|
50
|
+
timeoutMs: 1_000,
|
|
51
|
+
createdAt: "2026-06-16T00:00:00.000Z"
|
|
52
|
+
});
|
|
53
|
+
assert.equal(dashboard.records.length, 6);
|
|
54
|
+
assert.equal(existsSync(dashboard.dashboardPath), true);
|
|
55
|
+
for (const record of dashboard.records) {
|
|
56
|
+
assertHarnessRunResultV1(record.result);
|
|
57
|
+
assert.equal(existsSync(record.resultPath), true);
|
|
58
|
+
const written = JSON.parse(readFileSync(record.resultPath, "utf8"));
|
|
59
|
+
assertHarnessRunResultV1(written);
|
|
60
|
+
}
|
|
61
|
+
const statuses = dashboard.records.map((record) => record.result.status).sort();
|
|
62
|
+
assert.deepEqual(statuses, [
|
|
63
|
+
"failed",
|
|
64
|
+
"skipped",
|
|
65
|
+
"skipped",
|
|
66
|
+
"skipped",
|
|
67
|
+
"succeeded",
|
|
68
|
+
"succeeded"
|
|
69
|
+
]);
|
|
70
|
+
assert.equal(dashboard.records.find((record) => record.taskId === "claude-code-skipped")?.result
|
|
71
|
+
.harness_kind, "claude_code");
|
|
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-skipped")?.result.harness_kind, "cursor");
|
|
74
|
+
assert.equal(dashboard.records.find((record) => record.taskId === "cursor-skipped")?.result.status, "skipped");
|
|
75
|
+
const markdown = readFileSync(dashboard.dashboardPath, "utf8");
|
|
76
|
+
assert.match(markdown, /# HandoffKit Harness Smoke Dashboard/);
|
|
77
|
+
assert.match(markdown, /## Capability Matrix/);
|
|
78
|
+
assert.match(markdown, /## Adapter Readiness/);
|
|
79
|
+
assert.match(markdown, /contract\/mock ready/);
|
|
80
|
+
assert.match(markdown, /credentials missing\/skipped/);
|
|
81
|
+
assert.match(markdown, /live smoke not requested/);
|
|
82
|
+
assert.match(markdown, /command-failure/);
|
|
83
|
+
assert.match(markdown, /cursor-skipped/);
|
|
84
|
+
assert.match(markdown, /harness-run-results\/mock-success\.json/);
|
|
85
|
+
assert.equal(dashboard.readiness.length, 5);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
fixture.cleanup();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
test("smoke dashboard only adds live records when explicit smoke env is enabled", async () => {
|
|
92
|
+
const fixture = makeRepo();
|
|
93
|
+
try {
|
|
94
|
+
const dashboard = await runHarnessSmokeDashboard({
|
|
95
|
+
repo: fixture.repo,
|
|
96
|
+
outputRoot: fixture.outputRoot,
|
|
97
|
+
timeoutMs: 1_000,
|
|
98
|
+
createdAt: "2026-06-16T00:00:00.000Z",
|
|
99
|
+
env: {},
|
|
100
|
+
liveSmoke: ["claude-code", "codex"]
|
|
101
|
+
});
|
|
102
|
+
assert.equal(dashboard.records.length, 6);
|
|
103
|
+
assert.equal(dashboard.records.some((record) => record.purpose === "live"), false);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
fixture.cleanup();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
test("explicit live smoke without credentials records a failed preflight", async () => {
|
|
110
|
+
const fixture = makeRepo();
|
|
111
|
+
try {
|
|
112
|
+
const dashboard = await runHarnessSmokeDashboard({
|
|
113
|
+
repo: fixture.repo,
|
|
114
|
+
outputRoot: fixture.outputRoot,
|
|
115
|
+
timeoutMs: 1_000,
|
|
116
|
+
createdAt: "2026-06-16T00:00:00.000Z",
|
|
117
|
+
env: { WARRANT_CLAUDE_SMOKE: "1" },
|
|
118
|
+
liveSmoke: ["claude-code"]
|
|
119
|
+
});
|
|
120
|
+
const live = dashboard.records.find((record) => record.taskId === "claude-code-live");
|
|
121
|
+
assert.equal(live?.purpose, "live");
|
|
122
|
+
assert.equal(live?.result.status, "failed");
|
|
123
|
+
assert.match(live?.result.output_summary ?? "", /Explicit live smoke failed before launch/);
|
|
124
|
+
assert.equal(dashboard.readiness.find((row) => row.harnessId === "claude-code")?.liveSmoke, "live smoke failed");
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
fixture.cleanup();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
test("live smoke readiness reports sanitized local evidence refs", async () => {
|
|
131
|
+
const fixture = makeRepo();
|
|
132
|
+
const privateTranscript = "raw private transcript should not render";
|
|
133
|
+
try {
|
|
134
|
+
const claudeHarness = {
|
|
135
|
+
...createMockHarness({
|
|
136
|
+
id: "claude-code-live-mock",
|
|
137
|
+
candidates: {
|
|
138
|
+
claude: {
|
|
139
|
+
transcript: privateTranscript,
|
|
140
|
+
artifacts: [
|
|
141
|
+
{
|
|
142
|
+
artifact_id: "claude_safe_log",
|
|
143
|
+
kind: "log",
|
|
144
|
+
hash: `sha256:${"a".repeat(64)}`,
|
|
145
|
+
uri: "file:///tmp/private-claude.log",
|
|
146
|
+
redaction_status: "synthetic"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
artifact_id: "claude_raw_transcript",
|
|
150
|
+
kind: "transcript",
|
|
151
|
+
hash: `sha256:${"b".repeat(64)}`,
|
|
152
|
+
uri: "file:///tmp/raw-claude.txt",
|
|
153
|
+
redaction_status: "raw"
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}),
|
|
159
|
+
harnessKind: "claude_code"
|
|
160
|
+
};
|
|
161
|
+
const codexHarness = {
|
|
162
|
+
...createMockHarness({
|
|
163
|
+
id: "codex-live-mock",
|
|
164
|
+
candidates: {
|
|
165
|
+
codex: {
|
|
166
|
+
transcript: "codex private transcript should not render",
|
|
167
|
+
artifacts: [
|
|
168
|
+
{
|
|
169
|
+
artifact_id: "codex_safe_log",
|
|
170
|
+
kind: "log",
|
|
171
|
+
hash: `sha256:${"c".repeat(64)}`,
|
|
172
|
+
uri: "file:///tmp/private-codex.log",
|
|
173
|
+
redaction_status: "synthetic"
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}),
|
|
179
|
+
harnessKind: "codex"
|
|
180
|
+
};
|
|
181
|
+
const dashboard = await runHarnessSmokeDashboard({
|
|
182
|
+
repo: fixture.repo,
|
|
183
|
+
outputRoot: fixture.outputRoot,
|
|
184
|
+
timeoutMs: 1_000,
|
|
185
|
+
createdAt: "2026-06-16T00:00:00.000Z",
|
|
186
|
+
env: {
|
|
187
|
+
WARRANT_ENSEMBLE_LIVE_SMOKE: "1",
|
|
188
|
+
VERCEL_TOKEN: "vercel-test",
|
|
189
|
+
ANTHROPIC_API_KEY: "anthropic-test",
|
|
190
|
+
CODEX_API_KEY: "codex-test"
|
|
191
|
+
},
|
|
192
|
+
liveSmoke: ["claude-code", "codex"],
|
|
193
|
+
liveSmokeHarnesses: {
|
|
194
|
+
"claude-code": claudeHarness,
|
|
195
|
+
codex: codexHarness
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
assert.equal(dashboard.records.length, 8);
|
|
199
|
+
assert.equal(dashboard.records.find((record) => record.taskId === "claude-code-live")?.result.status, "succeeded");
|
|
200
|
+
assert.equal(dashboard.records.find((record) => record.taskId === "codex-live")?.result.status, "succeeded");
|
|
201
|
+
assert.equal(dashboard.readiness.find((row) => row.harnessId === "claude-code")?.liveSmoke, "live smoke passed");
|
|
202
|
+
assert.equal(dashboard.readiness.find((row) => row.harnessId === "codex")?.liveSmoke, "live smoke passed");
|
|
203
|
+
const markdown = readFileSync(dashboard.dashboardPath, "utf8");
|
|
204
|
+
assert.match(markdown, /log:claude_safe_log:sha256/);
|
|
205
|
+
assert.match(markdown, /log:codex_safe_log:sha256/);
|
|
206
|
+
assert.match(markdown, /raw artifact ref\(s\) withheld/);
|
|
207
|
+
assert.equal(markdown.includes(privateTranscript), false);
|
|
208
|
+
assert.equal(markdown.includes("file:///tmp/private-claude.log"), false);
|
|
209
|
+
assert.equal(markdown.includes("file:///tmp/private-codex.log"), false);
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
fixture.cleanup();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
@@ -9,6 +9,7 @@ import { PassThrough } from "node:stream";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { after, before, test } from "node:test";
|
|
11
11
|
import { FUSION_REPORT_HEADER, FUSION_RUN_ID_HEADER, runAcpAgent, runFrontDoorAcceptance } from "@fusionkit/model-gateway";
|
|
12
|
+
import { resolveCursorkitCli } from "@fusionkit/ensemble";
|
|
12
13
|
import { buildAcpRunner, startConfiguredGateway } from "../gateway.js";
|
|
13
14
|
/**
|
|
14
15
|
* Comprehensive front-door e2e. Exercises the real chain end to end:
|
|
@@ -386,7 +387,9 @@ test("unified acceptance suite passes every reachable front door against the rea
|
|
|
386
387
|
assert.equal(statusOf("cursor-acp"), "blocked");
|
|
387
388
|
assert.ok(backend.judgeCallCount() >= 4, "judge synthesis must hit the model backend per front door");
|
|
388
389
|
});
|
|
389
|
-
const LIVE_CLAUDE = process.env.WARRANT_GATEWAY_LIVE_CLAUDE === "1"
|
|
390
|
+
const LIVE_CLAUDE = (process.env.FUSIONKIT_GATEWAY_LIVE_CLAUDE ?? process.env.WARRANT_GATEWAY_LIVE_CLAUDE) === "1"
|
|
391
|
+
? false
|
|
392
|
+
: "set FUSIONKIT_GATEWAY_LIVE_CLAUDE=1 with a working claude CLI";
|
|
390
393
|
test("live: real Claude Code CLI drives the gateway fusion run and receives the synthesized answer", { skip: LIVE_CLAUDE }, async () => {
|
|
391
394
|
// A dedicated single-model gateway keeps the live run light: each Claude
|
|
392
395
|
// model call triggers one real unified harness run (worktree + command +
|
|
@@ -429,7 +432,9 @@ test("live: real Claude Code CLI drives the gateway fusion run and receives the
|
|
|
429
432
|
await liveGateway.close();
|
|
430
433
|
}
|
|
431
434
|
});
|
|
432
|
-
const LIVE_CODEX = process.env.WARRANT_GATEWAY_LIVE_CODEX === "1"
|
|
435
|
+
const LIVE_CODEX = (process.env.FUSIONKIT_GATEWAY_LIVE_CODEX ?? process.env.WARRANT_GATEWAY_LIVE_CODEX) === "1"
|
|
436
|
+
? false
|
|
437
|
+
: "set FUSIONKIT_GATEWAY_LIVE_CODEX=1 with a working codex CLI";
|
|
433
438
|
test("live: real Codex CLI drives the gateway fusion run and receives the synthesized answer", { skip: LIVE_CODEX }, async () => {
|
|
434
439
|
// Codex streams `/v1/responses`; the gateway must emit the Responses SSE
|
|
435
440
|
// sequence ending in response.completed for Codex to accept the answer.
|
|
@@ -482,15 +487,14 @@ test("live: real Codex CLI drives the gateway fusion run and receives the synthe
|
|
|
482
487
|
rmSync(codexHome, { recursive: true, force: true });
|
|
483
488
|
}
|
|
484
489
|
});
|
|
485
|
-
// Drives the real cursor-agent CLI in ACP mode through the
|
|
490
|
+
// Drives the real cursor-agent CLI in ACP mode through the bundled Cursorkit
|
|
486
491
|
// bridge, whose local model backend is pointed at this gateway. Requires a
|
|
487
|
-
// logged-in cursor-agent
|
|
488
|
-
const
|
|
489
|
-
const LIVE_CURSOR = process.env.WARRANT_GATEWAY_LIVE_CURSOR === "1" && CURSORKIT_DIR !== undefined
|
|
492
|
+
// logged-in cursor-agent (Cursorkit is bundled as an npm dependency).
|
|
493
|
+
const LIVE_CURSOR = (process.env.FUSIONKIT_GATEWAY_LIVE_CURSOR ?? process.env.WARRANT_GATEWAY_LIVE_CURSOR) === "1"
|
|
490
494
|
? false
|
|
491
|
-
: "set
|
|
495
|
+
: "set FUSIONKIT_GATEWAY_LIVE_CURSOR=1 with a logged-in cursor-agent";
|
|
492
496
|
test("live: real cursor-agent (ACP) drives the Cursorkit bridge into the gateway fusion run", { skip: LIVE_CURSOR }, async () => {
|
|
493
|
-
const
|
|
497
|
+
const { serveCli } = resolveCursorkitCli();
|
|
494
498
|
const liveGateway = await startConfiguredGateway({
|
|
495
499
|
config: { ...config, models: [{ id: "cursor-panel", model: "fusion-cursor" }] },
|
|
496
500
|
host: "127.0.0.1",
|
|
@@ -519,8 +523,7 @@ test("live: real cursor-agent (ACP) drives the Cursorkit bridge into the gateway
|
|
|
519
523
|
MODEL_PROVIDER_MODEL: "fusion-panel",
|
|
520
524
|
MODEL_CONTEXT_TOKEN_LIMIT: "128000"
|
|
521
525
|
});
|
|
522
|
-
const bridge = spawn(process.execPath, [
|
|
523
|
-
cwd: cursorkitDir,
|
|
526
|
+
const bridge = spawn(process.execPath, [serveCli, "serve"], {
|
|
524
527
|
env: bridgeEnv,
|
|
525
528
|
stdio: ["ignore", "pipe", "pipe"]
|
|
526
529
|
});
|
package/dist/test/local.test.js
CHANGED
|
@@ -13,23 +13,23 @@ test("claudeEnv points Claude Code at the gateway's Anthropic surface", () => {
|
|
|
13
13
|
});
|
|
14
14
|
test("claudeEnv falls back to a placeholder auth token", () => {
|
|
15
15
|
const env = claudeEnv("http://127.0.0.1:9000");
|
|
16
|
-
assert.equal(env.ANTHROPIC_AUTH_TOKEN, "
|
|
16
|
+
assert.equal(env.ANTHROPIC_AUTH_TOKEN, "fusionkit-local");
|
|
17
17
|
});
|
|
18
18
|
test("codexConfigToml declares a Responses provider at the gateway", () => {
|
|
19
19
|
const toml = codexConfigToml("http://127.0.0.1:9000", "local-model");
|
|
20
20
|
assert.ok(toml.includes('model = "local-model"'));
|
|
21
|
-
assert.ok(toml.includes("[model_providers.
|
|
21
|
+
assert.ok(toml.includes("[model_providers.fusionkit-local]"));
|
|
22
22
|
assert.ok(toml.includes('base_url = "http://127.0.0.1:9000/v1"'));
|
|
23
23
|
assert.ok(toml.includes('wire_api = "responses"'));
|
|
24
24
|
assert.ok(toml.includes("requires_openai_auth = false"));
|
|
25
25
|
});
|
|
26
26
|
test("opencodeConfig registers an OpenAI-compatible provider", () => {
|
|
27
27
|
const config = opencodeConfig("http://127.0.0.1:9000", "local-model");
|
|
28
|
-
const provider = config.provider["
|
|
28
|
+
const provider = config.provider["fusionkit-local"];
|
|
29
29
|
assert.equal(provider?.npm, "@ai-sdk/openai-compatible");
|
|
30
30
|
assert.equal(provider?.options.baseURL, "http://127.0.0.1:9000/v1");
|
|
31
31
|
assert.ok("local-model" in (provider?.models ?? {}));
|
|
32
|
-
assert.equal(opencodeModelArg("local-model"), "
|
|
32
|
+
assert.equal(opencodeModelArg("local-model"), "fusionkit-local/local-model");
|
|
33
33
|
});
|
|
34
34
|
test("cursorInstructions surfaces the public URL and plan-mode caveat", () => {
|
|
35
35
|
const text = cursorInstructions("https://abc.example", "local-model");
|
package/dist/tools.d.ts
ADDED
package/dist/tools.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fusionkit tool registry: the single place that knows every tool package.
|
|
3
|
+
* Importing this module also wires the ensemble harness gateway to resolve
|
|
4
|
+
* tool-backed adapters (codex / claude-code / cursor) from the registry, so
|
|
5
|
+
* `@fusionkit/ensemble` itself stays free of any per-tool dependency.
|
|
6
|
+
*
|
|
7
|
+
* Adding a new tool is one new `@fusionkit/tool-*` package plus one entry here.
|
|
8
|
+
*/
|
|
9
|
+
import { setToolHarnessProvider } from "@fusionkit/ensemble";
|
|
10
|
+
import { createToolRegistry } from "@fusionkit/tools";
|
|
11
|
+
import { claudeTool } from "@fusionkit/tool-claude";
|
|
12
|
+
import { codexTool } from "@fusionkit/tool-codex";
|
|
13
|
+
import { cursorTool } from "@fusionkit/tool-cursor";
|
|
14
|
+
import { opencodeTool } from "@fusionkit/tool-opencode";
|
|
15
|
+
export const toolRegistry = createToolRegistry([
|
|
16
|
+
codexTool,
|
|
17
|
+
claudeTool,
|
|
18
|
+
cursorTool,
|
|
19
|
+
opencodeTool
|
|
20
|
+
]);
|
|
21
|
+
setToolHarnessProvider({
|
|
22
|
+
adapter: (kind, options) => toolRegistry.harnessForKind(kind, options),
|
|
23
|
+
sideEffects: (kind) => toolRegistry.sideEffectsForKind(kind),
|
|
24
|
+
responseShape: (kind) => toolRegistry.responseShapeForKind(kind)
|
|
25
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusionkit/cli",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.6",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/velum-labs/handoffkit.git",
|
|
@@ -34,14 +34,19 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"commander": "14.0.3",
|
|
37
|
-
"@fusionkit/ensemble": "0.1.
|
|
38
|
-
"@fusionkit/handoff": "0.1.
|
|
39
|
-
"@fusionkit/model-gateway": "0.1.
|
|
40
|
-
"@fusionkit/
|
|
41
|
-
"@fusionkit/
|
|
42
|
-
"@fusionkit/runner": "0.1.
|
|
43
|
-
"@fusionkit/
|
|
44
|
-
"@fusionkit/
|
|
37
|
+
"@fusionkit/ensemble": "0.1.6",
|
|
38
|
+
"@fusionkit/handoff": "0.1.6",
|
|
39
|
+
"@fusionkit/model-gateway": "0.1.6",
|
|
40
|
+
"@fusionkit/protocol": "0.1.6",
|
|
41
|
+
"@fusionkit/plane": "0.1.6",
|
|
42
|
+
"@fusionkit/runner": "0.1.6",
|
|
43
|
+
"@fusionkit/sdk": "0.1.6",
|
|
44
|
+
"@fusionkit/tool-cursor": "0.1.6",
|
|
45
|
+
"@fusionkit/tool-codex": "0.1.6",
|
|
46
|
+
"@fusionkit/tool-claude": "0.1.6",
|
|
47
|
+
"@fusionkit/tool-opencode": "0.1.6",
|
|
48
|
+
"@fusionkit/tools": "0.1.6",
|
|
49
|
+
"@fusionkit/workspace": "0.1.6"
|
|
45
50
|
},
|
|
46
51
|
"optionalDependencies": {
|
|
47
52
|
"portless": "0.14.0"
|
package/scope/.next/BUILD_ID
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
x7wPUCpgS31-5ZHJkcKsU
|