44Emits SHIP_CHECK_OK to stdout ONLY when all gates pass. Called by execute-task
55at termination AND by Step 9 (--dry-run) as a per-task predicate.
66
7+ Standalone: this file ships into user projects as `.atlas-ai/ship-check.py`. It
8+ imports ONLY the stdlib and MUST stay that way (no `import prd_taskmaster` — that
9+ package is not importable in a user project). The oracle gate therefore shells
10+ the `atlas oracle grade` CLI directly via subprocess.
11+
712Gate logic (grounded against actual pipeline.json / tasks.json schemas
813observed 2026-06-04 in ai-human-tasker):
914
2732 skel checked .atlas-ai/ralph-loop-prompt.md — wrong path and irrelevant
2833 after /goal migration.
2934
30- Gate 5 (HARD) — No non-zero "Exit status N" line in any .atlas-ai/evidence/
31- file. This is the convergent must-do from the 2026-06-04 forensic audit
32- (T12 marked DONE while pnpm test exited 1 with 11 failing tests).
33- Override only via SHIP_CHECK_OVERRIDE_ADMIN token; overrides are logged
34- to .atlas-ai/state/execute-log.jsonl as an audit record.
35+ Gate 5 (HARD, ORACLE) — Each DONE task is RE-GRADED by the atlas oracle.
36+ The submitter's own .atlas-ai/evidence/ is NOT trusted; the oracle
37+ re-executes the CDD card's grading against HEAD and writes its own
38+ evidence/ledger. This replaces the fakable "no non-zero Exit status N in
39+ evidence" grep — which silently PASSED when no evidence existed and had a
40+ self-grantable bypass token. Both the silent-pass loophole and the bypass
41+ token are GONE. The gate is FAIL-CLOSED: a missing card, a card with no
42+ grading block, a CLI crash, unparseable output, or any verdict other than
43+ "PASS" all BLOCK the ship.
3544
3645Interface:
3746 python3 .atlas-ai/ship-check.py # standard gate
3847 python3 .atlas-ai/ship-check.py --dry-run # always exit 0; report on stderr
39- python3 .atlas-ai/ship-check.py --override SHIP_CHECK_OVERRIDE_ADMIN # bypass Gate 5
4048 python3 .atlas-ai/ship-check.py --cwd /path/to/project # explicit project root
4149
4250Exit codes:
43- 0 — SHIP_CHECK_OK (stdout: exactly "SHIP_CHECK_OK\n ", with " [OVERRIDE]" suffix if applicable )
51+ 0 — SHIP_CHECK_OK (stdout: exactly "SHIP_CHECK_OK\n ")
4452 1 — gate failures (stderr only; nothing on stdout — log watchers must not see partial matches)
45- 2 — script error (IO, JSON parse, bad token )
53+ 2 — script error (IO, JSON parse)
4654"""
4755from __future__ import annotations
4856
4957import argparse
5058import json
51- import re
59+ import os
60+ import shlex
61+ import subprocess
5262import sys
53- from datetime import datetime , timezone
5463from pathlib import Path
5564from typing import List , Tuple
5665
57- OVERRIDE_TOKEN = "SHIP_CHECK_OVERRIDE_ADMIN"
58- EXIT_STATUS_RE = re .compile (r"\bExit status\s+(\d+)\b" , re .IGNORECASE )
59-
6066
6167def gate_pipeline (atlas : Path ) -> Tuple [bool , List [str ]]:
6268 pf = atlas / "state" / "pipeline.json"
@@ -138,46 +144,101 @@ def gate_plan(repo_root: Path) -> Tuple[bool, List[str]]:
138144 return False , ["no plan file at .taskmaster/docs/plan.md or docs/superpowers/plans/*.md" ]
139145
140146
141- def gate_exit_codes (atlas : Path ) -> Tuple [bool , List [str ]]:
147+ def _oracle_cmd () -> List [str ]:
148+ """Configurable oracle CLI invocation. Default: the 'atlas' binary on PATH.
149+
150+ Set ATLAS_ORACLE_CMD (shell-split) to change it, e.g.:
151+ ATLAS_ORACLE_CMD="node /path/to/atlas-protocol/apps/cli/dist/index.js"
152+ """
153+ raw = os .environ .get ("ATLAS_ORACLE_CMD" )
154+ if raw :
155+ return shlex .split (raw )
156+ return ["atlas" ]
157+
158+
159+ def _head_commit (repo_root : Path ) -> str :
160+ """HEAD sha via `git rev-parse HEAD`. FAIL-CLOSED: any failure returns the
161+ sentinel "UNKNOWN" so the oracle mismatches the working tree and blocks."""
162+ try :
163+ proc = subprocess .run (
164+ ["git" , "-C" , str (repo_root ), "rev-parse" , "HEAD" ],
165+ capture_output = True , text = True , timeout = 30 ,
166+ )
167+ except (OSError , subprocess .SubprocessError ):
168+ return "UNKNOWN"
169+ if proc .returncode != 0 :
170+ return "UNKNOWN"
171+ sha = (proc .stdout or "" ).strip ()
172+ return sha or "UNKNOWN"
173+
174+
175+ def gate_oracle (repo_root : Path , tasks : list , head_commit : str ) -> Tuple [bool , List [str ]]:
176+ """Re-grade every DONE task via the atlas oracle CLI. FAIL-CLOSED.
177+
178+ For each done task: the CDD card must exist and carry a 'grading' block,
179+ and the oracle must return verdict=="PASS". Anything else — missing card,
180+ no grading block, CLI crash, unparseable output, a non-PASS verdict —
181+ appends a failure. The submitter's evidence is NOT read here; the oracle
182+ re-executes the grading and writes its own evidence/ledger.
183+ """
142184 failures : List [str ] = []
143- evidence_dir = atlas / "evidence"
144- if not evidence_dir .exists ():
145- # Gate 3 (CDD) catches missing evidence; this gate is silent when no evidence exists
146- return True , []
147- for f in evidence_dir .rglob ("*" ):
148- if not f .is_file ():
185+ atlas = repo_root / ".atlas-ai"
186+ held = atlas / "held-out"
187+ evidence = atlas / "evidence"
188+ ledger = atlas / "ledger"
189+ cmd_base = _oracle_cmd ()
190+
191+ for t in tasks :
192+ if t .get ("status" ) != "done" :
193+ continue
194+ tid = t .get ("id" )
195+ card_path = atlas / "cdd" / f"task-{ tid } .json"
196+ if not card_path .exists ():
197+ failures .append (f"task { tid } : no CDD card to grade" )
149198 continue
150199 try :
151- text = f .read_text (errors = "ignore" )
152- except OSError :
200+ card = json .loads (card_path .read_text ())
201+ except (OSError , json .JSONDecodeError ) as exc :
202+ failures .append (f"task { tid } : cannot read CDD card ({ exc } )" )
203+ continue
204+ if "grading" not in card :
205+ failures .append (f"task { tid } : CDD card has no grading block" )
206+ continue
207+
208+ cmd = cmd_base + [
209+ "oracle" , "grade" ,
210+ "--repo" , str (repo_root ),
211+ "--commit" , head_commit ,
212+ "--card" , str (card_path ),
213+ "--held" , str (held ),
214+ "--evidence" , str (evidence ),
215+ "--ledger" , str (ledger ),
216+ ]
217+ try :
218+ proc = subprocess .run (cmd , capture_output = True , text = True , timeout = 600 )
219+ except (OSError , subprocess .SubprocessError ) as exc :
220+ failures .append (f"task { tid } : oracle CLI invocation failed ({ exc } )" )
221+ continue
222+
223+ try :
224+ parsed = json .loads (proc .stdout )
225+ except (json .JSONDecodeError , TypeError ):
226+ failures .append (f"task { tid } : oracle produced no parseable JSON verdict (rc={ proc .returncode } )" )
153227 continue
154- for match in EXIT_STATUS_RE .finditer (text ):
155- try :
156- code = int (match .group (1 ))
157- except (ValueError , IndexError ):
158- continue
159- if code != 0 :
160- rel = f .relative_to (atlas .parent ) if atlas .parent in f .parents else f
161- failures .append (f"non-zero exit in { rel } : Exit status { code } " )
162- break # one report per file is enough
163- return len (failures ) == 0 , failures
164228
229+ verdict = parsed .get ("verdict" ) if isinstance (parsed , dict ) else None
230+ if verdict not in ("PASS" , "FAIL" ):
231+ failures .append (f"task { tid } : oracle verdict missing/invalid ({ verdict !r} )" )
232+ continue
233+ if verdict == "FAIL" :
234+ failures .append (f"task { tid } : oracle verdict FAIL" )
235+ continue
236+ # verdict == "PASS" — the only path that does NOT append a failure.
165237
166- def log_override (atlas : Path , message : str ) -> None :
167- log = atlas / "state" / "execute-log.jsonl"
168- log .parent .mkdir (parents = True , exist_ok = True )
169- entry = {
170- "iteration" : "OVERRIDE" ,
171- "timestamp" : datetime .now (timezone .utc ).isoformat (),
172- "task_id" : "SHIP_CHECK" ,
173- "event" : "override_invoked" ,
174- "message" : message ,
175- }
176- with log .open ("a" ) as fp :
177- fp .write (json .dumps (entry ) + "\n " )
238+ return len (failures ) == 0 , failures
178239
179240
180- def run_all_gates (repo_root : Path , override_active : bool ) -> Tuple [bool , List [str ]]:
241+ def run_all_gates (repo_root : Path ) -> Tuple [bool , List [str ]]:
181242 failures : List [str ] = []
182243 atlas = repo_root / ".atlas-ai"
183244
@@ -191,38 +252,28 @@ def run_all_gates(repo_root: Path, override_active: bool) -> Tuple[bool, List[st
191252 _ , f3 = gate_cdd (atlas , tasks )
192253 failures .extend (f3 )
193254
255+ head = _head_commit (repo_root )
256+ _ , f5 = gate_oracle (repo_root , tasks , head )
257+ failures .extend (f5 )
258+
194259 _ , f4 = gate_plan (repo_root )
195260 failures .extend (f4 )
196261
197- ok5 , f5 = gate_exit_codes (atlas )
198- if not ok5 :
199- if override_active :
200- log_override (atlas , f"Gate 5 bypassed: { '; ' .join (f5 [:3 ])} { ' ...' if len (f5 ) > 3 else '' } " )
201- # Override accepts the failures; no append to global failures list
202- else :
203- failures .extend (f5 )
204-
205262 return len (failures ) == 0 , failures
206263
207264
208265def main () -> int :
209266 parser = argparse .ArgumentParser (description = "Deterministic ship-check for prd-taskmaster pipelines." )
210267 parser .add_argument ("--dry-run" , action = "store_true" ,
211268 help = "Run all gates but always exit 0. Report goes to stderr. Used by execute-task Step 9 as a per-task predicate." )
212- parser .add_argument ("--override" , type = str , default = None ,
213- help = f"Bypass Gate 5 (exit codes) if value equals { OVERRIDE_TOKEN } . Logged to execute-log.jsonl." )
214269 parser .add_argument ("--cwd" , type = str , default = None ,
215270 help = "Project root (defaults to current working directory)." )
216271 args = parser .parse_args ()
217272
218273 repo_root = Path (args .cwd ).resolve () if args .cwd else Path .cwd ()
219- override_active = args .override == OVERRIDE_TOKEN
220- if args .override is not None and not override_active :
221- print ("FAIL: --override value does not match expected token" , file = sys .stderr )
222- return 2
223274
224275 try :
225- ok , failures = run_all_gates (repo_root , override_active = override_active )
276+ ok , failures = run_all_gates (repo_root )
226277 except Exception as exc : # noqa: BLE001 — top-level guard
227278 print (f"FAIL: ship-check script error: { exc !r} " , file = sys .stderr )
228279 return 2
@@ -237,8 +288,7 @@ def main() -> int:
237288 return 0
238289
239290 if ok :
240- suffix = " [OVERRIDE]" if override_active else ""
241- print (f"SHIP_CHECK_OK{ suffix } " )
291+ print ("SHIP_CHECK_OK" )
242292 return 0
243293
244294 for f in failures :
0 commit comments