@henols/vice-mcp 0.2.3 → 0.2.4

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.
@@ -6,30 +6,27 @@
6
6
  // rebuild.
7
7
  // broker-control.mts
8
8
  //
9
- // N / D-01 (plan 01, tracer): the framing, the token gate, and acquire/
10
- // release. Plan 05 (task 1) completed the message set: recycle, status,
9
+ // The framing, the token gate, acquire/release, recycle, status,
11
10
  // host_state, the arrival-ordered pending-acquire structure, and the
12
- // kernel-enforced singleton guard's low-level bind primitive. THIS PLAN's
13
- // task 2 adds a SEVENTH and EIGHTH op, `monitor_claim`/`monitor_release`
14
- // (BROK-02/PROTO-08, D-13): exclusive ownership of an instance's raw binmon
15
- // socket, enforced here rather than left to a client-side heuristic --
16
- // stock VICE services exactly one binmon client, and a second connect()
17
- // produces no reply and no EOF, so the refusal must happen BEFORE any
18
- // second dial is ever attempted. The subsystem's FIRST network listener: a
19
- // TCP control plane replacing the bash broker's requests/grants/denials/
20
- // leases directory tree entirely. One JSON object per line; the connection
21
- // open IS the claim, connection close IS the release (T-01.6.2-01 through
22
- // -09).
11
+ // kernel-enforced singleton guard's low-level bind primitive. Also adds
12
+ // `monitor_claim`/`monitor_release`: exclusive ownership of an instance's
13
+ // raw binmon socket, enforced here rather than left to a client-side
14
+ // heuristic -- stock VICE services exactly one binmon client, and a second
15
+ // connect() produces no reply and no EOF, so the refusal must happen
16
+ // BEFORE any second dial is ever attempted. The subsystem's FIRST network
17
+ // listener: a TCP control plane replacing the bash broker's
18
+ // requests/grants/denials/leases directory tree entirely. One JSON object
19
+ // per line; the connection open IS the claim, connection close IS the
20
+ // release (T-01.6.2-01 through -09).
23
21
  //
24
- // Wire format confirmed at plan 01's blocking checkpoint:decision
25
- // (2026-08-03, `as-specified`, no amendments -- see .planning/RE-FINDINGS.md
26
- // for the full record, including the two accepted residual risks and the
27
- // unix-domain-socket dead end). Auth: per-boot capability token compared
28
- // constant-time, checked BEFORE any state read or write. Bind: 0.0.0.0
29
- // explicitly, never 127.0.0.1 -- host.docker.internal is the bridge
30
- // address, not loopback, so a loopback-only listener is structurally
31
- // unreachable from the container. Port: 19510 default via
32
- // VICE_BROKER_CONTROL_PORT.
22
+ // Wire format confirmed at a blocking checkpoint decision (2026-08-03,
23
+ // `as-specified`, no amendments), which accepted some residual risk and
24
+ // considered and rejected a unix-domain-socket alternative. Auth: per-boot
25
+ // capability token compared constant-time, checked BEFORE any state read
26
+ // or write. Bind: 0.0.0.0 explicitly, never 127.0.0.1 --
27
+ // host.docker.internal is the bridge address, not loopback, so a
28
+ // loopback-only listener is structurally unreachable from the container.
29
+ // Port: 19510 default via VICE_BROKER_CONTROL_PORT.
33
30
  import { createServer } from "node:net";
34
31
  import { timingSafeEqual, randomBytes } from "node:crypto";
35
32
  /** 32 cryptographically random bytes rendered as hex -- the per-boot
@@ -40,21 +37,20 @@ export function newControlToken() {
40
37
  return randomBytes(32).toString("hex");
41
38
  }
42
39
  const MAX_LINE_BYTES = 65536;
43
- /** CR-03: the one refusal wording for a target-naming op whose `target_id` is
40
+ /** The one refusal wording for a target-naming op whose `target_id` is
44
41
  * not the grant the asking connection itself holds. Deliberately worded as an
45
42
  * authorisation refusal and NOT as an ownership conflict between two
46
43
  * legitimate holders (`monitor_owned`, which names a holder) and never as an
47
44
  * emulator fault -- see attachControlProtocol()'s own ownsTarget() comment,
48
- * and T-02-18's prohibition on wedge/hang vocabulary in this file's
45
+ * and this file's own prohibition on wedge/hang vocabulary in its
49
46
  * monitor-op refusals. */
