atlas-iac/dockerfiles/patch_hermes_decomposition_safety.py

263 lines
7.2 KiB
Python
Raw Normal View History

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