@fusionkit/cli 0.1.5 → 0.1.7
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 +32 -8
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1 -0
- package/dist/commands/doctor.js +7 -3
- 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 +16 -13
- package/dist/commands/local.js +3 -3
- package/dist/cursor-acp.d.ts +3 -5
- package/dist/cursor-acp.js +12 -11
- package/dist/dashboard.d.ts +65 -0
- package/dist/dashboard.js +587 -0
- package/dist/fusion/env.d.ts +111 -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 +66 -0
- package/dist/fusion/stack.js +315 -0
- package/dist/fusion-config.d.ts +58 -7
- package/dist/fusion-config.js +152 -28
- package/dist/fusion-init.d.ts +1 -0
- package/dist/fusion-init.js +50 -15
- package/dist/fusion-quickstart.d.ts +11 -222
- package/dist/fusion-quickstart.js +58 -759
- package/dist/gateway.d.ts +0 -2
- package/dist/gateway.js +0 -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 +11 -6
- package/dist/test/dashboard.test.d.ts +1 -0
- package/dist/test/dashboard.test.js +214 -0
- package/dist/test/fusion-config.test.js +64 -4
- 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 +10 -10
- package/scope/.next/app-path-routes-manifest.json +2 -2
- package/scope/.next/build-manifest.json +2 -2
- package/scope/.next/prerender-manifest.json +13 -13
- package/scope/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- 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/api/environments/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/api/ingest/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/api/models/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/api/replay/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/api/sessions/[traceId]/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/api/stream/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/environments/page_client-reference-manifest.js +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/page_client-reference-manifest.js +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/page_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/sessions/[traceId]/page_client-reference-manifest.js +1 -1
- package/scope/.next/server/app-paths-manifest.json +2 -2
- 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/scope/.next/static/{vxqImMqlOwssVTua5Facf → BrrtQvnEIgv-OVkaeanKI}/_buildManifest.js +0 -0
- /package/scope/.next/static/{vxqImMqlOwssVTua5Facf → BrrtQvnEIgv-OVkaeanKI}/_ssgManifest.js +0 -0
package/dist/test/cli.test.js
CHANGED
|
@@ -10,8 +10,13 @@ import { test } from "node:test";
|
|
|
10
10
|
import { MODEL_FUSION_SCHEMA_BUNDLE_HASH } from "@fusionkit/protocol";
|
|
11
11
|
const CLI = fileURLToPath(new URL("../index.js", import.meta.url));
|
|
12
12
|
const SMOKE_ENV_KEYS = [
|
|
13
|
+
"FUSIONKIT_CLAUDE_SMOKE",
|
|
14
|
+
"FUSIONKIT_CODEX_SMOKE",
|
|
15
|
+
"FUSIONKIT_CURSOR_SMOKE",
|
|
16
|
+
"FUSIONKIT_ENSEMBLE_LIVE_SMOKE",
|
|
13
17
|
"WARRANT_CLAUDE_SMOKE",
|
|
14
18
|
"WARRANT_CODEX_SMOKE",
|
|
19
|
+
"WARRANT_CURSOR_SMOKE",
|
|
15
20
|
"WARRANT_ENSEMBLE_LIVE_SMOKE"
|
|
16
21
|
];
|
|
17
22
|
async function readBody(req) {
|
|
@@ -177,12 +182,12 @@ test("gateway acp-registry rejects an unknown action", () => {
|
|
|
177
182
|
test("local without a tool prints usage and fails", () => {
|
|
178
183
|
const result = warrant(["local"]);
|
|
179
184
|
assert.equal(result.status, 1);
|
|
180
|
-
assert.match(result.stderr, /usage:
|
|
185
|
+
assert.match(result.stderr, /usage: fusionkit local </);
|
|
181
186
|
});
|
|
182
187
|
test("local rejects an unknown tool", () => {
|
|
183
188
|
const result = warrant(["local", "frobnicate"]);
|
|
184
189
|
assert.equal(result.status, 1);
|
|
185
|
-
assert.match(result.stderr, /usage:
|
|
190
|
+
assert.match(result.stderr, /usage: fusionkit local </);
|
|
186
191
|
});
|
|
187
192
|
test("local help documents the flags-before-tool contract", () => {
|
|
188
193
|
const result = warrant(["local", "--help"]);
|
|
@@ -194,15 +199,15 @@ test("fusion help documents the flags-before-tool contract", () => {
|
|
|
194
199
|
assert.equal(result.status, 0);
|
|
195
200
|
assert.match(result.stdout, /must precede the tool name/);
|
|
196
201
|
});
|
|
197
|
-
test("init scaffolds a fusionkit.json and refuses to clobber without --force", () => {
|
|
202
|
+
test("init scaffolds a .fusionkit/fusion.json and refuses to clobber without --force", () => {
|
|
198
203
|
const fixture = makeRepo();
|
|
199
204
|
try {
|
|
200
205
|
const result = warrant(["init", "--repo", fixture.repo]);
|
|
201
206
|
assert.equal(result.status, 0, result.stderr);
|
|
202
|
-
const configPath = join(fixture.repo, "fusionkit.json");
|
|
207
|
+
const configPath = join(fixture.repo, ".fusionkit", "fusion.json");
|
|
203
208
|
assert.ok(existsSync(configPath));
|
|
204
209
|
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
205
|
-
assert.equal(config.version, "fusionkit.fusion.
|
|
210
|
+
assert.equal(config.version, "fusionkit.fusion.v2");
|
|
206
211
|
const again = warrant(["init", "--repo", fixture.repo]);
|
|
207
212
|
assert.equal(again.status, 1);
|
|
208
213
|
assert.match(again.stderr, /already exists/);
|
|
@@ -620,7 +625,7 @@ test("ensemble dashboard rejects unknown live-smoke targets", () => {
|
|
|
620
625
|
"--out",
|
|
621
626
|
fixture.output,
|
|
622
627
|
"--live-smoke",
|
|
623
|
-
"
|
|
628
|
+
"bogus"
|
|
624
629
|
]);
|
|
625
630
|
assert.equal(result.status, 1);
|
|
626
631
|
assert.match(result.stderr, /--live-smoke must be/);
|
|
@@ -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
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { after, test } from "node:test";
|
|
6
|
-
import { FUSION_CONFIG_VERSION, FusionConfigError, fusionConfigPath, loadFusionConfig, parseFusionConfig, writeFusionConfig } from "../fusion-config.js";
|
|
6
|
+
import { FUSION_CONFIG_VERSION, FusionConfigError, fusionConfigPath, fusionPromptPath, legacyFusionConfigPath, loadFusionConfig, parseFusionConfig, readFusionPrompts, writeFusionConfig, writeFusionPrompts } from "../fusion-config.js";
|
|
7
7
|
const tmpRoots = [];
|
|
8
8
|
function freshDir() {
|
|
9
9
|
const dir = mkdtempSync(join(tmpdir(), "fusion-config-"));
|
|
@@ -31,6 +31,11 @@ test("parseFusionConfig accepts a valid config", () => {
|
|
|
31
31
|
assert.equal(config.observe, true);
|
|
32
32
|
assert.equal(config.port, 1234);
|
|
33
33
|
});
|
|
34
|
+
test("parseFusionConfig upgrades a legacy v1 version in memory", () => {
|
|
35
|
+
const config = parseFusionConfig({ version: "fusionkit.fusion.v1", tool: "claude" }, "test");
|
|
36
|
+
assert.equal(config.version, FUSION_CONFIG_VERSION);
|
|
37
|
+
assert.equal(config.tool, "claude");
|
|
38
|
+
});
|
|
34
39
|
test("parseFusionConfig rejects an unsupported version", () => {
|
|
35
40
|
assert.throws(() => parseFusionConfig({ version: "nope" }, "test"), FusionConfigError);
|
|
36
41
|
});
|
|
@@ -43,10 +48,10 @@ test("parseFusionConfig rejects a panel entry missing model", () => {
|
|
|
43
48
|
test("parseFusionConfig rejects a non-object", () => {
|
|
44
49
|
assert.throws(() => parseFusionConfig(["not", "an", "object"], "test"), FusionConfigError);
|
|
45
50
|
});
|
|
46
|
-
test("loadFusionConfig returns undefined when
|
|
51
|
+
test("loadFusionConfig returns undefined when no config exists", () => {
|
|
47
52
|
assert.equal(loadFusionConfig(freshDir()), undefined);
|
|
48
53
|
});
|
|
49
|
-
test("write then load round-trips the config", () => {
|
|
54
|
+
test("write then load round-trips the config through .fusionkit/fusion.json", () => {
|
|
50
55
|
const dir = freshDir();
|
|
51
56
|
const config = {
|
|
52
57
|
version: FUSION_CONFIG_VERSION,
|
|
@@ -61,6 +66,7 @@ test("write then load round-trips the config", () => {
|
|
|
61
66
|
};
|
|
62
67
|
const path = writeFusionConfig(dir, config);
|
|
63
68
|
assert.equal(path, fusionConfigPath(dir));
|
|
69
|
+
assert.ok(path.endsWith(join(".fusionkit", "fusion.json")));
|
|
64
70
|
const loaded = loadFusionConfig(dir);
|
|
65
71
|
assert.deepEqual(loaded, config);
|
|
66
72
|
});
|
|
@@ -73,8 +79,62 @@ test("writeFusionConfig refuses to clobber without force, overwrites with it", (
|
|
|
73
79
|
const reloaded = loadFusionConfig(dir);
|
|
74
80
|
assert.equal(reloaded?.tool, "claude");
|
|
75
81
|
});
|
|
82
|
+
test("writeFusionConfig omits the prompts field from fusion.json", () => {
|
|
83
|
+
const dir = freshDir();
|
|
84
|
+
writeFusionConfig(dir, { version: FUSION_CONFIG_VERSION, tool: "codex", prompts: { judge: "X" } });
|
|
85
|
+
const onDisk = JSON.parse(readFileSync(fusionConfigPath(dir), "utf8"));
|
|
86
|
+
assert.equal("prompts" in onDisk, false);
|
|
87
|
+
});
|
|
76
88
|
test("loadFusionConfig surfaces invalid JSON as a FusionConfigError", () => {
|
|
77
89
|
const dir = freshDir();
|
|
90
|
+
writeFusionConfig(dir, { version: FUSION_CONFIG_VERSION, tool: "codex" });
|
|
78
91
|
writeFileSync(fusionConfigPath(dir), "{ this is not json");
|
|
79
92
|
assert.throws(() => loadFusionConfig(dir), FusionConfigError);
|
|
80
93
|
});
|
|
94
|
+
test("prompt overrides are read from .fusionkit/prompts/*.md and attached on load", () => {
|
|
95
|
+
const dir = freshDir();
|
|
96
|
+
writeFusionConfig(dir, { version: FUSION_CONFIG_VERSION, tool: "codex" });
|
|
97
|
+
writeFusionPrompts(dir, { judge: "CUSTOM JUDGE", "trajectory-step": "CUSTOM STEP" });
|
|
98
|
+
const loaded = loadFusionConfig(dir);
|
|
99
|
+
assert.deepEqual(loaded?.prompts, { judge: "CUSTOM JUDGE", "trajectory-step": "CUSTOM STEP" });
|
|
100
|
+
});
|
|
101
|
+
test("empty prompt files are ignored (fall back to built-in defaults)", () => {
|
|
102
|
+
const dir = freshDir();
|
|
103
|
+
writeFusionConfig(dir, { version: FUSION_CONFIG_VERSION, tool: "codex" });
|
|
104
|
+
writeFusionPrompts(dir, { judge: "REAL" });
|
|
105
|
+
// Blank out the file: an empty override means "use the built-in default".
|
|
106
|
+
writeFileSync(fusionPromptPath(dir, "judge"), " \n");
|
|
107
|
+
assert.deepEqual(readFusionPrompts(dir), {});
|
|
108
|
+
assert.equal(loadFusionConfig(dir)?.prompts, undefined);
|
|
109
|
+
});
|
|
110
|
+
test("writeFusionPrompts does not clobber existing files without force", () => {
|
|
111
|
+
const dir = freshDir();
|
|
112
|
+
writeFusionPrompts(dir, { judge: "FIRST" });
|
|
113
|
+
const second = writeFusionPrompts(dir, { judge: "SECOND" });
|
|
114
|
+
assert.deepEqual(second, []);
|
|
115
|
+
assert.equal(readFusionPrompts(dir).judge, "FIRST");
|
|
116
|
+
writeFusionPrompts(dir, { judge: "THIRD" }, { force: true });
|
|
117
|
+
assert.equal(readFusionPrompts(dir).judge, "THIRD");
|
|
118
|
+
});
|
|
119
|
+
test("loadFusionConfig auto-migrates a legacy fusionkit.json into .fusionkit/", () => {
|
|
120
|
+
const dir = freshDir();
|
|
121
|
+
const legacy = { version: FUSION_CONFIG_VERSION, tool: "claude", local: true };
|
|
122
|
+
writeFileSync(legacyFusionConfigPath(dir), JSON.stringify(legacy, null, 2) + "\n");
|
|
123
|
+
const notices = [];
|
|
124
|
+
const loaded = loadFusionConfig(dir, (message) => notices.push(message));
|
|
125
|
+
assert.equal(loaded?.tool, "claude");
|
|
126
|
+
assert.equal(loaded?.local, true);
|
|
127
|
+
// The migrated copy now exists; the legacy file is left intact as a fallback.
|
|
128
|
+
assert.ok(existsSync(fusionConfigPath(dir)));
|
|
129
|
+
assert.ok(existsSync(legacyFusionConfigPath(dir)));
|
|
130
|
+
assert.equal(notices.length, 1);
|
|
131
|
+
assert.match(notices[0] ?? "", /migrated/);
|
|
132
|
+
});
|
|
133
|
+
test("loadFusionConfig migrates a legacy v1 file and upgrades the version", () => {
|
|
134
|
+
const dir = freshDir();
|
|
135
|
+
writeFileSync(legacyFusionConfigPath(dir), JSON.stringify({ version: "fusionkit.fusion.v1", tool: "codex" }, null, 2) + "\n");
|
|
136
|
+
const loaded = loadFusionConfig(dir);
|
|
137
|
+
assert.equal(loaded?.version, FUSION_CONFIG_VERSION);
|
|
138
|
+
const migrated = JSON.parse(readFileSync(fusionConfigPath(dir), "utf8"));
|
|
139
|
+
assert.equal(migrated.version, FUSION_CONFIG_VERSION);
|
|
140
|
+
});
|
|
@@ -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.7",
|
|
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/
|
|
39
|
-
"@fusionkit/
|
|
40
|
-
"@fusionkit/
|
|
41
|
-
"@fusionkit/
|
|
42
|
-
"@fusionkit/
|
|
43
|
-
"@fusionkit/sdk": "0.1.
|
|
44
|
-
"@fusionkit/
|
|
37
|
+
"@fusionkit/ensemble": "0.1.7",
|
|
38
|
+
"@fusionkit/plane": "0.1.7",
|
|
39
|
+
"@fusionkit/model-gateway": "0.1.7",
|
|
40
|
+
"@fusionkit/runner": "0.1.7",
|
|
41
|
+
"@fusionkit/protocol": "0.1.7",
|
|
42
|
+
"@fusionkit/handoff": "0.1.7",
|
|
43
|
+
"@fusionkit/sdk": "0.1.7",
|
|
44
|
+
"@fusionkit/tool-claude": "0.1.7",
|
|
45
|
+
"@fusionkit/tool-cursor": "0.1.7",
|
|
46
|
+
"@fusionkit/tool-codex": "0.1.7",
|
|
47
|
+
"@fusionkit/tools": "0.1.7",
|
|
48
|
+
"@fusionkit/workspace": "0.1.7",
|
|
49
|
+
"@fusionkit/tool-opencode": "0.1.7"
|
|
45
50
|
},
|
|
46
51
|
"optionalDependencies": {
|
|
47
52
|
"portless": "0.14.0"
|
package/scope/.next/BUILD_ID
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
BrrtQvnEIgv-OVkaeanKI
|
|
@@ -45,6 +45,13 @@
|
|
|
45
45
|
"static/chunks/main-app-2a6b1f94de31f96f.js",
|
|
46
46
|
"static/chunks/app/api/replay/route-95e822afbebe548b.js"
|
|
47
47
|
],
|
|
48
|
+
"/api/sessions/[traceId]/route": [
|
|
49
|
+
"static/chunks/webpack-4501ec292abda191.js",
|
|
50
|
+
"static/chunks/4bd1b696-409494caf8c83275.js",
|
|
51
|
+
"static/chunks/255-69a4a78fac9becef.js",
|
|
52
|
+
"static/chunks/main-app-2a6b1f94de31f96f.js",
|
|
53
|
+
"static/chunks/app/api/sessions/[traceId]/route-95e822afbebe548b.js"
|
|
54
|
+
],
|
|
48
55
|
"/api/sessions/route": [
|
|
49
56
|
"static/chunks/webpack-4501ec292abda191.js",
|
|
50
57
|
"static/chunks/4bd1b696-409494caf8c83275.js",
|
|
@@ -59,12 +66,13 @@
|
|
|
59
66
|
"static/chunks/main-app-2a6b1f94de31f96f.js",
|
|
60
67
|
"static/chunks/app/api/stream/route-95e822afbebe548b.js"
|
|
61
68
|
],
|
|
62
|
-
"/
|
|
69
|
+
"/environments/page": [
|
|
63
70
|
"static/chunks/webpack-4501ec292abda191.js",
|
|
64
71
|
"static/chunks/4bd1b696-409494caf8c83275.js",
|
|
65
72
|
"static/chunks/255-69a4a78fac9becef.js",
|
|
66
73
|
"static/chunks/main-app-2a6b1f94de31f96f.js",
|
|
67
|
-
"static/chunks/
|
|
74
|
+
"static/chunks/239-1c69ce437d02745f.js",
|
|
75
|
+
"static/chunks/app/environments/page-75403c3640fdf9de.js"
|
|
68
76
|
],
|
|
69
77
|
"/models/page": [
|
|
70
78
|
"static/chunks/webpack-4501ec292abda191.js",
|
|
@@ -75,14 +83,6 @@
|
|
|
75
83
|
"static/chunks/873-9351d1edaa9d58ef.js",
|
|
76
84
|
"static/chunks/app/models/page-d9b7d19485e9a640.js"
|
|
77
85
|
],
|
|
78
|
-
"/environments/page": [
|
|
79
|
-
"static/chunks/webpack-4501ec292abda191.js",
|
|
80
|
-
"static/chunks/4bd1b696-409494caf8c83275.js",
|
|
81
|
-
"static/chunks/255-69a4a78fac9becef.js",
|
|
82
|
-
"static/chunks/main-app-2a6b1f94de31f96f.js",
|
|
83
|
-
"static/chunks/239-1c69ce437d02745f.js",
|
|
84
|
-
"static/chunks/app/environments/page-75403c3640fdf9de.js"
|
|
85
|
-
],
|
|
86
86
|
"/page": [
|
|
87
87
|
"static/chunks/webpack-4501ec292abda191.js",
|
|
88
88
|
"static/chunks/4bd1b696-409494caf8c83275.js",
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
"/api/models/route": "/api/models",
|
|
5
5
|
"/api/ingest/route": "/api/ingest",
|
|
6
6
|
"/api/replay/route": "/api/replay",
|
|
7
|
+
"/api/sessions/[traceId]/route": "/api/sessions/[traceId]",
|
|
7
8
|
"/api/sessions/route": "/api/sessions",
|
|
8
9
|
"/api/stream/route": "/api/stream",
|
|
9
|
-
"/api/sessions/[traceId]/route": "/api/sessions/[traceId]",
|
|
10
|
-
"/models/page": "/models",
|
|
11
10
|
"/environments/page": "/environments",
|
|
11
|
+
"/models/page": "/models",
|
|
12
12
|
"/page": "/",
|
|
13
13
|
"/sessions/[traceId]/page": "/sessions/[traceId]"
|
|
14
14
|
}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"devFiles": [],
|
|
6
6
|
"ampDevFiles": [],
|
|
7
7
|
"lowPriorityFiles": [
|
|
8
|
-
"static/
|
|
9
|
-
"static/
|
|
8
|
+
"static/BrrtQvnEIgv-OVkaeanKI/_buildManifest.js",
|
|
9
|
+
"static/BrrtQvnEIgv-OVkaeanKI/_ssgManifest.js"
|
|
10
10
|
],
|
|
11
11
|
"rootMainFiles": [
|
|
12
12
|
"static/chunks/webpack-4501ec292abda191.js",
|