@henols/vice-mcp 1.0.0 → 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.
@@ -139,9 +139,6 @@ function resolveCeilingForRecord() {
139
139
  const n = Number(raw);
140
140
  return Number.isFinite(n) ? n : 16;
141
141
  }
142
- function resolveViceBinForHostState() {
143
- return process.env.VICE_BIN ?? "x64sc";
144
- }
145
142
  /** Duplicates vice-broker-client.ts's readBrokerLiveness() classification
146
143
  * logic (never_started / stale / alive against BROKER_STALE_MS) rather than
147
144
  * importing it -- confirmed empirically (plan 02's own SUMMARY) that
@@ -218,10 +215,17 @@ function writeBrokerRecordFile(stateDir, record) {
218
215
  * caller-supplied `stdio`. Merging in the other order would silently
219
216
  * redirect a launch's output away from the per-instance log file the
220
217
  * epoch record names, breaking the per-instance forensic logs while
221
- * appearing to work. */
222
- function makeLoggingSpawn(logDir) {
218
+ * appearing to work.
219
+ *
220
+ * `viceBin` (Phase 60, LOC-02) names the ALREADY-resolved binary this launch
221
+ * is about to spawn -- the log filename's own stem, via `basename()`, rather
222
+ * than a fresh read of the emulator environment variable on its own.
223
+ * Optional, defaulting to the literal "x64sc" for a caller (a test) that
224
+ * supplies neither this nor a real resolution up its own call chain, so the
225
+ * filename shape is unchanged for it. */
226
+ function makeLoggingSpawn(logDir, viceBin) {
223
227
  mkdirSync(logDir, { recursive: true });
224
- const viceBinForLog = basename(process.env.VICE_BIN ?? "x64sc");
228
+ const viceBinForLog = basename(viceBin ?? "x64sc");
225
229
  const logName = `${viceBinForLog}-${Date.now()}.log`;
226
230
  const logFd = openSync(join(logDir, logName), "a");
227
231
  return {
@@ -291,13 +295,25 @@ function writeEpochForLaunch(record, logRelPath) {
291
295
  * always `undefined` in production right now; the parameter exists so that
292
296
  * adding one later cannot reintroduce exactly this divergence between a
293
297
  * launch's argv and its respawn's argv. */
294
- function superviseDepsFor(stateDir, state, backend, binmonHost) {
298
+ /** Phase 60 (LOC-01/LOC-02): `viceBin` is now a REQUIRED-in-spirit fourth
299
+ * argument (kept optional only for source compatibility with a caller that
300
+ * has none to give) -- before this plan, a crash respawn's own
301
+ * SuperviseChildDeps.viceBin was silently left unset here, and
302
+ * broker-launch.mts's own launchSupervised() covered the gap by falling
303
+ * through to a fresh read of the emulator environment variable. That fallback is gone (see
304
+ * broker-launch.mts's own header for why); leaving this builder unchanged
305
+ * would have made a crash-respawned instance silently spawn the bare
306
+ * "x64sc" literal instead of the SAME binary a tools.json entry or
307
+ * VICE_BIN resolved for the launch it replaces -- exactly the
308
+ * disagreement LOC-02 exists to remove. */
309
+ function superviseDepsFor(stateDir, state, backend, viceBin, binmonHost) {
295
310
  return {
296
311
  state,
297
312
  stateDir,
298
313
  epoch: { epochPathFor, instanceLogDirFor, nextEpochFor, writeEpochRecord },
299
314
  log: (line) => process.stderr.write(`${line}\n`),
300
315
  backend,
316
+ viceBin,
301
317
  binmonHost,
302
318
  };
303
319
  }
@@ -526,6 +542,10 @@ export async function handleAcquire(requestId, stateDir, state, deps = {}) {
526
542
  // verdict handleAcquire already uses for buildViceArgs() -- on stock the port
527
543
  // speaks the binary monitor, so an HTTP POST there can never succeed.
528
544
  const backend = deps.backend ?? "stock";
545
+ // Same threaded-down-once discipline as `backend` immediately above --
546
+ // this function never resolves it itself. See HandleAcquireDeps.viceBin's
547
+ // own doc comment.
548
+ const viceBin = deps.viceBin;
529
549
  const probe = deps.probe ?? ((port) => probeReady(port, { backend }));
530
550
  // Textually a verifiedKill( call site, not merely a reference -- reused
531
551
  // UNCHANGED from broker-kill.mts, never re-derived, and never replaced by
@@ -560,6 +580,11 @@ export async function handleAcquire(requestId, stateDir, state, deps = {}) {
560
580
  // supervision deps below, so a crash-respawn of this instance can never
561
581
  // build a different backend's argv than the launch it replaces.
562
582
  backend,
583
+ // The SAME local `viceBin` const, threaded down unchanged -- see this
584
+ // function's own binding above and HandleAcquireDeps.viceBin's doc
585
+ // comment. `undefined` here is exactly what broker-launch.mts's own
586
+ // "x64sc"-literal default is for.
587
+ viceBin,
563
588
  allocateRemoteMonitorPort: deps.allocateRemoteMonitorPort,
564
589
  // The profile the warm arm just
565
590
  // refused to compromise on reaches buildViceArgs() here, and is
@@ -572,9 +597,9 @@ export async function handleAcquire(requestId, stateDir, state, deps = {}) {
572
597
  spawnFactory: deps.buildColdSpawnFactory ??
573
598
  ((port) => {
574
599
  const supervisorDir = join(stateDir, String(port));
575
- const { spawn, logRelPath } = makeLoggingSpawn(join(supervisorDir, "logs"));
600
+ const { spawn, logRelPath } = makeLoggingSpawn(join(supervisorDir, "logs"), viceBin);
576
601
  lastLogRelPath = logRelPath;
577
- return withCrashSupervision("acquire", port, spawn, superviseDepsFor(stateDir, state, backend));
602
+ return withCrashSupervision("acquire", port, spawn, superviseDepsFor(stateDir, state, backend, viceBin));
578
603
  }),
579
604
  });
580
605
  if (!result.ok) {
@@ -947,10 +972,38 @@ async function run(args) {
947
972
  // There is nothing left to detect -- the resolved
948
973
  // `backend` is always `"stock"`; what this call still does is resolve the
949
974
  // binary's own identity for the log line below and initialise the
950
- // capability cache backend-detect.mts's own record depends on.
951
- const backendResult = resolvedBackend({ supervisorDir: args.stateDir });
975
+ // capability cache backend-detect.mts's own record depends on. Phase 60
976
+ // (LOC-01/LOC-02): `toolsDir`/`projectRoot` are passed explicitly rather
977
+ // than left to backend-detect.mts's own supervisorDir-derived fallback
978
+ // (PD-03) -- this IS the one real call site with a genuine repoRoot to
979
+ // hand it, so there is no reason to make it guess. This is the ONE call
980
+ // in the whole real broker that ever reaches the tool-location seam; the
981
+ // resolved value below is threaded down through every real launch call
982
+ // site from here, never re-resolved per acquire.
983
+ const backendResult = resolvedBackend({
984
+ supervisorDir: args.stateDir,
985
+ toolsDir: join(args.repoRoot, ".c64-re-tools"),
986
+ projectRoot: args.repoRoot,
987
+ });
952
988
  const backend = backendResult.backend;
989
+ // The ONE value this process ever spawns as the emulator binary --
990
+ // resolved once, here, and threaded down through HandleAcquireDeps.viceBin
991
+ // (below) and onHostState's own `viceBin` field (task 2). Never re-read
992
+ // from resolvedBackend() a second time and never re-derived locally.
993
+ const resolvedViceBin = backendResult.binPath;
953
994
  process.stderr.write(`vice-broker: backend "${backend}" (binary: ${backendResult.binPath})\n`);
995
+ // Phase 60 gap closure (LOC-03, PD-13/T-60-15): written only when the
996
+ // tool-location seam refused a declared environment-variable override --
997
+ // `backendResult.locationRefusal` is `null` in every other case, including
998
+ // the PD-02 injected-override branch, which never reaches the seam at
999
+ // all. This is the ONE readable record a host operator has of "which
1000
+ // binary did this broker refuse to substitute, and why" -- no throw here:
1001
+ // this process's own uncaught-exception handlers kill the whole pool, and
1002
+ // only one of this broker's callers (the emulator spawn itself) needed
1003
+ // this value to be correct.
1004
+ if (backendResult.locationRefusal !== null) {
1005
+ process.stderr.write(`vice-broker: ${backendResult.locationRefusal}\n`);
1006
+ }
954
1007
  // THE BROKER mints/verifies the
955
1008
  // Ghidra runs-root handle here -- after the unconditional startup reap
956
1009
  // above, and BEFORE the control listener below accepts a single
@@ -989,6 +1042,20 @@ async function run(args) {
989
1042
  token,
990
1043
  onAcquire: (requestId, profile) => handleAcquire(requestId, args.stateDir, state, {
991
1044
  backend,
1045
+ // The ONCE-resolved `resolvedViceBin` local from this function's
1046
+ // own top (LOC-01/LOC-02) -- found missing here by Plan 60-05's
1047
+ // own required full-suite baseline diff (broker-e2e.test.ts's
1048
+ // "wired disconnect-while-queued" case): this real onAcquire
1049
+ // wiring is the ONE production call site that turns a tools.json
1050
+ // or VICE_BIN resolution into what the broker actually spawns,
1051
+ // and it was never supplying `viceBin` at all -- every unit test
1052
+ // calling handleAcquire() directly injects `viceBin` itself, so
1053
+ // this gap was invisible until an end-to-end, real-process test
1054
+ // exercised the genuine `run()` wiring. Without this, every real
1055
+ // acquire silently fell through to broker-launch.mts's own
1056
+ // "x64sc"-literal last-resort default, regardless of what
1057
+ // tools.json or VICE_BIN named.
1058
+ viceBin: resolvedViceBin,
992
1059
  // Threaded down to
993
1060
  // acquirePortAndLaunch()'s own gate (backend === "stock"); this
994
1061
  // callback does not re-read any environment variable itself.
@@ -1026,7 +1093,13 @@ async function run(args) {
1026
1093
  pid: process.pid,
1027
1094
  startedAt,
1028
1095
  nodeVersion: process.version,
1029
- viceBin: resolveViceBinForHostState(),
1096
+ // The SAME value THIS process just spawned (this function's own
1097
+ // `resolvedViceBin` closure binding, above) -- the deleted
1098
+ // per-host-state helper this file used to call here re-read the
1099
+ // emulator environment variable fresh on every call, which could
1100
+ // report a DIFFERENT binary than the one this broker actually
1101
+ // launched -- exactly the disagreement LOC-02 exists to remove.
1102
+ viceBin: resolvedViceBin,
1030
1103
  maxInstances: resolveCeilingForRecord(),
1031
1104
  basePort: resolveBasePort(),
1032
1105
  // The verdict THIS process resolved once, at
@@ -14,8 +14,8 @@
14
14
  // and a re-set must be refused rather than silently doubling up. Separately,
15
15
  // a `stop:false` checkpoint emits a CHECKPOINT_INFO frame per hit
16
16
  // SYNCHRONOUSLY, from inside the emulator's CPU loop, over the blocking
17
- // monitor socket (docs/phase0-binmon-findings.md §1; mon_breakpoint.c:557-562
18
- // calls mon_breakpoint_event() before checking cp->stop) -- on a hot address
17
+ // monitor socket -- confirmed directly from VICE's own source: mon_breakpoint.c:557-562
18
+ // calls mon_breakpoint_event() before checking cp->stop -- so on a hot address
19
19
  // this can stall the emulator thread and deadlock this client. Both guards
20
20
  // are correctness-as-safety issues, not polish, so they belong in the same
21
21
  // module as the tools that create the hazard.
package/stock-connect.ts CHANGED
@@ -67,8 +67,9 @@ export interface StockConnectBrokerControl {
67
67
 
68
68
  // ---------------------------------------------------------------------------
69
69
  // Capabilities -- BACK-04's version-gated answer set. Today this is just
70
- // CPUHISTORY_GET's three-way outcome (docs/phase0-binmon-findings.md §5,
71
- // D-10): 0x00 OK on the 3.10 fork, 0x83 INVALID_TYPE (opcode absent) on
70
+ // CPUHISTORY_GET's three-way outcome, confirmed by probing both a stock
71
+ // 3.9 build and the fork's 3.10-vintage build over the wire (D-10): 0x00 OK
72
+ // on the 3.10 fork, 0x83 INVALID_TYPE (opcode absent) on
72
73
  // stock 3.9, 0x8f CMD_FAILURE (compiled without support) as the distinct
73
74
  // third case. A later plan adding a second version-gated opcode extends this
74
75
  // type, not a parallel mechanism.
@@ -327,12 +328,12 @@ async function safeDisconnect(client: ViceMonitorClient): Promise<void> {
327
328
 
328
329
  /**
329
330
  * CR-02 (code review 2026-08-13). THE load-bearing counterpart to every
330
- * command this handshake sends. docs/phase0-binmon-findings.md §4, read from
331
- * VICE's own source, is explicit: `monitor_check_binary()` calls
332
- * `monitor_startup_trap()` on ANY INBOUND BYTE (monitor_binary.c:281), and
333
- * that check runs every vsync -- so the bare `PING` (0x81) below halts the
334
- * emulated C64 within roughly one frame and emits `STOPPED` (0x62). `EXIT`
335
- * (0xaa) is the ONLY thing that resumes it.
331
+ * command this handshake sends. Confirmed by reading directly from VICE's
332
+ * own source rather than any external summary: `monitor_check_binary()`
333
+ * calls `monitor_startup_trap()` on ANY INBOUND BYTE (monitor_binary.c:281),
334
+ * and that check runs every vsync -- so the bare `PING` (0x81) below halts
335
+ * the emulated C64 within roughly one frame and emits `STOPPED` (0x62).
336
+ * `EXIT` (0xaa) is the ONLY thing that resumes it.
336
337
  *
337
338
  * Before this function existed, nothing in this tree ever sent 0xaa: the
338
339
  * first `vice_ping` on the stock backend froze the machine and left it frozen
@@ -431,8 +432,9 @@ export async function stockConnect({ host, port, targetId, brokerControl, deps =
431
432
  // Step 3: api_version assertion. A non-0x02 api_version rejects this
432
433
  // send() call directly with a StockFramingError naming the observed
433
434
  // value -- see this function's own header comment. NOTE (CR-02): this
434
- // single byte HALTS the emulated machine (any inbound byte does --
435
- // docs/phase0-binmon-findings.md §4); step 7's EXIT is what undoes it.
435
+ // single byte HALTS the emulated machine (any inbound byte does, per
436
+ // monitor_check_binary()'s own vsync-driven trap -- see the CR-02
437
+ // comment above for the full mechanism); step 7's EXIT is what undoes it.
436
438
  await client.send(CommandType.Ping);
437
439
 
438
440
  // Step 4: build identity.
@@ -7,8 +7,10 @@
7
7
  // `handleExecutionUntilReturn` as `StockSessionHandler`s -- dispatch-table
8
8
  // and manifest wiring belong to plans 03-12/03-13, not here.
9
9
  //
10
- // WHY THIS FILE EXISTS: docs/phase0-binmon-findings.md §4 -- ANY inbound byte
11
- // halts the emulated machine (`monitor_startup_trap()` runs every vsync), so
10
+ // WHY THIS FILE EXISTS: ANY inbound byte halts the emulated machine --
11
+ // `monitor_check_binary()` calls `monitor_startup_trap()` on every byte the
12
+ // monitor socket receives (`monitor_binary.c:281`), and that check runs
13
+ // every vsync -- so
12
14
  // a bare PING (0x81) is the documented, side-effect-minimal way to trigger a
13
15
  // halt on demand, and EXIT (0xaa) is the ONLY thing that resumes it. D-05
14
16
  // means this client never sends an unrequested EXIT, which in turn means an
@@ -2,8 +2,9 @@
2
2
  // stock-memory-search.ts
3
3
  //
4
4
  // vice_memory_search / vice_memory_compare -- the DERIV-01 pair. Both are
5
- // DERIVED tools: the binary monitor's confirmed command set
6
- // (docs/phase0-binmon-findings.md §5) has no MEMORY_SEARCH or MEMORY_COMPARE
5
+ // DERIVED tools: the binary monitor's confirmed command set -- read directly
6
+ // out of VICE's own monitor_binary.c and reproduced by probing every opcode
7
+ // live against genuine stock VICE -- has no MEMORY_SEARCH or MEMORY_COMPARE
7
8
  // opcode at all, so both answers are computed CLIENT-SIDE from one bounded
8
9
  // MEM_GET read per range -- the same shape stock-disassemble.ts already
9
10
  // uses. Registered through withDerivedTool("...", { needsSession: true },
package/stock-protocol.ts CHANGED
@@ -73,10 +73,13 @@ export const MAX_BODY_LEN = 4 * 1024 * 1024;
73
73
  export const MAX_BUFFERED_LEN = RESPONSE_HEADER_LEN + MAX_BODY_LEN + 64 * 1024;
74
74
 
75
75
  // ---------------------------------------------------------------------------
76
- // Command / response / error "enums" -- one-for-one with
77
- // docs/phase0-binmon-findings.md §5's normative set, which is a superset of
78
- // the vendor's own CommandType (missing RESOURCE_GET/SET, CPUHISTORY_GET,
79
- // and USERPORT_SET).
76
+ // Command / response / error "enums" -- one-for-one with the wire format
77
+ // empirically confirmed against genuine stock VICE's binary monitor (every
78
+ // opcode probed live, request and response bytes captured and matched
79
+ // against monitor_binary.c's own encoder). This set is a superset of the
80
+ // vendor fork's own CommandType: it is missing RESOURCE_GET/SET,
81
+ // CPUHISTORY_GET, and USERPORT_SET, which stock supports and the fork's
82
+ // enum simply never grew to cover.
80
83
  //
81
84
  // Deviation from the plan's literal wording ("plain TypeScript enums, not
82
85
  // `const enum`"): a real TypeScript `enum` -- plain or const -- emits
@@ -375,9 +378,11 @@ export interface EncodeRequestHeaderOptions {
375
378
  body?: Buffer;
376
379
  }
377
380
 
378
- /** Build the normative 11-byte binary-monitor request header
379
- * (docs/phase0-binmon-findings.md §5) plus body: STX, api_version, uint32 LE
380
- * body length, uint32 LE request id, command type byte. */
381
+ /** Build the normative 11-byte binary-monitor request header -- the exact
382
+ * byte layout read directly out of VICE's own monitor_binary.c request
383
+ * decoder and reproduced on the wire against a genuine stock instance --
384
+ * plus body: STX, api_version, uint32 LE body length, uint32 LE request id,
385
+ * command type byte. */
381
386
  export function encodeRequestHeader({ commandType, requestId, body = Buffer.alloc(0) }: EncodeRequestHeaderOptions): Buffer {
382
387
  const header = Buffer.alloc(REQUEST_HEADER_LEN);
383
388
  header[0] = VICE_STX;
@@ -403,7 +408,8 @@ export function encodeRequestHeader({ commandType, requestId, body = Buffer.allo
403
408
  // converted to a TypeScript options-object signature matching
404
409
  // encodeRequestHeader()'s own style above. The rest are derived fresh from
405
410
  // the official VICE manual (vice-emu.sourceforge.io/vice_13.html §13) and
406
- // docs/phase0-binmon-findings.md §5; every encoder whose runtime BEHAVIOUR
411
+ // from the byte layout read directly out of VICE's own monitor_binary.c
412
+ // request decoder; every encoder whose runtime BEHAVIOUR
407
413
  // (not wire shape) is unconfirmed against a real binary says so explicitly
408
414
  // in its own JSDoc as [ASSUMED], naming the RESEARCH.md Assumptions Log
409
415
  // row -- never silently claimed as verified.
@@ -455,7 +461,10 @@ export interface MemspaceBodyOptions {
455
461
  }
456
462
 
457
463
  /** The one-byte body shared by REGISTERS_GET (0x31) and REGISTERS_AVAILABLE
458
- * (0x83). [CITED docs/phase0-binmon-findings.md §5] */
464
+ * (0x83): a single memspace byte, confirmed against monitor_binary.c's own
465
+ * request decoder and by a live probe against genuine stock VICE that sent
466
+ * each memspace value and matched the returned register set to the
467
+ * expected bank. */
459
468
  export function memspaceBody({ memspace }: MemspaceBodyOptions = {}): Buffer {
460
469
  return Buffer.from([memspaceByte(memspace)]);
461
470
  }
@@ -474,7 +483,8 @@ export interface MemGetBodyOptions {
474
483
  * MEM_GET (0x01) request body -- ALWAYS EXACTLY 8 BYTES:
475
484
  * `sidefx(1) start(u16LE) end(u16LE) memspace(1) bank(u16LE)`.
476
485
  * [VERIFIED against probe-binmon.mjs:268-276, this repo's own
477
- * offline-tested reference; CITED docs/phase0-binmon-findings.md §5]
486
+ * offline-tested reference, and against the field layout read directly
487
+ * out of monitor_binary.c's own MEM_GET request decoder]
478
488
  *
479
489
  * The body is always 8 bytes -- never shorter -- because stock VICE's
480
490
  * `monitor_binary.c` handler dereferences every one of these fields before
@@ -510,7 +520,9 @@ export interface MemSetBodyOptions {
510
520
  * MEM_SET (0x02) request body -- the same 8-byte header as memGetBody(),
511
521
  * with `sidefx` forced to `0x00` (MEM_SET has no side-effect flag on the
512
522
  * wire), then `data` appended at offset 8.
513
- * [VERIFIED probe-binmon.mjs:278-287; CITED docs/phase0-binmon-findings.md §5]
523
+ * [VERIFIED probe-binmon.mjs:278-287, and against monitor_binary.c's own
524
+ * MEM_SET request decoder, which reads the identical 8-byte header before
525
+ * the variable-length data that follows it]
514
526
  */
515
527
  export function memSetBody({ start, end, memspace, bank = 0x0000, data }: MemSetBodyOptions): Buffer {
516
528
  requireU16("start", start);
@@ -540,7 +552,9 @@ export function memSetBody({ start, end, memspace, bank = 0x0000, data }: MemSet
540
552
  * CHECKPOINT_DELETE (0x13). Not an options object (matching
541
553
  * probe-binmon.mjs:314-318's own bare-number signature) since it has
542
554
  * nothing else to validate offline.
543
- * [VERIFIED probe-binmon.mjs:314-318; CITED docs/phase0-binmon-findings.md §5]
555
+ * [VERIFIED probe-binmon.mjs:314-318, and against monitor_binary.c's own
556
+ * CHECKPOINT_GET/CHECKPOINT_DELETE request decoder, which reads exactly
557
+ * this 4-byte checkpoint number and nothing else]
544
558
  */
545
559
  export function cpNumBody(checkpointNum: number): Buffer {
546
560
  requireU32("checkpointNum", checkpointNum);
@@ -568,7 +582,9 @@ export interface CheckpointSetBodyOptions {
568
582
  * CHECKPOINT_SET (0x12) request body -- 8 bytes, or 9 when `memspace` is
569
583
  * supplied: `start(u16LE) end(u16LE) stop(1) enabled(1) operation(1)
570
584
  * temporary(1) [memspace(1)]`.
571
- * [VERIFIED probe-binmon.mjs:290-309; CITED docs/phase0-binmon-findings.md §5]
585
+ * [VERIFIED probe-binmon.mjs:290-309, and against monitor_binary.c's own
586
+ * CHECKPOINT_SET request decoder, which only reads the ninth memspace byte
587
+ * when the declared body length says it is present]
572
588
  */
573
589
  export function checkpointSetBody({
574
590
  start,
@@ -606,7 +622,9 @@ export interface CheckpointToggleBodyOptions {
606
622
  }
607
623
 
608
624
  /** CHECKPOINT_TOGGLE (0x15) request body -- 5 bytes,
609
- * `checkpointNum(u32LE) enabled(1)`. [CITED docs/phase0-binmon-findings.md §5] */
625
+ * `checkpointNum(u32LE) enabled(1)`: read directly out of monitor_binary.c's
626
+ * own CHECKPOINT_TOGGLE request decoder, which reads exactly these five
627
+ * bytes and nothing else. */
610
628
  export function checkpointToggleBody({ checkpointNum, enabled }: CheckpointToggleBodyOptions): Buffer {
611
629
  requireU32("checkpointNum", checkpointNum);
612
630
  const body = Buffer.alloc(5);
@@ -624,7 +642,9 @@ export interface ConditionSetBodyOptions {
624
642
  * CONDITION_SET (0x22) request body -- `checkpointNum(u32LE) exprLen(1)
625
643
  * expr(ASCII, NOT NUL-terminated)`.
626
644
  * [VERIFIED probe-binmon.mjs:320-332, including its own >255-byte guard,
627
- * ported verbatim; CITED docs/phase0-binmon-findings.md §5]
645
+ * ported verbatim, and against monitor_binary.c's own CONDITION_SET request
646
+ * decoder, which reads exprLen as a single uint8 immediately before the
647
+ * expression bytes]
628
648
  *
629
649
  * This is the ONLY function in this tree that ever turns condition TEXT
630
650
  * into wire bytes -- its `expression` argument must always come from
@@ -685,7 +705,8 @@ export interface RegistersSetBodyOptions {
685
705
  * REGISTERS_SET (0x32) request body -- `memspace(1) count(u16LE)` then per
686
706
  * item `itemSize(1) regId(1) value(u16LE)`, with `itemSize` always `3`
687
707
  * (the byte count following the itemSize byte itself: 1 id byte + 2 value
688
- * bytes). [CITED docs/phase0-binmon-findings.md §5]
708
+ * bytes), matching the stride monitor_binary.c's own REGISTERS_SET request
709
+ * decoder walks each item with.
689
710
  *
690
711
  * This is the structural inverse of this file's `ResponseType.RegisterInfo`
691
712
  * parser case (below, in the response-parsing section -- grep `case
@@ -738,8 +759,11 @@ export interface AdvanceInstructionsBodyOptions {
738
759
 
739
760
  /**
740
761
  * ADVANCE_INSTRUCTIONS (0x71) request body -- 3 bytes, `stepOver(1)
741
- * count(u16LE)`. [CITED docs/phase0-binmon-findings.md §5; body SHAPE also
742
- * exercised, with stepOver=0 only, in probe-binmon.mjs's async-events check]
762
+ * count(u16LE)`, confirmed byte-for-byte against monitor_binary.c's own
763
+ * decoder and, for the stepOver=0 case, exercised live against genuine
764
+ * stock VICE by probe-binmon.mjs's async-events check (the check that
765
+ * proved STOPPED/RESUMED events arrive interleaved with the command
766
+ * response rather than only after it).
743
767
  *
744
768
  * `stepOver = true`'s runtime meaning (skip a `JSR`'s subroutine as one
745
769
  * step, matching the fork's own `stepOver` field name) was live-probed
@@ -767,8 +791,10 @@ export interface KeyboardFeedBodyOptions {
767
791
  }
768
792
 
769
793
  /**
770
- * KEYBOARD_FEED (0x72) request body -- `textLen(1) text(bytes)`.
771
- * [CITED docs/phase0-binmon-findings.md §5]
794
+ * KEYBOARD_FEED (0x72) request body -- `textLen(1) text(bytes)`, read
795
+ * directly out of monitor_binary.c's own KEYBOARD_FEED request decoder,
796
+ * which takes a one-byte length prefix followed by exactly that many raw
797
+ * bytes -- no PETSCII conversion happens on the wire side.
772
798
  */
773
799
  export function keyboardFeedBody({ petscii }: KeyboardFeedBodyOptions): Buffer {
774
800
  if (petscii.length === 0) {
@@ -789,8 +815,8 @@ export interface JoyportSetBodyOptions {
789
815
  }
790
816
 
791
817
  /**
792
- * JOYPORT_SET (0xa2) request body -- 4 bytes, `port(u16LE) value(u16LE)`.
793
- * [CITED docs/phase0-binmon-findings.md §5]
818
+ * JOYPORT_SET (0xa2) request body -- 4 bytes, `port(u16LE) value(u16LE)`,
819
+ * read directly out of monitor_binary.c's own JOYPORT_SET request decoder.
794
820
  *
795
821
  * The body SHAPE is cited; the BIT MEANING of `value` (which bit is
796
822
  * up/down/left/right/fire) is [ASSUMED] -- RESEARCH.md Assumptions Log row
@@ -817,8 +843,9 @@ export interface ResetBodyOptions {
817
843
  }
818
844
 
819
845
  /**
820
- * RESET (0xcc) request body -- 1 byte, `resetMode`.
821
- * [CITED docs/phase0-binmon-findings.md §5]
846
+ * RESET (0xcc) request body -- 1 byte, `resetMode`, read directly out of
847
+ * monitor_binary.c's own RESET request decoder, which reads exactly this
848
+ * one byte and nothing else.
822
849
  *
823
850
  * NOT the RESOURCE_SET (0x52) power-cycle hazard CLAUDE.md warns about
824
851
  * (`MachineVideoStandard`/`VICIIModel`/`MachinePowerFrequency`, the CUT
@@ -845,7 +872,9 @@ export interface AutostartBodyOptions {
845
872
 
846
873
  /**
847
874
  * AUTOSTART (0xdd) request body -- `runAfter(1) fileIndex(u16LE)
848
- * filenameLen(1) filename(ASCII)`. [CITED docs/phase0-binmon-findings.md §5]
875
+ * filenameLen(1) filename(ASCII)`, read directly out of monitor_binary.c's
876
+ * own AUTOSTART request decoder, which reads the filename length as the
877
+ * single byte immediately following the fixed runAfter/fileIndex fields.
849
878
  *
850
879
  * AUTOSTART has NO drive-unit field at all -- a caller cannot target units
851
880
  * 9-11 through this opcode (plan 03-10 owns the refusal for those units).
@@ -871,7 +900,9 @@ export interface DumpBodyOptions {
871
900
 
872
901
  /**
873
902
  * DUMP (0x41) request body -- `saveRoms(1) saveDisks(1) filenameLen(1)
874
- * filename(ASCII)`. [CITED docs/phase0-binmon-findings.md §5]
903
+ * filename(ASCII)`, read directly out of monitor_binary.c's own DUMP
904
+ * request decoder, which reads the two flag bytes before the filename
905
+ * length prefix.
875
906
  */
876
907
  export function dumpBody({ saveRoms, saveDisks, filename }: DumpBodyOptions): Buffer {
877
908
  const filenameBuf = requireAsciiFilename("dumpBody", filename);
@@ -887,8 +918,9 @@ export interface UndumpBodyOptions {
887
918
  filename: string;
888
919
  }
889
920
 
890
- /** UNDUMP (0x42) request body -- `filenameLen(1) filename(ASCII)`.
891
- * [CITED docs/phase0-binmon-findings.md §5] */
921
+ /** UNDUMP (0x42) request body -- `filenameLen(1) filename(ASCII)`, read
922
+ * directly out of monitor_binary.c's own UNDUMP request decoder, which
923
+ * reads the filename length as the frame's very first byte. */
892
924
  export function undumpBody({ filename }: UndumpBodyOptions): Buffer {
893
925
  const filenameBuf = requireAsciiFilename("undumpBody", filename);
894
926
  const body = Buffer.alloc(1 + filenameBuf.length);
@@ -12,7 +12,9 @@
12
12
  // vice_registers_set takes a register NAME (D-03: stock keeps the fork's
13
13
  // argument shape). VICE enumerates its own register ids through
14
14
  // REGISTERS_AVAILABLE, and those ids are NOT guaranteed identical across
15
- // builds (docs/phase0-binmon-findings.md) -- so a caller must resolve a
15
+ // builds -- confirmed by reading VICE's own mon_register.c, which assigns
16
+ // ids from a per-machine table that is never pinned to a fixed numbering
17
+ // across ports -- so a caller must resolve a
16
18
  // name through the CONNECTED build's own answer, never a table this
17
19
  // module wrote down in advance. This file is the one seam that performs
18
20
  // that resolution, caches it per session (so every call after the first
package/text-protocol.ts CHANGED
@@ -301,9 +301,12 @@ export const HAZARD_SUBJECT_PRG_RELPATHS = Object.freeze({
301
301
  * `exportAsmTree()` itself emitted (`scope_087a.a`) rather than in the
302
302
  * hand-written `modified` fixture family, reassembled through the same
303
303
  * single oracle against a pre-registered byte manifest committed at
304
- * `fixtures/hazard-subject/exported-edit.manifest.json`. See
305
- * `docs/phase50-exported-edit-findings.md` and
306
- * `docs/phase50-exported-modifiability-transcript.md`. */
304
+ * `fixtures/hazard-subject/exported-edit.manifest.json`. Both the
305
+ * rebuild and the live-behaviour gate came back acknowledged: the
306
+ * reassembly reproduced the manifest byte-for-byte and both behaviour
307
+ * changes were independently confirmed to take effect in a running
308
+ * genuine stock VICE session via checkpoint-driven RAM captures, not
309
+ * just in the rebuilt bytes on disk. */
307
310
  "exported-edit": Object.freeze([
308
311
  "src",
309
312
  "mcp",
@@ -142,13 +142,18 @@ const RULES_LINE = "------------- ------ ------------- ------";
142
142
 
143
143
  /** VICE's own cold-profiler sentence, quoted byte-for-byte -- including its
144
144
  * embedded double quotes and its trailing period -- from
145
- * `text-protocol.ts`'s `TEXT_COMMAND_ALLOWLIST` doc comment and
146
- * `docs/phase42-text-format-drift-citations.md`'s Block 9. Unlike this
145
+ * `text-protocol.ts`'s `TEXT_COMMAND_ALLOWLIST` doc comment. Unlike this
147
146
  * module's siblings' source-traced strings, this one is MEASURED: observed
148
147
  * live against genuine stock `x64sc (VICE 3.9)`, 2026-09-09. `prof flat`
149
148
  * alone, on a freshly connected session that has never issued `prof on`,
150
149
  * returns exactly this sentence -- the profiler subsystem is compiled in
151
- * and the command itself is fine, it simply has nothing recorded yet. */
150
+ * and the command itself is fine, it simply has nothing recorded yet. This
151
+ * was a genuinely new live finding, present in neither committed fixture:
152
+ * VICE's flat profiler defaults OFF, and no production handler in this
153
+ * tree issues `prof on` before dialing `prof flat`, so `vice_profile_flat`
154
+ * as shipped cannot yet produce real profile rows against a freshly
155
+ * launched instance -- a real, separately-tracked gap this live run
156
+ * surfaced rather than silently absorbed. */
152
157
  export const PROFILING_NOT_STARTED_TEXT = 'No profiling data available. Start profiling with "prof on".';
153
158
 
154
159
  /** The narrow no-break space (U+202F) VICE uses as its thousands separator