From e10ddd34fc30200e74202fd53a08755242336e29 Mon Sep 17 00:00:00 2001 From: Michael Beutler Date: Fri, 25 Sep 2026 23:52:51 -0400 Subject: [PATCH] fix: resolve Codex command on Windows --- MANIFEST.sha256 | 9 +++-- .../plans/cpk-060-windows-codex-resolution.md | 40 +++++++++++++++++++ docs/roadmap.md | 1 + kit.py | 15 +++++-- tests/test_kit.py | 13 +++++- tests/test_ponytail.py | 1 + 6 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 docs/plans/cpk-060-windows-codex-resolution.md diff --git a/MANIFEST.sha256 b/MANIFEST.sha256 index 44c8dec..ea813e3 100644 --- a/MANIFEST.sha256 +++ b/MANIFEST.sha256 @@ -124,12 +124,13 @@ a2a2977403154705fc3d73fde0f45548c4722cc20b11cc189fe44f7982899224 ./docs/plans/c a5d8013da3d22afba28c1259e29e3255d1d19969d0934b2f59069f0de80ea31b ./docs/plans/cpk-057-review-and-astra-guidance.md 5f2abf3bc85fbfc580d05139165ff95f29a3a85246bab2724c5f949eb93f26a5 ./docs/plans/cpk-058-plan-history-compaction.md 43141b61bf800dc9956603b6e88cdd0c3a5564340d3f4aee3fb6b0682dceee00 ./docs/plans/cpk-059-public-pr-release.md -c14438f22cad4a83b4409a72e75b589d19440d20516511bd7d4a419b27deb7ed ./docs/roadmap.md +8cea5185f3a1181dc9a1419f3f980712b2b9e35936ec2a8011629d67cd0d041f ./docs/plans/cpk-060-windows-codex-resolution.md +23106fd9f30090fc85692c14612b95f69be5b8d2757137a3df19ed6093d2af0b ./docs/roadmap.md 6f8f14ca5fd8ad935eee66256b3f7c49d5c40957eedff938ba21365e30869719 ./doctor.ps1 ab56a88d9188979f06ea142d1f58ea0d6ad9088e83a965bd324209d83da513ad ./doctor.sh 8087bdb3ee654c0baac278a8e3bf85e33e314f8acfd486e7ebf675126a697058 ./install.ps1 3466d2e34145cfdaa83d793960794b2aefc96f3d9c48d22fd2af930ec819e9c4 ./install.sh -7843fa0b49e088495e603e8eb3ae6811cb40007d7b40bf3e621f7855d1722159 ./kit.py +7c4238f798f567de9b7de28268b5fd344a861ce8fff1d16da7f2e2966908f8f5 ./kit.py 331f17ca1486b5779d9b57a3a73834328eb3089ce612e636e6a8e5085899d61e ./run-tests.ps1 e6df9f1da02280a66161583529b06a34f0103041e9587afad0fac6074eecc15b ./run-tests.sh 85a2ba72e2b0cd1ab6c25a51f39059502415c3ac62a6bffa613d7e31b6422cd4 ./setup-repo.ps1 @@ -140,8 +141,8 @@ e1e231aac51822f7e1bb3aa61aba138cf8c5df9cefd3079b0c1b5a98eea03bd9 ./tests/fixtur a269775204ae7cad2457d00f5565c90da2df41a9719479f3489ec01afdd2519c ./tests/fixtures/cpk056-agent-cases.json e32941eb74be1f2cdcb6c82cf71e113b841651bf0d01786f364176de2da1b9c5 ./tests/fixtures/cpk056-agent-grading.json 289367b7090a0d71936b41f08c4a354bc2758013dceb709819de36a1b73d9aa6 ./tests/live_plan_history.py -74d96d387c814f60e37f0ba682bb42e1ba05b729c866b929d026166a4a9be4c9 ./tests/test_kit.py -cfa479b5248a628ecd71194b7ac4a36bcddf17af7eb52ca2a6c3cd0566269927 ./tests/test_ponytail.py +9b6d3048643461026467186632f3de49fbc533aaa8a91de46b699c67cfe7bad3 ./tests/test_kit.py +1979109d44a6b49c01747216c99dc6598e5cc725b986df4eea60215e4598f4d8 ./tests/test_ponytail.py d16f7eadc8c668e82a1d0d2554ed04def15cb0a4b2ce13ac1ce1339bc0e62161 ./uninstall.ps1 f12cff94ef7f9b0ca489cbe43e1de737011476966e04c1578f4977177b0da217 ./uninstall.sh e96e2cd99b37377ca84d420d142d097981f303f5006d930880352ad9404d3b13 ./upstream.lock.json diff --git a/docs/plans/cpk-060-windows-codex-resolution.md b/docs/plans/cpk-060-windows-codex-resolution.md new file mode 100644 index 0000000..b191a0a --- /dev/null +++ b/docs/plans/cpk-060-windows-codex-resolution.md @@ -0,0 +1,40 @@ +# Resolve the Codex command on Windows + +This ExecPlan records the scope and validation for CPK-060. Maintain it according to `.agent/PLANS.md` until review closure. + +## Purpose + +The Windows installer and Doctor must run the npm-installed Codex command. Python cannot start the extensionless `codex` name directly when only the Windows npm command shim is available. + +## Scope + +Resolve `codex` with the Python standard library on Windows and reuse that path for plugin installation and Doctor checks. Keep the existing command name on macOS and Linux. Isolate Git test fixtures from the host's line-ending conversion so the Windows suite verifies repository bytes consistently. + +This bug fix publishes version `0.24.1`. It adds no dependency, configuration option, or new installation path. + +## Progress + +- [x] Reproduced the Windows command lookup failure. +- [x] Added one shared Windows command resolver and its regression check. +- [x] Isolated Git fixtures from global `core.autocrlf` settings. +- [x] Passed the complete test suite, installer, and Doctor on Windows. +- [x] Completed adversarial review. Publication continues through the protected pull-request workflow. + +## Plan history reconciliation + +No Plan Mode record applies to CPK-060. The task began from the user's direct bug report and publication request. + +## Validation and acceptance + +Run from the repository root: + + pwsh -File .\run-tests.ps1 + pwsh -File .\install.ps1 + pwsh -File .\doctor.ps1 + git diff --check + +Require every manifest checksum to match. Require Doctor to report `Result: ready` for toolkit version `0.24.1` and the installed Codex CLI. + +## Outcome + +On Windows, the toolkit uses the executable path returned by `shutil.which("codex")`. Native plugin installation and Doctor use the resolved npm `.CMD` shim. Other platforms keep the existing `codex` command. Final adversarial review found no actionable defect. diff --git a/docs/roadmap.md b/docs/roadmap.md index e9e3326..1432a78 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -33,6 +33,7 @@ No residual risk is accepted. ## Completed +- ✅ **CPK-060 — Resolve the Codex command on Windows.** Use the Windows npm command shim for native plugin installation and Doctor checks. Publish version `0.24.1`. [ExecPlan](plans/cpk-060-windows-codex-resolution.md) - ✅ **CPK-059 — Publish through protected pull requests.** Add readiness-triggered CI, make the repository public, protect main, and release reusable Plan History as version `0.24.0`. [ExecPlan](plans/cpk-059-public-pr-release.md) - ✅ **CPK-058 — Compact Plan History and resume interrupted reads.** Reuse verified summaries without changing original records, and preserve reading progress across normal context compaction. [ExecPlan](plans/cpk-058-plan-history-compaction.md) - ✅ **CPK-057 — Separate review from testing and complete Astra guidance.** Keep test execution before review and add bounded autonomy, clear communication, and useful delegation to the existing rule owners. [ExecPlan](plans/cpk-057-review-and-astra-guidance.md) diff --git a/kit.py b/kit.py index 5d71337..928f59d 100755 --- a/kit.py +++ b/kit.py @@ -24,7 +24,7 @@ from typing import Any, Iterable KIT_ID = "codex-practical-kit" -KIT_VERSION = "0.24.0" +KIT_VERSION = "0.24.1" REPOWISE_VERSION = "0.45.0" UV_VERSION = "0.12.4" UV_INSTALLER_URL = f"https://astral.sh/uv/{UV_VERSION}/install.sh" @@ -178,6 +178,12 @@ def command_version(command: list[str]) -> str | None: return text.splitlines()[0] if text else None +def codex_executable() -> str: + if not is_windows(): + return "codex" + return shutil.which("codex") or "codex" + + def json_load(path: Path, default: Any) -> Any: if not path.exists(): return default @@ -493,7 +499,7 @@ def manifest_path(paths: InstallPaths) -> Path: def codex_plugin(paths: InstallPaths, *args: str) -> dict[str, Any]: result = run( - ["codex", "plugin", *args, "--json"], + [codex_executable(), "plugin", *args, "--json"], env={**os.environ, "CODEX_HOME": str(paths.codex_home)}, ) try: @@ -1121,11 +1127,12 @@ def doctor(args: argparse.Namespace, paths: InstallPaths) -> int: version or "unavailable", ) - codex_version = command_version(["codex", "--version"]) + codex = codex_executable() + codex_version = command_version([codex, "--version"]) ok &= check(bool(codex_version), "Codex CLI", codex_version or "not found") if codex_version: login = run( - ["codex", "login", "status"], check=False, timeout=30, + [codex, "login", "status"], check=False, timeout=30, env={**os.environ, "CODEX_HOME": str(paths.codex_home)}, ) login_text = (login.stdout or login.stderr).strip().replace("\n", " ") diff --git a/tests/test_kit.py b/tests/test_kit.py index 0e137de..0e07f0e 100644 --- a/tests/test_kit.py +++ b/tests/test_kit.py @@ -22,6 +22,7 @@ class GitFixture: def __init__(self, root: Path): self.root = root subprocess.run(["git", "init", "-q"], cwd=root, check=True) + subprocess.run(["git", "config", "core.autocrlf", "false"], cwd=root, check=True) subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=root, check=True) subprocess.run(["git", "config", "user.name", "Test"], cwd=root, check=True) @@ -78,6 +79,14 @@ def test_run_decodes_utf8_subprocess_output(self): ) self.assertEqual(result.stdout, "✓") + def test_windows_codex_command_resolves_npm_cmd_shim(self): + shim = r"C:\Users\Example\AppData\Roaming\npm\codex.CMD" + with mock.patch.object(kit, "is_windows", return_value=True), mock.patch.object( + kit.shutil, "which", return_value=shim + ) as which: + self.assertEqual(kit.codex_executable(), shim) + which.assert_called_once_with("codex") + def paths(self, base: Path) -> kit.InstallPaths: return kit.InstallPaths( base / "home", base / "codex", base / "skills", base / "kit" @@ -697,7 +706,7 @@ def test_plan_history_stays_internal_for_every_event(self): subprocess.run(["git", "add", "docs", ".agent/plan-history"], cwd=root, check=True) subprocess.run(["git", "commit", "-qm", "retain Plan history"], cwd=root, check=True) clone = Path(temp) / "clone" - subprocess.run(["git", "clone", "-q", str(root), str(clone)], check=True) + subprocess.run(["git", "-c", "core.autocrlf=false", "clone", "-q", str(root), str(clone)], check=True) cloned, = (clone / ".agent/plan-history").glob("*.md") self.assertEqual(cloned.read_bytes(), message.encode()) self.assertEqual(list((clone / "docs").glob("*.plan-summary.*.md")), []) @@ -3455,7 +3464,7 @@ def test_lifecycle_skills_and_version(self): self.assertIn("`pwsh -File .\\doctor.ps1`", publication) self.assertIn("from the reviewed candidate", publication) self.assertIn("`Result: ready`", publication) - self.assertEqual(kit.KIT_VERSION, "0.24.0") + self.assertEqual(kit.KIT_VERSION, "0.24.1") self.assertNotIn("Version `0.22.0`", (ROOT / "README.md").read_text()) self.assertNotIn("version 0.22.0", (ROOT / "CODEX-INSTALL-PROMPT.md").read_text()) diff --git a/tests/test_ponytail.py b/tests/test_ponytail.py index aba673d..109a6cd 100644 --- a/tests/test_ponytail.py +++ b/tests/test_ponytail.py @@ -31,6 +31,7 @@ def setUp(self): mock.patch.object(kit, "download_file", return_value=b"# upstream fixture\n"), mock.patch.object(kit, "ensure_repowise_runtime", return_value=("fake-uv", "fake-repowise")), mock.patch.object(kit, "find_runtime_command", side_effect=lambda paths, name: "fake-" + name), + mock.patch.object(kit, "codex_executable", return_value="codex"), mock.patch.dict(os.environ, {"CODEX_HOME": str(self.root / "default")}), ): patch.start()