c64bridge 1.0.0 → 1.0.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 +5 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +151 -37
- package/dist/config.js +18 -10
- package/dist/device.js +123 -87
- package/dist/knowledge.js +1 -1
- package/dist/loggingHttpClient.js +15 -3
- package/dist/mcp-server.js +56 -15
- package/dist/rag/indexer.js +11 -8
- package/dist/rag/init.js +11 -8
- package/dist/sidwaveCompiler.js +2 -2
- package/dist/tools/audio.js +26 -5
- package/dist/tools/input.js +9 -14
- package/dist/tools/memory.js +20 -5
- package/dist/tools/meta/artifacts.js +20 -8
- package/dist/tools/meta/background.js +11 -1
- package/dist/tools/pollValidator.js +19 -51
- package/dist/tools/printer.js +1 -0
- package/dist/tools/registry/platform.js +2 -0
- package/dist/tools/registry/printer.js +1 -0
- package/dist/vice/mockServer.js +33 -0
- package/dist/vice/process.js +11 -1
- package/dist/vice/viceClient.js +35 -6
- package/doc/reviews/hardening/01-fable/prompt-handover.md +125 -0
- package/doc/reviews/hardening/01-fable/prompt.md +98 -0
- package/doc/reviews/hardening/01-fable/review-prompt.md +468 -0
- package/doc/reviews/hardening/01-fable/review.md +1234 -0
- package/mcp/schemas/c64_input.schema.json +5 -5
- package/mcp/server.json +1 -1
- package/mcp/tools.json +7 -6
- package/mcp.json +1 -1
- package/package.json +5 -5
- package/scripts/mockC64Server.mjs +131 -2
- package/server.json +2 -2
package/dist/vice/viceClient.js
CHANGED
|
@@ -53,6 +53,8 @@ export class ViceClient {
|
|
|
53
53
|
}
|
|
54
54
|
rejectAllPending(reason) {
|
|
55
55
|
for (const [, pending] of this.pending) {
|
|
56
|
+
if (pending.timer)
|
|
57
|
+
clearTimeout(pending.timer);
|
|
56
58
|
try {
|
|
57
59
|
pending.reject(reason);
|
|
58
60
|
}
|
|
@@ -66,7 +68,7 @@ export class ViceClient {
|
|
|
66
68
|
// Frame: [0]=0x02, [1]=0x02, [2..5]=len, [6]=respType, [7]=err, [8..11]=reqId, [12..]=body
|
|
67
69
|
while (this.buffer.length >= 12) {
|
|
68
70
|
if (this.buffer[0] !== 0x02 || this.buffer[1] !== 0x02) {
|
|
69
|
-
const idx = this.buffer.indexOf(0x02, 1);
|
|
71
|
+
const idx = this.buffer.indexOf(Buffer.from([0x02, 0x02]), 1);
|
|
70
72
|
this.buffer = idx === -1 ? Buffer.alloc(0) : this.buffer.subarray(idx);
|
|
71
73
|
continue;
|
|
72
74
|
}
|
|
@@ -88,6 +90,8 @@ export class ViceClient {
|
|
|
88
90
|
continue;
|
|
89
91
|
}
|
|
90
92
|
if (err !== 0x00) {
|
|
93
|
+
if (pending.timer)
|
|
94
|
+
clearTimeout(pending.timer);
|
|
91
95
|
pending.reject(new Error(`BM error 0x${err.toString(16)}`));
|
|
92
96
|
this.pending.delete(reqId);
|
|
93
97
|
continue;
|
|
@@ -98,6 +102,8 @@ export class ViceClient {
|
|
|
98
102
|
pending.onFrame(responseType, frame);
|
|
99
103
|
}
|
|
100
104
|
catch (error) {
|
|
105
|
+
if (pending.timer)
|
|
106
|
+
clearTimeout(pending.timer);
|
|
101
107
|
pending.reject(error);
|
|
102
108
|
this.pending.delete(reqId);
|
|
103
109
|
}
|
|
@@ -108,6 +114,8 @@ export class ViceClient {
|
|
|
108
114
|
continue;
|
|
109
115
|
}
|
|
110
116
|
pending.resolve(frame);
|
|
117
|
+
if (pending.timer)
|
|
118
|
+
clearTimeout(pending.timer);
|
|
111
119
|
this.pending.delete(reqId);
|
|
112
120
|
}
|
|
113
121
|
}
|
|
@@ -115,7 +123,10 @@ export class ViceClient {
|
|
|
115
123
|
if (!this.socket || this.socket.destroyed) {
|
|
116
124
|
return Promise.reject(new Error("VICE monitor socket is not connected"));
|
|
117
125
|
}
|
|
118
|
-
|
|
126
|
+
if (this.nextReqId === 0xffffffff) { // Reserved for unsolicited events (see onData above); skip it on wraparound.
|
|
127
|
+
this.nextReqId = 0;
|
|
128
|
+
}
|
|
129
|
+
const reqId = this.nextReqId++ >>> 0;
|
|
119
130
|
const payload = body ?? Buffer.alloc(0);
|
|
120
131
|
const header = Buffer.alloc(11);
|
|
121
132
|
header[0] = 0x02; // STX
|
|
@@ -126,12 +137,20 @@ export class ViceClient {
|
|
|
126
137
|
const packet = Buffer.concat([header, payload]);
|
|
127
138
|
const expectedCmd = options?.responseType ?? cmd;
|
|
128
139
|
const promise = new Promise((resolve, reject) => {
|
|
129
|
-
|
|
140
|
+
const pending = {
|
|
130
141
|
cmd: expectedCmd,
|
|
131
142
|
resolve,
|
|
132
143
|
reject,
|
|
133
144
|
onFrame: options?.onFrame,
|
|
134
|
-
}
|
|
145
|
+
};
|
|
146
|
+
const timeoutMs = options?.timeoutMs ?? (cmd === 0x84 ? 30000 : 10000);
|
|
147
|
+
pending.timer = setTimeout(() => {
|
|
148
|
+
if (!this.pending.delete(reqId))
|
|
149
|
+
return;
|
|
150
|
+
reject(new Error(`VICE monitor request 0x${cmd.toString(16)} timed out after ${timeoutMs}ms`));
|
|
151
|
+
this.socket?.destroy();
|
|
152
|
+
}, timeoutMs);
|
|
153
|
+
this.pending.set(reqId, pending);
|
|
135
154
|
});
|
|
136
155
|
this.socket.write(packet);
|
|
137
156
|
return promise;
|
|
@@ -178,6 +197,13 @@ export class ViceClient {
|
|
|
178
197
|
const body = Buffer.concat([Buffer.from([bytes.length & 0xff]), bytes]);
|
|
179
198
|
await this.send(0x72, body);
|
|
180
199
|
}
|
|
200
|
+
/** Set the VICE joyport's active-low control byte (BM command 0xA2). */
|
|
201
|
+
async joyportSet(port, activeLowMask) {
|
|
202
|
+
const body = Buffer.alloc(4);
|
|
203
|
+
body.writeUInt16LE(port, 0);
|
|
204
|
+
body.writeUInt16LE(activeLowMask & 0xff, 2);
|
|
205
|
+
await this.send(0xA2, body);
|
|
206
|
+
}
|
|
181
207
|
async checkpointGet(id) {
|
|
182
208
|
const body = Buffer.alloc(4);
|
|
183
209
|
body.writeUInt32LE(id >>> 0, 0);
|
|
@@ -189,8 +215,11 @@ export class ViceClient {
|
|
|
189
215
|
const end = (options.end ?? options.start) & 0xffff;
|
|
190
216
|
const stop = options.stopOnHit !== false;
|
|
191
217
|
const enabled = options.enabled !== false;
|
|
192
|
-
const ops = options.operations
|
|
193
|
-
const
|
|
218
|
+
const ops = options.operations;
|
|
219
|
+
const hasExplicitOperation = ops && (ops.load !== undefined || ops.store !== undefined || ops.execute !== undefined);
|
|
220
|
+
const mask = hasExplicitOperation
|
|
221
|
+
? ((ops?.load ? 0x01 : 0) | (ops?.store ? 0x02 : 0) | (ops?.execute ? 0x04 : 0))
|
|
222
|
+
: 0x04;
|
|
194
223
|
const temporary = options.temporary === true;
|
|
195
224
|
const memspace = options.memspace ?? 0;
|
|
196
225
|
const hasMemspace = options.memspace !== undefined;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Hardening 01 — handover: finish the poll-truthfulness fix, then adversarially review and land the branch
|
|
2
|
+
|
|
3
|
+
Repository: `/home/chris/dev/c64/c64bridge`
|
|
4
|
+
Branch: `fix/hardening` (already pushed once as PR #143, previously brought to a green/merge-ready state)
|
|
5
|
+
This file: `doc/reviews/hardening/01-fable/prompt-handover.md`
|
|
6
|
+
|
|
7
|
+
This is a handover from a session that (1) got PR #143 to a fully green, merge-ready state addressing five Kilo Code review comments and raising patch coverage to ~96%, then (2) at the user's request, ran **real physical hardware tests** (C64U, Ultimate 64, Ultimate II+L) against the live MCP server, and (3) found a genuine, reproducible false-success bug during that testing. A fix is in progress but not finished, verified, or committed. You are picking this up cold — there is no prior conversation for you to recall.
|
|
8
|
+
|
|
9
|
+
Do not consider this session's work "done" until Part 3's exit criteria are met. Do not claim completion while any part below remains unresolved.
|
|
10
|
+
|
|
11
|
+
## Part 0 — Orient yourself first
|
|
12
|
+
|
|
13
|
+
1. Read `AGENTS.md` in full.
|
|
14
|
+
2. Read `doc/reviews/hardening/01-fable/review.md` and `doc/reviews/hardening/01-fable/prompt.md` for the original 35-finding hardening context (HARD01-001..035) — this branch already implements all of them, plus five follow-up fixes from a Kilo Code automated review round, plus coverage work. That prior work is committed (`git log --oneline` back to `cfb7e4a`). Nothing in that history needs redoing.
|
|
15
|
+
3. Run `git status` and `git diff --stat` to see the exact uncommitted state you're inheriting (summarized precisely in Part 1 below — but verify it yourself, the working tree may have drifted).
|
|
16
|
+
4. Check `gh pr view 143 --repo chrisgleissner/c64bridge` and `gh pr checks 143 --repo chrisgleissner/c64bridge` for the PR's current state. As of this handover, PR #143 is green and all prior review threads are resolved — the uncommitted work described below is a **new, additional** round on top of that.
|
|
17
|
+
|
|
18
|
+
## Background: what real-hardware testing found
|
|
19
|
+
|
|
20
|
+
The session tested the live MCP server against three genuinely separate physical devices (confirmed via distinct firmware `unique_id`s):
|
|
21
|
+
|
|
22
|
+
- **C64U** — hostname `c64u`, product "C64 Ultimate", firmware 1.2.0.
|
|
23
|
+
- **Ultimate 64 Elite** — hostname `u64`, firmware 3.15. Fully independent hardware (its own board), but uses the same `c64u` backend profile/config key as C64U (same REST API family). The c64bridge config format has exactly one `c64u` slot, and the running server caches whichever host was resolved into it the first time that backend is constructed in a process — editing `.c64bridge.json` and re-selecting the backend does **not** retarget an already-constructed facade. To test C64U and U64 as genuinely separate live connections, the session registered a second temporary MCP server entry, `c64bridge-u64`, in `.mcp.json`, pointed at a separate config file (`C64BRIDGE_CONFIG` env var) with `c64u.host = "u64"`. That second server entry is a legitimate, intentional part of the current uncommitted `.mcp.json` diff — keep it (or ask the user before removing it) rather than reverting it as accidental drift.
|
|
24
|
+
- **Ultimate II+L (U2)** — hostname `u2`, firmware 3.15. Physically a cartridge plugged into the back of the C64U unit — separately networked (own IP), but shares the same underlying simulated C64 bus/CPU/screen as the C64U it's plugged into. **Never drive `c64u` and `u2` operations concurrently** — they are electrically the same target machine. U64 is fully independent and safe to run alongside either.
|
|
25
|
+
|
|
26
|
+
Targeted verification (firmware `errors[]` truthfulness, drive-list shape, real ASM execution, `power_cycle` honesty) passed cleanly on **C64U and U64**. On **U2**, `upload_run_basic` / `upload_run_asm` reported `success: true, verified: true` while the program **never actually executed** — confirmed independently by bypassing the MCP client entirely and sending raw HTTP `POST /v1/runners:run_prg` requests directly to the device in both `application/octet-stream` and `multipart/form-data` encodings (matching the OpenAPI spec exactly). The U2 firmware returns `200 {"errors": []}` but memory at `$0801` never changes and the screen never advances past the boot banner. This was cross-checked against the *same* physical machine via the C64U side (which genuinely does execute, visible through the shared screen) — so it is a real gap in the U2 cartridge's own firmware implementation of the upload-and-run REST feature, **not** a client-side bug. That part needs no further code change; it's a hardware/firmware limitation to document, not fix.
|
|
27
|
+
|
|
28
|
+
**What is a fixable bug**, independent of the U2 firmware issue: `pollBasicOutcome()` / `pollAsmOutcome()` in `src/tools/pollValidator.ts` were blindly reporting success whenever the literal text "RUN" or "ERROR" never appeared on screen within the poll window. Since `upload_run_basic` / `upload_run_asm` execute via the native REST `run_prg` endpoint (or a typed `SYS<entry>` command for ASM) rather than a user visibly typing `RUN`, this fallback was structurally unable to provide a real signal — it happened to look correct on C64U/U64 only because execution there also happens to produce visible screen text (a KERNAL LOAD/RUN echo) for unrelated reasons. On U2, where nothing executes at all, the same fallback silently laundered a real failure into a false `verified: true`. This is exactly the class of bug the original 35 HARD01 findings targeted, just not one of them by number — a natural HARD01-036 in spirit, though nothing in code should assume that ID is registered anywhere; use it only as a descriptive tag if useful (as already done in the new tests, see below).
|
|
29
|
+
|
|
30
|
+
## Part 1 — Finish the in-progress fix (do this first)
|
|
31
|
+
|
|
32
|
+
### 1a. Fix already applied to `src/tools/pollValidator.ts` — verify, don't redo
|
|
33
|
+
|
|
34
|
+
- `pollBasicOutcome`: previously always fell through to `return { status: "ok", type: "BASIC" }` after the poll window if no BASIC error text was ever seen, regardless of whether `runDetected` was ever set. Now, if `runDetected` is still `false` after the full poll window, it returns `{ status: "error", type: "BASIC", message: "No execution activity detected: the firmware reported success, but the program does not appear to have run." }` instead.
|
|
35
|
+
- `pollAsmOutcome`: previously returned `{ status: "ok", type: "ASM" }` immediately if `runDetected` was never set, **skipping** the hardware liveness check (screen-RAM CRC comparison) entirely. That early return is removed — execution now always falls through to the liveness check regardless of whether "RUN"/"ERROR" text was ever seen. Only the two log lines and a comment changed around this; the liveness-check logic itself (already correct, from the original HARD01-019/020 work) is untouched.
|
|
36
|
+
|
|
37
|
+
Confirm this is still exactly what's in the working tree (`git diff src/tools/pollValidator.ts`) before proceeding — do not re-derive it from scratch, but do sanity-check it against the description above.
|
|
38
|
+
|
|
39
|
+
### 1b. Test updates already applied — verify, don't redo
|
|
40
|
+
|
|
41
|
+
`test/pollValidator.test.mjs`:
|
|
42
|
+
- Two tests that previously *encoded the bug as correct behavior* — `"pollForProgramOutcome BASIC returns ok if RUN not detected"` and `"pollForProgramOutcome ASM returns ok if RUN not detected"` (both used a mock whose `readScreen()` always returns `"READY.\n"` forever, and asserted `status: "ok"`) — were renamed and fixed to assert the corrected behavior: `"HARD01-036 pollForProgramOutcome BASIC reports an error instead of blind success when RUN is never detected"` (asserts `status: "error"`, message matches `/does not appear to have run/i`) and `"HARD01-036 pollForProgramOutcome ASM reports crashed instead of blind success when RUN is never detected"` (asserts `status: "crashed"`, `reason: "no program-visible screen progression within window"`).
|
|
43
|
+
- A new positive-case test was added, `"HARD01-036 pollForProgramOutcome ASM still detects real liveness even when RUN text never appears"`, confirming the fix doesn't regress the case where RUN/ERROR text never shows but screen RAM genuinely changes (must still report `ok`).
|
|
44
|
+
|
|
45
|
+
`test/programRunnersModule.test.mjs`:
|
|
46
|
+
- `"upload_run_basic verify true annotates metadata"`: mock's `readScreen()` previously always returned `"READY.\n"`; now returns `"READY.\n"` on the first call (the tool's own pre-poll screen check) and `"RUN\n"` thereafter (matching how a real backend's LOAD/RUN echo actually behaves), so the poll genuinely observes execution instead of relying on the old blind-success fallback.
|
|
47
|
+
- `"upload_run_basic verify tolerates repeated screen read failures"`: mock's `readScreen()` previously threw on *every* call, and the test asserted success. That was itself an instance of the bug (permanent failure ≠ "tolerated" success). Now the mock throws only on the first 3 calls, then returns `"RUN\n"` — genuinely testing transient-failure tolerance, not permanent-failure-as-success.
|
|
48
|
+
- `"upload_run_asm returns structured content on success"`: the mock previously had no `readScreen`/`readMemoryRaw` at all (relying on the old shortcut to skip verification entirely, even though `executeUploadRunAsm` calls `pollForProgramOutcome` unconditionally regardless of the `verify` flag). Now includes a minimal `readScreen` (`"RUN\n"`) and `readMemoryRaw` (returns a changing byte pattern) so the now-mandatory liveness check has something real to observe.
|
|
49
|
+
- `"upload_run_asm verify treats repeated screen read failures as instant execution"` — this test's *name* literally described the bug as a feature. Renamed to `"upload_run_asm verify tolerates transient screen read failures and still verifies via liveness"` and its mock changed from permanent to transient (3) failures followed by success, mirroring the BASIC fix above.
|
|
50
|
+
|
|
51
|
+
Confirm `test/pollValidator.test.mjs` and `test/programRunnersModule.test.mjs` diffs match this description; run `timeout 120 node scripts/run-tests.mjs test/pollValidator.test.mjs test/programRunnersModule.test.mjs test/groupedToolsShims.test.mjs` and confirm all green (96/96 in the second file, 22/22 in the first, as of last check).
|
|
52
|
+
|
|
53
|
+
### 1c. Leftover test artifacts already cleaned up — verify, don't redo
|
|
54
|
+
|
|
55
|
+
The hardware-testing session had left two untracked scratch files in the repo root — `.c64bridge.json` (containing `{"u2": {"host": "u2", "port": 80}}`) and `.c64bridge.u64.json`. These were interfering with `test/device.test.mjs`'s `"falls back to vice or c64u when no config"` test via `config.ts`'s "legacy package-adjacent config" fallback candidate (`dirname(import.meta.url of config.ts)/../.c64bridge.json`), which resolves relative to the *source file location*, not `process.cwd()` — so it bypasses that test's directory-isolation helper (`withConfigScenario`) entirely. Both files have already been deleted. Run `git status --porcelain` and confirm neither file is present; if either has reappeared (e.g. from a stray tool run), delete it again before running the full suite — it will cause spurious, hard-to-diagnose failures in `device.test.mjs` and possibly elsewhere.
|
|
56
|
+
|
|
57
|
+
### 1d. NOT yet fixed — this is the actual remaining work
|
|
58
|
+
|
|
59
|
+
`scripts/mockC64Server.mjs` was modified to make the C64U mock server's simulated `injectKeyboardQueue` path (used by `upload_run_asm`'s typed `SYS<entry>` trigger) produce *some* observable screen-RAM change, since the mock runs no real 6502 code and the now-mandatory liveness check in `pollAsmOutcome` needs something to observe. The current approach: `simulateKeyboardDrain()` (which already existed, to drain the KERNAL NDX counter) now also detects when the queued/typed text matches `/SYS\s*\d+/i` and, if so, starts a `setInterval` "heartbeat" that increments `state.memory[0x0400]` every 20ms for up to 50 ticks (~1 second), simulating an alive, still-running program.
|
|
60
|
+
|
|
61
|
+
**This introduced a real regression**: the heartbeat is a background timer with no cleanup tied to test boundaries. Because `test/c64Client.test.mjs` shares one mock server/client instance across many sequential `t.test()` subtests (no `resetState()`/reset call between them), an *earlier* subtest in that file — `"HARD01-018 uploadAndRunAsm loads the assembled PRG..."` (around line 456) or `"HARD01-018 uploadAndRunAsm keeps a non-$0801 origin intact..."` (around line 475), both of which trigger a `SYS...` pattern — leaves a heartbeat ticking for up to a second. A *later*, unrelated subtest a few lines down, `"readMemory returns hex string with prefix"` (around line 506), which expects to read back the exact bytes (`$AA55`) written by the immediately-preceding `"writeMemory writes bytes to mock memory"` subtest, instead observes `$AB55` — the first byte has been incremented by exactly 1 by the still-running heartbeat from the earlier subtest. Confirmed via direct code inspection; not yet re-confirmed by test run after the last edit (the fix attempt was mid-flight when this handover was written — you may find it still fails, or fails intermittently depending on timing).
|
|
62
|
+
|
|
63
|
+
**Fix this properly before doing anything else.** Suggested directions, roughly in order of robustness (pick one, don't feel obligated to preserve the current heartbeat mechanism if a cleaner design fits better):
|
|
64
|
+
|
|
65
|
+
1. **Time-computed overlay instead of a live timer (recommended).** Instead of a `setInterval` that mutates `state.memory` in the background, record `state.heartbeat = { startedAt: Date.now(), durationMs, baseValue }` when a `SYS` command is detected, and have the `machine:readmem` handler (or wherever `$0400` reads are served) compute a *synthetic* byte value on read — e.g. `baseValue + Math.floor((Date.now() - startedAt) / 20) & 0xff` — only while `Date.now() - startedAt < durationMs`, otherwise return the real stored byte. This eliminates all background timers and the cross-test leakage entirely: nothing mutates `state.memory` asynchronously, so an unrelated `writeMemory`/`readMemory` pair immediately after a `SYS`-triggering subtest is unaffected (the overlay only applies to reads of that specific address, and only while a window is "active" for a specific triggering event — consider whether the overlay should apply narrowly enough that a real write to `$0400` by a later subtest correctly overrides/disables it, e.g. by clearing `state.heartbeat` on any explicit write to that address).
|
|
66
|
+
2. **Track and clear timers explicitly.** Keep the `setInterval` approach, but store the active heartbeat handle in `state` (e.g. `state.activeHeartbeat`), clear any previous one before starting a new one, and clear it in `resetState()`. This only fully solves leakage across resets — it does **not** solve leakage between sequential subtests in the *same* file that never call reset (the actual failure mode observed), so this alone is likely insufficient without also auditing whether `t.test()` sequences in shared-client test files should call `mock.reset()` between subtests (check `test/c64Client.test.mjs`'s setup/teardown and `test/helpers/mcpTestHarness.mjs`'s `withSharedMcpClient` for whether that's feasible without breaking other assumptions).
|
|
67
|
+
3. **Shrink the blast radius.** If you keep a live timer, drastically shorten its duration (a handful of ticks over ~40-60ms) so it's very unlikely to outlive the specific subtest that triggered it, while remaining long enough to intersect the poll windows used by tests that override `C64BRIDGE_POLL_MAX_MS`/`C64BRIDGE_POLL_INTERVAL_MS` (values as low as `maxMs: 50, intervalMs: 10` appear in the test suite — check the *smallest* configured poll window across all callers before picking a duration, not just the ones you happen to be looking at). This is the least robust option and only worth using if 1 and 2 both prove impractical.
|
|
68
|
+
|
|
69
|
+
Whichever approach you take, the fix must satisfy **all** of:
|
|
70
|
+
- `test/suites/mcpServerCallToolSuite.mjs`'s `"c64_program upload_run_asm assembles source and runs program"` test passes reliably (run it standalone at least 10 times in a loop to rule out flakiness, not just once).
|
|
71
|
+
- `test/c64Client.test.mjs` passes with no cross-subtest interference (run the whole file standalone, and also as part of the full suite — the earlier failure only manifested in the full-suite/whole-file run, not a single-test run, so isolated single-test passes are not sufficient evidence).
|
|
72
|
+
- No new timer/interval leaks the Node/Bun process past test completion (check for "open handle" warnings if your test runner surfaces them).
|
|
73
|
+
- The underlying simulation stays scoped to the `SYS`-command pattern specifically — `injectKeyboardQueue` is also used by `src/tools/input.ts` for general `key`/`write_text` operations, and those must not trigger this heartbeat/overlay behavior.
|
|
74
|
+
|
|
75
|
+
After fixing, `timeout 300 npm test` must show **0 failures** across the entire suite (as of last check before the mock-server regression, the count was 792 tests / 788 pass / 2 skip / 0 fail with the pollValidator fix alone, before the mock-server change was introduced — use that as your target, adjusting only for whatever new tests you may have added).
|
|
76
|
+
|
|
77
|
+
### 1e. Full validation gate for Part 1
|
|
78
|
+
|
|
79
|
+
Once the full suite is green, run the complete validation gate used earlier in this branch's history (see recent commits `ad93c5d`, `417ec89` for the pattern):
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
npx tsc -p tsconfig.json --noEmit
|
|
83
|
+
npm test
|
|
84
|
+
npm run test:vice:mock
|
|
85
|
+
npm run build
|
|
86
|
+
npm run check:package
|
|
87
|
+
git diff --check
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
All must pass cleanly. `npm run build` regenerates `mcp/*.json` and `README.md` from source/schema — if those come out changed, that's expected only if you touched schemas; otherwise investigate rather than blindly committing a diff you don't understand.
|
|
91
|
+
|
|
92
|
+
Also reconsider **patch coverage**: this branch has an established pattern (see commit `ad93c5d`'s message and `.mcp.json`-adjacent `codecov.yml`) of keeping patch coverage above ~94% by, among other things, moving standalone explanatory comments onto the code line they document as trailing comments (since this project's bun-based coverage tooling counts every comment/blank line in a diff hunk as an uncovered patch line). Apply the same discipline to your new diff if it introduces many multi-line comments in changed regions — it's cheap and keeps the codecov gate comfortably clear.
|
|
93
|
+
|
|
94
|
+
## Part 2 — Adversarial review of the current branch
|
|
95
|
+
|
|
96
|
+
Once Part 1 is fully green and validated (but not yet committed — or committed as a checkpoint if you prefer smaller commits, your call), perform a genuinely adversarial review of the **entire branch diff against `main`** (i.e. `git diff main...HEAD` plus your uncommitted changes) — not just the new pollValidator/mock-server work. This branch has already been through one automated review round (Kilo Code, five findings, all fixed) and one round of real-hardware testing (which is what surfaced the bug you just fixed) — the goal now is to find what *those* rounds missed, not to re-litigate what they already caught.
|
|
97
|
+
|
|
98
|
+
Use whichever of these fits best; you have access to the same skills this session had available:
|
|
99
|
+
- `/codex challenge` — adversarial mode specifically designed to try to break the code. Well-suited to this task by name; consider using it as a first pass.
|
|
100
|
+
- `/review` or `/code-review` — structural pre-landing review (SQL safety, trust-boundary violations, conditional side effects, simplification opportunities).
|
|
101
|
+
- Manual review, if you prefer full control or the automated tools are unavailable in your environment.
|
|
102
|
+
|
|
103
|
+
Focus areas, in rough priority order:
|
|
104
|
+
|
|
105
|
+
1. **The same bug class elsewhere.** The pollValidator bug was "assume success when a specific text signal never appears, without checking whether that signal was ever achievable for this code path." Grep the codebase for similar patterns — any other poller, verifier, or "wait for X then assume success" loop that has a fallback branch reachable when X structurally cannot occur for some valid code path. `src/tools/programRunners.ts`, `src/c64Client.ts`, and `src/device.ts` are the highest-yield places to look, given they're where the original HARD01-011/018/019/020/021 truthfulness fixes lived.
|
|
106
|
+
2. **The mock-server fidelity gap you just fixed, generalized.** If `scripts/mockC64Server.mjs` didn't simulate keyboard-queue-triggered execution at all before your fix, ask whether other mock behaviors (in this file or `src/vice/mockServer.ts`) are similarly "accepts the request, does nothing, returns success" in ways that could mask a real truthfulness bug the same way the U2 firmware gap did. You don't need to fix speculative gaps, but flag anything concrete you find.
|
|
107
|
+
3. **Standard security review**: command injection, path traversal (especially around `src/tools/meta/filesystem.ts`, `src/tools/storage.ts`, and anywhere device filesystem paths are constructed — HARD01-010's fix should already cover segment encoding, but re-verify it holds against the newest code), credential/secret handling (HARD01-024's redaction — re-verify it's applied everywhere new logging was added, if any), and any newly-introduced regex (the `SYS_COMMAND_PATTERN` you may still be touching in Part 1) for ReDoS risk given it's small and simple but worth a two-second glance.
|
|
108
|
+
4. **Test-suite integrity**: any other test in the suite that, like the two `programRunnersModule.test.mjs` tests you fixed, encodes a *bug* as expected behavior rather than a genuine requirement. This is a real, demonstrated pattern in this codebase now (found twice in one session) — worth a deliberate second pass rather than assuming it was fully exhausted.
|
|
109
|
+
5. **The `.mcp.json` `c64bridge-u64` addition**: confirm it doesn't leak the hardware hostname `u64` or any credentials into anything that shouldn't have it (it shouldn't — no password was configured for these devices — but verify), and that it's harmless if a reader without access to that hardware tries to use this repo (the server entry will simply fail to connect, not error out the whole MCP client setup — confirm this assumption is actually true rather than taking it on faith).
|
|
110
|
+
|
|
111
|
+
## Part 3 — Fix all findings, then land the branch
|
|
112
|
+
|
|
113
|
+
Fix everything Part 2 surfaces, following the same discipline as the rest of this branch: minimal, root-cause fixes; a regression test for anything that was a real bug; no speculative refactors. Re-run the full Part 1e validation gate after every batch of fixes.
|
|
114
|
+
|
|
115
|
+
**Exit criteria — do not stop before all of these are true:**
|
|
116
|
+
|
|
117
|
+
1. `git status` is clean except for intentional, understood changes.
|
|
118
|
+
2. Full validation gate (Part 1e) passes with zero failures.
|
|
119
|
+
3. Changes are committed with a clear message (follow this branch's existing commit style — see `git log --oneline` — imperative, `fix:`/`test:`-prefixed, explaining *why* not just *what*).
|
|
120
|
+
4. Commits are pushed to `fix/hardening` on the `chrisgleissner/c64bridge` remote.
|
|
121
|
+
5. `gh pr checks 143 --repo chrisgleissner/c64bridge` shows every check green, including `codecov/patch` and `codecov/project`.
|
|
122
|
+
6. If your adversarial review or the push triggers new automated review comments (Kilo Code or otherwise) on PR #143, address every one — reply explaining the fix (or the reasoning if not applicable) and resolve the thread via the GitHub API, exactly as was done for the five Kilo Code findings earlier in this PR's history (see commit `ad93c5d` and its PR comment replies for the established pattern: reply via `gh api repos/chrisgleissner/c64bridge/pulls/143/comments/{id}/replies`, resolve via the GraphQL `resolveReviewThread` mutation).
|
|
123
|
+
7. No unresolved review threads remain on PR #143.
|
|
124
|
+
|
|
125
|
+
Only once all seven are true is this work complete. Report back with: a summary of what the mock-server fix ended up being, what the adversarial review found (if anything) and how it was fixed, confirmation of the exit criteria, and the final CI/PR status.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Hardening 01 implementation
|
|
2
|
+
|
|
3
|
+
Repository: `/home/chris/dev/c64/c64bridge`
|
|
4
|
+
|
|
5
|
+
Implement every confirmed finding in [review.md](./review.md): **HARD01-001 through HARD01-035**. This is an implementation task, not a second review. The review's executive-summary/count prose is stale in places; the findings index and detailed findings are authoritative. Do not omit a finding because it is P3, Medium confidence, or shares a file with another finding.
|
|
6
|
+
|
|
7
|
+
## Outcome
|
|
8
|
+
|
|
9
|
+
Deliver a coherent, production-safe hardening change that eliminates the false-success, wrong-target, lifecycle, input, file-system, SID, and packaging failures described in the review. Preserve the public MCP contract except where an existing success result is demonstrably false; in those cases, return a clear failure with actionable details. Add focused automated regression tests for every finding, update generated artifacts only when the source/contract change requires it, and leave the working tree with all relevant checks passing.
|
|
10
|
+
|
|
11
|
+
Do not perform state-changing operations against real Ultimate hardware. Use unit tests, mock Ultimate HTTP servers, Binary Monitor stubs, and managed VICE only where the repository's existing test setup makes that safe and deterministic. Do not expose passwords or other secrets in logs, tests, fixtures, errors, or documentation.
|
|
12
|
+
|
|
13
|
+
## Start correctly
|
|
14
|
+
|
|
15
|
+
1. Read `AGENTS.md` and the entire adjacent `review.md` before editing. Follow the repository's `.github/skills/*/SKILL.md` instructions for any C64 execution workflow. In particular, read `c64://vice/binary-monitor-spec`/`doc/vice/vice-binary-monitor-spec.md` before changing VICE monitor framing or commands.
|
|
16
|
+
2. Inspect the existing test conventions, package scripts, generated-artifact workflow, public MCP schemas, and call sites before changing result shapes or shared facade APIs.
|
|
17
|
+
3. Keep changes narrow but solve root causes. Do not paper over a failure with logging, an unchecked cast, a longer timeout, or a test-only special case.
|
|
18
|
+
4. Work in logical batches, but run targeted tests after each batch and the complete relevant suite before handoff. Do not claim hardware validation unless it was actually performed with explicit authorization.
|
|
19
|
+
|
|
20
|
+
## Cross-cutting implementation rules
|
|
21
|
+
|
|
22
|
+
- Treat Ultimate REST responses as unsuccessful when their JSON `errors` array is non-empty, even when HTTP status is 2xx. Preserve the firmware error strings in structured details. Missing `errors` remains compatible success.
|
|
23
|
+
- Pin backend/facade identity for the full lifetime of a tool invocation and any spawned work. A backend switch may affect later invocations but must never retarget an operation already underway.
|
|
24
|
+
- Never silently drop input, swallow a failed state-restoration operation, or report a physical action as verified when the required state cannot be observed. Report uncertainty/failure explicitly and include recovery guidance where useful.
|
|
25
|
+
- Keep monitor queues and background work cancellable and recoverable. A timeout or shutdown must release subsequent work rather than permanently wedging a backend.
|
|
26
|
+
- Use package-relative paths for assets shipped with the package and process-cwd paths only for deliberately user/project-local inputs. Use `fileURLToPath`, not raw URL pathnames.
|
|
27
|
+
- Validate and contain all user-influenced host filesystem paths after resolution. Never rely solely on string prefix checks that permit sibling-prefix escapes; use `path.relative`/platform-aware containment.
|
|
28
|
+
- Retain backward compatibility deliberately. If a formerly `void` method must signal a timeout, propagate an explicit typed result or error through every caller rather than silently discarding it.
|
|
29
|
+
|
|
30
|
+
## Required fixes and acceptance criteria
|
|
31
|
+
|
|
32
|
+
### HTTP transport, configuration, platform status, and package paths
|
|
33
|
+
|
|
34
|
+
- **HARD01-001:** Make `--http [port]` real, or remove the dead parser/imports and the documented option together. Prefer wiring the existing parsed CLI options to a clearly factored transport-selection path using `StreamableHTTPServerTransport`; retain stdio as the default. Test that `--http 8080` selects/listens on the HTTP path and that default invocation selects stdio. Ensure MCP server/prompt registry wiring is actually used or remove the dead path.
|
|
35
|
+
- **HARD01-004:** Capability probing must not cache a transient error as `unknown`. Cache confirmed availability/unavailability as appropriate, but evict/retry uncertain results. Test failure-once then success.
|
|
36
|
+
- **HARD01-005, HARD01-006, HARD01-027:** Replace divergent configuration loading with one typed, authoritative loader and pass its parsed backend sections to all consumers/facade creation. Candidate precedence must match documented behavior: `C64BRIDGE_CONFIG`, then `process.cwd()/.c64bridge.json`, then home config, then defaults; retain a package-relative legacy fallback only if necessary and document its lower precedence. Resolve home via `os.homedir()` fallback and module paths through `fileURLToPath`. A malformed/unreadable candidate must produce a prominent redacted warning that identifies the file and problem, then continue to the next candidate/defaults rather than crashing or silently disagreeing. Test layered/contradictory configs, project-cwd config under an installed-style foreign package cwd, malformed config fallback, and identical host/port/password selection by startup and facade code.
|
|
37
|
+
- **HARD01-016:** Establish an invocation-scoped backend snapshot/facade lease. Platform gating, all operations in a multi-step tool flow, verification, and background work must use that pinned facade. Make switching atomic with platform publication so no request is gated with one platform and sent to another. Avoid a global lock that deadlocks streams or long-running work; a reference-counted/read lease or a façade captured in tool context is preferable. Add a deterministic concurrent mock-facade test where a slow write/verify is interleaved with `c64_select_backend` and every write/read/resume step remains on the original backend.
|
|
38
|
+
- **HARD01-025:** Resolve all shipped RAG data, docs, embeddings, AGENTS/prompt assets relative to the installed package/module root, not `process.cwd()`. If an override is supported, make it explicit and test it. Do not silently return an empty index merely because a bundled path was calculated incorrectly; surface a useful diagnostic when an expected bundled index cannot load. Test initialization from a foreign cwd.
|
|
39
|
+
- **HARD01-031:** `c64://platform/status` must not block on a normal 10-second live REST probe. Return cached/static/not-yet-probed capability state quickly, or use a bounded short probe that cannot poison the cache. Test a never-resolving probe and assert a prompt resource result within the short bound.
|
|
40
|
+
|
|
41
|
+
### Ultimate REST, input, physical-state safety, drive shape, and logging
|
|
42
|
+
|
|
43
|
+
- **HARD01-002 and HARD01-003:** Rework C64U/U64 `power_cycle` to be fail-closed. Before sending final RETURN, positively determine from the menu screen's actual selection/highlight encoding that `POWER CYCLE` is selected; visible text anywhere on screen is insufficient. Confirm firmware color-matrix semantics from repository/device contract and abort on uncertainty. On every post-menu-open failure, make a safe, best-effort, idempotent attempt to close/escape the menu and return cleanup status in failure details. Never report a selected power cycle unless verification succeeds. Test different highlighted rows, all navigation-step failures, no final RETURN on mismatch, and cleanup attempts.
|
|
44
|
+
- **HARD01-009:** Do not overwrite an undrained KERNAL queue. Make keyboard queue injection return a typed completion result or throw a typed timeout/error, then update every input tool call site to report partial/non-delivery accurately. Test a permanently nonzero NDX, assert no subsequent chunk overwrite, and assert the user-visible result is failure rather than success.
|
|
45
|
+
- **HARD01-010:** Encode Ultimate device filesystem paths segment-by-segment so nested slash separators remain separators while special characters in segments are encoded exactly once. Cover leading slash and spaces in an HTTP request-capture test.
|
|
46
|
+
- **HARD01-011:** Centralize `ActionResponse` error-array handling in the C64U/U2 backend; apply it to every action, configuration, drive, program, file, and playback method rather than hand-patching a few call sites. Nonempty `errors` must result in `success: false` and retain useful response details. Mock 200+errors for mount, run, and config operations and assert the MCP/tool layer reports failure.
|
|
47
|
+
- **HARD01-017:** If write verification paused a machine and resume fails, do not let the original write success escape as a clean success. Return/throw a failure that records write/verification outcome, `machinePaused: true`, the resume failure, and clear recovery instructions; an appropriately bounded retry is acceptable. Test return and thrown resume failures.
|
|
48
|
+
- **HARD01-023:** Normalize `drivesList` at the facade boundary into one documented internal shape (at least `id`, `power`, `image`, `type`, plus any needed raw details), for both VICE and Ultimate/U2. Decode the actual Ultimate `{ errors, drives: [{ id: info }] }` response rather than leaking it. Make `drive_mount_and_verify` use that normalized contract and verify the correct image-path/power fields. Test the real firmware-shaped mock response.
|
|
49
|
+
- **HARD01-024:** Redact sensitive HTTP request/response diagnostic metadata case-insensitively before it reaches stderr or diagnostics persistence. At minimum mask `X-Password` and authorization/cookie-equivalent secrets. Test that a known test secret never appears while the header key/placeholder may remain useful for diagnosis.
|
|
50
|
+
- **HARD01-034:** Align printer module runtime gates with advertised U2 capability: support `c64u` and `u2` if device-4 BASIC printing is actually supported by the stated contract, otherwise remove the advertised U2 feature and explain it. The likely intended fix is explicit `supportedPlatforms: ["c64u", "u2"]` on both relevant modules. Add capability/gating coverage.
|
|
51
|
+
|
|
52
|
+
### VICE monitor, emulator lifecycle, input, and config
|
|
53
|
+
|
|
54
|
+
- **HARD01-012:** Add per-request Binary Monitor response deadlines that reject, remove the pending request, and reset/destroy the unhealthy socket so the serialized monitor queue always releases. Use a considered timeout policy (normal versus large display transfer if needed), clear timers on response/error/close, and avoid unhandled rejections or request-ID reuse confusion. The parser must preserve framing correctness during resynchronization; do not scan arbitrary payload bytes as a new frame unless the protocol framing permits it. A stub server that accepts but never answers must prove bounded rejection and a subsequent operation must succeed after reconnect.
|
|
55
|
+
- **HARD01-013:** Ensure managed VICE and Xvfb are stopped on normal exit and SIGINT/SIGTERM/SIGHUP. Since async work cannot complete in `exit`, expose/use a synchronous best-effort kill path there; signal handlers should coordinate bounded async shutdown where viable and then exit without recursive/double cleanup. Guard already-exited children. Add tests around registered handlers/child termination and, where reliable in CI, an integration check proving no managed child survives SIGTERM.
|
|
56
|
+
- **HARD01-014:** Implement VICE Binary Monitor `0xA2` Joyport Set from the in-repo spec, including correctly encoded port and active-low control mask. Route VICE joystick press/release/tap through it; do not write CIA data-port registers to simulate input. Test exact BM bytes for press/release/tap and retain C64U physical-input behavior unchanged.
|
|
57
|
+
- **HARD01-015:** Construct checkpoint masks so explicitly provided operations are exactly honored; default to execute only when no load/store/execute flag was requested. Test `{store:true}` serializes `0x02` and other combinations.
|
|
58
|
+
- **HARD01-026:** Make VICE reset/reboot success contingent on `waitForBasicReady`'s required readiness signals. Return structured `success:false` with readiness diagnostics when it does not reach READY; update callers/tests accordingly.
|
|
59
|
+
- **HARD01-035:** Stop coercing values solely because they look numeric. Determine VICE resource type from the monitor/resource API (or add an explicit value type where the public schema supports it) and send a string payload for numeric-looking string resources such as `"1541"`. Test both resource types and batched behavior.
|
|
60
|
+
|
|
61
|
+
### Program execution, BASIC generation, and validation
|
|
62
|
+
|
|
63
|
+
- **HARD01-007:** Generate valid Commodore BASIC V2 printer output for strings containing quotes. Do not use doubled quotes. Split expressions and emit `CHR$(34)` (while preserving all other literal characters), maintain line-length/chunking correctness, close the printer channel on normal/error paths where feasible, and ensure the tool does not claim completed printing merely because load/run started. Add generator/tokenizer-level coverage for quoted text and a VICE-facing regression if the suite supports it.
|
|
64
|
+
- **HARD01-008:** Make the PETSCII screen program actually wait for a key without immediately falling through to `READY.`. Use a valid GET loop and preserve intended termination behavior. Test generated source and, if possible, rendered-screen behavior before a key press.
|
|
65
|
+
- **HARD01-018:** Make `upload_run_asm` execute assembled code on every supported backend. Establish one robust contract for entry address and PRG layout: either emit a valid BASIC SYS stub for applicable loads while keeping labels/address accounting correct, or inject/run the PRG then explicitly `SYS` the assembled entry in a backend-safe manner. Do not point VICE BASIC pointers at arbitrary non-BASIC ML. Test the published `$0801` example by observing its intended memory/screen effect and test a non-$0801 origin does not corrupt BASIC workspace. Keep symbol and verification metadata truthful.
|
|
66
|
+
- **HARD01-019 and HARD01-020:** Replace the ASM liveness heuristic so it neither treats free-running VIC/CIA values as activity nor reads side-effectful CIA ICRs on hardware. Prefer program-specific expected effects/explicit verification where supplied; any fallback must restrict reads to side-effect-free addresses, mask volatile registers, and honestly describe its confidence. Test a memory stream differing only at `$D012` as non-alive/crashed and assert hardware polling never overlaps `$DC0D`/`$DD0D` (or the I/O ranges whose reads have side effects).
|
|
67
|
+
- **HARD01-021:** Detect BASIC runtime errors only using actual BASIC error syntax/patterns, not arbitrary occurrences of the word `ERROR`. Test `0 ERRORS FOUND` as successful and a canonical `?SYNTAX ERROR IN 20` as failure.
|
|
68
|
+
|
|
69
|
+
### Background work, artifacts, and SID playback
|
|
70
|
+
|
|
71
|
+
- **HARD01-022:** On reload, persisted tasks marked `running` must never remain zombie status. Either safely re-arm them once execution context is available or mark them `interrupted`/`stopped` with a restart explanation; choose and document one deterministic policy. Test persisted reload and task listing/state.
|
|
72
|
+
- **HARD01-028:** Treat resolved `{ success:false }` operation results as failed task iterations, recording `lastError` and truthful status/failure counts. Decide whether retry continues; if it does, make the continued/retry status explicit. Test both thrown and returned failures.
|
|
73
|
+
- **HARD01-029:** Reject unsafe `runId` and all artifact-derived filenames before writing. Resolve the output base and candidate then prove containment with `path.relative`; reject separators, traversal, absolute paths, empty normalized names, and unsafe range components. Test `../escape`, absolute/sibling-prefix escapes, and verify no out-of-tree file is created.
|
|
74
|
+
- **HARD01-030:** Replace detached `music_generate` playback with tracked, cancellable work. Pin its facade/backend at creation, expose an existing-compatible stop/cancel path (integrate with the background task registry or a dedicated playback registry), stop/cancel on shutdown and before/when backend switches according to a documented policy, and do not monopolize VICE's queue beyond individual note operations. Preserve immediate scheduling response only if it truthfully says playback is scheduled/running rather than completed. Test that switching backends cannot redirect later notes and cancellation prevents future writes.
|
|
75
|
+
- **HARD01-032:** Correct every SID frequency formula to the 24-bit phase accumulator: `round(hz * 16777216 / phi2)` with existing valid 16-bit clamp. Update both production implementations and knowledge/spec text, including PAL and NTSC handling. Test A4/PAL is approximately 7493 (`$1D45`) and that note-on writes low/high bytes correctly.
|
|
76
|
+
- **HARD01-033:** Retrigger the SID envelope between generated notes by writing gate-low for an appropriate bounded interval before the next gate-high/note-on. Do not leave a permanently high gate except where an explicit legato contract asks for it. Test register-write sequence includes gate clearing between successive notes.
|
|
77
|
+
|
|
78
|
+
## Test and validation gate
|
|
79
|
+
|
|
80
|
+
At a minimum, add/extend automated tests that demonstrate every numbered acceptance criterion above. Group them sensibly (config/path, Ultimate mock HTTP, VICE BM stub/lifecycle, program generation/validation, background/SID), but name cases with the HARD01 IDs or titles so coverage is auditable. Run:
|
|
81
|
+
|
|
82
|
+
1. formatter/linter/typecheck and the repository's normal test command;
|
|
83
|
+
2. focused tests for every changed subsystem;
|
|
84
|
+
3. package/build and generated-contract checks when applicable;
|
|
85
|
+
4. a final `git diff --check` and review of all changed public schemas/docs/skills.
|
|
86
|
+
|
|
87
|
+
If an existing test harness cannot safely perform a desired managed-VICE or device-level assertion, retain a deterministic stub/unit test and report the exact unperformed HIL validation as a limitation. Do not weaken a requirement merely because production hardware is unavailable.
|
|
88
|
+
|
|
89
|
+
## Documentation and handoff
|
|
90
|
+
|
|
91
|
+
Update docs, generated metadata, schemas, and `.github/skills` only where behavior, supported platform, failure/recovery behavior, or invocation semantics changed. In particular, keep `AGENTS.md`, README/developer docs, platform-status capabilities, and MCP descriptions consistent with the implementation. Do not edit `review.md` to hide findings.
|
|
92
|
+
|
|
93
|
+
At handoff, provide:
|
|
94
|
+
|
|
95
|
+
- a concise mapping of HARD01 IDs to the implemented change and tests;
|
|
96
|
+
- commands run and their outcomes;
|
|
97
|
+
- any intentionally deferred hardware-only validation, with reason;
|
|
98
|
+
- an explicit statement that all 35 findings were implemented, or a precise list of any remaining IDs and the blocker. Do not claim completion while any HARD01-001..035 finding remains unresolved.
|