@henols/vice-mcp 0.2.1 → 0.2.3
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 +4 -3
- package/THIRD-PARTY-NOTICES.md +423 -25
- package/{r2000-acme-ident.ts → anno-acme-ident.ts} +13 -13
- package/anno-bank.ts +171 -0
- package/anno-cli.ts +3040 -0
- package/{r2000-confidence.ts → anno-confidence.ts} +22 -22
- package/anno-coverage.ts +2465 -0
- package/anno-derive.ts +590 -0
- package/anno-details.ts +169 -0
- package/anno-enum-gen.ts +919 -0
- package/anno-export-asm.ts +2396 -0
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-index.ts +150 -0
- package/anno-join.ts +480 -0
- package/{r2000-memmap-render.ts → anno-memmap-render.ts} +236 -95
- package/anno-provenance-ledger.ts +472 -0
- package/{r2000-regbits-gen.ts → anno-regbits-gen.ts} +20 -15
- package/{r2000-regbits.json → anno-regbits.json} +2 -2
- package/anno-register.ts +399 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +4002 -0
- package/anno-symbols.ts +266 -0
- package/anno-tools.ts +3264 -0
- package/anno-types.ts +1909 -0
- package/backend-detect.mts +124 -312
- package/block-class.ts +201 -0
- package/build.ts +4 -2
- package/capture-predicate.ts +597 -0
- package/channel-lock.ts +349 -0
- package/disasm-decoder.ts +14 -14
- package/disasm-opcodes.ts +4 -4
- package/disasm-renderer.ts +2 -2
- package/evid-ingest.ts +217 -0
- package/evid-reconcile.ts +316 -0
- package/host-tool-client.ts +430 -0
- package/hostpath.ts +1 -1
- package/incident-record.ts +23 -12
- package/install-resources.ts +30 -14
- package/memmap-lookup.ts +285 -0
- package/package.json +48 -23
- package/prg-image.ts +118 -0
- package/repo-root.ts +107 -8
- package/resources/backend-detect.mjs +98 -236
- package/resources/broker-control.mjs +189 -16
- package/resources/broker-epoch.mjs +1 -1
- package/resources/broker-kill.mjs +8 -2
- package/resources/broker-launch.mjs +373 -214
- package/resources/broker-state.mjs +64 -18
- package/resources/container-guard.mjs +1 -1
- package/resources/ghidra-project.mjs +790 -0
- package/resources/host-tool.mjs +2561 -0
- package/resources/vice-broker.mjs +330 -184
- package/resources/vice-launcher.sh +130 -12
- package/stock-address.ts +6 -6
- package/stock-cia.ts +2 -2
- package/stock-condition.ts +8 -8
- package/stock-connect.ts +10 -6
- package/stock-derived.ts +29 -37
- package/stock-diagnose.ts +200 -36
- package/stock-dispatch.ts +200 -68
- package/stock-execution.ts +5 -3
- package/stock-handler.ts +1 -1
- package/stock-input.ts +9 -9
- package/stock-machine.ts +17 -6
- package/stock-paths.ts +18 -14
- package/stock-petscii.ts +1 -1
- package/stock-protocol.ts +17 -12
- package/stock-recycle.ts +83 -2
- package/stock-registers.ts +54 -29
- package/stock-reproducible-run.ts +811 -0
- package/stock-run-until.ts +100 -1
- package/stock-sprites.ts +3 -3
- package/stock-symbols.ts +13 -13
- package/stock-timing.ts +2 -2
- package/stock-vicii.ts +1 -1
- package/stop-oracle.ts +167 -0
- package/text-capability-probe.ts +660 -0
- package/text-connect.ts +157 -0
- package/text-protocol.ts +810 -0
- package/text-tools.ts +778 -0
- package/textmon-backtrace.ts +385 -0
- package/textmon-cpuhistory.ts +335 -0
- package/textmon-memmap.ts +494 -0
- package/textmon-profile.ts +458 -0
- package/textmon-registers.ts +748 -0
- package/tools-manifest.stock.json +864 -3
- package/version.ts +1 -1
- package/vice-broker-client.ts +189 -42
- package/vice-errors.ts +268 -0
- package/vice-proxy.ts +392 -2175
- package/vsf-slice.ts +640 -0
- package/capability-registry.ts +0 -388
- package/r2000-cli.ts +0 -1103
- package/r2000-d64.ts +0 -310
- package/r2000-enum-gen.ts +0 -574
- package/r2000-launch.ts +0 -357
- package/r2000-mcp-client.ts +0 -596
- package/r2000-project.ts +0 -190
- package/r2000-symbols.ts +0 -388
- package/r2000-tools.ts +0 -914
- package/r2000-verify.ts +0 -184
- package/refresh-manifest.ts +0 -124
- package/tools-manifest.json +0 -1223
- package/vice-probe.ts +0 -278
- package/vice-sync.ts +0 -336
- package/vice.ts +0 -772
package/stock-dispatch.ts
CHANGED
|
@@ -24,10 +24,13 @@
|
|
|
24
24
|
// one place a tools/call for the stock backend is routed from.
|
|
25
25
|
// - Never acquire a broker lease here (Task 2's own ensureStockSession()
|
|
26
26
|
// header comment explains this prohibition fully).
|
|
27
|
+
// - Never acquire channel-lock.ts's mutex anywhere but inside
|
|
28
|
+
// withChannelLockHeld() (plan 41-02, CHAN-04) -- that is the ONE acquire
|
|
29
|
+
// site on the binary side, and it wraps the delegated HANDLER call only,
|
|
30
|
+
// never the session-acquisition preamble above it.
|
|
27
31
|
import { resolve, join } from "node:path";
|
|
28
32
|
|
|
29
|
-
import type {
|
|
30
|
-
import type { ToolInfo } from "./vice.ts";
|
|
33
|
+
import type { ToolInfo } from "./vice-errors.ts";
|
|
31
34
|
import { type HeldLease } from "./vice-broker-client.ts";
|
|
32
35
|
import { stockConnect, stockDisconnect, stockReconnect, type StockConnectSession, type StockConnectDeps } from "./stock-connect.ts";
|
|
33
36
|
import {
|
|
@@ -42,6 +45,7 @@ import {
|
|
|
42
45
|
} from "./stock-handler.ts";
|
|
43
46
|
import { attachRunStateTracker } from "./stock-runstate.ts";
|
|
44
47
|
import { STOCK_DERIVED_TOOLS, type DerivedPureHandler } from "./stock-derived.ts";
|
|
48
|
+
import { acquireChannelLock, ChannelLockTimeoutError } from "./channel-lock.ts";
|
|
45
49
|
|
|
46
50
|
// The six family modules (plans 03-06 through 03-11) -- each exports its
|
|
47
51
|
// tools as StockSessionHandler-shaped values; this file (D-09) is the ONE
|
|
@@ -71,6 +75,7 @@ import { handleCyclesStopwatch, forgetTimingForOtherTargets } from "./stock-timi
|
|
|
71
75
|
import { handleRunUntil } from "./stock-run-until.ts";
|
|
72
76
|
import { handleDiagnoseStock } from "./stock-diagnose.ts";
|
|
73
77
|
import { handleRecycleStock } from "./stock-recycle.ts";
|
|
78
|
+
import { handleDeviceConsole, handleWarpSet, handleMemmapShow, handleMemmapZap, handleCpuHistory, handleProfileFlat, handleBacktrace, handleIoRegisters } from "./text-tools.ts";
|
|
74
79
|
|
|
75
80
|
// Re-exported so Phase 2's existing import surface (and its 921-line test
|
|
76
81
|
// file) keeps working unchanged -- these four names used to be DEFINED
|
|
@@ -85,21 +90,26 @@ export type { StockToolResult, StockOkResult, StockErrorResult };
|
|
|
85
90
|
// ---------------------------------------------------------------------------
|
|
86
91
|
|
|
87
92
|
/**
|
|
88
|
-
* Resolves
|
|
93
|
+
* Resolves the one manifest file backing the stock backend's advertised tool
|
|
89
94
|
* surface, following the EXACT override precedence vice-proxy.ts's own
|
|
90
95
|
* manifestPath() already establishes: an explicit VICE_TOOLS_MANIFEST value
|
|
91
96
|
* (passed in as `envOverride`, never read from process.env directly here --
|
|
92
|
-
* this function stays a pure, injectable seam) wins
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
+
* this function stays a pure, injectable seam) wins, unchanged; otherwise the
|
|
98
|
+
* committed default file beside `hereDir` is used. `envOverride` is
|
|
99
|
+
* deliberately a plain parameter, not a process.env read, so this function
|
|
100
|
+
* has no hidden global dependency and a test can drive both cases without
|
|
101
|
+
* mutating the real environment.
|
|
102
|
+
*
|
|
103
|
+
* FORKRM-05 (plan 52-07): this used to take a `backend` parameter selecting
|
|
104
|
+
* between two committed manifest files -- there is one manifest now, so the
|
|
105
|
+
* parameter is gone rather than pinned to a literal no caller could ever
|
|
106
|
+
* vary.
|
|
97
107
|
*/
|
|
98
|
-
export function manifestPathForBackend(
|
|
108
|
+
export function manifestPathForBackend(hereDir: string, envOverride: string | undefined): string {
|
|
99
109
|
if (envOverride) {
|
|
100
110
|
return resolve(envOverride);
|
|
101
111
|
}
|
|
102
|
-
return
|
|
112
|
+
return join(hereDir, "tools-manifest.stock.json");
|
|
103
113
|
}
|
|
104
114
|
|
|
105
115
|
/**
|
|
@@ -107,44 +117,38 @@ export function manifestPathForBackend(backend: ViceBackend, hereDir: string, en
|
|
|
107
117
|
* is a name-keyed record: whichever assignment to a given key runs LAST
|
|
108
118
|
* wins, and vice-proxy.ts's own registration order used to assign
|
|
109
119
|
* RECYCLE_TOOL/DIAGNOSE_TOOL's literal (fork-worded) definitions
|
|
110
|
-
* UNCONDITIONALLY,
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* corrected stock manifest entry sat right there in
|
|
120
|
+
* UNCONDITIONALLY, straight after the manifest loop had already populated
|
|
121
|
+
* the same keys correctly. `tools/list` served the fork's five-verdict
|
|
122
|
+
* vocabulary -- including `stale_read_path`, which stock cannot produce
|
|
123
|
+
* (D-03) -- and omitted `monitor_held_elsewhere`, which stock can, even
|
|
124
|
+
* though the corrected stock manifest entry sat right there in
|
|
116
125
|
* tools-manifest.stock.json, unread. This function is the ONE place that
|
|
117
126
|
* decision is now made, so the manifest loop's own per-tool selection and
|
|
118
127
|
* the two synthetic tools' registration agree.
|
|
119
128
|
*
|
|
120
|
-
* Behaviour:
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* (T-07-16-02).
|
|
130
|
-
* - NEVER merges fields from the two definitions. Picking one whole
|
|
131
|
-
* definition keeps `description`, `inputSchema` and `outputSchema`
|
|
132
|
-
* internally consistent; a field-by-field merge could pair a fork
|
|
133
|
-
* description with a stock `outputSchema`, or the reverse.
|
|
129
|
+
* Behaviour: returns the `manifestTools` entry whose `name` equals
|
|
130
|
+
* `syntheticDef.name`, if one exists. Falls back to `syntheticDef` when no
|
|
131
|
+
* match exists -- `readManifestTools()`'s own malformed/unreadable-manifest
|
|
132
|
+
* fallbacks answer `[]`, and in that case the proxy must still advertise a
|
|
133
|
+
* WORKING tool rather than none at all (T-07-16-02). NEVER merges fields
|
|
134
|
+
* from the two definitions -- picking one whole definition keeps
|
|
135
|
+
* `description`, `inputSchema` and `outputSchema` internally consistent; a
|
|
136
|
+
* field-by-field merge could pair one definition's description with the
|
|
137
|
+
* other's `outputSchema`.
|
|
134
138
|
*
|
|
135
139
|
* Declared as a `function`, not a `const` arrow, per this module tree's own
|
|
136
140
|
* standing rule: stock-dispatch.ts <-> stock-diagnose.ts <-> stock-recycle.ts
|
|
137
141
|
* form a runtime import cycle, and the phase already reproduced a live
|
|
138
142
|
* `ReferenceError` from a `const` handler export sitting in that cycle.
|
|
143
|
+
*
|
|
144
|
+
* FORKRM-05 (plan 52-07): this used to take a `backend` parameter and
|
|
145
|
+
* return `syntheticDef` unchanged when it was `"fork"` -- vice-proxy.ts's
|
|
146
|
+
* two call sites always passed the literal `"stock"`, so that branch was
|
|
147
|
+
* dead from the moment plan 52-06 collapsed backend detection. Removed
|
|
148
|
+
* rather than left as an unreachable branch a reader could mistake for live
|
|
149
|
+
* code.
|
|
139
150
|
*/
|
|
140
|
-
export function resolveAdvertisedToolDefinition(
|
|
141
|
-
syntheticDef: ToolInfo,
|
|
142
|
-
backend: ViceBackend,
|
|
143
|
-
manifestTools: ToolInfo[],
|
|
144
|
-
): ToolInfo {
|
|
145
|
-
if (backend === "fork") {
|
|
146
|
-
return syntheticDef;
|
|
147
|
-
}
|
|
151
|
+
export function resolveAdvertisedToolDefinition(syntheticDef: ToolInfo, manifestTools: ToolInfo[]): ToolInfo {
|
|
148
152
|
const manifestEntry = manifestTools.find((t) => t.name === syntheticDef.name);
|
|
149
153
|
return manifestEntry ?? syntheticDef;
|
|
150
154
|
}
|
|
@@ -185,8 +189,8 @@ export type LeaseProvider = () => Promise<{ ok: true; lease: HeldLease | null }
|
|
|
185
189
|
* never implies resolution it did not achieve. It is a plain string handed down from vice-proxy.ts's
|
|
186
190
|
* OWN single, module-scope call to `resolvedBackend()` (see that file's own
|
|
187
191
|
* "resolve the active backend once" discipline) -- this module must never
|
|
188
|
-
* call `resolvedBackend()
|
|
189
|
-
*
|
|
192
|
+
* call `resolvedBackend()` itself, per backend-detect.mts's own "do not
|
|
193
|
+
* call this per tool or per call" prohibition. Omitted entirely
|
|
190
194
|
* (never expected in production) falls back to an empty string rather than
|
|
191
195
|
* throwing.
|
|
192
196
|
*/
|
|
@@ -201,6 +205,12 @@ export interface StockDispatchDeps {
|
|
|
201
205
|
* Omitted defaults to `false` -- the honest answer when nothing said
|
|
202
206
|
* otherwise. */
|
|
203
207
|
resolvedBinaryPathIsResolved?: boolean;
|
|
208
|
+
/** Test-only override of channel-lock.ts's acquire bound for THIS call's
|
|
209
|
+
* withChannelLockHeld() wrapping. Production call sites never set this --
|
|
210
|
+
* they always take channel-lock.ts's own CHANNEL_LOCK_ACQUIRE_TIMEOUT_MS
|
|
211
|
+
* default. Exists so a test can observe a ChannelLockTimeoutError (and its
|
|
212
|
+
* refusal text) without waiting out the real ~630-second default. */
|
|
213
|
+
channelLockTimeoutMs?: number;
|
|
204
214
|
}
|
|
205
215
|
|
|
206
216
|
export type EnsureStockSessionOutcome = { ok: true; session: StockConnectSession } | { ok: false; message: string };
|
|
@@ -421,7 +431,7 @@ export async function ensureStockSession(deps: StockDispatchDeps): Promise<Ensur
|
|
|
421
431
|
* come from the lease vice-proxy.ts built (see HeldLease's own field comments
|
|
422
432
|
* for why they are two DIFFERENT directories), and `binPath` is the same
|
|
423
433
|
* already-settled `resolvedBinaryPath` vice_ping reports -- this module must
|
|
424
|
-
* never call resolvedBackend()
|
|
434
|
+
* never call resolvedBackend() itself.
|
|
425
435
|
*
|
|
426
436
|
* An empty string is treated as ABSENT rather than passed through: the two
|
|
427
437
|
* consumers both branch on truthiness, and passing "" would key a capability
|
|
@@ -461,6 +471,53 @@ export { stockDisconnect };
|
|
|
461
471
|
* prohibits). */
|
|
462
472
|
export type StockHandler = (args: Record<string, unknown>, deps: StockDispatchDeps) => Promise<StockToolResult>;
|
|
463
473
|
|
|
474
|
+
/**
|
|
475
|
+
* withChannelLockHeld -- acquires channel-lock.ts's mutex for
|
|
476
|
+
* `channel: "binary"` around `fn`, releasing in a `finally` so a throwing
|
|
477
|
+
* `fn` still releases (D-05). This is the ONE acquire site on the binary
|
|
478
|
+
* side; withStockSession() and withDerivedTool()'s `needsSession: true`
|
|
479
|
+
* branch both call it, and neither may acquire the lock any other way.
|
|
480
|
+
*
|
|
481
|
+
* This is what makes the lock's critical section span a whole LOGICAL
|
|
482
|
+
* operation, not a single wire command: `vice_run_until`'s wait
|
|
483
|
+
* (stock-run-until.ts's `waitForCheckpointHit()`) and the reproducible-run
|
|
484
|
+
* path's wait (stock-reproducible-run.ts's `waitForReproducibleStop()`,
|
|
485
|
+
* reached through `runReproducible()`) both run INSIDE the wrapped `fn`, so
|
|
486
|
+
* the lock stays held across resume -> wait -> observe without either wait
|
|
487
|
+
* path being re-cut.
|
|
488
|
+
*
|
|
489
|
+
* FORBIDDEN ALTERNATIVE, named here because it is the obvious-looking wrong
|
|
490
|
+
* design: acquiring and releasing this lock around each individual wire
|
|
491
|
+
* command instead of around the whole handler call. A per-wire-command lock
|
|
492
|
+
* preserves the resume count while destroying what the count protects -- a
|
|
493
|
+
* foreign command (e.g. a text-channel command) can land in the gap between
|
|
494
|
+
* "resume sent" and "checkpoint observed", halting a machine that was
|
|
495
|
+
* supposed to be running toward the checkpoint, so the checkpoint never
|
|
496
|
+
* fires even though no protocol invariant was technically violated per
|
|
497
|
+
* command (41-RESEARCH.md Pitfall 6).
|
|
498
|
+
*
|
|
499
|
+
* A `ChannelLockTimeoutError` is converted into refusal text using the
|
|
500
|
+
* error's OWN message verbatim -- it is already `channelLockRefusalMessage()`'s
|
|
501
|
+
* output -- and NEVER routed through `convertWireError()`, which would
|
|
502
|
+
* re-frame a legitimate ownership statement as a wire fault.
|
|
503
|
+
*/
|
|
504
|
+
async function withChannelLockHeld(toolName: string, timeoutMs: number | undefined, fn: () => Promise<StockToolResult>): Promise<StockToolResult> {
|
|
505
|
+
let handle;
|
|
506
|
+
try {
|
|
507
|
+
handle = await acquireChannelLock({ channel: "binary", operation: toolName, timeoutMs });
|
|
508
|
+
} catch (err) {
|
|
509
|
+
if (err instanceof ChannelLockTimeoutError) {
|
|
510
|
+
return isErrorText(err.message);
|
|
511
|
+
}
|
|
512
|
+
throw err;
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
return await fn();
|
|
516
|
+
} finally {
|
|
517
|
+
handle.release();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
464
521
|
/**
|
|
465
522
|
* withStockSession -- THE ONE adapter every STOCK_DISPATCH_TABLE entry goes
|
|
466
523
|
* through (Task 1, plan 03-12). Before this existed, `viceHandlerPing` was
|
|
@@ -488,6 +545,17 @@ export type StockHandler = (args: Record<string, unknown>, deps: StockDispatchDe
|
|
|
488
545
|
* Code for the rest of the session (T-3-04) -- a single escaped
|
|
489
546
|
* exception here would silently end the session's entire tool surface,
|
|
490
547
|
* not just this one call.
|
|
548
|
+
*
|
|
549
|
+
* Step 3's own try/catch runs INSIDE withChannelLockHeld(toolName, ...)
|
|
550
|
+
* (plan 41-02, CHAN-04): the session-acquisition step above (step 1/2) is
|
|
551
|
+
* NOT covered by the lock -- only the delegated handler call is -- so a
|
|
552
|
+
* broker liveness classification or a handshake failure never queues behind
|
|
553
|
+
* the OTHER channel's halt authority. This is what makes D-05's critical
|
|
554
|
+
* section span a whole logical operation: `vice_run_until`'s and the
|
|
555
|
+
* reproducible-run path's waits run inside their handlers, so the lock is
|
|
556
|
+
* held across resume -> wait -> observe without either wait path being
|
|
557
|
+
* re-cut (see withChannelLockHeld()'s own header for the forbidden
|
|
558
|
+
* per-wire-command alternative).
|
|
491
559
|
*/
|
|
492
560
|
export function withStockSession(toolName: string, handler: StockSessionHandler): StockHandler {
|
|
493
561
|
return async (args, deps) => {
|
|
@@ -502,11 +570,13 @@ export function withStockSession(toolName: string, handler: StockSessionHandler)
|
|
|
502
570
|
return isErrorText(outcome.message);
|
|
503
571
|
}
|
|
504
572
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
573
|
+
return withChannelLockHeld(toolName, deps.channelLockTimeoutMs, async () => {
|
|
574
|
+
try {
|
|
575
|
+
return await handler(args, outcome.session, deps);
|
|
576
|
+
} catch (err) {
|
|
577
|
+
return convertWireError(toolName, err);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
510
580
|
};
|
|
511
581
|
}
|
|
512
582
|
|
|
@@ -530,7 +600,14 @@ export function withStockSession(toolName: string, handler: StockSessionHandler)
|
|
|
530
600
|
* calls ensureStockSession() at all -- not a lighter-weight variant of it
|
|
531
601
|
* (04-RESEARCH.md Pitfall 3) -- and invokes `handler(args, deps)` inside a
|
|
532
602
|
* single try/catch converting through convertWireError(), so the
|
|
533
|
-
* never-throw boundary still holds.
|
|
603
|
+
* never-throw boundary still holds. This branch also NEVER acquires
|
|
604
|
+
* channel-lock.ts's mutex (plan 41-02, CHAN-04): taking halt authority for a
|
|
605
|
+
* pure client-side computation that never touches the wire would block a
|
|
606
|
+
* REAL halting operation for no reason. `vice_diagnose` is also registered
|
|
607
|
+
* `needsSession: false` and therefore also does not acquire here -- its own
|
|
608
|
+
* handler takes the lock with `tryAcquireChannelLock()` (channel-lock.ts)
|
|
609
|
+
* instead, deliberately, so that diagnosing contention never queues behind
|
|
610
|
+
* the holder it is diagnosing (plan 41-04).
|
|
534
611
|
*
|
|
535
612
|
* `needsSession: true` runs the EXACT same three-step preamble
|
|
536
613
|
* withStockSession() runs, reusing the same imported converters -- never a
|
|
@@ -538,7 +615,11 @@ export function withStockSession(toolName: string, handler: StockSessionHandler)
|
|
|
538
615
|
* inside its own try/catch -> convertHandshakeError(toolName, err); a
|
|
539
616
|
* `{ ok: false }` outcome returns outcome.message verbatim through
|
|
540
617
|
* isErrorText(), never re-worded; otherwise handler(args, outcome.session, deps)
|
|
541
|
-
* inside a SECOND try/catch -> convertWireError(toolName, err)
|
|
618
|
+
* inside a SECOND try/catch -> convertWireError(toolName, err), with that
|
|
619
|
+
* second try/catch running inside withChannelLockHeld(toolName, ...) --
|
|
620
|
+
* exactly the same wrapping withStockSession() applies, and for the same
|
|
621
|
+
* reason (see that function's own comment on the forbidden per-wire-command
|
|
622
|
+
* alternative).
|
|
542
623
|
*/
|
|
543
624
|
export function withDerivedTool(toolName: string, opts: { needsSession: true }, handler: StockSessionHandler): StockHandler;
|
|
544
625
|
export function withDerivedTool(toolName: string, opts: { needsSession: false }, handler: DerivedPureHandler): StockHandler;
|
|
@@ -571,11 +652,13 @@ export function withDerivedTool(
|
|
|
571
652
|
return isErrorText(outcome.message);
|
|
572
653
|
}
|
|
573
654
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
655
|
+
return withChannelLockHeld(toolName, deps.channelLockTimeoutMs, async () => {
|
|
656
|
+
try {
|
|
657
|
+
return await (handler as StockSessionHandler)(args, outcome.session, deps);
|
|
658
|
+
} catch (err) {
|
|
659
|
+
return convertWireError(toolName, err);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
579
662
|
};
|
|
580
663
|
}
|
|
581
664
|
|
|
@@ -591,7 +674,22 @@ export function withDerivedTool(
|
|
|
591
674
|
* comment on why). Built through stockAnswer() so the answer now also
|
|
592
675
|
* carries `runState` (D-06: every stock answer, and `vice_ping` is a stock
|
|
593
676
|
* answer) alongside every field that was already there.
|
|
677
|
+
*
|
|
678
|
+
* 2026-08-19 finding (closed Phase 15 plan 15-09): `resolvedBinaryPath` is a
|
|
679
|
+
* ONE-TIME, MCP-server-process-startup `$PATH` probe (see vice-proxy.ts's
|
|
680
|
+
* `ACTIVE_BACKEND` comment) -- it is independent of which binary the broker
|
|
681
|
+
* actually leased for THIS request. `resolvedBinaryPathScope` below is an
|
|
682
|
+
* additive, backward-compatible sibling field (the existing field name and
|
|
683
|
+
* shape are unchanged) that carries that qualification into the answer
|
|
684
|
+
* itself, so a caller reading the response -- not just this source comment --
|
|
685
|
+
* learns not to treat the path as this request's authoritative binary
|
|
686
|
+
* identity.
|
|
594
687
|
*/
|
|
688
|
+
const RESOLVED_BINARY_PATH_SCOPE =
|
|
689
|
+
"one-time MCP-server-process-startup PATH probe; NOT the binary the broker leased for this " +
|
|
690
|
+
"request -- for the authoritative per-instance binary, read the broker's own launch record " +
|
|
691
|
+
"(epoch.json's vice_bin field)";
|
|
692
|
+
|
|
595
693
|
const handlePing: StockSessionHandler = async (_args, session, deps) => {
|
|
596
694
|
return stockAnswer(session.client, {
|
|
597
695
|
status: "ok",
|
|
@@ -602,6 +700,9 @@ const handlePing: StockSessionHandler = async (_args, session, deps) => {
|
|
|
602
700
|
// it, an agent reading `"x64sc"` cannot tell "this is where the binary is"
|
|
603
701
|
// from "this is what we were told to look for, and we could not find it".
|
|
604
702
|
resolvedBinaryPathIsResolved: deps.resolvedBinaryPathIsResolved ?? false,
|
|
703
|
+
// 2026-08-19 finding: names what resolvedBinaryPath actually is (a
|
|
704
|
+
// startup-time probe) and where to look instead for a per-request answer.
|
|
705
|
+
resolvedBinaryPathScope: RESOLVED_BINARY_PATH_SCOPE,
|
|
605
706
|
capabilities: session.capabilities,
|
|
606
707
|
});
|
|
607
708
|
};
|
|
@@ -611,11 +712,11 @@ const handlePing: StockSessionHandler = async (_args, session, deps) => {
|
|
|
611
712
|
* not an oversight (Task 2, plan 03-12):
|
|
612
713
|
* - `vice_checkpoint_set_ignore_count` (D-15)
|
|
613
714
|
* - `vice_snapshot_list` (D-16 -- deleted from both manifests)
|
|
614
|
-
* - `vice_disk_detach` (
|
|
615
|
-
* - `vice_joystick_tap` (
|
|
715
|
+
* - `vice_disk_detach` (CUT from scope 2026-08-17 -- docs/stock-vice-parity.md's dated cut record)
|
|
716
|
+
* - `vice_joystick_tap` (permanently excluded -- stock-input.ts's own header)
|
|
616
717
|
* - `vice_disk_read_sector` (CUT from scope 2026-08-17 -- no skill calls it; see ROADMAP.md "Cut from scope (v0.2.0, 2026-08-17)" and docs/stock-vice-parity.md item 6)
|
|
617
718
|
* - `vice_sid_get_state` and the low-level keyboard family (hard losses)
|
|
618
|
-
* - `vice_machine_config_get` / `vice_machine_config_set` (
|
|
719
|
+
* - `vice_machine_config_get` / `vice_machine_config_set` (CUT from scope 2026-08-17 along with the whole of Stock-Only Gains -- docs/stock-vice-parity.md's dated cut record)
|
|
619
720
|
* `dispatchStock()`'s miss branch already refuses any of these by name,
|
|
620
721
|
* without reading `deps` -- there is nothing else to add for them here.
|
|
621
722
|
*/
|
|
@@ -694,9 +795,9 @@ const STOCK_DISPATCH_TABLE: Record<string, StockHandler> = {
|
|
|
694
795
|
vice_run_until: withDerivedTool("vice_run_until", { needsSession: true }, handleRunUntil),
|
|
695
796
|
|
|
696
797
|
// derived (TIME-04) -- the two proxy-local synthetic tools (RECYCLE_TOOL/
|
|
697
|
-
// DIAGNOSE_TOOL in vice-proxy.ts),
|
|
698
|
-
//
|
|
699
|
-
// transport. Deliberate asymmetry, documented at this call site (see also
|
|
798
|
+
// DIAGNOSE_TOOL in vice-proxy.ts), registered via buildViceTool() and
|
|
799
|
+
// routed to this table's own dispatchStock() entry point, never to the
|
|
800
|
+
// deleted fork transport. Deliberate asymmetry, documented at this call site (see also
|
|
700
801
|
// DerivedPureHandler's amended doc comment in stock-derived.ts):
|
|
701
802
|
// vice_diagnose uses needsSession:false because its own handler acquires
|
|
702
803
|
// the session itself (inside its own try/catch) so it can convert a
|
|
@@ -707,6 +808,30 @@ const STOCK_DISPATCH_TABLE: Record<string, StockHandler> = {
|
|
|
707
808
|
// evidence and has no verdict vocabulary of its own to preserve.
|
|
708
809
|
vice_diagnose: withDerivedTool("vice_diagnose", { needsSession: false }, handleDiagnoseStock),
|
|
709
810
|
vice_recycle: withDerivedTool("vice_recycle", { needsSession: true }, handleRecycleStock),
|
|
811
|
+
|
|
812
|
+
// text-channel remedy tools (plan 41-06, CHAN-03). needsSession:false,
|
|
813
|
+
// deliberately -- NOT withStockSession()/withDerivedTool(needsSession:
|
|
814
|
+
// true): both of those wrap the whole handler call in
|
|
815
|
+
// withChannelLockHeld("binary", ...), and each handler below takes its OWN
|
|
816
|
+
// channel-lock.ts acquire for `channel: "text"` internally
|
|
817
|
+
// (withTextChannelLock(), text-protocol.ts). Registering through either
|
|
818
|
+
// binary-locking adapter would nest a second acquireChannelLock() call
|
|
819
|
+
// inside the first (channel-lock.ts is one single, non-reentrant mutex
|
|
820
|
+
// across both channels) -- a self-deadlock that only resolves by expiring
|
|
821
|
+
// CHANNEL_LOCK_ACQUIRE_TIMEOUT_MS. See text-tools.ts's own header comment
|
|
822
|
+
// (ADAPTER CHOICE) for the full measurement. Neither handler needs a
|
|
823
|
+
// binary session at all -- both resolve the lease via deps.ensureLease()
|
|
824
|
+
// themselves and dial only through textConnect() -- so needsSession:false
|
|
825
|
+
// is not merely the deadlock-avoiding choice, it is also the structurally
|
|
826
|
+
// correct one, matching vice_diagnose's own precedent above.
|
|
827
|
+
vice_device_console: withDerivedTool("vice_device_console", { needsSession: false }, handleDeviceConsole),
|
|
828
|
+
vice_warp_set: withDerivedTool("vice_warp_set", { needsSession: false }, handleWarpSet),
|
|
829
|
+
vice_memmap_show: withDerivedTool("vice_memmap_show", { needsSession: false }, handleMemmapShow),
|
|
830
|
+
vice_memmap_zap: withDerivedTool("vice_memmap_zap", { needsSession: false }, handleMemmapZap),
|
|
831
|
+
vice_cpu_history: withDerivedTool("vice_cpu_history", { needsSession: false }, handleCpuHistory),
|
|
832
|
+
vice_profile_flat: withDerivedTool("vice_profile_flat", { needsSession: false }, handleProfileFlat),
|
|
833
|
+
vice_backtrace: withDerivedTool("vice_backtrace", { needsSession: false }, handleBacktrace),
|
|
834
|
+
vice_io_registers: withDerivedTool("vice_io_registers", { needsSession: false }, handleIoRegisters),
|
|
710
835
|
};
|
|
711
836
|
|
|
712
837
|
/** Looks up the table entry for `name` -- `undefined` on a miss, never a
|
|
@@ -721,20 +846,27 @@ export function stockHandlerFor(name: string): StockHandler | undefined {
|
|
|
721
846
|
/**
|
|
722
847
|
* The ONE dispatch entry point for the stock backend (D-09). On a hit,
|
|
723
848
|
* delegates to the table entry, unchanged. On a miss, refuses EXPLICITLY --
|
|
724
|
-
* naming the tool
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
849
|
+
* naming the tool and stating there is no dispatch entry for it -- WITHOUT
|
|
850
|
+
* reading `deps` at all (no lease is ever requested for a tool that does not
|
|
851
|
+
* exist on this backend). There is no third branch, and in particular NO
|
|
852
|
+
* fall-through to the fork's HTTP-forwarding path anywhere in this file or
|
|
853
|
+
* anything it calls -- that is D-09's whole point, grep-gated to zero
|
|
854
|
+
* occurrences of that function's name in this file's own code lines.
|
|
855
|
+
*
|
|
856
|
+
* FORKRM-05 (plan 52-07): this used to fall back to the deleted per-backend
|
|
857
|
+
* capability registry's refusal renderer first, naming the fork as the
|
|
858
|
+
* backend that provides the tool -- that renderer, and the fork it named,
|
|
859
|
+
* are both gone. A name reaching this branch is advertised on the stock
|
|
860
|
+
* manifest (so it passed vice-proxy.ts's own lookup) but has no dispatch
|
|
861
|
+
* entry: that is always an internal inconsistency now, never a capability
|
|
862
|
+
* gap with a second backend to point a caller at.
|
|
731
863
|
*/
|
|
732
864
|
export async function dispatchStock(name: string, args: Record<string, unknown>, deps: StockDispatchDeps): Promise<StockToolResult> {
|
|
733
865
|
const handler = stockHandlerFor(name);
|
|
734
866
|
if (!handler) {
|
|
735
867
|
return isErrorText(
|
|
736
|
-
`${name} is
|
|
737
|
-
`
|
|
868
|
+
`${name} is advertised on the stock backend's manifest but has no handler in the stock ` +
|
|
869
|
+
`dispatch table -- this is an internal inconsistency, not a capability gap; please file an issue.`,
|
|
738
870
|
);
|
|
739
871
|
}
|
|
740
872
|
return handler(args, deps);
|
package/stock-execution.ts
CHANGED
|
@@ -224,9 +224,11 @@ export const handleExecutionRun: StockSessionHandler = async (args, session) =>
|
|
|
224
224
|
* the derived run state is "unknown" (D-07, via refuseIfUnknown()).
|
|
225
225
|
*
|
|
226
226
|
* `stepOver: true`'s runtime semantic (skip a JSR's subroutine as one step)
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
227
|
+
* was live-probed against a real JSR on fork VICE 3.10 and CONFIRMED
|
|
228
|
+
* (RESEARCH.md Assumptions Log row A2, closed by 13-PROBE-RESULTS.md §A2):
|
|
229
|
+
* the post-step PC landed at JSR+3, matching a full subroutine skip, and
|
|
230
|
+
* the result was reproduced identically across two independent live
|
|
231
|
+
* sessions.
|
|
230
232
|
*/
|
|
231
233
|
export const handleExecutionStep: StockSessionHandler = async (args, session) => {
|
|
232
234
|
const unexpected = refuseUnexpectedArgs(args, ["count", "stepOver"], "vice_execution_step");
|
package/stock-handler.ts
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
// time, so it creates no runtime cycle even though stock-dispatch.ts
|
|
37
37
|
// imports this file at runtime.
|
|
38
38
|
import { MonitorOwnershipError } from "./vice-broker-client.ts";
|
|
39
|
-
import { MachineRestartedError } from "./vice.ts";
|
|
39
|
+
import { MachineRestartedError } from "./vice-errors.ts";
|
|
40
40
|
import { ErrorCode, StockFramingError, StockProtocolError, StockResponseMismatchError, type ViceMonitorClient } from "./stock-protocol.ts";
|
|
41
41
|
import { runStateFor } from "./stock-runstate.ts";
|
|
42
42
|
import type { StockConnectSession } from "./stock-connect.ts";
|
package/stock-input.ts
CHANGED
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
// resumes explicitly, on its own schedule.
|
|
26
26
|
// - Never add vice_joystick_tap. A tap needs the machine to RUN for a
|
|
27
27
|
// measured interval -- an unrequested EXIT (forbidden by D-05) plus a
|
|
28
|
-
// frame/cycle measurement
|
|
29
|
-
//
|
|
30
|
-
// vice_joystick_set (hold/release/centre)
|
|
31
|
-
// joystick half
|
|
28
|
+
// frame/cycle measurement stock's wire protocol has no route for at
|
|
29
|
+
// all (docs/stock-vice-parity.md section A item 7). This exclusion is
|
|
30
|
+
// permanent, not pending: vice_joystick_set (hold/release/centre)
|
|
31
|
+
// satisfies DIRECT-07's joystick half instead.
|
|
32
32
|
// - Never construct an ok-answer outside stockAnswer(). Every successful
|
|
33
33
|
// result below is built through it, never a bare
|
|
34
34
|
// `{ content: [...], isError: false }` literal.
|
|
@@ -187,11 +187,11 @@ function isValidDirection(value: string): value is JoystickDirection {
|
|
|
187
187
|
* the fork's exact names and documented string-or-array shape.
|
|
188
188
|
*
|
|
189
189
|
* vice_joystick_tap is deliberately absent from this module (and the whole
|
|
190
|
-
* stock manifest) -- a tap needs the machine to run for a
|
|
191
|
-
* interval, which is an unrequested EXIT (forbidden by D-05) plus
|
|
192
|
-
* frame/cycle measurement stock
|
|
193
|
-
*
|
|
194
|
-
*
|
|
190
|
+
* stock manifest), permanently -- a tap needs the machine to run for a
|
|
191
|
+
* measured interval, which is an unrequested EXIT (forbidden by D-05) plus
|
|
192
|
+
* a frame/cycle measurement stock's wire protocol has no route for at all
|
|
193
|
+
* (docs/stock-vice-parity.md section A item 7). Do not approximate it with
|
|
194
|
+
* a sleep; do not implement it here.
|
|
195
195
|
*/
|
|
196
196
|
export const handleJoystickSet: StockSessionHandler = async (args, session) => {
|
|
197
197
|
if (!isPlainObject(args)) {
|
package/stock-machine.ts
CHANGED
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
// WHAT NOT TO DO:
|
|
13
13
|
// - Never gate or deny vice_machine_reset's hard mode. CLAUDE.md's
|
|
14
14
|
// power-cycle warning is about RESOURCE_SET (0x52) writes to
|
|
15
|
-
// MachineVideoStandard/VICIIModel/MachinePowerFrequency --
|
|
16
|
-
//
|
|
15
|
+
// MachineVideoStandard/VICIIModel/MachinePowerFrequency -- the CUT
|
|
16
|
+
// vice_machine_config_get/set pair's resources (docs/stock-vice-parity.md), a DIFFERENT opcode entirely. RESET (0xcc) is a distinct
|
|
17
17
|
// command, and an agent-requested hard reset via RESET is exactly what
|
|
18
18
|
// DIRECT-06 asks for. It needs no deny-list (RESEARCH.md Pitfall 1).
|
|
19
19
|
// - Never look for a per-unit disk-attach route mid-implementation.
|
|
20
20
|
// AUTOSTART (0xdd) has NO drive-unit field on the wire at all -- this is
|
|
21
21
|
// a protocol gap, not a code bug you can fix by looking harder
|
|
22
22
|
// (RESEARCH.md Pitfall 2).
|
|
23
|
-
// - Never add a disk-detach handler here. D-13
|
|
24
|
-
//
|
|
25
|
-
// in this file's own acceptance criteria.
|
|
23
|
+
// - Never add a disk-detach handler here. D-13's vice_disk_detach was
|
|
24
|
+
// CUT from scope 2026-08-17 (docs/stock-vice-parity.md) -- grep-gated
|
|
25
|
+
// to zero occurrences of its name in this file's own acceptance criteria.
|
|
26
26
|
// - Never build a host path outside stock-paths.ts. Every filename this
|
|
27
27
|
// file sends through the wire goes through that same one wrapper --
|
|
28
28
|
// grep-gated to zero direct hostPath()/hostPathCandidates() calls here.
|
|
@@ -150,7 +150,18 @@ export const handleAutostart: StockSessionHandler = async (args, session) => {
|
|
|
150
150
|
* at all, so an agent told "attached to unit 9" when the image landed on
|
|
151
151
|
* unit 8 would debug the wrong drive. See docs/stock-vice-parity.md's D-14
|
|
152
152
|
* entry.
|
|
153
|
+
*
|
|
154
|
+
* The returned `approximation` string names BOTH real side effects Phase 13
|
|
155
|
+
* plan 13-03's live A5 probe observed against real fork VICE 3.10: a full
|
|
156
|
+
* machine reset AND a program load, not "attach without disturbing machine
|
|
157
|
+
* state." Exported so the pinning test derives its expectation from this
|
|
158
|
+
* constant rather than re-typing the sentence, so the two cannot drift. See
|
|
159
|
+
* `.planning/phases/13-external-verification/13-PROBE-RESULTS.md` § A5.
|
|
153
160
|
*/
|
|
161
|
+
export const DISK_ATTACH_APPROXIMATION =
|
|
162
|
+
"AUTOSTART (D-14): performs a full machine reset and loads a program from the image; " +
|
|
163
|
+
"unlike vice_autostart it does not issue a final run step, as far as observed.";
|
|
164
|
+
|
|
154
165
|
export const handleDiskAttach: StockSessionHandler = async (args, session) => {
|
|
155
166
|
const a = isPlainObject(args) ? args : {};
|
|
156
167
|
|
|
@@ -182,7 +193,7 @@ export const handleDiskAttach: StockSessionHandler = async (args, session) => {
|
|
|
182
193
|
unit: 8,
|
|
183
194
|
path: containerPath,
|
|
184
195
|
sentPath,
|
|
185
|
-
approximation:
|
|
196
|
+
approximation: DISK_ATTACH_APPROXIMATION,
|
|
186
197
|
});
|
|
187
198
|
} catch (err) {
|
|
188
199
|
return convertWireError("vice_disk_attach", err);
|
package/stock-paths.ts
CHANGED
|
@@ -17,11 +17,14 @@
|
|
|
17
17
|
// side finds this comment.
|
|
18
18
|
//
|
|
19
19
|
// WHAT NOT TO DO:
|
|
20
|
-
// - Never
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// is
|
|
20
|
+
// - Never merge this file's emulator-side translation with, or replace it
|
|
21
|
+
// by, a general argument-rewriting pass applied before dispatch. Stock
|
|
22
|
+
// tool calls no longer go through any such pass -- vice-proxy.ts's own
|
|
23
|
+
// fork-only per-call path-rewriter (and the generic forwarding function
|
|
24
|
+
// that ran it) is deleted outright -- and reintroducing one would
|
|
25
|
+
// re-create exactly the inversion this file's header names: what a
|
|
26
|
+
// general rewriter does for a client-side-derived path is precisely
|
|
27
|
+
// wrong for the four emulator-side filenames this file translates.
|
|
25
28
|
// - Never build a host path with a local heuristic (a hand-rolled prefix
|
|
26
29
|
// swap, a hardcoded mount guess, anything not routed through
|
|
27
30
|
// hostpath.ts's own hostPathCandidates()/tryHostPaths()). hostpath.ts is
|
|
@@ -35,8 +38,8 @@
|
|
|
35
38
|
// through withEmulatorSidePath() should stop and re-read this paragraph.
|
|
36
39
|
import { dirname, join } from "node:path";
|
|
37
40
|
|
|
38
|
-
import { ViceError, type ViceErrorOptions } from "./vice.ts";
|
|
39
|
-
import { repoRoot } from "./repo-root.ts";
|
|
41
|
+
import { ViceError, type ViceErrorOptions } from "./vice-errors.ts";
|
|
42
|
+
import { repoRoot, toolsDir } from "./repo-root.ts";
|
|
40
43
|
import { isInsideContainer } from "./container-guard.mts";
|
|
41
44
|
import { tryHostPaths } from "./hostpath.ts";
|
|
42
45
|
import { ErrorCode, StockProtocolError } from "./stock-protocol.ts";
|
|
@@ -167,21 +170,22 @@ export function sanitizeSnapshotName(name: unknown): string {
|
|
|
167
170
|
|
|
168
171
|
/**
|
|
169
172
|
* The container path a snapshot named `name` lives at:
|
|
170
|
-
* `<
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
173
|
+
* `<toolsDir>/snapshots/<name>.vsf` -- a subdirectory of the single
|
|
174
|
+
* tool-written root `repo-root.ts`'s `toolsDir()` owns (D-33). The directory
|
|
175
|
+
* is inside the workspace rather than under `~/.config/vice/` (the fork's own
|
|
176
|
+
* location) because only a workspace path is inside hostpath.ts's bind-mount
|
|
177
|
+
* mapping -- anything outside it cannot be translated for the host at all --
|
|
178
|
+
* and keeping it inside the workspace makes workspace escape structurally
|
|
175
179
|
* impossible rather than merely checked (T-3-05).
|
|
176
180
|
*/
|
|
177
181
|
export function snapshotPathFor(name: string): string {
|
|
178
|
-
return join(
|
|
182
|
+
return join(toolsDir(), "snapshots", `${sanitizeSnapshotName(name)}.vsf`);
|
|
179
183
|
}
|
|
180
184
|
|
|
181
185
|
/** The sidecar metadata path for the same snapshot: same directory, `.json`
|
|
182
186
|
* extension, same sanitisation. */
|
|
183
187
|
export function snapshotMetaPathFor(name: string): string {
|
|
184
|
-
return join(
|
|
188
|
+
return join(toolsDir(), "snapshots", `${sanitizeSnapshotName(name)}.json`);
|
|
185
189
|
}
|
|
186
190
|
|
|
187
191
|
// Re-exported so a caller building a directory before translating (Task 3's
|
package/stock-petscii.ts
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
// bytes (0x40/0x41, 0x5a/0x5b, 0x60/0x61, 0x7a/0x7b) and the control-code
|
|
33
33
|
// regions do not follow that rule uniformly; each range below is
|
|
34
34
|
// checked explicitly, not derived from a single arithmetic shortcut.
|
|
35
|
-
import { ViceError } from "./vice.ts";
|
|
35
|
+
import { ViceError } from "./vice-errors.ts";
|
|
36
36
|
|
|
37
37
|
/** PETSCII's Return code. Both ASCII LF (`\n`) and CR (`\r`) map here -- this
|
|
38
38
|
* is what the fork's own "Use \n for Return" tool description promises. */
|