@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.
Files changed (77) hide show
  1. package/README.md +32 -8
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +1 -0
  4. package/dist/commands/doctor.js +7 -3
  5. package/dist/commands/ensemble-gateway.js +0 -2
  6. package/dist/commands/ensemble-records.d.ts +2 -1
  7. package/dist/commands/ensemble-records.js +3 -1
  8. package/dist/commands/ensemble.js +3 -4
  9. package/dist/commands/fusion.js +16 -13
  10. package/dist/commands/local.js +3 -3
  11. package/dist/cursor-acp.d.ts +3 -5
  12. package/dist/cursor-acp.js +12 -11
  13. package/dist/dashboard.d.ts +65 -0
  14. package/dist/dashboard.js +587 -0
  15. package/dist/fusion/env.d.ts +111 -0
  16. package/dist/fusion/env.js +98 -0
  17. package/dist/fusion/observability.d.ts +39 -0
  18. package/dist/fusion/observability.js +227 -0
  19. package/dist/fusion/preflight.d.ts +12 -0
  20. package/dist/fusion/preflight.js +42 -0
  21. package/dist/fusion/stack.d.ts +66 -0
  22. package/dist/fusion/stack.js +315 -0
  23. package/dist/fusion-config.d.ts +58 -7
  24. package/dist/fusion-config.js +152 -28
  25. package/dist/fusion-init.d.ts +1 -0
  26. package/dist/fusion-init.js +50 -15
  27. package/dist/fusion-quickstart.d.ts +11 -222
  28. package/dist/fusion-quickstart.js +58 -759
  29. package/dist/gateway.d.ts +0 -2
  30. package/dist/gateway.js +0 -2
  31. package/dist/local.d.ts +10 -17
  32. package/dist/local.js +50 -116
  33. package/dist/shared/options.d.ts +2 -1
  34. package/dist/shared/options.js +13 -19
  35. package/dist/shared/proc.d.ts +4 -70
  36. package/dist/shared/proc.js +3 -228
  37. package/dist/test/cli.test.js +11 -6
  38. package/dist/test/dashboard.test.d.ts +1 -0
  39. package/dist/test/dashboard.test.js +214 -0
  40. package/dist/test/fusion-config.test.js +64 -4
  41. package/dist/test/gateway-e2e.test.js +13 -10
  42. package/dist/test/local.test.js +4 -4
  43. package/dist/tools.d.ts +2 -0
  44. package/dist/tools.js +25 -0
  45. package/package.json +14 -9
  46. package/scope/.next/BUILD_ID +1 -1
  47. package/scope/.next/app-build-manifest.json +10 -10
  48. package/scope/.next/app-path-routes-manifest.json +2 -2
  49. package/scope/.next/build-manifest.json +2 -2
  50. package/scope/.next/prerender-manifest.json +13 -13
  51. package/scope/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  52. package/scope/.next/server/app/_not-found.html +1 -1
  53. package/scope/.next/server/app/_not-found.rsc +1 -1
  54. package/scope/.next/server/app/api/environments/route_client-reference-manifest.js +1 -1
  55. package/scope/.next/server/app/api/ingest/route_client-reference-manifest.js +1 -1
  56. package/scope/.next/server/app/api/models/route_client-reference-manifest.js +1 -1
  57. package/scope/.next/server/app/api/replay/route_client-reference-manifest.js +1 -1
  58. package/scope/.next/server/app/api/sessions/[traceId]/route_client-reference-manifest.js +1 -1
  59. package/scope/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
  60. package/scope/.next/server/app/api/stream/route_client-reference-manifest.js +1 -1
  61. package/scope/.next/server/app/environments/page_client-reference-manifest.js +1 -1
  62. package/scope/.next/server/app/environments.html +1 -1
  63. package/scope/.next/server/app/environments.rsc +1 -1
  64. package/scope/.next/server/app/index.html +1 -1
  65. package/scope/.next/server/app/index.rsc +1 -1
  66. package/scope/.next/server/app/models/page_client-reference-manifest.js +1 -1
  67. package/scope/.next/server/app/models.html +1 -1
  68. package/scope/.next/server/app/models.rsc +1 -1
  69. package/scope/.next/server/app/page_client-reference-manifest.js +1 -1
  70. package/scope/.next/server/app/sessions/[traceId]/page_client-reference-manifest.js +1 -1
  71. package/scope/.next/server/app-paths-manifest.json +2 -2
  72. package/scope/.next/server/functions-config-manifest.json +2 -2
  73. package/scope/.next/server/pages/404.html +1 -1
  74. package/scope/.next/server/pages/500.html +1 -1
  75. package/scope/.next/server/server-reference-manifest.json +1 -1
  76. /package/scope/.next/static/{vxqImMqlOwssVTua5Facf → BrrtQvnEIgv-OVkaeanKI}/_buildManifest.js +0 -0
  77. /package/scope/.next/static/{vxqImMqlOwssVTua5Facf → BrrtQvnEIgv-OVkaeanKI}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -44,23 +44,25 @@ cloud panel (skip with `--yes`). Use `--local` for the on-device MLX panel, or
