@henols/vice-mcp 0.2.5 → 1.1.0

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.
@@ -0,0 +1,416 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "tools": {
4
+ "x64sc": {
5
+ "id": "x64sc",
6
+ "location": {
7
+ "envVar": "VICE_BIN",
8
+ "fileOverridable": true
9
+ },
10
+ "kind": "executable",
11
+ "unblocks": {
12
+ "skills": [
13
+ "c64-program-recon",
14
+ "c64-ram-capture",
15
+ "vice-wedge-triage"
16
+ ],
17
+ "mcp": [
18
+ "c1541.bam",
19
+ "c1541.dir",
20
+ "c1541.entry",
21
+ "c1541.chain",
22
+ "c1541.read",
23
+ "petcat.decode"
24
+ ]
25
+ },
26
+ "remedies": {
27
+ "linux": [
28
+ {
29
+ "ecosystem": "debian-trixie",
30
+ "text": "sudo apt install vice (enable the contrib component first — VICE ships there, not main; a stock debian:trixie installation has no vice candidate until it is)",
31
+ "provenance": "carried",
32
+ "source": "README.md:101"
33
+ },
34
+ {
35
+ "ecosystem": "debian-forky",
36
+ "text": "sudo apt install vice (same contrib-component requirement as trixie)",
37
+ "provenance": "carried",
38
+ "source": "README.md:102"
39
+ },
40
+ {
41
+ "ecosystem": "ubuntu-2510",
42
+ "text": "sudo apt install vice (enable multiverse first)",
43
+ "provenance": "carried",
44
+ "source": "README.md:103"
45
+ },
46
+ {
47
+ "ecosystem": "arch",
48
+ "text": "sudo pacman -S vice",
49
+ "provenance": "carried",
50
+ "source": "README.md:104"
51
+ },
52
+ {
53
+ "ecosystem": "fedora-rpmfusion",
54
+ "text": "enable RPM Fusion Non-Free, then sudo dnf install vice",
55
+ "provenance": "carried",
56
+ "source": "README.md:105"
57
+ },
58
+ {
59
+ "ecosystem": "alpine-edge",
60
+ "text": "enable the edge/testing repo, then apk add vice",
61
+ "provenance": "carried",
62
+ "source": "README.md:106"
63
+ },
64
+ {
65
+ "ecosystem": "homebrew",
66
+ "text": "brew install vice",
67
+ "provenance": "carried",
68
+ "source": "README.md:107"
69
+ }
70
+ ],
71
+ "darwin": [
72
+ {
73
+ "ecosystem": "homebrew",
74
+ "text": "brew install vice",
75
+ "provenance": "carried",
76
+ "source": "README.md:107"
77
+ }
78
+ ],
79
+ "win32": [
80
+ {
81
+ "ecosystem": "windows-official",
82
+ "text": "download the GTK3/SDL2 win64 zip",
83
+ "provenance": "carried",
84
+ "source": "README.md:108"
85
+ }
86
+ ]
87
+ }
88
+ },
89
+ "c1541": {
90
+ "id": "c1541",
91
+ "location": {
92
+ "fileOverridable": true
93
+ },
94
+ "kind": "executable",
95
+ "unblocks": {
96
+ "skills": [
97
+ "c64-disk-access"
98
+ ],
99
+ "mcp": [
100
+ "c1541.bam",
101
+ "c1541.dir",
102
+ "c1541.entry",
103
+ "c1541.chain",
104
+ "c1541.read"
105
+ ]
106
+ },
107
+ "remedies": {
108
+ "linux": [
109
+ {
110
+ "ecosystem": "debian-trixie",
111
+ "text": "sudo apt install vice (enable the contrib component first — VICE ships there, not main; a stock debian:trixie installation has no vice candidate until it is)",
112
+ "provenance": "carried",
113
+ "source": "README.md:101"
114
+ },
115
+ {
116
+ "ecosystem": "debian-forky",
117
+ "text": "sudo apt install vice (same contrib-component requirement as trixie)",
118
+ "provenance": "carried",
119
+ "source": "README.md:102"
120
+ },
121
+ {
122
+ "ecosystem": "ubuntu-2510",
123
+ "text": "sudo apt install vice (enable multiverse first)",
124
+ "provenance": "carried",
125
+ "source": "README.md:103"
126
+ },
127
+ {
128
+ "ecosystem": "arch",
129
+ "text": "sudo pacman -S vice",
130
+ "provenance": "carried",
131
+ "source": "README.md:104"
132
+ },
133
+ {
134
+ "ecosystem": "fedora-rpmfusion",
135
+ "text": "enable RPM Fusion Non-Free, then sudo dnf install vice",
136
+ "provenance": "carried",
137
+ "source": "README.md:105"
138
+ },
139
+ {
140
+ "ecosystem": "alpine-edge",
141
+ "text": "enable the edge/testing repo, then apk add vice",
142
+ "provenance": "carried",
143
+ "source": "README.md:106"
144
+ },
145
+ {
146
+ "ecosystem": "homebrew",
147
+ "text": "brew install vice",
148
+ "provenance": "carried",
149
+ "source": "README.md:107"
150
+ }
151
+ ],
152
+ "darwin": [
153
+ {
154
+ "ecosystem": "homebrew",
155
+ "text": "brew install vice",
156
+ "provenance": "carried",
157
+ "source": "README.md:107"
158
+ }
159
+ ],
160
+ "win32": [
161
+ {
162
+ "ecosystem": "windows-official",
163
+ "text": "download the GTK3/SDL2 win64 zip",
164
+ "provenance": "carried",
165
+ "source": "README.md:108"
166
+ }
167
+ ]
168
+ }
169
+ },
170
+ "petcat": {
171
+ "id": "petcat",
172
+ "location": {
173
+ "fileOverridable": true
174
+ },
175
+ "kind": "executable",
176
+ "unblocks": {
177
+ "skills": [
178
+ "c64-petcat",
179
+ "c64-program-recon"
180
+ ],
181
+ "mcp": [
182
+ "petcat.decode"
183
+ ]
184
+ },
185
+ "remedies": {
186
+ "linux": [
187
+ {
188
+ "ecosystem": "debian-trixie",
189
+ "text": "sudo apt install vice (enable the contrib component first — VICE ships there, not main; a stock debian:trixie installation has no vice candidate until it is)",
190
+ "provenance": "carried",
191
+ "source": "README.md:101"
192
+ },
193
+ {
194
+ "ecosystem": "debian-forky",
195
+ "text": "sudo apt install vice (same contrib-component requirement as trixie)",
196
+ "provenance": "carried",
197
+ "source": "README.md:102"
198
+ },
199
+ {
200
+ "ecosystem": "ubuntu-2510",
201
+ "text": "sudo apt install vice (enable multiverse first)",
202
+ "provenance": "carried",
203
+ "source": "README.md:103"
204
+ },
205
+ {
206
+ "ecosystem": "arch",
207
+ "text": "sudo pacman -S vice",
208
+ "provenance": "carried",
209
+ "source": "README.md:104"
210
+ },
211
+ {
212
+ "ecosystem": "fedora-rpmfusion",
213
+ "text": "enable RPM Fusion Non-Free, then sudo dnf install vice",
214
+ "provenance": "carried",
215
+ "source": "README.md:105"
216
+ },
217
+ {
218
+ "ecosystem": "alpine-edge",
219
+ "text": "enable the edge/testing repo, then apk add vice",
220
+ "provenance": "carried",
221
+ "source": "README.md:106"
222
+ },
223
+ {
224
+ "ecosystem": "homebrew",
225
+ "text": "brew install vice",
226
+ "provenance": "carried",
227
+ "source": "README.md:107"
228
+ }
229
+ ],
230
+ "darwin": [
231
+ {
232
+ "ecosystem": "homebrew",
233
+ "text": "brew install vice",
234
+ "provenance": "carried",
235
+ "source": "README.md:107"
236
+ }
237
+ ],
238
+ "win32": [
239
+ {
240
+ "ecosystem": "windows-official",
241
+ "text": "download the GTK3/SDL2 win64 zip",
242
+ "provenance": "carried",
243
+ "source": "README.md:108"
244
+ }
245
+ ]
246
+ }
247
+ },
248
+ "acme": {
249
+ "id": "acme",
250
+ "location": {
251
+ "envVar": "ACME_BIN",
252
+ "fileOverridable": true
253
+ },
254
+ "kind": "executable",
255
+ "unblocks": {
256
+ "skills": [
257
+ "acme-build"
258
+ ],
259
+ "mcp": [
260
+ "acme.build"
261
+ ]
262
+ },
263
+ "remedies": {
264
+ "linux": [
265
+ {
266
+ "ecosystem": "ubuntu",
267
+ "text": "sudo apt-get install -y acme",
268
+ "provenance": "measured",
269
+ "source": ".github/workflows/ci.yml:78"
270
+ },
271
+ {
272
+ "ecosystem": "debian",
273
+ "text": "sudo apt-get install -y acme",
274
+ "provenance": "carried",
275
+ "source": ".github/workflows/ci.yml:48"
276
+ }
277
+ ],
278
+ "universal": [
279
+ {
280
+ "ecosystem": "generic",
281
+ "text": "Install ACME.",
282
+ "provenance": "carried",
283
+ "source": "src/skills/acme-build/SKILL.md:254"
284
+ }
285
+ ]
286
+ }
287
+ },
288
+ "acme-lib": {
289
+ "id": "acme-lib",
290
+ "location": {
291
+ "envVar": "ACME",
292
+ "fileOverridable": true
293
+ },
294
+ "kind": "directory",
295
+ "marker": "cbm/c64/vic.a",
296
+ "unblocks": {
297
+ "skills": [
298
+ "acme-build"
299
+ ],
300
+ "mcp": [
301
+ "acme.build"
302
+ ]
303
+ },
304
+ "remedies": {
305
+ "universal": [
306
+ {
307
+ "ecosystem": "generic",
308
+ "text": "export ACME=<dir holding cbm/c64/vic.a>.",
309
+ "provenance": "carried",
310
+ "source": "src/skills/acme-build/SKILL.md:255"
311
+ }
312
+ ]
313
+ }
314
+ },
315
+ "ghidra": {
316
+ "id": "ghidra",
317
+ "location": {
318
+ "envVar": "GHIDRA_HOME",
319
+ "fileOverridable": true
320
+ },
321
+ "kind": "directory",
322
+ "marker": "support/analyzeHeadless",
323
+ "unblocks": {
324
+ "skills": [
325
+ "c64-program-recon"
326
+ ],
327
+ "mcp": [
328
+ "ghidra.analyze",
329
+ "ghidra.installExtension"
330
+ ]
331
+ },
332
+ "remedies": {
333
+ "universal": [
334
+ {
335
+ "ecosystem": "generic",
336
+ "text": "Set the GHIDRA_HOME environment variable to name a Ghidra installation directory.",
337
+ "provenance": "carried",
338
+ "source": "src/mcp/vice/host-tool.mts:1352"
339
+ }
340
+ ]
341
+ }
342
+ },
343
+ "dxa": {
344
+ "id": "dxa",
345
+ "location": {
346
+ "fileOverridable": false,
347
+ "reason": "dxa is vendored and built by this project, so an override could only select a binary it did not build and did not pin."
348
+ },
349
+ "kind": "executable",
350
+ "unblocks": {
351
+ "skills": [
352
+ "routine-queue-walker"
353
+ ],
354
+ "mcp": [
355
+ "dxa.disassemble"
356
+ ]
357
+ },
358
+ "remedies": {
359
+ "universal": [
360
+ {
361
+ "ecosystem": "generic",
362
+ "text": "bash vendor/dxa/build.bash build",
363
+ "provenance": "carried",
364
+ "source": "src/mcp/vice/host-tool.mts:1472"
365
+ }
366
+ ]
367
+ }
368
+ },
369
+ "node": {
370
+ "id": "node",
371
+ "versionFloor": ">=24.0.0",
372
+ "location": {
373
+ "fileOverridable": false,
374
+ "reason": "vice-launcher.sh is bash and reads VICE_BROKER_NODE before any working Node exists to parse JSON with."
375
+ },
376
+ "kind": "executable",
377
+ "unblocks": {
378
+ "skills": [
379
+ "acme-build",
380
+ "c64-disk-access",
381
+ "c64-memory-mapping",
382
+ "c64-petcat",
383
+ "c64-program-recon",
384
+ "c64-provenance-diff",
385
+ "c64-ram-capture",
386
+ "routine-queue-walker",
387
+ "vice-wedge-triage"
388
+ ],
389
+ "mcp": [
390
+ "acme.build",
391
+ "ghidra.analyze",
392
+ "oracle.probe",
393
+ "oracle.run",
394
+ "dxa.disassemble",
395
+ "ghidra.installExtension",
396
+ "c1541.bam",
397
+ "c1541.dir",
398
+ "c1541.entry",
399
+ "c1541.chain",
400
+ "c1541.read",
401
+ "petcat.decode"
402
+ ]
403
+ },
404
+ "remedies": {
405
+ "universal": [
406
+ {
407
+ "ecosystem": "generic",
408
+ "text": "Install Node >= v24 and put it on PATH, or set VICE_BROKER_NODE to an absolute path to one.",
409
+ "provenance": "carried",
410
+ "source": "src/mcp/vice/resources/vice-launcher.sh:219"
411
+ }
412
+ ]
413
+ }
414
+ }
415
+ }
416
+ }
package/prg-image.ts CHANGED
@@ -34,7 +34,7 @@
34
34
  // `flatImageOrigin` from here with a plain top-level import, and `vice-proxy.ts`
