Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/branch-review-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -593,3 +593,5 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie
| 2026-08-02 | claude/ds-v2-architecture | 93bb4b4756a7fad22f93008325f2c0f72471b0db | PR-Arch Wave4 motion/z/overlays/print | local gates green; frontend-ui-reviewer API-limited — glance required before auto-merge | unit 4972p; e2e:critical 15p; verify:ui 347p; verify:pr-local 0; eval:rag:offline pass |
| 2026-08-02 | claude/ds-v2-architecture | 649389ba7223f67281c8f3836dd542997c5cbd83 | PR #1583 review-and-fix | fixed Devin --ease-out Tailwind collision as --ease-out-keyword; synced main; Codex ledger-squash note outdated vs tip | vitest overlay+ckb-v2 34p; npm run test 4973p; merge-tree clean |
| 2026-08-02 | claude/ds-v2-architecture | 84147ee123bde50fceefad627d8c89791b27a713 | PR #1583 review-and-fix | fixed Devin --ease-out Tailwind collision as --ease-out-keyword; synced main; Codex ledger-squash note outdated vs tip | vitest overlay+ckb-v2 34p; npm run test 4973p; merge-tree clean |
| 2026-08-02 | codex/cloud-python-self-diagnosis | 2de7b7abbdea9a1286b198e14eecb021fff32693 | Cloud Python lock and self-diagnostics | PASS: no actionable findings | verify:pr-local PASS; focused Vitest 21/21; Python 3.11 and 3.12 lock reproduction PASS |
| 2026-08-02 | codex/cloud-python-self-diagnosis | 32cb80020bf1a63628dbf805f54f393aee5af528 | Cloud Python lock and self-diagnostics | PASS: review findings fixed; fail return claim refuted by Bash execution proof | verify:pr-local PASS; focused Vitest 22/22; Bash ERR trap proof PASS |
19 changes: 19 additions & 0 deletions docs/codex-cloud.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,25 @@ repairs the normal Node command boundary. Any runtime, dependency, CLI, Deno, Py
browser drift reruns the full setup instead of repairing only `node_modules`. All profile
insertions, command shims, CLI installs, and remote repair are idempotent.

### Automatic setup diagnostics

Cloud setup tracks its current phase. If a command fails, the error trap runs
`npm run diagnose:codex-cloud` automatically and prints sanitized `ISSUE` and `FIX` lines after
the original error. The diagnostic checks the Node/npm contract, the active Cloud Python runtime,
the runtime-specific Python lock header, and the failed setup phase without printing environment
values or calling a provider. Run it manually in an agent shell when setup completed but the
runtime later appears stale:

```bash
npm run diagnose:codex-cloud
```

The production worker image and Codex Cloud deliberately use separate hashed Python locks because
medspaCy 1.3.1 requires spaCy `<3.8` on Python 3.11 but `>=3.8` on Python 3.12. Production uses
`worker/python/requirements.txt` (Python 3.11); Cloud uses
`worker/python/requirements-cloud.txt` (Python 3.12). After changing
`worker/python/requirements.in`, regenerate and verify both locks with their matching interpreters.

## Acceptance

Run this in a fresh Cloud task before relying on the environment:
Expand Down
2 changes: 1 addition & 1 deletion docs/scripts-index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Scripts index

