Skip to content
Open
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
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
File renamed without changes.
44 changes: 44 additions & 0 deletions tests/test_repository.py
Original file line number Diff line number Diff line change
@@ -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()
3 changes: 2 additions & 1 deletion tests/test_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions tools/pstack_opencodex.py
Original file line number Diff line number Diff line change
@@ -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
"""
Expand All @@ -23,7 +23,7 @@
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"
Expand Down