@henols/c64-re-tools 0.2.0 → 0.2.2
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/README.md +1 -1
- package/THIRD-PARTY-NOTICES.md +26 -0
- package/bin/cli.mjs +18 -7
- package/package.json +6 -4
- package/skills/acme-build/SKILL.md +67 -47
- package/skills/acme-build/scripts/acme.mjs +2 -19
- package/skills/acme-build/template.a +17 -3
- package/skills/c64-memory-mapping/SKILL.md +426 -2
- package/skills/c64-program-recon/SKILL.md +479 -3
- package/skills/c64-program-recon/references/reconstruction.md +10 -4
- package/skills/c64-program-recon/references/tool-selection.md +2 -3
- package/skills/c64-program-recon/scripts/packer-finding.mjs +631 -0
- package/skills/c64-program-recon/templates/memory-map.template.md +86 -51
- package/skills/c64-provenance-diff/SKILL.md +4 -4
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +3 -0
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +14 -4
- package/skills/c64-ram-capture/RELEASES.json.example +17 -0
- package/skills/c64-ram-capture/SKILL.md +46 -2
- package/skills/c64-ram-capture/scripts/project-paths.mjs +1 -1
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +6 -0
- package/skills/routine-queue-walker/SKILL.md +273 -0
- package/skills/vice-wedge-triage/SKILL.md +16 -0
- package/skills/c64-provenance-diff/scripts/diff-images.test.mjs +0 -665
- package/skills/c64-ram-capture/scripts/d64-parse.test.mjs +0 -243
- package/skills/c64-ram-capture/scripts/dump-artifacts.test.mjs +0 -133
- package/skills/c64-ram-capture/scripts/test-corpus.mjs +0 -75
- package/skills/c64-ram-capture/scripts/watch-loads.test.mjs +0 -339
|
@@ -1,62 +1,97 @@
|
|
|
1
|
-
# Memory map
|
|
2
|
-
|
|
3
|
-
Capture: `<path to the 64K image>` · SHA-256 `<hash>`
|
|
4
|
-
`$01` = `<value>` · VIC bank `<n>` (`$DD00` = `<value>`) · video standard `<PAL/NTSC>`
|
|
5
|
-
Live vector pair: `<$0314/$0315 or $FFFE/$FFFF>` → `<handler>`
|
|
6
|
-
|
|
7
|
-
Every row carries a confidence. Do not promote a row by editing its grade — re-verify and restate
|
|
8
|
-
the evidence, so the record of when something stopped being a guess survives.
|
|
9
|
-
|
|
10
|
-
| Range | Contents | Confidence | Evidence |
|
|
11
|
-
|---|---|---|---|
|
|
12
|
-
| `$0000-$00FF` | Zero page — game variables | | |
|
|
13
|
-
| `$0100-$01FF` | Stack | CONFIRMED | hardware |
|
|
14
|
-
| `$0200-$03FF` | KERNAL work area / vectors | | |
|
|
15
|
-
| `$0400-$07E7` | Screen RAM (if VM resolves here) | | |
|
|
16
|
-
| `$0801-$` | | | |
|
|
17
|
-
| `$D800-$DBFF` | Colour RAM | CONFIRMED | hardware, not banked |
|
|
18
|
-
| `$E000-$FFFF` | RAM under KERNAL (HIRAM=0) or KERNAL ROM | | |
|
|
19
|
-
|
|
20
|
-
Confidence vocabulary — the project's HIGH / MEDIUM / LOW scale, applied to classification:
|
|
21
|
-
|
|
22
|
-
| Grade | Means |
|
|
23
|
-
|---|---|
|
|
24
|
-
| **confirmed code** | Executed during tracing, PC observed inside it |
|
|
25
|
-
| **probable code** | Reachable through a `JSR`/`JMP`/vector, not yet observed executing |
|
|
26
|
-
| **confirmed data** | Never hit as an instruction stream across full gameplay coverage |
|
|
27
|
-
| **probable data** | Indexed-load target, or matches a data shape (sprite blocks, PETSCII, address tables) |
|
|
28
|
-
| **unknown** | No reliable interpretation yet |
|
|
1
|
+
# Memory map generation
|
|
29
2
|
|
|
30
|
-
|
|
31
|
-
|
|
3
|
+
**The memory map is GENERATED, not hand-authored (D-24).** The store — labels, comments, block
|
|
4
|
+
types and scopes written through the `anno_*` tools described in `../SKILL.md` — is canonical. This
|
|
5
|
+
file used to be a fill-in-the-rows document; it is now the schema for the one input the generator
|
|
6
|
+
needs beyond the store itself, plus the confidence vocabulary that store comments carry.
|
|
32
7
|
|
|
33
|
-
|
|
8
|
+
Run the generator once findings are in the store:
|
|
34
9
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
10
|
+
```bash
|
|
11
|
+
npx -y @henols/vice-mcp anno render-memmap game.annostore --provenance sidecar.json
|
|
12
|
+
node <plugin-root>/src/mcp/vice/vice-proxy.ts anno render-memmap game.annostore --provenance sidecar.json
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Add `--check` to compare the rendered file on disk against a fresh render — it exits non-zero and
|
|
16
|
+
prints the first differing line. Drift is reported when, and only when, one of these changed: the
|
|
17
|
+
rendered file itself (a hand edit); a store row (a range, a label, a comment, or a comment's
|
|
18
|
+
confidence grade); the provenance sidecar's bytes; the location of the store or the sidecar
|
|
19
|
+
**relative to the workspace root**; or the renderer. **Relocating the checkout is not drift** — the
|
|
20
|
+
same tree at a different absolute path renders the same bytes, because the banner records
|
|
21
|
+
workspace-relative locations rather than absolute ones.
|
|
22
|
+
|
|
23
|
+
There is no way to "fix" drift by editing the rendered file directly: the fix is always to re-run
|
|
24
|
+
the generator (or, if the sidecar itself is stale, correct it and re-run). That remedy is safe to
|
|
25
|
+
follow on any machine — because the banner records workspace-relative locations, re-running in a
|
|
26
|
+
different checkout or in a worktree does not rewrite the file with that machine's absolute paths.
|
|
27
|
+
The generated file carries a banner naming the store, the sidecar and a content digest — do not
|
|
28
|
+
strip it.
|
|
29
|
+
|
|
30
|
+
**One-time drift after upgrading, 2026-08-30.** A memory map rendered *before* 2026-08-30 will
|
|
31
|
+
report `drifted` on its first `--check` after this change, exactly once: the banner's `store:` and
|
|
32
|
+
`sidecar:` lines moved from absolute to workspace-relative spellings. Re-run the generator once and
|
|
33
|
+
commit the new banner, after which the file is stable across machines and worktrees. This is a
|
|
34
|
+
one-time, self-clearing banner correction — not a bug, and not a migration.
|
|
42
35
|
|
|
43
|
-
|
|
36
|
+
## The provenance sidecar
|
|
44
37
|
|
|
45
|
-
|
|
38
|
+
Some facts belong to the **run** (which capture, which `$01`, which video standard) rather than to
|
|
39
|
+
any address, and the store has no address-keyed shape for them (D-27). They are supplied to the
|
|
40
|
+
renderer as a small JSON sidecar, hand-authored from `c64-ram-capture`'s and `derive.mjs`'s own
|
|
41
|
+
outputs and validated by the renderer — a missing or malformed key is a named error listing every
|
|
42
|
+
problem at once, never a `<placeholder>` silently rendered into a published document.
|
|
46
43
|
|
|
47
|
-
| |
|
|
44
|
+
| Key | Type | Where it comes from |
|
|
48
45
|
|---|---|---|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
|
|
|
46
|
+
| `capturePath` | string | The path to the captured 64K image, as given to `c64-ram-capture` |
|
|
47
|
+
| `captureSha256` | string, 64 hex chars | `compare.mjs digest`'s `sha256` — proves which image the map describes |
|
|
48
|
+
| `port01` | string | `derive.mjs vectors`' `$01` value |
|
|
49
|
+
| `dd00` | string | `derive.mjs vic`'s `--dd00` input, i.e. the observed `$DD00` |
|
|
50
|
+
| `vicBank` | string | `derive.mjs vic` — VIC bank derived from `$DD00` bits 0-1, inverted |
|
|
51
|
+
| `screenRam` | string | `derive.mjs vic` — screen RAM derived from `$D018` bits 4-7 |
|
|
52
|
+
| `charsetOrBitmap` | string | `derive.mjs vic` — charset/bitmap derived from `$D018` bits 1-3 (note the char-ROM shadow case) |
|
|
53
|
+
| `mode` | string | `derive.mjs vic` — graphics mode derived from `$D011` bits 5-6 and `$D016` bit 4 |
|
|
54
|
+
| `videoStandard` | `"PAL"` or `"NTSC"` | Known from the capture's origin/hardware context |
|
|
55
|
+
| `liveVectorPair` | string | `derive.mjs vectors` — the live vector pair (`$0314/$0315` or `$FFFE/$FFFF`) |
|
|
56
|
+
| `vectorHandler` | string | The address the live vector pair points at, confirmed live at a checkpoint |
|
|
57
|
+
| `rasterPositions` | string array, optional | One entry per observed `$D012` write on the way out of the live IRQ handler; `derive.mjs sprites` where sprite coordinates are relevant |
|
|
53
58
|
|
|
54
|
-
|
|
59
|
+
A fully-filled example, with plausible values in place of placeholders — copy this shape, never the
|
|
60
|
+
literal values:
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"capturePath": "captures/game.raw",
|
|
65
|
+
"captureSha256": "3f8a1c9e2b7d4a6f0c5e8b2d9a1f4c7e6b3d0a9c8f5e2b1d4a7c0f3e6b9d2a5c",
|
|
66
|
+
"port01": "$40",
|
|
67
|
+
"dd00": "$06",
|
|
68
|
+
"vicBank": "1 ($4000-$7FFF)",
|
|
69
|
+
"screenRam": "$0400",
|
|
70
|
+
"charsetOrBitmap": "$1000 (ROM shadow)",
|
|
71
|
+
"mode": "text, multicolor off",
|
|
72
|
+
"videoStandard": "PAL",
|
|
73
|
+
"liveVectorPair": "$FFFE/$FFFF",
|
|
74
|
+
"vectorHandler": "$1103",
|
|
75
|
+
"rasterPositions": ["$F8", "$00"]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
59
78
|
|
|
60
|
-
##
|
|
79
|
+
## Confidence vocabulary
|
|
80
|
+
|
|
81
|
+
Every comment written into the store through `anno_set_comment` that grades a finding leads with
|
|
82
|
+
one of these five bracket tokens (the parser in `anno-confidence.ts` throws on anything that is
|
|
83
|
+
close but not exact — a typo never silently degrades into an ungraded comment):
|
|
84
|
+
|
|
85
|
+
| Grade | Bracket token | Means |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| **confirmed code** | `[confirmed-code]` | Executed during tracing, PC observed inside it |
|
|
88
|
+
| **probable code** | `[probable-code]` | Reachable through a `JSR`/`JMP`/vector, not yet observed executing |
|
|
89
|
+
| **confirmed data** | `[confirmed-data]` | Never hit as an instruction stream across full gameplay coverage |
|
|
90
|
+
| **probable data** | `[probable-data]` | Indexed-load target, or matches a data shape (sprite blocks, PETSCII, address tables) |
|
|
91
|
+
| **unknown** | `[unknown]` | No reliable interpretation yet |
|
|
92
|
+
|
|
93
|
+
Do not force an unknown range through a disassembler and record the output as code. A linear
|
|
94
|
+
decode of data is silently wrong and contaminates everything downstream.
|
|
61
95
|
|
|
62
|
-
-
|
|
96
|
+
**Do not promote a row by editing its grade.** Re-verify and restate the evidence with a fresh
|
|
97
|
+
`anno_set_comment` call, so the record of when something stopped being a guess survives.
|
|
@@ -15,7 +15,7 @@ either proves its own precondition or refuses to emit.
|
|
|
15
15
|
`ledger` will not write a verdict the earlier stages did not earn.
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
D
|
|
18
|
+
D=src/skills/c64-provenance-diff/scripts/diff-images.mjs # from the repo root
|
|
19
19
|
|
|
20
20
|
node $D anchor-search # 1. prove the per-release offset [WRITES]
|
|
21
21
|
node $D diff # 2. N-way byte diff at that offset
|
|
@@ -25,7 +25,7 @@ node $D ledger # 4. regenerate recovery/PROVENANC
|
|
|
25
25
|
node $D diff --json # machine-readable, with per-range reasons
|
|
26
26
|
node $D diff --gap-tolerance 16 # coalescing width (default shown)
|
|
27
27
|
node $D anchor-search --reference <id> # pick the reference release
|
|
28
|
-
node
|
|
28
|
+
node src/skills/c64-provenance-diff/scripts/releases.mjs list # the release ids in play
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
Pure Node over committed files — the `.bin` dumps, their `.map.json` manifests, and
|
|
@@ -231,7 +231,7 @@ addresses.
|
|
|
231
231
|
| A verified 64K image, or proving two captures equivalent | `c64-ram-capture` |
|
|
232
232
|
| Which address to read next, and what the answer rules out | `c64-program-recon` |
|
|
233
233
|
| What a specific address or bit means | `c64-memory-mapping` — `node … lookup '$D018'` |
|
|
234
|
-
| Assembling
|
|
234
|
+
| Assembling | `acme-build` |
|
|
235
235
|
| **Whether a byte is original, cracker-changed, or unknown** | here |
|
|
236
236
|
|
|
237
237
|
Findings that make RE faster go in `.planning/RE-FINDINGS.md` **at the moment you
|
|
@@ -254,4 +254,4 @@ through a GSD command (`/gsd-quick`).
|
|
|
254
254
|
| A range's `kind` looks wrong past its start | You resolved `kind` from `start`. Use `splitRangeByManifestKind`; coalescing does not respect kind boundaries. |
|
|
255
255
|
| A loader range disagrees with `NOTES.md` | `RELEASES.json`'s `loader_ranges` wins — it is earned from disassembly. Prose is how `$08F5` got misclassified. |
|
|
256
256
|
| Title-screen text shows up as cracktro | You used a bare printable-run scan. The vocabulary scan exists because `$4771-$4779` is the game's own text. |
|
|
257
|
-
| `unknown release "x" -- known releases: …` | `node
|
|
257
|
+
| `unknown release "x" -- known releases: …` | `node src/skills/c64-provenance-diff/scripts/releases.mjs list` for the valid ids. |
|
|
@@ -693,6 +693,9 @@ export function renderLedger({ generatedRanges, gapTolerance, prose }) {
|
|
|
693
693
|
throw new Error(`renderLedger: refusing to emit -- generated tier stops at ${hex4(expected - 1)}, does not reach $FFFF`);
|
|
694
694
|
}
|
|
695
695
|
|
|
696
|
+
// NOTE (plans 16-01, 16-10): the embedded invocation path below is deliberately the
|
|
697
|
+
// CONSUMER's installed location (`.claude/skills/...`), not this repository's
|
|
698
|
+
// source-tree location (`src/skills/...`) -- pinned by diff-images.test.mjs and skill-consumer-paths.test.ts (16-REVIEW.md CR-01).
|
|
696
699
|
let generated = `<!-- GENERATED, DO NOT HAND-EDIT. Regenerate with: node .claude/skills/c64-provenance-diff/scripts/diff-images.mjs ledger --gap-tolerance ${gapTolerance} -->\n\n`;
|
|
697
700
|
generated += `| Start | End | Kind | Verdict | Confidence | Agreeing releases | Evidence / Reason |\n`;
|
|
698
701
|
generated += `|---|---|---|---|---|---|---|\n`;
|
|
@@ -25,17 +25,27 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
25
25
|
const REPO_ROOT = projectRoot();
|
|
26
26
|
const RECOVERY_DIR = dataRoot();
|
|
27
27
|
const DISKS_DIR = disksRoot();
|
|
28
|
+
|
|
29
|
+
const die = (m) => { console.error(`error: ${m}`); process.exit(1); };
|
|
30
|
+
|
|
28
31
|
// The parameterisation gate must cover EVERY module of the recovery pipeline, not
|
|
29
32
|
// just the ones sitting next to this file. When the six modules moved out of
|
|
30
33
|
// `tools/` into the two skills that use them (2026-08-04), a `HERE`-only scan
|
|
31
34
|
// silently stopped covering `d64-parse.mjs` and `dump-artifacts.mjs` -- a static
|
|
32
35
|
// guard that keeps passing while checking less is worse than one that fails.
|
|
36
|
+
// 2026-08-22 (plan 16-01): the second entry used to be built project-root-relative,
|
|
37
|
+
// naming the skills tree's pre-relocation auto-discovery location by hand -- which
|
|
38
|
+
// stopped resolving the moment the skills tree moved to its current source-tree
|
|
39
|
+
// location. Rebuilt `HERE`-relative instead -- correct in both this dev checkout
|
|
40
|
+
// and a consumer's install, since a sibling skill's `scripts/` directory is always
|
|
41
|
+
// one level up and back down from this file's own location either way.
|
|
33
42
|
const SCAN_DIRS = [
|
|
34
|
-
HERE, //
|
|
35
|
-
resolve(
|
|
43
|
+
HERE, // src/skills/c64-provenance-diff/scripts
|
|
44
|
+
resolve(HERE, "..", "..", "c64-ram-capture", "scripts"),
|
|
36
45
|
];
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
for (const dir of SCAN_DIRS) {
|
|
47
|
+
if (!existsSync(dir)) die(`SCAN_DIRS entry does not exist: ${dir} -- a guard that keeps passing while checking less is worse than one that fails`);
|
|
48
|
+
}
|
|
39
49
|
|
|
40
50
|
function sha256File(path) {
|
|
41
51
|
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0",
|
|
3
|
+
"schema_notes": "Adding a release is one releases[] entry plus one invocation of tools/recover.mjs -- see c64-ram-capture/SKILL.md '## Release registry shape'.",
|
|
4
|
+
"releases": [
|
|
5
|
+
{
|
|
6
|
+
"id": "example-release",
|
|
7
|
+
"canonical": true,
|
|
8
|
+
"disk_image": "disks/example-release.d64",
|
|
9
|
+
"dumps": [
|
|
10
|
+
{
|
|
11
|
+
"label": "example-label",
|
|
12
|
+
"range_manifest": "recovery/example-release/dumps/example-release-example-label.map.json"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -14,7 +14,7 @@ failure and it is invisible in a hex dump. Two committed modules do that byte wo
|
|
|
14
14
|
and name the offending address when it is wrong.
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
S
|
|
17
|
+
S=src/skills/c64-ram-capture/scripts # from the repo root
|
|
18
18
|
P=$S/d64-parse.mjs A=$S/dump-artifacts.mjs
|
|
19
19
|
C=$S/compare.mjs L=$S/releases.mjs
|
|
20
20
|
|
|
@@ -33,6 +33,12 @@ node $C floor a.bin b.bin c.bin # drift floor across a capture
|
|
|
33
33
|
All three modules read only committed files and the JSON **you** wrote from your
|
|
34
34
|
own `mcp__plugin_c64-re-tools_vice__*` calls. They contact nothing.
|
|
35
35
|
|
|
36
|
+
**Prerequisite: a resolvable project root.** `scripts/project-paths.mjs` uses
|
|
37
|
+
`C64RE_PROJECT_ROOT` when it is set, and otherwise walks up from the toolkit's
|
|
38
|
+
own location for the nearest ancestor directory containing a `.git` entry. A
|
|
39
|
+
scratch project has neither by default, so `git init` it first or set the
|
|
40
|
+
variable — the thrown error names both when this fails.
|
|
41
|
+
|
|
36
42
|
## The order
|
|
37
43
|
|
|
38
44
|
| # | Phase | Settles |
|
|
@@ -78,6 +84,9 @@ directory chain that leaves the image or loops, reported instead of hanging.
|
|
|
78
84
|
3. `mcp__plugin_c64-re-tools_vice__vice_execution_run`.
|
|
79
85
|
4. `mcp__plugin_c64-re-tools_vice__vice_registers_get` and confirm the program counter has moved.
|
|
80
86
|
|
|
87
|
+
The broker sets the drive type at launch for every stock instance, so no drive setup
|
|
88
|
+
is needed before attaching.
|
|
89
|
+
|
|
81
90
|
If the program counter has not moved, type `LOAD"*",8,1` with
|
|
82
91
|
`mcp__plugin_c64-re-tools_vice__vice_keyboard_type`, run it, then type `RUN` and run it.
|
|
83
92
|
|
|
@@ -262,6 +271,17 @@ Capture the power-on image as the very first action against a fresh machine, the
|
|
|
262
271
|
idle-capture twice more and run `floor` over the set. State the result as a
|
|
263
272
|
floor, not a complete set — more captures can only widen it.
|
|
264
273
|
|
|
274
|
+
## Feeding the memory map's provenance sidecar
|
|
275
|
+
|
|
276
|
+
`c64-program-recon`'s generated memory map (`vice-mcp anno render-memmap`) takes a small provenance
|
|
277
|
+
sidecar as input, and this skill supplies one of its fields: `scripts/compare.mjs digest`'s `sha256`
|
|
278
|
+
and `size` become the sidecar's `captureSha256`, proving which image the rendered map describes. The
|
|
279
|
+
sidecar's other run-scoped keys (`port01`, `dd00`, `vicBank`, `screenRam`, `charsetOrBitmap`, `mode`,
|
|
280
|
+
`liveVectorPair`, `vectorHandler`, `rasterPositions`) come from `c64-program-recon`'s own
|
|
281
|
+
`derive.mjs` — **this skill does not emit the sidecar itself.** The sidecar is hand-authored from
|
|
282
|
+
those two skills' outputs and validated by the renderer, which throws naming every missing or
|
|
283
|
+
malformed key at once rather than rendering a document that silently carries a placeholder.
|
|
284
|
+
|
|
265
285
|
## Which skill does what
|
|
266
286
|
|
|
267
287
|
This one owns the image and its identity. It does not restate what the others carry.
|
|
@@ -271,11 +291,33 @@ This one owns the image and its identity. It does not restate what the others ca
|
|
|
271
291
|
| Which address to read next, and what the answer rules out | `c64-program-recon` |
|
|
272
292
|
| Every way a live read gives a wrong answer | `c64-program-recon` — `references/observation-hazards.md`. **Read before driving.** |
|
|
273
293
|
| What a specific address or bit means | `c64-memory-mapping` — `node … lookup '$D018'` |
|
|
274
|
-
| Assembling
|
|
294
|
+
| Assembling | `acme-build` |
|
|
275
295
|
| Whether a byte is original or cracker-changed, and what `bucketed` means | `c64-provenance-diff` |
|
|
276
296
|
| Whether the emulator is wedged, and whether it is safe to recycle | `vice-wedge-triage` |
|
|
277
297
|
| **A verified 64K image, or proving two captures equivalent** | here |
|
|
278
298
|
|
|
299
|
+
## Release registry shape
|
|
300
|
+
|
|
301
|
+
`scripts/releases.mjs` is the only module that reads a release id out of the
|
|
302
|
+
registry — every other module takes the id as an argument. Its shape:
|
|
303
|
+
|
|
304
|
+
| Field | Level | Required | For |
|
|
305
|
+
|---|---|---|---|
|
|
306
|
+
| `schema_version` | top-level | — | The registry format version. |
|
|
307
|
+
| `schema_notes` | top-level | — | Free-text stating the registry's N-readiness claim (`node $L schema-notes`). |
|
|
308
|
+
| `releases` | top-level | yes | The array of release entries below. |
|
|
309
|
+
| `id` | per-release | yes | The `--release` argument every other script takes. |
|
|
310
|
+
| `canonical` | per-release | — | A boolean on one entry, not "the canonical image" — there are N releases. |
|
|
311
|
+
| `disk_image` | per-release | yes | Project-relative path to the release's `.d64`. |
|
|
312
|
+
| `dumps` | per-release | **yes, as an array** | Per-capture records written by `write-set`. **Must be an array, never omitted** — `releases.mjs`'s `list` command reads `r.dumps.length` with no guard (`releases.mjs:98`), so a missing `dumps` throws `TypeError: Cannot read properties of undefined` instead of listing anything. An empty array (`[]`) is fine; an absent key is not. `releases.mjs` itself imposes no per-entry shape; `scripts/watch-loads.mjs` (a different reader) looks up an entry by `label` and reads its `range_manifest`, which the example below follows. |
|
|
313
|
+
|
|
314
|
+
The registry lives at `<project root>/recovery/RELEASES.json` by default —
|
|
315
|
+
override the whole path with `C64RE_REGISTRY`, or just the containing
|
|
316
|
+
directory with `C64RE_DATA_DIR`.
|
|
317
|
+
|
|
318
|
+
`src/skills/c64-ram-capture/RELEASES.json.example` is a copyable starting
|
|
319
|
+
point with every field above populated with placeholder values.
|
|
320
|
+
|
|
279
321
|
## References
|
|
280
322
|
|
|
281
323
|
What this skill ships, and the committed modules it leans on. No `references/`
|
|
@@ -287,6 +329,7 @@ split: the workflow fits in one file, which is the right call when it does.
|
|
|
287
329
|
| `templates/capture-record.template.md` | The per-capture record: identity, machine state read in the same paused window, the void checklist, and the per-pairing comparison table. |
|
|
288
330
|
| `scripts/d64-parse.mjs` | `.d64` directory, BAM, and `--json` fakery detection. Fixture-tested against both real images by `scripts/d64-parse.test.mjs`. |
|
|
289
331
|
| `scripts/dump-artifacts.mjs` | `assemble` / `chip-state` / `manifest` / `write-set` — the guarded byte work, and the source of every `assembleImage:` message in the table below. |
|
|
332
|
+
| `RELEASES.json.example` | A copyable release-registry shape — see `## Release registry shape` above. |
|
|
290
333
|
|
|
291
334
|
Findings that make RE faster go in `.planning/RE-FINDINGS.md` **at the moment you
|
|
292
335
|
find them**, graded with `Evidence:` and `Confidence:`. Promote by re-logging with
|
|
@@ -309,3 +352,4 @@ through a GSD command (`/gsd-quick`).
|
|
|
309
352
|
| `--limit 0` printed nothing | Fixed 2026-08-04 — it now means unlimited. Re-pull the script if you see the old behaviour. |
|
|
310
353
|
| An epoch-drift error appeared mid-capture | The machine restarted under you. Void the run; do not salvage the artifacts. The next call succeeding does not undo it. |
|
|
311
354
|
| The emulator looks dead | `vice-wedge-triage` — and enumerate your own armed checkpoints before concluding anything. |
|
|
355
|
+
| `` `project-paths: could not locate the project root -- no `.git` found above ...` `` | No `.git` ancestor and no `C64RE_PROJECT_ROOT`. `git init` the project, or set the variable to its root. |
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
// 1. The project root is found by walking UP for a `.git` marker. Counting
|
|
8
8
|
// hops from `import.meta.url` breaks the moment the toolkit is installed
|
|
9
|
-
// somewhere other than
|
|
9
|
+
// somewhere other than `src/skills/<skill>/scripts/`, and it breaks
|
|
10
10
|
// silently -- paths resolve to a plausible wrong place rather than erroring.
|
|
11
11
|
// 2. Every data location is overridable by environment variable, so a project
|
|
12
12
|
// that does not use this repo's `recovery/` + `disks/` layout can point the
|
|
@@ -445,6 +445,12 @@ function renderReleaseSection(id, log) {
|
|
|
445
445
|
* string templating over already-fetched data -- nothing here reads a file
|
|
446
446
|
* or contacts anything; the CLI `render` verb below is what reads the
|
|
447
447
|
* hit-log files and writes the result.
|
|
448
|
+
*
|
|
449
|
+
* The two script paths named in the output below are deliberately the
|
|
450
|
+
* consumer's installed location, not this repository's source tree -- this
|
|
451
|
+
* string is written into a `recovery/LOADING.md` a consumer keeps. Plan
|
|
452
|
+
* 16-04's blanket sweep rewrote them once (16-REVIEW.md CR-01 class); pinned
|
|
453
|
+
* by the test below plus the class-level registry in skill-consumer-paths.test.ts.
|
|
448
454
|
*/
|
|
449
455
|
export function renderLoading(entries) {
|
|
450
456
|
let out = "# `recovery/LOADING.md` -- the on-demand-load detection record\n\n";
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: routine-queue-walker
|
|
3
|
+
description: Drive an existing C64 annotation store's backlog of undocumented routines and auto-named symbols to closure — build the candidate queue from labels and comments, work it one entry at a time against explicit addresses, rebuild it after every pass, and report every leftover. Use when asked to annotate every remaining routine in a project, document all undocumented subroutines left in an annotation project, rename the leftover auto-generated labels, clear a backlog of unnamed symbols, drive an annotation pass to completion, or list what is still unannotated after a pass.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Walking the routine and symbol queue to closure
|
|
7
|
+
|
|
8
|
+
**Do not start annotating whatever is in front of you.** The expensive failure
|
|
9
|
+
here is not slow work — it is a pass that *looks* finished while a hundred
|
|
10
|
+
`p_XXXX` labels are still nameless and nobody wrote down which ones. Build the
|
|
11
|
+
queue first, from data, then walk it to the end.
|
|
12
|
+
|
|
13
|
+
This playbook assumes block classification has already happened and an
|
|
14
|
+
annotation store already exists. If you do not yet know what the program is —
|
|
15
|
+
where it starts, which vector is live, which regions are code — stop and run
|
|
16
|
+
`c64-program-recon` first. That skill answers *what is this program*; this one
|
|
17
|
+
answers *what is still undocumented in it, and how do I finish*.
|
|
18
|
+
|
|
19
|
+
## The one rule that makes this different from upstream's version
|
|
20
|
+
|
|
21
|
+
**Work the queue one entry at a time.** Not as a throughput compromise — as an
|
|
22
|
+
accurate model of the store underneath. One `.annostore` is one writer: every
|
|
23
|
+
mutating call opens it, commits and closes inside the call, and every one of
|
|
24
|
+
them accepts an optional `base_revision` compare-and-swap that REFUSES a write
|
|
25
|
+
computed against a revision the store has already moved past. Fanning several
|
|
26
|
+
writers at one store therefore buys **zero** extra throughput and costs
|
|
27
|
+
correctness: the losers come back as named stale-revision refusals you then
|
|
28
|
+
have to re-derive and replay. Reading fan-out — several agents *thinking* over
|
|
29
|
+
already-fetched answers — is fine, and its value is reasoning bandwidth, never
|
|
30
|
+
I/O.
|
|
31
|
+
|
|
32
|
+
**Every call names its own store.** There is no ambient "current store" on this
|
|
33
|
+
surface: pass `store` (a `.annostore` path) on every call, and pass `image` as
|
|
34
|
+
well on every call that derives its answer from the program's bytes rather than
|
|
35
|
+
from the annotations — `anno_get_binary_info`, `anno_read_region`,
|
|
36
|
+
`anno_disassemble`, `anno_get_cross_references`, `anno_search` and
|
|
37
|
+
`anno_get_address_details`. The store holds annotations and never bytes, so an
|
|
38
|
+
omitted image would read as a plausible success against whatever was recorded
|
|
39
|
+
last.
|
|
40
|
+
|
|
41
|
+
## Phase 0 — context, and the packed-binary gate
|
|
42
|
+
|
|
43
|
+
1. Call `anno_get_binary_info`. Keep `origin`, `size`, `system`, `filename`,
|
|
44
|
+
`description` and `may_contain_undocumented_opcodes` — every later step
|
|
45
|
+
quotes them.
|
|
46
|
+
2. Read the returned `entropy` against the threshold of **7.5** carried in that
|
|
47
|
+
tool's own description. At or above it, the bytes are very likely packed.
|
|
48
|
+
3. If the binary looks packed, **stop and say so.** Do not annotate a packed
|
|
49
|
+
image: you would be documenting a decruncher, and every label you write is
|
|
50
|
+
thrown away the moment the real image is recovered. This project's route to
|
|
51
|
+
an unpacked image is `c64-ram-capture` — run the program in the emulator and
|
|
52
|
+
capture RAM at a checkpoint past the decrunch — plus the packer-identity
|
|
53
|
+
finding in `c64-program-recon`, which names the packer when an oracle can.
|
|
54
|
+
Come back with the captured image and start again at Phase 0.
|
|
55
|
+
|
|
56
|
+
Upstream's in-place `unpack_binary` step is deliberately not carried: it is
|
|
57
|
+
destructive (it clears the comments, labels and blocks already in the store)
|
|
58
|
+
and this project has a non-destructive route to the same answer.
|
|
59
|
+
|
|
60
|
+
## Phase 1 — make sure blocks are classified
|
|
61
|
+
|
|
62
|
+
Region classification is a prerequisite for everything below: a routine
|
|
63
|
+
candidate is only meaningful once the bytes around it are known to be code.
|
|
64
|
+
|
|
65
|
+
1. Follow `src/skills/c64-memory-mapping/SKILL.md` for the classification pass
|
|
66
|
+
and for what each region type means.
|
|
67
|
+
2. Do that pass yourself, in one sitting. It is a single long walk over the
|
|
68
|
+
whole binary, not a queue of independent items.
|
|
69
|
+
|
|
70
|
+
## Phase 2 — the routine queue
|
|
71
|
+
|
|
72
|
+
### 2.1 Build the candidate list
|
|
73
|
+
|
|
74
|
+
A routine counts as **already documented** when its entry address carries a
|
|
75
|
+
line comment. That is the only test; do not guess from the label name.
|
|
76
|
+
|
|
77
|
+
1. Call `anno_get_symbols` for all labels — user, system and external, with
|
|
78
|
+
an explicit `max_results` above the program's label count (`max_results` is
|
|
79
|
+
REQUIRED on this surface and has no default, so a truncated answer is always
|
|
80
|
+
a ceiling you chose). Keep the answer; Phase 3 reuses it.
|
|
81
|
+
2. Call `anno_get_comments`, again with an explicit `max_results`. Keep that
|
|
82
|
+
too — the true match count rides beside the list, so truncation is a fact
|
|
83
|
+
you are told rather than one you infer.
|
|
84
|
+
3. Keep a label as a routine candidate when any of these holds:
|
|
85
|
+
- its name starts with `s_` (an auto-generated subroutine label);
|
|
86
|
+
- it sits in a code region and is the target of at least one `JSR`
|
|
87
|
+
cross-reference (`anno_get_cross_references`);
|
|
88
|
+
- it is a `p_XXXX` label sitting **inside a code region**. These come from
|
|
89
|
+
split lo/hi immediate loads and from address tables, and they are almost
|
|
90
|
+
always chained raster-IRQ handlers, hardware- or shadow-vector handlers,
|
|
91
|
+
or jump-table and callback targets. Treat every one of them as a
|
|
92
|
+
candidate rather than pattern-matching specific vector addresses;
|
|
93
|
+
- it is the label named exactly `start`.
|
|
94
|
+
4. Drop every candidate that already carries a line comment.
|
|
95
|
+
5. What is left is the routine queue.
|
|
96
|
+
6. **Order it with `start` first** when `start` is in it. The entry point sets
|
|
97
|
+
the context every other routine is read against.
|
|
98
|
+
|
|
99
|
+
### 2.2 Walk it
|
|
100
|
+
|
|
101
|
+
- **Always work from an explicit address** — `$XXXX`, or the decimal
|
|
102
|
+
equivalent. Never from "wherever we are"; there is no editor cursor in this
|
|
103
|
+
project's route, and upstream's own text forbids relying on one anyway. Read
|
|
104
|
+
the routine's bytes with `anno_read_region` over the explicit range.
|
|
105
|
+
- Take **one** entry at a time, to completion, before starting the next.
|
|
106
|
+
- For each entry, do the full job: rename the label (`anno_set_label_name`),
|
|
107
|
+
add a header line comment describing what the routine does and what it
|
|
108
|
+
leaves in the registers and memory, add side comments on the instructions
|
|
109
|
+
that carry the meaning (`anno_set_comment`), and record anything you are
|
|
110
|
+
unsure about rather than smoothing it over.
|
|
111
|
+
- Record per entry: the address, the old label, the new label, a one-line
|
|
112
|
+
summary, and any uncertainty. That record is the report in Phase 4.
|
|
113
|
+
|
|
114
|
+
### 2.3 Refresh point
|
|
115
|
+
|
|
116
|
+
When the queue is empty, read the store's revision with `anno_save_project`.
|
|
117
|
+
**It performs no write, and it exists to say so:** every mutating verb on this
|
|
118
|
+
surface has already committed and fsynced its own write by the time it
|
|
119
|
+
returned, so there is nothing for an explicit save to flush. Record the
|
|
120
|
+
revision — it is the checkpoint this pass is measured from, and the
|
|
121
|
+
`base_revision` a later compare-and-swap write would quote. Everything after
|
|
122
|
+
this point re-reads the store, because Phase 2 has just changed the label names
|
|
123
|
+
Phase 3 filters on.
|
|
124
|
+
|
|
125
|
+
## Phase 3 — the symbol queue
|
|
126
|
+
|
|
127
|
+
### 3.1 Build the candidate list
|
|
128
|
+
|
|
129
|
+
A symbol counts as **already documented** when it has a name a human chose, or
|
|
130
|
+
when it is a well-known system address (hardware register, KERNAL entry point,
|
|
131
|
+
OS variable).
|
|
132
|
+
|
|
133
|
+
1. Call `anno_get_symbols` **again** — Phase 2 renamed things.
|
|
134
|
+
2. Keep every label whose name still matches an auto-generated pattern:
|
|
135
|
+
`zpp_XX`, `zpf_XX`, `zpa_XX` in the zero page; `p_XXXX`, `f_XXXX`, `a_XXXX`
|
|
136
|
+
and `e_XXXX` outside it.
|
|
137
|
+
3. Exclude: `s_XXXX` (Phase 2 handled those), `b_XXXX` (branch targets, not
|
|
138
|
+
data symbols), and any `p_XXXX` inside a code region (also Phase 2's).
|
|
139
|
+
4. What is left is the symbol queue.
|
|
140
|
+
|
|
141
|
+
### 3.2 Walk it
|
|
142
|
+
|
|
143
|
+
Same discipline as Phase 2: explicit address, one entry at a time, to
|
|
144
|
+
completion. For each symbol, use `anno_get_cross_references` to find who
|
|
145
|
+
touches it — a symbol's meaning is what its callers do with it — then rename it
|
|
146
|
+
and comment it. Classify it plainly: flag, counter, pointer, state variable,
|
|
147
|
+
buffer, table.
|
|
148
|
+
|
|
149
|
+
**No premature halting.** The symbol queue is routinely far larger than the
|
|
150
|
+
routine queue — fifty, a hundred entries is normal. Do not truncate it, do not
|
|
151
|
+
skip "secondary" symbols, and do not stop early because it is long. Feeding the
|
|
152
|
+
whole queue through is the job. Stopping early and labelling the remainder
|
|
153
|
+
"skipped for review" is a failed pass, not a completed one — unless the
|
|
154
|
+
remainder is reported explicitly, in full, under Phase 4's leftovers table.
|
|
155
|
+
|
|
156
|
+
For naming conventions and for what any given hardware or KERNAL address
|
|
157
|
+
means, follow `src/skills/c64-memory-mapping/SKILL.md` rather than guessing.
|
|
158
|
+
|
|
159
|
+
### 3.3 Refresh point
|
|
160
|
+
|
|
161
|
+
Read the revision again with `anno_save_project` and record it. No write is
|
|
162
|
+
performed; the writes already landed.
|
|
163
|
+
|
|
164
|
+
## Phase 4 — save and report
|
|
165
|
+
|
|
166
|
+
1. Read the revision one last time with `anno_save_project` and quote it in
|
|
167
|
+
the report, so the pass is attributable to an exact store state.
|
|
168
|
+
2. Write the report. Four sections, all of them required:
|
|
169
|
+
|
|
170
|
+
**Regions.** How many regions are classified, grouped by type, plus anything
|
|
171
|
+
notable — text at a fixed address, a jump table, a sprite block.
|
|
172
|
+
|
|
173
|
+
**Routines.**
|
|
174
|
+
|
|
175
|
+
| Address | Old label | New label | What it does |
|
|
176
|
+
| ------- | --------- | --------- | ------------ |
|
|
177
|
+
| `$C000` | `s_C000` | `init_screen` | Clears screen RAM, sets the border colour |
|
|
178
|
+
|
|
179
|
+
**Symbols.**
|
|
180
|
+
|
|
181
|
+
| Address | Old label | New label | Classification |
|
|
182
|
+
| ------- | --------- | --------- | -------------- |
|
|
183
|
+
| `$02` | `zpp_02` | `ptr_screen` | Zero-page indirect pointer |
|
|
184
|
+
|
|
185
|
+
**Leftovers — uncertain, skipped, or still unannotated.** This section is not
|
|
186
|
+
optional and it is not allowed to be empty when the queues were not emptied.
|
|
187
|
+
List every routine and every symbol that was left undone, with its address and
|
|
188
|
+
the reason. Never report "no uncertain areas" or "nothing left" while a single
|
|
189
|
+
`f_XXXX` or `a_XXXX` label is still auto-named or a queued routine is still
|
|
190
|
+
uncommented — those must be listed by name for a human to pick up.
|
|
191
|
+
|
|
192
|
+
## Phase 5 — measure the pass instead of asserting it finished
|
|
193
|
+
|
|
194
|
+
A report that says "all routines documented" is a claim about the report, not
|
|
195
|
+
about the program. Measure it. From the repository root:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
node src/mcp/vice/vice-proxy.ts anno coverage game.prg --store game.annostore
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**`--store` is REQUIRED and is a second path, not a spelling of the first.**
|
|
202
|
+
`<program>` supplies the payload bytes and the load origin; `--store` names the
|
|
203
|
+
annotation store holding the labels, comments and typed ranges. The store holds
|
|
204
|
+
annotations and never bytes, so the verb refuses to guess either path from the
|
|
205
|
+
other.
|
|
206
|
+
|
|
207
|
+
**Dated note, 2026-08-30 — the positional is a program IMAGE, and the command
|
|
208
|
+
above is now correct against the shipped verb.** `<program>` is a `.prg` (a
|
|
209
|
+
2-byte little-endian load address followed by the payload) or an
|
|
210
|
+
**exactly-65536-byte** flat capture with a `.raw` or `.bin` extension — the two
|
|
211
|
+
forms every other verb and tool on this surface already reads, and the two
|
|
212
|
+
`c64-ram-capture` produces. The intermediate project-file format this verb
|
|
213
|
+
previously required has **no producer left in this repo**; it is still accepted
|
|
214
|
+
so an existing project file keeps working, but nothing here writes one, so do
|
|
215
|
+
not go looking for a step that produces it.
|
|
216
|
+
|
|
217
|
+
Dispatch is by **file extension first, length second**. A short flat capture is
|
|
218
|
+
therefore refused by name — `a flat 64K capture must be exactly 65536 bytes` —
|
|
219
|
+
rather than misread as a `.prg` whose first two payload bytes become the load
|
|
220
|
+
address. If you get that refusal, the capture is truncated; re-capture it, do
|
|
221
|
+
not rename it.
|
|
222
|
+
|
|
223
|
+
Add `--out coverage.json` to keep the machine-readable report, `--force` to
|
|
224
|
+
overwrite one, and `--sample N` to widen the reproducibility sample. The verb
|
|
225
|
+
reads the same store every call in this playbook writes to, and exits **0 even
|
|
226
|
+
when the numbers are bad** — a low measurement is a result, not a failure.
|
|
227
|
+
Non-zero means a caller error, an image it could not read, or a store it could
|
|
228
|
+
not read at all.
|
|
229
|
+
|
|
230
|
+
**Run it three times:** once before Phase 2, so the pass has a starting point
|
|
231
|
+
to be compared against; once at Phase 2.3's refresh point; and once at the end,
|
|
232
|
+
after Phase 4's final save. The last run is what goes in the report.
|
|
233
|
+
|
|
234
|
+
**Read the three numbers against each other. Never quote one of them alone.**
|
|
235
|
+
There is deliberately no single "percent documented" figure, because one
|
|
236
|
+
combined number lets a weak measure hide behind a strong one and makes the
|
|
237
|
+
claim unfalsifiable:
|
|
238
|
+
|
|
239
|
+
- **A high user fraction beside a large unreached count means the wrong things
|
|
240
|
+
were named.** Every label got a human name, but most of the image was never
|
|
241
|
+
reached by the descent walk from any seed — the queue was worked over the
|
|
242
|
+
easily-visible part of the program and the rest was never entered. Go back to
|
|
243
|
+
Phase 0 and find more entry points (chained IRQ vectors, dispatch tables),
|
|
244
|
+
not more labels.
|
|
245
|
+
- **A large divergence means the store and the bytes disagree about what is
|
|
246
|
+
code.** Bytes the census reached as instructions that the store does not call
|
|
247
|
+
`Code` are places where Phase 1's classification is behind the actual control
|
|
248
|
+
flow. The reverse direction (the store calls it `Code`, the census never
|
|
249
|
+
reached it) is ordinary on an image with unreachable filler — read it, do not
|
|
250
|
+
chase it.
|
|
251
|
+
- **A low distinct-comment ratio means the comments are filler.** Fifty
|
|
252
|
+
addresses carrying the same sentence counts once, not fifty times. That is
|
|
253
|
+
the number that catches a pass which renamed everything and explained
|
|
254
|
+
nothing.
|
|
255
|
+
|
|
256
|
+
Anything the per-measure findings list names belongs in Phase 4's leftovers
|
|
257
|
+
table, by address. A finding is a named defect in one named measure — it is
|
|
258
|
+
never a rating, and there is no number to report as "the coverage".
|
|
259
|
+
|
|
260
|
+
## When something fails
|
|
261
|
+
|
|
262
|
+
- A failed call is not a reason to drop a queue entry. Log the address, the
|
|
263
|
+
call and the error, put the entry back on the queue, and carry on with the
|
|
264
|
+
next one. Report every one of those in the leftovers table.
|
|
265
|
+
- A refused write is not silent and must not be treated as one. A
|
|
266
|
+
stale-revision refusal (a `base_revision` that the store has moved past), an
|
|
267
|
+
illegal label name, or a scope that overlaps an existing one all come back
|
|
268
|
+
REFUSED and named, with nothing written. Re-read, re-derive and replay that
|
|
269
|
+
one entry; never widen the range or drop the `base_revision` to make the
|
|
270
|
+
refusal go away.
|
|
271
|
+
- Never invent an answer to make a queue entry go away. An honest "this looks
|
|
272
|
+
like a table, callers unclear" in the leftovers table is worth more than a
|
|
273
|
+
confident wrong label that the next reader has to un-learn.
|