@henols/c64-re-tools 1.0.0 → 2.0.0-rc.10

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.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -37
  3. package/distribution/package.ts +21 -0
  4. package/distribution/plugin.ts +122 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +76 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +110 -0
  8. package/skills/c64-assembler/scripts/compare.ts +84 -0
  9. package/skills/c64-basic/SKILL.md +66 -0
  10. package/skills/c64-basic/scripts/basic.ts +46 -0
  11. package/skills/c64-disk/SKILL.md +93 -0
  12. package/skills/c64-disk/scripts/containers.ts +124 -0
  13. package/skills/c64-disk/scripts/disk.ts +120 -0
  14. package/skills/c64-emulator/SKILL.md +134 -0
  15. package/skills/c64-knowledge/SKILL.md +166 -0
  16. package/skills/c64-knowledge/scripts/knowledge.ts +492 -0
  17. package/skills/c64-memory-map/SKILL.md +79 -0
  18. package/skills/c64-memory-map/scripts/memmap.ts +92 -0
  19. package/skills/c64-provenance/SKILL.md +110 -0
  20. package/skills/c64-provenance/scripts/compare.ts +152 -0
  21. package/skills/c64-provenance/scripts/provenance.ts +334 -0
  22. package/skills/c64-provenance/scripts/runtime.ts +191 -0
  23. package/skills/c64-provenance/scripts/separate.ts +140 -0
  24. package/skills/c64-reverse-engineering/SKILL.md +144 -0
  25. package/skills/c64-source/SKILL.md +126 -0
  26. package/skills/c64-source/scripts/generate.ts +490 -0
  27. package/skills/c64-source/scripts/loader.ts +571 -0
  28. package/skills/c64-source/scripts/opcodes.ts +61 -0
  29. package/skills/c64-source/scripts/source.ts +595 -0
  30. package/skills/c64-static-analysis/SKILL.md +119 -0
  31. package/skills/c64-static-analysis/scripts/analyze.ts +275 -0
  32. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  33. package/skills/c64-static-analysis/scripts/findings.ts +75 -0
  34. package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
  35. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  36. package/skills/c64-testing/SKILL.md +103 -0
  37. package/skills/c64-testing/references/scenario-format.md +116 -0
  38. package/skills/c64-testing/scripts/checklist.ts +51 -0
  39. package/skills/c64-testing/scripts/run.ts +189 -0
  40. package/skills/c64-testing/scripts/scenario.ts +493 -0
  41. package/skills/c64-testing/scripts/test.ts +65 -0
  42. package/skills/c64-unpacker/SKILL.md +135 -0
  43. package/skills/c64-unpacker/scripts/evidence.ts +207 -0
  44. package/skills/c64-unpacker/scripts/loads.ts +209 -0
  45. package/skills/c64-unpacker/scripts/machine.ts +80 -0
  46. package/skills/c64-unpacker/scripts/stages.ts +130 -0
  47. package/skills/c64-unpacker/scripts/unpack.ts +683 -0
  48. package/src/c64.ts +411 -0
  49. package/src/cli/ap-sdk.ts +44 -0
  50. package/src/cli/cleanup.ts +194 -0
  51. package/src/cli/main.ts +125 -0
  52. package/src/cli/status.ts +61 -0
  53. package/src/host/main.ts +134 -0
  54. package/src/host/server.ts +392 -0
  55. package/src/host/tools/c1541.ts +284 -0
  56. package/src/host/tools/index.ts +35 -0
  57. package/src/host/tools/petcat.ts +77 -0
  58. package/src/host/tools/status.ts +47 -0
  59. package/src/host/tools/version.ts +41 -0
  60. package/src/host/vice/adapter.ts +804 -0
  61. package/src/host/vice/binary-monitor.ts +389 -0
  62. package/src/host/vice/chips.ts +118 -0
  63. package/src/host/vice/connect.ts +34 -0
  64. package/src/host/vice/process.ts +413 -0
  65. package/src/host/vice/screen.ts +184 -0
  66. package/src/host/vice/session.ts +1426 -0
  67. package/src/host/vice/text-monitor.ts +223 -0
  68. package/src/host-client/autostart.ts +55 -0
  69. package/src/host-client/connect.ts +241 -0
  70. package/src/host-client/coverage.ts +63 -0
  71. package/src/host-client/steps.ts +170 -0
  72. package/src/host-client/tools.ts +89 -0
  73. package/src/host-client/transfer.ts +126 -0
  74. package/src/host-client/vice-session.ts +316 -0
  75. package/src/knowledge/database.ts +132 -0
  76. package/src/knowledge/history.ts +163 -0
  77. package/src/knowledge/import.ts +391 -0
  78. package/src/knowledge/read.ts +342 -0
  79. package/src/knowledge/schema.ts +229 -0
  80. package/src/knowledge/write.ts +663 -0
  81. package/src/mcp/main.ts +134 -0
  82. package/src/mcp/output.ts +32 -0
  83. package/src/mcp/server.ts +316 -0
  84. package/src/mcp/tools/debug.ts +327 -0
  85. package/src/mcp/tools/execution.ts +117 -0
  86. package/src/mcp/tools/input.ts +72 -0
  87. package/src/mcp/tools/machine.ts +100 -0
  88. package/src/mcp/tools/media.ts +129 -0
  89. package/src/mcp/tools/memory.ts +250 -0
  90. package/src/mcp/tools/video.ts +271 -0
  91. package/src/native/acme.ts +116 -0
  92. package/src/native/discover.ts +179 -0
  93. package/src/native/dxa.ts +166 -0
  94. package/src/native/ghidra/analyze.ts +86 -0
  95. package/src/native/ghidra/index.ts +173 -0
  96. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  97. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  98. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  99. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  100. package/src/native/ghidra/scripts/C64Export.java +274 -0
  101. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  102. package/src/native/local.ts +34 -0
  103. package/src/native/processes.ts +350 -0
  104. package/src/native/run.ts +172 -0
  105. package/src/native/staging.ts +83 -0
  106. package/src/native/status.ts +113 -0
  107. package/src/native/types.ts +257 -0
  108. package/src/native/watchdog.ts +78 -0
  109. package/src/package.ts +21 -0
  110. package/src/platform.ts +441 -0
  111. package/src/project.ts +39 -0
  112. package/src/protocol/framing.ts +132 -0
  113. package/src/protocol/messages.ts +259 -0
  114. package/src/protocol/tools.ts +266 -0
  115. package/src/protocol/vice.ts +1348 -0
  116. package/src/script.ts +60 -0
  117. package/src/time.ts +15 -0
  118. package/src/trace.ts +194 -0
  119. package/THIRD-PARTY-NOTICES.md +0 -26
  120. package/bin/cli.mjs +0 -283
  121. package/skills/acme-build/SKILL.md +0 -260
  122. package/skills/acme-build/scripts/acme.mjs +0 -341
  123. package/skills/acme-build/template.a +0 -53
  124. package/skills/c64-disk-access/SKILL.md +0 -159
  125. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  126. package/skills/c64-memory-mapping/SKILL.md +0 -631
  127. package/skills/c64-memory-mapping/memmap.json +0 -8800
  128. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  129. package/skills/c64-petcat/SKILL.md +0 -87
  130. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  131. package/skills/c64-program-recon/SKILL.md +0 -706
  132. package/skills/c64-program-recon/references/control-flow.md +0 -180
  133. package/skills/c64-program-recon/references/graphics.md +0 -79
  134. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  135. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  136. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  137. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  138. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  139. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  140. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  141. package/skills/c64-provenance-diff/SKILL.md +0 -292
  142. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  143. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  144. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  145. package/skills/c64-ram-capture/SKILL.md +0 -474
  146. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  147. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  148. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  149. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  150. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  151. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  152. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  153. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  154. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  155. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  156. package/skills/c64-ram-capture/transients/README.md +0 -137
  157. package/skills/routine-queue-walker/SKILL.md +0 -394
  158. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  159. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -1,260 +0,0 @@
