From 1ee7f612a11e1623e1f7f9c3f019d5948fa26bce Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Fri, 9 Oct 2026 15:12:00 +0900 Subject: [PATCH 1/4] 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 @@ 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" From f24eb70426a6f3eb7047877542bf008fc6189016 Mon Sep 17 00:00:00 2001 From: foxytanuki Date: Wed, 21 Oct 2026 05:10:21 +0000 Subject: [PATCH 2/4] chore(upstream): sync pstack to 88d4751 (0.15.29) --- UPSTREAM.lock | 2 +- .../0001-worktree-audit-linux-and-codex.patch | 12 +- tests/test_build.py | 182 +++++++++ tests/test_models.py | 140 +++++++ tests/test_worktree_audit.py | 191 +++++++++ upstream/.cursor-plugin/plugin.json | 2 +- upstream/README.md | 60 ++- upstream/agents/comment-sicko.md | 2 +- upstream/agents/poteto-agent.md | 4 +- upstream/docs/guide/01-setup.md | 25 +- upstream/docs/guide/02-poteto-mode.md | 51 ++- upstream/docs/guide/03-understand.md | 36 +- upstream/docs/guide/04-design.md | 54 ++- upstream/docs/guide/05-build-and-clean.md | 16 +- upstream/docs/guide/06-verify-and-ship.md | 85 +++- upstream/docs/guide/07-overnight.md | 45 ++- upstream/docs/guide/08-principles.md | 15 +- upstream/docs/guide/09-make-it-yours.md | 43 +- .../docs/guide/10-recipes-and-pitfalls.md | 90 ++++- upstream/docs/guide/README.md | 20 +- upstream/docs/harness/claude-code.md | 67 +++ upstream/plugin.json | 21 + upstream/skills/architect/SKILL.md | 18 +- .../architect/references/design-red-flags.md | 24 ++ .../architect/references/runner-prompt.md | 2 +- upstream/skills/arena/SKILL.md | 51 +-- upstream/skills/automate-me/SKILL.md | 2 +- upstream/skills/benchmark-checklist/SKILL.md | 39 ++ upstream/skills/blast-radius/SKILL.md | 2 +- upstream/skills/correct/SKILL.md | 32 ++ .../create-formal-verification-skill/SKILL.md | 41 ++ .../skills/create-verification-skill/SKILL.md | 6 +- upstream/skills/figure-it-out/SKILL.md | 9 +- upstream/skills/how/SKILL.md | 44 +- .../skills/how/references/explorer-prompt.md | 17 - upstream/skills/in-your-own-words/SKILL.md | 7 + upstream/skills/interrogate/SKILL.md | 86 ++-- .../interrogate/references/lead-judgment.md | 19 - .../interrogate/references/reviewer-prompt.md | 18 +- .../skills/interrogate/references/rubric.md | 16 +- upstream/skills/ketchup/SKILL.md | 43 ++ .../SKILL.md | 58 +++ upstream/skills/make-bot-ui/SKILL.md | 2 +- upstream/skills/no-comments/SKILL.md | 2 +- upstream/skills/poteto-help/SKILL.md | 161 ++++++++ .../poteto-help/references/prompting.md | 51 +++ .../skills/poteto-help/references/recipes.md | 51 +++ upstream/skills/poteto-mode/SKILL.md | 120 +++--- .../poteto-mode/playbooks/autonomous-run.md | 2 +- .../poteto-mode/playbooks/autopilot-full.md | 30 +- .../poteto-mode/playbooks/autopilot-stack.md | 16 +- .../skills/poteto-mode/playbooks/babysit.md | 4 - .../skills/poteto-mode/playbooks/bug-fix.md | 2 +- upstream/skills/poteto-mode/playbooks/eval.md | 2 +- .../skills/poteto-mode/playbooks/feature.md | 2 +- .../skills/poteto-mode/playbooks/hillclimb.md | 6 +- .../poteto-mode/playbooks/multi-phase-plan.md | 13 +- .../poteto-mode/playbooks/opening-a-pr.md | 33 +- .../poteto-mode/playbooks/orchestrate.md | 15 +- .../poteto-mode/playbooks/perf-issue.md | 23 +- .../poteto-mode/playbooks/refactoring.md | 2 +- .../playbooks/runtime-forensics.md | 2 +- .../poteto-mode/playbooks/session-pickup.md | 2 +- .../skills/poteto-mode/playbooks/shipping.md | 21 +- .../poteto-mode/playbooks/trace-forensics.md | 8 +- .../poteto-mode/playbooks/worktree-cleanup.md | 6 +- .../skills/poteto-mode/scripts/check-plan.mjs | 33 +- .../poteto-mode/scripts/check-plan.test.ts | 96 +++++ .../poteto-mode/scripts/orch/orch.test.ts | 321 ++++++++++----- .../skills/poteto-mode/scripts/orch/orch.ts | 16 +- .../skills/poteto-mode/scripts/orch/store.ts | 382 +++++++++++------- .../skills/poteto-mode/scripts/package.json | 2 +- .../scripts/watch-pr/fakes.test-helper.ts | 17 +- .../scripts/watch-pr/github.test.ts | 134 +++++- .../poteto-mode/scripts/watch-pr/github.ts | 87 ++-- .../scripts/watch-pr/policy.test.ts | 2 +- .../poteto-mode/scripts/watch-pr/policy.ts | 8 +- .../poteto-mode/scripts/watch-pr/types.ts | 14 +- .../poteto-mode/scripts/worktree-audit.sh | 62 ++- .../principle-attack-the-premise/SKILL.md | 14 +- .../skills/principle-build-the-lever/SKILL.md | 6 +- .../SKILL.md | 22 +- .../SKILL.md | 14 +- .../principle-explain-the-number/SKILL.md | 17 + .../skills/principle-fix-root-causes/SKILL.md | 24 +- .../SKILL.md | 4 - .../principle-laziness-protocol/SKILL.md | 8 - .../SKILL.md | 10 +- .../SKILL.md | 8 - .../principle-minimize-reader-load/SKILL.md | 11 +- .../principle-model-the-domain/SKILL.md | 12 +- .../SKILL.md | 19 +- .../SKILL.md | 4 +- .../SKILL.md | 5 +- .../SKILL.md | 21 +- .../principle-type-system-discipline/SKILL.md | 28 +- upstream/skills/recall/SKILL.md | 2 +- upstream/skills/reflect/SKILL.md | 46 +-- .../reflect/references/divergent-reviewer.md | 21 +- .../reflect/references/judgment-reviewer.md | 21 +- .../skills/reflect/references/synthesizer.md | 4 +- .../reflect/references/tooling-reviewer.md | 25 +- upstream/skills/setup-pstack/SKILL.md | 32 +- upstream/skills/sharpen/SKILL.md | 16 + upstream/skills/show-me-your-work/SKILL.md | 2 +- upstream/skills/swarm/SKILL.md | 9 +- upstream/skills/technical-writing/SKILL.md | 34 +- .../skills/typescript-best-practices/SKILL.md | 2 +- .../references/patterns.md | 39 +- upstream/skills/unslop/SKILL.md | 25 +- upstream/skills/why/SKILL.md | 118 ++---- upstream/skills/why/references/epistemics.md | 117 +----- .../why/references/investigator-prompt.md | 65 ++- .../references/sources/code-archaeology.md | 49 +-- .../skills/why/references/sources/datadog.md | 24 +- .../references/sources/incident-postmortem.md | 10 - .../skills/why/references/sources/sentry.md | 40 +- .../skills/why/references/sources/slack.md | 37 -- .../why/references/synthesizer-prompt.md | 74 ++-- 119 files changed, 3025 insertions(+), 1461 deletions(-) create mode 100644 tests/test_build.py create mode 100644 tests/test_models.py create mode 100644 tests/test_worktree_audit.py create mode 100644 upstream/docs/harness/claude-code.md create mode 100644 upstream/plugin.json create mode 100644 upstream/skills/benchmark-checklist/SKILL.md create mode 100644 upstream/skills/correct/SKILL.md create mode 100644 upstream/skills/create-formal-verification-skill/SKILL.md create mode 100644 upstream/skills/in-your-own-words/SKILL.md create mode 100644 upstream/skills/ketchup/SKILL.md create mode 100644 upstream/skills/maintain-formal-verification-skill/SKILL.md create mode 100644 upstream/skills/poteto-help/SKILL.md create mode 100644 upstream/skills/poteto-help/references/prompting.md create mode 100644 upstream/skills/poteto-help/references/recipes.md create mode 100644 upstream/skills/poteto-mode/scripts/check-plan.test.ts create mode 100644 upstream/skills/principle-explain-the-number/SKILL.md create mode 100644 upstream/skills/sharpen/SKILL.md diff --git a/UPSTREAM.lock b/UPSTREAM.lock index 1773278..ea1c234 100644 --- a/UPSTREAM.lock +++ b/UPSTREAM.lock @@ -1,3 +1,3 @@ repo=https://github.com/cursor/plugins.git path=pstack -ref=12d587dfb20741cafc376c42c696c5f6e2a64487 +ref=88d47514b82e7c44f00f2b2a72ff6dd12158038c diff --git a/overlay/patches/0001-worktree-audit-linux-and-codex.patch b/overlay/patches/0001-worktree-audit-linux-and-codex.patch index 262f1d8..854de08 100644 --- a/overlay/patches/0001-worktree-audit-linux-and-codex.patch +++ b/overlay/patches/0001-worktree-audit-linux-and-codex.patch @@ -1,8 +1,8 @@ --- a/skills/poteto-mode/scripts/worktree-audit.sh +++ b/skills/poteto-mode/scripts/worktree-audit.sh -@@ -22,11 +22,19 @@ - gh pr list --author "@me" --state all --limit 1000 \ - --json number,state,headRefName 2>/dev/null > "$prs" || echo "[]" > "$prs" +@@ -61,11 +61,19 @@ + fi + [ -s "$prs" ] || echo "[]" > "$prs" -# Transcripts dir: ~/.cursor/projects//agent-transcripts. +# Transcripts: Cursor keeps them per project under @@ -18,10 +18,10 @@ +mtimes() { if stat -c %Y / >/dev/null 2>&1; then xargs stat -c '%Y %n'; else xargs stat -f '%m %N'; fi; } +day() { date -d "@$1" '+%Y-%m-%d' 2>/dev/null || date -r "$1" '+%Y-%m-%d'; } + + echo "# base: $base" printf "SIZE\tAGE\tMERGED\tDIRTY\tREMOTE\tPR\tLAST_CHAT\tBUCKET\tWORKTREE\n" - git worktree list --porcelain | awk '/^worktree /{print $2}' | while read -r wt; do -@@ -63,12 +71,11 @@ +@@ -103,12 +111,11 @@ # Most recent chat whose transcript operated in this worktree. Match path # followed by "/" or a quote so glint-482 does not match glint-482-r37. last="-"; last_ts=0 @@ -39,7 +39,7 @@ recent=$([ "$last_ts" -gt 0 ] 2>/dev/null && [ $(( (now - last_ts) / 86400 )) -le 4 ] && echo yes || echo no) case "$dirty" in wip:*) bucket=hold-wip ;; *) -@@ -83,4 +90,4 @@ +@@ -123,4 +130,4 @@ "$size" "$age" "$merged" "$dirty" "$remote" "$pr" "$last" "$bucket" "$wt" done | sort -t$'\t' -k1,1 -rh diff --git a/tests/test_build.py b/tests/test_build.py new file mode 100644 index 0000000..8727c06 --- /dev/null +++ b/tests/test_build.py @@ -0,0 +1,182 @@ +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +TOOL = ROOT / "tools" / "pstack_opencodex.py" +sys.path.insert(0, str(ROOT / "tools")) +import pstack_opencodex # noqa: E402 + +# Skill folders cursor/plugins pstack added between 12d587d and 88d4751 (0.15.29). +ADDED_IN_0_15_29 = ( + "benchmark-checklist", + "correct", + "create-formal-verification-skill", + "in-your-own-words", + "ketchup", + "maintain-formal-verification-skill", + "poteto-help", + "principle-explain-the-number", + "sharpen", +) +EXPLICIT_ONLY_POLICY = "policy:\n allow_implicit_invocation: false\n" + + +def excluded_skills(): + lines = (ROOT / "overlay" / "exclude").read_text().splitlines() + return {line.strip() for line in lines if line.strip() and not line.startswith("#")} + + +def upstream_skills(): + return {p.name for p in (ROOT / "upstream" / "skills").iterdir() if (p / "SKILL.md").exists()} + + +def frontmatter(path): + front, _ = pstack_opencodex.split_frontmatter(path.read_text()) + return front + + +def build(out): + result = subprocess.run([sys.executable, str(TOOL), "build", "--out", str(out)], capture_output=True, text=True, timeout=120) + if result.returncode != 0: + raise AssertionError(f"build failed:\n{result.stdout}{result.stderr}") + return result + + +class BuiltSkills(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.tmp = tempfile.TemporaryDirectory(prefix="pstack-build-test-") + # Resolve symlinked temp roots (macOS /var -> /private/var) so paths match the build output. + cls.dist = Path(cls.tmp.name).resolve() / "dist" + try: + cls.result = build(cls.dist) + except AssertionError: + cls.tmp.cleanup() + raise + cls.info = json.loads((cls.dist / "BUILD-INFO.json").read_text()) + cls.built = {p.name for p in (cls.dist / "skills").iterdir() if (p / "SKILL.md").exists()} + + @classmethod + def tearDownClass(cls): + cls.tmp.cleanup() + + def test_build_ships_every_upstream_skill_except_the_exclusions(self): + self.assertEqual(self.built, upstream_skills() - excluded_skills()) + self.assertEqual(set(self.info["excluded"]), excluded_skills()) + self.assertNotIn("make-bot-ui", self.built) + + def test_build_reports_how_many_skills_it_ships(self): + count = len(upstream_skills() - excluded_skills()) + self.assertEqual(self.info["skills"], count) + self.assertIn(f"built {count} skills into {self.dist}", self.result.stdout) + + def test_skills_added_in_0_15_29_are_built_with_the_harness_pointer(self): + pointer = f"read {self.dist}/HARNESS.md" + for name in ADDED_IN_0_15_29: + with self.subTest(skill=name): + skill_md = self.dist / "skills" / name / "SKILL.md" + self.assertTrue(skill_md.exists(), f"{name} was not built") + if name.startswith("principle-"): + self.assertNotIn(pointer, skill_md.read_text()) + else: + self.assertIn(pointer, skill_md.read_text()) + + def test_each_built_skill_is_named_after_its_folder(self): + for name in sorted(self.built): + with self.subTest(skill=name): + front = frontmatter(self.dist / "skills" / name / "SKILL.md") + names = [line.split(":", 1)[1].strip() for line in front if line.startswith("name:")] + self.assertEqual(names, [name]) + + def test_explicit_only_policy_follows_upstream_frontmatter(self): + explicit = 0 + for name in sorted(self.built): + with self.subTest(skill=name): + upstream_front = frontmatter(ROOT / "upstream" / "skills" / name / "SKILL.md") + flagged = "disable-model-invocation: true" in [line.strip() for line in upstream_front] + meta = self.dist / "skills" / name / "agents" / "openai.yaml" + self.assertEqual(meta.exists(), flagged) + if flagged: + self.assertEqual(meta.read_text(), EXPLICIT_ONLY_POLICY) + explicit += flagged + self.assertEqual(self.info["explicit_only"], explicit) + + def test_upstream_plugin_manifests_are_not_shipped(self): + self.assertFalse((self.dist / "plugin.json").exists()) + self.assertFalse((self.dist / ".cursor-plugin").exists()) + + def test_both_license_notices_are_shipped(self): + self.assertEqual((self.dist / "LICENSE").read_bytes(), (ROOT / "LICENSE").read_bytes()) + self.assertEqual((self.dist / "LICENSE.pstack").read_bytes(), (ROOT / "upstream" / "LICENSE").read_bytes()) + + +class SkillNameNormalization(unittest.TestCase): + """Upstream names every skill after its folder since #702. The build keeps its existing + normalization as a safety net: a no-op for matching names, a rewrite for a future mismatch.""" + + def setUp(self): + self.temporary = tempfile.TemporaryDirectory(prefix="pstack-name-test-") + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name).resolve() + + def skill(self, folder, name): + skill_dir = self.root / "skills" / folder + skill_dir.mkdir(parents=True) + (skill_dir / "SKILL.md").write_text( + f"---\nname: {name}\ndescription: test skill\ndisable-model-invocation: true\n---\n\n# Test\n" + ) + return skill_dir + + def test_a_name_that_differs_from_the_folder_is_rewritten_to_the_folder(self): + skill_dir = self.skill("poteto-mode", "Poteto Mode") + self.assertTrue(pstack_opencodex.adapt_skill(skill_dir, self.root)) + front = frontmatter(skill_dir / "SKILL.md") + self.assertEqual([line for line in front if line.startswith("name:")], ["name: poteto-mode"]) + + def test_a_matching_name_is_kept_and_adapted(self): + skill_dir = self.skill("ketchup", "ketchup") + self.assertTrue(pstack_opencodex.adapt_skill(skill_dir, self.root)) + text = (skill_dir / "SKILL.md").read_text() + self.assertTrue(text.startswith("---\nname: ketchup\n")) + self.assertIn(f"read {self.root}/HARNESS.md", text) + self.assertEqual((skill_dir / "agents" / "openai.yaml").read_text(), EXPLICIT_ONLY_POLICY) + + +class OverlayPatches(unittest.TestCase): + def test_every_patch_applies_to_upstream_without_offset_or_fuzz(self): + if shutil.which("patch") is None: + self.skipTest("patch is not available") + # GNU patch translates its diagnostics. The C locale keeps "offset" and "fuzz" in English, + # and LANGUAGE is dropped so gettext cannot pick another catalog. + env = {key: value for key, value in os.environ.items() if key != "LANGUAGE"} + env["LC_ALL"] = "C" + version = subprocess.run(["patch", "--version"], capture_output=True, text=True, env=env) + if "GNU patch" not in version.stdout: + self.skipTest("offset and fuzz reporting is checked with GNU patch") + patches = sorted((ROOT / "overlay" / "patches").glob("*.patch")) + self.assertTrue(patches) + for patch in patches: + with self.subTest(patch=patch.name), tempfile.TemporaryDirectory() as tmp: + targets = re.findall(r"^\+\+\+ b/(\S+)", patch.read_text(), re.M) + self.assertTrue(targets, f"{patch.name} names no target file") + for rel in targets: + (Path(tmp) / rel).parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(ROOT / "upstream" / rel, Path(tmp) / rel) + result = subprocess.run( + ["patch", "-p1", "--batch", "--forward", "--dry-run", "-F", "0", "-d", tmp, "-i", str(patch)], + capture_output=True, text=True, timeout=30, env=env, + ) + output = result.stdout + result.stderr + self.assertEqual(result.returncode, 0, output) + self.assertNotRegex(output, r"(?i)\boffset\b|\bfuzz\b") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..4796807 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,140 @@ +import json +import os +import re +import subprocess +import sys +import tempfile +import unittest +from collections import Counter +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +TOOL = ROOT / "tools" / "pstack_opencodex.py" +sys.path.insert(0, str(ROOT / "tools")) +from pstack_opencodex import parse_roles, upstream_roles # noqa: E402 + +OPUS = "claude-opus-5-5-xhigh" +GROK = "grok-4.7-xhigh-fast" + +# The role table in upstream setup-pstack step 5 at cursor/plugins 88d4751 (pstack 0.15.29), +# written out by hand so the tests do not take their expectations from the parser under test. +# Update it when a sync changes upstream's defaults; that change should be reviewed. +DEFAULTS_AT_0_15_29 = { + "feature, refactoring": [GROK], + "bug-fix": [GROK], + "perf-issue": [GROK], + "hillclimb": [GROK], + "judgment and prose": [OPUS], + "hardest tasks": [OPUS], + "how explorer": [GROK], + "how explainer": [OPUS], + "why investigators": [GROK], + "why synthesizer": [OPUS], + "reflect tooling": [GROK], + "reflect judgment, divergent, synthesizer": [OPUS], + "arena runners": [OPUS, GROK], + "arena cross-judge pool": [OPUS, GROK], + "swarm workers": [GROK], + "architect runners": [OPUS, GROK], + "interrogate reviewers": [OPUS, GROK], +} +PANELS = {"arena runners", "arena cross-judge pool", "architect runners", "interrogate reviewers"} + +# What each default must resolve to against the synthetic catalog below: the provider-prefixed +# slug and xhigh, with Grok's speed suffix dropped rather than carried into the model or effort. +RESOLVED = { + OPUS: ("anthropic/claude-opus-5-5", "xhigh"), + GROK: ("devin/grok-4.7", "xhigh"), +} + +# One check-models line for a resolved value, e.g. +# "OK arena runners: grok-4.7-xhigh-fast -> model=devin/grok-4.7 reasoning_effort=xhigh". +# Anything after the effort (a clamp note, "[not an upstream role line]") makes the line not match. +OK_LINE = re.compile(r"OK +(?P