The third audit cleared the four P0s and the npm-fallback change, and rejected
2.0.15 on one point: the Frida gate hashed AFTER it imported. `verify()` — which
imports frida and enumerates devices — ran before `judgeBinding()` compared the
digest, so a mismatched native module was executed inside the provisioning process
and then reported as rejected (the audit measured `moduleImportedBeforeRejection:
true` on a Linux copy). Writing `available: false` afterwards does not undo an
import, and the shim does not consume that field either, so nothing stopped REA
from loading the same file on the next tool call.
- `inspect()` is now the first thing that touches the binding: path + SHA-256, no
import. `verify()` only runs when the digest matches the recorded one, or when
this platform has no expectation at all (then `verified: false`, as before).
- after `npm rebuild frida`, the result is inspected BEFORE it is imported too — a
rebuild can produce a binding that exists, loads, and is not the measured one.
- a mismatch now MOVES the file to `frida_binding.node.rejected` instead of only
recording a verdict: REA reaches `build/frida_binding.node` directly, so taking it
off that path is the only thing that actually stops the load. Quarantined rather
than deleted, so the mismatch stays inspectable; if the move itself fails the
message says so and tells the operator to remove the file by hand.
- the message is now "moved it to … before any import, so nothing can load it",
which is what the code does — the old "rather than loading an unverified native
module" was the line 2.0.15 could not back up.
Two regression tests, both injecting a frida package whose `index.js` writes a
marker and then throws, so "was this module imported?" is a fact on disk:
- the mismatched binding is never imported (no marker, output says "before any
import");
- the binding is gone from the load path and present as `.rejected`.
Both skip on targets without a pinned digest — the only measured one is
`17.23.2/win32-x64` — and the skip message says the reason is "no expectation to
mismatch", not that the platform does not matter.
Tests: 160 (157 pass / 0 fail / 3 skipped). audit 24/24 hash-pinned clean,
dependency gate clean, host-compat 16 covered, verify-branch ALL CHECKS PASSED.
On this machine `--reuse` reports `fridaBindings — FRIDA_OK 3 — digest matches the
recorded one`, the Ghidra smoke imports java.exe in 31 s, exit 0.
Still open, unchanged from the last round: the shim rewrites the lock from its
parent process without a token comparison (low-priority race, not release
blocking), and Frida has a pinned digest for win32-x64 only.
A second audit of 5c71bf8 found 4 release-blocking gaps, all reproduced, all real.
Two are things 2.0.14 left undone (one of them documented as done), two are
defects 2.0.14 itself introduced. The common shape this time is the mirror of the
first round: the fixed path was not the path a user actually walks — rollback only
in the multi-file branch, closure floors only after a fresh install, the Frida
verdict written once, the old closure deleted before the new one was verified.
P0 — left undone by 2.0.14
- a plain archive had no rollback: the 2.0.14 note claimed "renamed to .backup,
rolled back if the probe fails", but that lived only in the multi-file branch and
`archive` still did rm -> rename -> return on a failed probe. Measured: exit 1,
old known-good tree gone, broken tree in place, no .backup. Both branches now
share the staging -> backup -> swap -> probe -> drop contract, and the message
distinguishes "put back", "parked at", "nothing to restore".
- the advisory fix never reached existing installs: the `recorded` branch and the
"already vendored" branch both returned before any closure check, so a 2.0.13 ->
2.0.14 upgrade printed `already provisioned (6.8.0)` with zod still 4.4.3.
`closureAdvisories()` reads both node_modules roots and runs before BOTH early
returns; below a floor it reinstalls, and offline it fails instead of reporting
health. Verified on this machine, which was in exactly that state: it reported
zod 4.4.3 < 4.6.6, minimatch 10.2.6 < 11.0.1, glob 13.0.6 < 14.0.2, reinstalled
from the audited lock to 4.6.7 / 11.0.5 / 14.0.4, and the closure has been
vulnerable-free since.
P0 — introduced by 2.0.14
- the Frida verdict was cached forever: gated on `frida === undefined`, one
transient failure became permanent (every later run read `available: false` and
skipped), and a binding deleted after a good record was never re-checked. The
check now runs on every non-dry-run.
- the swap deleted the old closure before verifying the new one: `.retired` was
removed one line after the rename, so a closure that renamed fine and then could
not start left the machine with no usable REA. `retired` now survives until
BOTH `--version` and `doctor --format json` answer, and `restorePreviousClosure()`
puts it back otherwise. doctor's verdict is "produced parseable JSON", not
"healthy" — it exits non-zero for any unconfigured optional capability.
P1
- `npm ci` no longer degrades silently to an unlocked install: that fallback traded
byte-reproducibility for a green run. It now fails closed with the reason, and
`--allow-unlocked-npm-fallback` is the explicit opt-in.
- Frida binding digests: FRIDA_BINDING_SHA256 holds the ONE measured value
(17.23.2/win32-x64). Match -> verified: true; no expectation for this platform ->
usable but verified: false and the step says the digest was recorded, not
checked; MISMATCH -> Frida disabled rather than loading an unmeasured native
module. "We hashed what we got" is no longer reported as a verified chain.
P2
- the run lock is only deleted or rewritten when it still carries this run's token
(or pid); otherwise it is left alone, because removing somebody else's lock is
how a live run loses its brake.
Also: the offline reinstall message now names which of the three reasons applies
instead of calling all of them "not usable".
Tests: 158 (155 pass / 0 fail / 3 skipped), including the four fault injections the
audit asked for — archive probe failure restores the old install, a recorded 6.8.0
with zod 4.4.3 is not reported as provisioned, a stale frida verdict is re-checked,
and the swap ordering contract (retired outlives both probes). audit 24/24
hash-pinned clean, dependency gate clean, host-compat 16 covered, verify-branch
ALL CHECKS PASSED.
An external audit of 2.0.13 found that install state could lie: a partially
failed install was recorded as a success, a broken recorded path was vouched for
by a different working copy, and an expired lock was deleted out from under a
live process. Every finding shares one shape — the thing verified was not the
thing being delivered — so the fix in each case is to make the probe look at the
artefact that will actually be used, and to keep "cannot prove" distinct from
"proved false". All 18 items are addressed. No upstream component moved: REA
6.8.0 / Ghidra 12.1.4 / JDK 21 / apktool 3.0.3 / jadx 0.7.1 / Node 24.22.1.
Provisioning (scripts/setup.mjs)
- multi-file components are all-or-nothing: every entry must arrive, files are
staged in <target>.staging and swapped in atomically only when complete, the
previous directory is kept as .backup and restored if the swap or the probe
fails, and the record carries one {id,version,sha256} per entry with the
primary chosen by kind instead of by manifest order
- a recorded component is probed AT its recorded path (independent resolve
context) and its version is checked against the manifest pin; JDK matches by
major floor; an enabled Ghidra gate override is honoured so a plain setup can
no longer silently re-pin over one
- JSON state files are written through writeJsonAtomic(); an unparseable
setup.lock is replaced instead of throwing; --dry-run writes nothing at all
- Frida's native binding is repaired by naming the one package whose install
script is load-bearing (npm rebuild frida) and then proved with
import('frida') + enumerateDevices(); checked in main, so existing bundles
(i.e. every upgrade) get fixed too, and reported as skip rather than failing
- the tar fallback passes filter='data' to Python's extractall
Runtime (bin/rea-shim.mjs)
- one definition of bundleUsable = loaded.ok && resolveRea(...).ok drives the
launch decision, the --shim-status exit code and the repair path; an unusable
bundle no longer wins against a working system REA, and status returns 78
- liveness outranks age for a running lock: a live pid is never deleted, an
expired one is reported as in-flight-stale; locks with no pid keep the old
time-based rule; setup heartbeats `at` from log(); shim->setup handoff uses a
token (REA_BUNDLE_LOCK_TOKEN) instead of a pid
Dependencies (scripts/vendor-lock/, scripts/check-dependency-advisories.mjs)
- REA pins zod exactly at 4.4.3, which sits inside GHSA-mrhx-c7xc-m3w5; the
closure is now replayed from an audited lock with npm ci and overrides to zod
4.6.7 / minimatch 11.0.5 / glob 14.0.4, verified afterwards against 4.6.6
- an offline gate fails the build if the lock drifts back, and CI runs it
Host adaptation (scripts/host-compat.mjs)
- HOST_RELEASES 10 -> 16: 0.2.2-rc.1, 0.2.2-alpha.3, 0.2.1-rc.5, 0.2.2-alpha.2,
0.2.1-rc.4, 0.2.1-rc.3, all three-source verified and in release-date order;
engines.dsh and OUTSIDE_HOSTS unchanged
Also fixed while verifying (not in the audit)
- a bare-major pin ("21") was treated as a family prefix, so 25.0.5 counted as
drift while setup counted it as satisfied; it is a floor, in all three files
- check-updates.ps1 no longer reports a false red: `rea doctor` exits non-zero
for any unconfigured optional capability, so that exit code is now [upstream]
and the parsed provider checks decide
- two new JSON reads went around the BOM-tolerant reader; routed through
readJsonSafe
Tests: 154 (151 pass / 0 fail / 3 skipped), audit 24/24 hash-pinned clean,
dependency gate clean, host-compat 16 covered, verify-branch ALL CHECKS PASSED.
Restarting dsh with the 2.0.12 copy still in the profile re-provisioned
the bundle to 2.0.12's pin: REA went back to 6.7.0, the Ghidra gate
override went with it, and the MCP row failed with "REA could not start".
That is the card doing its job on a machine whose plugin copy was stale —
but the card never said the aligned run also wipes the override.
- client.js: third note in both languages. It names the override case and
the command that restores it, so the card is not advice that silently
undoes a deliberate choice.
- README (both): the "you must run setup" block says the same thing.
- ghidra-gate-override.mjs: judge a recorded override by the files on disk.
Re-provisioning replaces rea-agents while leaving the record behind, so
--enable now replaces a stale record instead of refusing, and --disable
counts already-lost files instead of throwing.
- CHANGELOG: 2.0.13 records both, plus the failure that prompted them.
Verified after re-aligning this machine: setup --reuse reached
"setup complete" with its Ghidra smoke importing java.exe in 28 s on 12.2,
the shim resolves REA 6.8.0, and the MCP path through the shim returns the
same segments / procedure inventory / entry pseudocode as the 12.1.4
baseline with provider.version = 12.2.
Gates: audit 24/24 hash-pinned clean, 88 tests (85 pass / 3 skipped),
verify-branch ALL CHECKS PASSED, host-compat 10 covered / 5 outside.
The four CVEs in the chain are Ghidra 12.1.4's and are fixed in 12.2, and
12.2 is exactly the release REA 6.8.0 refuses: SUPPORTED_GHIDRA_VERSION is
still 12.1.4, and an out-of-line build withdraws the whole Ghidra family.
Measured today, so the decision rests on evidence rather than on the gate:
Ghidra 12.2 (ghidra_12.2_PUBLIC_20261016.zip, 569,712,708 B, sha256
2c2cd776... matching the release body) against 6.8.0's own bridge produced
the same 9 segments, the same 341-procedure inventory (74,483 chars) and a
byte-identical `entry` pseudocode as the 12.1.4 baseline, through the shim,
with provider.version = 12.2. The gate blocks a working combination; it does
not describe a broken one.
scripts/ghidra-gate-override.mjs therefore makes that choice explicit,
recorded and reversible: it rewrites only the constant in the vendored copy
(three files), writes what it changed to <bundle>/ghidra-gate-override.json,
and --disable restores the original values. Re-provisioning rea-agents
removes the patch, so the default after any setup run is the pinned,
verified combination again. 12.2 needs JDK 25+ (application.java.min=25),
which the adopted Corretto 25.0.5 satisfies here but the pinned Temurin 21
would not.
check-updates.ps1 now reads that record and reports "local override" instead
of advising a setup re-run, which is what would have silently undone it.
Gates: audit 24/24 hash-pinned clean, 88 tests (85 pass / 3 skipped),
verify-branch ALL CHECKS PASSED.
Also recorded: editing check-updates.ps1 stripped its UTF-8 BOM, which makes
PowerShell 5.1 read it as ANSI and fail to parse at all; the BOM is restored
and the note is in docs/project-notes.md.
REA 6.8.0 (npm latest, 2026-10-25) is a tool expansion, not a patch: 229
MCP tools instead of 207 (+22: APK signatures, binary diagnostics, model
artifacts, CUDA/HIP code objects), 175 CLI commands, 6 prompts, and a
shipped dist/mcp-catalog.json that records the counts and three digests.
It also raises engines.node to ^22.23.4 || ^24.22.1 || >=26.12.1, which
makes the pinned Node 24.22.1 exactly the floor of its line. The pin,
package.json engines and the node probe therefore move together: a probe
has no version comparator, so the range is one alternation verified
against 24 versions (22.23.3, 24.22.0 and 26.12.0 must be rejected).
Nothing else moved. 6.8.0 still declares SUPPORTED_GHIDRA_VERSION 12.1.4
(12.2 stays unsupported) and JADX_RELEASE 0.7.1 with the same sha256, and
Adoptium still publishes no Windows build of 21.0.13 — so Ghidra, jadx,
JDK, apktool, platform-tools and Node keep their pins.
Measured here, not inferred: setup --reuse reported cli=6.8.0 tools=229
and its Ghidra smoke imported java.exe in 26 s; an independent stdio MCP
handshake answered serverInfo 6.8.0 with 229 tools / 6 prompts on
protocol 2024-11-05; executing 6.8.0's release policy gave 12.1.4
verified / 12.1.5 compatible / 12.2 unsupported; apktool 3.0.3, adb
37.0.1 and the 0.7.1 jar hash all re-checked. Gates: audit 24/24
hash-pinned clean, 88 unit tests (85 pass, 3 skipped), verify-branch
ALL CHECKS PASSED, host-compat 10 covered / 5 outside.
Docs: both READMEs (versions, tool counts, engines sentence, tag example),
docs/bundle-design.md and the new docs/update-review-2026-10-26.md, which
also records the npm/proxy trap that made the first provisioning run fail
with ECONNRESET, and why WSL needs no separate update.
The paragraph below it already states what is pinned, why 12.2 / 0.8.0
are refused, and what the trade-off is; the one-line lead was redundant
and read as a judgement rather than a fact. No other change.
Adds docs/update-review-2026-10-24.md and one paragraph to both READMEs,
directly under the "which version is this" block.
Measured findings:
- Ghidra 12.2 is published (2026-10-22), but GhidraReleasePolicy.js accepts
the 12.1 line only: 12.2 resolves to "unsupported" and the whole Ghidra
decompile/analysis group disappears rather than degrading.
- jadx-headless-mcp 0.8.0 passes class admission but is rejected by
JadxSession.js:61, which requires the engine to report 0.7.1. Only
BuildInfo and JadxSession differ from 0.7.1; the other bridge classes are
byte-identical, so the break is the version assertion, not ABI.
- Temurin 21.0.13+9 ships 204 assets and none for Windows, so win32-x64 has
nothing to point at; it also removes only 3 of the 4 eMudhra anchors.
- REA 6.7.0, Node 24.22.1, apktool 3.0.3 and the platform-tools bytes are
already the newest upstream.
No versions pinned or moved; documentation only.
The one-shot setup card appears mostly after an upgrade, when the runtime is
already on the machine. Its command carried no `--reuse`, so following it
re-fetched the ~570 MB JDK + Ghidra pair — the opposite of "align the runtime",
and enough to turn a thirty-second step into fifteen minutes. The command now
reads `node "…/scripts/setup.mjs" --reuse`; on a first install the flag costs
nothing, because every candidate is probed and a machine with none downloads
exactly as before.
The card also states the two things that look like failures and are not: setup
ends by importing a small file to prove the engine works (32–42 s of silence),
and a concurrent run holds `setup.lock`, which makes setup exit 75 with the
holder's pid and log path.
The README install lines move to #v2.0.12, which verify-branch pins against
package.json.
audit-manifest — item 13 tested a string with `Number.isFinite`, which never
coerces, so the branch was always false and "does the probe accept the next
patch/minor" could not fail. Verified end to end with a probe of `^6\.`: the
audit now reports "accepts 6.7.1 (the next patch)" and "accepts 6.8.0".
rea-shim — a zero-argument start printed usage and exited 0, while both launch
paths still carried an unreachable `argv.length > 0 ? argv : ["mcp"]` fallback.
With `failOnStartupError: false`, that exit 0 leaves REA silently absent from
every tool list. The two cases are now told apart by whether a terminal is
attached: usage + 0 for a person, usage + stderr + EX_USAGE(64) for a harness
that lost its arguments. Both dead fallbacks removed.
setup.mjs — three:
* provisioning cleared the destination BEFORE extracting, so a missing
extractor or a full disk destroyed a working install and replaced it with
nothing; the tree is cleared only for the rename now.
* a `.part` holding the whole body could not be rescued when the manifest
declared no size — the Range request got 416 on every retry. A pinned
sha256 is now enough evidence to finish the rename.
* `setup.lock` was written only when a run ENDED, so it recorded an outcome
and excluded nothing: the shim's detached run and a hand-run setup could
write the same `.part` and the same staging directory. It is claimed with
O_EXCL before the first byte moves; a live holder is reported with its pid
and log path and the run exits 75, while a dead pid or a `failed` lock is
taken over.
check-updates.ps1 — a missing `application.version` line was reported as a
version mismatch ("requires 12.1.x, currently "), sending the reader after a
newer Ghidra instead of a reinstall.
rea-bundle-bootstrap.ps1 — the engines check compared only major/minor, so
`^24.11.1` accepted 24.11.0; all three components compare now.
rea-shim PATH dedupe — Windows compared path spellings case-sensitively and
could prepend the vendored Node twice.
cordis.patch.yml — the home fallback skipped HOMEDRIVE+HOMEPATH, so a service
account resolved `/.dsh/...`. It now mirrors `os.homedir()`: USERPROFILE →
HOMEDRIVE+HOMEPATH → HOME. Both READMEs follow, because the audit compares the
documented expression with the shipped one.
setup.mjs header — the TOFU sentence promised "enforced afterwards" while an
`unverified` checksum is trusted on a cache hit. The comment now states what the
code does and names the gap in `bundle.json` instead of implying verification.
Tests 85 -> 88: extraction order, the audit's item-13 branch, and a live lock
blocking a second run; the bare-invocation case asserts EX_USAGE plus the
`--shim-help` path that must stay 0.
setup.mjs — three defects that all made `bundle.json` describe something other
than what the machine runs:
* A reused component carried no version. `provisionComponent` rebuilt
`{path, reused}` from the adopted record, so `versions.node` fell back to the
interpreter running setup and `versions.jdk`/`versions.ghidra` to the
manifest pin. Both are wrong whenever the adopted build is not that one — the
exact case `check-updates.ps1` exists to catch. Each installation is now asked
what it is: `-v` for Node, `release` for a JDK, `Ghidra/application.properties`
for Ghidra.
* The provisioning-time `rea doctor` got neither `GHIDRA_INSTALL_DIR` nor
`JAVA_HOME`, so REA fell back to probing the machine and reported whichever
Ghidra and JDK were installed globally. On this host that was a different
Ghidra than the one being provisioned. Doctor and the smoke step now receive
the same two variables the mirrored shim exports.
* New `smoke` step: after wiring, import a small PE through `rea analyze` and
fail the run when it cannot. `doctor` validates paths, versions and which Java
would be chosen but never imports a binary, so a broken bridge or a mismatched
native addon surfaced only as an MCP tool timeout on the first real analysis —
which reads exactly like "the runtime upgrade broke REA". Measured here: 36 s
for a small PE, 64 s for a 6 MB DLL. `--skip-smoke` omits it.
manifest — Node 24.21.0 -> 24.22.1 on all six platforms (version, url, size,
sha256, verification date). 24.22.1 is Node's 2026-10-21 security release
(Brotli decoder out-of-bounds write RCE, two TLS hostname bypasses, eMudhra
intermediate removal); the reuse path was fine on a machine that already had it,
but a clean environment downloaded the vulnerable 24.21.0. Checksums come from
the official SHASUMS256.txt; the win32-x64 archive was also downloaded, hashed,
unpacked and run (`node -v`, `npm -v`) here.
scripts/check-updates.mjs — `npm run check-updates` goes through a portable
wrapper: PowerShell (5.1 first, then pwsh) on Windows, and on other platforms an
explicit explanation plus the two commands that do work, exiting non-zero rather
than pretending "not applicable" is "all green".
Tests 81 -> 85: smoke is skipped without an engine, --skip-smoke wins, the smoke
step really drives `analyze` and a failure is a FAIL, and the reuse record
spreads the adopted version instead of rebuilding it.
The setup step is mandatory but invisible: a market install or update replaces
the package and stops there, and a skipped setup fails silently in both
directions - no tools on a first install, the old REA after an upgrade. The
guide names the actual state instead of asking whether it was shown before.
- bin/rea-guide.mjs: computeGuideState compares the REA version recorded in
plugins/rea-bundle/bundle.json with the manifest pin and reports
missing / stale / unknown / unreadable / ok. Same pin rules as
scripts/pin-match.ps1 (dotted pin exact, bare major as a family), and it
reads UTF-16 records the way the shim and setup.mjs now do.
- index.mjs: serves GET /dsh-plugin-rea/guide-state, mounted only through
ctx.inject(['webServer']), so headless and ACP hosts load the plugin exactly
as before. Read-only: no provisioning, no spawning, no writes.
- client.js: shell.overlay card shown once per plugin version, with the exact
command for this install; "Later" and "I ran it" record the version in
localStorage, so the next upgrade re-arms it and the same version stays quiet.
- cordis.patch.yml: one extra row for the package entry only (no tools, no REA).
- 5 new tests (78 total); verify-branch section 9 checks all four ends of the
chain, so a rename cannot make the guide silently never appear.
systemReaCandidates followed this process's platform instead of the one it is resolving, so the win32 shape could never be exercised on POSIX; a failed background spawn left a lock naming a live pid (and an unhandled error event could kill the shim), so it now clears or marks the lock failed and logs why; probeRea accumulated stdout without a bound; readJson now decodes UTF-16 LE/BE in both the shim and setup, because PowerShell 5.1 writes that by default; foreignReaRegistration strips a YAML trailing comment.
A market install or update replaces the package and stops; nobody runs setup.mjs for the user. That sentence now sits at the top of both READMEs, and the standing per-release note in the changelog says the same so later release notes inherit it.
check-updates compared every component by major version, so 6.5.0 matched the 6.7.0 pin and the only signal that setup must be re-run disappeared. Dotted pins now compare exactly; a major-only pin (jdk 21) still matches its family; an adopted (reused) Node is checked against engines and is never counted as pin-consistent.
Also: the screenshot generator reads versions from the manifest instead of hardcoding 6.5.0, and both READMEs state the two standing upgrade rules (the mirrored launcher self-heals from 2.0.8; the mirrored setup copy is refreshed only by running setup from the plugin directory).
6.6.0 was tagged upstream but never published to npm; 6.7.0 is npm latest and keeps
the contract unchanged: same engines (^22.19.0 || ^24.11.0 || >=26.0.0), same bin
(scripts/rea.mjs), same optional frida 17.23.2, and the tarball still ships no
node_modules (1775 entries, 0 there), so provisioning stays an npm install.
Measured on this machine: rea --version 6.7.0, MCP initialize answers
serverInfo {name: rea, version: 6.7.0}, tools/list and doctor both report 207 tools
- unchanged from 6.5.0, so this is a feature/fix upgrade rather than tool growth.
doctor gains kuna-installation, another caller-supplied check provisioning leaves alone.
The rea stub in the tests now takes its version from the manifest pin instead of
hard-coding it: a bump otherwise fails --offline for a reason unrelated to offline.
Review of 2.0.9 found that the fallback still could not fire in the case it was written for, and that a hand-edited mirror.json behaved as if absent.
- The re-exec promise resolved true at the `spawn` event, which fires before the child has read anything. A later "it actually failed" could no longer be seen, so a non-zero exit with the source gone fell through to `resolve(false)`, was ignored, and the process ended 0 - silently, with the message never printed. It now resolves ONLY on failure: while the child lives this process is parked and the child's exit is the exit. Measured before/after with a checkout launcher that deletes itself and exits 3: exit 0 (no output) -> exit 78 with "could not run ... running the mirrored copy instead".
- That also covers the realistic upgrade race, which is not a spawn failure at all: node starts, finds its file replaced, and exits 1 with MODULE_NOT_FOUND. A non-zero exit whose source no longer exists now falls back instead of being passed through.
- JSON reads strip a leading U+FEFF. JSON.parse rejects it, and every caller treats a parse failure as "no record", so a BOM (which Windows editors write) silently disabled the mirror check and made bundle.json look unprovisioned. Measured with PowerShell-written fixtures.
Docs/CI: the README pin example names the version this tree publishes (#v2.0.9); both verify-branch.ps1 and validate.yml now fail when the pin does not equal package.json's version, and the README range check moved into the same step. A mutation (pin reverted to v2.0.8) fails the check and was restored.
Tests: 69 (66 pass + 3 skipped); the new one deletes the checkout launcher mid-flight and asserts the fallback. verify-branch 41 checks green, audit clean, host-compat 10 covered / 5 outside. Version 2.0.9, NOT published: no tag, no release, no npm.
Two problems blocked publishing 2.0.8's follow-up, both found by an external review:
- reexecFromCheckout announced a fallback it did not perform. On a spawn error it printed "continuing with the mirrored copy" and returned; main() had already returned, no exit event followed, the loop drained, and the row exited 0 with no tools and no message. It now resolves a boolean: only a started child takes over the exit code, and a failure falls through to the local launch path with an honest message.
- bin/rea-shim.mjs carried 16 mojibake dashes (U+9225 + "?") from a GBK edit in 2.0.7, two of them in user-visible stderr ("first run - provisioning started", "nothing is running right now - read the log"). All restored to an em dash and the space the mis-decode had eaten.
A checkout copy no longer hops into the plugin a bundle's mirror.json names: only a mirror re-execs, a checkout (it ships cordis.patch.yml) never does. That was a real footgun - a developer running the checkout would silently test the installed plugin. The re-exec's comment now matches the code, and the mirror tests run the mirrored copy instead of the checkout.
Docs and CI: the README pin example moves from v2.0.0 (the most bug-prone release) to v2.0.8; the READMEs state the Windows limitation that a forced kill of the shim leaves the delegated REA running; docs/bundle-design.md gains the 2.0.7 launch mode and the 2.0.8 self-healing mirror, including mirror.json's shimSha256/pluginVersion/dshHome; and CI now checks that both READMEs quote engines.dsh verbatim, which previously only verify-branch.ps1 did. systemReaCandidates dedupes on the platform argument rather than process.platform.
Tests: 68 (65 pass + 3 skipped), one new for the checkout guard. verify-branch 39 checks green, audit clean, host-compat 10 covered / 5 outside. Version is 2.0.9 and NOT published: no tag, no release, no npm.
An independent audit caught three non-blocking inconsistencies in the 2.0.8 tree:
- Both READMEs still quoted engines.dsh as ">=0.1.7-rc.2 <0.3.0" after the declaration moved to "<0.3.0-0" — the stale form the range's own negative test proves is wrong (it admits 0.3.0 prereleases). verify-branch now compares the sentence with package.json, so the drift cannot come back silently; a mutation (reverting one README) fails the check and the file was restored.
- CHANGELOG 2.0.8 still claimed 62 tests (59+3); the tree has 67 (64+3), seven of them new in this release.
- check-updates.ps1 computed $homeEarly only inside the "no REA_BUNDLE_ROOT" branch, so with the override set it stayed $null and Join-Path threw, degrading the profile section to "no profile". It is resolved unconditionally now and the profile lookup uses it.
Verified: 67 tests (64 pass + 3 skipped), audit clean, verify-branch ALL CHECKS PASSED with the new README check, and the home/profile block re-run with REA_BUNDLE_ROOT set resolves DSH_HOME and the real profile directory.
The duplicate-registration check reads <DSH_HOME>/cordis.patch.yml, but an MCP child has every DSH_* variable stripped, so a mirrored copy with a custom home read ~/.dsh instead: the wrong file, and a conflict it could never report. setup.mjs now records the home it ran under in mirror.json (dshHome), and the shim prefers the environment, then that record, then ~/.dsh. The mirrored copy's own bundle root was never affected - it comes from its location - so this only changes which patch file the warning reads.
check-updates.ps1 had a second home algorithm in the same script: its profile section hardcoded $env:USERPROFILE, so on a custom DSH_HOME it reported "no profile found" while the rest of the run had resolved the home correctly. It now reuses the same rule.
Test: a bundle whose mirror.json names a home containing a foreign serverName: rea, run with DSH_HOME="" (the scrubbed shape), must report duplicateRegistration with that file; 67 tests total (64 pass + 3 skipped).
The row launches <bundle>/bin/rea-shim.mjs, a copy made by setup. A market upgrade replaces the package and runs no setup, so that copy kept executing the code it was made from: every fix in a new plugin version reached a user only if they happened to re-run setup, and an old copy keeps working, which is why nobody notices.
From 2.0.8 the copy compares itself with <mirror.json.pluginRoot>/bin/rea-shim.mjs and re-execs that file when the two differ: one hop, guarded by REA_BUNDLE_SHIM_REEXEC, and only mirror -> checkout (the checkout finds itself and does nothing). --shim-status reports mirror.{pluginRoot,mirroredAt,stale,reason}; setup records shimSha256 and pluginVersion; check-updates.ps1 reports a lagging mirror and says to re-run setup.
A version older than this one cannot repair its own upgrades - the copy has no such code - so upgrading from 2.0.0..2.0.7 still needs one setup run. The README says so in both languages.
Tests: 66 (63 pass + 3 skipped). Four new ones cover the re-exec, the guard, the stale report and the identical case.
Launch decision at startup: a REA on PATH that answers --version and is not older than the provisioned build wins, and the bundle is never provisioned (the 1.x contract, reachable from the 2.x row). REA_BUNDLE_LAUNCH=auto|system|bundle overrides it; REA_BUNDLE_SYSTEM_REA names a binary off PATH; --shim-status reports launch.{mode,reason,unusable,systemRea,rejected}. A delegated child carries REA_BUNDLE_DELEGATED=1 so a wrapper cannot make the shim delegate to itself.
Two independent reviews found real defects in the first cut, all fixed here: the cmd line quoted the arguments, so a .cmd's %* received "mcp"; a 3 s probe budget against a measured 2.4 s --version turned detection into a coin flip and leaked a live REA per timeout; SIGINT forwarding on Windows killed only the cmd.exe wrapper. Also: status reported 0 for a pinned-but-unusable system launch, existsSync accepted directories, PATH entries were not normalised, and fail() suggested a setup command without --root.
The engines.dsh ceiling is now <0.3.0-0: with includePrerelease, <0.3.0 admitted 0.3.0-alpha.1 and 0.3.0-rc.1, which nobody audited. host-compat's tests pin the release list literally and prove the leak check has teeth; the evaluator accepts +build metadata and rejects v-prefixes and zero-padded identifiers as semver does.
Environment independence: every shim call in the suite pins REA_BUNDLE_LAUNCH, setup's own shim probe pins it too, and both CI jobs set it at job level. Bootstrap now honours REA_BUNDLE_ROOT like the other three readers.
engines.dsh = '>=0.1.7-rc.2 <0.3.0'. The market's deriveHostCompatibility reads only engines.dsh (top-level or dsh.engines) and peers on @deepseek-ai/dsh*; with neither it reports 'unknown', which is what the card has shown until now. Peer declarations are deliberately NOT used: @deepseek-ai/dsh-mcp-client is published on npm at 0.0.1-rc.1 while the host ships 0.2.x, so a peer range would resolve to a version the host does not have.
The range is checked, not guessed: scripts/host-compat.mjs carries the dsh release list (0.1.7-rc.2 .. 0.2.1-rc.2) and fails when the declaration stops covering one of them, or starts admitting 0.3.0. The row surface was compared at dsh-v0.1.7-rc.2 and dsh-v0.2.1-rc.2: packages/mcp/mcp-client exposes the same config keys, and dsh.bundle.patch exists at both.
Upstream rea-agents 6.6.0 is released on GitHub but npm latest is still 6.5.0, so the pin stays at 6.5.0; the bump is scheduled for 2026-10-19.
dshmarket's update path asks hasLoadableEntry whether the new build has an entry file that exists (main/exports, else index.js) and rolls back when it does not. This package declared neither, and the package it mounts (@deepseek-ai/dsh-mcp-client) ships inside the harness build rather than in node_modules, so every location the check probes was empty: 2.0.1 -> 2.0.4 failed three times with 'updated build has no loadable entry' on a healthy profile, and the rollback was reported unverifiable for the same reason.
index.mjs is a real but deliberately empty cordis plugin: the working row is still inserted by cordis.patch.yml, so loading it must do nothing (a second serverName: rea would be refused). Two tests, verify-branch and the CI manifest/payload checks now pin both the pointer and its presence in files[].
Each item was reproduced before it was changed, and every fix has a test that
fails when it is reverted (18 reverse checks; two of them on WSL because the
code path only exists on POSIX).
- A lock older than the freshness window was never removed, so the exclusive
create below it failed with EEXIST forever: after a killed download the shim
never provisioned again — and said nothing, because main() printed no message
for "in-flight". Reproduced with a 7-hour dead lock (no run started, lock
untouched, no output) against a 5-minute control lock that behaved correctly.
An expired lock, or one whose holder is gone, is now cleared; "in-flight",
"spawn-failed" and "unwritable" each say what they are.
- examples/ still held the 1.x rows (`command: rea`, `cmd.exe /c rea`) and shipped
inside the 2.x npm package, where copying one bypasses the bundle row entirely.
Removed from main (v1 keeps them, where they are correct) together with the
package.json files[] entry and the three validate.yml checks that named them;
the payload is 19 files now.
- `--offline` was a promise the npm step ignored: provisionReaAgents ran
`npm install` regardless. It now refuses that step when the vendored copy is
unusable, and still accepts one that is.
- That npm step is the only one that cannot use this bundle's own route/proxy
resolution; its failure now says so, and both READMEs name HTTPS_PROXY /
NPM_CONFIG_REGISTRY.
- POSIX extraction had exactly one tool per format while Ghidra is a zip on every
platform, so a bare image without unzip failed for certain. zip now falls back
unzip -> python3 -m zipfile -> the provisioned JDK's jar, tarballs tar ->
python3, and when none is present the log names the tools it tried.
- The platform-tools arm64 comments were wrong (Google ships x86_64 binaries for
both linux and darwin): corrected, so an arm64 host gets an accurate
optional-component report instead of an implied native adb.
- docs/bundle-design.md caught up with the implementation: the row resolves
<DSH_HOME>/plugins/rea-bundle/bin/rea-shim.mjs at load time, and the vendored
CLI lives under vendor/node_modules/rea-agents.
- release.yml's publish job now requires the version's tag and its release asset,
so a workflow_dispatch run cannot publish to npm while the market's fallback
tarball link is missing.
- Numeric environment overrides are sanitised instead of becoming NaN ("gave up
after NaN attempt(s)"), and the effective limits are logged; the PowerShell
route's docstring now says it is the primary Windows route; five redundant
dynamic `node:fs` imports are gone.
37 unit tests (Windows 35 pass + 2 platform skips; WSL 36 pass + 1), audit
clean, verify-branch ALL CHECKS PASSED, and the real machine re-provisioned with
the mirror byte-identical to this tree (REA 6.5.0, 207 tools, a live tool call
reporting path_source: environment).
Every item below was confirmed in the code (or reproduced) before it was changed,
and each now has a regression test that fails when its fix is reverted.
- `--only` rewrote bundle.json from this run's results alone, erasing every other
component's record: the shim stopped exporting JAVA_HOME/GHIDRA_INSTALL_DIR and
the next full run re-extracted them. Untouched records are carried over, and
only while their recorded path still exists.
- `--only` skipped the requires check entirely, so `--only apktool` resolved
{jvm} to a vendor/jdk that only a full run creates and failed on a machine whose
JDK was recorded next to it.
- A complete .part was not treated as finished work: it logged "already complete"
and then asked for `Range: bytes=<size>-` (416, four attempts, reported failed).
It is verified and renamed now.
- The Windows PowerShell route deleted the partial file before every attempt, and
the Node fallback reused a stale `have`, appending a full 200 response onto the
bytes a failed route had already written. The route resumes from what is on
disk, and the fallback re-reads the file's real length.
- POSIX wrappers landed 0644 while REA spawns them without a shell (EACCES); a
POSIX entry marked `executable` is now set 0755.
- apktool's wrapper was pinned to `master` with a hard checksum, so one upstream
commit was enough to break every new install. Pinned to the immutable v3.0.3
tag, with the hashes measured from it.
- `--reuse` adopted a JDK because `javac` existed and a Ghidra because
`analyzeHeadless` existed. A candidate must now pass the component's own probe,
and a `file` probe finally honours `expect.contains`.
- A malformed bundle.json failed permanently: setup could not get far enough to
replace the file it could not parse, and nothing started setup. Setup tolerates
it, and the shim provisions for `malformed` as well as `missing`.
- A failed run kept a fresh lock that made the shim claim "provisioning started in
the background" for six hours while nothing ran. The lock records its state and
pid, and a dead holder no longer blocks a retry.
- A successful `--json` run never released the lock, because the removal sat
inside the non-`--json` branch.
- The shim's fallback REA path omitted `node_modules`, so an older bundle.json
without paths.reaEntry reported a working install as gone.
- bootstrap.ps1 threw on a non-numeric `node -v` (an int cast under
ErrorActionPreference=Stop) instead of falling through to the portable download,
and could exit 0 when `&` failed; it reads REA's engines from the manifest pin
instead of a second hardcoded copy, and -DryRun no longer creates directories
while claiming nothing changed.
- check-updates.ps1 looked for REA in the npm global root and for Ghidra in a
user-level variable, which is the 1.x story: on a 2.x machine the REA section
pointed at the wrong object, the doctor self-check was skipped entirely, and the
bundle drift check silently skipped unrecorded pins while printing "all pins
match". It reads bundle.json first now.
audit-manifest.mjs gained four checks: manifest components vs INSTALL_ORDER,
pins.nodeEngines vs package.json engines, the REA pin probe against the next
patch/minor, and the POSIX wrapper exec flags. validate.yml gained a
windows-latest job, because the PowerShell download route, the zip extraction and
the bootstrap were Windows-only code that no CI ever ran.
33 unit tests (32 pass, 1 platform skip), audit clean, on Windows and WSL.
1.x was only mentioned in the opening version note and the upgrade section; the question
"when would I actually want it?" had no answer in the README. Both READMEs now end (just
before the license) with a short section: what 1.x is — the adapter without provisioning,
launching `rea mcp` from PATH — when it is the better choice (you already manage REA and
Ghidra, you do not want the plugin fetching anything, or the row must point at your own
`rea`), and how to install it (`[email protected]` or `github:czcdr/dsh-plugin-rea#v1.0.6`).
Docs-only patch release so the npm package page renders it.
The npm package page renders the README straight from the published tarball, and its first
screen was title -> badge -> language line -> a 1280x720 screenshot, which pushed the 1.x/2.x
explanation below the fold — the 2.0.0 README therefore looked unchanged there.
- both READMEs: the version note (which line is this, what 1.x vs 2.x do, where 1.x lives)
now sits directly under the language switcher; the comparison table and the upgrade
section keep their places
- docs/bundle-design.md: main IS the bundle line now (it said "this branch, main unchanged"),
and section 8 is about the approach, not a branch
- docs-only patch release on purpose: republishing is the only way npm re-renders that page
main fast-forwards onto the bundle branch (no merge commit), so the default branch,
npm latest and the market all point at the all-in-one variant; the 1.x
configuration-only adapter is preserved on the new v1 branch and on npm 1.0.6.
- package.json 2.0.0-bundle.2 -> 2.0.0; CHANGELOG gains the 2.0.0 entry including the
1.x -> 2.x upgrade steps (run setup once, then restart: the 2.x row launches a shim
that only setup creates, so an unprovisioned upgrade loses the tools silently)
- both READMEs: a new "two lines" section comparing 1.x and 2.x, install commands
switched to the npm name (plus a #v2.0.0 pin), an "Upgrading from 1.x" section, and
a Verified-against note that separates what was exercised live from what was not
(the JDK + Ghidra download branch, macOS/Linux first runs)
- assets/make-screenshots.ps1: the diagram now shows the 2.x chain (host Node ->
mirrored shim -> vendored REA -> engines), and its param default no longer reads
$PSScriptRoot, which is not populated while parameter defaults are bound under
Windows PowerShell 5.1 — the documented invocation died there, so this script had
never actually regenerated a screenshot. Both screenshots regenerated.
- docs/marketplace-entry.yml: 2.x description, and the stale "current v1.0.5" comment
Two contracts this repository already states but the code did not keep, both
found by running the bundle on a real machine:
- manifest.components.androidPlatformTools declares optional: true (without a
device these tools simply report no devices), but setup.mjs never read the
field, so an unreachable dl.google.com - the only Google-hosted component -
failed the whole run on a machine where everything REA needs had provisioned
fine. An optional component's failure is now reported as such, kept out of the
exit code, and left unrecorded so the next run retries it; a component that
another component requires is never treated as optional.
- manifest.components.node.probe says the reuse path must reject an incompatible
runtime, but adoption was fileExists-only: a machine whose PATH Node is 20
recorded it in bundle.json, and the failure surfaced much later inside REA.
Every candidate (REA_BUNDLE_NODE and each PATH hit, not just the first) now has
to pass that probe; one that cannot even be run is rejected with its reason.
README: apktool's real Windows gate (upstream execFile's the launcher without a
shell, so the official .bat cannot work) and the fact that the CLI refuses to
manage the Electron app's desktop profile.
Tests: 21 (was 17); Windows 21/21 and WSL 21/21. Reverse-verified - disabling the
optional accounting reddens that test, disabling the reuse check reddens the
other two.
The previous round made the shim resolve its bundle from its own location, so a
custom DSH_HOME could not send it looking in ~/.dsh. That is right for the
mirrored copy the published row launches — but wrong for the checkout copy a
human runs, whose own directory is the plugin, not a bundle:
* `npm run bundle:status` reported "not provisioned" on a fully provisioned
machine, and the status line setup prints pointed at that same copy;
* a bare `node bin/rea-shim.mjs mcp` would have provisioned ~800 MB into the
plugin directory.
Only a checkout ships cordis.patch.yml, so that file now discriminates: the
mirrored copy keeps using its own position (an MCP child has no DSH_HOME to
consult), while the checkout resolves the same root setup.mjs writes — including
a custom --root, which is why the status hint now names the mirrored copy.
The audit guards the discriminator by rejecting a mirror list that would copy it
along; mirroring it would silently flip a mirrored copy back to the DSH_HOME
rule.
The README's "install the plugin first" paragraph was also false: the row points
at a file setup creates, so before the first provisioning run it exits with
MODULE_NOT_FOUND and nothing starts a background provisioning. It now says so and
names the one manual step.
Verified: 16/16 unit tests on Windows and on Linux — three of them spawn the shim
in both layouts, and reverting the fix turns the checkout one red; audit clean
with 5/5 deliberate sabotages caught; 25/25 end-to-end path checks; and the row →
shim → MCP handshake still returning rea 6.4.0 with 170 tools.
Three defects made the published flow impossible to complete:
* The !!js scalar in the MCP row was unquoted, and a bare scalar containing
"? " and ": " is read by YAML as an explicit key/value pair. The loader
therefore received a mapping, so args[0] was {"[object Object]": "..."} and
the row could never start. Reproduced against the harness's own dialect
(JSON_SCHEMA plus the !!js type definition read out of app.asar).
* The DSH_HOME fallback stopped one level above <home>/.dsh, while the shim,
setup.mjs and bootstrap.ps1 all resolve to <home>/.dsh. DSH never sets
DSH_HOME — zero writes anywhere in the archive — so this was the default
path, not an edge case: the row pointed at a file provisioning never writes.
* The mirror step copied the shim and setup.mjs but not the manifest, and the
mirrored setup derives its plugin root from its own location. Background
provisioning therefore died on a bare ENOENT that stdio:"ignore" hid, while
the lock it had written suppressed retries for six hours.
Also fixed:
* The shim locates its bundle from its own position. MCP children have every
DSH_* name stripped (case-insensitively), so with a custom DSH_HOME the old
shim looked in ~/.dsh, reported "not provisioned" and started a second
~800 MB run there.
* setup records the real checkout in <bundle>/mirror.json, so a mirrored or
later-upgraded install finds the source instead of copying the bundle onto
itself and provisioning the previous manifest forever.
* Empty REA_BUNDLE_ROOT / DSH_HOME no longer degrade the root to "" or a
relative path, and --root/--cache are resolved (a relative root would be
recorded in bundle.json and make every component look deleted).
* fail() no longer recommends a mirrored copy that cannot work, and
check-updates.ps1 reports the migration state for old bundles.
Guarded by checks that were themselves reverse-tested — 4/4 deliberate
sabotages caught: unquoted !!js mapping detection, DSH_HOME agreement across
unset/empty/set, README-versus-patch drift, and mirror-list completeness. CI
now runs bundle:test and bundle:audit, and its YAML step registers the !!js
scalar constructor: safe_load had been rejecting the very row this repository
ships, so the workflow was permanently red.
Verified: audit clean; 13/13 unit tests on Windows and on Linux (WSL); 25/25
end-to-end path checks in a sandbox; and a full JSON-RPC handshake launched
from the row's own evaluated command line (rea 6.4.0, 170 tools) with DSH_HOME
absent from the child environment.
An external review of this branch found that the headline promise did not
actually work, and it was right. Each claim was checked before acting on it.
Fatal, now fixed:
- **The patch could never launch anything.** The JS string literal used
backslashes, and a single-quoted YAML scalar keeps them literal for JavaScript
to eat: '\plugins' is the BEL control character, '\rea-bundle' a carriage
return, '\bin' a backspace. Evaluated, the path was
"C:\...\.dshplugins<CR>ea-bundle<BS>in<CR>ea-shim.mjs". Forward slashes now,
with a DSH_HOME fallback so an unset variable no longer yields
"undefined/plugins/...".
- **Nothing created the file the patch points at.** `setup.mjs` contained no
copy of the shim anywhere, so provisioning could finish and the row would
still fail with MODULE_NOT_FOUND — and the documented "install the plugin
first and it provisions in the background" could never happen, because the
shim that would do that was never at the path the row launches. Setup now
mirrors bin/rea-shim.mjs and scripts/setup.mjs into the bundle root, before
the probe, which now tests the mirrored copy rather than the source tree.
- **`npm` was invoked from PATH.** The entire point of this branch is the
machine without Node, where PATH has no npm, so resolving the REA dependency
closure died with ENOENT on exactly the target machine. It now uses the npm
that ships inside the provisioned Node (verified: the portable Windows zip
puts it at <node>\node_modules\npm\bin\npm-cli.js and it runs).
- **`{exe}` was not substituted on POSIX.** The `resolved !== ""` guard in
swap() rejected the empty string, so `adb{exe}` stayed literal on Linux and
macOS and platform-tools failed its probe. Now replaced unconditionally, and
the test mirror matches. (The review also claimed the existing test suite
fails on ubuntu; it does not — the mirror derives EXE from the platform.)
Also from the review, verified and fixed:
- resolveProxy() is cached; it spawned reg.exe twice per download.
- sha256File() streams instead of reading the whole file, which for the Ghidra
archive meant a 570 MB buffer.
- The provisioning lock uses an exclusive create, so two rows starting together
cannot both provision.
- reaAgents' probe pointed at the pre-npm-install layout, reporting a healthy
vendored copy as unverifiable on every run.
- The node probe's regex accepted Node 20, which REA's engines reject.
- bootstrap.ps1 read its Node version and checksum from a hard-coded pair that
duplicated the manifest's pins; it now reads the manifest, refuses a TOFU
pin, and its ARM64 check no longer passes while downloading the x64 zip.
- check-updates.ps1 gained the bundle section bundle-design.md promised.
Two bugs I introduced while fixing the above, both caught by running things
rather than reading them:
- Editing check-updates.ps1 stripped its BOM. PowerShell 5.1 then read the file
as ANSI, mangling the CJK comments into parse errors. Restored, and the BOM
is now asserted. bootstrap.ps1 was ASCII-only so it still parsed, but it has
a BOM too now so a future Chinese comment cannot silently break it.
- The node probe's own-token fallback returned the vendor/node DIRECTORY, so
the probe spawned a folder and failed with ENOENT while the extraction was
fine — a self-inflicted replay of the bug class this commit fixes.
New: scripts/verify-branch.ps1, which checks syntax, PowerShell parse + BOM,
the manifest audit, the unit tests, that the patch is machine-path-free and
escape-safe, the published payload list, and a dry-run of the whole chain.
Verified end to end: with DSH_HOME set, provisioning reaches `setup complete`,
the file the patch computes exists, and launching it the way the row does
answers an MCP handshake as rea 6.4.0 with 170 tools.
Self-review (not upstream changes) caught real faults in the provisioning layer:
- A recorded component whose entry had no usable path (a previous run that
could not provision it) was treated as ready, which permanently wedged every
later run: 'node' reported available with path null, so reaAgents and the
final probe failed forever. A recorded entry is now trusted only when its
path exists AND the probe passes.
- Downloads had a single 10 s connect timeout and no resume. Measured CDN
throughput here is 0.35-0.54 MiB/s, so a 570 MB Ghidra fetch could not
finish. Transfers now retry with backoff, honour the 60 s default timeout
(REA_BUNDLE_FETCH_TIMEOUT_MS / REA_BUNDLE_FETCH_RETRIES), resume a partial
file with a Range request, restart cleanly if the server ignores Range, and
report short transfers as resumable instead of fatal.
- process.kill(pid, signal) in the shim throws ERR_UNKNOWN_SIGNAL on Windows;
the child's signal death is now reported as 128+n.
- {node} could not resolve while provisioning node itself (the token referred
to a component not yet recorded), so the node step always failed its probe
even though the extraction was correct.
- Root-directory stripping was a fragile two-step special case; it now descends
one level per declared component. Removed dead code and a hard-coded jadx jar
name, and dropped a stale comment about resume.
Verified: node now provisions from cache to vendor/node with the recorded
sha256, and 'recorded without a usable path; provisioning again' leads to a
passing probe instead of a stuck bundle.