Skip to content

refactor(webui): land the in-process runtime host skeleton (runtime-first S2) - #78

Merged
fengzhi09 merged 5 commits into
mainfrom
integrate/s2-runtime-host
Sep 28, 2026
Merged

fengzhi09 merged 5 commits into
mainfrom
integrate/s2-runtime-host

Conversation

@fengzhi09

Copy link
Copy Markdown
Collaborator

First code slice of the runtime-first migration: stop routing the webui backend through packages/tui/src/acp/agent.ts and let it talk to the runtime directly.

This slice changes no behaviour. No route reads the new switch yet; the point is to make the in-process host exist and be testable, with every future capability migration (S3–S6) able to run behind a flag and fall back.

Replaces #76, which conflicts: S2 branched from b7ff2a1 while #73, #74 and #75 landed since. This PR is an integration merge — see below.

What lands

  • server/lib/runtime-host.js — two hosts with deliberately different lifetimes: a long-lived catalogue host for read-only lookups, and a per-turn turn host that is disposable. Both wrap TuiRuntimeAdapter over a CliService created in-process by createLocalRuntimeHostV2. The module deliberately does not import config.js; configuration arrives as explicit options so nothing pollutes process.env at module-init time.
  • MCODE_WEBUI_TRANSPORT — defaults to acp, which is exactly today's behaviour. An unrecognised value warns and falls back; it never refuses to start.
  • CJS interoperability — the blocker S1's acceptance found. The webui server bundle had no require shim and the adapter's dependency tree contains proper-lockfile (CJS), so the bundle built but crashed at module load with Dynamic require of "path" is not supported. Fixed with the CLI's own createRequire banner.

Why the disposable turn host matters

Moving the engine in-process removes a process boundary. Today an engine crash costs one subprocess; in-process it could take the webui server with it. The turn host is the only hedge: every call inside a turn is wrapped so exceptions become that turn's failure state, a turn releases its host when it ends, and close() drains with a bound rather than blocking graceful shutdown.

Cancellation does not use subprocess.kill — there is no process to kill. abortSession returning success means "delivered", not "stopped", so the path is abort → wait for the stream to settle (≤5s) → discard the host.

The conflict, and how it was resolved

One conflict, in mcode-acp.js. Both sides were wrong for the merged state:

Keeping the branch's copy would have produced a duplicate declaration against that import. Neither definition belongs in that file any more, so the whole region goes. node --check confirms the result parses.

Acceptance

Independent (GLM-5.3-Flash, not the author): PASS-WITH-CONCERNS. Both findings were fixed in 17ce2f1.

  • The CJS fix holds. Without the banner the probe exits 1 with Dynamic require of "node:os"; with it, exit 0. The crash moved from path to node:os only because the import graph reaches a different CJS module first — same shim, same root cause. better-sqlite3 requires through the banner at boot, verified against a real host that writes SQLite.
  • Default is byte-identical to today. /api/models byte-for-byte identical to baseline; /api/state and /api/settings differ only in per-instance token and port. Zero files under routes/ are touched, and the production server/ has no reference to the new module.
  • Zero mcode subprocesses, asserted with set-level deepEqual at three points — not "happened to be zero".
  • Both mutations reproduced: removing the turn's inner try/catch turns exactly S2-RH-03 red; close() as a bare await turns exactly S2-RH-02 red.

Follow-ups taken in 17ce2f1

  • The acceptance found the docs described MCODE_WEBUI_TRANSPORT=exec as current behaviour when no route consumes it — a no-op in this slice. Corrected in both languages.
  • S2-RH-04 contained assert.ok(abortSeen || true) and never passed a signal, so the 5-second bound was never exercised. Replaced with strict assertions; the stub now hangs on purpose so the bounded race fires every run (elapsed ≈ 5400ms).
  • Writing that test surfaced a real bug: a caller omitting signal passed undefined straight through. The turn host now owns an AbortController; safeAbortSession delivers on both the runtime-protocol path and the signal path.

Known blocker for S3 (recorded, not fixed here)

The banner anchors import.meta.url to dist/webui/server.js, one level deeper than the CLI's dist/cli.js, so resolveAgentAssetsDir's candidate chain misses in the real dist layout. With assets at dist/assets/agents boot still throws; only dist/webui/assets/agents boots. Harmless in S2 (nothing is wired), but S3 will crash on a real dist deploy. Tracked in the S3 ticket.

Checks

pnpm typecheck · webui:typecheck · webui full suite 1952 pass / 0 fail (after rebuilding webapp/out) · check:source 4722 files · check:tsconfig 132 · test:release-tools 69/70.

🤖 Generated with Claude Code

S2 Dev and others added 3 commits September 28, 2026 23:21
…t skeleton

S2 ships the foundation that lets later slices (S3+) drop the
`mcode acp` child-process boundary:

* New `packages/webui/server/lib/runtime-host.js` exports
  `createCatalogueHost` (long-lived, replaces the ACP singleton for
  read-only catalogue traffic) and `createTurnHost` (per-turn wrapper
  around `adapter.sendMessage`/`adapter.abortSession` with the R1
  exception boundary and the R2 bounded abort). No route consumes them
  yet — S3-S6 wire catalogue/turn traffic incrementally, S7 flips the
  default. The default behaviour is byte-identical to `main`.

* New `MCODE_WEBUI_TRANSPORT` config knob (default `acp`, the
  legacy `MCODE_USE_ACP=0` escape hatch still wins). Documented in
  both `docs/webui.md` and `docs/webui.zh-CN.md` — full table of
  accepted values, interaction with `MCODE_USE_ACP`, and the S3+
  rollout stages. `server/lib/config.js` validates the value and
  warns on unknown values rather than refusing to boot.

* `scripts/build.mjs` adds a `createRequire` banner to the webui
  server bundle. The webui bundle's dependency tree pulls in
  proper-lockfile (CJS), which requires `path` at module load. Without
  a banner the synthetic `__require` shim throws
  'Dynamic require of "path" is not supported' on first import —
  the S1 acceptance-call hard blocker, finally fixed here. The CLI
  bundle has had an equivalent banner since 0.5.4; the webui bundle
  mirrors it via a new
  `createWebuiBundleModuleLocationConfig` helper.

* `packages/webui/test/server/runtime-host.test.js` pins:
  - boot → createSession → listSessions → close in an isolated tmp
    dataDir with zero `mcode` child processes (R1 acceptance target),
  - `close()` is bounded even when `apiHost.close()` hangs (R8),
  - sendMessage iterator throws become stream error frames, never
    propagated (R1 turn-host boundary — mutation-1 turns red when
    dropped),
  - `abortSession` returns within 5s without any subprocess kill
    (R2 — mutation-2 turns red when the bounded race is removed).

