Skip to content

fix(shape): move the record out of docs/ — that directory is gitignored #829

fix(shape): move the record out of docs/ — that directory is gitignored

fix(shape): move the record out of docs/ — that directory is gitignored #829

Workflow file for this run

name: ci
on:
push:
branches: [main]
pull_request: {}
workflow_dispatch: {}
permissions:
contents: read
# One live run per ref: a newer push (or PR update) cancels the superseded run
# instead of piling four full matrices onto the runners at once. main and each PR
# get their own group so they never cancel each other.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
# tjs legs, CI tier (user decision 2026-07-11): every OS in the release
# matrix on every push, ONE arch each — the manifest (scripts/tjs-legs.mjs)
# owns the list, the reusable workflow owns the job; nothing is duplicated
# against release.yml. VM legs run soft-fail until they earn hard status
# (house rule); the native legs keep their historical hard gate. The tjs
# build restores from the shared cache on native legs, so a typical push
# pays only bundle+fuse+smoke there; VM legs pay the guest boot + in-guest
# build.
tjs:
uses: ./.github/workflows/tjs-legs.yml
permissions:
contents: read
with:
tier: ci
# Windows shim + sync-primitive coverage (windows-tjs-port spec). Windows is
# the ONE platform whose shim + sync-primitive layer diverges from POSIX
# (CRLF, cmd.exe/PATHEXT, drive paths, O_BINARY, the Win32 C sync primitives
# mod_fs_sync/mod_spawn_sync) — POSIX legs get this coverage free from the
# host node:test suite on the dev box, but it can only be exercised on a
# real Windows kernel against a real tjs.exe. Consumes the windows-amd64 leg's
# tjs-windows-amd64 engine; the leg's own exec=host job does build+fuse+PONG+
# publish (subsuming the old fuse-build/fuse-run/builder-smoke jobs). Because
# this installs the latest @anthropic-ai/claude-code it is also the Windows
# bundle-bump gate. These finer signals isolate a C-primitive regression from
# a shim/Bash-tool regression — what the leg's end-to-end PONG cannot.
windows-amd64-tests:
needs: tjs
# Run once `tjs` FINISHES, whatever its conclusion — do NOT skip because some
# OTHER leg failed. This job consumes ONE named artifact; a leg it does not use
# has no bearing on whether it can run. Plain `needs: tjs` skips the moment any
# HARD leg fails, which silently deletes this signal: when haiku-x64 was promoted
# to gating (2026-07-17), a single real haiku regression skipped this job and the
# three windows jobs in one stroke — a broken gate masking working gates, the
# exact disease that let the dep-closure P0 through (node-shim-oracle skipped at
# 9e968b4 for the same reason). If the artifact this job needs is genuinely
# missing, download-artifact fails it LOUDLY, which is the honest answer.
if: ${{ !cancelled() }}
runs-on: windows-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v7.0.1
with:
submodules: false
- uses: actions/setup-node@v7.0.0
with:
node-version-file: .tool-versions
- uses: actions/download-artifact@v8.0.1
with:
name: tjs-windows-amd64
path: .
- name: Install the Claude provider (npm, latest — the bundle-bump gate)
run: npm i -g @anthropic-ai/claude-code 2>&1 | tail -3
- name: Install runtime deps (semver et al.) for the oracles
run: npm ci --prefix deps/claude
- name: Sync-primitive battery on the real Windows kernel
run: |
set -euo pipefail
chmod +x ./tjs.exe || true
./tjs.exe run test/win-sync-battery.js | tee battery.out
grep -q '^BATTERY OK' battery.out
- name: node-shim oracles (roundtrip + agentic) on the real Windows kernel
run: |
set -euo pipefail
PROV="$(node scripts/stage-provider.mjs)" || exit 1
echo "provider: $PROV"
CLODE_TJS="$PWD/tjs.exe" CLODE_PROVIDER_BIN="$PROV" \
node --test --test-concurrency=1 \
test/node-shim-roundtrip.test.cjs \
test/node-shim-agentic.test.cjs
# Windows ConPTY TUI render (windows-tjs-port spec, sub-project D). The FIRST
# CI PTY harness on any platform: build a real quaude (`clode build`) and drive
# IT directly (tjs.exe + node-shim + the real Claude Code bundle → Ink; no
# launcher involved — a fused quaude carries its own engine and deps as
# members) inside a real ConPTY (node-pty) with an xterm emulator on the far
# end, and assert the welcome box painted. Runs the SAME
# test/e2e-tui-tjs.test.cjs as POSIX, un-gated here via CLODE_LIVE_RENDER=1 —
# true on Windows because there is no Keychain GUI modal to hang on (the reason
# POSIX CI can't run it). Consumes the windows-amd64 leg's tjs-windows-amd64 engine.
# Because it installs the latest provider it is also a Windows render bump gate.
# Render-only; the ConPTY INPUT round-trip is the tracked follow-on (D2).
windows-amd64-tui:
needs: tjs
# Run once `tjs` FINISHES, whatever its conclusion — do NOT skip because some
# OTHER leg failed. This job consumes ONE named artifact; a leg it does not use
# has no bearing on whether it can run. Plain `needs: tjs` skips the moment any
# HARD leg fails, which silently deletes this signal: when haiku-x64 was promoted
# to gating (2026-07-17), a single real haiku regression skipped this job and the
# three windows jobs in one stroke — a broken gate masking working gates, the
# exact disease that let the dep-closure P0 through (node-shim-oracle skipped at
# 9e968b4 for the same reason). If the artifact this job needs is genuinely
# missing, download-artifact fails it LOUDLY, which is the honest answer.
if: ${{ !cancelled() }}
runs-on: windows-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v7.0.1
with:
submodules: false
- uses: actions/setup-node@v7.0.0
with:
node-version-file: .tool-versions
- uses: actions/download-artifact@v8.0.1
with:
name: tjs-windows-amd64
path: .
- name: Install the Claude provider (npm, latest — the render bump gate)
run: npm i -g @anthropic-ai/claude-code 2>&1 | tail -3
- name: Install root runtime deps (render seed deps)
run: npm ci --prefix deps/claude
- name: Install the PTY harness (node-pty win32-x64 prebuilt + @xterm/headless)
run: npm install --prefix test
- name: Preflight — node-pty can open a ConPTY on this kernel
run: node test/harness-preflight.cjs
- name: Render the real TUI under ConPTY and assert the welcome box
run: |
set -euo pipefail
PROV="$(node scripts/stage-provider.mjs)" || exit 1
echo "provider: $PROV"
chmod +x ./tjs.exe || true
CLODE_TJS="$PWD/tjs.exe" CLODE_CLAUDE_BIN="$PROV" CLODE_LIVE_RENDER=1 \
node --test --test-concurrency=1 test/e2e-tui-tjs.test.cjs
- name: Characterize the node-shim TTY input path under ConPTY (D2)
run: |
set -euo pipefail
chmod +x ./tjs.exe || true
# Hermetic tjs-vs-node stdin/TTY differential (raw-keystroke ordering,
# paused-mode read() as Ink reads, setRawMode, UTF-8 split-write) under
# a real ConPTY. No provider/network — CLODE_TJS + the node-pty harness
# (already installed above) are all it needs. First CI TTY-input coverage
# on any platform; a failure pinpoints a shim divergence from node.
CLODE_TJS="$PWD/tjs.exe" \
node --test --test-concurrency=1 test/node-shim-tty.test.cjs
# Windows shim + sync-primitive coverage on the ARM64 kernel — the same
# rationale as windows-amd64-tests, exercised against the windows-arm64 leg's
# native MSVC ARM64 engine. No tui counterpart yet (out of scope).
windows-arm64-tests:
needs: tjs
# Run once `tjs` FINISHES, whatever its conclusion — do NOT skip because some
# OTHER leg failed. This job consumes ONE named artifact; a leg it does not use
# has no bearing on whether it can run. Plain `needs: tjs` skips the moment any
# HARD leg fails, which silently deletes this signal: when haiku-x64 was promoted
# to gating (2026-07-17), a single real haiku regression skipped this job and the
# three windows jobs in one stroke — a broken gate masking working gates, the
# exact disease that let the dep-closure P0 through (node-shim-oracle skipped at
# 9e968b4 for the same reason). If the artifact this job needs is genuinely
# missing, download-artifact fails it LOUDLY, which is the honest answer.
if: ${{ !cancelled() }}
runs-on: windows-11-arm
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v7.0.1
with:
submodules: false
- uses: actions/setup-node@v7.0.0
with:
node-version-file: .tool-versions
- uses: actions/download-artifact@v8.0.1
with:
name: tjs-windows-arm64
path: .
- name: Install the Claude provider (npm, latest — the bundle-bump gate)
run: npm i -g @anthropic-ai/claude-code 2>&1 | tail -3
- name: Install runtime deps (semver et al.) for the oracles
run: npm ci --prefix deps/claude
- name: Sync-primitive battery on the real Windows kernel
run: |
set -euo pipefail
chmod +x ./tjs.exe || true
./tjs.exe run test/win-sync-battery.js | tee battery.out
grep -q '^BATTERY OK' battery.out
- name: node-shim oracles (roundtrip + agentic) on the real Windows kernel
run: |
set -euo pipefail
PROV="$(node scripts/stage-provider.mjs)" || exit 1
echo "provider: $PROV"
CLODE_TJS="$PWD/tjs.exe" CLODE_PROVIDER_BIN="$PROV" \
node --test --test-concurrency=1 \
test/node-shim-roundtrip.test.cjs \
test/node-shim-agentic.test.cjs
# templates-drift MOVED OUT OF ci.yml (2026-08-24).
#
# It compared the published pack's engine recipe against this tree's, on every
# push — so it went red the moment engine sources moved past the last release,
# which is the NORMAL state of a repo doing engine work. That is a STATE, not a
# fault, and "cut a release" was never a fix: it reset the baseline until the
# next engine commit. The cost was real — main was permanently red, so "is main
# green?" stopped being a usable question, which is precisely how an ambient red
# hid a P0 across 13 jobs before (see BACKLOG).
#
# Worse, the hazard it warned about was defended NOWHERE. libexec/clode-templates
# .cjs checked only the coarse tjsPin (txiki version + short sha), so two clodes
# with the same pin and different patch stacks both accepted the same pack.
#
# The defence now lives where a wrong answer costs someone something: obtainEngine
# compares the manifest's recipe (stamped by 4f86738) against the recipe baked
# into the running clode (__CLODE_BAKED_ENGINE_RECIPE__) and REFUSES a mismatched
# pack at fetch. Missing on either side is reported as "cannot check", never
# treated as a match. See test/clode-templates.test.cjs.
#
# And the drift check itself moved to release.yml, where "we are about to publish
# templates that do not match this tree" IS a fault.
test:
uses: ./.github/workflows/suite.yml
permissions:
contents: read
# node-shim oracle — the node-vs-tjs behavioral diff, run under a REAL tjs.
# WHY THIS JOB EXISTS: the `test` job above (npm test) never builds a tjs, so
# every `skipUnlessTjs` node-shim oracle test SILENTLY SKIPS there — the diff
# that catches shim-vs-node divergence was disabled in the default path. That
# gap let a real bug ship: fs.readFileSync returned a bare Uint8Array (not a
# Buffer), so .toString('hex')/.readUInt8 were silently wrong and the Edit tool
# failed under tjs — no wall, no crash, green everywhere (fixed 2026-07-15,
# A1 audit). A dedicated, visible job is the standing guard: this class cannot
# hide again. Reuses the CI-tier linux-x64-musl leg's bare-engine artifact
# (tjs-<leg>, uploaded expressly "for oracle jobs") — no rebuild. Same run
# pattern release.yml's be-oracle proved green on s390x-musl.
node-shim-oracle:
needs: tjs
# Run once `tjs` FINISHES, whatever its conclusion — do NOT skip because some
# OTHER leg failed. This job consumes ONE named artifact; a leg it does not use
# has no bearing on whether it can run. Plain `needs: tjs` skips the moment any
# HARD leg fails, which silently deletes this signal: when haiku-x64 was promoted
# to gating (2026-07-17), a single real haiku regression skipped this job and the
# three windows jobs in one stroke — a broken gate masking working gates, the
# exact disease that let the dep-closure P0 through (node-shim-oracle skipped at
# 9e968b4 for the same reason). If the artifact this job needs is genuinely
# missing, download-artifact fails it LOUDLY, which is the honest answer.
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
# LIKE-FOR-LIKE LIBC, ON PURPOSE — the reference node runs under musl because
# the engine does.
#
# This job diffs a MUSL-built engine against "host node". On a stock
# ubuntu-latest, host node is a GLIBC build, so the premise "our engine vs the
# node on this host" is false and the ratchet was really measuring libc. It
# caught itself doing that on 2026-08-23 (run 32606247462), three rows red with
# BOTH SIDES CORRECT:
# os.constants.signals ours had SIGUNUSED: 31, node's did not
# os.constants.dlopen ours 4 members, node's 5 (RTLD_DEEPBIND)
# musl's arch/*/bits/signal.h literally says `#define SIGUNUSED SIGSYS`, and
# musl's dlfcn.h has no RTLD_DEEPBIND; glibc 2.39 says the opposite on both
# (verified directly: `echo '#include <signal.h>' | gcc -E -dM -` on Ubuntu
# 24.04 prints SIGSYS 31 and no SIGUNUSED). Since f8546da/0decc98 the engine
# reports what ITS OWN headers say — that is the entire point of the generated
# constants work, it is what abolished the hand-maintained platform tables — so
# a glibc reference makes an exact gate fail for something that is not a defect.
#
# The fix is the INSTRUMENT, not the ratchet. There is deliberately NO exception
# list, NO skip list, NO "allowed libc differences" file: every entry in such a
# file is a hole in a gate whose whole value is being exact, and a curated table
# of blessed divergences is the hand-maintained platform table walking back in
# through the side door. Make the comparison honest instead.
#
# WHY A CONTAINER AND NOT setup-node: actions/setup-node serves the official
# nodejs.org builds, which are glibc-only — there is no musl node it can give
# us. The image IS the reference node. Its tag is pinned to the SAME version as
# .tool-versions and asserted below, so a bump to one and not the other is a
# named failure rather than a silent skew.
container:
image: node:24.19.0-alpine
defaults:
run:
shell: bash
steps:
# BEFORE checkout on purpose: a `run:` step in a container job needs no repo,
# and actions/checkout with no git on PATH silently degrades to a REST tarball
# download. Each package is load-bearing; alpine ships none of them:
# bash `defaults.run.shell: bash`, and the steps below are bash
# git so checkout is a real clone, not the degraded fallback
# openssl two rows (node-shim-http-client, node-shim-http-proxy) MINT A
# CERTIFICATE and t.skip() without it. A skipped oracle row is
# not a pass — that silent-green is the disease this whole job
# exists to prevent, so the container must not reintroduce it.
# procps node-shim-signals reads `ps -o state= -p PID` to prove SIGSTOP
# actually stopped the child. busybox's ps has no `state`
# column, so the probe would return 'GONE' and the row would
# fail for the tool, not the shim.
# libstdc++ the Bun-packaged provider binary links it. node:alpine
# already pulls it in for node itself; named explicitly so the
# dependency is stated rather than inherited by luck.
# The other externals these tests reach for — /bin/echo, /bin/cat, /bin/sh,
# /bin/sleep, /bin/pwd, mkfifo, kill, uname, true — are busybox applets that
# alpine already symlinks into place, and every row that uses one runs the
# SAME binary on both sides of the diff, so busybox-vs-coreutils cannot skew
# a comparison here.
- name: musl prerequisites (bash, git, openssl, procps — see above)
shell: sh
run: |
set -e
# `procps` is the provides-name; the real package is procps-ng on
# alpine >= 3.19. Asking for the provides-name works on both sides of
# that rename.
apk add --no-cache bash git openssl procps libstdc++
# Name the tool failure HERE rather than letting it surface as a red
# SIGSTOP row two steps later. procps-ng installs /usr/bin/ps, which
# precedes busybox's /bin/ps on alpine's PATH.
ps -o state= -p $$ >/dev/null \
|| { echo "ps has no 'state' column — node-shim-signals' SIGSTOP probe cannot work" >&2; exit 1; }
- uses: actions/checkout@v7.0.1
# The same .tool-versions skew guard suite.yml runs, PLUS the assertion that
# makes this job's premise checkable instead of assumed. If someone ever drops
# the container: key, or the image starts shipping a glibc node, this fails by
# name here rather than reappearing as a mystery constants diff.
- name: Assert the reference node matches the .tool-versions pin AND is musl
run: |
set -euo pipefail
want=$(sed -n 's/^nodejs //p' .tool-versions)
have=$(node -v); have=${have#v}
[ "$have" = "$want" ] || {
echo "node skew: .tool-versions=$want but this container's node=$have." >&2
echo " Bump the container: image: tag in this job to node:$want-alpine." >&2
exit 1; }
# process.report is node's own answer about its own linkage: a glibc build
# reports header.glibcVersionRuntime, a musl build has no such field. That
# is arch-independent and does not depend on ldd's output format.
node -e '
const h = process.report.getReport().header;
if (h.glibcVersionRuntime) {
console.error("reference node is GLIBC-linked (" + h.glibcVersionRuntime + "),");
console.error("but this job diffs a musl engine against it. The comparison would");
console.error("be measuring libc, not shim fidelity. Restore the alpine container.");
process.exit(1);
}
console.log("reference node: " + process.version + ", musl-linked (no glibcVersionRuntime)");
'
- uses: actions/download-artifact@v8.0.1
with:
name: tjs-linux-x64-musl # bare static engine from the CI-tier leg
path: ${{ runner.temp }}/tjs
- name: Install the runtime dep closure (ws/yaml/semver/string-width/... for the shim)
run: npm ci --prefix deps/claude
# The naude-vs-quaude parity gate stages cli.cjs from a real Bun-packaged
# provider; with none present it SKIPS, which is exactly the silent-green
# this job exists to prevent. Same provisioning the windows-amd64 job uses.
# Under the musl container npm resolves the optional dep
# @anthropic-ai/claude-code-linux-x64-musl (upstream publishes it beside the
# glibc one), so the provider is musl too — the same like-for-like the
# reference node is here for.
- name: Install the Claude provider (npm, latest — the parity gate's input)
run: npm i -g @anthropic-ai/claude-code 2>&1 | tail -3
# Live-network gate (regression guard): nothing else in CI makes a real
# outbound connection — every API turn is opt-in and skips — which is how an
# IPv6-first fetch-connect regression once shipped (f8f6e1d: connected to only
# the first resolved address, so a v4-only host died at wsi=NULL). A
# credential-free HEAD proves the built engine can DNS + TLS-connect + get a
# response. Retries so a transient network blip doesn't flake the gate.
- name: Live-connect smoke — the engine reaches the internet over TLS
env:
CLODE_TJS: ${{ runner.temp }}/tjs/tjs
run: |
set -uo pipefail
chmod +x "$CLODE_TJS"
# Raw `tjs eval` has no node-shim `process`, so signal via a marker the
# shell checks (a rejected fetch -> no OK marker -> the gate fails).
P='fetch("https://example.com/",{method:"HEAD"}).then(function(r){console.log("LIVE-CONNECT-OK "+r.status)}).catch(function(e){console.log("LIVE-CONNECT-FAIL "+String(e))})'
for i in 1 2 3; do
out=$("$CLODE_TJS" eval "$P" 2>&1 || true); echo "$out"
case "$out" in *LIVE-CONNECT-OK*) exit 0 ;; esac
echo "retry $i…"; sleep 3
done
echo "live-connect smoke: the engine could not establish a TLS connection" >&2; exit 1
- name: node-shim oracle suite (every module diffed vs host node, under tjs)
env:
CLODE_TJS: ${{ runner.temp }}/tjs/tjs
run: |
set -euo pipefail
chmod +x "$CLODE_TJS"
"$CLODE_TJS" eval 'console.log("linux-x64-musl tjs alive:", tjs.version)'
# EXCLUDED — the PTY tests, which need a controlling terminal + the
# node-pty harness dep this bare-engine runner has neither of:
# node-shim-tty no controlling terminal on a headless runner
# (Ctrl-Z is no longer a node-shim test: node-shim-ctrlz-pty was replaced by
# test/e2e-ctrlz-tui.test.cjs — a real quaude TUI survives-Ctrl-Z regression
# guard, opt-in via CLODE_LIVE_RENDER; the SIGTSTP/SIGCONT wiring stays in
# node-shim-signals. The new node-shim-api-surface-gate IS picked up here —
# it hard-gates upstream Bun.*/require drift against the latest provider.)
# node-shim-core and node-shim-bunshim are NO LONGER excluded: the two
# Linux-portability bugs they caught are FIXED and now gated here —
# node-shim-core os.constants.signals is OS-derived per platform
# (SIGBUS 7 etc. on Linux; os.cjs SIGNALS_LINUX +
# __tjs_signals), no longer a hardcoded darwin table.
# node-shim-bunshim bun:ffi suffix is platform-aware ('so' on Linux).
# node-shim-child-process was un-excluded earlier: its `spawnSync: cwd` row
# failed because musl rejected posix_spawn cwd (the addchdir_np gate was
# #ifdef __GLIBC__), so tar's KAT and `clode fetch --naude` died "no tar
# tool found on PATH". Fixed in txiki-sync-spawn.patch (parent-side chdir
# dance on musl); this file runs on musl as the standing guard.
EXCLUDE='node-shim-tty'
FILES=$(ls test/node-shim-*.test.cjs | grep -vE "$EXCLUDE")
# The parity gate's input: cli.cjs is staged from this provider and run
# under BOTH targets' runtimes (naude = node, quaude = tjs+shim), then
# diffed. Fail loud if it is missing — a skipped oracle is not a pass.
PROV="$(node scripts/stage-provider.mjs)" || exit 1
echo "provider: $PROV"
export CLODE_PROVIDER_BIN="$PROV"
node --test --test-concurrency=1 $FILES
# The BUILDER and the PACKAGED TARGETS — on glibc, on purpose.
#
# Two standing guards that were the tail of node-shim-oracle until that job had
# to move into a musl container to make its comparison honest (see its header).
# They could not follow it: both build and RUN a naude, and a naude embeds the
# pinned official Node from nodejs.org — a glibc binary that does not exec on
# alpine. Splitting them off costs a second artifact download, a second
# deps/claude install and a second provider install; the alternative was one job
# whose libc had to be wrong for one half of it or the other.
#
# Consumes the SAME tjs-linux-x64-musl artifact — the engine is static, so it
# runs on either libc; it is only the REFERENCE side of a comparison that has to
# match, and nothing here compares against host node.
native-builder-oracle:
needs: tjs
# Same rationale as node-shim-oracle's: run once `tjs` FINISHES whatever its
# conclusion. This job consumes ONE named artifact; a leg it does not use has
# no bearing on whether it can run, and a job that skips is a gate that is not
# there. If the artifact is genuinely missing, download-artifact fails LOUDLY.
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/setup-node@v7.0.0
with:
node-version-file: .tool-versions
- uses: actions/download-artifact@v8.0.1
with:
name: tjs-linux-x64-musl # bare static engine from the CI-tier leg
path: ${{ runner.temp }}/tjs
- name: Install the runtime dep closure (the fuse's ext-dep closure)
run: npm ci --prefix deps/claude
# Fail loud if absent, below — a skipped acceptance is not a pass.
- name: Install the Claude provider (npm, latest — the acceptances' input)
run: npm i -g @anthropic-ai/claude-code 2>&1 | tail -3
- name: mark the engine executable
run: chmod +x "${{ runner.temp }}/tjs/tjs"
# The BUILDER, driven the way a user drives it: fuse a native clode, then
# make THAT binary build a quaude with node absent from PATH.
#
# WHY THIS STEP EXISTS (same disease as this job's own header, caught again
# 2026-07-16): clode-native's acceptances 2+3 are the only proof that the
# node-free builder can build the product, and they skip silently without
# CLODE_PROVIDER_BIN. No workflow named the file, so they ran ONLY under
# `npm test` — a job with no tjs and no provider, where they always skipped.
# A commit then made the dep-closure gate spawn `process.execPath -e` to
# introspect the bun-shim; under a fused builder process.execPath IS the
# fused clode (no node on the box, no `-e`), so EVERY `clode build` under
# clode-native died at the gate — on pushed main, with CI fully green. The
# binary that is the entire point of the Q1 split was 100% broken and
# nothing said a word. This step is the standing guard.
#
# It used to run as the last steps of node-shim-oracle, sharing that job's
# tjs + deps/claude + provider. It moved here when that job went into a musl
# container (see its header): this step and the one after it BUILD AND RUN a
# naude, and a naude embeds the pinned OFFICIAL Node from nodejs.org, which
# is a glibc build that will not exec on alpine. So the split is along the
# libc line, which is the honest place for it: the shim oracle needs a musl
# reference, the naude guards need glibc, and one job cannot be both.
# Cache the pinned Node naude embeds so acceptance 4 (below) downloads it
# from nodejs.org at most once per pin, not every run — and a transient
# nodejs.org outage reuses the cache instead of turning this step red. Keyed
# on the pin file so a version bump misses and re-fetches automatically.
- name: Cache the pinned Node (naude engine)
uses: actions/cache@v6.1.0
with:
path: ${{ runner.temp }}/clode-nodes
key: pinned-node-${{ hashFiles('deps/clode/node-pin.json') }}-${{ runner.os }}-${{ runner.arch }}
- name: the native builder BUILDS THE PRODUCT (node-free, real provider)
env:
CLODE_TJS: ${{ runner.temp }}/tjs/tjs
# acceptance 4 (naude) fetches the pinned Node into this persistent,
# cached store instead of a per-run temp dir (test/clode-native.test.cjs
# honors CLODE_NODES); acceptances 1-3 (quaude) ignore it.
CLODE_NODES: ${{ runner.temp }}/clode-nodes
run: |
set -euo pipefail
# `build --self` embeds this; without it the fuse has no payload.
node scripts/build-clode-main.mjs
# Provision postject into deps/clode: quaude-fuse.js carries it as a
# builder member (soft — skipped if absent), and acceptance 4's naude
# build needs it on disk. Without this the fused clode-native ships no
# postject and the naude assembly dies "Cannot find module .../postject".
npm ci --prefix deps/clode 2>&1 | tail -3
PROV="$(node scripts/stage-provider.mjs)" || exit 1
echo "provider: $PROV"
# The acceptances prove node-freeness by running the fused builder with
# PATH=/usr/bin:/bin, and they SKIP if a node lives there ("cannot prove
# node-freeness here"). Surface that up front: if this runner image ever
# puts node on the minimal PATH, the skipped-0 assertion below would fail
# with a confusing message about skips instead of the real reason.
if PATH=/usr/bin:/bin command -v node; then
echo "ERROR: this runner has node on /usr/bin:/bin, so the node-free proof cannot run." >&2
echo " Give test/clode-native.test.cjs a synthetic minimal PATH, or run this step" >&2
echo " on an image without a system node — do NOT just drop the assertion." >&2
exit 1
fi
echo "minimal PATH has no node — the node-free proof can run"
# A skipped acceptance is not a pass: this test skips itself when the
# provider is absent, so assert it actually RAN rather than trusting a
# green exit. That distinction is the whole reason this step exists.
# Both summary forms are matched on purpose — node prints "ℹ skipped N"
# (spec reporter) or "# skipped N" (tap), and pinning the wrong one
# would make this guard fire always or never. Verified locally: node 24
# emits the spec form even when piped.
CLODE_PROVIDER_BIN="$PROV" CLODE_CLAUDE_BIN="$PROV" \
node --test --test-timeout=1200000 test/clode-native.test.cjs 2>&1 | tee native.out
grep -qE '^(ℹ|#) skipped 0$' native.out \
|| { echo "ERROR: a clode-native acceptance SKIPPED (or the summary format changed) — the node-free builder went unproven" >&2; exit 1; }
# The packaged targets themselves: build a real naude (Node SEA) and a real
# quaude (fused tjs) from the same CC and diff them. The model oracle above
# runs cli.cjs under two engines; only this covers the PACKAGING. Nothing in
# CI built a naude before, which is how a bug that killed every naude on its
# first boot ("sea.getRawAsset is not a function") stayed green.
- name: full-binary oracle — real naude vs real quaude
env:
# /tjs/tjs, not /tjs: download-artifact unpacks INTO ${{ runner.temp }}/tjs,
# so that dir is the artifact, and the engine is the file inside it. Pointing
# CLODE_TJS at the DIRECTORY made `clode build` read it as the template —
# "EISDIR: illegal operation on a directory, read". Wrong since this step was
# written, and invisible twice over: the job used to die at an earlier step
# (the drifted autoupdater anchor), and then skipped entirely once a hard leg
# failed. The two steps above always had it right.
CLODE_TJS: ${{ runner.temp }}/tjs/tjs
CLODE_ORACLE_BINARIES: '1'
run: |
set -euo pipefail
PROV="$(node scripts/stage-provider.mjs)" || exit 1
CLODE_PROVIDER_BIN="$PROV" CLODE_CLAUDE_BIN="$PROV" \
node --test --test-timeout=1200000 test/oracle-binaries.test.cjs
# THE ORACLE PROPERTY the step above spends: naude is only a control for quaude
# because it carries the bun-shim and NOT the node-shim. The gate for that reads a
# BUILT naude's SEA asset table, so it belongs HERE, in the one job that has one —
# under `npm test` it would skip forever, which is the 2026-08-25 disease (sixteen
# tests skipping in CI since forever, invisibly). `skipped 0` is asserted for the
# same reason clode-native's acceptances assert it: a gate that skipped is a gate
# that is not there.
- name: naude shim boundary — the bytes that make the oracle an oracle
run: |
set -euo pipefail
node --test test/naude-shim-boundary.test.cjs 2>&1 | tee boundary.out
grep -qE '^(ℹ|#) skipped 0$' boundary.out \
|| { echo "ERROR: the naude shim-boundary gate SKIPPED — it found no built naude to inspect (see its skip message above)" >&2; exit 1; }
# node-shim oracle on DARWIN — the same node-vs-tjs behavioral diff as
# node-shim-oracle above, but on macOS against a real darwin tjs.
# WHY A SEPARATE JOB: the node-shim is darwin-oriented (signal table, dylib
# suffix, spawn env handling), so a class of divergences appears ONLY on
# darwin and the linux oracle cannot see it. Concretely: a process.env mutation
# did not reach a default-env spawned child under tjs — but only on darwin,
# because on linux tjs mirrors env into the real environ, so node-shim-env
# passed there and the bug was invisible in CI until a dev box hit it
# (2026-07-18). No darwin shim run = that whole class hides. This job is the
# standing guard. It also runs the child-process/core/bunshim tests the linux
# oracle must EXCLUDE for musl/signal/dylib portability — on darwin they are
# native behavior and pass. Consumes the darwin-arm64 leg's bare engine.
node-shim-oracle-darwin:
needs: tjs
# Run once `tjs` FINISHES, whatever its conclusion (same rationale as the
# windows/node-shim-oracle jobs): this consumes ONE named artifact; a leg it
# does not use has no bearing on whether it can run.
if: ${{ !cancelled() }}
runs-on: macos-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/setup-node@v7.0.0
with:
node-version-file: .tool-versions
- uses: actions/download-artifact@v8.0.1
with:
name: tjs-darwin-arm64 # bare arm64 engine from the CI-tier darwin leg
path: ${{ runner.temp }}/tjs
- name: Install the runtime dep closure (ws/yaml/semver/string-width/... for the shim)
run: npm ci --prefix deps/claude
# The parity/roundtrip oracles stage cli.cjs from a real Bun-packaged
# provider; with none present they SKIP, the silent-green these jobs exist
# to prevent. Same provisioning as the linux oracle.
- name: Install the Claude provider (npm, latest — the oracle's input)
run: npm i -g @anthropic-ai/claude-code 2>&1 | tail -3
# Live-network gate (regression guard) — see the linux node-shim-oracle copy.
# A credential-free HEAD proves the darwin engine can DNS + TLS-connect.
- name: Live-connect smoke — the engine reaches the internet over TLS
env:
CLODE_TJS: ${{ runner.temp }}/tjs/tjs
run: |
set -uo pipefail
chmod +x "$CLODE_TJS"
# Raw `tjs eval` has no node-shim `process`, so signal via a marker the
# shell checks (a rejected fetch -> no OK marker -> the gate fails).
P='fetch("https://example.com/",{method:"HEAD"}).then(function(r){console.log("LIVE-CONNECT-OK "+r.status)}).catch(function(e){console.log("LIVE-CONNECT-FAIL "+String(e))})'
for i in 1 2 3; do
out=$("$CLODE_TJS" eval "$P" 2>&1 || true); echo "$out"
case "$out" in *LIVE-CONNECT-OK*) exit 0 ;; esac
echo "retry $i…"; sleep 3
done
echo "live-connect smoke: the engine could not establish a TLS connection" >&2; exit 1
- name: node-shim oracle suite (every module diffed vs host node, under a darwin tjs)
env:
CLODE_TJS: ${{ runner.temp }}/tjs/tjs
run: |
set -euo pipefail
chmod +x "$CLODE_TJS"
"$CLODE_TJS" eval 'console.log("darwin-arm64 tjs alive:", tjs.version)'
# EXCLUDE only node-shim-tty (needs a controlling terminal, covered by
# windows-amd64-tui + the POSIX e2e). Everything else runs here — including
# child-process/core/bunshim (which the linux oracle excludes for portability)
# AND the new node-shim-api-surface-gate (hard-gates upstream Bun.*/require
# drift against the latest provider this job installs). (Ctrl-Z moved to the
# opt-in test/e2e-ctrlz-tui.test.cjs; the wiring stays in node-shim-signals.)
EXCLUDE='node-shim-tty'
FILES=$(ls test/node-shim-*.test.cjs | grep -vE "$EXCLUDE")
PROV="$(node scripts/stage-provider.mjs)" || exit 1
echo "provider: $PROV"
export CLODE_PROVIDER_BIN="$PROV"
node --test --test-concurrency=1 $FILES