@henols/c64-re-tools 0.1.11 → 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 +47 -4
- package/package.json +2 -2
- package/skills/c64-program-recon/SKILL.md +1 -1
- package/skills/c64-program-recon/references/control-flow.md +9 -0
- package/skills/c64-program-recon/references/graphics.md +6 -0
- package/skills/c64-program-recon/references/observation-hazards.md +23 -0
- package/skills/c64-program-recon/references/sound-and-input.md +10 -2
- package/skills/c64-program-recon/references/tool-selection.md +14 -9
- package/skills/c64-ram-capture/SKILL.md +5 -0
- package/skills/vice-wedge-triage/SKILL.md +113 -16
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
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
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.
|
|
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` / `
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
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`
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
**
|
|
9
|
-
one of them.** Work the order below. Do not start with
|
|
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
|
|
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`
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
| `
|
|
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
|
|
76
|
-
|
|
77
|
-
timeout to bound
|
|
78
|
-
|
|
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
|
|
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>
|