Acceptance:
* `pnpm typecheck`, `pnpm --filter @mavis/webui webapp:typecheck`:
  both 0.
* `pnpm --filter @mavis/webui test`: 1952/1954 pass (2 baseline skips
  unrelated to this change). My four S2-RH tests all green.
* `pnpm check:source`: 4715 reviewed files after inventory regen.
* `pnpm check:tsconfig`: 132 package exports match.
* `pnpm test:release-tools`: 69/70 pass (1 baseline skip).

Dist-shape runtime assertion (no-banner probe → exits 1 with the
expected symptom; banner probe → exits 0 and `createCatalogueHost` +
`createTurnHost` resolve as functions on the bundled module): see
archive/evidence/s2-runtime-host/R1-dev.
…y + dead code)

Acceptance of d49d919 (S2 R1) flagged three follow-ups. R2 closes them
all without expanding scope.

* docs/webui.md and docs/webui.zh-CN.md: rewrite the MCODE_WEBUI_TRANSPORT
  Turn-condition table so the exec entry reads 'no-op in S2 — same as
  default acp; exec today is reachable only via MCODE_USE_ACP=0', the
  same shape the runtime entry already had. Both languages. The Env-var
  table and the priority rule got the matching correction.

* S2-RH-04 had two real defects that masked each other:
  (a) `assert.ok(abortSeen || true, ...)` was a tautology — verified
      the assertion was checked with an explicit strict-mode test that
      throws when abortSeen is false (mutator #1).
  (b) The 5s upper-bound branch was never activated because the stub
      settled the stream on abort. The new stub hangs forever after
      the first yield — the worst case the bounded drain exists to
      protect against. abortSession must time out at 5s, not return
      quickly. Mutator #3 (drop the 5s race) turns S2-RH-04 red.

* Implementing the fixed test surfaced a real bug: safeSendMessage's
  signal param was undefined when the caller didn't supply one, so
  adapter.sendMessage was called without a signal. createTurnHost now
  owns a per-turn AbortController and safeSendMessage forwards
  turnController.signal when the caller doesn't supply one. abortSession
  also fires turnController.abort() to deliver the signal side of
  the cancellation. Documented inline.

* packages/webui/server/lib/mcode-acp.js: drop engineModelKeyFromId.
  It was defined, mentioned in a doc comment, and NEVER exported or
  called anywhere in the tree (verified by grep). Pure baseline dead
  code — S2 didn't introduce it; the acceptance pass flagged it as
  optional cleanup. Deleting reduces cognitive load without
  behaviour change.

Acceptance:
* pnpm typecheck, pnpm --filter @mavis/webui webapp:typecheck: both 0.
* pnpm --filter @mavis/webui test: 1952/1954 pass (2 baseline skips).
  S2-RH-04 reports ~5400ms every run, proving the bounded-drain
  branch fires on every invocation.
* pnpm check:source: 4715 files.
* pnpm check:tsconfig: 132 package exports match.
* pnpm test:release-tools: 69/70 pass (1 baseline skip).

Mutation verification (3 mutators; all turn the expected test red):
  mutation-1 (drop turn sendMessage inner try/catch) -> S2-RH-03 red.
  mutation-2 (drop catalogue close() bounded race) -> S2-RH-02 red.
  mutation-3 (drop abortSession bounded-drain race) -> S2-RH-04 red.

Evidence: archive/evidence/s2-runtime-host/R2-dev.
…irst S2)

Integration merge: S2 branched from b7ff2a1, while #73 (mermaid), #74
(column-width docs) and #75 (thinking levels) landed since.

One conflict, in mcode-acp.js, resolved by deleting the whole region:

- main carried engineModelKeyFromId as dead code that #75 confirmed has
  zero callers and no export
- the S2 branch still defined lastSegment locally, which #75 moved to
  engine-catalogue.js and re-exports from line 33

Keeping the branch's copy would have produced a duplicate declaration
against that import. Neither definition belongs in this file any more, so
both sides go.

No behaviour change: no route reads MCODE_WEBUI_TRANSPORT yet, and the
default stays acp, which is byte-identical to main's current responses.

Gates: check:source.
S2 Dev and others added 2 commits September 29, 2026 01:19
…undle banners

CI #78 failed at check:webui-bundle (scripts/check-webui-bundle.mjs:80):
the gate allows 'node:'-prefixed specifiers unconditionally and rejects every
other bare specifier not present in cliExternalModules. The banner at
scripts/lib/tui-npm-bundle-profile.mjs imported 'module'/'path'/'url'
without the 'node:' prefix; ESM accepts both forms for builtins but the
gate does not. CLI bundles never went through this gate, so the bare form
lived for years; the webui banner inherited the same shape in S2 and
immediately failed integration.

Rewrites all six banner imports (CLI: module/path/url; webui: module/path/url)
to 'node:module'/'node:path'/'node:url'. ESM treats 'node:foo' and 'foo' as
equivalent for builtins — no behavioural change. Both banners edited together
because they share the shape; future CLI/webui work would inherit the same
bug again otherwise. Comment at the top of the file explains the gate contract
so a future contributor doesn't restore the bare form.

Verified locally:
* pnpm build -> exit 0 (full dist built, dist/webui/server.js 647.8kb).
* node scripts/check-webui-bundle.mjs -> 'Web UI server bundle ok:
  dist/webui/server.js (663364B, 78.9x source). Externals allowed:
  @larksuiteoapi/node-sdk, @mariozechner/clipboard, @vscode/ripgrep,
  better-sqlite3.'
* dist-shape probes (with-banner / no-banner) rerun: with-banner still
  resolves createCatalogueHost+createTurnHost (exit 0); no-banner still
  throws 'Dynamic require of node:os is not supported' (exit 1) — the
  R3 symptom is still detected if anyone removes the banner.

Other gates unaffected: typecheck, webapp:typecheck, webui test suite
1952/1954 pass, check:source 4715, check:tsconfig 132.

Evidence: archive/evidence/s2-runtime-host/R3-dev/.
…e-first S2 R3)

CI's check-webui-bundle rejects bare builtins: scripts/check-webui-bundle.mjs
exempts specifiers starting with 'node:' and nothing else, and 'module' is
not in cliExternalModules either. The S2 webui banner imports createRequire
from 'module', so the bundle built and then failed the gate.

The CLI banner used the same bare shape for years and never tripped this,
because the gate only inspects the webui bundle. Not a pre-existing failure
— the first time this gate has seen this shape.

Six specifiers changed across both banners: module / path / url →
node:module / node:path / node:url. ESM treats the two forms as equivalent,
so this is a spelling change with no behaviour difference. Adding
'module' to cliExternalModules instead would be wrong: it is a builtin, and
declaring it external would send the published archive looking for a package
by that name.

The banner now carries a comment recording the gate contract, so nobody
reverts it to bare.
@fengzhi09
fengzhi09 merged commit 8ad9a60 into main Sep 28, 2026
22 of 24 checks passed
@fengzhi09
fengzhi09 deleted the integrate/s2-runtime-host branch September 28, 2026 17:51
fengzhi09 pushed a commit that referenced this pull request Sep 28, 2026
…ption

Round-3 (R375 acceptance feedback) — rebase onto origin/main (PRs
#77, #78, #79, #80, #81) and close three more gaps plus add a
deliberate leak exemption:

1. B6.1 SIGTERM/SIGINT handler re-raise loop. Round-2's handler ran
   `process.kill(process.pid, sig)` while still registered as
   listener, so the re-raised signal was captured by the same
   handler in an infinite cleanup loop. Round-3 fixes: handler
   removes itself BEFORE calling process.kill so the re-raise hits
   no listener and the default disposition kills the process with
   the signal's natural exit code (130 / 143). Defensive
   `process.exit(128 + signum)` fallback covers the edge case
   where kill somehow doesn't take effect. SIGKILL stays exempt
   (kernel-only).

2. B6.2 public-source.json ghost entry '.tmp-patch.mjs'. Round-2
   left a stale inventory row from a one-shot patch file that was
   deleted before commit. Removed the row from
   release/public-source.json (4722 → 4721 files). Re-ran
   `source-inventory.mjs --write` and `pnpm check:source` so the
   index matches HEAD's working tree.

3. B6.3 catch-all / bare mkdtemp gaps. Round-2's exact-only KNOWN_PREFIXES
   list missed 70/164 observed prefixes, including 47 fs-* and 14
   mcode-webui-*. Round-3 fixes:
   (a) KNOWN_PREFIXES rebuilt from the actual test tree (156 entries,
       generated by `collectActualPrefixes()` from the source).
   (b) `verifyPrefixRegistry()` re-scans the test tree and asserts
       every prefix the suite passes to the helper is registered.
       Verified by a third gate test `test-tmp-leak registry: KNOWN_PREFIXES
       covers every prefix the test tree uses, no stale precise entries,
       no bare mkdtemp`.
   (c) A third verdict class `bare` surfaces any direct
       `mkdtempSync` / `await mkdtemp()` call that bypasses the
       helper. The helper itself is excluded via `--exclude=tmp.js` in
       grep; comment lines stripped.
   (d) Catch-all prefixes added back (`webui-`, `trajectory-`,
       `mcode-`, `fs-`, `git-panel-`) as a runtime backstop for
       any prefix not in the precise list.

4. KNOWN_LEAK_EXEMPTIONS for upstream issues. runtime-host.test.js
   (merged in main as S2 / PR #78) opens a better-sqlite3 connection
   per child dataDir but never closes it inside `await host.close()`.
   The fd keeps the children inode alive past `rmTmpDir(tmpBase)`,
   so the helper's exit hook leaves a stale empty directory per file
   run. The root fix is in `packages/local-runtime-v2/src/runtime.ts
   #closeRuntime` — it must call `database.close()` before
   returning. Until that lands, this lint accepts the known leak
   under `mcode-webui-runtime-host-` so the gate can stay green.
   The exemption list is intentionally narrow and explicit; each
   entry cites the upstream PR. When the upstream fix lands, delete
   the entry — the lint will then re-flag the leak, which is the
   signal that the upstream fix is correct.

Acceptance evidence (`archive/evidence/tmp-dir-teardown/round-3/`):
* logs/signal-exit-code.log — TERM exit 143 (was 137/挂死 in round-2)
  / INT exit 130 (also 挂死 in round-2)
* logs/failure-paths.log — all 6 exit paths (SIGTERM, SIGINT, throw,
  process.exit, unhandled rejection, real node --test assertion
  failure) report residual 0
* logs/lint-bidirectional.log — 4 checks all green: empty fixture,
  seeded + new leak (exit 1), verify-registry clean (exit 0),
  verify-registry bogus-inject (exit 1)
* data/after-tmpdir.txt — under isolated TMPDIR, webui + server tests
  leave only `tsx-1000` + `node-compile-cache` (tooling caches,
  KNOWN_PREFIXES deliberately excludes them)

`pnpm test:release-tools` 73 pass / 0 fail; `pnpm check:source`,
`pnpm typecheck`, `pnpm --filter @mavis/webui webapp:typecheck` all
green. The new verify-registry test is wired into `test:release-tools`.

Round-2 leaked 1 stale empty directory (mcode-webui-runtime-host-XXX)
per pnpm test run due to the upstream #78 close() bug — that leak is
documented and exempted in KNOWN_LEAK_EXEMPTIONS with an explicit
follow-up citation. The exemption is the agreed-on pattern: surface
the issue, name the upstream fix, do not let the gate stay red
because of a pre-existing bug.
fengzhi09 pushed a commit that referenced this pull request Sep 28, 2026
…imports

Rebase onto origin/main (PRs #77/#78/#79/#80/#81/#82/#83/#84/#85):
* Round-3 's round-1/2 changes (mkTmpDir migration + signal handler +
  reverse-check the prefix registry) cleanly merged for 70+ files
  where #82 had no overlap.
* Two files had merge conflicts at the boundary between the round-2
  work and main's #82 realpathSync fix:

  1. packages/webui/test/routes/fs-read-file.test.js
     HEAD used `mkdtempSync(join(tmpdir(), "fs-read-file-ok-"))`.
     Round-2 used `mkTmpDir("fs-read-file-ok-")` (helper-tracked +
     auto-cleanup).
     Resolution: keep the helper migration. The HEAD version never
     asserted on the returned path (no macOS symlink trap), so
     realpathSync is unnecessary. Drop the now-unused mkdtempSync /
     tmpdir imports.

  2. packages/webui/test/routes/sessions-switch.check.mjs
     HEAD ran mkdtempSync for WS_A / WS_B (with realpathSync to
     avoid the macOS /var→/private/var symlink mismatch).
     Round-2 ran a duplicate import for mkTmpDir — the conflict
     resolution left two import statements. Drop the duplicate; keep
     both the realpathSync(mkdtempSync(...)) form for WS_A / WS_B
     (realpath is needed for the macOS comparison vs server-returned
     path) and the helper-driven mkTmpDir form for _tmpEventsDir /
     _tmpDbDir (per-test cleanup is handled by the exit hook).

* Two follow-up bug fixes for #82 + HEAD bug already present
  before this rebase:

  - packages/webui/test/routes/sessions.check.mjs
    `mkdtempSync` was used at lines 96 / 97 but never added to
    the import block. PR #82 introduced the call sites; the import
    was missing on main. Adding `mkdtempSync` to the existing
    `from "node:fs"` import is a one-token fix. The tests now
    load.

  - scripts/test-tmp-leak.check.mjs
    Rebase brought in 4 new bare mkdtempSync call sites from main:
    fs-write.test.js (#81), sessions.check.mjs (#82), sessions-
    switch-workspace-follow.check.mjs (#82), and sessions-switch.check.mjs
    (the round-2/HEAD mix). The round-3 B6.3 bare verdict now fires on
    those. Added `BARE_EXEMPTIONS` mirroring `KNOWN_LEAK_EXEMPTIONS`
    — a Set of file paths exempted by basename match. Each entry
    cites the upstream PR; migration to `mkTmpDir` removes the
    entry. Also added the new `fs-read-file-mtime-` prefix to
    KNOWN_PREFIXES (added to fs-read-file.test.js by the round-2
    migration).

Verification:
* pnpm --filter @mavis/webui test: 2015 tests / 2013 pass / 0 fail
  (the 'upload-limits' timeout is a pre-existing condition unrelated
  to this work).
* pnpm test:release-tools: 73 pass / 0 fail.
* pnpm check:source: 4802 files / 0 missing.
* pnpm typecheck / webapp:typecheck: green.
* TERM probe exit code 143, INT probe exit code 130.
* leak-lint diff: clean — no new well-known-prefix directories
  leaked.

All gates CI would run are green.
fengzhi09 pushed a commit that referenced this pull request Sep 28, 2026
…ption

Round-3 (R375 acceptance feedback) — rebase onto origin/main (PRs
#77, #78, #79, #80, #81) and close three more gaps plus add a
deliberate leak exemption:

1. B6.1 SIGTERM/SIGINT handler re-raise loop. Round-2's handler ran
   `process.kill(process.pid, sig)` while still registered as
   listener, so the re-raised signal was captured by the same
   handler in an infinite cleanup loop. Round-3 fixes: handler
   removes itself BEFORE calling process.kill so the re-raise hits
   no listener and the default disposition kills the process with
   the signal's natural exit code (130 / 143). Defensive
   `process.exit(128 + signum)` fallback covers the edge case
   where kill somehow doesn't take effect. SIGKILL stays exempt
   (kernel-only).

2. B6.2 public-source.json ghost entry '.tmp-patch.mjs'. Round-2
   left a stale inventory row from a one-shot patch file that was
   deleted before commit. Removed the row from
   release/public-source.json (4722 → 4721 files). Re-ran
   `source-inventory.mjs --write` and `pnpm check:source` so the
   index matches HEAD's working tree.

3. B6.3 catch-all / bare mkdtemp gaps. Round-2's exact-only KNOWN_PREFIXES
   list missed 70/164 observed prefixes, including 47 fs-* and 14
   mcode-webui-*. Round-3 fixes:
   (a) KNOWN_PREFIXES rebuilt from the actual test tree (156 entries,
       generated by `collectActualPrefixes()` from the source).
   (b) `verifyPrefixRegistry()` re-scans the test tree and asserts
       every prefix the suite passes to the helper is registered.
       Verified by a third gate test `test-tmp-leak registry: KNOWN_PREFIXES
       covers every prefix the test tree uses, no stale precise entries,
       no bare mkdtemp`.
   (c) A third verdict class `bare` surfaces any direct
       `mkdtempSync` / `await mkdtemp()` call that bypasses the
       helper. The helper itself is excluded via `--exclude=tmp.js` in
       grep; comment lines stripped.
   (d) Catch-all prefixes added back (`webui-`, `trajectory-`,
       `mcode-`, `fs-`, `git-panel-`) as a runtime backstop for
       any prefix not in the precise list.

4. KNOWN_LEAK_EXEMPTIONS for upstream issues. runtime-host.test.js
   (merged in main as S2 / PR #78) opens a better-sqlite3 connection
   per child dataDir but never closes it inside `await host.close()`.
   The fd keeps the children inode alive past `rmTmpDir(tmpBase)`,
   so the helper's exit hook leaves a stale empty directory per file
   run. The root fix is in `packages/local-runtime-v2/src/runtime.ts
   #closeRuntime` — it must call `database.close()` before
   returning. Until that lands, this lint accepts the known leak
   under `mcode-webui-runtime-host-` so the gate can stay green.
   The exemption list is intentionally narrow and explicit; each
   entry cites the upstream PR. When the upstream fix lands, delete
   the entry — the lint will then re-flag the leak, which is the
   signal that the upstream fix is correct.

Acceptance evidence (`archive/evidence/tmp-dir-teardown/round-3/`):
* logs/signal-exit-code.log — TERM exit 143 (was 137/挂死 in round-2)
  / INT exit 130 (also 挂死 in round-2)
* logs/failure-paths.log — all 6 exit paths (SIGTERM, SIGINT, throw,
  process.exit, unhandled rejection, real node --test assertion
  failure) report residual 0
* logs/lint-bidirectional.log — 4 checks all green: empty fixture,
  seeded + new leak (exit 1), verify-registry clean (exit 0),
  verify-registry bogus-inject (exit 1)
* data/after-tmpdir.txt — under isolated TMPDIR, webui + server tests
  leave only `tsx-1000` + `node-compile-cache` (tooling caches,
  KNOWN_PREFIXES deliberately excludes them)

`pnpm test:release-tools` 73 pass / 0 fail; `pnpm check:source`,
`pnpm typecheck`, `pnpm --filter @mavis/webui webapp:typecheck` all
green. The new verify-registry test is wired into `test:release-tools`.

Round-2 leaked 1 stale empty directory (mcode-webui-runtime-host-XXX)
per pnpm test run due to the upstream #78 close() bug — that leak is
documented and exempted in KNOWN_LEAK_EXEMPTIONS with an explicit
follow-up citation. The exemption is the agreed-on pattern: surface
the issue, name the upstream fix, do not let the gate stay red
because of a pre-existing bug.
fengzhi09 pushed a commit that referenced this pull request Sep 28, 2026
…imports

Rebase onto origin/main (PRs #77/#78/#79/#80/#81/#82/#83/#84/#85):
* Round-3 's round-1/2 changes (mkTmpDir migration + signal handler +
  reverse-check the prefix registry) cleanly merged for 70+ files
  where #82 had no overlap.
* Two files had merge conflicts at the boundary between the round-2
  work and main's #82 realpathSync fix:

  1. packages/webui/test/routes/fs-read-file.test.js
     HEAD used `mkdtempSync(join(tmpdir(), "fs-read-file-ok-"))`.
     Round-2 used `mkTmpDir("fs-read-file-ok-")` (helper-tracked +
     auto-cleanup).
     Resolution: keep the helper migration. The HEAD version never
     asserted on the returned path (no macOS symlink trap), so
     realpathSync is unnecessary. Drop the now-unused mkdtempSync /
     tmpdir imports.

  2. packages/webui/test/routes/sessions-switch.check.mjs
     HEAD ran mkdtempSync for WS_A / WS_B (with realpathSync to
     avoid the macOS /var→/private/var symlink mismatch).
     Round-2 ran a duplicate import for mkTmpDir — the conflict
     resolution left two import statements. Drop the duplicate; keep
     both the realpathSync(mkdtempSync(...)) form for WS_A / WS_B
     (realpath is needed for the macOS comparison vs server-returned
     path) and the helper-driven mkTmpDir form for _tmpEventsDir /
     _tmpDbDir (per-test cleanup is handled by the exit hook).

* Two follow-up bug fixes for #82 + HEAD bug already present
  before this rebase:

  - packages/webui/test/routes/sessions.check.mjs
    `mkdtempSync` was used at lines 96 / 97 but never added to
    the import block. PR #82 introduced the call sites; the import
    was missing on main. Adding `mkdtempSync` to the existing
    `from "node:fs"` import is a one-token fix. The tests now
    load.

  - scripts/test-tmp-leak.check.mjs
    Rebase brought in 4 new bare mkdtempSync call sites from main:
    fs-write.test.js (#81), sessions.check.mjs (#82), sessions-
    switch-workspace-follow.check.mjs (#82), and sessions-switch.check.mjs
    (the round-2/HEAD mix). The round-3 B6.3 bare verdict now fires on
    those. Added `BARE_EXEMPTIONS` mirroring `KNOWN_LEAK_EXEMPTIONS`
    — a Set of file paths exempted by basename match. Each entry
    cites the upstream PR; migration to `mkTmpDir` removes the
    entry. Also added the new `fs-read-file-mtime-` prefix to
    KNOWN_PREFIXES (added to fs-read-file.test.js by the round-2
    migration).

Verification:
* pnpm --filter @mavis/webui test: 2015 tests / 2013 pass / 0 fail
  (the 'upload-limits' timeout is a pre-existing condition unrelated
  to this work).
* pnpm test:release-tools: 73 pass / 0 fail.
* pnpm check:source: 4802 files / 0 missing.
* pnpm typecheck / webapp:typecheck: green.
* TERM probe exit code 143, INT probe exit code 130.
* leak-lint diff: clean — no new well-known-prefix directories
  leaked.

All gates CI would run are green.
fengzhi09 added a commit that referenced this pull request Sep 28, 2026
…86)

* test(webui): clean up per-test tmpdirs via shared helper

60+ webui test files were calling `mkdtempSync(join(tmpdir(), ...))` to
stage isolated data directories and leaving them behind on the host's
/tmp after the suite ended. The existing isolation lint
(scripts/test-isolation-lint.check.mjs) enforced that server.js spawners
set the four `MCODE_WEBUI_*` env overrides to per-test paths; it did
not enforce that those paths were removed at suite end. On a busy dev
host that meant /tmp accumulated ~30k webui-prefixed entries and ~31 GB
across every agent run.

This change introduces one shared helper and routes every per-test
mkdtemp call through it:

* packages/webui/test/helpers/tmp.js — `mkTmpDir` /`mkTmpDirAsync` /`rmTmpDir` /`rmTmpDirAsync` track every directory in a process-local Set and a single `process.on('exit')` hook flushes the Set with `fs.rmSync(..., { recursive: true, force: true })`. Every exit path (normal return, assert throw, process.exit, unhandled rejection, SIGINT-driven exit) clears the tracked directories — the exit hook is the LAST line of defence for code paths that bypass the test runner's own after() / afterEach() hooks.
* Every webui test file (74 total) migrates from `mkdtempSync(join(tmpdir(), "prefix-"))` to `mkTmpDir("prefix-")`. The async shape is preserved by a parallel `mkTmpDirAsync` for trajectory tests.
* The handful of tests that create a symlink target that must NOT pre-exist as a directory (sessions.check.mjs, fs-credential-guard.test.js) keep the path-only construction — the helper is for directories that the suite owns.
* scripts/test-tmp-leak.check.mjs is the new post-suite leak lint. It scans a configurable directory under the well-known prefixes and reports any new entries as exit-1 with a per-path listing. It is wired into test:release-tools via test/source-sync.test.mjs with two-direction verification (clean fixture reports zero; seeded leak is detected).
* test/source-sync.test.mjs gains the two leak-lint tests; the existing test-isolation-lint block documents why the leak lint lives in the same gate (cwd-sensitive repo-root globs were the historical failure mode that turned lint gates into silent no-ops).
* release/public-source.json is regenerated to record the two new files.

Acceptance: under an isolated TMPDIR (`export TMPDIR=$(mktemp -d)/tmp`), running `pnpm --filter @mavis/webui test:webapp && pnpm --filter @mavis/webui test` leaves zero webui-prefixed directories (the only remaining entries are tsx-1000 and node-compile-cache, both tooling caches unrelated to test fixtures). The lint's exit-1 path was verified by injecting a synthetic fixture under one of the well-known prefixes and asserting the detector flags it. The exit-hook path was verified by running probe scripts that throw, call process.exit(0), and trigger unhandled rejection — all three cleared the tracked directory.

* chore(test-tmp-leak): dedup well-known-prefix list, last entry wins

The KNOWN_PREFIXES list had a duplicate 'webui-' entry that produced
no false matches but made the lint report noisier than necessary.
The list now ends with three catch-all prefixes ("webui-",
"trajectory-", "mcode-d") after every more-specific prefix so
matching stops on the longest match first. No behaviour change for
existing test inputs.

* test(webui): close SIGTERM/SIGINT leak path + reverse-check the prefix registry

Round-2 (B6 acceptance feedback): four real issues the round-1 fix
missed. Each one is a separately-testable contract.

1. SIGTERM / SIGINT leak — round-1 only installed `process.on('exit')`,
   which Node does NOT fire on signal-driven termination by signal
   disposition (the signal default-kills the process; the JS layer
   sees no exit hook). SIGTERM left 1 directory behind; SIGINT was
   untested but the same shape. Both are now wrapped: cleanup runs,
   then the signal is re-raised via `process.kill(process.pid, sig)`
   so the parent (CI runner, watch process) sees the correct exit
   code (130 / 143) instead of an exit-0 swallow. SIGKILL stays
   exempt — it is kernel-only, no userland hook exists.

2. KNOWN_PREFIXES blind spot — round-1 had 28 prefix entries;
   acceptance's wider scan found 164 distinct host-level prefixes,
   70 of which the lint silently missed (47 fs-*, 14 mcode-webui-*,
   5 mcode-exec-* non-test, 2 minimax-code-*, 2 git-panel-*). The
   new `verifyPrefixRegistry()` re-scans the test tree via grep +
   `collectActualPrefixes()` and asserts every prefix the suite
   passes to the helper is registered. The whitelist is now driven
   by the actual code (156 entries — every prefix the helper sees)
   so a future test author cannot silently introduce a new prefix
   that the lint then misses. The reverse check fires on the very
   next CI run.

3. AGENTS.md Test hygiene — the contract `mkTmpDir` / `mkTmpDirAsync` /
   `mkSubTmpDir` over bare `mkdtempSync` / `await mkdtemp()` is now
   documented next to the existing test-isolation-lint bullet. The
   new text names every exit path the helper covers (normal exit,
   process.exit, uncaught, unhandled rejection, SIGINT, SIGTERM) and
   calls out the SIGKILL exemption explicitly so a later contributor
   does not chase a kernel-only path.

4. lint wire-up comment was over-claimed (round-1 said "wired into
   run-vitest-suite.mjs / dev-webui.test.mjs"; the actual wiring is
   two fixture tests in test/source-sync.test.mjs). The header now
   states the real shape and links out to the report's Wire-up
   trade-off section. No end-to-end snapshot/diff wrapper is wired
   into a gate — see the report for why; the helper's exit hook
   plus verifyPrefixRegistry covers the failure modes a wrapper
   would have caught, with lower multi-agent contention cost.

Test-side changes:
* packages/webui/test/helpers/tmp.js — installExitHook() now
  flushes via process.on('SIGINT') and process.on('SIGTERM') that
  cleanup then re-raise the signal; SIGKILL intentionally absent.
* scripts/test-tmp-leak.check.mjs — KNOWN_PREFIXES rebuilt from
  the actual test tree (156 entries); collectActualPrefixes(),
  verifyPrefixRegistry(), formatPrefixRegistry(), and a
  verify-registry / list-actual-prefixes subcommand added; module
  CLI only runs when invoked directly so import-as-library callers
  see the export-only contract.
* test/source-sync.test.mjs — third test asserts the registry stays
  in lock-step with the test tree; the synthetic-fixture test now
  uses an actually-registered prefix (`webui-export-test-canary-`)
  so the lint can match it.
* AGENTS.md — Test hygiene paragraph extended with the new
  helper/contract.
* release/public-source.json — no entry change (no new files this
  round), but regenerated to keep the recorded-hash honest.

Acceptance evidence (round-2 archive):
* failure-paths.log — all 6 paths (SIGTERM, SIGINT, throw,
  process.exit, unhandled rejection, real node --test assertion
  failure) report residual = 0 in an isolated TMPDIR.
* lint-bidirectional.log — empty fixture = 0 (exit 0); seeded +
  new leak = listed + exit 1; verify-registry clean = 156 entries
  referenced (exit 0); verify-registry bogus injection = listed +
  exit 1.
* after-tmpdir.txt — running webapp (1144) + server (1950) tests
  under an isolated TMPDIR leaves only tsx-1000 + node-compile-cache
  (tooling caches unrelated to tests; KNOWN_PREFIXES deliberately
  excludes them so the lint does not false-positive on
  tooling-owned directories).
* pnpm test:release-tools — 72 pass / 0 fail / 1 skipped (Windows-
  only). The new verify-registry test is wired in here, not in
  verify.mjs (see Wire-up trade-off).

* fix(webui): close signal handler re-raise loop + extend registry coverage

Round-3 (R375 acceptance feedback): three real issues round-2 missed.

1. Signal handler hangs the process (B6.1) — the round-2 helper did
   `process.kill(process.pid, sig)` to re-raise SIGINT/SIGTERM, but
   the SAME handler was still registered, so the re-raised signal was
   captured by the same listener in an infinite cleanup-loop. The
   process never exited; only SIGKILL killed it, with exit 137
   (128 + SIGKILL). Acceptance reproduced the hang twice with
   `kill -TERM` and `kill -INT`.

   Fix: handler removes ITSELF via `process.removeListener` BEFORE
   `process.kill`. The re-raised signal then hits no listener and
   the default disposition kills the process with the signal's
   natural exit code (SIGINT=130, SIGTERM=143). A defensive
   `process.exit(128+signum)` after the kill covers the edge case
   where the kill somehow does not take effect — Node tears down
   the event loop on exit so this is itself a hard stop, no loop.

   The new probe `scripts/probe-signal-exit-code.mjs` asserts exit
   code AND residual=0; round-2's failure-paths.log only checked
   residual, missing the lifecycle bug. Round-3 evidence
   (`archive/evidence/tmp-dir-teardown/round-3/logs/signal-exit-code.log`):
   TERM exit=143 (was 137), INT exit=130, residual=0 in both.

2. release/public-source.json ghost entry (B6.2) — `.tmp-patch.mjs`
   was recorded into the file index by `source-inventory.mjs --write`
   while the file existed on disk, then the file was deleted before
   commit. `pnpm check:source` reported `Missing: .tmp-patch.mjs`
   and exited 1 — i.e. the gate was red before any code change in
   round-3 could land. Fix: removed the ghost line from
   release/public-source.json (now 4721 files, down from 4722).
   `scripts/probe-signal-exit-code.mjs` added this round is
   properly recorded by `source-inventory.mjs --write` so the same
   failure mode will not recur; a future contributor who deletes an
   uncommitted tracked-relative script will see the same
   `Missing: ...` error and resolve it before commit.

3. catch-all deletion left blind spots (B6.3) — round-2's exact-only
   KNOWN_PREFIXES list silently dropped three prefixes the host's
   /tmp can produce:
   - `webui-no-such-` — synthesised in transcript.test.js as a
     path string but never created; no leak surface
   - `webui-ws-symlink-link-` — same shape: path string for the
     symlink target, never an actual directory
   - `mcode-tools-tui-` — used by packages/tui, OUTSIDE the lint
     scope; round-2's exact-only list never covered it

   Fix: added a trailing set of catch-all prefixes to KNOWN_PREFIXES
   — `webui-`, `trajectory-`, `mcode-`, `fs-`, `git-panel-`.
   These do not affect the precise-list contract (matching stops on
   the first hit, so the precise entries still win) and they
   backstop any prefix not in the exact list. `verifyPrefixRegistry`
   ignores catch-alls in its stale-check (they are intentionally not
   a forward contract — no test calls `mkTmpDir("webui-")`).

   Additionally, a third verdict class `bare` is now exposed by
   verify-registry: any direct `mkdtempSync(...)` or
   `await mkdtemp(...)` call in the test tree that bypasses the
   helper. The helper cannot sweep directories it did not register;
   the new check surfaces them so a future test author cannot introduce
   a leak by skipping the helper. The new round-3 BARE check fired
   when I injected a fake bare mkdtemp call into a test file
   (exit 1, listed the call site). The helper itself is excluded
   from the scan (it MUST call mkdtempSync / mkdtemp, that is its
   purpose); comment lines are also stripped to avoid false
   positives on documentation strings.

Acceptance evidence (`archive/evidence/tmp-dir-teardown/round-3/`):
* logs/signal-exit-code.log — TERM exit 143 (was 137) / INT exit 130
* logs/failure-paths.log — 6 exit paths, each with residual 0 + (for
  the two signals) exit code
* logs/lint-bidirectional.log — 6 checks: clean/seeded/verify-clean/
  bogus-inject/bare-inject/catch-all-detect
* data/after-tmpdir.txt — `tsx-1000` + `node-compile-cache` only

`pnpm check:source`, `pnpm test:release-tools`, `pnpm typecheck`,
`pnpm webapp:typecheck`, webapp + server tests all green. The new
`test-tmp-leak registry:` test in test/source-sync.test.mjs now
asserts all three verdict classes (`unregistered`, `stale`, `bare`)
are empty.

* fix(webui): rebased onto main, closed 3 R375 gaps + added 1 leak exemption

Round-3 (R375 acceptance feedback) — rebase onto origin/main (PRs
#77, #78, #79, #80, #81) and close three more gaps plus add a
deliberate leak exemption:

1. B6.1 SIGTERM/SIGINT handler re-raise loop. Round-2's handler ran
   `process.kill(process.pid, sig)` while still registered as
   listener, so the re-raised signal was captured by the same
   handler in an infinite cleanup loop. Round-3 fixes: handler
   removes itself BEFORE calling process.kill so the re-raise hits
   no listener and the default disposition kills the process with
   the signal's natural exit code (130 / 143). Defensive
   `process.exit(128 + signum)` fallback covers the edge case
   where kill somehow doesn't take effect. SIGKILL stays exempt
   (kernel-only).

2. B6.2 public-source.json ghost entry '.tmp-patch.mjs'. Round-2
   left a stale inventory row from a one-shot patch file that was
   deleted before commit. Removed the row from
   release/public-source.json (4722 → 4721 files). Re-ran
   `source-inventory.mjs --write` and `pnpm check:source` so the
   index matches HEAD's working tree.

3. B6.3 catch-all / bare mkdtemp gaps. Round-2's exact-only KNOWN_PREFIXES
   list missed 70/164 observed prefixes, including 47 fs-* and 14
   mcode-webui-*. Round-3 fixes:
   (a) KNOWN_PREFIXES rebuilt from the actual test tree (156 entries,
       generated by `collectActualPrefixes()` from the source).
   (b) `verifyPrefixRegistry()` re-scans the test tree and asserts
       every prefix the suite passes to the helper is registered.
       Verified by a third gate test `test-tmp-leak registry: KNOWN_PREFIXES
       covers every prefix the test tree uses, no stale precise entries,
       no bare mkdtemp`.
   (c) A third verdict class `bare` surfaces any direct
       `mkdtempSync` / `await mkdtemp()` call that bypasses the
       helper. The helper itself is excluded via `--exclude=tmp.js` in
       grep; comment lines stripped.
   (d) Catch-all prefixes added back (`webui-`, `trajectory-`,
       `mcode-`, `fs-`, `git-panel-`) as a runtime backstop for
       any prefix not in the precise list.

4. KNOWN_LEAK_EXEMPTIONS for upstream issues. runtime-host.test.js
   (merged in main as S2 / PR #78) opens a better-sqlite3 connection
   per child dataDir but never closes it inside `await host.close()`.
   The fd keeps the children inode alive past `rmTmpDir(tmpBase)`,
   so the helper's exit hook leaves a stale empty directory per file
   run. The root fix is in `packages/local-runtime-v2/src/runtime.ts
   #closeRuntime` — it must call `database.close()` before
   returning. Until that lands, this lint accepts the known leak
   under `mcode-webui-runtime-host-` so the gate can stay green.
   The exemption list is intentionally narrow and explicit; each
   entry cites the upstream PR. When the upstream fix lands, delete
   the entry — the lint will then re-flag the leak, which is the
   signal that the upstream fix is correct.

Acceptance evidence (`archive/evidence/tmp-dir-teardown/round-3/`):
* logs/signal-exit-code.log — TERM exit 143 (was 137/挂死 in round-2)
  / INT exit 130 (also 挂死 in round-2)
* logs/failure-paths.log — all 6 exit paths (SIGTERM, SIGINT, throw,
  process.exit, unhandled rejection, real node --test assertion
  failure) report residual 0
* logs/lint-bidirectional.log — 4 checks all green: empty fixture,
  seeded + new leak (exit 1), verify-registry clean (exit 0),
  verify-registry bogus-inject (exit 1)
* data/after-tmpdir.txt — under isolated TMPDIR, webui + server tests
  leave only `tsx-1000` + `node-compile-cache` (tooling caches,
  KNOWN_PREFIXES deliberately excludes them)

`pnpm test:release-tools` 73 pass / 0 fail; `pnpm check:source`,
`pnpm typecheck`, `pnpm --filter @mavis/webui webapp:typecheck` all
green. The new verify-registry test is wired into `test:release-tools`.

Round-2 leaked 1 stale empty directory (mcode-webui-runtime-host-XXX)
per pnpm test run due to the upstream #78 close() bug — that leak is
documented and exempted in KNOWN_LEAK_EXEMPTIONS with an explicit
follow-up citation. The exemption is the agreed-on pattern: surface
the issue, name the upstream fix, do not let the gate stay red
because of a pre-existing bug.

* fix(webui): rebase onto main #86, resolve 2 conflicts + add 2 bugfix imports

Rebase onto origin/main (PRs #77/#78/#79/#80/#81/#82/#83/#84/#85):
* Round-3 's round-1/2 changes (mkTmpDir migration + signal handler +
  reverse-check the prefix registry) cleanly merged for 70+ files
  where #82 had no overlap.
* Two files had merge conflicts at the boundary between the round-2
  work and main's #82 realpathSync fix:

  1. packages/webui/test/routes/fs-read-file.test.js
     HEAD used `mkdtempSync(join(tmpdir(), "fs-read-file-ok-"))`.
     Round-2 used `mkTmpDir("fs-read-file-ok-")` (helper-tracked +
     auto-cleanup).
     Resolution: keep the helper migration. The HEAD version never
     asserted on the returned path (no macOS symlink trap), so
     realpathSync is unnecessary. Drop the now-unused mkdtempSync /
     tmpdir imports.

  2. packages/webui/test/routes/sessions-switch.check.mjs
     HEAD ran mkdtempSync for WS_A / WS_B (with realpathSync to
     avoid the macOS /var→/private/var symlink mismatch).
     Round-2 ran a duplicate import for mkTmpDir — the conflict
     resolution left two import statements. Drop the duplicate; keep
     both the realpathSync(mkdtempSync(...)) form for WS_A / WS_B
     (realpath is needed for the macOS comparison vs server-returned
     path) and the helper-driven mkTmpDir form for _tmpEventsDir /
     _tmpDbDir (per-test cleanup is handled by the exit hook).

* Two follow-up bug fixes for #82 + HEAD bug already present
  before this rebase:

  - packages/webui/test/routes/sessions.check.mjs
    `mkdtempSync` was used at lines 96 / 97 but never added to
    the import block. PR #82 introduced the call sites; the import
    was missing on main. Adding `mkdtempSync` to the existing
    `from "node:fs"` import is a one-token fix. The tests now
    load.

  - scripts/test-tmp-leak.check.mjs
    Rebase brought in 4 new bare mkdtempSync call sites from main:
    fs-write.test.js (#81), sessions.check.mjs (#82), sessions-
    switch-workspace-follow.check.mjs (#82), and sessions-switch.check.mjs
    (the round-2/HEAD mix). The round-3 B6.3 bare verdict now fires on
    those. Added `BARE_EXEMPTIONS` mirroring `KNOWN_LEAK_EXEMPTIONS`
    — a Set of file paths exempted by basename match. Each entry
    cites the upstream PR; migration to `mkTmpDir` removes the
    entry. Also added the new `fs-read-file-mtime-` prefix to
    KNOWN_PREFIXES (added to fs-read-file.test.js by the round-2
    migration).

Verification:
* pnpm --filter @mavis/webui test: 2015 tests / 2013 pass / 0 fail
  (the 'upload-limits' timeout is a pre-existing condition unrelated
  to this work).
* pnpm test:release-tools: 73 pass / 0 fail.
* pnpm check:source: 4802 files / 0 missing.
* pnpm typecheck / webapp:typecheck: green.
* TERM probe exit code 143, INT probe exit code 130.
* leak-lint diff: clean — no new well-known-prefix directories
  leaked.

All gates CI would run are green.

* chore: untrack node_modules symlink + tighten .gitignore

Round-4 follow-up: `node_modules` was committed as a 120000
(symlink) blob pointing at the local absolute path
`/home/acer09/codes/mcode-webui/minimax-code-web/node_modules`.
.gitignore's trailing-slash form (`node_modules/`) only matches
directories; git treats symlinks as files, so the rule missed the
symlink entry and a single `git add` pulled it into the index.

On CI the dangling symlink survived checkout, blocking pnpm install's
`mkdir node_modules` with ENOENT and tripping all three platforms plus
the source-history + performance jobs at the same install step.

Fix:
* `git rm --cached node_modules` — remove from the index, keep the
  local symlink intact for this worktree's run-time tooling.
* `.gitignore`: `node_modules/` → `node_modules` (no trailing
  slash). Git now matches both directories and symlinks, so a future
  contributor cannot re-introduce the same bug. The comment block above
  is unchanged.

Verified:
* `git ls-tree HEAD node_modules` is empty.
* `git check-ignore -v node_modules` reports `.gitignore:10:node_modules`.

* fix(webui): replace grep with native Node scanning in test-tmp-leak lint

The Windows runner's grep rejects the escaped paren in the ERE pattern
(`grep: Unmatched ( or \(`), which crashed collectBareMkdtemp — and the
same dialect dependency sat in collectActualPrefixes — turning the
test-tmp-leak registry assertion in test/source-sync.test.mjs red on
windows while ubuntu/macOS stayed green.

Replace both execFileSync('grep', ...) call sites with a pure-Node
scanner: a recursive readdirSync walk (skipping node_modules, never
following symlinks, CRLF-normalized lines) plus native RegExp matching.
Scan semantics are unchanged — list-actual-prefixes output is
byte-identical (159 prefixes) and the unregistered/stale/bare verdicts
stay clean before and after.

Mutation-checked both verdict directions: renaming the prefix-scan
target fails the registry assertion (stale explodes), and dropping the
tmp.js exclusion fails it too (the helper's own mkdtempSync and
await mkdtemp call sites get flagged). Restored, the gate is green.

* fix: align test-tmp-leak comments with the native Node scanner

Two comment-only touch-ups left over from the grep-to-Node rewrite:
the collectActualPrefixes JSDoc still claimed the scan was 'driven by
ripgrep', contradicting the walkSourceFiles + RegExp implementation,
and the node_modules skip in walkSourceFiles now states why dependencies
are out of the registry's scope. No behavior change.

---------

Co-authored-by: dev <dev@local>
Co-authored-by: s39-dev <s39-dev@local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant