fix(shape): move the record out of docs/ — that directory is gitignored #829
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |