diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 846e0c297..1093e8c46 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -901,12 +901,13 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-12 | 1815 | 27ce96e1755055ceee2eeae02d6efdf11259fcde | babysit | fixed | Unit coverage: targeted vitest passed: tests/shared-home-empty-state.dom.test.tsx (17 passed). PR required still blocked on pre-existing check failure at old remote head before sync. | | 2026-08-12 | claude/rag-canary-test-review-seprbt | bcf357a96fde74d39fc4726ffabb5079a744ef28 | eval-canary review: workflow, compare tooling, snapshot builder, alias tiering, rag-behaviour docs | PR #1843 opened; no retrieval behaviour change; snapshot refresh handed off as /issues #304 | verify:pr-local (green except env-only #296), eval:rag:offline 574/574, focused suites 40/40 | | 2026-08-12 | codex/pr-workflow-safety-230-296 | bc0a491fdf4146775629f9b2b03e2a2cc61bd7cb | pr-1830 unblock | unblocked: merged origin/main (outstanding-issues conflict), PR body RAG impact + governance, resolved Copilot thread; merge-tree clean; required CI in progress | check:outstanding-issues pass; evaluatePullRequestPolicy ok; merge-tree clean; PR policy/mergeability/Change scope in progress | -| 2026-08-12 | claude/segmented-control-count | d5ff3160242986d2fac80ed9a915390b5c69b6e1 | SegmentedControl option hint slot (filter rollout prerequisite) | PR #1848 opened; additive, no call site passes hint yet; fixed a concatenated accessible name (All62 -> All (62)) | lint/typecheck/test green, 80 in ui-v2-components, 56 design-sync with no regen needed, clean build, bundle-budget within tolerance | +| 2026-08-12 | 1849 | 34e5581c4418ef8a08909dff9ecf91d4a8f622de | full PR diff and unresolved review feedback | P1 setup-only PAT remained accessible through gh credential storage; removed agent-phase PAT persistence and retained safe base/shim changes | check:codex-cloud PASS; docs:check-inventory PASS; focused Vitest blocked by active repository lease | | 2026-08-12 | claude/design-issues-triage-wnr7k9 | a6bfc6f2707975d9b9c649843e083965c80afb9c | Tier 1 design-issue re-verification: archive #171/#172/#174/#181/#273/#274/#302, correct #293 | docs-only; six rows verified delivered on main, min-h-tap finding refuted as deliberate sm: step-down | verify:pr-local (10/10 green); check:outstanding-issues 138 open/163 archived | | 2026-08-12 | claude/design-issues-triage-wnr7k9 | 970d39bc7023823d3283c660df090659a2f07aec | Full outstanding-issues ledger sweep: 47 rows individually verified against merged main | 19 archived (delivered or duplicate), 10 re-scoped with re-measured evidence, 1 refuted (#293), 4 machine-local rows annotated do-not-close-from-cloud; 98 rows bucketed by blocker, not individually verified | verify:pr-local 10/10 green; check:outstanding-issues 126 open/175 archived, no ids deleted from base | -| 2026-08-12 | claude/filter-contract-global | a0add717c2521c7fdeba4da5b094377014383c3e | global filter contract: lens/facet kinds + docs/filter-contract.md (no rendered change) | PR #1847 opened; additive only, zero call sites touched; fixed an accessible-name leak caught by the new DOM tests | verify:pr-local fully green (no failures), 4 new DOM tests, git diff over all 7 mode files empty | | 2026-08-12 | claude/design-issues-triage-wnr7k9 | 2553b64b2342b0ef2ebef0afde152e76c1252496 | Ledger sweep round 2: open-PR cross-check plus clinical/answer-surface verification | 24 rows flagged IN FLIGHT against 7 open PRs (none had said so); 3 answer-surface rows archived (#166 #208 #216); #250 wave plan re-scoped; main-merge conflict resolved preserving both sides, 23 branch changes re-applied via the writer | verify:pr-local 10/10 green; check:outstanding-issues 121 open/180 archived, no ids deleted from base | | 2026-08-12 | claude/design-issues-triage-wnr7k9 | 586012639565e4d3306b44361ebc5a3bdb3024ad | Land PR #1838 ledger sweep; close #147 mobile CLS by measurement | Merge resolved as union (main renumbered #302/#303 to #306/#307 — not lost, correcting an earlier claim); #306/#307 archived as already-delivered. #147 archived on two identical offline Lighthouse runs: mobile CLS 0.035/0.000/0.013/0.081/0.000, all under 0.1, cause fixed by PR #1616 not this session. #118 updated (browser drift 141-vs-151, wider than recorded); new #308 for desktop /documents/search CLS 0.119 | verify:pr-local 10/10 green; check:outstanding-issues 121 open/185 archived; verify:lighthouse x2 (gate ungraded on browser drift, measurements valid) | +| 2026-08-12 | claude/segmented-control-count | d5ff3160242986d2fac80ed9a915390b5c69b6e1 | SegmentedControl option hint slot (filter rollout prerequisite) | PR #1848 opened; additive, no call site passes hint yet; fixed a concatenated accessible name (All62 -> All (62)) | lint/typecheck/test green, 80 in ui-v2-components, 56 design-sync with no regen needed, clean build, bundle-budget within tolerance | +| 2026-08-12 | claude/filter-contract-global | a0add717c2521c7fdeba4da5b094377014383c3e | global filter contract: lens/facet kinds + docs/filter-contract.md (no rendered change) | PR #1847 opened; additive only, zero call sites touched; fixed an accessible-name leak caught by the new DOM tests | verify:pr-local fully green (no failures), 4 new DOM tests, git diff over all 7 mode files empty | | 2026-08-12 | 1848 | c65b9d91b760862c2ff9d5001974670219c5da02 | full PR diff and unresolved review feedback | P2 hint contrast and live-count width fixes applied | focused Vitest passed (81); focused ESLint passed; design-system contract passed | | 2026-08-12 | PR #1595 / claude/ds-v2-adopt | 590eb6cfb229c5ae0f7a5025352fa871d8321521 | Supersedes 2026-08-03 PR-J clinical-governance review at f9f73c707d9b6b6226fc04d172fef8e426513055; accepted delta through merged PR head | SUPERSEDES the earlier PR-J clinical-governance row for merge evidence. The final delta added the answer-state projection, the two scoped review fixes, and the clinically approved #228 attribution wording. The user accepted that delta without a second clinical-governance review; this record preserves that explicit limitation rather than implying the earlier review covered the final tree. | Final PR head 590eb6cfb229c5ae0f7a5025352fa871d8321521; squashed to main as f4448f8c1 (historical mapping recorded in #232); no new provider or clinical review performed | | 2026-08-12 | PR-1835 | fe46e5ade8018d21cf15d149711579b1b43c7fb8 | full PR diff and unresolved review feedback | P1/P2 findings fixed during fresh open-PR sweep; rename-only historical review comments dispositioned no-change | focused drift/docs/ledger/migration checks pass; verify:pr-local static+lint+typecheck pass, unrelated Windows unit baseline failures | diff --git a/docs/codex-cloud.md b/docs/codex-cloud.md index cc925e54b..d7c7c0999 100644 --- a/docs/codex-cloud.md +++ b/docs/codex-cloud.md @@ -43,8 +43,8 @@ Keep agent internet access off for this repository's ordinary Cloud environments installation happens during setup; ordinary structure-only work, including the RAG decomposition prompt below, remains offline. The appended command-shim installer is required: it makes every normal `node`, `npm`, and `npx` invocation load the generated sanitized -profile before starting Node. It is idempotent and uses `nvm which` rather than -`command -v node`, so maintenance cannot accidentally wrap an earlier wrapper. +profile before starting Node. It is idempotent and builds the executable path from `nvm version` +instead of resolving through `PATH`, so maintenance cannot accidentally wrap an earlier wrapper. The setup command fails if the required Cloud toolchain cannot be installed. It intentionally does not install Railway CLI: hosted Railway access comes from the authenticated workspace app, and the @@ -204,9 +204,11 @@ Setup restores a missing `origin` to the credential-free URL `https://github.com/BigSimmo/Database.git`; it preserves an existing correct remote and fails instead of overwriting a wrong or credential-bearing remote. When GitHub CLI authentication is already available, setup asks `gh auth setup-git` to install its token-free helper command. It -never embeds a token or invents a PAT. `git ls-remote` and a dry-run push remain separate -acceptance checks; if the connector does not expose shell Git authentication, report that -platform capability gap. +never embeds a token or invents a PAT. It also fetches `origin/main`, stores the current task's merge +base outside the checkout, and exports +that exact 40-character SHA as `CODEX_CLOUD_EXPECTED_BASE_SHA` in subsequent agent shells. `git +ls-remote` and a dry-run push remain separate acceptance checks; if the connector does not expose +shell Git authentication, report that platform capability gap. Suggested GitHub acceptance task: @@ -270,8 +272,8 @@ Run this in a fresh Cloud task before relying on the environment: Read all applicable AGENTS.md files and docs/codex-cloud.md. State whether this is the offline or connected profile. Report tool versions without printing environment values. Run npm run check:codex-cloud, npm run check:runtime, -npm run check:installed-lock-parity, and set CODEX_CLOUD_EXPECTED_BASE_SHA to the intended -merge/base commit before running npm run check:codex-cloud -- --runtime. Do not call a +npm run check:installed-lock-parity, and npm run check:codex-cloud -- --runtime. Confirm that +setup exported CODEX_CLOUD_EXPECTED_BASE_SHA as a verified 40-character ancestor. Do not call a provider unless this task explicitly names and authorizes that provider. Report the decisive line from every command and any unrun check. ``` @@ -295,7 +297,8 @@ the full current HEAD, local main and origin/main when present, expected base, a and a separate freshness state. Setup and maintenance use the process-local `CODEX_CLOUD_PROVISIONING=1` flag so an unavoidable task-only checkout reports `freshness=unverified` without entering a repair loop. The explicit acceptance command does -not set that flag and fails until `CODEX_CLOUD_EXPECTED_BASE_SHA` proves the intended base. +not set that flag and fails unless the setup-generated `CODEX_CLOUD_EXPECTED_BASE_SHA` proves the +intended base. MCP inspection emits server names, commands, and environment variable names only. A repository cannot remove a variable already inherited by the top-level task process. Before @@ -450,8 +453,9 @@ copying credentials into the checkout. 4. **Prove the shell boundary before providers.** First run the direct raw-shell command above before profiles or command shims. Then run `npm run check:codex-cloud`, `npm run check:codex-cloud -- --runtime`, `npm run check:runtime`, and - `npm run check:installed-lock-parity`. Set `CODEX_CLOUD_EXPECTED_BASE_SHA` to the intended - merged base commit. Require the raw PASS line, both Cloud PASS lines, correct runtime/lock + `npm run check:installed-lock-parity`. Confirm the setup-generated + `CODEX_CLOUD_EXPECTED_BASE_SHA` is a full intended merge-base commit. Require the raw PASS line, + both Cloud PASS lines, correct runtime/lock parity, `CODEX_CLOUD_ACCESS_PROFILE=connected`, no provider variable reported present, and a credential-free matching origin. Repository MCP metadata is configuration evidence only. 5. **Prove each provider read-only.** Use the tools exposed by the fresh host session, not shell diff --git a/docs/scripts-index.md b/docs/scripts-index.md index 4ed722104..30e887818 100644 --- a/docs/scripts-index.md +++ b/docs/scripts-index.md @@ -1,6 +1,6 @@ # Scripts index -Curated map of `scripts/` (225 files) and the `package.json` script surface (236 entries), +Curated map of `scripts/` (226 files) and the `package.json` script surface (236 entries), grouped by purpose. This is orientation, not an exhaustive per-file listing — the authoritative command list is `package.json`, and `npm run docs:check-scripts` verifies every `npm run ` referenced in docs resolves to a real script. `npm run docs:update` refreshes the exact counts above. diff --git a/scripts/check-codex-cloud-setup.mjs b/scripts/check-codex-cloud-setup.mjs index 2b05e564c..3163d3f25 100644 --- a/scripts/check-codex-cloud-setup.mjs +++ b/scripts/check-codex-cloud-setup.mjs @@ -752,6 +752,7 @@ export function validateCodexCloudSetup() { const setup = read("scripts/setup-codex-cloud.sh"); const maintenance = read("scripts/maintain-codex-cloud.sh"); const commandShims = read("scripts/install-codex-cloud-command-shims.sh"); + const checkoutBaseRefresh = read("scripts/refresh-codex-cloud-base.sh"); const rawEnvironmentProbe = read("scripts/check-codex-cloud-raw-env.sh"); const patDelete = read("scripts/delete-codex-cloud-branch-with-pat.sh"); const guide = read("docs/codex-cloud.md"); @@ -799,6 +800,7 @@ export function validateCodexCloudSetup() { [/\.bash_profile/, "Cloud setup must cover Bash login-profile precedence."], [/@openai\/codex/, "Cloud setup must install the Codex CLI."], [/ensure-codex-cloud-git-remote\.mjs/, "Cloud setup must restore a safe origin remote."], + [/refresh-codex-cloud-base\.sh/, "Cloud setup must refresh and pin the task checkout base."], [/check:codex-cloud -- --runtime/, "Cloud setup must run runtime acceptance."], [ /BEGIN clinical-kb-codex-cloud shell policy/, @@ -843,6 +845,11 @@ export function validateCodexCloudSetup() { if (setup.includes("@railway/cli") || setup.includes('setup_step="railway-cli"')) { errors.push("Cloud setup must not install or invoke Railway CLI; hosted access comes from the authenticated app."); } + if (/gh auth login|configure-codex-cloud-github-shell\.sh/.test(`${setup}\n${maintenance}`)) { + errors.push( + "Cloud lifecycle scripts must not persist setup-only GitHub credentials for the agent phase; use the native connector.", + ); + } const providerScrubIndex = setup.indexOf("unset OPENAI_API_KEY"); const accessProfileBranchIndex = setup.indexOf('if [ "\\$CODEX_CLOUD_ACCESS_PROFILE" = "connected" ]'); if (providerScrubIndex < 0 || accessProfileBranchIndex < 0 || providerScrubIndex > accessProfileBranchIndex) { @@ -868,11 +875,23 @@ export function validateCodexCloudSetup() { /ensure-codex-cloud-git-remote\.mjs/, "Maintenance must preserve the safe origin remote.", ); + requireMatch( + errors, + maintenance, + /refresh-codex-cloud-base\.sh/, + "Maintenance must refresh and pin the task checkout base.", + ); requireMatch( errors, commandShims, - /nvm which/, - "Cloud command shims must resolve the selected Node version through nvm.", + /nvm version/, + "Cloud command shims must resolve the selected Node version without following their own wrappers.", + ); + requireMatch( + errors, + commandShims, + /clean_path=.*\.local.*bin/, + "Cloud command shims must remove their directory before running child npm scripts.", ); requireMatch( errors, @@ -889,6 +908,18 @@ export function validateCodexCloudSetup() { if (!commandShims.includes('exec "$node_bin/$command_name" "\\$@"')) { errors.push("Cloud command shims must execute absolute Node commands."); } + requireMatch( + errors, + checkoutBaseRefresh, + /git merge-base HEAD refs\/remotes\/origin\/main/, + "Checkout-base refresh must pin the merge base shared by the task and origin/main.", + ); + requireMatch( + errors, + checkoutBaseRefresh, + /cloud-expected-base-sha/, + "Checkout-base refresh must persist the verified base outside the repository.", + ); requireMatch( errors, patDelete, diff --git a/scripts/install-codex-cloud-command-shims.sh b/scripts/install-codex-cloud-command-shims.sh index 6a473bb4b..7d2230d95 100644 --- a/scripts/install-codex-cloud-command-shims.sh +++ b/scripts/install-codex-cloud-command-shims.sh @@ -20,7 +20,9 @@ runtime_profile="$HOME/.clinical-kb-codex-cloud.sh" source "$runtime_profile" command -v nvm >/dev/null 2>&1 || fail "nvm is unavailable after loading the Cloud runtime profile." -node_bin="$(dirname "$(nvm which "$expected_node_major")")" +resolved_node_version="$(nvm version "$expected_node_major")" +[[ "$resolved_node_version" != "N/A" ]] || fail "Node ${expected_node_major} is unavailable through nvm." +node_bin="$NVM_DIR/versions/node/$resolved_node_version/bin" mkdir -p "$HOME/.local/bin" for command_name in node npm npx; do [[ -x "$node_bin/$command_name" ]] || fail "${command_name} is unavailable in Node ${expected_node_major}." @@ -30,6 +32,18 @@ set -Eeuo pipefail # Generated by scripts/install-codex-cloud-command-shims.sh. Re-running setup # or maintenance replaces this wrapper through the same idempotent command. . "$runtime_profile" +clean_path=":\$PATH:" +while [[ "\$clean_path" == *":\$HOME/.local/bin:"* ]]; do + clean_path="\${clean_path//:\$HOME\/.local\/bin:/:}" +done +clean_path="\${clean_path#:}" +clean_path="\${clean_path%:}" +if [[ -n "\$clean_path" ]]; then + export PATH="$node_bin:\$clean_path" +else + export PATH="$node_bin" +fi +unset clean_path exec "$node_bin/$command_name" "\$@" EOF chmod 0755 "$HOME/.local/bin/$command_name" diff --git a/scripts/maintain-codex-cloud.sh b/scripts/maintain-codex-cloud.sh index c85fdb152..44d206c66 100644 --- a/scripts/maintain-codex-cloud.sh +++ b/scripts/maintain-codex-cloud.sh @@ -8,6 +8,8 @@ repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || { } cd "$repo_root" +export CODEX_CLOUD=1 + if [[ -f "$HOME/.clinical-kb-codex-cloud.sh" ]]; then # shellcheck source=/dev/null source "$HOME/.clinical-kb-codex-cloud.sh" @@ -19,6 +21,9 @@ if ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then fi node scripts/ensure-codex-cloud-git-remote.mjs --configure-gh-helper +bash scripts/refresh-codex-cloud-base.sh +# shellcheck source=/dev/null +source "$HOME/.clinical-kb-codex-cloud.sh" npm run check:codex-cloud if ! CODEX_CLOUD_PROVISIONING=1 npm run check:codex-cloud -- --runtime; then printf '[codex-cloud:maintenance] Runtime or toolchain drift detected; rerunning full setup.\n' diff --git a/scripts/refresh-codex-cloud-base.sh b/scripts/refresh-codex-cloud-base.sh new file mode 100755 index 000000000..524947fb9 --- /dev/null +++ b/scripts/refresh-codex-cloud-base.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +fail() { + printf '[codex-cloud:base] ERROR: %s\n' "$*" >&2 + exit 1 +} + +repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || fail "Run this script from the Database repository." +cd "$repo_root" + +git fetch --quiet --no-tags origin '+refs/heads/main:refs/remotes/origin/main' || + fail "Could not refresh origin/main." + +expected_base="$(git merge-base HEAD refs/remotes/origin/main 2>/dev/null)" || + fail "Could not determine the checkout merge base with origin/main." +[[ "$expected_base" =~ ^[0-9a-f]{40}$ ]] || fail "The checkout merge base is not a full Git commit SHA." + +cache_dir="$HOME/.cache/clinical-kb-codex" +expected_base_file="$cache_dir/cloud-expected-base-sha" +mkdir -p "$cache_dir" +chmod 0700 "$cache_dir" +expected_base_candidate="$(mktemp "$cache_dir/.cloud-expected-base-sha.XXXXXX")" +trap 'rm -f "$expected_base_candidate"' EXIT +printf '%s\n' "$expected_base" > "$expected_base_candidate" +chmod 0600 "$expected_base_candidate" +mv -f "$expected_base_candidate" "$expected_base_file" +trap - EXIT + +printf '[codex-cloud:base] PASS: expected_base=%s origin_main_refreshed=true\n' "$expected_base" diff --git a/scripts/setup-codex-cloud.sh b/scripts/setup-codex-cloud.sh index 80c88911b..9f5816c97 100644 --- a/scripts/setup-codex-cloud.sh +++ b/scripts/setup-codex-cloud.sh @@ -135,6 +135,14 @@ export CODEX_CLOUD=1 export CODEX_CLOUD_ACCESS_PROFILE="${access_profile}" export NEXT_PUBLIC_DEMO_MODE="\${NEXT_PUBLIC_DEMO_MODE:-true}" export PLAYWRIGHT_OFFLINE_MODE="\${PLAYWRIGHT_OFFLINE_MODE:-true}" +cloud_expected_base_file="\$HOME/.cache/clinical-kb-codex/cloud-expected-base-sha" +if [ -r "\$cloud_expected_base_file" ]; then + IFS= read -r cloud_expected_base < "\$cloud_expected_base_file" || true + if [[ "\$cloud_expected_base" =~ ^[0-9a-f]{40}\$ ]]; then + export CODEX_CLOUD_EXPECTED_BASE_SHA="\$cloud_expected_base" + fi +fi +unset cloud_expected_base cloud_expected_base_file unset npm_config_http_proxy npm_config_https_proxy npm_config_proxy # Connected access is provided by host-installed, OAuth-backed apps, never by # repository MCP registration or raw provider variables in the agent shell. @@ -265,6 +273,11 @@ install_npm_cli "@openai/codex" "$codex_cli_version" "codex" setup_step="git-remote" node scripts/ensure-codex-cloud-git-remote.mjs --configure-gh-helper +setup_step="checkout-base" +bash scripts/refresh-codex-cloud-base.sh +# shellcheck source=/dev/null +source "$runtime_profile" + setup_step="deno-runtime" if ! command -v deno >/dev/null 2>&1 || [[ "$(deno --version 2>/dev/null | sed -n '1s/^deno \([0-9]*\).*/\1/p')" != "2" ]]; then log "Installing Deno 2.x." diff --git a/tests/codex-cloud-setup.test.ts b/tests/codex-cloud-setup.test.ts index fb990d838..acc632017 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -1,5 +1,5 @@ import { spawnSync } from "node:child_process"; -import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { chmodSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; @@ -576,6 +576,10 @@ describe("Codex Cloud environment contract", () => { new URL("../scripts/install-codex-cloud-command-shims.sh", import.meta.url), "utf8", ); + const checkoutBaseRefresh = readFileSync( + new URL("../scripts/refresh-codex-cloud-base.sh", import.meta.url), + "utf8", + ); const patDelete = readFileSync( new URL("../scripts/delete-codex-cloud-branch-with-pat.sh", import.meta.url), "utf8", @@ -620,14 +624,33 @@ describe("Codex Cloud environment contract", () => { expect(setup).toContain('setup_step="python-worker-requirements"'); expect(setup).not.toContain("@railway/cli"); expect(setup).not.toContain('setup_step="railway-cli"'); + expect(setup).not.toContain("gh auth login"); + expect(setup).not.toContain("configure-codex-cloud-github-shell.sh"); + expect(maintenance).not.toContain("gh auth login"); + expect(maintenance).not.toContain("configure-codex-cloud-github-shell.sh"); expect(setup).toContain("--require-hashes -r worker/python/requirements-cloud.txt"); expect(setup).toContain('"$ocr_venv/bin/python" -m pip check'); - expect(setup).toContain("CODEX_CLOUD_PROVISIONING=1 npm run check:codex-cloud -- --runtime"); - expect(maintenance).toContain("CODEX_CLOUD_PROVISIONING=1 npm run check:codex-cloud -- --runtime"); + const refreshCommand = "bash scripts/refresh-codex-cloud-base.sh"; + const runtimeCheck = "CODEX_CLOUD_PROVISIONING=1 npm run check:codex-cloud -- --runtime"; + + expect(setup).toContain(runtimeCheck); + expect(setup).toContain(refreshCommand); + expect(maintenance).toContain(runtimeCheck); expect(maintenance).toContain("ensure-codex-cloud-git-remote.mjs"); - expect(commandShims).toContain('nvm which "$expected_node_major"'); + expect(maintenance).toContain(refreshCommand); + for (const script of [setup, maintenance]) { + expect(script.indexOf(refreshCommand)).toBeLessThan(script.indexOf(runtimeCheck)); + } + expect(commandShims).toContain('nvm version "$expected_node_major"'); + expect(commandShims).toContain('node_bin="$NVM_DIR/versions/node/$resolved_node_version/bin"'); + expect(commandShims).toContain('while [[ "\\$clean_path" == *":\\$HOME/.local/bin:"* ]]; do'); + expect(commandShims).toContain('clean_path="\\${clean_path//:\\$HOME\\/.local\\/bin:/:}"'); + expect(commandShims).toContain('if [[ -n "\\$clean_path" ]]; then'); + expect(commandShims).toContain('export PATH="$node_bin"'); expect(commandShims).toContain('. "$runtime_profile"'); expect(commandShims).toContain('mkdir -p "$HOME/.local/bin"'); + expect(checkoutBaseRefresh).toContain("git merge-base HEAD refs/remotes/origin/main"); + expect(checkoutBaseRefresh).toContain("cloud-expected-base-sha"); expect(patDelete).toContain('[[ "${CODEX_CLOUD:-0}" != "1" ]]'); expect(patDelete).toContain('[[ "$branch" != -* ]]'); expect(patDelete).toContain("git check-ref-format --branch"); @@ -636,6 +659,40 @@ describe("Codex Cloud environment contract", () => { expect(patDelete).toContain("core.hooksPath=/dev/null"); }); + it("removes every adjacent command-shim PATH entry", () => { + const home = temporaryDirectory("codex-cloud-shims-"); + const bashHome = bashPathEntry(home); + const nodeVersion = "v24.19.0"; + const nodeBin = path.join(home, ".nvm", "versions", "node", nodeVersion, "bin"); + const bashNodeBin = bashPathEntry(nodeBin); + mkdirSync(nodeBin, { recursive: true }); + for (const command of ["node", "npm", "npx"]) { + const executable = path.join(nodeBin, command); + writeFileSync(executable, '#!/usr/bin/env bash\nprintf "%s\\n" "$PATH"\n'); + chmodSync(executable, 0o755); + } + writeFileSync( + path.join(home, ".clinical-kb-codex-cloud.sh"), + [`export NVM_DIR="${bashHome}/.nvm"`, `nvm() { printf '${nodeVersion}\\n'; }`, ""].join("\n"), + ); + + const install = spawnSync(bashCommand, ["scripts/install-codex-cloud-command-shims.sh"], { + cwd: repoRoot, + encoding: "utf8", + env: { ...process.env, HOME: bashHome, PATH: bashPathList(process.env.PATH || "") }, + }); + expect(install.status, install.stderr || install.stdout).toBe(0); + + const shimDir = `${bashHome}/.local/bin`; + const result = spawnSync(bashCommand, [`${shimDir}/node`], { + cwd: repoRoot, + encoding: "utf8", + env: { ...process.env, HOME: bashHome, PATH: `${shimDir}:${shimDir}:/usr/bin:/bin` }, + }); + expect(result.status, result.stderr || result.stdout).toBe(0); + expect(result.stdout.trim()).toBe(`${bashNodeBin}:/usr/bin:/bin`); + }); + it("writes managed shell policy behaviorally and preserves unrelated Codex config", () => { const home = temporaryDirectory("codex-cloud-home-"); mkdirSync(path.join(home, ".codex"), { recursive: true });