diff --git a/docs/handoff.md b/docs/handoff.md index 9e01633..671a81f 100644 --- a/docs/handoff.md +++ b/docs/handoff.md @@ -14,7 +14,8 @@ records: - the commit and `git status --short` at that moment. On resuming, anything that differs from the handoff is treated as unverified, and the gate runs -again before anything builds on an earlier PASS. Add `.stackwich/` to `.gitignore`. +again before anything builds on an earlier PASS. The tools keep `.stackwich/` out of git with a +`.stackwich/.gitignore` of `*`; a handoff written by hand needs `.stackwich/` in `.gitignore`. ## The helper: `tools/handoff.py` diff --git a/docs/verify-gate.md b/docs/verify-gate.md index 102b9d4..29f5896 100644 --- a/docs/verify-gate.md +++ b/docs/verify-gate.md @@ -24,8 +24,9 @@ python3 ~/stackwich-tools/tools/verify_gate.py check prints `UNVERIFIED` or `STALE` with each added, modified, or removed file, and exits 1. - `reset --by NAME --reason TEXT` records that a person reviewed a change that ran out of cycles. -The change id defaults to the current branch; pass `--change ID` to name it yourself. Add -`.stackwich/` to `.gitignore`. The tool leaves it out of the fingerprint either way. +The change id defaults to the current branch; pass `--change ID` to name it yourself. The tool +writes `.stackwich/.gitignore` (`*`) when it creates the directory, so its records never get +committed, and leaves `.stackwich/` out of the fingerprint either way. ## Example diff --git a/tools/handoff.py b/tools/handoff.py index e7fa87f..ef43ab3 100644 --- a/tools/handoff.py +++ b/tools/handoff.py @@ -17,7 +17,7 @@ import sys from pathlib import Path -from verify_gate import STATE_DIR, GateError, check, default_change, diff, git, head, manifest, now, records, repo_root, tree_hash +from verify_gate import STATE_DIR, GateError, check, default_change, diff, git, head, manifest, now, records, repo_root, state_dir, tree_hash HANDOFF = "handoff.md" FILES = "handoff-files.json" @@ -57,8 +57,7 @@ def save(root: Path, change: str, stage: str, next_step: str, plan: str | None, ] if note: lines += ["", "Note:", "", note.strip()] - state = root / STATE_DIR - state.mkdir(parents=True, exist_ok=True) + state = state_dir(root) (state / HANDOFF).write_text("\n".join(lines) + "\n", encoding="utf-8") (state / FILES).write_text(json.dumps({"change": change, "head": head(root), "tree": tree_hash(files), "files": files, "artifacts": artifacts}, indent=1) + "\n", diff --git a/tools/test_handoff.py b/tools/test_handoff.py index 93040a4..5c266cd 100644 --- a/tools/test_handoff.py +++ b/tools/test_handoff.py @@ -96,6 +96,33 @@ def test_hand_written_handoff(self): self.assertIn("Stage: reviewing", out.stdout) self.assertIn("UNCHECKED", out.stdout) + def untracked(self): + out = subprocess.run(["git", "-C", str(self.repo), "status", "--porcelain", "--untracked-files=all"], + check=True, capture_output=True, text=True) + return out.stdout + + def test_state_directory_ignores_itself(self): + """Neither tool should leave .stackwich/ for a later `git add -A` to commit.""" + self.run_tool(GATE, "run", "--", "python3 -c 'pass'") + self.assertTrue((self.repo / ".stackwich" / "verify.jsonl").is_file()) + self.assertNotIn(".stackwich", self.untracked()) + self.save() + self.assertTrue((self.repo / ".stackwich" / "handoff.md").is_file()) + self.assertNotIn(".stackwich", self.untracked()) + self.assertEqual((self.repo / ".gitignore").read_text(), "build/\n") + + def test_handoff_alone_ignores_its_state(self): + self.save() + self.assertNotIn(".stackwich", self.untracked()) + + def test_existing_state_gitignore_is_kept(self): + (self.repo / ".stackwich").mkdir() + (self.repo / ".stackwich" / ".gitignore").write_text("verify.jsonl\n") + self.save() + self.run_tool(GATE, "run", "--", "python3 -c 'pass'") + self.assertEqual((self.repo / ".stackwich" / ".gitignore").read_text(), "verify.jsonl\n") + self.assertIn(".stackwich/handoff.md", self.untracked()) + def test_bad_input(self): self.assertEqual(self.run_tool(HANDOFF, "save", "--stage", "executing", "--next", " ").returncode, 2) self.assertEqual(self.run_tool(HANDOFF, "save", "--stage", "nowhere", "--next", "x").returncode, 2) diff --git a/tools/verify_gate.py b/tools/verify_gate.py index 2e682e7..b61aaa3 100644 --- a/tools/verify_gate.py +++ b/tools/verify_gate.py @@ -125,9 +125,18 @@ def records(root: Path) -> list[dict]: return out +def state_dir(root: Path) -> Path: + """Create .stackwich/ ignoring itself, so `git add -A` never commits it. An existing .gitignore there is kept.""" + state = root / STATE_DIR + state.mkdir(parents=True, exist_ok=True) + ignore = state / ".gitignore" + if not ignore.exists(): + ignore.write_text("*\n", encoding="utf-8") + return state + + def append(root: Path, record: dict) -> None: - path = root / STATE_DIR / RECORDS - path.parent.mkdir(parents=True, exist_ok=True) + path = state_dir(root) / RECORDS with path.open("a", encoding="utf-8") as handle: handle.write(json.dumps(record, sort_keys=True, separators=(",", ":")) + "\n")