1
- ---
2
- name: acme-build
3
- description: Assemble Commodore 64 6510 assembly with the ACME cross assembler. Use when asked to assemble, build, compile or link .a/.asm 6502/6510 source, produce a C64 .prg, scaffold a new C64 program, or list which symbols an assembled build actually used from its symbol file.
4
- ---
5
-
6
- # Assembling C64 source with ACME
7
-
8
- Source in, `.prg` out. Everything goes through one script:
9
-
10
- ```bash
11
- A=src/skills/acme-build/scripts/acme.mjs # from the repo root
12
-
13
- node $A new game.asm # scaffold a C64 program
14
- node $A build game.asm # assemble -> .prg .sym .vs .rep
15
- node $A sym game.asm # the symbols the program uses
16
- ```
17
-
18
- The script wraps `acme` and nothing else — **assembling only**. Running
19
- the result on a C64 belongs to the emulator skills (`acme.mjs:3-4` says so, and the
20
- absent `run` verb is not an omission). It contacts nothing.
21
-
22
- Options: `-o FILE` `--out-dir DIR` `-f FORMAT` `--setpc ADDR` `-DSYM=VAL`
23
- `-I DIR` `--no-report` `--json`.
24
-
25
- The seam resolves `-I DIR` **workspace-relative** to the project root the host
26
- broker starts with — the same resolution `source`/`--out-dir` already
27
- go through — before it ever reaches the assembler. The seam refuses an absolute
28
- or escaping `-I` directory rather than passing it to ACME. This is a
29
- documented contract change from earlier releases, when an absolute include
30
- reached the assembler unchecked.
31
-
32
- ## Build
33
-
34
- ```bash
35
- node $A build game.asm
36
- ```
37
- ```
38
- Saving 53 (0x35) bytes (0x801 - 0x836 exclusive).
39
- built game.prg (55 bytes) load $0801-$0836 53 bytes of code
40
- symbols: game.sym (4 used / 121 total)
41
- debug labels: game.vs (4 addresses)
42
- ```
43
-
44
- The indented line is ACME's own `-v1` note, passed straight through. Three side
45
- files land next to the `.prg`, and `--no-report` drops the `.rep`:
46
-
47
- | file | contents |
48
- |---|---|
49
- | `.prg` | the program, with its load address |
50
- | `.sym` | every symbol, with the used ones marked |
51
- | `.vs` | address labels, ready for a debugger or monitor |
52
- | `.rep` | each source line with the address and bytes it produced |
53
-
54
- Only address-typed *and* referenced symbols survive into the `.vs` — raw
55
- `--vicelabels` output lists constants too, and a debugger reading
56
- `viccolor_WHITE = $1` would relabel the 6510 processor port at `$0001`
57
- (`curateLabels`, `scripts/acme.mjs`) — hence 4 addresses against 121 total
58
- symbols above. Load it with `mcp__plugin_c64-re-tools_vice__vice_symbols_load` (format `vice`), this
59
- project's only route to the emulator (`.claude/CLAUDE.md` § Version Compatibility
60
- / § Emulator Access).
61
-
62
- Re-run `build` until it exits 0 — a clean exit means every symbol resolved.
63
-
64
- Add `--json` to act on diagnostics programmatically:
65
-
66
- ```bash
67
- node $A build game.asm --json
68
- ```
69
- ```json
70
- { "ok": false,
71
- "diags": [ { "file": "game.asm", "line": 26, "severity": "error",
72
- "zone": "Zone <untitled>",
73
- "message": "Number does not fit in 8 bits." } ] }
74
- ```
75
-
76
- Use the `.rep` listing to map source to memory:
77
-
78
- ```
79
- 16 0801 0b080a00 !word .eol, 10 ; link to next line, line number
80
- 26 080d a900 lda #viccolor_BLACK
81
- 27 080f 8d21d0 sta vic_cbg ; $d021 background
82
- 29 0814 8d20d0 sta vic_cborder ; $d020 border
83
- ```
84
-
85
- Build variants with `-D`, giving each its own `-o` so the symbol files stay
86
- separate:
87
-
88
- ```bash
89
- node $A build game.asm -DBORDER=2 -o v2.prg # -> v2.prg v2.sym v2.vs
90
- node $A build game.asm -DBORDER=5 -o v5.prg # -> v5.prg v5.sym v5.vs
91
- ```
92
-
93
- Inspect the result with `od`:
94
-
95
- ```bash
96
- od -An -tx1 game.prg | head -2
97
- ```
98
-
99
- The first two bytes are the little-endian load address (`01 08` = `$0801`). Code
100
- follows.
101
-
102
- ## Writing source
103
-
104
- Start from the scaffold — it carries a BASIC stub that computes its `SYS` target
105
- rather than hard-coding it, so the entry point stays correct as the program grows:
106
-
107
- ```bash
108
- node $A new game.asm
109
- ```
110
-
111
- Use the C64 symbol library instead of writing addresses by hand:
112
-
113
- | `!source <...>` | gives you | example |
114
- |---|---|---|
115
- | `<cbm/c64/vic.a>` | `vic_*` registers, `viccolor_*` constants | `vic_cborder` = `$d020` |
116
- | `<cbm/c64/kernal.a>` | `k_*` KERNAL entry points `$ff81`–`$fff5` | `k_chrout` = `$ffd2` |
117
- | `<cbm/c64/cia1.a>` / `<cbm/c64/cia2.a>` | `cia1_*` / `cia2_*` | keyboard, joystick, timers |
118
- | `<cbm/c64/sid.a>` | `sid_*` | sound |
119
-
120
- KERNAL routines use the **`k_`** prefix — `k_chrout`, `k_getin`, `k_setnam`,
121
- `k_plot`. Several have aliases (`k_bsout` and `k_basout` are also `$ffd2`).
122
- Run `node $A sym game.asm` to see what a build resolved:
123
-
124
- ```
125
- addr $80d entry
126
- addr $ffd2 k_chrout
127
- addr $d021 vic_cbg
128
- addr $d020 vic_cborder
129
- ```
130
-
131
- Let `-o` name the output and leave `!to` out of the source, so the filename you
132
- pass is the filename you get.
133
-
134
- The 6510's illegal opcodes are always available: `lax dcp sax slo rla sre rra
135
- isc anc alr arr sbx las tas sha shx shy jam`. Verified — `lax $fb / dcp $fc /
136
- sax $fd / slo $02 / anc #$0f / sbx #$10` assembles to
137
- `a7 fb c7 fc 87 fd 07 02 0b 0f cb 10`. Keep `!cpu 6510` at the top of any source
138
- you also assemble by hand, so these stay recognised as mnemonics.
139
-
140
- ## Disassembly
141
-
142
- This skill does not disassemble. The route that does is `anno export-asm`, and
143
- it lives in the `anno` CLI rather than here.
144
-
145
- **Dated withdrawal 2026-08-29, dated return 2026-08-31 — this document keeps
146
- both halves, because the withdrawal records why the route has its current shape.**
147
- Whole-program static disassembly was WITHDRAWN on 2026-08-29 rather than left standing on an unverified reassembly claim: the removed route
148
- settled correctness with a transcript parser, and the recorded false pass that
149
- discipline exists against read `ACME not found in PATH (skipped)` / `All
150
- roundtrip verifications passed.` / `EXIT=0` — exit zero, an aggregate line
151
- reading as a full pass, and the one assembler this project cares about never
152
- having run. On **2026-08-31 the route returned**, rebuilt over the annotation
153
- store as `anno export-asm`. It is not a rename of the removed route: its
154
- correctness comes from **assembling the output with a real ACME and diffing
155
- the bytes against the input** — never from an exit code, and never from a string
156
- match on the exporter's own output.
157
-
158
- **The live invocation:**
159
-
160
- ```bash
161
- npx -y @henols/vice-mcp anno export-asm game.prg --store game.annostore --out game-src
162
- node <plugin-root>/src/mcp/vice/vice-proxy.ts anno export-asm game.prg --store game.annostore
163
- ```
164
-
165
- `<image>` and `--store` are **two separate arguments, and neither one derives from
166
- the other** — the image supplies the bytes, the store supplies the names, ranges,
167
- typed regions and comments, and naming one does not name the other. `--out` names
168
- a **directory**: the verb writes the whole export into it, defaulting to the image's
169
- basename stem beside the **store**, not beside the image, because the export is a
170
- generated view of the annotations. The directory holds a root file that sources
171
- the rest, one file per annotation scope, and an `unscoped.a` for anything inside
172
- no scope. The verb refuses a non-empty destination rather than overwriting it, unless you
173
- pass `--force`.
174
-
175
- **The verb writes source text and runs no assembler.** Its own second output line
176
- says so, in as many words:
177
-
178
- ```
179
- export-asm: this file has NOT been assembled -- this command writes source text and runs no assembler.
180
- ```
181
-
182
- The real-ACME byte-diff oracle is **test-only** — it lives in
183
- `src/mcp/vice/acme-verify.ts`, `acme-verify.test.ts` exercises it (hard-failed
184
- in CI with `VICE_REQUIRE_ACME=1`), and it stays deliberately absent from the published
185
- package, so nothing on the runtime path can reach it. A clean `export-asm` run is
186
- therefore evidence that the verb wrote source and nothing more. It is not an
187
- assembler verdict. If you need to know the emitted source reassembles, assemble
188
- it yourself — that is what this skill's own build route is for.
189
-
190
- **Reading one range at a time is still the right move for a single routine.**
191
- `anno_read_region` and `anno_disassemble` render an explicit inclusive range out
192
- of the image on demand, capped at 4096 bytes per call and REFUSED by name above
193
- the cap rather than truncated. `c64-program-recon` documents that route and this
194
- history together. It is not restated there.
195
-
196
- ## Setup
197
-
198
- `acme` on `$PATH` — **on the host**, never inside a container — is the **only**
199
- requirement. The scaffold that `new` writes assembles against a bare install
200
- with no standard hardware-register library — that's deliberate: neither a
201
- plain `~/.local/bin/acme` build nor the Debian trixie `apt` candidate ships
202
- one, so a scaffold that depended on it would fail to assemble on a fresh
203
- install.
204
-
205
- **Route:** `scripts/acme.mjs` never spawns `acme` itself
206
- and never probes a container PATH for it — a container has no such PATH to
207
- probe (the project owner's rule of 2026-08-28). The script reaches the
208
- assembler only through the host-tool execution seam
209
- (`src/mcp/vice/host-tool.mts`'s `acme.build` allowlist entry), which runs on
210
- the HOST and probes the library there — the same handful of conventional
211
- install locations `acme.mts`'s own `findAcmeLib()` names, none of them
212
- documented a second time here. `$ACME` still matters for **your own** sources
213
- that use angle-bracket includes (see "Writing source" above), not for the
214
- scaffold — but set it in the environment the **host** broker process sees, not
215
- this script's own environment, since the probe now runs host-side inside the
216
- seam's executor. If you don't have that library, the scaffold doesn't need it.
217
-
218
- Re-checked against ACME release 0.97 "Zem" (31 Jan 2021). CI now assembles
219
- the shipped scaffold on every build with `$ACME` cleared (the "Assemble the
220
- acme-build scaffold (library-free)" step in `.github/workflows/ci.yml`), so
221
- this claim is re-checkable rather than a one-machine observation.
222
-
223
- Copy `acme.mjs` into any project's `.claude/skills/acme-build/scripts/`, and
224
- `template.a` into `.claude/skills/acme-build/`, to use this elsewhere (the path Claude Code auto-discovers and `installSkills()` deploys to).
225
-
226
- ## Which skill does what
227
-
228
- This one turns source into bytes. It does not restate what the others carry.
229
-
230
- | Need | Go to |
231
- |---|---|
232
- | Where to start on an unknown program, and which address to read next | `c64-program-recon` |
233
- | What a specific address or bit means, or annotating a listing | `c64-memory-mapping` — `node … lookup '$D018'` |
234
- | A verified 64K image, or comparing two captures | `c64-ram-capture` |
235
- | Static disassembly of a `.prg` or flat image | **`anno export-asm`** — withdrawn 2026-08-29, returned 2026-08-31 behind a real-ACME byte-diff oracle that is test-only, so the verb writes source and assembles nothing (see Disassembly above) |
236
- | **Source in, `.prg` out** | here |
237
-
238
- ## References
239
-
240
- | File | Covers |
241
- |---|---|
242
- | `scripts/acme.mjs` | The driver. Its comments are the contract for every flag above |
243
- | `template.a` | The scaffold `new` writes: BASIC stub with a computed `SYS`, five local hardware constants (no library needed), no `!to` |
244
-
245
- Record findings that make RE faster in your own project notes **at the moment you
246
- find them**, graded with `Evidence:` and `Confidence:`. Promote a finding by
247
- re-logging it with the new evidence, never by editing an old grade in place — the
248
- grade is only worth anything if it says what you actually knew when you wrote it.
249
-
250
- ## Troubleshooting
251
-
252
- | Symptom | Correct |
253
- |---|---|
254
- | `install the ACME cross assembler and put acme on PATH` | Install ACME. |
255
- | `for <...> includes, set $ACME to …` | `export ACME=<dir holding cbm/c64/vic.a>`. |
256
- | `Value not defined (kernal_chrout)` | Use the `k_` prefix — `k_chrout`. `node $A sym` lists what resolved. |
257
- | `Label name not in leftmost column` + `Syntax error` on a mnemonic | Add `!cpu 6510`. |
258
- | `Output file already chosen` | Remove `!to` from the source and keep `-o`. |
259
- | `Number does not fit in 8 bits` | Pass a value 0–255, or drop the `#` if you meant an address. |
260
- | `This tool cannot read binary files. The file appears to be a binary .a file.` | The file is fine. ACME source is plain text. The agent's Read tool refuses the `.a` extension regardless of content. Edit needs a prior successful Read. Scaffold and write source as `.asm` instead — the driver accepts `.a`/`.asm`/`.s`, all three assemble byte-identically (verified both directions in this container, 2026-08-04). To read an existing `.a` file, use `sed -n '1,60p' file.a`. |
@@ -1,341 +0,0 @@
1
- #!/usr/bin/env node
2
- // ACME -> C64 assembler driver. Target is fixed: C64, 6510 CPU, cbm output.
3
- // Scope is assembling only: source in, .prg + symbol files out. Running the
4
- // result on a C64 belongs to the emulator skill.
5
- //
6
- // The assembler is reached ONLY through the host-tool execution seam -- the
7
- // project owner's rule of 2026-08-28 is that this script runs container-side,
8
- // `acme` lives host-side, and there is no container PATH to find it on. This file used to spawn `acme` directly (a synchronous
9
- // `spawnSync("acme", args, { env })`) and probed FOUR fixed HOST paths
10
- // (`/usr/local/share/acme`, `/usr/share/acme`, `/usr/lib/acme`, `~/.acme`)
11
- // for its `<...>`-include library -- both are exactly what the owner's rule
12
- // says cannot work from inside a container. The spawn and the library probe
13
- // both moved to `src/mcp/vice/host-tool.mts`'s `acme.build` allowlist entry;
14
- // this file now only constructs a TYPED request and reads the produced files
15
- // back off the shared workspace tree.
16
- //
17
- // WHAT NOT TO DO: never reintroduce a local child-process call to the
18
- // assembler as a fallback when the seam is unreachable -- a fallback that
19
- // works on the developer's own host and silently fails inside a container is
20
- // the exact failure this seam exists to remove. A seam refusal is reported
21
- // and the build fails; it is never retried by spawning `acme` here.
22
- import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from "node:fs";
23
- import { dirname, join, basename, relative, isAbsolute, resolve, sep } from "node:path";
24
- import { fileURLToPath } from "node:url";
25
- import { spawn } from "node:child_process";
26
-
27
- import { resolveMcpModule, refusalMessage } from "../../c64-ram-capture/scripts/mcp-module.mjs";
28
-
29
- const SELF = fileURLToPath(import.meta.url);
30
- const HERE = dirname(SELF);
31
-
32
- /** The MCP-side module this script reaches -- never imported statically
33
- * (cross-package: this file ships in `@henols/c64-re-tools`, the seam client
34
- * ships in `@henols/vice-mcp`), only located via the ladder and invoked with
35
- * `process.execPath`, the interpreter already running this script, on an
36
- * in-tree module -- not an external host binary. */
37
- const HOST_TOOL_CLIENT_FILE = "host-tool-client.ts";
38
-
39
- /**
40
- * Invokes the host-tool execution seam for `tool`/`args`, rooted at
41
- * `repoRoot` for THIS invocation's workspace-relative path resolution.
42
- * Never rejects: a resolution failure, a spawn failure, or unparseable
43
- * output all resolve to `{ ok: false, message }` -- the same shape a tool's
44
- * own refusal uses, so a caller never needs a try/catch.
45
- */
46
- function invokeSeam(tool, args, repoRoot) {
47
- return new Promise((resolvePromise) => {
48
- const resolved = resolveMcpModule(HOST_TOOL_CLIENT_FILE);
49
- if (!resolved.ok) {
50
- resolvePromise({ ok: false, message: refusalMessage(HOST_TOOL_CLIENT_FILE, resolved.rungs) });
51
- return;
52
- }
53
-
54
- const cliArgs = [resolved.path, "run", "--tool", tool, "--args", JSON.stringify(args), "--repo-root", repoRoot];
55
- let child;
56
- try {
57
- child = spawn(process.execPath, cliArgs, { stdio: ["ignore", "pipe", "pipe"] });
58
- } catch (e) {
59
- resolvePromise({ ok: false, message: e instanceof Error ? e.message : String(e) });
60
- return;
61
- }
62
-
63
- let stdout = "";
64
- let stderr = "";
65
- child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
66
- child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
67
- child.on("error", (err) => resolvePromise({ ok: false, message: err.message }));
68
- child.on("close", () => {
69
- const lines = stdout.split("\n").filter((line) => line.trim() !== "");
70
- const last = lines[lines.length - 1];
71
- if (last === undefined) {
72
- resolvePromise({ ok: false, message: `host-tool-client.ts produced no output${stderr ? ` (stderr: ${stderr})` : ""}` });
73
- return;
74
- }
75
- try {
76
- resolvePromise(JSON.parse(last));
77
- } catch {
78
- resolvePromise({ ok: false, message: `host-tool-client.ts produced non-JSON output: ${last}` });
79
- }
80
- });
81
- });
82
- }
83
-
84
- /** The smallest common ancestor directory of two absolute paths -- computed,
85
- * never a fixed guess, so the request's `--repo-root` for THIS invocation is
86
- * always exactly big enough to contain both the source and the output
87
- * directory, and no bigger. This is what keeps a build entirely outside this
88
- * project's own tree (this repo's `skill-acme-build-cli.test.ts`'s own
89
- * scratch directories under the SYSTEM temp dir, and CI's own
90
- * `RUNNER_TEMP`-rooted scaffold check) working after the migration: the
91
- * seam's `resolveWorkspacePath()` refuses any path outside its given root, so
92
- * the root for one invocation is chosen to be wherever that invocation's own
93
- * files actually live, never a client-supplied absolute path sent as-is. */
94
- function commonAncestorDir(a, b) {
95
- const partsA = resolve(a).split(sep);
96
- const partsB = resolve(b).split(sep);
97
- const common = [];
98
- for (let i = 0; i < Math.min(partsA.length, partsB.length); i++) {
99
- if (partsA[i] === partsB[i]) common.push(partsA[i]);
100
- else break;
101
- }
102
- const joined = common.join(sep);
103
- return joined === "" ? sep : joined;
104
- }
105
-
106
- /** `path.relative()`, except the "same directory" case yields `"."` rather
107
- * than `""` -- the seam's `resolveWorkspacePath()` refuses an empty string,
108
- * but accepts `"."` as a no-op relative reference to its own root. */
109
- function toRel(root, abs) {
110
- const r = relative(root, abs);
111
- return r === "" ? "." : r;
112
- }
113
-
114
- // How to refer to this script in hints, from wherever we were run.
115
- function selfPath() {
116
- const r = relative(process.cwd(), SELF);
117
- return !r || r.startsWith("..") || isAbsolute(r) ? SELF : r;
118
- }
119
-
120
- const die = (m) => { console.error(`error: ${m}`); process.exit(1); };
121
-
122
- // ------------------------------------------------------------------- build
123
-
124
- // ACME's --msvc format: file(line) : Error (Zone <z>): message.
125
- const MSVC = /^(.*?)\((\d+)\)\s*:\s*(Error|Warning|Serious error)\s*(?:\(([^)]*)\))?\s*:\s*(.*)$/;
126
-
127
- function parseDiagnostics(text) {
128
- const out = [];
129
- for (const line of text.split("\n")) {
130
- const m = line.match(MSVC);
131
- if (m) {
132
- out.push({
133
- file: m[1], line: Number(m[2]),
134
- severity: m[3].toLowerCase().replace(" ", "_"),
135
- zone: m[4] || null, message: m[5].trim(),
136
- });
137
- } else if (line.trim()) {
138
- out.push({ file: null, line: null, severity: "note", zone: null, message: line.trim() });
139
- }
140
- }
141
- return out;
142
- }
143
-
144
- // The symbol list marks address-typed symbols with a leading "!addr" and
145
- // never-referenced ones with a trailing "; unused". Both matter downstream.
146
- function parseSymbols(path) {
147
- if (!existsSync(path)) return [];
148
- return readFileSync(path, "utf8").split("\n").flatMap((raw) => {
149
- const m = raw.match(/^(!addr\s+)?(\S+)\s*=\s*(\S+?)\s*(?:;\s*(.*))?$/);
150
- if (!m) return [];
151
- return [{
152
- name: m[2], value: m[3],
153
- isAddress: Boolean(m[1]),
154
- used: !/^unused/.test(m[4] || ""),
155
- }];
156
- });
157
- }
158
-
159
- // ACME's label file lists every global symbol, constants included, in address
160
- // form. A debugger reads `viccolor_WHITE = $1` as a name for address $0001 and
161
- // relabels the 6510 processor port with it. Keep referenced addresses only, so
162
- // the emitted file is safe to load anywhere.
163
- function curateLabels(vsPath, symbols) {
164
- if (!existsSync(vsPath)) return { kept: 0, dropped: 0 };
165
- const addr = new Set(symbols.filter((s) => s.isAddress && s.used).map((s) => s.name));
166
- const kept = [];
167
- let dropped = 0;
168
- for (const l of readFileSync(vsPath, "utf8").split("\n")) {
169
- const m = l.match(/^al\s+C:[0-9a-f]+\s+\.(\S+)/i);
170
- if (!m) continue;
171
- if (addr.has(m[1])) kept.push(l); else dropped++;
172
- }
173
- writeFileSync(vsPath, kept.join("\n") + (kept.length ? "\n" : ""));
174
- return { kept: kept.length, dropped };
175
- }
176
-
177
- async function build(src, opts) {
178
- if (!existsSync(src)) die(`no such source file: ${src}`);
179
-
180
- const srcAbs = resolve(src);
181
- // Side files follow the .prg, not the source: two -DVARIANT builds of one
182
- // source must not overwrite each other's symbol tables.
183
- const desiredPrg = opts.out
184
- ? resolve(opts.out)
185
- : join(resolve(opts.outDir || dirname(src)), basename(src).replace(/\.(a|asm|s)$/i, "") + ".prg");
186
- const desiredOutDirAbs = dirname(desiredPrg);
187
- if (!existsSync(desiredOutDirAbs)) mkdirSync(desiredOutDirAbs, { recursive: true });
188
- const desiredStem = desiredPrg.replace(/\.prg$/i, "");
189
-
190
- // Workspace-relative request construction (A-03): the root for THIS
191
- // invocation is the smallest ancestor containing both the source and the
192
- // output directory -- see commonAncestorDir()'s own header.
193
- const repoRoot = commonAncestorDir(dirname(srcAbs), desiredOutDirAbs);
194
- const autoOutDirAbs = dirname(srcAbs); // the executor's own default when outDir is omitted
195
-
196
- const args = { source: toRel(repoRoot, srcAbs) };
197
- if (desiredOutDirAbs !== autoOutDirAbs) args.outDir = toRel(repoRoot, desiredOutDirAbs);
198
- if (opts.format) args.format = opts.format;
199
- if (opts.setpc) args.setpc = opts.setpc;
200
- if (opts.defines && opts.defines.length) args.defines = opts.defines;
201
- if (opts.includes && opts.includes.length) args.includes = opts.includes;
202
- if (opts.noReport) args.noReport = true;
203
-
204
- const response = await invokeSeam("acme.build", args, repoRoot);
205
-
206
- if (!response.ok) {
207
- // A seam-level refusal (unresolvable seam, unreachable broker, a bad
208
- // request) -- never a local fallback that spawns the assembler itself.
209
- die(response.message);
210
- }
211
-
212
- // The executor always names outputs after the SOURCE's own basename (never
213
- // a caller-chosen stem) -- see host-tool.mts's buildHostToolArgv(). When
214
- // `-o`/`--out-dir` asked for a DIFFERENT stem (a rename, not just a
215
- // different directory), the produced files are moved here to the exact
216
- // requested names -- a workspace file operation, not a second copy of
217
- // argv construction.
218
- const autoStem = join(desiredOutDirAbs, basename(srcAbs).replace(/\.(a|asm|s)$/i, ""));
219
- if (autoStem !== desiredStem) {
220
- for (const ext of [".prg", ".sym", ".vs", ".rep"]) {
221
- const from = `${autoStem}${ext}`;
222
- const to = `${desiredStem}${ext}`;
223
- if (existsSync(from) && from !== to) {
224
- mkdirSync(dirname(to), { recursive: true });
225
- renameSync(from, to);
226
- }
227
- }
228
- }
229
-
230
- const stem = desiredStem;
231
- const prg = desiredPrg;
232
-
233
- const diags = parseDiagnostics((response.stderrTail || "").trim());
234
- const errors = diags.filter((d) => d.severity.endsWith("error"));
235
- const ok = response.exitStatus === 0 && existsSync(prg);
236
-
237
- let range = null, size = null, symbols = [], labels = null;
238
- if (ok) {
239
- symbols = parseSymbols(`${stem}.sym`);
240
- labels = curateLabels(`${stem}.vs`, symbols);
241
- const buf = readFileSync(prg);
242
- size = buf.length;
243
- if ((opts.format || "cbm") === "cbm" && buf.length >= 2) {
244
- const load = buf[0] | (buf[1] << 8);
245
- range = { load, end: load + buf.length - 2, bytes: buf.length - 2 };
246
- }
247
- }
248
- return { ok, prg, stem, diags, errors, symbols, labels, range, size };
249
- }
250
-
251
- const hex = (n, w = 4) => n.toString(16).padStart(w, "0");
252
-
253
- function reportBuild(res, { json }) {
254
- if (json) { console.log(JSON.stringify(res, null, 2)); return; }
255
- for (const d of res.diags) {
256
- if (d.file) console.log(`${d.file}:${d.line}: ${d.severity}: ${d.message}`);
257
- else console.log(` ${d.message}`);
258
- }
259
- if (!res.ok) { console.error(`build FAILED (${res.errors.length} error(s))`); return; }
260
- const r = res.range;
261
- console.log(
262
- `built ${res.prg} (${res.size} bytes)` +
263
- (r ? ` load $${hex(r.load)}-$${hex(r.end)} ${r.bytes} bytes of code` : "")
264
- );
265
- const used = res.symbols.filter((s) => s.used).length;
266
- console.log(`symbols: ${res.stem}.sym (${used} used / ${res.symbols.length} total)`);
267
- if (res.labels) {
268
- console.log(`debug labels: ${res.stem}.vs (${res.labels.kept} addresses)`);
269
- }
270
- }
271
-
272
- // -------------------------------------------------------------------- verbs
273
-
274
- async function cmdBuild(argv) {
275
- const o = parseOpts(argv);
276
- const res = await build(o.src, o);
277
- reportBuild(res, o);
278
- process.exit(res.ok ? 0 : 1);
279
- }
280
-
281
- async function cmdSym(argv) {
282
- const o = parseOpts(argv);
283
- const res = await build(o.src, { ...o, noReport: true });
284
- if (!res.ok) { reportBuild(res, o); process.exit(1); }
285
- const used = res.symbols.filter((s) => s.used).sort((a, b) => a.name.localeCompare(b.name));
286
- if (o.json) { console.log(JSON.stringify(used, null, 2)); return; }
287
- for (const s of used) console.log(`${s.isAddress ? "addr " : "const"} ${s.value.padStart(6)} ${s.name}`);
288
- }
289
-
290
- // A skeleton that is correct on the first try: BASIC stub with a computed SYS
291
- // target, the C64 symbol libraries, and no !to (the CLI supplies -o).
292
- function cmdNew(argv) {
293
- const path = argv[0];
294
- if (!path) die("usage: new <file.a>");
295
- if (existsSync(path)) die(`${path} already exists`);
296
- // template.a lives at the skill root, one level up from scripts/, by
297
- // decision: only .mjs modules move into scripts/.
298
- writeFileSync(path, readFileSync(join(HERE, "..", "template.a"), "utf8"));
299
- console.log(`wrote ${path}`);
300
- console.log(`next: node ${selfPath()} build ${path}`);
301
- }
302
-
303
- // ------------------------------------------------------------------ options
304
-
305
- function parseOpts(argv) {
306
- const o = { defines: [], includes: [], json: false };
307
- const rest = [];
308
- for (let i = 0; i < argv.length; i++) {
309
- const a = argv[i];
310
- if (a === "--json") o.json = true;
311
- else if (a === "--no-report") o.noReport = true;
312
- else if (a === "-o" || a === "--out") o.out = argv[++i];
313
- else if (a === "--out-dir") o.outDir = argv[++i];
314
- else if (a === "-f" || a === "--format") o.format = argv[++i];
315
- else if (a === "--setpc") o.setpc = argv[++i];
316
- else if (a === "-D") o.defines.push(argv[++i]);
317
- else if (a.startsWith("-D")) o.defines.push(a.slice(2));
318
- else if (a === "-I") o.includes.push(argv[++i]);
319
- else rest.push(a);
320
- }
321
- o.src = rest[0];
322
- if (!o.src) die("no source file given");
323
- return o;
324
- }
325
-
326
- // --------------------------------------------------------------------- main
327
-
328
- const [cmd, ...rest] = process.argv.slice(2);
329
- const VERBS = { new: cmdNew, build: cmdBuild, sym: cmdSym };
330
- if (!cmd || !VERBS[cmd]) {
331
- console.log(`usage: node ${selfPath()} <command> [options]
332
-
333
- new <file.a> scaffold a C64 program (BASIC stub, no libraries needed)
334
- build <file.a> assemble -> .prg .sym .vs .rep
335
- sym <file.a> list the symbols the program uses
336
-
337
- options: -o FILE --out-dir DIR -f FORMAT --setpc ADDR -DSYM=VAL -I DIR
338
- --no-report --json`);
339
- process.exit(cmd ? 1 : 0);
340
- }
341
- await VERBS[cmd](rest);
@@ -1,53 +0,0 @@
1
- ; C64 / 6510 program skeleton.
2
- ; Build: node .claude/skills/acme-build/scripts/acme.mjs build THIS.a
3
- ; No !to here on purpose - the driver passes -o, and having both makes ACME
4
- ; warn "Output file already chosen" and silently ignore the !to.
5
- ;
6
- ; No angle-bracket library includes here on purpose. Neither documented ACME
7
- ; provisioning route ships the standard hardware-register library - a bare
8
- ; `~/.local/bin/acme` install and the Debian trixie `apt` candidate were both
9
- ; verified to lack it - so a scaffold that depends on
10
- ; it fails to assemble on a fresh install. The five constants below are
11
- ; exactly what this scaffold's body uses, defined locally instead. If you
12
- ; want the full library for your own sources' angle-bracket includes, set
13
- ; $ACME to a directory containing that library (with vic.a inside it);
14
- ; acme.mjs's findAcmeLib() already probes for it and passes it through to
15
- ; the child.
16
-
17
- !cpu 6510 ; C64's CPU: legal 6502 + the illegal opcodes
18
-
19
- !address vic_cborder = $d020 ; VIC-II border color register
20
- !address vic_cbg = $d021 ; VIC-II background color register
21
- viccolor_BLACK = 0
22
- viccolor_GREEN = 5
23
- !address k_chrout = $ffd2 ; KERNAL: print one PETSCII char
24
-
25
- * = $0801 ; start of BASIC RAM
26
-
27
- ; --- BASIC stub: "10 SYS <entry>", so LOAD"...",8,1 + RUN reaches the code.
28
- ; The SYS digits are computed from `entry`, so inserting code above never
29
- ; breaks the stub - which is the classic way to lose an afternoon.
30
- !word .eol, 10 ; link to next line, line number
31
- !byte $9e ; SYS token
32
- !byte '0' + entry % 10000 / 1000
33
- !byte '0' + entry % 1000 / 100
34
- !byte '0' + entry % 100 / 10
35
- !byte '0' + entry % 10
36
- !byte 0 ; end of BASIC line
37
- .eol !word 0 ; end of BASIC program
38
-
39
- entry
40
- lda #viccolor_BLACK
41
- sta vic_cbg ; $d021 background
42
- lda #viccolor_GREEN
43
- sta vic_cborder ; $d020 border
44
-
45
- ldx #0
46
- - lda .msg,x
47
- beq +
48
- jsr k_chrout ; $ffd2 print one PETSCII char
49
- inx
50
- bne -
51
- + rts
52
-
53
- .msg !pet "hello from acme", 13, 0