Skip to content

fix: five silently-unwired capabilities — CI pointed at a deleted dir, deps that never re-installed, an engine nobody asked its version, a memory bridge that couldn't find airc, and a plugin frozen 2 weeks behind - #2213

Open
joelteply wants to merge 8 commits into
canaryfrom
fix/ci-clients-gate

Conversation

@joelteply

@joelteply joelteply commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Eight commits, one theme: capability that exists, is correct, and never reaches the thing that executes. Each was invisible because the repo looked right — because it was.

1. The clients suite existed and nothing ran it

The workflow was working-directory: src — the deleted Node monolith — calling build:ts/test:crud. Rewritten to npm ci + typecheck:clients + test:clients on main AND canary.

2. The workspace repairs itself when the manifest moves

install.sh ran npm install once, at install time. Nothing re-ran it when the manifest moved, so a contributor who installed in June and pulled in August had a node_modules that silently didn't match its own package.json — and the first symptom pointed at missing source. Hooked to pre* on the client scripts only; start-server.sh stays headless-Rust by doctrine.

3. One front door: uu is the command

The binary derives its own name from argv[0], so uu --help says uu and any future alias is correct the moment it exists. Reports build number + sha + built-at.

4. The daemon asks the engine what it IS

A wedge got misattributed to "the fork bump in build 4577" — a Rust core build number read as the engine's. Settling it meant comparing binary mtimes on two machines. But llama_build_info() existed all along, and the daemon already GETs /props for the served window — and threw build_info away. Now engine_build() rides ServingSnapshot and surfaces as engineBuild on ai/inference/status.

5. ninja becomes a manifest module; cmake/ninja stop being CUDA's business

Both pins lived inside a nvcc && !cl.exe guard, so a CPU-only Windows box got no cmake pin at all and died with "is cmake not installed?" while cmake sat a directory away. PATH knowledge moved to [module.runtime_path] where the windows-vs-unix split belongs as data. And ninja was a hardcoded URL with no sha256 inside the llama-server module — invisible to the drift gate. Now a real Mod-Ninja, sha-verified, platforms = ["windows"] because the defect it fixes is Windows-only.

6–8. The memory bridge, and why its fixes wouldn't have shipped

Engram recall was silently off: hooks don't inherit the operator's shell, airc lives in ~/.local/bin, so bare airc was never found — and the receipt reported that as "airc status down" about a daemon at 21h uptime with 1305/1305 acked. Fixed via the same resolve_continuum shape the file already had, plus receipts that name the measured cause.

Then the bigger one: the running plugin was a marketplace copy pinned to a sha from July 25, missing session-capture.sh entirely — automatic per-turn capture had never run once. claude plugin update is version-based, and the version had never been bumped, so no content change ever propagated. Bumping 0.1.0 → 0.2.0 shipped two weeks of fixes in one command (verified). Added check-plugin-version.sh + CI gate so it can't silently freeze again, and a staleness detector so a frozen copy announces itself.

Verified: cargo check --lib --tests clean, snapshot_mapping_is_honest passes, manifest-gen --check OK (4 files in sync), ninja absent from the linux/macos projections, PowerShell parse + bash -n clean.

Deliberately not done: the gate is NOT wired into .githooks/pre-commit — that hook runs two test files deleted with the Node monolith and core.hooksPath doesn't point at it, so no pre-commit hook runs at all. Wiring a gate there would look enforced while running never. Flagged, not silently patched.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

…hat should have was pointed at a deleted directory

Three defects that compose into one: the TypeScript client tests were unguarded,
the guard that should have covered them was dead, and on Windows the suite could
not be run at all.

1. THE SUITE WAS RUN BY NO WORKFLOW. `npm run test:clients` was already in
   package.json and already passing. Nothing invoked it. So renderBench.spec.ts
   and six sibling spec files across apps/web and packages/ existed, were green
   on their authors' machines, and gated nothing — a correct check nothing calls,
   indistinguishable from having no check until someone greps for the caller.

2. ci.yml HAD BEEN RED FOR TWO MONTHS. Last success 2026-06-07; failing
   continuously from 2026-06-09 with `Cannot find module 'dotenv'`. Root cause
   was not dotenv: the job ran in `working-directory: src` against
   `src/package-lock.json`, calling `npm run build:ts` and `npm run test:crud`.
   `src/` was the Node monolith, retired when the substrate became a headless
   Rust core (#1840). Every path and script it named is gone; the error was
   describing the absence of its whole world.

   A check that ALWAYS fails is worse than no check — it cannot distinguish a
   broken PR from a healthy one, so the only lesson available is to stop reading
   CI. That is what happened: work routed around it through the Rust and
   drift-guard workflows for two months. Replaced rather than patched, and it now
   watches `canary` as well as `main`, because a gate that only sees the stable
   line learns about breakage after the merge.

3. THE SUITE COULD NOT RUN ON WINDOWS AT ALL. `@continuum/web` and
   `@continuum/chat-view` declared `"test": "TZ=UTC vitest run"`. npm runs
   scripts through cmd.exe on Windows, which has no POSIX env prefix, so both
   died with `'TZ' is not recognized as an internal or external command`. The
   other three workspaces use a plain `vitest run` and were fine — the split is
   exactly the prefix.

   Fixed by REMOVING the portability assumption rather than packaging a tool to
   satisfy it: apps/web already pinned `test: { env: { TZ: 'UTC' } }` in its vite
   config, making the prefix redundant; chat-view had no config at all, so it
   gets one whose only job is that pin. No cross-env dependency added.

   Worth stating: this one is only findable by RUNNING the suite on Windows.
   Linux CI and macOS both pass. Adding the CI gate alone would have gone green
   and left every Windows contributor unable to run the tests locally — the gate
   would have hidden this rather than caught it.

Verified on Windows after the fix: 25 test files, 153 tests, exit 0. Before it,
the two affected workspaces produced no test run whatsoever.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
joelteply and others added 2 commits August 8, 2026 22:30
… client scripts only

`install.sh` runs `npm install` ONCE. Nothing re-ran it when the manifest
changed. So a contributor who installed in June and pulled in August had a
node_modules that silently did not match its own package.json, and the first
symptom was every client spec failing at COLLECTION with:

    Failed to load url @continuum/chat-view. Does the file exist?

which points a newcomer at missing SOURCE rather than at their missing deps.
Measured on a real checkout: tree from Jun 17, manifest from Aug 5, `lit` and
every @continuum/* workspace package absent, seven spec files dead.

WHERE IT HANGS, AND WHY NOT THE START PATH. `start-server.sh` is headless Rust
by doctrine — "No Node, no TS, no widgets. The Node orchestrator stays out of
the loop." A dependency guard wired there would drag npm into the one runtime
path that exists to avoid it. So this hangs off `pre*` hooks on the CLIENT
scripts only: dev:web, dev:desktop, build/lint/typecheck/test:clients. Anyone
who only ever runs the core pays nothing and never sees it execute.

mtime, not a checksum: npm writes node_modules/.package-lock.json when it
materialises the tree, so "tree older than manifest" is exactly the question
worth asking and needs no parsing. A checksum would be more precise about
CONTENT and no more precise about what actually breaks people.

LOUD, never silent. A guard that repairs things without saying so teaches the
operator that installs are magic and hides a real signal — a lockfile moving
under them — that is sometimes worth knowing. Skipped under CI, where `npm ci`
is authoritative and already ran; CONTINUUM_SKIP_DEP_CHECK opts out for anyone
hand-managing a tree.

Verified both directions: silent on a fresh tree, fires with the reason named on
a stale one, no-ops under CI=1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
…its own name

Four names were in play and two were installed:

  uu          ON PATH   core/continuum-core/src/bin/continuum.rs
  continuum   ON PATH   same build, same size, same mtime — one binary, two names
  ctm         absent    declared as [[bin]] in apps/cli/Cargo.toml, never installed
  jtag        absent    used 40 times in CLAUDE.md, installed by nothing (Node era)

So the declared name, the documented name, and the installed names were three
different answers and nothing reconciled them. Joel reached for `uu` and was
right; I nearly "corrected" him from the stale Cargo.toml, which would have been
ratifying a declaration over the live system.

WHAT CHANGED

`program_name()` derives from argv[0]. The usage text was the literal string
"usage: continuum ...", so `uu --help` printed examples for a command the reader
had not typed — the front door did not know its own name. One binary can now
ship under any number of names and each tells the truth; a future alias is
correct the moment it exists, with nothing to remember to update.

`version` is handled LOCALLY and never dispatched. `uu version` used to fall
through to the substrate and return `Unknown command: 'version'` — the CLI could
ask the core what IT was and could not say what ITSELF was, and with no core
running it answered nothing at all. That is the gap that lets someone debug a
fixed bug with an unfixed binary in their hand, which is the whole point of the
version ruling.

CLAUDE.md: 38 `./jtag` invocations rewritten to `uu`. Deliberately NOT rewritten:
`.continuum/jtag/logs/...` paths (3, real directories), the
[[jtag-probes-are-rtos-debugger]] memory slug, and the legacy JTAGClient
snippets — none of those are commands, and a blind replace would have broken
log paths.

The mistake entry itself was inverted and is now the correction: it told every
new session "Always work from `src`" and "Commands: `./jtag` NOT `./continuum`".
`src/` is the retired Node monolith, `jtag` is installed by nothing, and
`continuum` — the one it warned against — works. That is a documentation lie
with a live cost: it is the first thing a fresh agent reads and the first ten
minutes it wastes.

Env vars corrected mid-edit: I wrote CONTINUUM_BUILD_SHA/_BRANCH from memory;
build.rs emits CONTINUUM_BUILD_GIT_SHA and no branch at all. `option_env!` would
have silently printed "unknown" rather than failing, so that would have shipped
as a quiet lie about the build.

cargo check -p continuum-core --bin continuum: 0 errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
@github-actions github-actions Bot added size: L and removed size: M labels Aug 9, 2026
joelteply and others added 2 commits August 9, 2026 12:36
…nfo on the snapshot

A per-slot wedge was attributed to "the fork bump in build 4577". 4577 is the
Rust CORE's build number; the engine has its own, unrelated one, and the fork
engine had not been rebuilt at all. Settling it took comparing binary mtimes on
two machines.

I told M5 the engine had no version surface. That was wrong: llama_build_info()
has existed all along — `--version`, the startup log, `/props.build_info`, and
`system_fingerprint` on every completion. The gap was entirely on OUR side. The
daemon already GETs `/props` for the served window and the modalities verdict,
and threw `build_info` away.

So this is silently-unwired-capability with the polarity reversed: not something
we built and failed to wire, but something upstream hands us free that we drop
on the floor. Same cost — a fact available for the asking gets re-derived by
archaeology, and the derivation was wrong.

- LlamaServerControl::engine_build() reads /props.build_info, default Ok(None)
  so fakes and remote controls stay honest by construction
- rides on ServingSnapshot, stamped at reconcile, probed on the reconcile line
- surfaces as engineBuild on ai/inference/status

The commit sha is the load-bearing half: build numbers are ancestor counts, so
our fork and upstream can both say b6789 and mean different code.

Three assertions pin it: the identity must reach the snapshot; an engine that
cannot say what it is reads as unknown rather than a guess; and a not-live
snapshot never names an engine even when one answered the probe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
… CUDA's business

Two defects, both found by a plain `cargo check` dying with "is `cmake` not
installed?" on a box where cmake was installed a directory away.

1. The cmake + ninja pins lived INSIDE windows-build-env.sh's CUDA block, gated
   on `nvcc present AND cl.exe absent`. Neither is a CUDA concern. A CPU-only
   Windows box (no nvcc) skipped the block and got no cmake pin at all; a shell
   where cl.exe already resolved skipped it for the same reason. Hoisted into
   two self-guarded blocks that run on their own applicability.

   The PATH half is now the manifest's job — [module.runtime_path] on cmake and
   ninja, consumed by the generic loop that was already there. That is where the
   windows-vs-unix split belongs, because it IS a packaging fact: brew/apt put
   these on PATH, the Windows archives do not. Linux/macOS projections carry
   nothing, as they should.

2. ninja was fetched by a HARDCODED url inside the llama-server PowerShell
   module — no manifest entry, no pinned sha256, invisible to the manifest-gen
   drift gate. One tool provisioned by different rules than every other tool is
   exactly the drift the manifest exists to prevent, and an unverified download
   is a supply-chain hole however convenient the url is. Now a real Mod-Ninja
   with the same guard shape, sha256 verification and source-of-truth as
   Mod-CMake, and it runs in install.ps1 beside Mod-CMake instead of only on
   boxes that had already built llama-server with CUDA.

ninja is declared platforms = ["windows"] deliberately. The defect it fixes is
Windows-only (cmake auto-picks the newest VS; "Visual Studio 18 2026" is a
generator cmake 3.30.x cannot name). On unix the default generator is never
broken, so listing macos/linux would make those contributors install a package
to buy nothing. Adapting to the platform means stating the asymmetry, not
smearing one platform's workaround across all three.

Verified: manifest-gen --check OK (4 files in sync), ninja absent from the linux
and macos projections, CMAKE and CMAKE_GENERATOR=Ninja both resolve from a fresh
shell with no CUDA involved, Mod-Ninja/Mod-CMake idempotent-skip, PowerShell
parse + bash -n clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
@github-actions github-actions Bot added size: XL and removed size: L labels Aug 9, 2026
joelteply and others added 3 commits August 9, 2026 13:18
…own" for "program not found"

Engram recall has been silently off on BigMama. Every session start printed
"MEMORY BRIDGE DOWN — could not resolve this agent's persona id (airc status
gave nothing)" while airc had 21h uptime and 1305/1305 acked.

The probe never observed the daemon. Hooks do not inherit the operator's
interactive shell — airc installs to ~/.local/bin, routinely absent from a
hook's PATH — so bare `airc` was not a program it could find, and 2>/dev/null
swallowed the "command not found" that would have said so.

The same file already had resolve_continuum() doing this correctly. Two answers
to "find a binary" in one file, one of them robust, is the drift; now there is
one shape (resolve_airc, honoring $AIRC_BIN, with .exe variants for Windows).
share.sh's two bare `airc` calls go through it too.

And the receipts stop guessing. "No airc binary" and "airc ran and reported
nothing" are different types, not two values of one type — conflating them is
what wrote "airc status down" about a healthy daemon, twice, and sent an
afternoon of diagnosis at the wrong layer. persona_failure_reason() re-derives
the measured cause for both the receipt and the notice the AGENT reads.

It re-derives rather than setting a variable inside resolve_agent_persona
because callers invoke that as $(...) — a subshell, where any assignment dies.
The first version of this fix did exactly that and the receipt came out blank.
The negative test caught it, which is why it exists.

Verified both directions: with ~/.local/bin stripped from PATH (the environment
that failed for two sessions) the id now resolves; with no airc reachable at all
the failure names the real cause instead of blaming the daemon.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
…indistinguishable from "stale"

The bridge's founding rule is that "installed" must never be indistinguishable
from "working". This is that rule one level up, and it is why yesterday's fixes
would have changed nothing on the machine that needed them.

There are two install paths with very different freshness semantics:

  * `claude --plugin-dir tools/plugins/memory-bridge` runs LIVE from the repo —
    `git pull` IS the update.
  * a marketplace install COPIES the plugin to
    ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/ and pins it to a
    git sha. Nothing re-syncs it. `git pull` changes nothing.

Nothing told you which one you were on. Measured on BigMama: the running copy
was pinned to 60fa0db (2026-07-25), two weeks stale. Its lib.sh had no
persona-id cache, and it contained NO session-capture.sh at all — so automatic
per-turn capture, the entire "volitional memory isn't memory" point, had never
run once on that machine. The README said the bridge was live the whole time.

So session-recall now compares its own running location against
tools/plugins/memory-bridge/scripts in the current checkout, and on drift emits
⚠️ MEMORY BRIDGE STALE plus a `stale` receipt. A missing file counts as drift —
that is exactly how session-capture.sh went absent unnoticed.

It runs on the SUCCESS path too, because staleness is orthogonal to whether
recall worked: a frozen copy can recall perfectly and still be missing every fix
since it was installed.

Only a cached copy can be stale, so running from the repo stays silent; and if
the cwd is not the continuum checkout there is nothing to compare against, so it
says nothing rather than guessing.

README: the "Status: Live" line was a claim a README cannot make — it describes
the repo, while liveness depends on your install. Replaced with the two paths
and how to tell which you are on.

Verified against the real frozen copy on this box (4 of 4 scripts differ →
notice fires) and against a live repo run (silent).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
…l reason fixes never shipped

Plugin updates are VERSION-based, not content-based. `claude plugin update`
compares the DECLARED version against the installed one and never looks at the
files. memory-bridge had sat at 0.1.0 since 2026-07-25, so every installed copy
answered:

    ✔ memory-bridge is already at the latest version (0.1.0).

...no matter what changed. That single static string is why two weeks of work
never reached the machine that needed it: the copy running on BigMama had no
persona-id cache and no session-capture.sh AT ALL, so automatic per-turn memory
capture — the whole "volitional memory isn't memory" point — had never run once,
while the repo held working code and the README said the bridge was live.

Bumping 0.1.0 -> 0.2.0 propagated all of it in one command, verified here:
marketplace update -> `Plugin "memory-bridge" updated from 0.1.0 to 0.2.0`, the
new copy carries session-capture.sh + resolve_airc + the staleness detector, and
now differs from this tree in 0 of 4 scripts.

So the guard. check-plugin-version.sh fails when files under a plugin dir change
without that plugin's plugin.json version changing, and it compares the version
BEFORE and AFTER rather than trusting that the manifest was touched — editing a
description is not a release. A brand-new plugin has nothing to bump from and
passes. Verified both directions: passes on this commit's real bump, fails on a
simulated missed one.

Wired into CI, deliberately NOT into .githooks/pre-commit: that hook invokes
tests/adversarial-protocol.test.cjs and tests/command-processing.test.cjs, both
deleted with the Node monolith, and core.hooksPath does not point at it — so no
pre-commit hook runs at all right now. Adding a gate there would have looked
enforced while running never, which is the same defect one level up. Flagged
rather than silently repaired; that hook needs its own decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
@joelteply joelteply changed the title fix(ci): the clients suite existed and nothing ran it; the workflow that should have was pointed at a deleted directory fix: five silently-unwired capabilities — CI pointed at a deleted dir, deps that never re-installed, an engine nobody asked its version, a memory bridge that couldn't find airc, and a plugin frozen 2 weeks behind Aug 10, 2026
@joelteply

Copy link
Copy Markdown
Contributor Author

Rebase attempt 2026-08-14: first two commits rebased clean (CLAUDE.md resolved to current continuum doctrine — the branch's edits there were Node-era uu/npm-start guidance; bin/continuum.rs resolved keeping canary's newer start-flag handling PLUS this PR's local version arm). Then commit 779810a (ninja→manifest module) conflicts across 4 files in the windows-install seam that #2249 just merged into — including BOTH generated manifests (manifest.windows.{ps1,sh}), which must be REGENERATED from a resolved install-manifest.toml, not hand-merged. Windows install behavior also can't be validated from this Mac. Needs a deliberate rework on top of merged #2249 — likely the Windows-lane owner's pass. The partial resolution above is preserved locally on branch pr2213 if useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant