Some checks failed
Deploy Development / deploy (push) Failing after 42s
Test Suite / pytest-backend (push) Failing after 1s
Test Suite / k6 /api/health Baseline (push) Has been skipped
Test Suite / playwright-smoke (push) Has been skipped
Test Suite / lint-backend (push) Successful in 3s
Test Suite / compose-smoke (push) Has been skipped
Agenten authentifizieren per X-Actor-Token; /api/operational/ Fassade mit context, next-action, status, evidence; Token-Verwaltung unter /api/actors/{id}/service-tokens.
Co-authored-by: Cursor <cursoragent@cursor.com>
452 lines
14 KiB
Python
452 lines
14 KiB
Python
"""Dogfooding R1 — Referenz-Vorhaben „Jinkendo Kairo“ (idempotent, Nicht-Prod).
|
|
|
|
Spiegelt Plan aus docs/product/ gemäß Kairo_Dogfooding_Mirror_Steering_v0.1.md.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any, Optional
|
|
|
|
GITEA_BASE = "http://192.168.2.144:3000/Lars/Kairo-Jinkendo"
|
|
INITIATIVE_TITLE = "Jinkendo Kairo"
|
|
TENANT_SLUG = "stommer"
|
|
|
|
|
|
def _gitea_commit(sha: str, label: str = "") -> str:
|
|
line = f"Commit: [{sha[:7]}]({GITEA_BASE}/commit/{sha})"
|
|
if label:
|
|
return f"{label}\n{line}\nBranch: develop"
|
|
return f"{line}\nBranch: develop"
|
|
|
|
|
|
def _resolve_context() -> tuple[str, str]:
|
|
from db import get_connection
|
|
from psycopg2.extras import RealDictCursor
|
|
|
|
conn = get_connection()
|
|
try:
|
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
cur.execute("SELECT id FROM tenants WHERE slug = %s", (TENANT_SLUG,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise RuntimeError(
|
|
f"Tenant '{TENANT_SLUG}' nicht gefunden — zuerst Portal-Admin/seed_003"
|
|
)
|
|
tenant_id = str(row["id"])
|
|
cur.execute(
|
|
"""
|
|
SELECT a.id
|
|
FROM actors a
|
|
WHERE a.tenant_id = %s AND a.actor_type = 'human' AND a.is_active = TRUE
|
|
ORDER BY a.created_at
|
|
LIMIT 1
|
|
""",
|
|
(tenant_id,),
|
|
)
|
|
actor_row = cur.fetchone()
|
|
if not actor_row:
|
|
raise RuntimeError("Kein Human-Actor im Tenant — seed_003 zuerst")
|
|
return tenant_id, str(actor_row["id"])
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _find_initiative(tenant_id: str) -> Optional[dict[str, Any]]:
|
|
from services.initiatives import list_initiatives
|
|
|
|
for item in list_initiatives(tenant_id=tenant_id):
|
|
if item["title"] == INITIATIVE_TITLE:
|
|
return item
|
|
return None
|
|
|
|
|
|
def _find_by_title(items: list[dict[str, Any]], title: str) -> Optional[dict[str, Any]]:
|
|
for item in items:
|
|
if item["title"] == title:
|
|
return item
|
|
return None
|
|
|
|
|
|
def _close_gate_and_verify(*, tenant_id: str, item_id: str) -> None:
|
|
from services.roadmap import verify_reached
|
|
from services.roadmap_criteria import list_criteria_for_item, waive_criterion
|
|
|
|
for criterion in list_criteria_for_item(tenant_id=tenant_id, item_id=item_id):
|
|
if criterion["status"] not in {"satisfied", "waived", "deferred"}:
|
|
waive_criterion(
|
|
tenant_id=tenant_id,
|
|
criterion_id=criterion["id"],
|
|
decision_title="Dogfooding R1 — historisches Gate",
|
|
decision_description="Rückwirkende Abnahme per Seed.",
|
|
)
|
|
verify_reached(tenant_id=tenant_id, item_id=item_id)
|
|
|
|
|
|
def _ensure_gate_chain(
|
|
*,
|
|
tenant_id: str,
|
|
initiative_id: str,
|
|
gates: list[tuple[str, str, str]],
|
|
) -> dict[str, str]:
|
|
"""gates: (title, status, goal) -> id map."""
|
|
from services.roadmap import add_dependency, create_roadmap_item, list_roadmap_items_for_initiative
|
|
|
|
existing = list_roadmap_items_for_initiative(tenant_id=tenant_id, initiative_id=initiative_id)
|
|
ids: dict[str, str] = {}
|
|
ordered: list[str] = []
|
|
|
|
for index, (title, status, goal) in enumerate(gates):
|
|
found = _find_by_title(existing, title)
|
|
if found:
|
|
gate_id = found["id"]
|
|
else:
|
|
created = create_roadmap_item(
|
|
tenant_id=tenant_id,
|
|
initiative_id=initiative_id,
|
|
title=title,
|
|
item_type="milestone",
|
|
goal_description=goal,
|
|
status="planned" if status == "reached" else status,
|
|
sort_order=index * 10,
|
|
initial_criterion_title=f"{title} — Abnahme",
|
|
)
|
|
gate_id = created["id"]
|
|
existing.append(created)
|
|
|
|
ids[title] = gate_id
|
|
ordered.append(gate_id)
|
|
|
|
if status == "reached":
|
|
_close_gate_and_verify(tenant_id=tenant_id, item_id=gate_id)
|
|
|
|
for i in range(1, len(ordered)):
|
|
try:
|
|
add_dependency(
|
|
tenant_id=tenant_id,
|
|
from_item_id=ordered[i],
|
|
to_item_id=ordered[i - 1],
|
|
dependency_type="requires",
|
|
)
|
|
except ValueError as exc:
|
|
if "existiert bereits" not in str(exc):
|
|
raise
|
|
|
|
return ids
|
|
|
|
|
|
def _ensure_projects(
|
|
*, tenant_id: str, initiative_id: str, titles: list[str]
|
|
) -> dict[str, str]:
|
|
from services.projects import create_project, list_projects_for_initiative
|
|
|
|
existing = list_projects_for_initiative(tenant_id=tenant_id, initiative_id=initiative_id)
|
|
ids: dict[str, str] = {}
|
|
for index, title in enumerate(titles):
|
|
found = _find_by_title(existing, title)
|
|
if found:
|
|
ids[title] = found["id"]
|
|
continue
|
|
created = create_project(
|
|
tenant_id=tenant_id,
|
|
initiative_id=initiative_id,
|
|
title=title,
|
|
description=f"Ist-Struktur — {title}",
|
|
sort_order=index,
|
|
)
|
|
ids[title] = created["id"]
|
|
existing.append(created)
|
|
return ids
|
|
|
|
|
|
def _ensure_action(
|
|
*,
|
|
tenant_id: str,
|
|
initiative_id: str,
|
|
title: str,
|
|
status: str,
|
|
roadmap_item_id: Optional[str] = None,
|
|
project_id: Optional[str] = None,
|
|
description: str = "",
|
|
) -> dict[str, Any]:
|
|
from services.actions import create_action, list_actions_for_initiative
|
|
|
|
existing = list_actions_for_initiative(tenant_id=tenant_id, initiative_id=initiative_id)
|
|
found = _find_by_title(existing, title)
|
|
if found:
|
|
if found.get("status") != status:
|
|
from services.actions import update_action
|
|
|
|
updated = update_action(
|
|
tenant_id=tenant_id,
|
|
action_id=found["id"],
|
|
status=status,
|
|
)
|
|
if updated:
|
|
found = updated
|
|
return found
|
|
return create_action(
|
|
tenant_id=tenant_id,
|
|
initiative_id=initiative_id,
|
|
title=title,
|
|
description=description,
|
|
status=status,
|
|
roadmap_item_id=roadmap_item_id,
|
|
project_id=project_id,
|
|
)
|
|
|
|
|
|
def _ensure_evidence(
|
|
*,
|
|
tenant_id: str,
|
|
initiative_id: str,
|
|
title: str,
|
|
description: str,
|
|
roadmap_item_id: Optional[str] = None,
|
|
action_id: Optional[str] = None,
|
|
actor_id: Optional[str] = None,
|
|
) -> None:
|
|
from services.evidence import create_evidence, list_evidence_for_initiative
|
|
|
|
for item in list_evidence_for_initiative(tenant_id=tenant_id, initiative_id=initiative_id):
|
|
if item["title"] == title:
|
|
return
|
|
create_evidence(
|
|
tenant_id=tenant_id,
|
|
initiative_id=initiative_id,
|
|
title=title,
|
|
description=description,
|
|
status="accepted",
|
|
roadmap_item_id=roadmap_item_id,
|
|
action_id=action_id,
|
|
submitted_by_actor_id=actor_id,
|
|
)
|
|
|
|
|
|
def _ensure_decision(
|
|
*,
|
|
tenant_id: str,
|
|
initiative_id: str,
|
|
title: str,
|
|
outcome: str,
|
|
) -> None:
|
|
from db import get_connection
|
|
from psycopg2.extras import RealDictCursor
|
|
from services.decisions import create_decision
|
|
|
|
conn = get_connection()
|
|
try:
|
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
cur.execute(
|
|
"""
|
|
SELECT 1 FROM decisions
|
|
WHERE tenant_id = %s AND initiative_id = %s AND title = %s
|
|
""",
|
|
(tenant_id, initiative_id, title),
|
|
)
|
|
if cur.fetchone():
|
|
return
|
|
finally:
|
|
conn.close()
|
|
|
|
create_decision(
|
|
tenant_id=tenant_id,
|
|
initiative_id=initiative_id,
|
|
title=title,
|
|
description=outcome,
|
|
status="decided",
|
|
outcome=outcome,
|
|
)
|
|
|
|
|
|
def _ensure_portfolio_first(*, tenant_id: str, initiative_id: str) -> None:
|
|
from services.initiatives import list_initiatives, reorder_portfolio
|
|
|
|
all_items = list_initiatives(tenant_id=tenant_id)
|
|
ids = [initiative_id] + [i["id"] for i in all_items if i["id"] != initiative_id]
|
|
reorder_portfolio(tenant_id=tenant_id, initiative_ids=ids)
|
|
|
|
|
|
def run() -> None:
|
|
if os.getenv("ENVIRONMENT", "development").strip().lower() == "production":
|
|
print("[dogfooding] Übersprungen in Production")
|
|
return
|
|
|
|
tenant_id, owner_actor_id = _resolve_context()
|
|
|
|
initiative = _find_initiative(tenant_id)
|
|
if not initiative:
|
|
from services.initiatives import create_initiative
|
|
|
|
initiative = create_initiative(
|
|
tenant_id=tenant_id,
|
|
title=INITIATIVE_TITLE,
|
|
owner_actor_id=owner_actor_id,
|
|
archetype_key="initiative.product",
|
|
method_profile_key="product.kairo_dev",
|
|
goal=(
|
|
"Operativer Program Director — MVP v0.3 Stufe A (B2b). "
|
|
f"Repository: {GITEA_BASE} (Branch develop)"
|
|
),
|
|
vision="Kairo steuert Kairo — Dogfooding Referenz-Vorhaben.",
|
|
target_state_summary="Leitfrage in ≤2 Min; Gates + Ist gespiegelt aus Git/Docs.",
|
|
priority="high",
|
|
)
|
|
print(f"[dogfooding] Initiative angelegt: {initiative['id']}")
|
|
else:
|
|
print(f"[dogfooding] Initiative vorhanden: {initiative['id']}")
|
|
|
|
initiative_id = initiative["id"]
|
|
|
|
gate_defs = [
|
|
("G1 — Foundation AP0", "reached", "Tenant, Actor, Auth, OM, Migrationen"),
|
|
("G2 — IA & Steering Skeleton", "reached", "AP1.2c, AP1.0, Snapshot, Signals"),
|
|
("G3 — Gates & Checkliste", "reached", "AP1.4/4b/4c, Verify, Kriterien"),
|
|
("G4 — Ist-Hierarchie & Plan/Ist", "reached", "AP1.5/5c, AP1.6, AP1.14, Journey"),
|
|
("G5 — PM Work Modes & Plan-Outline", "active", "AP1.9a/9c ✓; AP1.12 ◐; AP1.16 ◐"),
|
|
("G6 — Archetyp-Steuerung MVP", "active", "AP2.0 ◐; AP2.0d ✓; Execution-Graph ✓"),
|
|
("G7 — MVP-Abnahfe B2b", "planned", "Validation, Leitfrage, Cockpit-Signale"),
|
|
("G8 — Agent & Integration", "planned", "AP1.7, Gitea-Webhook, MCP"),
|
|
]
|
|
gate_ids = _ensure_gate_chain(
|
|
tenant_id=tenant_id,
|
|
initiative_id=initiative_id,
|
|
gates=gate_defs,
|
|
)
|
|
|
|
project_ids = _ensure_projects(
|
|
tenant_id=tenant_id,
|
|
initiative_id=initiative_id,
|
|
titles=["Backend", "Frontend", "Docs & ADPs", "Deploy / CI"],
|
|
)
|
|
|
|
g5 = gate_ids["G5 — PM Work Modes & Plan-Outline"]
|
|
g6 = gate_ids["G6 — Archetyp-Steuerung MVP"]
|
|
g4 = gate_ids["G4 — Ist-Hierarchie & Plan/Ist"]
|
|
|
|
done_actions = [
|
|
(
|
|
"AP1.6b — Journey Gate-Fortschritt",
|
|
"done",
|
|
g4,
|
|
project_ids["Frontend"],
|
|
"b85a6ec",
|
|
),
|
|
(
|
|
"AP1.8a — Portfolio-Rang Cockpit",
|
|
"done",
|
|
g5,
|
|
project_ids["Frontend"],
|
|
"a19c6ab",
|
|
),
|
|
(
|
|
"AP1.14 — Plan/Ist-Snapshots",
|
|
"done",
|
|
g4,
|
|
project_ids["Backend"],
|
|
"064779e",
|
|
),
|
|
(
|
|
"AP2.0 — Archetyp-Slice",
|
|
"done",
|
|
g6,
|
|
project_ids["Backend"],
|
|
"3bd9832",
|
|
),
|
|
(
|
|
"AP1.16 — Execution-Graph & Durchführungsplan",
|
|
"done",
|
|
g6,
|
|
project_ids["Backend"],
|
|
"b9bfa9e",
|
|
),
|
|
(
|
|
"AP1.9c — Cockpit-Signale auf Portfolio-Kacheln",
|
|
"done",
|
|
g5,
|
|
project_ids["Frontend"],
|
|
"b9bfa9e",
|
|
),
|
|
(
|
|
"AP2.0d — Next-Action-Strategien pro Archetyp",
|
|
"done",
|
|
g6,
|
|
project_ids["Backend"],
|
|
"284b9d5",
|
|
),
|
|
|
|
(
|
|
"DOC-Sync — Status-Review & Roadmap",
|
|
"done",
|
|
g6,
|
|
project_ids["Docs & ADPs"],
|
|
None,
|
|
),
|
|
]
|
|
|
|
for title, status, gate_id, project_id, sha in done_actions:
|
|
action = _ensure_action(
|
|
tenant_id=tenant_id,
|
|
initiative_id=initiative_id,
|
|
title=title,
|
|
status=status,
|
|
roadmap_item_id=gate_id,
|
|
project_id=project_id,
|
|
)
|
|
if sha:
|
|
_ensure_evidence(
|
|
tenant_id=tenant_id,
|
|
initiative_id=initiative_id,
|
|
title=f"Merge {title}",
|
|
description=_gitea_commit(sha, title),
|
|
roadmap_item_id=gate_id,
|
|
action_id=action["id"],
|
|
actor_id=owner_actor_id,
|
|
)
|
|
|
|
open_actions = [
|
|
("Dogfooding R2 — Ist laufend pflegen", g6, project_ids["Docs & ADPs"]),
|
|
("AP1.7 — Operational Actor API", g6, project_ids["Backend"]),
|
|
("AP2.0f — work_cycle / Sprint-Zeitbox", g6, project_ids["Backend"]),
|
|
]
|
|
for title, gate_id, project_id in open_actions:
|
|
_ensure_action(
|
|
tenant_id=tenant_id,
|
|
initiative_id=initiative_id,
|
|
title=title,
|
|
status="open",
|
|
roadmap_item_id=gate_id,
|
|
project_id=project_id,
|
|
)
|
|
|
|
_ensure_decision(
|
|
tenant_id=tenant_id,
|
|
initiative_id=initiative_id,
|
|
title="Join/Branch Graph — deferred",
|
|
outcome="OR-Alternativpfade und volle Join-Logik erst bei Use Case (AP1.15d). "
|
|
"Siehe ADP_Roadmap_Graph_and_Gate_Checklist_v0.1.md",
|
|
)
|
|
_ensure_decision(
|
|
tenant_id=tenant_id,
|
|
initiative_id=initiative_id,
|
|
title="MCP nach Operational API",
|
|
outcome="MCP-Adapter erst nach AP1.7 (/api/operational). Schicht 4 eingefroren bis B2b-Validation.",
|
|
)
|
|
_ensure_decision(
|
|
tenant_id=tenant_id,
|
|
initiative_id=initiative_id,
|
|
title="MVP v0.3 PO-Freigabe",
|
|
outcome="Abnahfe Stufe A: A1, A2, B2b + B3 minimal. Dokument: Kairo_MVP_Definition_v0.3.md",
|
|
)
|
|
|
|
_ensure_portfolio_first(tenant_id=tenant_id, initiative_id=initiative_id)
|
|
|
|
from services.actors import create_actor, list_actors
|
|
|
|
agents = list_actors(tenant_id=tenant_id, actor_type="agent", q="Cursor")
|
|
if not any(a["name"] == "Cursor Agent" for a in agents):
|
|
create_actor(tenant_id=tenant_id, actor_type="agent", name="Cursor Agent")
|
|
print("[dogfooding] Agent „Cursor Agent“ angelegt — Service Token manuell erstellen")
|
|
|
|
print("[dogfooding] R2 sync — Ist-Stand gespiegelt (AP1.9c, AP1.16, AP2.0d done)")
|