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):

  1. Watched repos only: ~/workspace/orchestrata-ultimate-architecture, ~/workspace/knowledge-wiki, ~/workspace/dr-hannah (WATCHED). A wt-* worktree is not in the list, so an unpushed commit on a feature worktree (for example 8a2cb42ad on feat/maps-via-codetools in wt-MAP-PRECOMMIT) does not trip it.
  2. Local-only commits: the repo has an upstream, HEAD is on a branch (not detached), and git rev-list --count @{u}..HEAD is above 0. A dirty tree alone never fires ("still working").
  3. This session's doing (since 32b8eb6e8, 2026-10-22): the session start is the first timestamp in 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."
  4. A ship already running: ship_in_flight scans /proc for a process whose cwd is inside the repo and whose argv contains, as a whole element, the resolved ship on PATH or the repo's deploy/ship.sh. Process-based on purpose: "a lockfile outlives a SIGKILL and would then suppress the guard forever" (added 124741ead, 2026-08-14, after it blocked three turns during a 1,627-module ship).

Diagram projects-orchestrata-ultimate-gates-unshipped-guard-1

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

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