diff --git a/devlog/_plan/261009_prompt_reduction/040_wp5_release_deploy.md b/devlog/_plan/261009_prompt_reduction/040_wp5_release_deploy.md index fc018ed1..c5bdcdd3 100644 --- a/devlog/_plan/261009_prompt_reduction/040_wp5_release_deploy.md +++ b/devlog/_plan/261009_prompt_reduction/040_wp5_release_deploy.md @@ -51,3 +51,12 @@ No new design decision: no architect consultation (the 0.2.40 release precedent) - R3 (rollback). Before install the script copies the current cache directory (`~/.codex/plugins/cache/codexclaw/codexclaw/`) and `~/.codex/config.toml` to `~/.codexclaw-rollback/0.2.42-/` on that host. Restore: copy the saved cache directory back, restore `config.toml`, run `hooks retrust` from the restored cache, and run the same doctor and deny smoke. A host that fails verification is restored, then reported. - Nit. After the version bump: `check-versions.mjs 0.2.42`, plus `rg` for leftover `0.2.41` (excluding CHANGELOG, devlog and fixture paths), `npm ls --workspaces --depth=0` to check cli, GUI and lock entries; doctor output must contain hook-trust PASS with a nonzero count. + +## wp5 D summary (2026-10-09) + +Conclusion: codexclaw 0.2.42 is released from `main` `17e23f5c` (v0.2.42, Latest, assets verified) and deployed from the same tree (`dev` `417ba6f8`) to the local Mac and six SSH hosts with trust PASS (32 hooks), a byte-identical payload and the SHELL-SUBST-01 deny smoke on each; five hosts were unreachable, as in 0.2.41. Record: 041_delivery.md. + +What did not go well: runner saturation stretched the release by about an hour (each push to `dev` queued CI, Packed install and WSL ahead of the PR); deployment therefore ran from the CI-verified `dev` SHA before the `main` promotion finished, a deliberate order change from this doc. macmini-cf needed a zsh login shell for node (the 0.2.41 record said so, the script defaulted to bash). mini kept a busy 0.2.41 cache directory. A `git stash pop` misfire applied an unrelated 2026-09-08 stash to the release branch; it was reverted to HEAD before any commit and the stash entry is intact. The hypothesis that died: that the compiled guard smoke runs through `hook pre-tool-use`; SHELL-SUBST-01 lives under `hook worktree-guard-pretool`. Evidence the direction is wrong: a host reporting hook-trust failures or the old 0.2.41 cache still loaded after a Codex restart. + +Next: wp6 (pstack-opencodex PRs) per 050. + diff --git a/devlog/_plan/261009_prompt_reduction/041_delivery.md b/devlog/_plan/261009_prompt_reduction/041_delivery.md new file mode 100644 index 00000000..02d7557c --- /dev/null +++ b/devlog/_plan/261009_prompt_reduction/041_delivery.md @@ -0,0 +1,40 @@ +# 041 — Delivery record: PRs, release 0.2.42, deployment + +## PRs (merge commits on `dev`) + +| PR | Content | Head (checks) | Merge | +|---|---|---|---| +| #287 | fork-lane dispatch docs (user's PR) | `977bd116` (14/14) | `6520b7b8` | +| #288 | L1 hook injections | `eda55ed3` (13/13, enforce-target ran on open) | `a3001789` | +| #289 | standard, gate, core routers | `df7c7b34` (14/14) | `af7e9afe` | +| #290 | catalog and routers | `38330756` (14/14) | `5feda454` | +| #291 | release 0.2.42 | `db752b56` (14/14) | `417ba6f8` | +| #292 | `dev` → `main` promotion | see below | see below | + +## Deployment of `dev` `417ba6f8946a100d900c1d49648fbef21e60b9b5` (0.2.42+codex.20261009045258) + +Script: `evidence/deploy-host.sh` (pinned SHA, rollback capture under `~/.codexclaw-rollback/0.2.42-/`, `dev-install.sh --no-build`, retrust, doctor, set+bytes payload compare against `git archive`, compiled SHELL-SUBST-01 deny smoke through `hook worktree-guard-pretool`). Deployed from the CI-verified `dev` SHA before the `main` promotion finished, because runners were saturated; the tree equals what `main` receives. + +| Host | OS | Before | After | Trust | Payload (missing/changed/extra) | Smoke | Notes | +|---|---|---|---|---|---|---|---| +| local Mac (`~/Developer/new/700_projects/codexclaw`) | macOS | 0.2.41 @225a2a7d | 0.2.42 @417ba6f8 | PASS 32 | 0/0/0 | deny | | +| lidge | Linux | 0.2.41 | 0.2.42 | PASS 32 | 0/0/0 | deny | | +| macmini-cf | macOS | 0.2.41 | 0.2.42 | PASS 32 | 0/0/0 | deny | first run under `bash -l` could not find node (exit 127) and left 0.2.41 installed; rerun under `zsh -l` succeeded | +| clisu-oracle (= cli-jaw-server) | Linux | 0.2.41 | 0.2.42 | PASS 32 | 0/0/0 | deny | | +| suji | macOS | 0.2.41 | 0.2.42 | PASS 32 | 0/0/0 | deny | | +| desktop-c795oh4 | Windows (Git Bash) | 0.2.41 | 0.2.42 | PASS 32 | 0/0/0 | deny | | +| mini | Windows (Git Bash) | 0.2.41 | 0.2.42 | PASS 32 | 0/0/0 | deny | installer exit 1: pruning the old 0.2.41 cache failed with "Device or resource busy" (a running Codex session holds it); 0.2.42 is installed and verified; the stale directory goes away on the next install | +| intmb, win | — | — | not deployed | — | — | — | Cloudflare websocket bad handshake (same as 0.2.41) | +| oracle, cursor, ocx-ci | — | — | not deployed | — | — | — | ssh timeout (same as 0.2.41) | + +Running Codex sessions keep the plugin version they started with; new threads load 0.2.42. On the local Mac the 0.2.41 cache was pruned, so this coordinator session uses the 0.2.42 CLI for its remaining FSM commands. + + +## Promotion and release + +- #292 (`dev` → `main`): 28/28 checks on head `417ba6f8` (pull_request and push events, CI, Packed install, WSL); merged as `main` `17e23f5c6fe286eb7cafdb7491ce32c784e97886`, tree identical to `417ba6f8`. +- `main` push runs on `17e23f5c`: CI success, Packed install success (WSL was still running at dispatch). +- Release dry run 37891482501: `release verify: READY — 0.2.42 @ 17e23f5c`, `pass=3696 fail=0 total=3772` (the 76 unlisted are platform skips, as in 0.2.41). +- Publish 37891760770: success, "published v0.2.42 with 3 assets". +- Independent asset check: `shasum -a 256 -c SHA256SUMS` OK; payload unpacked vs `git archive 17e23f5c plugins/codexclaw`: 0 differences; manifest `0.2.42+codex.20261009045258`; tag `v0.2.42` → `17e23f5c`; latest release `v0.2.42`; not a prerelease. + diff --git a/devlog/_plan/261009_prompt_reduction/050_wp6_pstack_prs.md b/devlog/_plan/261009_prompt_reduction/050_wp6_pstack_prs.md index c1fb39d1..64be2bf2 100644 --- a/devlog/_plan/261009_prompt_reduction/050_wp6_pstack_prs.md +++ b/devlog/_plan/261009_prompt_reduction/050_wp6_pstack_prs.md @@ -47,3 +47,37 @@ Dispositions: W1, W3, W5 accepted as planned. W2 accepted: the collision test li Gap P1 (delivery dependency) resolved: PR B is stacked on PR A's branch (base `main` in the PR, with a note that it contains A's commit until A merges), so its fresh macOS proof runs on a tree where A's rename exists. If A merges first, B is rebased onto `main`. Gap P2 and P3 folded above. + +## wp6 P revalidation and A residuals (2026-10-09) + +Continuity, quoting the wp5 D summary: "Next: wp6 (pstack-opencodex PRs) per 050." Upstream `main` is still `2dd2800` with no PRs, so both defects and the plan stand. Fork: `lidge-jun/pstack-opencodex` (created during wp5 wait, no branches pushed). + +Residual 1 (research/09): the delivery line "independent, either order" is replaced. PR A targets `main`. PR B is branched from PR A's branch and also targets `main`; its description says it contains A's commit until A merges, links A, and names its own review range (the last commit). After A merges, B is rebased onto `main` and re-verified. + +Residual 2: tests are labeled by role. RED regression tests must fail on `2dd2800` and pass after the fix: the case-collision test against the real index (A); the eight catalog error-contract cases (B: missing file, malformed JSON, non-object document, non-array `models`, non-object entry, missing slug, non-string slug, malformed effort). Compatibility tests pass before and after: the collision helper's shared-parent fixture (A) and the successful `check-models` run with a clamped effort (B). C records the RED run on `2dd2800` and the GREEN run on each branch, plus `git diff --exit-code 2dd2800 HEAD -- upstream/`. + + +## wp6 B record + +Implemented in a clone of foxytanuki/pstack-opencodex (`/tmp/pso.EjTe/repo`, base `2dd2800`); the exact patches are in `evidence/pstack-opencodex/` (`0001-…` = PR A, `0002-…` = PR B, stacked). + +- PR A, branch `fix/upstream-lock-case` (`1ee7f61`): `UPSTREAM` → `UPSTREAM.lock` (bytes unchanged, renamed through the index), readers updated (`LOCK_FILE`, module docstring, README layout row, CONTRIBUTING), NEW `tests/test_repository.py` (case-collision helper over tracked paths and their directory prefixes; real-index test), `.github/workflows/ci.yml` adds a `macos-15` job that first checks the checkout is complete. The macOS run exposed an existing failure on `2dd2800`: `test_explicit_catalog_override_wins_over_config` compares an unresolved temp path with the CLI's resolved one (`/var` → `/private/var`); the test now resolves its temp root (one line). +- PR B, branch `fix/check-models-catalog-errors` (`80ea77c`, on top of A): `catalog_efforts(records)` split out of `load_catalog`, which now validates through `pstack_runtime.catalog_records` and keeps its return shape; `check-models` reads the catalog once inside one `(OSError, ValueError)` boundary and exits 1 with `cannot read model catalog : `; the disabled-model set reuses the validated records. +- RED on `2dd2800` (this Mac): the real-index collision test fails with `[['UPSTREAM', 'upstream']]` (both helper fixtures pass); the eight catalog cases fail (tracebacks or wrong exit/prefix); the clamp compatibility case passes. GREEN: A 25/25, B 27/27; build (46 skills), both license `cmp`, `git diff --check` and `git diff --exit-code 2dd2800 -- upstream/` pass on both branches. + + +## wp6 C round 1 (research/19_review_wp6.md, GO-WITH-FIXES) + +Finding 1 (duplicate slugs): routing validation through `catalog_records` (a slug-keyed dict) dropped earlier entries for a repeated slug, so a disabled first entry or a malformed first entry stopped failing. Fixed by `catalog_models(path)`, which shape-checks every entry in file order and keeps duplicates; `catalog_efforts` walks that list; `load_catalog` keeps its return shape and its original per-entry validation, so `check-runtime` behaves as on `2dd2800`. New regression `test_every_duplicate_slug_entry_is_still_checked` (both cases). PR B is now 28/28 locally; the patch in `evidence/pstack-opencodex/0002-…` is refreshed. + + +## wp6 delivery and D summary (2026-10-09) + +- PR A: https://github.com/foxytanuki/pstack-opencodex/pull/1 (`fix/upstream-lock-case` `1ee7f61`). +- PR B: https://github.com/foxytanuki/pstack-opencodex/pull/2 (`fix/check-models-catalog-errors` `1f6db41`, on top of A, disclosed in the body). +- Checks: GitGuardian SUCCESS on both heads. The repository's CI workflow runs are `action_required` (GitHub's approval gate for a first-time contributor's fork PR); only the maintainer can approve them, so hosted CI on these heads is approval-blocked, not failed. Local proof: fresh macOS clones of both branches pass the repository's four CI steps (`WP6_VERIFIED` under `cxc receipt test`). Review: research/19 GO-WITH-FIXES (duplicate slugs) → fixed → research/20 PASS. + +Conclusion: two small fixes for defects reproduced on macOS went to pstack-opencodex; nothing went to Cursor's pstack (no external change has been merged there). Three ideas from the comparison landed in codexclaw itself through #289 (neutral review packets, failure classes for delegated calls, compact routing). + +What did not go well: the first PR B routed validation through a slug-keyed helper and silently dropped duplicate entries, a behavior change only an independent probe caught; the FSM's source-delta rule cannot see work in another repository, so the B evidence had to be the recorded patches. Evidence the direction is wrong: the maintainer rejecting the lock rename in favor of renaming `upstream/`, or the macOS job being unwanted CI cost. + diff --git a/devlog/_plan/261009_prompt_reduction/evidence/pstack-opencodex/0001-fix-rename-the-upstream-lock-so-it-does-not-collide-.patch b/devlog/_plan/261009_prompt_reduction/evidence/pstack-opencodex/0001-fix-rename-the-upstream-lock-so-it-does-not-collide-.patch new file mode 100644 index 00000000..9eec8707 --- /dev/null +++ b/devlog/_plan/261009_prompt_reduction/evidence/pstack-opencodex/0001-fix-rename-the-upstream-lock-so-it-does-not-collide-.patch @@ -0,0 +1,176 @@ +From 1ee7f612a11e1623e1f7f9c3f019d5948fa26bce Mon Sep 17 00:00:00 2001 +From: bitkyc08-arch +Date: Fri, 9 Oct 2026 15:12:00 +0900 +Subject: [PATCH 1/2] fix: rename the upstream lock so it does not collide with + upstream/ + +UPSTREAM and the upstream/ directory differ only by case, so a fresh clone on +a case-insensitive filesystem (default macOS and Windows) cannot hold both: +git reports UPSTREAM as deleted and build fails with IsADirectoryError. + +Rename the lock to UPSTREAM.lock (content unchanged) and update its readers. +Add a test that fails when two tracked paths, including directory prefixes, +differ only by case, and a macOS CI job that checks the checkout is complete. +The macOS job also surfaced a temp-path comparison in test_runtime.py that +failed on macOS before this change (/var is a symlink to /private/var); the +test now resolves its temp root. +--- + .github/workflows/ci.yml | 21 +++++++++++++++++++ + CONTRIBUTING.md | 2 +- + README.md | 2 +- + UPSTREAM => UPSTREAM.lock | 0 + tests/test_repository.py | 44 +++++++++++++++++++++++++++++++++++++++ + tests/test_runtime.py | 3 ++- + tools/pstack_opencodex.py | 4 ++-- + 7 files changed, 71 insertions(+), 5 deletions(-) + rename UPSTREAM => UPSTREAM.lock (100%) + create mode 100644 tests/test_repository.py + +diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml +index 77eb885..c6f485e 100644 +--- a/.github/workflows/ci.yml ++++ b/.github/workflows/ci.yml +@@ -30,3 +30,24 @@ jobs: + run: | + git diff --check + git diff --exit-code -- upstream/ ++ verify-macos: ++ # Case-insensitive filesystem: catches tracked paths that differ only by case. ++ runs-on: macos-15 ++ steps: ++ - uses: actions/checkout@v7.0.1 ++ - name: Check the checkout is complete ++ run: test -z "$(git status --porcelain)" ++ - uses: actions/setup-python@v7.0.0 ++ with: ++ python-version: "3.13" ++ - name: Test runtime diagnostics ++ run: python3 -m unittest discover -s tests -v ++ - name: Build skills and verify license notices ++ run: | ++ python3 tools/pstack_opencodex.py build ++ cmp LICENSE dist/LICENSE ++ cmp upstream/LICENSE dist/LICENSE.pstack ++ - name: Check whitespace and upstream preservation ++ run: | ++ git diff --check ++ git diff --exit-code -- upstream/ +diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md +index bdb23a9..4a26e1f 100644 +--- a/CONTRIBUTING.md ++++ b/CONTRIBUTING.md +@@ -4,7 +4,7 @@ Issues and pull requests are welcome. Include the command or workflow you used, + + ## Make a change + +-- Keep `upstream/` identical to the source pinned in `UPSTREAM`. Use the sync command to update it. ++- Keep `upstream/` identical to the source pinned in `UPSTREAM.lock`. Use the sync command to update it. + - Put harness instructions and patches in `overlay/`, and Python tooling in `tools/`. + - Keep `dist/` out of Git. It is generated and contains local absolute paths. + - Use English for project documentation and Conventional Commits for commit messages. +diff --git a/README.md b/README.md +index d5fef57..79400ed 100644 +--- a/README.md ++++ b/README.md +@@ -89,7 +89,7 @@ The harness maps Cursor cloud-agent steps to local workers. Available tools and + + | Path | Purpose | + | --- | --- | +-| `UPSTREAM` | Source repository, subtree, and pinned commit. | ++| `UPSTREAM.lock` | Source repository, subtree, and pinned commit. | + | `upstream/` | Unmodified copy of `cursor/plugins/pstack`. | + | `overlay/` | Harness instructions, patches, exclusions, and model example. | + | `tools/` | Build, install, synchronization, and diagnostic tools. | +diff --git a/UPSTREAM b/UPSTREAM.lock +similarity index 100% +rename from UPSTREAM +rename to UPSTREAM.lock +diff --git a/tests/test_repository.py b/tests/test_repository.py +new file mode 100644 +index 0000000..404c0c2 +--- /dev/null ++++ b/tests/test_repository.py +@@ -0,0 +1,44 @@ ++import shutil ++import subprocess ++import unittest ++from pathlib import Path ++ ++ROOT = Path(__file__).resolve().parents[1] ++ ++ ++def case_collisions(paths): ++ """Return groups of distinct tracked spellings that only differ by case. ++ ++ Every directory prefix counts as a path too, so a file named UPSTREAM and a ++ directory named upstream/ collide on case-insensitive filesystems (default ++ macOS and Windows), where one of them cannot be checked out. ++ """ ++ spellings = {} ++ for path in paths: ++ parts = path.split("/") ++ for depth in range(1, len(parts) + 1): ++ name = "/".join(parts[:depth]) ++ spellings.setdefault(name.casefold(), set()).add(name) ++ return sorted(sorted(group) for group in spellings.values() if len(group) > 1) ++ ++ ++class CaseCollisions(unittest.TestCase): ++ def test_helper_flags_a_file_that_collides_with_a_directory(self): ++ self.assertEqual(case_collisions(["UPSTREAM", "upstream/LICENSE"]), [["UPSTREAM", "upstream"]]) ++ ++ def test_helper_allows_shared_parents(self): ++ self.assertEqual(case_collisions(["upstream/a", "upstream/b", "tools/x.py"]), []) ++ ++ def test_tracked_paths_check_out_on_case_insensitive_filesystems(self): ++ if shutil.which("git") is None: ++ self.skipTest("git is not available") ++ probe = subprocess.run(["git", "-C", str(ROOT), "rev-parse", "--is-inside-work-tree"], capture_output=True, text=True) ++ if probe.returncode != 0: ++ self.skipTest("not a git work tree") ++ listed = subprocess.run(["git", "-C", str(ROOT), "ls-files", "-z"], capture_output=True, check=True) ++ paths = [p for p in listed.stdout.decode("utf-8").split("\0") if p] ++ self.assertEqual(case_collisions(paths), []) ++ ++ ++if __name__ == "__main__": ++ unittest.main() +diff --git a/tests/test_runtime.py b/tests/test_runtime.py +index 7901f99..484a1b4 100644 +--- a/tests/test_runtime.py ++++ b/tests/test_runtime.py +@@ -91,7 +91,8 @@ class RuntimeCLI(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory(prefix="pstack-runtime-test-") + self.addCleanup(self.temporary.cleanup) +- self.root = Path(self.temporary.name) ++ # Resolve symlinked temp roots (macOS /var -> /private/var) so path comparisons match the CLI. ++ self.root = Path(self.temporary.name).resolve() + self.catalog = self.root / "selected.json" + self.roles = self.root / "models.md" + self.codex = self.root / "config.toml" +diff --git a/tools/pstack_opencodex.py b/tools/pstack_opencodex.py +index 0bd9a34..d34dfef 100644 +--- a/tools/pstack_opencodex.py ++++ b/tools/pstack_opencodex.py +@@ -1,7 +1,7 @@ + #!/usr/bin/env python3 + """Keep pstack identical to Cursor's upstream and build a Codex-ready copy. + +-upstream/ verbatim copy of cursor/plugins/pstack at the ref in UPSTREAM ++upstream/ verbatim copy of cursor/plugins/pstack at the ref in UPSTREAM.lock + overlay/ the only files this repository owns (harness map, patches, examples) + dist/ generated by "build"; skills are installed as symlinks into dist/skills + """ +@@ -23,7 +23,7 @@ REPO = Path(__file__).resolve().parent.parent + UPSTREAM_DIR = REPO / "upstream" + OVERLAY_DIR = REPO / "overlay" + DIST_DIR = REPO / "dist" +-LOCK_FILE = REPO / "UPSTREAM" ++LOCK_FILE = REPO / "UPSTREAM.lock" + CODEX_HOME = Path(os.environ.get("CODEX_HOME", Path.home() / ".codex")) + DEFAULT_MODELS_FILE = CODEX_HOME / "pstack-models.md" + DEFAULT_CATALOG = CODEX_HOME / "models_cache.json" +-- +2.50.1 + diff --git a/devlog/_plan/261009_prompt_reduction/evidence/pstack-opencodex/0002-fix-report-unreadable-model-catalogs-without-a-trace.patch b/devlog/_plan/261009_prompt_reduction/evidence/pstack-opencodex/0002-fix-report-unreadable-model-catalogs-without-a-trace.patch new file mode 100644 index 00000000..114c8dac --- /dev/null +++ b/devlog/_plan/261009_prompt_reduction/evidence/pstack-opencodex/0002-fix-report-unreadable-model-catalogs-without-a-trace.patch @@ -0,0 +1,162 @@ +From 1f6db410bea0027df08a71a71dd8c43530434ec2 Mon Sep 17 00:00:00 2001 +From: bitkyc08-arch +Date: Fri, 9 Oct 2026 15:12:55 +0900 +Subject: [PATCH 2/2] fix: report unreadable model catalogs without a traceback + +check-models read the catalog with a bare json.loads and indexed model["slug"], +so a missing file, malformed JSON or an unexpected shape ended in a Python +traceback. check-runtime already reports the same inputs as a BLOCKED +diagnostic. + +catalog_models() now shape-checks every entry in file order (duplicates are +kept, so a disabled or malformed earlier entry is still seen), catalog_efforts() +projects reasoning efforts, and load_catalog() keeps its return shape. +check-models reads the catalog once inside one error boundary and exits 1 with +"cannot read model catalog : ". +--- + tests/test_runtime.py | 60 +++++++++++++++++++++++++++++++++++++++ + tools/pstack_opencodex.py | 33 +++++++++++++++++---- + 2 files changed, 88 insertions(+), 5 deletions(-) + +diff --git a/tests/test_runtime.py b/tests/test_runtime.py +index 484a1b4..ef6d822 100644 +--- a/tests/test_runtime.py ++++ b/tests/test_runtime.py +@@ -198,5 +198,65 @@ class RuntimeCLI(unittest.TestCase): + self.assertIn("catalog path must be absolute", result.stdout) + + ++class CheckModelsCLI(unittest.TestCase): ++ def setUp(self): ++ self.temporary = tempfile.TemporaryDirectory(prefix="pstack-models-test-") ++ self.addCleanup(self.temporary.cleanup) ++ self.root = Path(self.temporary.name).resolve() ++ self.catalog = self.root / "catalog.json" ++ self.roles = self.root / "models.md" ++ self.roles.write_text("how explainer: anthropic/claude-opus-5-5-xhigh\n") ++ ++ def run_models(self): ++ return subprocess.run([sys.executable, str(ROOT / "tools/pstack_opencodex.py"), "check-models", "--file", str(self.roles), "--catalog", str(self.catalog)], text=True, capture_output=True, timeout=10) ++ ++ def test_unreadable_catalogs_fail_with_a_message_instead_of_a_traceback(self): ++ bad_effort = models() ++ bad_effort["gpt-6.1-sol"]["supported_reasoning_levels"] = [{}] ++ cases = { ++ "missing file": None, ++ "malformed JSON": "not-json", ++ "non-object document": "[]", ++ "non-array models": '{"models": {}}', ++ "non-object entry": '{"models": [null]}', ++ "missing slug": '{"models": [{}]}', ++ "non-string slug": '{"models": [{"slug": 7}]}', ++ "malformed effort": json.dumps({"models": list(bad_effort.values())}), ++ } ++ for name, contents in cases.items(): ++ with self.subTest(name): ++ if contents is None: ++ self.catalog.unlink(missing_ok=True) ++ else: ++ self.catalog.write_text(contents) ++ before = self.catalog.read_bytes() if self.catalog.exists() else None ++ result = self.run_models() ++ self.assertEqual(result.returncode, 1, result.stderr) ++ self.assertTrue(result.stderr.startswith("cannot read model catalog " + str(self.catalog)), result.stderr) ++ self.assertNotIn("Traceback", result.stderr) ++ self.assertNotIn("checking names", result.stdout) ++ after = self.catalog.read_bytes() if self.catalog.exists() else None ++ self.assertEqual(before, after) ++ ++ def test_every_duplicate_slug_entry_is_still_checked(self): ++ disabled_first = [{"slug": "demo", "multi_agent_version": "disabled"}, {"slug": "demo", "multi_agent_version": "v1"}] ++ self.roles.write_text("how explainer: demo\n") ++ self.catalog.write_text(json.dumps({"models": disabled_first})) ++ result = self.run_models() ++ self.assertEqual(result.returncode, 1, result.stdout + result.stderr) ++ self.assertIn("disabled for collaboration", result.stdout) ++ bad_first = [{"slug": "demo", "supported_reasoning_levels": [{}]}, {"slug": "demo", "supported_reasoning_levels": [{"effort": "high"}]}] ++ self.catalog.write_text(json.dumps({"models": bad_first})) ++ result = self.run_models() ++ self.assertEqual(result.returncode, 1, result.stdout + result.stderr) ++ self.assertTrue(result.stderr.startswith("cannot read model catalog"), result.stderr) ++ ++ def test_a_valid_catalog_still_resolves_and_clamps(self): ++ self.catalog.write_text(json.dumps({"models": list(models().values())})) ++ result = self.run_models() ++ self.assertEqual(result.returncode, 0, result.stdout + result.stderr) ++ self.assertIn("anthropic/claude-opus-5-5", result.stdout) ++ self.assertIn("clamped from xhigh", result.stdout) ++ + if __name__ == "__main__": + unittest.main() +diff --git a/tools/pstack_opencodex.py b/tools/pstack_opencodex.py +index d34dfef..b18a49c 100644 +--- a/tools/pstack_opencodex.py ++++ b/tools/pstack_opencodex.py +@@ -148,20 +148,39 @@ def cmd_build(args: argparse.Namespace) -> None: + print(f"built {len(skills)} skills into {dist} ({explicit} explicit-only)") + + +-def load_catalog(path: Path) -> dict[str, list[str]]: ++def catalog_models(path: Path) -> list[dict]: ++ """Read and shape-check every model entry, keeping duplicates in file order.""" + data = json.loads(path.read_text()) ++ if not isinstance(data, dict): ++ raise ValueError("expected an object") ++ models = data.get("models", []) ++ if not isinstance(models, list): ++ raise ValueError("expected a models array") ++ for model in models: ++ if not isinstance(model, dict) or not isinstance(model.get("slug"), str): ++ raise ValueError("expected model objects with string slugs") ++ return models ++ ++ ++def catalog_efforts(models: list[dict]) -> dict[str, list[str]]: ++ """Map each catalog slug to the reasoning efforts pstack understands.""" + catalog = {} +- for model in data.get("models", []): ++ for model in models: ++ slug = model["slug"] + raw_levels = model.get("supported_reasoning_levels") or [] + if not isinstance(raw_levels, list): + raise ValueError("supported_reasoning_levels must be an array") + levels = [lvl.get("effort") if isinstance(lvl, dict) else lvl for lvl in raw_levels] + if any(not isinstance(lvl, str) for lvl in levels): + raise ValueError("reasoning levels must be strings or objects with string efforts") +- catalog[model["slug"]] = [lvl for lvl in levels if lvl in EFFORTS] ++ catalog[slug] = [lvl for lvl in levels if lvl in EFFORTS] + return catalog + + ++def load_catalog(path: Path) -> dict[str, list[str]]: ++ return catalog_efforts(catalog_models(path)) ++ ++ + def split_effort(value: str) -> tuple[str, str | None]: + tokens = value.split("-") + if tokens[-1] == "fast": +@@ -222,7 +241,12 @@ def upstream_roles() -> dict[str, list[str]]: + + + def cmd_check_models(args: argparse.Namespace) -> None: +- catalog = load_catalog(Path(args.catalog)) ++ catalog_path = Path(args.catalog) ++ try: ++ records = catalog_models(catalog_path) ++ catalog = catalog_efforts(records) ++ except (OSError, ValueError) as error: ++ raise SystemExit(f"cannot read model catalog {catalog_path}: {error}") + defaults = upstream_roles() + if args.defaults: + roles, source = defaults, "upstream defaults" +@@ -233,7 +257,6 @@ def cmd_check_models(args: argparse.Namespace) -> None: + roles, source = parse_roles(path.read_text()), str(path) + print(f"checking names and reasoning efforts in {source} against {args.catalog}") + print("This does not verify collaboration transport or live provider availability; run check-runtime before delegation.") +- records = json.loads(Path(args.catalog).read_text()).get("models", []) + disabled = {model["slug"] for model in records if model.get("multi_agent_version") == "disabled"} + failures = 0 + for label, values in roles.items(): +-- +2.50.1 + diff --git a/devlog/_plan/261009_prompt_reduction/research/18_audit_wp6_round2.md b/devlog/_plan/261009_prompt_reduction/research/18_audit_wp6_round2.md new file mode 100644 index 00000000..88fd767a --- /dev/null +++ b/devlog/_plan/261009_prompt_reduction/research/18_audit_wp6_round2.md @@ -0,0 +1,11 @@ +# wp6 A re-check + +HEAD 664e69c0. Read-only except report; reviewed 050_wp6_pstack_prs.md against research/09_audit_wp6.md. + +1. Delivery residual resolved (050:55). Explicitly supersedes :37's independence claim: A targets main; B contains A temporarily, links A and names B's review range. Rebase and fresh verification after A merges are required. The disclosed main-targeted dependency is executable. + +2. Activation residual resolved (050:57). Separates RED collision/eight error-contract regressions from compatibility fixtures that pass before and after; requires baseline RED and branch GREEN records. Base-to-head upstream/ comparison also closes the earlier vendor-proof caveat. + +No new blocker. :53's upstream/fork state remains a PR-time admission check under :5's NOOP rule; this audit verifies the amended plan, not implementation or hosted CI. + +VERDICT: PASS diff --git a/devlog/_plan/261009_prompt_reduction/research/19_review_wp6.md b/devlog/_plan/261009_prompt_reduction/research/19_review_wp6.md new file mode 100644 index 00000000..109db8ed --- /dev/null +++ b/devlog/_plan/261009_prompt_reduction/research/19_review_wp6.md @@ -0,0 +1,13 @@ +# wp6 C review + +Reviewed both patches and branches: base 2dd2800, A 1ee7f61, B 80ea77c. Source anchors below are relative to /tmp/pso.EjTe/repo. Report-only; no Git/product/outbound writes. + +Pre-scan: base-to-B git diff --check and upstream/ diff both exit 0. Fourteen RuntimeRules/collision tests pass without scratch writes. In-memory filesystem probes pass all eight new error contracts and verify one catalog read on the valid path. Full CLI suite/build/hosted CI not rerun here. + +1. Medium, verified — B discards earlier duplicate-slug records before checking them (tools/pstack_opencodex.py:154-168,237-251; tools/pstack_runtime.py:54-58). Trigger: two entries for demo, first disabled, last v1. Baseline check-models exits 1 with disabled diagnostic; B exits 0. With malformed efforts on the first duplicate and valid efforts on the last, baseline check-runtime returns BLOCKED/invalid_input (1); B returns CONFIGURATION_OK (0). Reproduced with actual base/head functions and in-memory reads. This changes runtime behavior and weakens validation. Fix: validate every original entry and retain the any-disabled check before deduplication; add both regressions. check-runtime calls modified load_catalog (:280). + +Other coverage: A's byte-identical lock rename updates all consumers/docs. Its prefix/casefold test and macOS CI fit the repo. tests/test_runtime.py:94-95 resolves the temp fixture; existing RuntimeCLI assertions are otherwise identical, and runtime diagnostics source is untouched. B's boundary precedes output, uses exit 1 and the requested stderr prefix, and preserves effort/clamping for unique slugs. CONTRIBUTING.md, README.md, lock, CI, repository/runtime tests and tooling were reviewed. English, Conventional Commits, no dependencies and preserved vendor/license sources fit contribution rules. Disclose B's A dependency/review range in its PR body. + +blocking_issues: 1 (finding 1; fix before sending PR B). PR A has no blocker. + +VERDICT: GO-WITH-FIXES (blockers=1) diff --git a/devlog/_plan/261009_prompt_reduction/research/20_review_wp6_round2.md b/devlog/_plan/261009_prompt_reduction/research/20_review_wp6_round2.md new file mode 100644 index 00000000..994250e1 --- /dev/null +++ b/devlog/_plan/261009_prompt_reduction/research/20_review_wp6_round2.md @@ -0,0 +1,9 @@ +# wp6 C round 2 + +Base 2dd2800; fix/check-models-catalog-errors at 1f6db410bea0027df08a71a71dd8c43530434ec2. Report-only. Re-ran base/head functions with in-memory filesystem reads; no scratch writes. + +Finding 1 resolved. Six probes pass: disabled-first check-models retains identical disabled diagnostic/exit 1; check-runtime retains baseline CONFIGURATION_OK/0; load_catalog retains the last entry's efforts. Malformed-first load_catalog retains identical ValueError; check-runtime retains identical BLOCKED/invalid_input JSON and exit 1. check-models still rejects with exit 1, adding only the intended catalog-error prefix. + +Source /tmp/pso.EjTe/repo/tools/pstack_opencodex.py:151-181,260 validates the ordered list before overwriting efforts and scans all entries for disabled slugs. New regression at tests/test_runtime.py:241 covers both cases. Valid check-models still reads once; fixtures unchanged. diff --check passes. No residual blocker; full suite/CI not rerun. + +VERDICT: PASS