"""Patch transactional automatic-decomposition execution-history guards.""" import os from pathlib import Path from hermes_execution_patch_support import replace_once source_root = Path(os.environ.get("HERMES_SOURCE_ROOT", "/opt/hermes")) db_path = source_root / "hermes_cli/kanban_db.py" db = db_path.read_text(encoding="utf-8") specify_signature_before = '''def specify_triage_task( conn: sqlite3.Connection, task_id: str, *, title: Optional[str] = None, body: Optional[str] = None, assignee: Optional[str] = None, author: Optional[str] = None, ) -> bool: ''' specify_signature_after = '''def specify_triage_task( conn: sqlite3.Connection, task_id: str, *, title: Optional[str] = None, body: Optional[str] = None, assignee: Optional[str] = None, author: Optional[str] = None, require_no_runs: bool = False, ) -> bool: ''' db = replace_once( db, specify_signature_before, specify_signature_after, "transactional specify signature", ) specify_guard_before = ''' if existing is None: return False sets: list[str] = ["status = 'todo'"] ''' specify_guard_after = ''' if existing is None: return False if require_no_runs and conn.execute( "SELECT 1 FROM task_runs WHERE task_id = ? LIMIT 1", (task_id,), ).fetchone() is not None: return False sets: list[str] = ["status = 'todo'"] ''' db = replace_once( db, specify_guard_before, specify_guard_after, "transactional specify execution-history guard", ) decompose_signature_before = '''def decompose_triage_task( conn: sqlite3.Connection, task_id: str, *, root_assignee: Optional[str], children: list[dict], author: Optional[str] = None, auto_promote: bool = True, ) -> Optional[list[str]]: ''' decompose_signature_after = '''def decompose_triage_task( conn: sqlite3.Connection, task_id: str, *, root_assignee: Optional[str], children: list[dict], author: Optional[str] = None, auto_promote: bool = True, require_no_runs: bool = False, ) -> Optional[list[str]]: ''' db = replace_once( db, decompose_signature_before, decompose_signature_after, "transactional decomposition signature", ) decompose_guard_before = ''' if root_row["status"] != "triage": return None tenant = root_row["tenant"] ''' decompose_guard_after = ''' if root_row["status"] != "triage": return None if require_no_runs and conn.execute( "SELECT 1 FROM task_runs WHERE task_id = ? LIMIT 1", (task_id,), ).fetchone() is not None: return None tenant = root_row["tenant"] ''' db = replace_once( db, decompose_guard_before, decompose_guard_after, "transactional decomposition execution-history guard", ) db_path.write_text(db, encoding="utf-8") decompose_path = source_root / "hermes_cli/kanban_decompose.py" decompose = decompose_path.read_text(encoding="utf-8") helper_anchor = ''' if chosen not in valid_names: return default_assignee return chosen ''' helper_replacement = helper_anchor + '''def _has_execution_history(task_id: str) -> bool: """Return whether a task has ever entered a worker run.""" with kb.connect_closing() as conn: return bool(kb.list_runs(conn, task_id)) ''' decompose = replace_once( decompose, helper_anchor, helper_replacement, "auto-decompose execution-history helper", ) signature_before = '''def decompose_task( task_id: str, *, author: Optional[str] = None, timeout: Optional[int] = None, ) -> DecomposeOutcome: ''' signature_after = '''def decompose_task( task_id: str, *, author: Optional[str] = None, timeout: Optional[int] = None, automatic: bool = False, ) -> DecomposeOutcome: ''' decompose = replace_once( decompose, signature_before, signature_after, "auto-decompose function signature", ) status_before = ''' if task.status != "triage": return DecomposeOutcome( task_id, False, f"task is not in triage (status={task.status!r})" ) cfg = _load_config() ''' status_after = ''' if task.status != "triage": return DecomposeOutcome( task_id, False, f"task is not in triage (status={task.status!r})" ) if automatic and _has_execution_history(task_id): return DecomposeOutcome( task_id, False, "task has execution history and requires deliberate manual triage", ) cfg = _load_config() ''' decompose = replace_once( decompose, status_before, status_after, "auto-decompose preflight guard", ) single_before = ''' author=audit_author, ) if not ok: ''' single_after = ''' author=audit_author, require_no_runs=automatic, ) if not ok: if automatic and _has_execution_history(task_id): return DecomposeOutcome( task_id, False, "task gained execution history and requires deliberate manual triage", ) ''' decompose = replace_once( decompose, single_before, single_after, "auto-decompose single-task commit guard", ) fanout_before = ''' author=audit_author, auto_promote=auto_promote, ) ''' fanout_after = ''' author=audit_author, auto_promote=auto_promote, require_no_runs=automatic, ) ''' decompose = replace_once( decompose, fanout_before, fanout_after, "auto-decompose transactional fanout guard", ) fanout_outcome_before = ''' if child_ids is None: return DecomposeOutcome( task_id, False, "task moved out of triage before decomposition", ) ''' fanout_outcome_after = ''' if child_ids is None: if automatic and _has_execution_history(task_id): return DecomposeOutcome( task_id, False, "task gained execution history and requires deliberate manual triage", ) return DecomposeOutcome( task_id, False, "task moved out of triage before decomposition", ) ''' decompose = replace_once( decompose, fanout_outcome_before, fanout_outcome_after, "auto-decompose fanout rejection reason", ) decompose_path.write_text(decompose, encoding="utf-8") watcher_path = source_root / "gateway/kanban_watchers.py" watcher = watcher_path.read_text(encoding="utf-8") call_before = ''' outcome = _decomp.decompose_task( tid, author="auto-decomposer", ) ''' call_after = ''' outcome = _decomp.decompose_task( tid, author="auto-decomposer", automatic=True, ) ''' watcher = replace_once( watcher, call_before, call_after, "gateway automatic-decomposition call", ) watcher_path.write_text(watcher, encoding="utf-8")