44
44
 
45
45
  ## Per-repo config
46
46
 
47
- Tired of long flag lines? Scaffold a committed `fusionkit.json`:
47
+ Tired of long flag lines? Scaffold a committed `.fusionkit/` folder:
48
48
 
49
49
  ```bash
50
50
  fusionkit init
51
51
  ```
52
52
 
53
- It records the panel, judge, default tool, and run defaults so the whole team
54
- can just run `fusionkit codex`. Only env-var *names* for keys are stored, never
55
- secrets. Explicit CLI flags always override the file. Inspect the effective
56
- config and a dry-run preview with `fusionkit status`.
53
+ It writes `.fusionkit/fusion.json` (the panel, judge, default tool, and run
54
+ defaults) plus editable system-prompt overrides in `.fusionkit/prompts/*.md`, so
55
+ the whole team can just run `fusionkit codex`. Only env-var *names* for keys are
56
+ stored, never secrets. Explicit CLI flags always override the folder. A legacy
57
+ `fusionkit.json` is auto-migrated on first run. Inspect the effective config and
58
+ a dry-run preview with `fusionkit status`.
57
59
 
58
60
  ## Commands
59
61
 
60
62
  - `fusionkit codex | claude | cursor` — launch that agent backed by the panel.
61
63
  - `fusionkit serve` — just run the gateway and print setup snippets for any tool.
62
64
  - `fusionkit fusion [tool]` — the generic launcher (interactive picker on a TTY).
63
- - `fusionkit init` — scaffold `fusionkit.json` for this repo.
65
+ - `fusionkit init` — scaffold the committed `.fusionkit/` folder for this repo.
64
66
  - `fusionkit doctor` — check prerequisites with fix hints.
65
67
  - `fusionkit status` — show the effective config and what a run will do.
66
68
 
@@ -73,5 +75,27 @@ tool name; everything after the tool is forwarded to it.
73
75
  - `--observe` boots a local dashboard that streams live trace events. It is a
74
76
  separate app and is not bundled in the npm package; fusionkit prints how to
75
77
  enable it if it isn't available.
76
- - `cursor` needs a built Cursorkit checkout (`--cursor-kit-dir` or
77
- `FUSIONKIT_CURSORKIT_DIR`); fusionkit prints setup guidance if it's missing.
78
+ - `cursor` only needs a logged-in `cursor-agent` CLI; Cursorkit ships bundled
79
+ with this package, so no separate checkout is required.
80
+
81
+ ## Adding a new tool
82
+
83
+ Each coding tool is its own workspace package implementing a single
84
+ `ToolIntegration` (the adapter), so supporting a new tool is additive:
85
+
86
+ 1. Create `packages/tool-<name>/` (copy `packages/tool-codex` as a template). It
87
+ depends on `@fusionkit/tools` for the `ToolIntegration` / `ToolLaunchContext`
88
+ contract, and on `@fusionkit/ensemble` if it also ships a harness adapter.
89
+ 2. Export a `const <name>Tool: ToolIntegration` with:
90
+ - `launch(ctx)` — boot the tool's binary against `ctx.gatewayUrl` (the host
91
+ injects `spawnTool`, portless, teardown, etc. via the context; tool packages
92
+ never import the CLI).
93
+ - `modes` — `"fusion"`, `"local"`, or both.
94
+ - `createHarness` + `harnessKinds` — optional, only if the tool also runs as
95
+ an ensemble harness in the gateway/e2e matrix.
96
+ 3. Register it in [`packages/cli/src/tools.ts`](src/tools.ts) by adding it to the
97
+ `createToolRegistry([...])` list.
98
+
99
+ That single registry entry wires the tool into the `fusionkit <tool>` launcher,
100
+ `fusionkit local <tool>`, the interactive picker, preflight, and (when it has a
101
+ harness) the ensemble gateway — no other switch statements to update.
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  import { Command } from "commander";
2
+ import "./tools.js";
2
3
  export declare function buildProgram(): Command;
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { Command } from "commander";
4
+ import "./tools.js";
4
5
  import { FUSIONKIT_PYPI_VERSION } from "./fusion-quickstart.js";
5
6
  import { registerDoctor } from "./commands/doctor.js";
6
7
  import { registerEnsemble } from "./commands/ensemble.js";
@@ -56,13 +56,15 @@ function runDoctor() {
56
56
  console.log("");
57
57
  console.log(bold("repo config"));
58
58
  try {
59
- const config = loadFusionConfig(repoRoot);
59
+ const config = loadFusionConfig(repoRoot, (message) => console.log(` ${gray(glyph.bullet())} ${dim(message)}`));
60
60
  if (config === undefined) {
61
- console.log(` ${gray(glyph.bullet())} no ${cyan("fusionkit.json")} yet — run ${bold("fusionkit fusion init")}`);
61
+ console.log(` ${gray(glyph.bullet())} no ${cyan(".fusionkit/")} yet — run ${bold("fusionkit fusion init")}`);
62
62
  }
63
63
  else {
64
+ const overrides = Object.keys(config.prompts ?? {});
64
65
  console.log(` ${green(glyph.tick())} ${cyan(fusionConfigPath(repoRoot))}`);
65
66
  console.log(` ${dim(`tool: ${config.tool ?? "(unset)"} panel: ${(config.panel ?? []).map((s) => s.id).join(", ") || "(unset)"}`)}`);
67
+ console.log(` ${dim(`prompt overrides: ${overrides.length > 0 ? overrides.join(", ") : "(none — built-in defaults)"}`)}`);
66
68
  }
67
69
  }
68
70
  catch (error) {
@@ -94,7 +96,7 @@ function runStatus() {
94
96
  }
95
97
  let config;
96
98
  try {
97
- config = loadFusionConfig(repoRoot);
99
+ config = loadFusionConfig(repoRoot, (message) => console.log(dim(message)));
98
100
  }
99
101
  catch (error) {
100
102
  console.log(`${red("config error:")} ${error instanceof Error ? error.message : String(error)}`);
@@ -110,6 +112,8 @@ function runStatus() {
110
112
  console.log(`${dim("tool:")} ${bold(tool)}`);
111
113
  console.log(`${dim("judge:")} ${judge}`);
112
114
  console.log(`${dim("observe:")} ${config?.observe === true ? "on" : "off"}`);
115
+ const overrides = Object.keys(config?.prompts ?? {});
116
+ console.log(`${dim("prompts:")} ${overrides.length > 0 ? overrides.join(", ") : dim("(built-in defaults)")}`);
113
117
  console.log(bold("\npanel"));
114
118
  for (const spec of panel)
115
119
  console.log(` ${glyph.bullet()} ${panelLabel(spec)}`);
@@ -12,7 +12,6 @@ function addCommonGatewayOptions(cmd) {
12
12
  .option("--out <dir>", "output directory")
13
13
  .option("--model <spec>", "panel model mapping ID=MODEL (repeatable)", collect)
14
14
  .option("--judge-model <model>", "model used for judge synthesis")
15
- .option("--cursor-kit-dir <dir>", "Cursorkit repo for cursor scenarios")
16
15
  .option("--timeout-ms <n>", "candidate timeout")
17
16
  .option("--fusion-api-key <key>", "API key for the fusion backend");
18
17
  }
@@ -30,7 +29,6 @@ function gatewayConfig(opts) {
30
29
  timeoutMs,
31
30
  ...(opts.command !== undefined ? { command: opts.command } : {}),
32
31
  ...(opts.judgeModel !== undefined ? { judgeModel: opts.judgeModel } : {}),
33
- ...(opts.cursorKitDir !== undefined ? { cursorKitDir: resolve(opts.cursorKitDir) } : {}),
34
32
  ...(opts.fusionApiKey !== undefined ? { fusionApiKey: opts.fusionApiKey } : {})
35
33
  };
36
34
  }
@@ -1,4 +1,5 @@
1
- import type { EnsembleRunResult, HarnessAdapter, HarnessSmokeDashboard } from "@fusionkit/ensemble";
1
+ import type { EnsembleRunResult, HarnessAdapter } from "@fusionkit/ensemble";
2
+ import type { HarnessSmokeDashboard } from "../dashboard.js";
2
3
  import type { BenchmarkTaskRecordV1, ModelFusionHarnessKind, ModelFusionRecordV1, ModelFusionSideEffects } from "@fusionkit/protocol";
3
4
  export type HandoffPayload = {
4
5
  category?: string;
@@ -1,6 +1,8 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { claudeCodeHarness, claudeCodeHarnessCredentialSkipReason, codexHarness, codexHarnessCredentialSkipReason, createCommandHarness, createMockHarness } from "@fusionkit/ensemble";
3
+ import { createCommandHarness, createMockHarness } from "@fusionkit/ensemble";
4
+ import { claudeCodeHarness, claudeCodeHarnessCredentialSkipReason } from "@fusionkit/tool-claude";
5
+ import { codexHarness, codexHarnessCredentialSkipReason } from "@fusionkit/tool-codex";
4
6
  import { assertBenchmarkTaskRecordV1, assertHarnessCandidateRecordV1, assertJudgeSynthesisRecordV1, assertModelCallRecordV1, assertModelFusionRecord, assertToolExecutionRecordV1, MODEL_FUSION_SCHEMA_BUNDLE_HASH } from "@fusionkit/protocol";
5
7
  import { gitText } from "@fusionkit/workspace";
6
8
  import { fail } from "../shared/errors.js";
@@ -1,9 +1,10 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { Command } from "commander";
4
- import { createCommandHarness, createMockHarness, createMockJudgeSynthesizer, runEnsemble, runHarnessSmokeDashboard, runUnifiedHarnessE2E } from "@fusionkit/ensemble";
4
+ import { createCommandHarness, createMockHarness, createMockJudgeSynthesizer, runEnsemble, runUnifiedHarnessE2E } from "@fusionkit/ensemble";
5
5
  import { assertHarnessRunRequestV1, assertHarnessRunResultV1 } from "@fusionkit/protocol";
6
6
  import { gitText } from "@fusionkit/workspace";
7
+ import { runHarnessSmokeDashboard } from "../dashboard.js";
7
8
  import { fail } from "../shared/errors.js";
8
9
  import { collect, ensembleModels, liveSmokeTargets, parseTimeoutMs, unifiedHarnessKinds } from "../shared/options.js";
9
10
  import { buildGatewayCommand } from "./ensemble-gateway.js";
@@ -172,8 +173,7 @@ async function runEnsembleE2E(task, opts) {
172
173
  models,
173
174
  ...(opts.command !== undefined ? { command: opts.command } : {}),
174
175
  timeoutMs,
175
- ...(opts.judgeModel !== undefined ? { judgeModel: opts.judgeModel } : {}),
176
- ...(opts.cursorKitDir !== undefined ? { cursorKitDir: resolve(opts.cursorKitDir) } : {})
176
+ ...(opts.judgeModel !== undefined ? { judgeModel: opts.judgeModel } : {})
177
177
  });
178
178
  const counts = new Map();
179
179
  for (const row of result.results) {
@@ -245,7 +245,6 @@ export function registerEnsemble(program) {
245
245
  .option("--id <id>", "descriptor id")
246
246
  .option("--model <spec>", "panel model mapping ID=MODEL (repeatable)", collect)
247
247
  .option("--judge-model <model>", "model used for judge synthesis")
248
- .option("--cursor-kit-dir <dir>", "Cursorkit repo for cursor ACP/desktop scenarios")
249
248
  .option("--timeout-ms <n>", "candidate timeout")
250
249
  .option("--task-file <file>", "read task prompt from file")
251
250
  .action(runEnsembleE2E);
@@ -16,11 +16,10 @@ function applyFusionOptions(command) {
16
16
  .option("--synthesis-url <url>", "pre-running fusionkit serve for synthesis")
17
17
  .option("--fusionkit-dir <dir>", "local FusionKit checkout (dev override for the uvx synthesizer)")
18
18
  .option("--repo <dir>", "coding workspace the panel fuses over")
19
- .option("--cursor-kit-dir <dir>", "built Cursorkit checkout for the cursor tool")
20
19
  .option("--local", "use the local MLX panel trio instead of the default cloud panel")
21
- .option("--no-local", "override a fusionkit.json default of local=true")
20
+ .option("--no-local", "override a .fusionkit default of local=true")
22
21
  .option("--observe", "boot the local scope dashboard and stream live trace events")
23
- .option("--no-observe", "override a fusionkit.json default of observe=true")
22
+ .option("--no-observe", "override a .fusionkit default of observe=true")
24
23
  .option("--yes", "skip the interactive cloud-panel cost confirmation")
25
24
  .option("--auth-token <token>", "require a bearer token on the gateway")
26
25
  .option("--port <n>", "gateway port (default: ephemeral)")
@@ -45,8 +44,6 @@ function resolveOptions(opts) {
45
44
  options.fusionkitDir = resolve(opts.fusionkitDir);
46
45
  if (opts.repo !== undefined)
47
46
  options.repo = resolve(opts.repo);
48
- if (opts.cursorKitDir !== undefined)
49
- options.cursorKitDir = resolve(opts.cursorKitDir);
50
47
  // local/observe are tri-state: only set when the user passed --local/--no-local
51
48
  // (or --observe/--no-observe), so an unset flag can fall through to the config.
52
49
  if (opts.local !== undefined)
@@ -99,10 +96,10 @@ function mergeConfig(options, config) {
99
96
  options.observe = config.observe;
100
97
  if (options.portless === undefined && config.portless !== undefined)
101
98
  options.portless = config.portless;
102
- if (options.cursorKitDir === undefined && config.cursorKitDir != null)
103
- options.cursorKitDir = config.cursorKitDir;
104
99
  if (options.port === undefined && config.port != null)
105
100
  options.port = config.port;
101
+ if (options.prompts === undefined && config.prompts !== undefined)
102
+ options.prompts = config.prompts;
106
103
  }
107
104
  /** The repo root used for config lookup: --repo if given, else the cwd's git root. */
108
105
  function configRepoRoot(options) {
@@ -129,15 +126,21 @@ function resolveContext(opts) {
129
126
  return { options, ...(config?.tool !== undefined ? { configTool: config.tool } : {}) };
130
127
  }
131
128
  export function registerFusion(program) {
132
- // Top-level `init` — scaffold a committed fusionkit.json for this repo.
129
+ // Top-level `init` — scaffold a committed .fusionkit/ folder for this repo.
133
130
  program
134
131
  .command("init")
135
- .description("scaffold a committed fusionkit.json for this repo")
132
+ .description("scaffold a committed .fusionkit/ folder for this repo")
136
133
  .option("--repo <dir>", "coding workspace the panel fuses over")
137
- .option("--force", "overwrite an existing fusionkit.json")
134
+ .option("--fusionkit-dir <dir>", "local FusionKit checkout (dev override for default prompts)")
135
+ .option("--force", "overwrite an existing .fusionkit/ config and prompts")
138
136
  .action(async (opts) => {
139
- const repoRoot = configRepoRoot(resolveOptions(opts));
140
- const code = await runFusionInit({ repoRoot, force: opts.force === true });
137
+ const options = resolveOptions(opts);
138
+ const repoRoot = configRepoRoot(options);
139
+ const code = await runFusionInit({
140
+ repoRoot,
141
+ force: opts.force === true,
142
+ ...(options.fusionkitDir !== undefined ? { fusionkitDir: options.fusionkitDir } : {})
143
+ });
141
144
  process.exit(code);
142
145
  });
143
146
  // Generic `fusion [tool]` — keeps the original surface and interactive pick.
@@ -148,7 +151,7 @@ export function registerFusion(program) {
148
151
  .argument("[args...]", "arguments forwarded to the tool")
149
152
  .option("--tool <tool>", `coding agent to launch (${FUSION_TOOLS.join(" | ")})`))
150
153
  .addHelpText("after", "\nfusionkit's own flags must precede the tool name; everything after the tool is forwarded to it." +
151
- "\nRun `fusionkit init` to scaffold a committed fusionkit.json for this repo." +
154
+ "\nRun `fusionkit init` to scaffold a committed .fusionkit/ folder for this repo." +
152
155
  "\nRun `fusionkit fusion stop` to reap portless singleton services (router, dashboard, ...).")
153
156
  .action(async (positionalTool, args, opts) => {
154
157
  // `fusion stop` reaps persistent portless singletons left running by prior
@@ -6,14 +6,14 @@ export function registerLocal(program) {
6
6
  .description("back a vendor agent with a local model")
7
7
  .argument("[tool]", `${LOCAL_TOOLS.join(" | ")}`)
8
8
  .argument("[args...]", "arguments forwarded to the tool")
9
- .option("--public-url <url>", "public tunnel URL for Cursor (or WARRANT_PUBLIC_URL)")
9
+ .option("--public-url <url>", "public tunnel URL for Cursor (or FUSIONKIT_PUBLIC_URL)")
10
10
  .option("--auth-token <token>", "require a bearer token on the gateway")
11
11
  .allowUnknownOption()
12
12
  .passThroughOptions()
13
- .addHelpText("after", "\nwarrant's own flags must precede the tool name; everything after the tool is forwarded to it.")
13
+ .addHelpText("after", "\nfusionkit's own flags must precede the tool name; everything after the tool is forwarded to it.")
14
14
  .action(async (tool, args, opts) => {
15
15
  if (tool === undefined || !LOCAL_TOOLS.includes(tool)) {
16
- fail(`usage: warrant local <${LOCAL_TOOLS.join(" | ")}> [args...]`);
16
+ fail(`usage: fusionkit local <${LOCAL_TOOLS.join(" | ")}> [args...]`);
17
17
  }
18
18
  const options = {
19
19
  ...(opts.publicUrl !== undefined ? { publicUrl: opts.publicUrl } : {}),
@@ -1,15 +1,13 @@
1
1
  /**
2
- * Real Cursor ACP front-door producer. Spawns the Cursorkit bridge (its
2
+ * Real Cursor ACP front-door producer. Spawns the bundled Cursorkit bridge (its
3
3
  * local-model backend pointed at the running Fusion Harness Gateway) and drives
4
4
  * the real cursor-agent CLI in ACP mode, asserting the fusion-synthesized
5
5
  * sentinel reaches Cursor via session/update. Returns undefined when the
6
- * Cursorkit checkout or the cursor-agent CLI are unavailable, so the acceptance
7
- * suite records the explicit `blocked` / `cursorkit_backend_not_running`
8
- * outcome instead of a silent pass.
6
+ * cursor-agent CLI is unavailable, so the acceptance suite records the explicit
7
+ * `blocked` / `cursorkit_backend_not_running` outcome instead of a silent pass.
9
8
  */
10
9
  import type { FrontDoorOutcomeProducer } from "@fusionkit/model-gateway";
11
10
  export type CursorAcpProducerInput = {
12
- cursorKitDir: string | undefined;
13
11
  gatewayUrl: string;
14
12
  sentinel: string;
15
13
  repo: string;
@@ -1,16 +1,17 @@
1
1
  /**
2
- * Real Cursor ACP front-door producer. Spawns the Cursorkit bridge (its
2
+ * Real Cursor ACP front-door producer. Spawns the bundled Cursorkit bridge (its
3
3
  * local-model backend pointed at the running Fusion Harness Gateway) and drives
4
4
  * the real cursor-agent CLI in ACP mode, asserting the fusion-synthesized
5
5
  * sentinel reaches Cursor via session/update. Returns undefined when the
6
- * Cursorkit checkout or the cursor-agent CLI are unavailable, so the acceptance
7
- * suite records the explicit `blocked` / `cursorkit_backend_not_running`
8
- * outcome instead of a silent pass.
6
+ * cursor-agent CLI is unavailable, so the acceptance suite records the explicit
7
+ * `blocked` / `cursorkit_backend_not_running` outcome instead of a silent pass.
9
8
  */
10
9
  import { spawn } from "node:child_process";
11
10
  import { existsSync } from "node:fs";
12
11
  import { delimiter, join } from "node:path";
13
12
  import { createInterface } from "node:readline";
13
+ import { resolveCursorkitCli } from "@fusionkit/ensemble";
14
+ import { readEnv } from "@fusionkit/tools";
14
15
  function commandOnPath(command) {
15
16
  if (command.includes("/"))
16
17
  return existsSync(command);
@@ -26,10 +27,11 @@ function normalizeModelBaseUrl(gatewayUrl) {
26
27
  }
27
28
  export function buildCursorAcpProducer(input) {
28
29
  const command = input.command ?? "cursor-agent";
29
- if (input.cursorKitDir === undefined || input.cursorKitDir.length === 0) {
30
- return undefined;
31
- }
32
- if (!existsSync(join(input.cursorKitDir, "dist/src/cli.js"))) {
30
+ // The live Cursor ACP probe drives the real cursor-agent CLI through the
31
+ // bundled Cursorkit bridge, so it stays opt-in: without the live flag the
32
+ // acceptance suite reports this door as `blocked` rather than spawning live
33
+ // tooling (keeping deterministic runs free of credential/CLI dependencies).
34
+ if (readEnv(process.env, "FUSIONKIT_GATEWAY_LIVE_CURSOR") !== "1") {
33
35
  return undefined;
34
36
  }
35
37
  if (!commandOnPath(command)) {
@@ -38,7 +40,6 @@ export function buildCursorAcpProducer(input) {
38
40
  return () => runCursorAcpOutcome({ ...input, command });
39
41
  }
40
42
  async function runCursorAcpOutcome(input) {
41
- const cursorKitDir = input.cursorKitDir;
42
43
  const modelName = input.modelName ?? "local-fusion";
43
44
  const bridgePort = 9700 + Math.floor(Math.random() * 250);
44
45
  const bridgeEnv = {};
@@ -60,9 +61,9 @@ async function runCursorAcpOutcome(input) {
60
61
  MODEL_PROVIDER_MODEL: "fusion-panel",
61
62
  MODEL_CONTEXT_TOKEN_LIMIT: "128000"
62
63
  });
64
+ const { serveCli } = resolveCursorkitCli();
63
65
  let bridgeOut = "";
64
- const bridge = spawn(process.execPath, ["dist/src/cli.js", "serve"], {
65
- cwd: cursorKitDir,
66
+ const bridge = spawn(process.execPath, [serveCli, "serve"], {
66
67
  env: bridgeEnv,
67
68
  stdio: ["ignore", "pipe", "pipe"]
68
69
  });
@@ -0,0 +1,65 @@
1
+ import type { HarnessRunResultV1, ModelFusionHarnessKind } from "@fusionkit/protocol";
2
+ import type { HarnessAdapter, HarnessCapabilities } from "@fusionkit/ensemble";
3
+ import type { ToolDashboardMetadata } from "@fusionkit/tools";
4
+ /** Dashboard target id (a tool id like "claude-code", or "command"/"mock"). */
5
+ export type HarnessCapabilityTarget = string;
6
+ export type HarnessAvailability = "available" | "credential_gated" | "missing";
7
+ /** A tool id that exposes a live smoke (e.g. "claude-code", "codex", "cursor"). */
8
+ export type HarnessLiveSmokeTarget = string;
9
+ export type HarnessSmokePurpose = "contract" | "credential-skip" | "live" | "missing";
10
+ export type HarnessAdapterReadiness = {
11
+ harnessId: HarnessCapabilityTarget;
12
+ displayName: string;
13
+ contractReadiness: string;
14
+ credentialState: string;
15
+ liveSmoke: string;
16
+ evidence: string[];
17
+ artifactRefs: string[];
18
+ };
19
+ export type HarnessCapabilityMatrixRow = {
20
+ harnessId: HarnessCapabilityTarget;
21
+ harnessKind: ModelFusionHarnessKind;
22
+ displayName: string;
23
+ availability: HarnessAvailability;
24
+ capabilities: HarnessCapabilities;
25
+ notes: string[];
26
+ };
27
+ export type HarnessCapabilityMatrix = {
28
+ capabilities: string[];
29
+ rows: HarnessCapabilityMatrixRow[];
30
+ };
31
+ export type HarnessSmokeOutcome = "success" | "failure" | "missing" | "skipped";
32
+ export type HarnessSmokeRecord = {
33
+ taskId: string;
34
+ harnessId: HarnessCapabilityTarget;
35
+ purpose: HarnessSmokePurpose;
36
+ outcome: HarnessSmokeOutcome;
37
+ result: HarnessRunResultV1;
38
+ resultPath: string;
39
+ };
40
+ export type HarnessSmokeDashboard = {
41
+ outputRoot: string;
42
+ dashboardPath: string;
43
+ matrix: HarnessCapabilityMatrix;
44
+ records: HarnessSmokeRecord[];
45
+ readiness: HarnessAdapterReadiness[];
46
+ };
47
+ export type HarnessSmokeDashboardOptions = {
48
+ repo?: string;
49
+ outputRoot?: string;
50
+ timeoutMs?: number;
51
+ createdAt?: string;
52
+ env?: Record<string, string | undefined>;
53
+ commandSuccess?: string;
54
+ commandFailure?: string;
55
+ liveSmoke?: readonly HarnessLiveSmokeTarget[];
56
+ liveSmokeHarnesses?: Partial<Record<HarnessLiveSmokeTarget, HarnessAdapter>>;
57
+ /** Per-tool dashboard metadata; defaults to the registered tool registry. */
58
+ tools?: readonly ToolDashboardMetadata[];
59
+ };
60
+ export declare function createHarnessCapabilityMatrix(options?: HarnessSmokeDashboardOptions): HarnessCapabilityMatrix;
61
+ export declare function runHarnessSmokeDashboard(options?: HarnessSmokeDashboardOptions): Promise<HarnessSmokeDashboard>;
62
+ export declare const harnessDashboard: {
63
+ readonly capabilities: typeof createHarnessCapabilityMatrix;
64
+ readonly run: typeof runHarnessSmokeDashboard;
65
+ };