day 1: Kin from zero to production - agents, GUI, autopilot, 352 tests

This commit is contained in:
Gros Frumos 2026-03-15 23:22:49 +02:00
parent 8d9facda4f
commit 8a6f280cbd
22 changed files with 1907 additions and 103 deletions

View file

@ -41,6 +41,7 @@ CREATE TABLE IF NOT EXISTS tasks (
security_result JSON,
forgejo_issue_id INTEGER,
execution_mode TEXT,
blocked_reason TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
@ -211,6 +212,9 @@ def _migrate(conn: sqlite3.Connection):
if "execution_mode" not in task_cols:
conn.execute("ALTER TABLE tasks ADD COLUMN execution_mode TEXT")
conn.commit()
if "blocked_reason" not in task_cols:
conn.execute("ALTER TABLE tasks ADD COLUMN blocked_reason TEXT")
conn.commit()
def init_db(db_path: Path = DB_PATH) -> sqlite3.Connection:

View file

@ -146,6 +146,17 @@ def _get_hook(conn: sqlite3.Connection, hook_id: int) -> dict:
return dict(row) if row else {}
def _substitute_vars(command: str, task_id: str | None, conn: sqlite3.Connection) -> str:
"""Substitute {task_id} and {title} in hook command."""
if task_id is None or "{task_id}" not in command and "{title}" not in command:
return command
row = conn.execute("SELECT title FROM tasks WHERE id = ?", (task_id,)).fetchone()
title = row["title"] if row else ""
# Sanitize title for shell safety (strip quotes and newlines)
safe_title = title.replace('"', "'").replace("\n", " ").replace("\r", "")
return command.replace("{task_id}", task_id).replace("{title}", safe_title)
def _execute_hook(
conn: sqlite3.Connection,
hook: dict,
@ -159,9 +170,11 @@ def _execute_hook(
exit_code = -1
success = False
command = _substitute_vars(hook["command"], task_id, conn)
try:
proc = subprocess.run(
hook["command"],
command,
shell=True,
cwd=hook.get("working_dir") or None,
capture_output=True,

View file

@ -9,6 +9,12 @@ from datetime import datetime
from typing import Any
VALID_TASK_STATUSES = [
"pending", "in_progress", "review", "done",
"blocked", "decomposed", "cancelled",
]
def _row_to_dict(row: sqlite3.Row | None) -> dict | None:
"""Convert sqlite3.Row to dict with JSON fields decoded."""
if row is None:
@ -249,6 +255,19 @@ def get_decisions(
return _rows_to_list(conn.execute(query, params).fetchall())
def get_decision(conn: sqlite3.Connection, decision_id: int) -> dict | None:
"""Get a single decision by id."""
row = conn.execute("SELECT * FROM decisions WHERE id = ?", (decision_id,)).fetchone()
return _row_to_dict(row) if row else None
def delete_decision(conn: sqlite3.Connection, decision_id: int) -> bool:
"""Delete a decision by id. Returns True if deleted, False if not found."""
cur = conn.execute("DELETE FROM decisions WHERE id = ?", (decision_id,))
conn.commit()
return cur.rowcount > 0
# ---------------------------------------------------------------------------
# Modules
# ---------------------------------------------------------------------------