#!/usr/bin/env python3 """Bounded Kanban access and workspace resolution for Hermes CLI workers.""" from __future__ import annotations import json import sqlite3 import sys import time from pathlib import Path from typing import Any, Callable from cli_lane_config import ( BOARD_CORRUPTION_ERRORS, EXTERNAL_PREFIX, KANBAN_STORAGE_ATTEMPTS, WORKTREE_LOCK, ) def _task_value(task: Any, name: str, default: Any = None) -> Any: return getattr(task, name, default) def _task_context(kanban_db: Any, conn: Any, task_id: str) -> str: value = kanban_db.build_worker_context(conn, task_id) if isinstance(value, str): return value return json.dumps(value, indent=2, default=str) def _resolve_workspace(kanban_db: Any, conn: Any, task: Any, board: str) -> Path: # External coding lanes always receive their own linked worktree. A task # that does not resolve to a Git repository is blocked rather than sharing # a mutable checkout with another unattended worker. # Git worktree creation updates common-repository metadata. Serialize that # short materialization step while allowing the provider workers themselves # to run concurrently in independent worktrees. with WORKTREE_LOCK: value, branch_name = kanban_db._resolve_worktree_workspace(task, board=board) kanban_db.set_branch_name(conn, str(_task_value(task, "id")), branch_name) return Path(value).resolve() def _record_board_access_error(board: str, error: Exception) -> None: """Report one board access failure once while allowing other lanes to run.""" failure_kind = "storage" if isinstance(error, (OSError, sqlite3.Error)) else "access" detail = f"{failure_kind} {type(error).__name__}: {error}" if BOARD_CORRUPTION_ERRORS.get(board) == detail: return print( f"temporarily skipping Kanban board {board!r}: {detail}", file=sys.stderr, flush=True, ) BOARD_CORRUPTION_ERRORS[board] = detail def _board_call( kanban_db: Any, board: str, operation: Callable[[Any], Any], ) -> Any: """Run one bounded Kanban operation on a fresh, promptly closed connection.""" last_storage_error: Exception | None = None for attempt in range(KANBAN_STORAGE_ATTEMPTS): conn = None try: with kanban_db.scoped_current_board(board): conn = kanban_db.connect(board=board) result = operation(conn) BOARD_CORRUPTION_ERRORS.pop(board, None) return result except (OSError, sqlite3.Error) as error: last_storage_error = error _record_board_access_error(board, error) if attempt + 1 < KANBAN_STORAGE_ATTEMPTS: time.sleep(0.2 * (attempt + 1)) finally: if conn is not None: conn.close() assert last_storage_error is not None raise last_storage_error def _external(task: Any) -> bool: return str(_task_value(task, "assignee", "") or "").startswith(EXTERNAL_PREFIX)