Orchestrata Ultimate › Gates › unshipped-guard
Gate: unshipped-guard
At a glance
| Purpose | "Refuse to finish a turn with committed work left unpushed" (module docstring). Mohamed, twice, four hours apart: "when code is tested and verified, push, deploy and wire needs to be automatic." |
| Where it runs | Stop hook in ~/.claude/settings.json: /home/mohamed/.claude/.venv/bin/python /home/mohamed/.claude/hooks/unshipped_guard.py. Source: claude/hooks/unshipped_guard.py (plugin copy plugins/orchestrata/hooks/unshipped_guard.py is byte-identical; the deployed copy is identical after line 1, the shebang the config installer rewrites). |
| Trigger | Every turn end of every Claude session on this host. |
| Exact command | None to run by hand; it reads the Stop payload on stdin. Its remedy is cd <repo> && ship. |
| Blocks | Prints {"decision": "block", "reason": "Verified work is committed but not shipped: …"}. Fail-open on any internal error; never blocks twice on one turn (stop_hook_active). |
| Ruling / ADR | No RUL page names it. Its scoping follows RUL-035 ("a user-level hook applies to the work at hand"), per commit 32b8eb6e8. |
What it checks
Read with the AST toolkit (main, unshipped, moved_since, session_started,
ship_in_flight):
- Watched repos only:
~/workspace/orchestrata-ultimate-architecture,~/workspace/knowledge-wiki,~/workspace/dr-hannah(WATCHED). Awt-*worktree is not in the list, so an unpushed commit on a feature worktree (for example8a2cb42adonfeat/maps-via-codetoolsinwt-MAP-PRECOMMIT) does not trip it. - Local-only commits: the repo has an upstream, HEAD is on a branch (not
detached), and
git rev-list --count @{u}..HEADis above 0. A dirty tree alone never fires ("still working"). - This session's doing (since
32b8eb6e8, 2026-10-22): the session start is the firsttimestampin the first 200 transcript lines. The repo counts only when the branch reflog's newest entry, or the committer date of an unpushed commit, is at or after that second. A fast-forward over old commits therefore counts. When the transcript is missing or unreadable it checks every watched repo: "a false alarm is visible and arguable, a false silence is the failure this hook exists to prevent." - A ship already running:
ship_in_flightscans/procfor a process whose cwd is inside the repo and whose argv contains, as a whole element, the resolvedshipon PATH or the repo'sdeploy/ship.sh. Process-based on purpose: "a lockfile outlives a SIGKILL and would then suppress the guard forever" (added124741ead, 2026-08-14, after it blocked three turns during a 1,627-module ship).
What it does not do (docstring): it does not push ("a hook that fires a ten-minute suite as a side effect of stopping would be its own defect"), and it ignores a repo with no upstream or a detached HEAD.
History
980530457,ac7b6c3d4,124741ead,debf80f0b(2026-08-14) — the guard, ship on PATH, the in-flight check matching the executable, and recognising both ship copies.32b8eb6e8(2026-10-22) — scoped to branches the session moved. On 2026-10-21 dr-hannah's day-old unpushed commits blocked every turn of an unrelated$HOMEsession. Tests:tests/claude/test_unshipped_guard_session_scope.py(15 tests, real git repos and transcripts), with the two older modules still passing (40/40) and 95% hook line coverage (commit message).
Known defect (open)
IMPROVEMENT-LOG D22 (2026-10-22): the guard "blocked the turn end twice
('1 commit(s) on master exist only locally ... ship') while a ship of that same
commit was already running in the background, past its push step", after the
ADR-0124 and RUL-041 wiki ships. D22's diagnosis says the guard "cannot see a ship
in flight", but ship_in_flight has existed since 124741ead. Why it did not match
that background ship (its cwd, or an argv that named a wrapper instead of ship)
was not measured. D22's proposed fold-back: treat a live ship for that repo as in
progress.
Typical failures and how to fix them
| Symptom | Cause | Fix |
|---|---|---|
Turn end blocked: <repo>: N commit(s) on <branch> exist only locally |
This session committed in a watched repo and did not push | Run cd <repo> && ship (it gates, pushes, deploys, verifies). If the gate fails, fix it; the message says "a failing gate is a reason to work" |
| Blocked by another session's commits | The transcript start could not be read, so all watched repos were checked | Check the payload's transcript_path; the fallback is deliberate |
| Blocked while a ship runs | D22 above | Let the ship finish; the next Stop re-evaluates |