@henols/c64-re-tools 0.1.12 → 0.2.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.
package/bin/cli.mjs CHANGED
@@ -26,14 +26,57 @@ const HERE = dirname(fileURLToPath(import.meta.url)); // installer/bin (packed)
26
26
  const PKG_ROOT = dirname(HERE);
27
27
  const SKILLS_SRC = join(PKG_ROOT, "skills");
28
28
 
29
+ // The single version-resolution seam this repo maintains is
30
+ // `.claude/mcp/vice/version.ts` (quick-260819-tsz, D-5). This package
31
+ // deliberately does NOT import it: it ships without the seam file (its
32
+ // `files[]` is `bin/`, `skills/`, `README.md`) and targets node >= 18, which
33
+ // cannot type-strip the seam's `.ts` the way the vice-mcp package's own
34
+ // node >= 22.18 runtime can. What follows is exactly the seam's own
35
+ // precedence step 1 -- "read my own package.json's `.version`, trust it
36
+ // when it is a real published number" -- not a second, independent
37
+ // implementation of the resolution algorithm; there is no template/`-`
38
+ // handling here because this package is never resolved from `VERSION`,
39
+ // only ever published with a concrete version already stamped in. That
40
+ // number is PRODUCED elsewhere: `scripts/version.mjs stamp` writes the
41
+ // working-tree placeholder, and CI's `npm version` writes the real one at
42
+ // publish time. Do not reimplement D-2's template-resolution rules here.
29
43
  const SELF = readJson(join(PKG_ROOT, "package.json")) ?? {};
30
44
  const SELF_VERSION = typeof SELF.version === "string" ? SELF.version : "0.0.0";
31
45
  const MCP_PKG = "@henols/vice-mcp";
32
- // Wire the project to the exact vice-mcp version this installer was built against.
33
- const MCP_VERSION =
34
- (SELF.dependencies && typeof SELF.dependencies[MCP_PKG] === "string"
46
+ // The dev placeholder every derived, publishable version string carries in
47
+ // the working tree (R-2, quick-260819-tsz). Defined authoritatively as
48
+ // `DEV_PLACEHOLDER` in `.claude/mcp/vice/version.ts` -- repeated here as a
49
+ // literal, NOT imported, because this package deliberately ships without
50
+ // that seam file (see the comment above) and targets node >= 18, which
51
+ // cannot type-strip a `.ts` import the way the vice-mcp package's own
52
+ // node >= 22.18 runtime can. This is the same disclosed divergence as
53
+ // `SELF_VERSION` above: one literal, kept in sync by hand, documented here
54
+ // so a future edit to the seam's placeholder is not missed.
55
+ const MCP_DEV_PLACEHOLDER = "0.0.0-dev";
56
+ // Wire the project to the exact vice-mcp version this installer was built
57
+ // against -- EXCEPT when run from an unstamped dev checkout (MED-3):
58
+ // `installer/package.json`'s dependency pin is the permanent working-tree
59
+ // placeholder outside a CI-stamped publish job, and `@henols/vice-mcp` at
60
+ // that literal version will never exist on the npm registry. Silently
61
+ // writing it into a consumer's .mcp.json would 404 every time Claude Code
62
+ // tries to launch the server, with no error until the user actually tries
63
+ // to use it -- strictly worse than falling back to "latest". Fall back AND
64
+ // warn loudly, so a developer testing the installer locally knows their
65
+ // .mcp.json is unpinned rather than discovering it via a silent 404 later.
66
+ const MCP_VERSION_RAW =
67
+ SELF.dependencies && typeof SELF.dependencies[MCP_PKG] === "string"
35
68
  ? SELF.dependencies[MCP_PKG].replace(/^[\^~]/, "")
36
- : SELF_VERSION);
69
+ : SELF_VERSION;
70
+ if (MCP_VERSION_RAW === MCP_DEV_PLACEHOLDER) {
71
+ console.error(
72
+ `c64-re-tools: WARNING -- running from an unstamped dev checkout (installer/package.json pins ` +
73
+ `${MCP_PKG}@${MCP_DEV_PLACEHOLDER}, the dev placeholder, not a real published version). ` +
74
+ `Falling back to "${MCP_PKG}@latest" in the generated .mcp.json instead of writing an ` +
75
+ `unresolvable pin. If you intended to test against a specific version, pass --vendor or fix ` +
76
+ `installer/package.json's dependency pin before packaging a release.`
77
+ );
78
+ }
79
+ const MCP_VERSION = MCP_VERSION_RAW === MCP_DEV_PLACEHOLDER ? "latest" : MCP_VERSION_RAW;
37
80
 
38
81
  function readJson(path) {
39
82
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henols/c64-re-tools",
3
- "version": "0.1.12",
3
+ "version": "0.2.0",
4
4
  "description": "Installer that adds the C64 reverse-engineering skills and the VICE emulator MCP server (@henols/vice-mcp) to a project.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,7 +44,7 @@
44
44
  "installer"
45
45
  ],
46
46
  "dependencies": {
47
- "@henols/vice-mcp": "0.1.12"
47
+ "@henols/vice-mcp": "0.2.0"
48
48
  },
49
49
  "scripts": {
50
50
  "sync-skills": "node scripts/sync-skills.mjs",
@@ -168,5 +168,5 @@ editing a grade in place. File-changing work enters through a GSD command (`/gsd
168
168
  | A sprite decodes as noise | Check `$D015` first; a disabled sprite's registers are stale. Then check MCM — multicolor decoded as hires comes out twice as wide. |
169
169
  | Computed mode is "INVALID — screen goes black" | You caught the registers mid-update inside a raster split. Re-read. |
170
170
  | The emulator looks dead | Enumerate armed checkpoints before anything else. See hazard 2. |
171
- | `vice_keyboard_type` does nothing | The game polls `$DC00`/`$DC01` directly. Use `vice_keyboard_matrix`. |
171
+ | `vice_keyboard_type` does nothing | The game polls `$DC00`/`$DC01` directly. Use `vice_keyboard_matrix` (**requires the fork backend** — see `references/observation-hazards.md` § 4 for the stock route). |
172
172
  | Two captures of the same checkpoint differ | Expected. Full-64K identity is impossible in principle; use `c64-ram-capture`'s drift rules. |
@@ -87,6 +87,11 @@ address to checkpoint when the emulator is next available — press RESTORE with
87
87
  until the line is released, so it is a press→release **edge**), then `vice_machine_reset` soft and
88
88
  hard, and record where the PC actually lands.
89
89
 
90
+ **`vice_keyboard_restore` requires the fork backend.** The RESTORE key pulses the NMI line
91
+ directly and is not part of the keyboard matrix, so stock's `KEYBOARD_FEED` (which only injects
92
+ PETSCII text into the buffer) cannot produce it. Calling it on the stock backend returns an error
93
+ naming the reason and the fork backend, rather than pulsing RESTORE.
94
+
90
95
  **Evidence:** derived mechanically from six three-run-verified captures; every value identical
91
96
  across all three runs of its release, so none of it is drift.
92
97
  **Confidence:** HIGH for the values and for the cross-release divergence. The *interpretation* of
@@ -156,6 +161,10 @@ at a title screen and again in gameplay, diff the two captures, and look for a s
156
161
  changed in zero page or low RAM. `vice_memory_compare` narrows this; `c64-ram-capture` § Compare
157
162
  two captures gives the volatility rules that stop you chasing drift.
158
163
 
164
+ On stock, only `mode: 'ranges'` is served — capture the two states at different points in time and
165
+ compare two live ranges. `mode: 'snapshot'` is refused with an explanatory message; there is no
166
+ memory-only snapshot producer on either backend.
167
+
159
168
  ## Verified against this project — 2026-08-04
160
169
 
161
170
  Running `derive.mjs vectors` cold on both releases' `*-gameentry-run1.bin` returns `$01` = `$40`
@@ -71,3 +71,9 @@ hardware registers are what the game uses.
71
71
  `vice_sprite_get` / `vice_sprite_inspect` do the pointer arithmetic and the multicolor bit-pair
72
72
  unpacking. Verify what they return once against a hand-resolved pointer — `derive.mjs sprites`
73
73
  gives you that hand resolution — then trust them.
74
+
75
+ On stock, `vice_vicii_get_state`'s `$D018` pointers are reported **bank-relative**;
76
+ `vice_sprite_get` resolves the absolute `screenBase` and per-sprite `dataAddress` for you.
77
+ `vice_sprite_inspect`'s ASCII grid is the sprite's native 24x21 (hi-res) or 12x21 (multicolour)
78
+ **data block** — it is **not** scaled by the `$D017`/`$D01D` expansion bits, so a sprite shown
79
+ double-size on screen still renders at its native resolution in the grid.
@@ -80,6 +80,21 @@ Prefer the whole-chip reads — `vice_vicii_get_state`, `vice_cia_get_state`, `v
80
80
  — over raw register reads. Whether the VICE monitor's own read path is side-effect-free is
81
81
  **unverified**: treat it as verify-don't-assume rather than taking it on faith.
82
82
 
83
+ On the stock backend, `vice_vicii_get_state`/`vice_cia_get_state` reads are `sidefx: false` with
84
+ no argument able to override it — **VERIFIED**, asserted on the wire body by a regression test.
85
+ Whether the emulator's own `MEM_GET` read path actually honours that flag for
86
+ `$D01E`/`$D01F`/`$DC0D`/`$DD0D` — i.e. whether it truly cannot clear them — is **ASSUMED**, with
87
+ no probe recorded in this repo; treat it as no worse than the fork's own unverified path, not as
88
+ a proven guarantee. `vice_sid_get_state` is **fork-only**, since SID `$D400-$D418` is write-only
89
+ in hardware and the binary monitor has no SID command. Also: on stock, an internal field the
90
+ register map cannot expose is marked `{ available: false, reason }` in the answer, never a bare
91
+ `0` — do not record a stock `0` from one of these fields as a measurement; check `available`
92
+ first. A stock chip-state or sprite answer also **names the memory view it read** (`bank`, or
93
+ `registerBank`/`dataBank`), read through the emulator's own `io`/`ram` banks, so it stays valid
94
+ even while the program has I/O banked out ($01 driving the RAM/ROM/I-O switch). An answer with
95
+ **no** bank field — an older transcript, or the fork backend — is suspect whenever `$01` may not
96
+ have been `$37`: those bytes may be the RAM underneath the I/O area, not registers.
97
+
83
98
  ## 4. The keyboard buffer is not how games read keys
84
99
 
85
100
  **Evidence: live. Confidence: HIGH. Cost: an afternoon.**
@@ -88,6 +103,14 @@ Games and cracks poll the `$DC00`/`$DC01` matrix directly, bypassing the KERNAL
88
103
  `vice_keyboard_type` is invisible to them. Use `vice_keyboard_matrix`, and hold a key across a
89
104
  gate by releasing it at the trigger checkpoint, never earlier.
90
105
 
106
+ **`vice_keyboard_matrix` requires the fork backend.** The binary monitor's `KEYBOARD_FEED` (0x72)
107
+ only injects PETSCII text into the KERNAL keyboard buffer; the emulator recomputes CIA port B from
108
+ its own keyboard array on every read, so there is no wire command that can drive the raw matrix —
109
+ this is unrecoverable on stock, not merely unbuilt. On stock, use `vice_keyboard_type` /
110
+ `vice_keyboard_petscii` when the gate reads the KERNAL buffer, or `vice_joystick_set` when it polls
111
+ the matrix directly instead; either way, buffer injection is invisible to a program polling
112
+ `$DC00`/`$DC01` itself, so a matrix-polling gate must be driven by the joystick or not at all.
113
+
91
114
  ## 5. Most state reads pause the emulator and do not resume it
92
115
 
93
116
  Read all state first, poll with `vice_ping` (the non-pausing poll), and resume **exactly once** at
@@ -53,12 +53,20 @@ One that programs `$DC04-$DC07` and enables timer A runs its own timebase.
53
53
  writing `$DD00` is usually talking to the drive.
54
54
  - **`$DC0D`/`$DD0D` clear the interrupt flags on read**, the same shape as `$D01E`/`$D01F`. Reading
55
55
  one steals an interrupt the game was about to service. Prefer `vice_cia_get_state`. The VICE
56
- monitor's exact behaviour here is **unverified** — verify, don't assume.
56
+ monitor's exact behaviour here is **unverified** — verify, don't assume. On stock,
57
+ `vice_cia_get_state` reports the **read** side of `$xx0D` as `interruptStatus` and marks the
58
+ write-side enable mask `unavailable` — the two share one address with different meanings, so a
59
+ reader looking for "which interrupts are enabled" is not silently handed the flags that have
60
+ fired.
57
61
  - **Direct `$DC00`/`$DC01` polling is the norm, and it defeats `vice_keyboard_type`.**
58
62
  **Evidence: live, established on this project during recovery work. Confidence: HIGH. Cost: an
59
63
  afternoon.** Games and cracks bypass the KERNAL keyboard buffer and read the matrix directly.
60
64
  Assume it until shown otherwise, and drive input with `vice_keyboard_matrix` or the joystick
61
- tools instead.
65
+ tools instead. **`vice_keyboard_matrix` requires the fork backend** — the binary monitor's
66
+ `KEYBOARD_FEED` only injects PETSCII buffer text and cannot drive the raw matrix. On stock, use
67
+ `vice_keyboard_type` / `vice_keyboard_petscii` when the gate reads the KERNAL buffer, or
68
+ `vice_joystick_set` when it polls the matrix directly; buffer injection stays invisible to a
69
+ program polling `$DC00`/`$DC01` itself.
62
70
 
63
71
  ## Finding input handling from the observable side
64
72
 
@@ -14,11 +14,12 @@ usage, not measured). Individual rows that have since been exercised live are ma
14
14
  | What does the handler at this vector do? | `vice_disassemble` — the emulator's own decoder, not a dead listing |
15
15
  | Is this really the main loop? | `vice_checkpoint_add` + `vice_run_until` + `vice_registers_get` — fires once per frame ⇒ proven |
16
16
  | What code writes this? | `vice_watch_add` — finds **writers**. Best targets: `$D018`, VM+`$03F8`, `$D404` |
17
- | Whole-chip state without the read hazards | `vice_vicii_get_state` / `vice_sid_get_state` / `vice_cia_get_state`**prefer these over raw register reads** |
18
- | Decode sprite data | `vice_sprite_get` / `vice_sprite_inspect` |
19
- | Find a known byte pattern | `vice_memory_search` |
20
- | Carry labels across sessions | `vice_symbols_load` / `vice_symbols_lookup` — ACME `--vicelabels` and regenerator2000 output share this channel |
21
- | Is the machine wedged, or did it stop itself? | `vice_diagnose` five-state verdict with its evidence. **Reachable and proxy-intercepted as of 2026-08-04** (verified live). Triage tree: `vice-wedge-triage` |
17
+ | Whole-chip VIC-II/CIA state without the read hazards | `vice_vicii_get_state` / `vice_cia_get_state` (**both backends**) — prefer these over raw register reads |
18
+ | Whole-chip SID state without the read hazards | `vice_sid_get_state` (**requires the fork** — SID `$D400-$D418` is write-only in hardware and the binary monitor has no SID command; unrecoverable on stock) |
19
+ | Decode sprite data | `vice_sprite_get` / `vice_sprite_inspect` (**both backends**) |
20
+ | Find a known byte pattern | `vice_memory_search` (**both backends**) |
21
+ | Carry labels across sessions | `vice_symbols_load` / `vice_symbols_lookup` (**both backends**) ACME `--vicelabels` and regenerator2000 output share this channel |
22
+ | Is the machine wedged, or did it stop itself? | `vice_diagnose` — five-state verdict with its evidence (the two backends' verdict sets differ by one; see `docs/stock-vice-parity.md` D-03). **Reachable and proxy-intercepted as of 2026-08-04** (verified live). Triage tree: `vice-wedge-triage` |
22
23
  | Replace a wedged instance | `vice_recycle` — destructive, requires a `reason`, and that reason is written into `.planning/incidents/` **before** anything is killed. The reason *is* the evidence record |
23
24
  | Read the restart epoch | **No tool does.** The proxy compares it around every forwarded call and raises drift itself; a value comes from that error or from `vice_diagnose` |
24
25
 
@@ -34,10 +35,14 @@ usage, not measured). Individual rows that have since been exercised live are ma
34
35
 
35
36
  ## Three traps in this table
36
37
 
37
- **`vice_run_until` has no working timeout.** Its schema documents `cycles` as *"not yet
38
- implemented"*, so a run to an address the program never reaches has nothing to bound it and looks
39
- exactly like a wedged emulator. Prefer `vice_checkpoint_add` + a bounded poll when the address is
40
- a hypothesis rather than a certainty. **Confidence: MEDIUM** read off the schema, not reproduced.
38
+ **`vice_run_until`'s timeout is backend-qualified it has none on the fork, but stock bounds it
39
+ (Phase 7, D-02).** On the fork, `cycles` is documented as *"not yet implemented"* and there is no
40
+ `timeout_ms` either, so a run to an address the program never reaches has nothing to bound it and
41
+ looks exactly like a wedged emulator; prefer `vice_checkpoint_add` + a bounded poll when the
42
+ address is a hypothesis rather than a certainty. **Confidence: MEDIUM on the fork** — read off the
43
+ schema, not reproduced. On stock, `timeout_ms` (default 30000, ceiling 600000) bounds the wait, and
44
+ a timed-out answer says the machine is left halted rather than looking like a wedge — see
45
+ `vice-wedge-triage` for the full triage judgement and its live evidence; do not restate it here.
41
46
 
42
47
  **`vice_diagnose` leaves the machine paused.** When it measures a cycle bracket it resumes the
43
48
  machine once or twice and then leaves it **paused** — resuming is your own next call. And a
@@ -156,6 +156,11 @@ only after the provenance diff partitions loader from cracktro from game — see
156
156
  ## Find an entry point
157
157
 
158
158
  1. Press past any "hit any key" gate with `mcp__plugin_c64-re-tools_vice__vice_keyboard_matrix`.
159
+ **This call requires the fork backend** — the binary monitor's `KEYBOARD_FEED` only injects
160
+ PETSCII text into the KERNAL buffer and cannot drive the raw matrix. On stock, use
161
+ `vice_keyboard_type` / `vice_keyboard_petscii` when the gate reads the KERNAL buffer, or
162
+ `vice_joystick_set` when it polls the matrix directly; buffer injection stays invisible to a
163
+ program polling `$DC00`/`$DC01` itself.
159
164
  2. Step forward in batches with `mcp__plugin_c64-re-tools_vice__vice_execution_step`, reading
160
165
  `mcp__plugin_c64-re-tools_vice__vice_registers_get` after each batch.
161
166
  3. Stop when the program counter and the stack pointer both settle into a
@@ -5,8 +5,9 @@ description: Decide whether a VICE emulator that has stopped responding is genui
5
5
 
6
6
  # Triage a VICE that stopped moving
7
7
 
8
- **Four states look identical from outside, and the intuitive fix destroys a healthy machine in
9
- one of them.** Work the order below. Do not start with a remedy.
8
+ **On the fork, four states look identical from outside; on stock, it is five, and the intuitive
9
+ fix destroys a healthy machine in more than one of them.** Work the order below. Do not start with
10
+ a remedy.
10
11
 
11
12
  | State | Cheap tell | Safe action |
12
13
  |---|---|---|
@@ -14,14 +15,22 @@ one of them.** Work the order below. Do not start with a remedy.
14
15
  | **Stopped itself at your checkpoint** | An armed *stopping* checkpoint on the live IRQ path | Delete/disable the checkpoint. **Never recycle** |
15
16
  | **Crashed and respawned** | The proxy raises epoch drift on the next forwarded call | Void the run, reboot from scratch. Already handled for you |
16
17
  | **Genuinely wedged** | Two consecutive cycle brackets read exactly `0` | `vice_recycle` with a reason, as a last resort |
18
+ | **Monitor held elsewhere (stock only)** | A second client already holds this instance's single binary-monitor socket | Find the other holder. **Never recycle** — the instance is healthy, just claimed elsewhere |
17
19
 
18
20
  ```
19
- mcp__plugin_c64-re-tools_vice__vice_diagnose # one call, no arguments, answers which of the five it is
21
+ mcp__plugin_c64-re-tools_vice__vice_diagnose # one call, no arguments, answers which state it is
20
22
  ```
21
23
 
22
- `vice_diagnose` returns a closed five-verdict vocabulary `restarted`, `checkpoint_trap`,
23
- `wedged`, `stale_read_path`, `live` with the evidence that produced it. Read its schema for the
24
- contract; this skill is the judgement around it.
24
+ `vice_diagnose`'s verdict vocabulary differs by backend, because stock VICE's binary monitor
25
+ services exactly one client and the fork's non-pausing `vice_ping` has no stock equivalent (see
26
+ `docs/stock-vice-parity.md` D-03 for the full reasoning). The fork answers `restarted`,
27
+ `checkpoint_trap`, `wedged`, `stale_read_path`, `live`; stock answers `restarted`,
28
+ `checkpoint_trap`, `wedged`, `monitor_held_elsewhere`, `live`. Read the tool's own schema for the
29
+ exact contract on whichever backend is active — as of **07-16 (WR-07)** this instruction is
30
+ finally sound: `tools/list`'s advertised stock schema is the corrected stock manifest entry, not
31
+ the fork's synthetic literal it was silently overwritten by before. Stock `vice_diagnose` can also
32
+ answer a `diagnosis_unavailable` outcome when no verdict could be established at all — that is
33
+ **not** a sixth verdict; see the table below.
25
34
 
26
35
  ## The order
27
36
 
@@ -31,7 +40,13 @@ contract; this skill is the judgement around it.
31
40
  2. **Read the verdict, not the vibe.** Each verdict has exactly one correct response — the table
32
41
  below. A verdict is not a suggestion to try things.
33
42
  3. **`diagnose` leaves the machine paused** when it ran a bracket. Resuming is your own next call.
34
- Do not treat "still paused afterwards" as a symptom.
43
+ Do not treat "still paused afterwards" as a symptom. Every established verdict also reports
44
+ `machinePaused` plus `machinePausedSource` (07-15), so you can tell an actual observation from
45
+ an inference: `observed` means a wire `stopped`/`resumed`/`jam` event directly reported the
46
+ state; `structural` means it was inferred from the fact that every stock read halts the machine
47
+ (D-05), not from a specific event; `no_session` means no session was ever obtained (e.g. the
48
+ `monitor_held_elsewhere` verdict, or a `diagnosis_unavailable` acquisition failure) so no claim
49
+ about pause state is being made at all.
35
50
  4. **If the verdict is `wedged`, capture evidence before recovering.** `vice_recycle` requires a
36
51
  `reason`, and that string is written verbatim into a permanent, repo-tracked incident record
37
52
  under `.planning/incidents/` **before anything is killed**. That record is the evidence
