kin: KIN-048 Post-pipeline hook: автокоммит после успешного завершения задачи. git add -A && git commit -m 'kin: TASK_ID TITLE'. Срабатывает автоматически как rebuild-frontend.

This commit is contained in:
Gros Frumos 2026-03-16 06:59:46 +02:00
parent 8a6f280cbd
commit ae21e48b65
13 changed files with 1554 additions and 65 deletions

View file

@ -110,6 +110,7 @@ def _slim_project(project: dict) -> dict:
"path": project["path"],
"tech_stack": project.get("tech_stack"),
"language": project.get("language", "ru"),
"execution_mode": project.get("execution_mode"),
}

View file

@ -216,12 +216,61 @@ def _migrate(conn: sqlite3.Connection):
conn.execute("ALTER TABLE tasks ADD COLUMN blocked_reason TEXT")
conn.commit()
if "autocommit_enabled" not in proj_cols:
conn.execute("ALTER TABLE projects ADD COLUMN autocommit_enabled INTEGER DEFAULT 0")
conn.commit()
# Rename legacy 'auto' → 'auto_complete' (KIN-063)
conn.execute(
"UPDATE projects SET execution_mode = 'auto_complete' WHERE execution_mode = 'auto'"
)
conn.execute(
"UPDATE tasks SET execution_mode = 'auto_complete' WHERE execution_mode = 'auto'"
)
conn.commit()
def _seed_default_hooks(conn: sqlite3.Connection):
"""Seed default hooks for the kin project (idempotent).
Creates rebuild-frontend hook only when:
- project 'kin' exists in the projects table
- the hook doesn't already exist (no duplicate)
"""
kin_exists = conn.execute(
"SELECT 1 FROM projects WHERE id = 'kin'"
).fetchone()
if not kin_exists:
return
exists = conn.execute(
"SELECT 1 FROM hooks"
" WHERE project_id = 'kin'"
" AND name = 'rebuild-frontend'"
" AND event = 'pipeline_completed'"
).fetchone()
if not exists:
conn.execute(
"""INSERT INTO hooks (project_id, name, event, command, enabled)
VALUES ('kin', 'rebuild-frontend', 'pipeline_completed',
'cd /Users/grosfrumos/projects/kin/web/frontend && npm run build',
1)"""
)
conn.commit()
# Enable autocommit for kin project (opt-in, idempotent)
conn.execute(
"UPDATE projects SET autocommit_enabled=1 WHERE id='kin' AND autocommit_enabled=0"
)
conn.commit()
def init_db(db_path: Path = DB_PATH) -> sqlite3.Connection:
conn = get_connection(db_path)
conn.executescript(SCHEMA)
conn.commit()
_migrate(conn)
_seed_default_hooks(conn)
return conn

View file

@ -14,6 +14,15 @@ VALID_TASK_STATUSES = [
"blocked", "decomposed", "cancelled",
]
VALID_COMPLETION_MODES = {"auto_complete", "review"}
def validate_completion_mode(value: str) -> str:
"""Validate completion mode from LLM output. Falls back to 'review' if invalid."""
if value in VALID_COMPLETION_MODES:
return value
return "review"
def _row_to_dict(row: sqlite3.Row | None) -> dict | None:
"""Convert sqlite3.Row to dict with JSON fields decoded."""
@ -220,6 +229,32 @@ def add_decision(
return _row_to_dict(row)
def add_decision_if_new(
conn: sqlite3.Connection,
project_id: str,
type: str,
title: str,
description: str,
category: str | None = None,
tags: list | None = None,
task_id: str | None = None,
) -> dict | None:
"""Add a decision only if no existing one matches (project_id, type, normalized title).
Returns the new decision dict, or None if skipped as duplicate.
"""
existing = conn.execute(
"""SELECT id FROM decisions
WHERE project_id = ? AND type = ?
AND lower(trim(title)) = lower(trim(?))""",
(project_id, type, title),
).fetchone()
if existing:
return None
return add_decision(conn, project_id, type, title, description,
category=category, tags=tags, task_id=task_id)
def get_decisions(
conn: sqlite3.Connection,
project_id: str,