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
Original file line number Diff line number Diff line change
Expand Up @@ -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/<old version>`) and `~/.codex/config.toml` to `~/.codexclaw-rollback/0.2.42-<UTC>/` 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.

40 changes: 40 additions & 0 deletions devlog/_plan/261009_prompt_reduction/041_delivery.md
Original file line number Diff line number Diff line change
@@ -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-<UTC>/`, `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.

34 changes: 34 additions & 0 deletions devlog/_plan/261009_prompt_reduction/050_wp6_pstack_prs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>: <reason>`; 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.

Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
From 1ee7f612a11e1623e1f7f9c3f019d5948fa26bce Mon Sep 17 00:00:00 2001
From: bitkyc08-arch <bitkyc08@gmail.com>
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

Loading
Loading