Curated map of `scripts/` (210 files) and the `package.json` script surface (218 entries),
Curated map of `scripts/` (212 files) and the `package.json` script surface (222 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 <x>`
referenced in docs resolves to a real script. `npm run docs:update` refreshes the exact counts above.
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
"build:internal": "node scripts/guard-next-build.mjs && npm run check:upload-limit-parity && node --max-old-space-size=8192 ./node_modules/next/dist/bin/next build --webpack && node scripts/check-client-bundle-secrets.mjs",
"build:analyze": "node scripts/build-analyze.mjs",
"generate:worker-python-lock": "node scripts/generate-worker-python-lock.mjs",
"generate:worker-python-cloud-lock": "node scripts/generate-worker-python-lock.mjs --target cloud",
"check:worker-python-lock": "node scripts/check-worker-python-lock.mjs",
"check:worker-python-cloud-lock": "node scripts/check-worker-python-lock.mjs --target cloud",
"check:worker-python-locks:static": "node scripts/check-worker-python-lock.mjs --static",
"diagnose:codex-cloud": "node scripts/diagnose-codex-cloud.mjs",
"smoke:app-container": "node scripts/app-container-smoke.mjs",
"check:bundle-budget": "node scripts/check-bundle-budget.mjs",
"check:lighthouse-budget": "node scripts/check-lighthouse-budget.mjs",
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-codex-cloud-setup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ export function validateCodexCloudSetup() {
for (const [pattern, message] of [
[/npm ci --include=dev/, "Cloud setup must install the exact lockfile with dev dependencies."],
[/deno@2/, "Cloud setup must install Deno 2.x."],
[/worker\/python\/requirements\.txt/, "Cloud setup must install Python worker requirements."],
[/worker\/python\/requirements-cloud\.txt/, "Cloud setup must install the Python 3.12 Cloud worker lock."],
[/CODEX_CLOUD_OCR_PYTHON/, "Cloud setup must expose the Python worker environment."],
[/playwright install --with-deps chromium firefox webkit/, "Cloud setup must install every browser."],
[/CODEX_CLOUD_ACCESS_PROFILE/, "Cloud setup must support explicit access profiles."],
Expand Down
48 changes: 31 additions & 17 deletions scripts/check-worker-python-lock.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,17 @@ import { tmpdir } from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";

import {
assertLockTarget,
pythonMajorMinor,
requestedLockTarget,
workerPythonLockTargets,
} from "./worker-python-lock-config.mjs";

const PYTHON = process.env.PYTHON_BIN?.trim() || (process.platform === "win32" ? "python" : "python3");
const PIP_TOOLS_VERSION = "7.6.0";
const IN_FILE = "worker/python/requirements.in";
const OUT_FILE = "worker/python/requirements.txt";
const target = requestedLockTarget();

function run(cmd, args, opts = {}) {
const result = spawnSync(cmd, args, { encoding: "utf8", stdio: "pipe", ...opts });
Expand All @@ -37,8 +44,10 @@ function ensureVenv(venvDir) {

function pipCompile(venvDir, outputFile) {
const python = venvPython(venvDir);
run(python, ["-m", "piptools", "compile", "--generate-hashes", "--output-file", outputFile, IN_FILE], {
env: { ...process.env, CUSTOM_COMPILE_COMMAND: "npm run generate:worker-python-lock" },
const args = ["-m", "piptools", "compile", "--generate-hashes", "--output-file", outputFile, IN_FILE];
if (target.includeUnsafe) args.splice(3, 0, "--allow-unsafe");
run(python, args, {
env: { ...process.env, CUSTOM_COMPILE_COMMAND: target.generateCommand },
});
}

Expand All @@ -49,26 +58,31 @@ function normalize(contents) {
.join("\n");
}

function assertHashedLockfile(contents) {
const lines = contents.split(/\r?\n/);
const hasPinned = lines.some((line) => /==/.test(line));
const hasHash = lines.some((line) => /--hash=sha256:/.test(line));
if (!hasPinned) throw new Error("requirements.txt does not contain pinned (==) versions");
if (!hasHash) throw new Error("requirements.txt does not contain --hash=sha256 entries");
}

function main() {
if (!existsSync(IN_FILE)) throw new Error(`Missing ${IN_FILE}`);
if (!existsSync(OUT_FILE)) throw new Error(`Missing ${OUT_FILE}`);
for (const lockTarget of Object.values(workerPythonLockTargets)) {
if (!existsSync(lockTarget.outputFile)) throw new Error(`Missing ${lockTarget.outputFile}`);
assertLockTarget(lockTarget);
}
if (process.argv.includes("--static")) {
console.log("Worker Python lock headers, pins, and hashes are valid for production and Cloud targets.");
return;
}

const actualPythonVersion = pythonMajorMinor(PYTHON);
if (actualPythonVersion !== target.pythonVersion) {
throw new Error(
`${target.name} lock verification requires Python ${target.pythonVersion}; ${PYTHON} is Python ${actualPythonVersion}. Set PYTHON_BIN to the matching interpreter.`,
);
}

const committed = readFileSync(OUT_FILE, "utf8");
assertHashedLockfile(committed);
const committed = readFileSync(target.outputFile, "utf8");

const venvDir = mkdtempSync(path.join(tmpdir(), "pip-tools-check-"));
let generated;
try {
ensureVenv(venvDir);
const tempOut = path.join(venvDir, "requirements.txt");
const tempOut = path.join(venvDir, path.basename(target.outputFile));
pipCompile(venvDir, tempOut);
generated = readFileSync(tempOut, "utf8");
} finally {
Expand All @@ -81,10 +95,10 @@ function main() {
"tmp/requirements-generated-diff.txt",
`--- committed\n+++ generated\n${committed}\n---\n${generated}`,
);
throw new Error(`${OUT_FILE} is out of sync with ${IN_FILE}. Regenerate with: npm run generate:worker-python-lock`);
throw new Error(`${target.outputFile} is out of sync with ${IN_FILE}. Regenerate with: ${target.generateCommand}`);
}

console.log(`${OUT_FILE} is in sync with ${IN_FILE}`);
console.log(`${target.outputFile} is in sync with ${IN_FILE} for Python ${target.pythonVersion}`);
}

main();
120 changes: 120 additions & 0 deletions scripts/diagnose-codex-cloud.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env node
import { existsSync, readFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { pathToFileURL } from "node:url";

import { lockPythonVersion, workerPythonLockTargets } from "./worker-python-lock-config.mjs";

function major(version) {
return version?.match(/\d+/)?.[0] ?? null;
}

export function diagnoseCodexCloud({
nodeVersion,
npmVersion,
pythonVersion,
cloudLockContents,
setupStep,
setupExitCode,
}) {
const issues = [];
if (major(nodeVersion) !== "24") {
issues.push({
code: "NODE_RUNTIME",
issue: `Node 24.x is required; detected ${nodeVersion || "unavailable"}.`,
fix: "Select Node 24 in the Cloud environment or rerun setup with nvm available.",
});
}
if (major(npmVersion) !== "11") {
issues.push({
code: "NPM_RUNTIME",
issue: `npm 11.x is required; detected ${npmVersion || "unavailable"}.`,
fix: "Run the repository Cloud setup so it installs the packageManager version from package.json.",
});
}
const expectedPython = workerPythonLockTargets.cloud.pythonVersion;
const actualLockPython = cloudLockContents ? lockPythonVersion(cloudLockContents) : null;
if (!cloudLockContents) {
issues.push({
code: "CLOUD_PYTHON_LOCK_MISSING",
issue: `${workerPythonLockTargets.cloud.outputFile} is missing.`,
fix: `Generate it with Python ${expectedPython}: ${workerPythonLockTargets.cloud.generateCommand}.`,
});
} else if (actualLockPython !== expectedPython) {
issues.push({
code: "CLOUD_PYTHON_LOCK_TARGET",
issue: `The Cloud Python lock targets ${actualLockPython || "an unknown version"}; Cloud requires Python ${expectedPython}.`,
fix: `Regenerate it with Python ${expectedPython}: ${workerPythonLockTargets.cloud.generateCommand}.`,
});
} else if (!/^setuptools==/m.test(cloudLockContents) || !/--hash=sha256:/.test(cloudLockContents)) {
issues.push({
code: "CLOUD_PYTHON_LOCK_INTEGRITY",
issue: "The Cloud Python lock does not pin and hash its Python 3.12 bootstrap dependencies.",
fix: `Regenerate it with Python ${expectedPython}: ${workerPythonLockTargets.cloud.generateCommand}.`,
});
}
if (!pythonVersion) {
issues.push({
code: "CLOUD_PYTHON_RUNTIME",
issue: `The Cloud OCR Python interpreter could not be executed; Python ${expectedPython} is required.`,
fix: `Rerun Cloud setup so it creates the Python ${expectedPython} OCR environment, or select Python ${expectedPython} in the Cloud environment.`,
});
} else if (!pythonVersion.startsWith(`${expectedPython}.`) && pythonVersion !== expectedPython) {
issues.push({
code: "CLOUD_PYTHON_RUNTIME",
issue: `Cloud OCR uses Python ${pythonVersion}; the Cloud lock targets Python ${expectedPython}.`,
fix: `Select Python ${expectedPython} in the Cloud environment, or deliberately add and validate a lock for the new runtime.`,
});
}
if (setupExitCode && setupExitCode !== "0") {
const pythonStep = setupStep === "python-worker-requirements";
issues.push({
code: "SETUP_COMMAND_FAILED",
issue: `Cloud setup stopped in ${setupStep || "an unknown step"} with exit code ${setupExitCode}.`,
fix: pythonStep
? `Review the pip error immediately above, regenerate the Python ${expectedPython} Cloud lock, then run npm run check:worker-python-cloud-lock.`
: "Review the first error immediately above, apply its suggested fix, and retry setup. The failed step is reported here to narrow the search.",
});
}
return issues;
}

function commandVersion(command, args = ["--version"]) {
const result = spawnSync(command, args, { encoding: "utf8", shell: false });
if (result.status !== 0) return null;
return `${result.stdout || result.stderr}`.match(/\d+(?:\.\d+){1,2}/)?.[0] ?? null;
}

function npmVersion() {
if (process.env.npm_execpath) return commandVersion(process.execPath, [process.env.npm_execpath, "--version"]);
if (process.platform === "win32")
return commandVersion(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", "npm.cmd --version"]);
return commandVersion("npm");
}

function argument(name) {
const index = process.argv.indexOf(name);
return index === -1 ? null : (process.argv[index + 1] ?? null);
}

export function main() {
const cloudLock = workerPythonLockTargets.cloud.outputFile;
const pythonBin = argument("--python-bin") || process.env.CODEX_CLOUD_OCR_PYTHON || "python3";
const issues = diagnoseCodexCloud({
nodeVersion: commandVersion(process.execPath, ["--version"]),
npmVersion: npmVersion(),
pythonVersion: commandVersion(pythonBin),
cloudLockContents: existsSync(cloudLock) ? readFileSync(cloudLock, "utf8") : null,
setupStep: argument("--setup-step"),
setupExitCode: argument("--exit-code"),
});
console.log(`[Codex Cloud Diagnose] status=${issues.length === 0 ? "HEALTHY" : "DEGRADED"} issues=${issues.length}`);
for (const issue of issues) {
console.log(`[Codex Cloud Diagnose] ISSUE ${issue.code}: ${issue.issue}`);
console.log(`[Codex Cloud Diagnose] FIX ${issue.code}: ${issue.fix}`);
}
if (issues.length === 0) console.log("[Codex Cloud Diagnose] No locally detectable setup issues.");
return issues.length === 0 ? 0 : 1;
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) process.exitCode = main();
17 changes: 13 additions & 4 deletions scripts/generate-worker-python-lock.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import { tmpdir } from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";

import { pythonMajorMinor, requestedLockTarget } from "./worker-python-lock-config.mjs";

const PYTHON = process.env.PYTHON_BIN?.trim() || (process.platform === "win32" ? "python" : "python3");
const PIP_TOOLS_VERSION = "7.6.0";
const IN_FILE = "worker/python/requirements.in";
const OUT_FILE = "worker/python/requirements.txt";
const target = requestedLockTarget();

function run(cmd, args, opts = {}) {
const result = spawnSync(cmd, args, { encoding: "utf8", stdio: "pipe", ...opts });
Expand All @@ -38,17 +40,24 @@ function ensureVenv(venvDir) {
function pipCompile(venvDir, outputFile) {
const python = venvPython(venvDir);
const args = ["-m", "piptools", "compile", "--generate-hashes", "--output-file", outputFile, IN_FILE];
run(python, args, { env: { ...process.env, CUSTOM_COMPILE_COMMAND: "npm run generate:worker-python-lock" } });
if (target.includeUnsafe) args.splice(3, 0, "--allow-unsafe");
run(python, args, { env: { ...process.env, CUSTOM_COMPILE_COMMAND: target.generateCommand } });
}

function main() {
if (!existsSync(IN_FILE)) throw new Error(`Missing ${IN_FILE}`);
const actualPythonVersion = pythonMajorMinor(PYTHON);
if (actualPythonVersion !== target.pythonVersion) {
throw new Error(
`${target.name} lock generation requires Python ${target.pythonVersion}; ${PYTHON} is Python ${actualPythonVersion}. Set PYTHON_BIN to the matching interpreter.`,
);
}

const venvDir = mkdtempSync(path.join(tmpdir(), "pip-tools-"));
try {
ensureVenv(venvDir);
pipCompile(venvDir, OUT_FILE);
console.log(`Generated ${OUT_FILE}`);
pipCompile(venvDir, target.outputFile);
console.log(`Generated ${target.outputFile} for Python ${target.pythonVersion}`);
} finally {
rmSync(venvDir, { recursive: true, force: true });
}
Expand Down
Loading
Loading