35
35
  // imports `anno-tools.ts` statically. `anno-coverage.ts` (the byte-coverage
36
36
  // census) imports `decodeRawData`, `parsePrg` and `flatImageOrigin` for the
37
- // same three facts on the CLI's route. `scripts/check-npm-packages.mjs` walks
37
+ // same three facts on the CLI's route. The published closure once walked
38
38
  // that closure over `files[]` and fails the pack the moment a reachable module
39
39
  // sits outside the listed set.
40
40
  //
package/repo-root.ts CHANGED
@@ -200,17 +200,21 @@ export function repoRoot({ from = HERE, env = process.env, exists = existsSync }
200
200
  * deployed launcher path), and vice.ts + vice-broker-client.ts, both via
201
201
  * `supervisorDir()` (`supervisor`, the broker state directory -- one
202
202
  * writer, two readers).
203
- * - FOUR files cannot import this container-side module at all, so each
203
+ * - FIVE files cannot import this container-side module at all, so each
204
204
  * joins `".c64-re-tools"` with its own trailing segment(s) directly,
205
205
  * matching this function's shape by CONVENTION, never by shared code:
206
206
  * install-resources.ts's `installTargetDir()` (`bin` -- module-cycle
207
207
  * avoidance, since THIS file's own bottom-of-module call invokes it),
208
208
  * vice-broker.mts's `parseArgs()` state-dir fallback (`supervisor` --
209
- * host-bound, compiled separately by build.ts), host-tool.mts's
210
- * `oracle.run` scratch directory (`runs/oracle` -- host-bound), and
209
+ * host-bound, compiled separately by build.ts) and its `run()` tool-
210
+ * location deps (the bare root -- same host-bound reason), host-tool.mts's
211
+ * `oracle.run` scratch directory (`runs/oracle` -- host-bound),
211
212
  * ghidra-project.mts's `ghidraRunsRoot()`/`ghidraRunsRealRoot()`
212
213
  * (`runs/ghidra` -- host-bound, reached through a symlinked alias
213
- * handle, see below). Every one of these four must keep its literal
214
+ * handle, see below), and backend-detect.mts's `resolvedBackend()`
215
+ * cwd-relative fallback (the bare root -- host-bound, and the ONE place
216
+ * the emulator binary's own location is resolved, Phase 60 LOC-01/LOC-02).
217
+ * Every one of these five must keep its literal
214
218
  * equal to `join(toolsDir(...), <same segments>)`, by convention, or the
215
219
  * two halves of this codebase silently disagree on where the root is.
216
220
  *
@@ -218,8 +222,8 @@ export function repoRoot({ from = HERE, env = process.env, exists = existsSync }
218
222
  * files that call this function directly -- it is host-bound
219
223
  * (ghidra-project.mts) and cannot import this file at all.
220
224
  *
221
- * The literal string ".c64-re-tools" therefore has exactly 6 non-comment
222
- * occurrences in this codebase, across 5 files. repo-root.test.ts's census
225
+ * The literal string ".c64-re-tools" therefore has exactly 10 non-comment
226
+ * occurrences in this codebase, across 6 files. repo-root.test.ts's census
223
227
  * gate reads BOTH the count and this file list straight out of this
224
228
  * sentence and the bullet list below -- never duplicated by hand a second
225
229
  * time in the test -- and compares both against the real tree, with a
@@ -229,10 +233,17 @@ export function repoRoot({ from = HERE, env = process.env, exists = existsSync }
229
233
  * - repo-root.ts -- this definition, the line below (1)
230
234
  * - install-resources.ts -- `installTargetDir()`'s `bin` join (1)
231
235
  * - vice-broker.mts -- `parseArgs()`'s state-dir fallback, BOTH branches
232
- * of one ternary on the same line (2)
236
+ * of one ternary on the same line (2), plus `run()`'s `toolsDir` for the
237
+ * once-per-process emulator-binary resolution (1) = 3
233
238
  * - ghidra-project.mts -- `GHIDRA_RUNS_HANDLE_TARGET`, the alias handle's
234
239
  * relative symlink target (1)
235
- * - host-tool.mts -- `oracle.run`'s scratch-directory join (1)
240
+ * - host-tool.mts -- `oracle.run`'s scratch-directory join (1), plus
241
+ * Phase 60 (LOC-01, plan 60-03)'s `HostToolLocator` plumbing:
242
+ * `locatorFrom()`'s `process.cwd()`-derived fallback (1) and
243
+ * `runHostTool()`'s own locator built from its already-resolved
244
+ * `repoRootAbs` (1) = 3
245
+ * - backend-detect.mts -- `resolvedBackend()`'s cwd-relative `toolsDir`
246
+ * fallback, used only when no caller supplied one (1)
236
247
  *
237
248
  * The Ghidra alias handle: a non-dotted sibling of this root
238
249
  * (`<repoRoot>/c64-re-tools`, no leading dot), a symlink whose RELATIVE
@@ -47,8 +47,34 @@
47
47
  // is the exact failure class the 2026-08-01 triple-launch outage came
48
48
  // from (D-03/T-02-25) -- unchanged reasoning from before this plan, only
49
49
  // the mechanism inside resolvedBackend() changed.
50
+ //
51
+ // Phase 60 (LOC-01/LOC-02, PD-01/PD-02/PD-03): resolvedBackend() gains a real
52
+ // VALUE dependency on the tool-location seam (tool-location.mts) for the
53
+ // emulator binary's own resolution -- when neither `viceBin` nor
54
+ // `resolveBinPath` is injected, resolution goes through the seam's
55
+ // `resolveTool("x64sc", ...)` instead of this file's own ordering, so a
56
+ // `.c64-re-tools/tools.json` entry for `x64sc` now changes what this broker
57
+ // actually spawns. `defaultResolveBinPath()` collapses into a thin wrapper
58
+ // over the seam's own exported `resolveOnPath()` -- the first of Phase 59
59
+ // D-02's three independent `$PATH`-walk copies to collapse.
60
+ //
61
+ // This file ships two ways (see the cache-section comment below): unbuilt,
62
+ // imported directly by container-side .ts (vice-proxy.ts's own `import *
63
+ // as backendDetect from "./backend-detect.mts"`), and compiled into
64
+ // resources/ for the host (vice-broker.mts's own `./backend-detect.mjs`
65
+ // value import). A STATIC `import ... from "./tool-location.mjs"` would
66
+ // resolve in only the SECOND form -- that file exists as a real sibling
67
+ // only once both are compiled into resources/, never beside the unbuilt
68
+ // source. Loading it instead through node:module's `createRequire()`
69
+ // (`toolLocationSeam()` below) defers resolution to the call site rather
70
+ // than parse time, so trying the compiled sibling first and falling back to
71
+ // the unbuilt source sibling keeps this ONE seam call working in both
72
+ // shipped forms -- one implementation, no #ifdef-style split, and every
73
+ // existing unbuilt importer of this file needs no change at all.
50
74
  import { existsSync, readFileSync, writeFileSync, chmodSync, renameSync, mkdirSync, statSync, } from "node:fs";
51
- import { join, resolve as resolvePath } from "node:path";
75
+ import { dirname, join } from "node:path";
76
+ import { fileURLToPath } from "node:url";
77
+ import { createRequire } from "node:module";
52
78
  /** The client-side capability-decision schema version stamped into every
53
79
  * capability record this module writes (see BackendCacheRecord's own field
54
80
  * comment). BUMP THIS whenever the client's capability probing or the
@@ -124,20 +150,36 @@ function writeCacheRecordAtomic(supervisorDir, record) {
124
150
  writeFileSync(tmpPath, JSON.stringify(record, null, 2) + "\n");
125
151
  renameSync(tmpPath, finalPath);
126
152
  }
153
+ /** This module's own directory. Computed once, purely to seed
154
+ * `toolLocationSeam()`'s two-candidate join below -- mirrors
155
+ * `tool-location.mts`'s own `HERE` constant and its `readDeclaration()`
156
+ * "beside `here`, take the first that exists" idiom exactly. */
157
+ const HERE = dirname(fileURLToPath(import.meta.url));
158
+ /** Loads the tool-location seam through `node:module`'s `createRequire()`
159
+ * rather than a static ESM import -- see this file's own header for why a
160
+ * static specifier cannot work in both of this file's two shipped forms.
161
+ * `require()` resolves at THIS call site, not at parse time, so trying the
162
+ * compiled resources/ sibling first and falling back to the unbuilt source
163
+ * sibling (the exact two-candidate order `tool-location.mts`'s own
164
+ * `readDeclaration()` already uses for `prerequisites.json`) lets the SAME
165
+ * source file resolve correctly whichever way this file itself was loaded.
166
+ * Never memoised here -- `resolvedBackend()`'s own `memoisedResult` already
167
+ * ensures this runs at most once in the one production path that reaches
168
+ * it, and a test process that resets that memo between scenarios must be
169
+ * free to call this again, cheaply, rather than replay a stale answer. */
170
+ function toolLocationSeam() {
171
+ const req = createRequire(import.meta.url);
172
+ const specifier = existsSync(join(HERE, "tool-location.mjs")) ? "./tool-location.mjs" : "./tool-location.mts";
173
+ return req(specifier);
174
+ }
175
+ /** Reduced (Phase 60) to a thin wrapper over the seam's own exported
176
+ * `resolveOnPath()` -- the first of Phase 59 D-02's three independent
177
+ * `$PATH`-walk copies to collapse. Kept as a named function (rather than
178
+ * inlined at its one call site) only because `ResolvedBackendDeps.resolveBinPath`
179
+ * needs a real default to fall back to when a caller supplies `viceBin` but
180
+ * not this override (PD-02). */
127
181
  function defaultResolveBinPath(bin, env) {
128
- if (bin.includes("/")) {
129
- const abs = resolvePath(bin);
130
- return existsSync(abs) ? abs : null;
131
- }
132
- const pathEnv = env.PATH ?? "";
133
- for (const dir of pathEnv.split(":")) {
134
- if (!dir)
135
- continue;
136
- const candidate = join(dir, bin);
137
- if (existsSync(candidate))
138
- return candidate;
139
- }
140
- return null;
182
+ return toolLocationSeam().resolveOnPath(bin, env).path;
141
183
  }
142
184
  function defaultStat(resolvedPath) {
143
185
  try {
@@ -190,11 +232,41 @@ export function resolvedBackend(deps = {}) {
190
232
  if (memoisedResult !== null)
191
233
  return memoisedResult;
192
234
  const env = deps.env ?? process.env;
193
- const viceBin = deps.viceBin ?? env.VICE_BIN ?? "x64sc";
194
- const resolveBinPath = deps.resolveBinPath ?? defaultResolveBinPath;
195
235
  const stat = deps.stat ?? defaultStat;
196
236
  const now = deps.now ?? (() => Date.now());
197
- const resolvedPath = resolveBinPath(viceBin, env);
237
+ let viceBin;
238
+ let resolvedPath;
239
+ // Phase 60 gap closure (LOC-03, PD-13): non-null only in the PD-01 branch
240
+ // below, and only when the seam itself refused. Carried verbatim onto the
241
+ // returned result's own `locationRefusal` field (see that field's own doc
242
+ // comment for why); this module reads no environment variable to compose
243
+ // it, and reads none to name it here either.
244
+ let locationRefusal = null;
245
+ if (deps.viceBin !== undefined || deps.resolveBinPath !== undefined) {
246
+ // PD-02: an explicit override bypasses the seam entirely -- byte-for-byte
247
+ // the same behaviour every existing injected test case already exercises.
248
+ viceBin = deps.viceBin ?? "x64sc";
249
+ const resolveBinPath = deps.resolveBinPath ?? defaultResolveBinPath;
250
+ resolvedPath = resolveBinPath(viceBin, env);
251
+ }
252
+ else {
253
+ // PD-01: resolvedBackend() gains the tools.json layer internally by
254
+ // calling the seam; it keeps no ordering of its own. The seam's WHOLE
255
+ // result is taken here (PD-13), not only its `path`: the display name
256
+ // handed to binPathFields() below falls back to the seam's own
257
+ // `envCandidate` -- the value the developer actually wrote -- when the
258
+ // seam reports one, and to the literal "x64sc" only when it does not
259
+ // (retiring the hardcoded display name IN-01 logged). `refusal` is
260
+ // carried onto `locationRefusal` above unchanged.
261
+ const projectRoot = deps.projectRoot ?? (deps.supervisorDir !== undefined ? dirname(dirname(deps.supervisorDir)) : process.cwd());
262
+ const toolsDir = deps.toolsDir ?? (deps.supervisorDir !== undefined ? dirname(deps.supervisorDir) : join(process.cwd(), ".c64-re-tools"));
263
+ const locate = deps.locate ?? toolLocationSeam().resolveTool;
264
+ const locateDeps = { toolsDir, projectRoot, env };
265
+ const locateResult = locate("x64sc", locateDeps);
266
+ resolvedPath = locateResult.path;
267
+ viceBin = locateResult.envCandidate ?? "x64sc";
268
+ locationRefusal = locateResult.refusal;
269
+ }
198
270
  const identity = resolvedPath ? stat(resolvedPath) : null;
199
271
  const cacheEligible = resolvedPath !== null && identity !== null && typeof deps.supervisorDir === "string";
200
272
  if (cacheEligible) {
@@ -213,7 +285,7 @@ export function resolvedBackend(deps = {}) {
213
285
  });
214
286
  }
215
287
  }
216
- const result = { backend: "stock", ...binPathFields(resolvedPath, viceBin) };
288
+ const result = { backend: "stock", ...binPathFields(resolvedPath, viceBin), locationRefusal };
217
289
  memoisedResult = result;
218
290
  return result;
219
291
  }
@@ -320,7 +320,13 @@ export function buildViceArgs(port, { backend, mcpHost, binmonHost, viceArgsEnv,
320
320
  function spawnAndRecordInstance(reason, port, deps) {
321
321
  const spawnFn = deps.spawn ?? ((cmd, args, opts) => nodeSpawn(cmd, args, opts));
322
322
  const now = deps.now ?? (() => Date.now());
323
- const viceBin = deps.viceBin ?? process.env.VICE_BIN ?? "x64sc";
323
+ // Phase 60 (LOC-02): no environment-variable fallback here any more -- the
324
+ // real broker resolves the binary ONCE at startup through backend-detect.mts's
325
+ // resolvedBackend() (which itself now consults the tool-location seam) and
326
+ // threads that SAME value down through deps.viceBin on every call. The
327
+ // literal "x64sc" default below is only ever reached by a caller (a test)
328
+ // that supplies neither.
329
+ const viceBin = deps.viceBin ?? "x64sc";
324
330
  const backend = deps.backend ?? "stock";
325
331
  // The ONE construction site for a fresh InstanceRecord asserts the
326
332
  // invariant every downstream consumer (HeldLease, textConnect(), etc.) was
@@ -1156,7 +1162,10 @@ function launchSupervised(reason, port, deps, crashTimes, backoffMs, remoteMonit
1156
1162
  const logDir = deps.epoch.instanceLogDirFor(deps.stateDir, port);
1157
1163
  mkdirSync(logDir, { recursive: true });
1158
1164
  const epoch = deps.epoch.nextEpochFor(supervisorDir);
1159
- const viceBin = deps.viceBin ?? process.env.VICE_BIN ?? "x64sc";
1165
+ // Phase 60 (LOC-02): same narrowing as spawnAndRecordInstance() above --
1166
+ // no environment-variable fallback here, the real broker always threads
1167
+ // its once-resolved viceBin down through deps.viceBin.
1168
+ const viceBin = deps.viceBin ?? "x64sc";
1160
1169
  // Timestamp PLUS the epoch number: Date.now() alone can collide across
1161
1170
  // two respawns inside the same millisecond when the injected sleepMs
1162
1171
  // resolves immediately (exactly what this module's own tests do to stay
@@ -366,8 +366,8 @@ const SLAFILE_ATTR_PATTERN = /\bslafile\s*=\s*"([^"]*)"/;
366
366
  * whether that `slafile` exists on disk beside its own `.ldefs` -- a SORTED
367
367
  * list (by `id`) so any assertion over the result is order-independent.
368
368
  *
369
- * `ghidraHome` is an explicit parameter, never read from
370
- * `process.env.GHIDRA_HOME` internally, so the whole function is drivable
369
+ * `ghidraHome` is an explicit parameter; this function never reads the
370
+ * `GHIDRA_HOME` environment variable internally, so the whole function is drivable
371
371
  * against a synthetic directory tree with no real Ghidra installation
372
372
  * present.
373
373
  *