@@ -43,11 +58,52 @@ contract; this skill is the judgement around it.
43
58
 
44
59
  | Verdict | What it means | Do |
45
60
  |---|---|---|
46
- | `live` | Cycles advanced | Resume and carry on. Suspect your own checkpoint conditions, not the emulator |
61
+ | `live` | Cycles advanced | Resume and carry on. Suspect your own checkpoint conditions, not the emulator — **unless `evidence.jamObserved` is true** (below) |
47
62
  | `checkpoint_trap` | The machine stopped **itself** at an armed checkpoint | `vice_checkpoint_delete` or `vice_checkpoint_toggle` it, or `vice_execution_step` past it, then re-run `diagnose`. **Recycling here destroys a healthy instance** |
48
63
  | `restarted` | The epoch changed — a crash-and-respawn already happened | The run is void. `c64-ram-capture` § Void a run gives the artifact procedure. Reboot from `vice_disk_attach` |
49
- | `stale_read_path` | Some reads move while others do not | Do not trust any measurement taken across the boundary. Treat as void and re-derive |
50
- | `wedged` | Two brackets, zero cycles, no epoch change | Last resort: `vice_recycle` with a real reason |
64
+ | `stale_read_path` **(fork only)** | Some reads move while others do not | Do not trust any measurement taken across the boundary. Treat as void and re-derive |
65
+ | `monitor_held_elsewhere` **(stock only)** | A different client already holds this instance's single binary-monitor slot | Release or identify the other holder. **Never a reason to recycle** — recycling here destroys an instance that is not even wedged |
66
+ | `wedged` | Two brackets, zero cycles, no epoch change | Last resort: `vice_recycle` with a real reason — **but check `evidence.jamObserved` first** (below) |
67
+ | `diagnosis_unavailable` **(stock only, non-verdict outcome — not one of the five)** | No verdict could be established at all; the message starts `vice_diagnose: diagnosis_unavailable (<reason>)`. The machine's state is **UNKNOWN**, not any of the five above | **Do not recycle on this answer alone.** Read the reason class in the message and act on it — see below |
68
+
69
+ ### `evidence.jamObserved` — read it before acting on `wedged` *or* `live` (stock only, 07-REVIEW WR-04)
70
+
71
+ Every stock `vice_diagnose` verdict carries `evidence.jamObserved` (always present, never omitted).
72
+ It is `true` once a `JAM` (0x61) event has arrived on this instance's wire — the 6510 executed an
73
+ illegal opcode and **the CPU is dead regardless of the verdict above**. The flag latches: it stays
74
+ true for the rest of the session.
75
+
76
+ It cuts across two verdicts in opposite directions, which is exactly why it is separate evidence
77
+ rather than a sixth verdict:
78
+
79
+ | `jamaction` | What `vice_diagnose` answers | Why | What to actually do |
80
+ |---|---|---|---|
81
+ | `-jamaction 2` (Monitor) | `wedged` | The machine stopped, so both brackets read zero advance | **`vice_machine_reset`, not `vice_recycle`.** Recycling destroys an instance a reset recovers — the same trap as `checkpoint_trap` |
82
+ | default (continue) | `live` | The emulator keeps burning cycles refetching the same opcode, so both brackets **advance** | **`vice_machine_reset`.** "Cycles advanced" is true and irrelevant: the machine will never execute another instruction |
83
+
84
+ **`jamObserved: true` is never a reason to recycle.** A jam is recovered by a reset; the instance
85
+ itself is healthy. Treat a `live` verdict with `jamObserved: true` as a false negative on liveness,
86
+ and a `wedged` verdict with it as a false positive on wedging.
87
+
88
+ ### `diagnosis_unavailable` — reason classes and response (07-15)
89
+
90
+ `diagnosis_unavailable` is what `vice_diagnose` answers, on the `isError:true` channel, when it
91
+ could not reach any of the five verdicts above — including a CR-01-class decode failure. It is
92
+ never added to the verdict enum and is never grounds to `vice_recycle` by itself: the message says
93
+ so explicitly. **Every** `isError:true` answer this tool can produce carries this prefix — there is
94
+ no unclassified no-verdict path left (07-REVIEW.md WR-02). Eight reason classes exist, each with
95
+ its own next move:
96
+
97
+ | Reason | What it means | Do |
98
+ |---|---|---|
99
+ | `connection_lost` | The socket died mid-session | Retry once. If it recurs, treat as a real transport problem, not a wedge |
100
+ | `request_timeout` | The wire went silent past the request bound | Retry once. If it recurs, fall to the manual cycle bracket below |
101
+ | `monitor_acquisition_timeout` | Another client holds the monitor and the wait bound expired | Wait for the current holder to release, then retry — this is the bounded sibling of `monitor_held_elsewhere`, not a wedge. **The abandoned acquisition is not cancelled** (07-REVIEW WR-19): a session may be established moments after this answer, so a later-appearing held session is not a ghost. Its real outcome is written to stderr |
102
+ | `session_refused` | The broker/lease itself refused the session | Read the raw detail in the message; this is a broker-level problem, not an emulator state |
103
+ | `protocol_decode_failure` | This build answered a frame the client cannot decode | Report it as a tool defect — check `docs/stock-vice-parity.md`'s `CPUHISTORY_GET` history for a known class of this — and fall back to the manual cycle bracket below |
104
+ | `evidence_gathering_failed` | A session was obtained but a read needed to build the verdict failed | `vice_execution_run` may be needed to unstick a stalled read path, then retry |
105
+ | `liveness_unmeasurable` | The liveness bracket could not be **measured at all** — no `CPUHISTORY_GET` (needs VICE ≥ 3.10) and no `LIN`/`CYC` enumerated. **The expected outcome on a stock 3.9-class build**, e.g. every current Debian/Ubuntu package | **Not a wedge and not a tool defect.** A bracket that cannot measure is not one that measured zero. Judge liveness from outside the monitor (screenshot, process state), or use the fork backend. Retrying will produce the same answer |
106
+ | `unknown` | None of the above classified the failure | Read the raw detail in the message; retry once before escalating |
51
107
 