50
47
  const MONITOR_OWNERSHIP_DENIAL = "monitor_claim/monitor_release may only target the grant this connection itself holds";
51
48
  /** Resolves the `channel` field on a `monitor_claim`/`monitor_release`
52
- * request line (plan 41-03, D-14): an ABSENT field means `binary`
53
- * deliberately -- a broker restarted mid-phase against a client that
54
- * predates this field keeps working (backward compatibility, this plan's
55
- * own must-have). An unrecognised NON-EMPTY value is `bad_request`, never a
56
- * silent fallback and never cast -- the caller below names both accepted
57
- * values in the refusal message. */
49
+ * request line: an ABSENT field means `binary` deliberately -- a broker
50
+ * restarted mid-upgrade against a client that predates this field keeps
51
+ * working (backward compatibility). An unrecognised NON-EMPTY value is
52
+ * `bad_request`, never a silent fallback and never cast -- the caller
53
+ * below names both accepted values in the refusal message. */
58
54
  function resolveMonitorChannel(raw) {
59
55
  if (raw === undefined)
60
56
  return "binary";
@@ -63,8 +59,7 @@ function resolveMonitorChannel(raw) {
63
59
  return "bad_request";
64
60
  }
65
61
  // ---------------------------------------------------------------------------
66
- // Phase 33, plan 33-06 (REPRO-05, D-15, T-33-03/T-33-04): the launch-profile
67
- // narrowing site.
62
+ // The launch-profile narrowing site.
68
63
  //
69
64
  // THIS IS THE ONE PLACE `profile` IS NARROWED. Do not re-derive this check
70
65
  // anywhere else -- not in vice-broker.mts, not in broker-launch.mts, not in
@@ -79,10 +74,11 @@ function resolveMonitorChannel(raw) {
79
74
  //
80
75
  // WHY UNKNOWN KEYS ARE REFUSED BY NAME rather than dropped: a silently
81
76
  // accepted typo means a caller asked for warp, got an unwarped instance, and
82
- // received a confident success. That is the same undetectable-lie failure
83
- // D-16 exists to prevent one layer down, and it is why the message below
84
- // names the offending key -- the by-name unexpected-argument discipline the
85
- // tool handlers already use (RUN_UNTIL_KEYS' own convention).
77
+ // received a confident success -- the same undetectable-lie failure a
78
+ // mismatched grant-and-request eligibility check exists to prevent one
79
+ // layer down, and it is why the message below names the offending key --
80
+ // the by-name unexpected-argument discipline the tool handlers already use
81
+ // (RUN_UNTIL_KEYS' own convention).
86
82
  //
87
83
  // WHAT MUST NEVER BE ADDED HERE: a passthrough string, an `extraArgs`, or any
88
84
  // key whose VALUE reaches argv. `profile` maps to exactly two literal flag
@@ -159,16 +155,15 @@ function writeLine(socket, obj) {
159
155
  socket.write(`${JSON.stringify(obj)}\n`);
160
156
  }
161
157
  }
162
- /** Phase 34, plan 34-01: writes a `host_tool` SUCCESS response line -- the
163
- * object host-tool.mts's runHostTool() produced, whatever shape that is
164
- * (`{ ok: true, ... }` or its own `{ ok: false, message }` refusal). This is
165
- * deliberately NOT `writeLine()`/`ControlResponse`: the host-tool response
166
- * shape is host-tool.mts's own contract, not one more `ControlResponse`
167
- * variant this module would otherwise have to keep in sync with a sibling
168
- * module's allowlist. A REJECTED onHostTool() promise never reaches this
169
- * function -- it is answered through the ordinary `writeLine()`/`error`
170
- * path instead, so every protocol-level failure still goes through one
171
- * shape. */
158
+ /** Writes a `host_tool` SUCCESS response line -- the object host-tool.mts's
159
+ * runHostTool() produced, whatever shape that is (`{ ok: true, ... }` or
160
+ * its own `{ ok: false, message }` refusal). This is deliberately NOT
161
+ * `writeLine()`/`ControlResponse`: the host-tool response shape is
162
+ * host-tool.mts's own contract, not one more `ControlResponse` variant this
163
+ * module would otherwise have to keep in sync with a sibling module's
164
+ * allowlist. A REJECTED onHostTool() promise never reaches this function --
165
+ * it is answered through the ordinary `writeLine()`/`error` path instead,
166
+ * so every protocol-level failure still goes through one shape. */
172
167
  function writeHostToolLine(socket, obj) {
173
168
  if (socket.writable) {
174
169
  socket.write(`${JSON.stringify(obj)}\n`);
@@ -193,9 +188,10 @@ export function enqueueAcquire(queue, entry) {
193
188
  * to stay correct; a genuinely adversarial retry pattern could still starve
194
189
  * an entry across MULTIPLE passes, which is exactly the direct fairness
195
190
  * proof this module deliberately does not author -- injecting N acquires
196
- * and asserting grants return in that order is Phase 01.6.2.1's D-08
197
- * deliverable. The original defect this queue replaces (a lexical iteration
198
- * over `req-<pid>-<ms>-<hex>` filenames) cannot exist here regardless: there
191
+ * and asserting grants return in that order is left as a property for a
192
+ * future test to prove, not this module's own deliverable. The original
193
+ * defect this queue replaces (a lexical iteration over
194
+ * `req-<pid>-<ms>-<hex>` filenames) cannot exist here regardless: there
199
195
  * is no file, and no re-ordering call of any kind anywhere in this region. */
200
196
  export async function drainPendingAcquires(queue) {
201
197
  const snapshot = queue.splice(0, queue.length);
@@ -262,9 +258,9 @@ function attachControlProtocol(server, opts, pendingAcquires) {
262
258
  // every other connection and from the server itself (T-01.6.2-06).
263
259
  });
264
260
  /**
265
- * CR-03 (code review 2026-08-13). THE per-connection ownership predicate
266
- * every target-naming op is gated on -- the same rule `recycle` has
267
- * enforced since T-01.6.2-31, now shared rather than copied.
261
+ * THE per-connection ownership predicate every target-naming op is
262
+ * gated on -- the same rule `recycle` has enforced since this
263
+ * protocol's earliest version, now shared rather than copied.
268
264
  *
269
265
  * Before this existed, `monitor_claim`/`monitor_release` took `target_id`
270
266
  * from the request and passed it straight through, so any connection
@@ -276,8 +272,8 @@ function attachControlProtocol(server, opts, pendingAcquires) {
276
272
  * session B could lock session A out of its own monitor socket, or
277
273
  * RELEASE A's live claim, after which a third client was free to dial the
278
274
  * same single-client binmon socket. That is precisely the unserviced-
279
- * backlog state D-13 exists to prevent and that CLAUDE.md says must never
280
- * be reachable.
275
+ * backlog state this ownership check exists to prevent and that
276
+ * CLAUDE.md says must never be reachable.
281
277
  *
282
278
  * WHAT NOT TO DO: never add another op that acts on a caller-supplied
283
279
  * `target_id` without gating it here first. The grant a connection holds
@@ -294,10 +290,9 @@ function attachControlProtocol(server, opts, pendingAcquires) {
294
290
  * `drainPendingAcquires()` drives, so the two paths can never answer
295
291
  * differently for the same requestId.
296
292
  *
297
- * Gap closure (plan 14, WR-03/T-01.6.2-87/-88): two destroyed-socket
298
- * checks guard a grant against outliving the connection that owns it,
299
- * and they bound TWO DIFFERENT failures -- do not conflate them into one
300
- * claim.
293
+ * Two destroyed-socket checks guard a grant against outliving the
294
+ * connection that owns it, and they bound TWO DIFFERENT failures -- do
295
+ * not conflate them into one claim.
301
296
  *
302
297
  * Half one -- the pre-check immediately below, BEFORE onAcquire() is
303
298
  * ever called -- closes the ALWAYS-REACHABLE leak: a client that
@@ -325,8 +320,8 @@ function attachControlProtocol(server, opts, pendingAcquires) {
325
320
  if (socket.destroyed)
326
321
  return Promise.resolve(true);
327
322
  return opts
328
- // Phase 33, plan 33-06: the profile is threaded through THIS shared
329
- // helper, which both the immediate first attempt and every later
323
+ // The profile is threaded through THIS shared helper, which both
324
+ // the immediate first attempt and every later
330
325
  // drainPendingAcquires() retry go through -- so a request that
331
326
  // queued behind an in-flight launch is retried later with the
332
327
  // profile it was MADE with, never with a profile-less one.
@@ -350,12 +345,12 @@ function attachControlProtocol(server, opts, pendingAcquires) {
350
345
  url: outcome.grant.url,
351
346
  epoch_file: outcome.grant.epochFile,
352
347
  supervisor_dir: outcome.grant.supervisorDir,
353
- // D-15; tightened by plan 41-05 (D-16): key omitted entirely
354
- // when absent -- the fork case only now. A stock grant whose
355
- // second (text-monitor) port allocation failed never reaches
356
- // this line at all: acquirePortAndLaunch() fails the WHOLE
357
- // acquire (`no_free_text_port`) before any grant is produced,
358
- // so "absent" no longer needs to cover that case. Never a
348
+ // Key omitted entirely when absent -- the fork case only now.
349
+ // A stock grant whose second (text-monitor) port allocation
350
+ // failed never reaches this line at all:
351
+ // acquirePortAndLaunch() fails the WHOLE acquire
352
+ // (`no_free_text_port`) before any grant is produced, so
353
+ // "absent" no longer needs to cover that case. Never a
359
354
  // fabricated 0 or null standing in for "no port".
360
355
  ...(outcome.grant.remoteMonitorPort === undefined ? {} : { remote_monitor_port: outcome.grant.remoteMonitorPort }),
361
356
  });
@@ -402,14 +397,14 @@ function attachControlProtocol(server, opts, pendingAcquires) {
402
397
  socket.destroy();
403
398
  return;
404
399
  }
405
- // Phase 34, plan 34-01 (SEAM-01): dispatched FIRST in the chain, before
406
- // "acquire" -- so the ordering reads as the requirement does. Dispatch
407
- // here is on EXACT STRING EQUALITY, never fallthrough, so branch order
408
- // does not itself change which requests reach attemptAcquire() -- what
409
- // actually makes this branch unable to touch lease state is that
410
- // opts.onHostTool is its OWN callback (see StartControlListenerOptions'
411
- // own comment), never composed from onAcquire/onRelease/onRecycle/
412
- // onStatus/onHostState/onMonitorClaim/onMonitorRelease.
400
+ // Dispatched FIRST in the chain, before "acquire" -- so the ordering
401
+ // reads clearly. Dispatch here is on EXACT STRING EQUALITY, never
402
+ // fallthrough, so branch order does not itself change which requests
403
+ // reach attemptAcquire() -- what actually makes this branch unable to
404
+ // touch lease state is that opts.onHostTool is its OWN callback (see
405
+ // StartControlListenerOptions' own comment), never composed from
406
+ // onAcquire/onRelease/onRecycle/onStatus/onHostState/onMonitorClaim/
407
+ // onMonitorRelease.
413
408
  if (req.op === "host_tool") {
414
409
  opts
415
410
  .onHostTool(req)
@@ -425,24 +420,23 @@ function attachControlProtocol(server, opts, pendingAcquires) {
425
420
  }
426
421
  else if (req.op === "acquire") {
427
422
  const requestId = typeof req.id === "string" && req.id !== "" ? req.id : defaultRequestId("req");
428
- // Phase 33, plan 33-06 (T-33-03): narrow BEFORE attemptAcquire, so a
429
- // malformed profile never reaches onAcquire and therefore never
430
- // reaches the port allocator, a spawn, or argv construction. A
431
- // refusal also does NOT enqueue -- the request is answered and
432
- // dropped, never retried on a later drain pass with the same bad
433
- // shape.
423
+ // Narrow BEFORE attemptAcquire, so a malformed profile never
424
+ // reaches onAcquire and therefore never reaches the port allocator,
425
+ // a spawn, or argv construction. A refusal also does NOT enqueue --
426
+ // the request is answered and dropped, never retried on a later
427
+ // drain pass with the same bad shape.
434
428
  const normalised = normaliseLaunchProfile(req.profile);
435
429
  if (!normalised.ok) {
436
430
  writeLine(socket, { kind: "error", code: "bad_request", message: normalised.message });
437
431
  return;
438
432
  }
439
433
  const profile = normalised.profile;
440
- // 33 review WR-03's profile-is-stock-only refusal lived here: it
441
- // refused `profile.warp`/`profile.headless` when this broker's
442
- // resolved backend had no `-warp`/`-console` route at all, so a
443
- // caller learned a knob would be silently ignored rather than
444
- // getting a confident grant with no effect. FORKRM-01 (plan 52-06):
445
- // there is one backend now and it always has that route, so the
434
+ // A profile-is-stock-only refusal used to live here: it refused
435
+ // `profile.warp`/`profile.headless` when this broker's resolved
436
+ // backend had no `-warp`/`-console` route at all, so a caller
437
+ // learned a knob would be silently ignored rather than getting a
438
+ // confident grant with no effect. Now that the fork backend is
439
+ // gone, there is one backend and it always has that route, so the
446
440
  // condition this refused can no longer occur -- deleted rather than
447
441
  // left as a check against a value that can never disagree.
448
442
  void attemptAcquire(requestId, profile).then((settled) => {
@@ -467,7 +461,7 @@ function attachControlProtocol(server, opts, pendingAcquires) {
467
461
  // called, so a mismatched target never reaches the kill discipline
468
462
  // and never signals anything -- an injected signal recorder stays
469
463
  // empty for this case. Now expressed through the SAME ownsTarget()
470
- // predicate monitor_claim/monitor_release use (CR-03), so the three
464
+ // predicate monitor_claim/monitor_release use, so the three
471
465
  // target-naming ops cannot drift apart.
472
466
  if (!ownsTarget(targetId)) {
473
467
  writeLine(socket, {
@@ -538,12 +532,11 @@ function attachControlProtocol(server, opts, pendingAcquires) {
538
532
  writeLine(socket, { kind: "monitor_claimed" });
539
533
  }
540
534
  else if (outcome.code === "monitor_owned") {
541
- // Ownership conflict, named by holder AND channel (plan 41-03,
542
- // D-14) -- deliberately worded to never suggest the emulator
543
- // itself has stopped answering (T-02-18; the plan's own grep gate
544
- // polices this).
535
+ // Ownership conflict, named by holder AND channel -- deliberately
536
+ // worded to never suggest the emulator itself has stopped
537
+ // answering.
545
538
  //
546
- // WR-08 (broker side): `holder` is REQUIRED by MonitorClaimOutcome for
539
+ // `holder` is REQUIRED by MonitorClaimOutcome for
547
540
  // this code, but this handler runs inside socket.on("data") with no
548
541
  // try/catch above it, so a producer that ever omitted it would throw a
549
542
  // TypeError out of the control listener and take the broker process
@@ -6,13 +6,12 @@
6
6
  // rebuild.
7
7
  // broker-epoch.mts
8
8
  //
9
- // B / D-04: the per-instance epoch.json writer, held to the frozen
10
- // eight-field contract captured in fixtures/ (task 1, before the bash
11
- // writer that produced them is deleted later in this phase). Ports
12
- // write_epoch()'s exact field shape and its atomic tmp-sibling-then-rename
13
- // discipline -- the tmp file is created empty, mode tightened to
14
- // owner-read-write BEFORE any content reaches it, content written, then
15
- // renamed -- matching writeBrokerRecord()'s own choke point in
9
+ // The per-instance epoch.json writer, held to the frozen eight-field
10
+ // contract captured in fixtures/ before the bash writer that produced them
11
+ // was deleted. Ports write_epoch()'s exact field shape and its atomic
12
+ // tmp-sibling-then-rename discipline -- the tmp file is created empty, mode
13
+ // tightened to owner-read-write BEFORE any content reaches it, content
14
+ // written, then renamed -- matching writeBrokerRecord()'s own choke point in
16
15
  // vice-broker.mts exactly.
17
16
  //
18
17
  // Plan 03, Task 1 completes this module: the path derivations
@@ -6,7 +6,7 @@
6
6
  // rebuild.
7
7
  // broker-kill.mts
8
8
  //
9
- // D (complete, this plan -- 01.6.2-04): the identity-verified kill discipline,
9
+ // The identity-verified kill discipline,
10
10
  // ported from resources/vice-broker.sh's signal_recorded_pid()/
11
11
  // signal_vice_child_pid(): zero-signal liveness check, identity check against
12
12
  // the process's own argument string, SIGTERM, poll-then-SIGKILL. The
@@ -30,13 +30,13 @@
30
30
  // unconditionally (kill-never-recycle). The uncatchable signals (SIGKILL,
31
31
  // SIGSTOP) are deliberately unhandled -- see registerShutdownHandlers()'s
32
32
  // own comment.
33
- // - reapOrphanedInstances(): the unconditional startup reap (criterion I,
34
- // D-15) that reaches instances this broker process has no in-memory
33
+ // - reapOrphanedInstances(): the unconditional startup reap that reaches
34
+ // instances this broker process has no in-memory
35
35
  // record of, derived from the emulator port band plus this broker's OWN
36
36
  // on-disk allocation record (epoch.json) -- never a host process
37
- // listing or a scan of another process's argv (02-03-PLAN.md/D-14/D-15;
37
+ // listing or a scan of another process's argv --
38
38
  // see reapOrphanedInstances()'s own header comment for the incident
39
- // this revision closes).
39
+ // this revision closes.
40
40
  import { execFileSync } from "node:child_process";
41
41
  import { readFileSync, readdirSync } from "node:fs";
42
42
  import { join } from "node:path";
@@ -80,7 +80,7 @@ function defaultLog(line) {
80
80
  * signal_vice_child_pid() do. An empty/null/non-positive pid, or a pid
81
81
  * already gone, returns "already_exited" without ever signalling -- "the
82
82
  * machine being gone is the goal", per the bash version's own comment. An
83
- * EMPTY expectedIdentity is REFUSED outright (CR-04 -- see the guard's own
83
+ * EMPTY expectedIdentity is REFUSED outright (see the guard's own
84
84
  * comment below), as is a live pid whose OWN argument string does not contain
85
85
  * expectedIdentity: both return "identity_refused" without ever signalling,
86
86
  * the one outcome a caller must be able to tell apart from every other stage
@@ -99,7 +99,7 @@ export async function verifiedKill({ pid, expectedIdentity, deps = {} }) {
99
99
  if (!isAlive(pid)) {
100
100
  return "already_exited";
101
101
  }
102
- // CR-04 (code review 2026-08-13): an EMPTY expectedIdentity REFUSES, it
102
+ // An EMPTY expectedIdentity REFUSES, it
103
103
  // never permits. `"".includes` is vacuously satisfied by every process's
104
104
  // argv, so the guard below was unconditionally true for the empty string --
105
105
  // which disabled it entirely and let the caller SIGTERM (then SIGKILL)
@@ -294,18 +294,19 @@ export function registerShutdownHandlers(deps) {
294
294
  }
295
295
  };
296
296
  }
297
- /** D-25's mandatory start-time banner: printed unconditionally, before the
297
+ /** The mandatory start-time banner: printed unconditionally, before the
298
298
  * control listener begins accepting, naming exactly what a keyboard
299
299
  * interrupt or a closed terminal destroys. On 2026-08-02 a `^C` produced
300
300
  * "reap saw 4 recorded instance(s), terminated 4" and killed a live
301
301
  * session -- the incident was not caused by missing machinery, it was
302
302
  * caused by nobody being told. Detaching stays the operator's own
303
- * nohup/setsid/systemd choice (D-25) -- this banner names that choice
303
+ * nohup/setsid/systemd choice -- this banner names that choice
304
304
  * rather than offering a flag; the launcher stays thin.
305
305
  *
306
- * D-25/P-13 (01.6.2.1-05-PLAN.md): the one place naming the retired
307
- * warm-floor environment variable does not weaken D-10/D-11's clean break --
308
- * the line added below reports the variable's mere PRESENCE, never its
306
+ * The one place naming the retired
307
+ * warm-floor environment variable does not weaken the clean break made when
308
+ * the warm floor was retired -- the line added below reports the variable's
309
+ * mere PRESENCE, never its
309
310
  * value, and no reader anywhere in this broker still consults it (the
310
311
  * structural gate in broker-kill.test.ts proves that). Without it, an
311
312
  * operator with the retired variable set in a shell profile would silently
@@ -324,9 +325,9 @@ export function startupBanner() {
324
325
  "vice-broker: to run this broker outside the current terminal session, use your own",
325
326
  "vice-broker: nohup/setsid/systemd -- this launcher does not offer a --detach flag.",
326
327
  ];
327
- if (process.env.VICE_BROKER_SPARES !== undefined) { // banner-only presence check (D-25/P-13) -- never reads the value
328
+ if (process.env.VICE_BROKER_SPARES !== undefined) { // banner-only presence check -- never reads the value
328
329
  lines.push(
329
- // Plan 41-05 (folded todo): this note's own former "use the warm-floor
330
+ // This note's own former "use the warm-floor
330
331
  // knob instead" replacement is ITSELF retired along with the warm
331
332
  // floor -- pointing an operator at a second dead knob would be worse
332
333
  // than pointing at none. VICE now launches strictly on demand, on the
@@ -336,21 +337,18 @@ export function startupBanner() {
336
337
  return lines.join("\n");
337
338
  }
338
339
  // ============================================================================
339
- // Startup reap: unconditional, file-free... but no longer PROCESS-TABLE-free
340
- // (criterion I, D-15, as revised by 02-03-PLAN.md/D-14/D-15).
340
+ // Startup reap: unconditional, file-free... but no longer PROCESS-TABLE-free.
341
341
  //
342
- // 02-03-PLAN.md (BROK-03) retires this section's entire former identity
343
- // mechanism -- the two functions it lived in are gone from this tree
342
+ // This section's entire former identity
343
+ // mechanism is retired -- the two functions it lived in are gone from this tree
344
344
  // outright, not merely unused -- which used to select kill targets by
345
345
  // scanning EVERY host process's own argument string for a plain substring
346
346
  // match on the configured emulator binary path, gated only by "some bare
347
- // integer token >= basePort appears somewhere in that same string" -- folded
348
- // todo
349
- // `.planning/todos/pending/2026-08-12-broker-orphan-reap-substring-identity-match.md`,
347
+ // integer token >= basePort appears somewhere in that same string". This was
350
348
  // observed killing two unrelated orchestrator shell processes on a
351
349
  // developer's host (a long scratchpad path supplied the qualifying integer;
352
- // a short VICE_BIN like `/bin/sleep` supplied the substring). D-15 replaces
353
- // that heuristic with the broker's OWN allocation record: this reap now
350
+ // a short VICE_BIN like `/bin/sleep` supplied the substring). The replacement
351
+ // uses the broker's OWN allocation record: this reap now
354
352
  // enumerates the instance directories under `stateDir` (which THIS broker,
355
353
  // or a same-machine predecessor, created) and kills only the pid each
356
354
  // directory's own epoch.json actually recorded launching. A host process
@@ -430,8 +428,9 @@ function bumpEpochForInstanceDir(deps, stateDir, port) {
430
428
  };
431
429
  deps.writeEpochRecord({ supervisorDir, record });
432
430
  }
433
- /** The unconditional startup reap (criterion I, D-15, kill-target identity
434
- * revised by 02-03-PLAN.md/D-14/D-15). Runs on every broker start, before the
431
+ /** The unconditional startup reap (kill-target identity
432
+ * revised to use the broker's own allocation record instead of a process-table
433
+ * scan). Runs on every broker start, before the
435
434
  * control listener accepts and before anything is launched -- unconditional
436
435
  * because a broker killed with SIGKILL never runs a shutdown path, so "was
437
436
  * the last shutdown clean" is unanswerable, and a marker file recording that
@@ -441,14 +440,14 @@ function bumpEpochForInstanceDir(deps, stateDir, port) {
441
440
  * Enumerates the on-disk instance directories under `stateDir` in the
442
441
  * allocation band (`port >= basePort` -- the 6510-6599 range below it stays
443
442
  * reserved by convention for an emulator a human launched for their own
444
- * work, D-18), and for each one reads its OWN `epoch.json` -- never a host
443
+ * work), and for each one reads its OWN `epoch.json` -- never a host
445
444
  * process listing, never an argv scan. A directory whose record is absent,
446
445
  * unparseable, or carries no finite positive `pid` contributes nothing to
447
446
  * `found`/`killed` and is skipped by the kill half entirely, but the epoch
448
447
  * bump below still runs for it: a registry-free restart must still void
449
448
  * every in-band instance directory it finds, including one it has no usable
450
- * pid for, which is the exact case this seed
451
- * (.planning/seeds/broker-restart-reaps-and-voids.md) flags -- the void has
449
+ * pid for, which is the exact case a registry-free restart must handle --
450
+ * the void has
452
451
  * to reach instances a registry-free restart never heard of. A record that
453
452
  * DOES carry a usable pid AND a non-empty `vice_bin` is killed via
454
453
  * verifiedKill() with `expectedIdentity` set to THAT record's own `vice_bin`
@@ -456,7 +455,7 @@ function bumpEpochForInstanceDir(deps, stateDir, port) {
456
455
  * does not match what THIS broker itself recorded launching there is refused
457
456
  * (`identity_refused`), exactly like every other verifiedKill() call site in
458
457
  * this module. A record carrying a usable pid but NO `vice_bin` is NOT a kill
459
- * candidate at all (CR-04): it contributes nothing to `found`/`killed`, the
458
+ * candidate at all: it contributes nothing to `found`/`killed`, the
460
459
  * kill dep is never invoked, and only the epoch bump runs -- an unidentifiable
461
460
  * pid is refused, never killed on the strength of the pid alone.
462
461
  *
@@ -484,7 +483,7 @@ export async function reapOrphanedInstances(options) {
484
483
  if (typeof pid === "number" && Number.isFinite(pid) && pid > 0) {
485
484
  const expectedIdentity = typeof epochFields?.vice_bin === "string" ? epochFields.vice_bin : "";
486
485
  if (expectedIdentity === "") {
487
- // CR-04: a record with a usable pid but NO recorded identity is not a
486
+ // A record with a usable pid but NO recorded identity is not a
488
487
  // kill candidate at all -- it is not counted in `found` and the kill
489
488
  // dep is never invoked. verifiedKill() refuses an empty identity too
490
489
  // (second layer, deliberately: removing either leaves the other