The zerolag bundle exports only `XtermZerolagInput`, but app.js constructs
`new LocalEchoOverlay(terminal)` directly. scripts/build.mjs appends global
aliases after esbuild (build.mjs:53-66); the first version of this script
omitted that step.
Without them initTerminal() throws `LocalEchoOverlay is not defined` at the
line that builds the overlay — and because that is midway through the function,
EVERY later step silently never runs, including the mobile touch handlers on
#terminalContainer. The page still had a terminal, so the failure looked like a
tap-routing bug rather than a boot error.
Verified: boot errors none, and all four terminalContainer touch listeners
(touchstart/touchmove/touchend/touchcancel) now register.
The mobile suite drives a real browser against a WebServer started from
TypeScript source, so fastify-static serves join(__dirname, 'public') =
src/web/public — not dist/web/public, where `npm run build` puts the vendor
bundles. Every /vendor/xterm* request 404s, so `Terminal` is never defined,
initTerminal() never runs, and any test touching app.terminal dies with
"Cannot read properties of null".
Measured in one worktree, toggling only the vendor files:
before: 404s=5 Terminal=undefined app.terminal=null 8 failed | 26 passed
after: 404s=0 Terminal=function app.terminal=live 6 failed | 28 passed
The 6 remaining failures are genuine pre-existing bugs (stale layout and
accessory-bar expectations, a CJK timeout) and are left alone here.
This went unnoticed because config/vitest.ci.config.ts excludes test/mobile/**,
so CI never ran the suite. `npm run test:mobile` now runs it, with a pretest
hook that builds the bundles.
The asset list was derived from the actual 404s rather than from build.mjs —
which is how xterm-addon-unicode11 and xterm-zerolag-input got included; reading
the build file alone would have missed both. Outputs go to the gitignored
src/web/public/vendor/, so they stay build artifacts. The script is idempotent
(skips outputs newer than their source) and does not touch the normal build.
Full CI suite unchanged: 4368 passed.
Two defects in the background-task hook scripts.
SubagentStop had no handler at all. When a subagent launched background work and
one watcher ended while others were still running, Claude could publish the
worker's last progress sentence as its final result, abandoning the live tasks.
A new guard pairs launched task IDs against completed ones and confirms liveness
by scanning /proc/<pid>/fd for an open tasks/<id>.output handle, blocking the
stop only while genuinely-live work remains. It fails open — allowing the stop —
when /proc is unavailable, nothing was launched, or everything finished.
The rewake helper watched only input.transcript_path. A subagent has its own
transcript, but Claude writes the completion queue-operation to the PARENT
transcript, so the record it waited for never appeared and the wake never fired.
It now watches both paths, but only when the relationship is provable: the
transcript's parent directory is subagents/ and its grandparent basename equals
input.session_id. It also now requires operation === 'enqueue'.
The rewake marker moves V2 -> V3; refreshStaleCodemanHooks treats absence of the
current marker as stale, so existing cases self-heal on next launch (the same
mechanism as the V1 -> V2 bump). Ownership matches on marker PREFIXES, so a
future bump still recognises older Codeman handlers and never adopts a user's.
12 tests fail on unmodified master, e.g.
expected '[{"matcher":"Bash",…' to contain 'CODEMAN_BACKGROUND_REWAKE_V3'
expected 'Background command bg-report-1 comple…' to contain '<codeman-background-result>'
AiCheckerBase spawned the check with `> out 2>&1`, so anything the Claude CLI
wrote to stderr landed inside the same file the verdict parser reads. A CLI that
failed to start (corrupt settings, missing auth) produced either an empty verdict
or an unparseable one, and the actual cause was destroyed on the way through —
the user saw only "Empty output from AI idle check".
stderr now goes to its own temp file. When output is empty or the verdict cannot
be parsed, the first 200 characters of stderr are appended to the error message.
The file is cleaned up alongside the existing temp files, including on the error
paths.
Two tests, both failing on master:
expected 'export PATH="…' to contain ' 2> "'
expected 'Empty output from AI idle check' to contain 'Claude CLI failed to load settings'
The suite never selects a session, so initTerminal() does not run and both
`app.terminal` and `app.fitAddon` are null at rest. `_scheduleViewportSettle`
returns early on a falsy terminal, so the coalescing assertions could not
reach the behavior they claimed to cover -- the test errored on
`Cannot read properties of null` rather than measuring anything.
Installs the minimum surface the settle callback touches and restores it
afterwards, so the coalescing path executes for real.
Adds a behavioral counterpart driven through the PUBLIC entry point
(`onKeyboardShow`) instead of the internal scheduler: three viewport steps
in quick succession must produce exactly ONE refit. On master that returns
3 (each show arms its own uncoalesced 150ms timeout), so this fails by
COUNT rather than by a missing method -- which is the failure mode that
actually demonstrates the bug.
Verified: `expected 3 to be 1` on unmodified master; passes here. The
remaining 8 failures in this file are pre-existing on master and unrelated
(same null-initialization limitation of the headless harness).