52
108
  ## What is not recoverable
53
109
 
@@ -72,10 +128,37 @@ at a checkpoint — VICE's flag flips before the trap fires. Checkpoint bookkeep
72
128
  (`vice_checkpoint_add`/`list`/`delete`) also keeps returning healthy, self-consistent responses
73
129
  throughout a real wedge, so "the tools respond" proves nothing.
74
130
 
75
- **A `vice_run_until` on an address that is never reached looks exactly like a wedge.** Its `cycles`
76
- parameter is documented in its own schema as *"not yet implemented"*, so there is no working
77
- timeout to bound it. Before concluding anything, check whether you asked the machine to run to an
78
- address it cannot reach. **Confidence: MEDIUM** read off the tool schema, not reproduced.
131
+ **A `vice_run_until` on an address that is never reached looks exactly like a wedge on the fork,
132
+ still without a bound.** Its `cycles` parameter is *"not yet implemented"* on both backends, and the
133
+ fork has no timeout to bound the wait for an address either an unreachable address there is
134
+ unbounded and indistinguishable from a wedge. **On stock, passing `cycles` is now REFUSED rather
135
+ than ignored** (07-REVIEW WR-18) — including alongside `address`, where it used to be silently
136
+ dropped while the answer still reported `reached: true`. Unexpected argument names are refused by
137
+ name too, so a `timeoutMs`/`timeout_ms` typo can no longer run with the default bound in silence.
138
+ **On stock, this is now bounded (D-02):** pass `timeout_ms` (default 30000, clamped to a ceiling of
139
+ 600000); an unreachable address returns an explicit, bounded `timedOut: true` answer — with the
140
+ temporary checkpoint already cleaned up — rather than looking like a wedge. **Two further
141
+ behaviours (07-14, closing WR-01/WR-02):** every non-error answer, hit or timeout, carries
142
+ `machineHalted` plus a `machineHaltedNote` naming the resume call — the tool halts the machine on
143
+ every read and says so explicitly. **`machineHalted` is `true` on a hit and on a timeout whose
144
+ cleanup delete was answered (`cleanup: "deleted"` / `"already_gone"`); it is `false` when
145
+ `cleanup: "delete_failed"` or the socket is already gone** and the run-state projection does not
146
+ say `"stopped"`. **Do not read `machineHalted: false` as "still running"** — it means nothing here
147
+ could establish the state, `machineHaltedNote` says so, and the next call should be
148
+ `vice_diagnose`, not `vice_execution_run` (which may not reach the instance at all). And a timeout whose cleanup
149
+ delete lands on an already-gone race no longer asserts `reached: false` outright: it reads the
150
+ program counter and resolves the race (`raceResolved: "pc_at_address"` / `"pc_elsewhere"`), or, if
151
+ the PC read itself fails, omits `reached` entirely and reports `reachedUnknown: true`
152
+ (`raceResolved: "unresolved"`). **An absent `reached` is not "false"** — check `reachedUnknown`
153
+ before assuming a miss. The underlying judgement is unchanged and still the right first question on
154
+ either backend: before concluding anything, check whether you asked the machine to run to an
155
+ address it cannot reach. **Confidence: HIGH on stock for the reach/timeout mechanism** —
156
+ live-confirmed against genuine, unmodified `/usr/bin/x64sc` (VICE 3.9) and `/usr/local/bin/x64sc`
157
+ (VICE 3.10): a real KERNAL address ($EA31) reached within its timeout, an unreached one ($C000)
158
+ timing out with the checkpoint deleted (07-10's live pass). **MEDIUM for the WR-01/WR-02 honesty
159
+ fields above** — unit-proven (`stock-run-until.test.ts`, 21/21, 07-14) but not independently
160
+ re-exercised against a real emulator by this gap-closure batch. **MEDIUM on the fork** — read off
161
+ the tool schema, not reproduced.
79
162
 
80
163
  ## The manual fallback, when `vice_diagnose` cannot answer
81
164
 
@@ -84,7 +167,7 @@ exists, that is a **host action for a human** — say so and stop. Nothing conta
84
167
  the emulator by another route.
85
168
 
86
169
  When the broker is up but you want the raw measurement, the cycle bracket is the only trustworthy
87
- liveness test, and it is four calls:
170
+ liveness test. **On the fork**, it is four calls:
88
171
 
89
172
  1. `vice_cycles_stopwatch` `{action: "reset"}`
90
173
  2. `vice_execution_run`
@@ -96,6 +179,18 @@ liveness test, and it is four calls:
96
179
  thing — merely slow, a separate documented hazard measured at ~6,000/s when a loop polls without
97
180
  re-resuming. Read all state first, poll with `vice_ping`, resume exactly once at the end.
98
181
 
182
+ **On stock, there is no non-pausing call at all — any inbound byte halts the machine — so the
183
+ `vice_ping` ×3 poll measures nothing there and is fork-only.** The stock equivalent is the same
184
+ bracket shape with zero calls during the wait:
185
+
186
+ 1. `vice_cycles_stopwatch` `{action: "reset"}`
187
+ 2. `vice_execution_run`
188
+ 3. A real wall-clock wait, with **no calls at all** during it
189
+ 4. `vice_cycles_stopwatch` `{action: "read"}`
190
+
191
+ `vice_diagnose` already runs exactly this bracket internally on stock, so the manual fallback above
192
+ is only for when the broker itself is unreachable and `vice_diagnose` cannot be called at all.
193
+
99
194
  **Enumerate your own checkpoints before running any bracket.** `vice_checkpoint_list`, then
100
195
  resolve the live IRQ handler (`$0314/$0315`, or `$FFFE/$FFFF` when `$01` has the ROMs banked out).
101
196
  An armed stopping checkpoint at or inside the live IRQ path, with the PC pinned at or just past
@@ -114,7 +209,8 @@ session, the last three all on that call).
114
209
  | Checkpoint delete / reset / step can all fail to recover | One recorded incident, all four attempts in sequence | HIGH, single incident |
115
210
  | A checkpoint trap explains all three recorded "silent stalls" | Cross-read, 3/3 correlation, mechanism consistent with every symptom — **not reproduced** | MEDIUM |
116
211
  | `vice_diagnose`'s five-verdict path behaves as its schema says | Schema read, and cross-checked against the tracked implementation's own report builders. **Not exercised end to end** | MEDIUM |
117
- | `vice_run_until` has no working timeout | Its schema says `cycles` is "not yet implemented" | MEDIUM |
212
+ | `vice_run_until` has no working timeout **(fork only)** | Its schema says `cycles` is "not yet implemented"; the fork has no `timeout_ms` bound | MEDIUM |
213
+ | Stock's five-verdict path (`restarted`, `checkpoint_trap`, `wedged`, `monitor_held_elsewhere`, `live`) and its bounded `vice_run_until` | Unit-proven (40/40 `stock-diagnose.test.ts`, 21/21 `stock-run-until.test.ts`, 07-15/07-14). **Live-proven** against genuine `/usr/bin/x64sc` (VICE 3.9) and `/usr/local/bin/x64sc` (VICE 3.10) for `live` (07-10), `checkpoint_trap`, `wedged` (confirmed on both capability routes — `frame_position` on 3.9, `cpu_history` on 3.10) and `restarted` (07-17). `monitor_held_elsewhere`'s **socket-level** contention bound is live-proven (07-13, ~1501-1502ms against a 1500ms bound). **UPDATED 2026-08-18 (quick task 260818-obc, `stock-live-broker-monitor.test.ts`, command `VICE_LIVE_BROKER_BIN=/usr/bin/x64sc` (or `/usr/local/bin/x64sc`) `node --test stock-live-broker-monitor.test.ts`):** both remaining unit-only residuals are now ALSO live-proven, on both binaries, in one real run — the **broker-mediated** `monitor_held_elsewhere` verdict (a real second `claimMonitor()` refusal from a genuine host broker daemon, naming the other real grant's id, settling in 1ms against the 10000ms bound) and the **broker-supervised** (not test-performed) `restarted` respawn (the host broker's OWN crash supervision relaunched the killed instance; `vice_diagnose` answered `restarted` with `baselineEpoch:1`/`currentEpoch:2` at zero-to-minimal emulator cost). `vice_run_until`'s reach/timeout mechanism is live-proven against both binaries (07-10); its WR-01/WR-02 honesty fields (`machineHalted`, `raceResolved`, `reachedUnknown`) remain unit-proven only (07-14) — NOT re-exercised live by this task, no blanket claim made here | HIGH for the five verdicts (including both the broker-mediated `monitor_held_elsewhere` path and the broker-supervised `restarted` path, both now live-proven) and the run_until reach/timeout mechanism; MEDIUM for the run_until honesty fields only, which stay unit-only |
118
214
 
119
215
  Full provenance in `.planning/RE-FINDINGS.md`. **Log a new incident there at the moment you hit
120
216
  it**, graded with `Evidence:` and `Confidence:`; promote by re-logging, never by editing a grade.
@@ -146,4 +242,5 @@ others carry.
146
242
  | Zero cycles, nothing armed, epoch unchanged | A wedge. `vice_recycle` with a reason that names the evidence |
147
243
  | A run "survived a reset" | Distrust it. You cannot read the epoch to confirm — but an unintended respawn inside the bracket would have raised a drift error on the next forwarded call, so absence of that error is the only evidence available |
148
244
  | `vice_recycle` refused for a missing reason | It is required, by design — the reason *is* the incident record |
245
+ | `vice_diagnose` answers `monitor_held_elsewhere`, or a call hangs with no reply and no EOF (stock only) | Not a wedge. Find the other client holding this instance's single binary-monitor slot |
149
246
  </content>