# New script: spec-revocation-check — generic spec-amendment revocation gate ## Rules (read first) - Stay in this worktree; don't push; don't work around permission prompts (stop and report). - Follow this repo's CLAUDE.md exactly: OpenSpec in `specs/spec-revocation-check.spec.md` FIRST, then `scripts/spec-revocation-check`, then `tests/test-spec-revocation-check.sh`, wired into `tests/run-all.sh`. - Python 3 stdlib only (`#!/usr/bin/env python3`). No third-party deps. - Do NOT symlink into ~/sbin (the supervisor does that after review). - Reference implementation (READ-ONLY copy; do not try to write there): `/run/user/1000/mm-worktree-task/small-scripts/ref-spec-revocation/` — `check_spec_revocations.py` (the completeness half), `test_spec_m33_quotes.py` (the verbatim-quote half), `test_check_spec_revocations.py` (its temp-repo tests). They come from a private project; reuse the logic, drop every project-specific default. ## Why it exists When a spec clause is revoked or reworded, the old text must stay in the file as a verbatim quote (`Revokes prior text (): ""`). An LLM agent fabricated such quotes twice; a second failure mode is deleting normative text with no quote at all. This script catches BOTH, for any git repo. ## Required behaviour `spec-revocation-check --base [--tag TAG] --glob PATTERN [--glob ...] [--repo-root DIR] [--marker-regex REGEX] [--min-line-chars N] [--json] [--dryrun|-n] [--help]` 1. **Repo root** defaults to `git rev-parse --show-toplevel` of the cwd (never the script's own location). 2. **--glob is required** (repeatable, relative to repo root, `**` supported). No project-specific defaults. 3. **Marker**: default regex matches `Revokes prior text (): ""` on one line, quote extends to the LAST `"` on the line (quotes may contain `"`). `--tag` optional: without it, match any tag AND the untagged form `Revokes prior text: ""`. `--marker-regex` overrides with a regex that has exactly one capture group (validate this; exit 2 otherwise). Also report any line that contains `Revokes prior text` but does not parse (malformed marker) as a violation. 4. **Check A — verbatim quotes**: every parsed quote, whitespace-collapsed (with the reference's markdown prefix stripping: blockquote `>`, list bullets, table outer pipes), must be a substring of the SAME file at `--base` (same stripping). Quote in a file that did not exist at base → violation. 5. **Check B — completeness**: for each file matched at HEAD that differs from base: every non-blank base line (collapsed) must still be present in the current file or be contained in one of that file's quotes. Lines shorter than `--min-line-chars` (default 12, after collapsing) are ignored, and markdown-only lines (table separators like `|---|---|`, fences ```, horizontal rules) are ignored. 6. **Deleted / renamed files**: a file that matched the globs at base but no longer exists at HEAD is a violation ("deleted since base") unless git reports it renamed (`git diff --find-renames --name-status `), in which case check the renamed file against the old one. 7. Output: human-readable `path:line (base): ` lines grouped under "unquoted deletions", "fabricated quotes", "malformed markers", "deleted files"; `--json` emits the same as JSON. Summary line. 8. Exit codes: 0 clean, 1 violations, 2 usage/environment error (bad --base, not a git repo, bad regex). 9. `--dryrun`: read-only anyway — print the resolved repo root, base sha, file list and marker regex, then run the checks (they have no side effects). State this in the spec. ## Tests (tests/test-spec-revocation-check.sh) Build throwaway git repos under `mktemp -d` (never touch this repo or any real repo; `git -c commit.gpgsign=false`). Cover: clean amendment; unquoted deletion → 1; fabricated quote → 1; quote with bold dropped → 1; quote containing `"` → 0; malformed marker → 1; deleted file → 1; renamed file with quotes → 0; short/markdown-only line removed → 0; untagged marker with no --tag → 0; --tag filters other tags; bad --base → 2; not a git repo → 2; --json parses (python3 -m json.tool); --help → 0. Show each negative test would fail if the check were disabled (say how you proved it in your report). ## Done when `./tests/test-spec-revocation-check.sh` and `./tests/run-all.sh` pass (report any pre-existing run-all failures separately; don't fix unrelated scripts). Report: files, test list, how negatives were proven, open questions.