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
3 changes: 2 additions & 1 deletion docs/handoff.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
5 changes: 3 additions & 2 deletions docs/verify-gate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 2 additions & 3 deletions tools/handoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand Down
27 changes: 27 additions & 0 deletions tools/test_handoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 11 additions & 2 deletions tools/verify_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Loading