3333
3434import argparse
3535import json
36+ import subprocess
3637import sys
3738from pathlib import Path
3839from typing import Any , Callable , Optional
4748 FakeClock ,
4849)
4950from prd_taskmaster .tournament .adjudicate import adjudicate_job , settle_job
51+ from prd_taskmaster .tournament import watcher as _watcher
5052from prd_taskmaster .reputation import record_tournament , summarize_reputation , _winner_id
5153
5254
55+ def _resolve_repo_root (cwd : "str | None" = None , default : str = "." ) -> str :
56+ """Resolve the git repo root for the watcher's re-execution worktrees.
57+
58+ Falls back to *default* (never raises) when not in a git repo, so the
59+ watcher gate degrades gracefully rather than crashing the settle path.
60+ """
61+ try :
62+ proc = subprocess .run (
63+ ["git" , "rev-parse" , "--show-toplevel" ],
64+ cwd = cwd , capture_output = True , text = True , timeout = 10 ,
65+ )
66+ if proc .returncode == 0 and proc .stdout .strip ():
67+ return proc .stdout .strip ()
68+ except Exception : # noqa: BLE001
69+ pass
70+ return default
71+
72+
5373# ─── Core orchestration function ─────────────────────────────────────────────
5474
5575
@@ -74,6 +94,10 @@ def run_tournament(
7494 now : str ,
7595 window_s : float = 120.0 ,
7696 enforce_slash : bool = False ,
97+ repo_root : str = "." ,
98+ watcher_ledger_path : "str | Path | None" = None ,
99+ _re_adjudicate : "Optional[Callable[..., dict]]" = None ,
100+ _permit : "Optional[Callable[..., dict]]" = None ,
77101 _spawn_fn : "Callable[[Any], dict]" = default_launcher_adapter ,
78102 _inbox_read : "Callable[..., list[dict]]" = default_inbox_adapter ,
79103 _dispatch_reveal : "Callable[..., Optional[dict]]" = default_reveal_adapter ,
@@ -290,8 +314,38 @@ def run_tournament(
290314 job_poster = job_poster ,
291315 )
292316
317+ # ── Step 5b: Watcher gate on REAL slashing (engine-enforced) ─────
318+ # Shadow-slash only until the independent out-of-band watcher PERMITS
319+ # real forfeiture for THIS job. Fail-closed: a watcher error, a failed
320+ # re-adjudication, or a non-permit downgrades enforce_slash to shadow
321+ # so no AtlasCoin is ever burned without an independent confirmation.
322+ effective_enforce = enforce_slash
323+ if enforce_slash :
324+ _radj = _re_adjudicate or _watcher .re_adjudicate_job
325+ _perm = _permit or _watcher .permit_enforce_slash
326+ w_ledger = (
327+ Path (watcher_ledger_path ) if watcher_ledger_path
328+ else Path (".atlas-ai/tournament/watcher.jsonl" )
329+ )
330+ try :
331+ wrec = _radj (
332+ job_dir = job_dir , repo_root = repo_root , card_path = card_path ,
333+ held_root = held_root , task_id = task_id , start_commit = base_ref ,
334+ base_ref = base_ref , now = now , ledger_path = w_ledger ,
335+ )
336+ if not wrec .get ("ok" ):
337+ permit = {"permitted" : False , "reason" : "watcher re-adjudication failed" }
338+ else :
339+ permit = _perm (wrec , ledger_path = w_ledger , current_job_id = job_id )
340+ except Exception as exc : # noqa: BLE001 — fail-closed → shadow
341+ permit = {"permitted" : False , "reason" : f"watcher error: { exc } " }
342+ summary ["watcher_permit" ] = permit
343+ if not permit .get ("permitted" ):
344+ effective_enforce = False
345+ summary ["enforce_slash_downgraded" ] = True
346+
293347 # ── Step 6: Settle (FAIL-CLOSED on ok:false) ─────────────────────
294- settle_env = _settle_fn (job_dir = job_dir , enforce_slash = enforce_slash )
348+ settle_env = _settle_fn (job_dir = job_dir , enforce_slash = effective_enforce )
295349
296350 settled_ok = settle_env .get ("ok" ) is True
297351 summary ["settled_ok" ] = settled_ok
@@ -415,6 +469,7 @@ def cmd_tournament_run(args: argparse.Namespace) -> None:
415469 now = now ,
416470 window_s = float (getattr (args , "window" , 120.0 )),
417471 enforce_slash = bool (getattr (args , "enforce_slash" , False )),
472+ repo_root = _resolve_repo_root (),
418473 # Real adapters must be wired by the orchestrator skill; the defaults
419474 # raise RuntimeError with guidance if called directly.
420475 _spawn_fn = default_launcher_adapter ,
@@ -426,6 +481,60 @@ def cmd_tournament_run(args: argparse.Namespace) -> None:
426481 _emit ({"ok" : False , "error" : str (exc )})
427482
428483
484+ def cmd_watcher_run (args : argparse .Namespace ) -> None :
485+ """CLI handler for ``watcher-run``.
486+
487+ Re-adjudicates a settled job out-of-band from primary evidence, appends a
488+ concordance row to the watcher ledger, and reports the fail-closed
489+ real-slash permit for the job. FAIL-CLOSED: a missing/unreadable job → ok:false.
490+ """
491+ job_dir = Path (args .job )
492+ repo_root = getattr (args , "repo_root" , None ) or "."
493+ card_path = Path (args .card )
494+ held_root = Path (getattr (args , "held_root" , None ) or ".atlas-ai/cdd" )
495+ ledger_path = Path (getattr (args , "ledger_path" , None ) or ".atlas-ai/tournament/watcher.jsonl" )
496+
497+ import datetime
498+ now = datetime .datetime .now (datetime .timezone .utc ).isoformat ()
499+
500+ try :
501+ rec = _watcher .re_adjudicate_job (
502+ job_dir = job_dir ,
503+ repo_root = repo_root ,
504+ card_path = card_path ,
505+ held_root = held_root ,
506+ task_id = args .task ,
507+ start_commit = args .base_ref ,
508+ base_ref = args .base_ref ,
509+ now = now ,
510+ ledger_path = ledger_path ,
511+ )
512+ except Exception as exc : # noqa: BLE001
513+ _emit ({"ok" : False , "error" : str (exc )})
514+ return
515+
516+ if not rec .get ("ok" ):
517+ _emit ({"ok" : False , ** rec })
518+ return
519+
520+ # Exclude the in-flight job (just appended to the ledger) from its own gate.
521+ permit = _watcher .permit_enforce_slash (
522+ rec , ledger_path = ledger_path , current_job_id = rec .get ("job_id" )
523+ )
524+ _emit ({"ok" : True , ** rec , "permit" : permit })
525+
526+
527+ def cmd_watcher_status (args : argparse .Namespace ) -> None :
528+ """CLI handler for ``watcher-status``.
529+
530+ Reports the watcher's historical concordance and whether the track record
531+ would clear the real-slash gate (``real_slash_ready``). A view command.
532+ """
533+ ledger_path = Path (getattr (args , "ledger_path" , None ) or ".atlas-ai/tournament/watcher.jsonl" )
534+ summ = _watcher .concordance_summary (ledger_path )
535+ _emit ({"ok" : True , ** summ })
536+
537+
429538def cmd_tournament_status (args : argparse .Namespace ) -> None :
430539 """CLI handler for ``tournament-status``.
431540
0 commit comments