kin: auto-commit after pipeline

This commit is contained in:
Gros Frumos 2026-03-17 14:03:53 +02:00
parent 04cbbc563b
commit b6f40a6ace
9 changed files with 1690 additions and 16 deletions

View file

@ -65,9 +65,11 @@ def build_context(
specs = _load_specialists()
ctx["available_specialists"] = list(specs.get("specialists", {}).keys())
ctx["routes"] = specs.get("routes", {})
ctx["departments"] = specs.get("departments", {})
except Exception:
ctx["available_specialists"] = []
ctx["routes"] = {}
ctx["departments"] = {}
elif role == "architect":
ctx["modules"] = models.get_modules(conn, project_id)
@ -111,6 +113,41 @@ def build_context(
conn, project_id, category="security",
)
elif role.endswith("_head"):
# Department head: load department config and previous handoff
ctx["decisions"] = models.get_decisions(conn, project_id)
ctx["modules"] = models.get_modules(conn, project_id)
try:
specs = _load_specialists()
all_specs = specs.get("specialists", {})
departments = specs.get("departments", {})
spec = all_specs.get(role, {})
dept_name = spec.get("department", "")
dept_info = departments.get(dept_name, {})
ctx["department"] = dept_name
ctx["department_workers"] = dept_info.get("workers", [])
ctx["department_description"] = dept_info.get("description", "")
except Exception:
ctx["department"] = ""
ctx["department_workers"] = []
ctx["department_description"] = ""
# Previous handoff from another department (if any)
try:
dept = ctx.get("department")
last_handoff = models.get_last_handoff(conn, task_id, to_department=dept)
# Fallback: get latest handoff NOT from our own department
# (avoids picking up our own outgoing handoff)
if not last_handoff and dept:
all_handoffs = models.get_handoffs_for_task(conn, task_id)
for h in reversed(all_handoffs):
if h.get("from_department") != dept:
last_handoff = h
break
if last_handoff:
ctx["incoming_handoff"] = last_handoff
except Exception:
pass
else:
# Unknown role — give decisions as fallback
ctx["decisions"] = models.get_decisions(conn, project_id, limit=20)
@ -175,6 +212,13 @@ def format_prompt(context: dict, role: str, prompt_template: str | None = None)
prompt_path = PROMPTS_DIR / f"{role}.md"
if prompt_path.exists():
prompt_template = prompt_path.read_text()
elif role.endswith("_head"):
# Fallback: all department heads share the base department_head.md prompt
dept_head_path = PROMPTS_DIR / "department_head.md"
if dept_head_path.exists():
prompt_template = dept_head_path.read_text()
else:
prompt_template = f"You are a {role}. Complete the task described below."
else:
prompt_template = f"You are a {role}. Complete the task described below."
@ -265,6 +309,22 @@ def format_prompt(context: dict, role: str, prompt_template: str | None = None)
sections.append(f"- {name}: {steps}")
sections.append("")
# Department context (department heads)
dept = context.get("department")
if dept:
dept_desc = context.get("department_description", "")
sections.append(f"## Department: {dept}" + (f"{dept_desc}" if dept_desc else ""))
sections.append("")
dept_workers = context.get("department_workers")
if dept_workers:
sections.append(f"## Department workers: {', '.join(dept_workers)}")
sections.append("")
incoming_handoff = context.get("incoming_handoff")
if incoming_handoff:
sections.append("## Incoming handoff from previous department:")
sections.append(json.dumps(incoming_handoff, ensure_ascii=False))
sections.append("")
# Module hint (debugger)
hint = context.get("module_hint")
if hint:

View file

@ -140,10 +140,29 @@ CREATE TABLE IF NOT EXISTS pipelines (
total_cost_usd REAL,
total_tokens INTEGER,
total_duration_seconds INTEGER,
parent_pipeline_id INTEGER REFERENCES pipelines(id),
department TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
completed_at DATETIME
);
-- Межотдельные handoff-ы (KIN-098)
CREATE TABLE IF NOT EXISTS department_handoffs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pipeline_id INTEGER NOT NULL REFERENCES pipelines(id),
task_id TEXT NOT NULL REFERENCES tasks(id),
from_department TEXT NOT NULL,
to_department TEXT,
artifacts JSON,
decisions_made JSON,
blockers JSON,
status TEXT DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_handoffs_pipeline ON department_handoffs(pipeline_id);
CREATE INDEX IF NOT EXISTS idx_handoffs_task ON department_handoffs(task_id);
-- Post-pipeline хуки
CREATE TABLE IF NOT EXISTS hooks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -591,6 +610,35 @@ def _migrate(conn: sqlite3.Connection):
""")
conn.commit()
# Migrate pipelines: add parent_pipeline_id and department columns (KIN-098)
pipeline_cols = {r[1] for r in conn.execute("PRAGMA table_info(pipelines)").fetchall()}
if "parent_pipeline_id" not in pipeline_cols:
conn.execute("ALTER TABLE pipelines ADD COLUMN parent_pipeline_id INTEGER REFERENCES pipelines(id)")
conn.commit()
if "department" not in pipeline_cols:
conn.execute("ALTER TABLE pipelines ADD COLUMN department TEXT")
conn.commit()
# Create department_handoffs table (KIN-098)
if "department_handoffs" not in existing_tables:
conn.executescript("""
CREATE TABLE IF NOT EXISTS department_handoffs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pipeline_id INTEGER NOT NULL REFERENCES pipelines(id),
task_id TEXT NOT NULL REFERENCES tasks(id),
from_department TEXT NOT NULL,
to_department TEXT,
artifacts JSON,
decisions_made JSON,
blockers JSON,
status TEXT DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_handoffs_pipeline ON department_handoffs(pipeline_id);
CREATE INDEX IF NOT EXISTS idx_handoffs_task ON department_handoffs(task_id);
""")
conn.commit()
# Rename legacy 'auto' → 'auto_complete' (KIN-063)
conn.execute(
"UPDATE projects SET execution_mode = 'auto_complete' WHERE execution_mode = 'auto'"

View file

@ -473,12 +473,14 @@ def create_pipeline(
project_id: str,
route_type: str,
steps: list | dict,
parent_pipeline_id: int | None = None,
department: str | None = None,
) -> dict:
"""Create a new pipeline run."""
cur = conn.execute(
"""INSERT INTO pipelines (task_id, project_id, route_type, steps)
VALUES (?, ?, ?, ?)""",
(task_id, project_id, route_type, _json_encode(steps)),
"""INSERT INTO pipelines (task_id, project_id, route_type, steps, parent_pipeline_id, department)
VALUES (?, ?, ?, ?, ?, ?)""",
(task_id, project_id, route_type, _json_encode(steps), parent_pipeline_id, department),
)
conn.commit()
row = conn.execute(
@ -923,6 +925,68 @@ def delete_attachment(conn: sqlite3.Connection, attachment_id: int) -> bool:
return cur.rowcount > 0
# ---------------------------------------------------------------------------
# Department Handoffs (KIN-098)
# ---------------------------------------------------------------------------
def create_handoff(
conn: sqlite3.Connection,
pipeline_id: int,
task_id: str,
from_department: str,
to_department: str | None = None,
artifacts: dict | None = None,
decisions_made: list | None = None,
blockers: list | None = None,
status: str = "pending",
) -> dict:
"""Record a department handoff with artifacts for inter-department context."""
cur = conn.execute(
"""INSERT INTO department_handoffs
(pipeline_id, task_id, from_department, to_department, artifacts, decisions_made, blockers, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(pipeline_id, task_id, from_department, to_department,
_json_encode(artifacts), _json_encode(decisions_made), _json_encode(blockers), status),
)
conn.commit()
row = conn.execute(
"SELECT * FROM department_handoffs WHERE id = ?", (cur.lastrowid,)
).fetchone()
return _row_to_dict(row)
def get_handoffs_for_task(conn: sqlite3.Connection, task_id: str) -> list[dict]:
"""Get all handoffs for a task ordered by creation time."""
rows = conn.execute(
"SELECT * FROM department_handoffs WHERE task_id = ? ORDER BY created_at",
(task_id,),
).fetchall()
return _rows_to_list(rows)
def get_last_handoff(
conn: sqlite3.Connection,
task_id: str,
to_department: str | None = None,
) -> dict | None:
"""Get the most recent handoff for a task, optionally filtered by destination department."""
if to_department:
row = conn.execute(
"""SELECT * FROM department_handoffs
WHERE task_id = ? AND to_department = ?
ORDER BY created_at DESC LIMIT 1""",
(task_id, to_department),
).fetchone()
else:
row = conn.execute(
"""SELECT * FROM department_handoffs
WHERE task_id = ?
ORDER BY created_at DESC LIMIT 1""",
(task_id,),
).fetchone()
return _row_to_dict(row)
def get_chat_messages(
conn: sqlite3.Connection,
project_id: str,