feat(steering): K-Ext-2 Attention-Registry, Gate/Intake-Proposals, generische UI
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 4m13s
Test Suite / lint-backend (push) Successful in 3s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 19s
Test Suite / playwright-smoke (push) Successful in 12s

Methodenuebergreifende Provider auf Kernel v0.3; verbindliche Coding Rules fuer Agenten in ADP und .cursor/rules.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Lars 2026-07-27 11:48:10 +02:00
parent 1bbc5dbfce
commit b747200f3a
29 changed files with 1364 additions and 182 deletions

View File

@ -6,7 +6,7 @@ alwaysApply: true
# Kairo Plugin-Architektur (AP2.3 / AP2.4) # Kairo Plugin-Architektur (AP2.3 / AP2.4)
Verbindliche ADPs: `docs/architecture/ADP_Archetype_Method_Plugin_Architecture_v0.1.md`, `docs/architecture/ADP_AP2_4_Steering_Elements_and_Method_Contract_v0.1.md`, `docs/architecture/ADP_Steering_Kernel_Spine_v0.1.md` Verbindliche ADPs: `docs/architecture/ADP_Archetype_Method_Plugin_Architecture_v0.1.md`, `docs/architecture/ADP_AP2_4_Steering_Elements_and_Method_Contract_v0.1.md`, `docs/architecture/ADP_Steering_Kernel_Spine_v0.1.md`, **`docs/architecture/ADP_Steering_Kernel_Coding_Rules_v0.1.md`**, **`.cursor/rules/kairo-steering-kernel.mdc`**
## Vier Schichten — Auflösung zur Laufzeit ## Vier Schichten — Auflösung zur Laufzeit

View File

@ -0,0 +1,62 @@
---
description: Steering Kernel v0.3 — verbindliche Regeln für Read Models, Proposals, Attention, Ranker (alle Coding-Agenten)
globs: backend/steering/**,backend/data_layer/initiative_snapshot.py,frontend/src/components/SteeringProposalsPanel.jsx,frontend/src/components/SprintCommitProposalPanel.jsx,frontend/src/utils/steeringProposals.js,frontend/src/utils/sprintCommitProposal.js,frontend/src/pages/initiative/InitiativeInboxPage.jsx,frontend/src/pages/initiative/InitiativePlanPage.jsx,frontend/src/pages/modes/PlanSprintPage.jsx
alwaysApply: true
---
# Kairo Steering Kernel — Coding Rules (verbindlich)
**Primärdokument:** `docs/architecture/ADP_Steering_Kernel_Coding_Rules_v0.1.md`
**Architektur:** `docs/architecture/ADP_Steering_Kernel_Extension_Model_v0.1.md`
## Herzmaschine
- Initiative-Steuerung **nur** via `evaluate_steering()` in `backend/steering/kernel/evaluate.py`
- Snapshot spiegelt `evaluation.read_models` + `evaluation.proposals` — **kein** Duplikat-Rechnen
## Neue Fähigkeit = Provider registrieren
| Was | Wo |
|-----|-----|
| Read Model | `steering/read_models/registry.py` |
| Proposal | `steering/proposals/registry.py` |
| Attention | `steering/attention/contributors/*.py` + Registry |
| Sprint-Ranker | `register_sprint_commit_ranker()` — weitere Ranker analog |
Aktivierung über `steering_elements` / `data_slices` im Methoden-Vertrag — **nicht** über Page-Ifs oder `method_key`-Branches im Kernel.
## Proposals
- **Kein Auto-Commit** — Accept in UI/API
- **Keine feste Bug/Story-Policy** — `ranker_key` + `factors[]`; Heuristik = Fallback (`heuristic_v0`)
- Abhängigkeiten: `dependency_refs`, `dependency_blocked`
- KI: `agent_v1` Ranker + Actor-Slot — **keine** hardcodierten Prompts (Principle Gate)
## Frontend
- `SteeringProposalsPanel` + `PROPOSAL_UI_CONFIG` in `utils/steeringProposals.js`
- Daten aus `steeringSnapshot.steering_kernel.proposals`
- Keine neue Proposal-Page pro Methode
## Methoden-Parität (Ist)
| Provider | Element-Gate |
|----------|--------------|
| `sprint_commit` | `work_cycle_scope` |
| `gate_next_actions` | `gate_fulfillment` |
| `intake_triage` | `backlog` slice, **excludes** `work_cycle_scope` |
| `epic_rollup` | `backlog_epic_hierarchy` |
| `planning_debt` / `execution_graph` | `gate_fulfillment` / `critical_path` |
## Verboten
- Steuerungslogik außerhalb `backend/steering/`
- Archetyp-Ifs für Steuerungs-UI
- `evaluate_next_work` für Commit-Vorschläge (Plan ≠ Ist)
- OM-Tabellen für Vorschläge
## Tests
- Unit-Test pro Provider/Ranker
- Snapshot-Integration wenn DB-Test verfügbar
- Truth Table aktualisieren

View File

@ -50,6 +50,7 @@ Lies bei Projektstart in dieser Reihenfolge:
8. `docs/architecture/Kairo_Tenant_Invariants_v0.1.md` 8. `docs/architecture/Kairo_Tenant_Invariants_v0.1.md`
9. `docs/architecture/Kairo_Sprint0_Principle_Gate_v0.1.md` 9. `docs/architecture/Kairo_Sprint0_Principle_Gate_v0.1.md`
10. `.cursor/rules/kairo-architecture.mdc` 10. `.cursor/rules/kairo-architecture.mdc`
11. **`.cursor/rules/kairo-steering-kernel.mdc`** + `docs/architecture/ADP_Steering_Kernel_Coding_Rules_v0.1.md` (Steering Kernel v0.3 — Read Models, Proposals, Ranker)
11. `docs/architecture/ADP_Archetype_Method_Plugin_Architecture_v0.1.md` (AP2.3 — **geliefert**) 11. `docs/architecture/ADP_Archetype_Method_Plugin_Architecture_v0.1.md` (AP2.3 — **geliefert**)
12. `docs/architecture/ADP_AP2_4_Steering_Elements_and_Method_Contract_v0.1.md` (AP2.4 — **geliefert**) 12. `docs/architecture/ADP_AP2_4_Steering_Elements_and_Method_Contract_v0.1.md` (AP2.4 — **geliefert**)
13. `.cursor/rules/kairo-plugin-architecture.mdc` 13. `.cursor/rules/kairo-plugin-architecture.mdc`

View File

@ -362,6 +362,8 @@ def get_initiative_steering_snapshot(
"active_work_cycle": active_work_cycle, "active_work_cycle": active_work_cycle,
"epic_rollup": epic_rollup, "epic_rollup": epic_rollup,
"sprint_commit_proposals": sprint_commit_proposals, "sprint_commit_proposals": sprint_commit_proposals,
"gate_next_actions_proposals": evaluation.proposals.get("gate_next_actions") or [],
"intake_triage_proposals": evaluation.proposals.get("intake_triage") or [],
"counts": { "counts": {
"actions_open": open_actions, "actions_open": open_actions,
"actions_blocked": blocked_actions, "actions_blocked": blocked_actions,

View File

@ -0,0 +1 @@
"""Attention contributors — K-Ext-2."""

View File

@ -0,0 +1,109 @@
"""Attention evaluation context — shared caches for contributors."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from steering.eval_context import SteeringEvalContext
from steering.kernel.models import HorizonMarker, SteeringBinding
from tenant_context import TenantContext
@dataclass
class AttentionEvalContext:
tenant_ctx: TenantContext
initiative_id: str
binding: SteeringBinding
horizon: HorizonMarker
next_work: list[dict[str, Any]]
read_models: dict[str, Any]
proposals: dict[str, Any]
eval_ctx: SteeringEvalContext | None = None
_actions: list[dict[str, Any]] | None = field(default=None, repr=False)
_blockers: list[dict[str, Any]] | None = field(default=None, repr=False)
_roadmap_items: list[dict[str, Any]] | None = field(default=None, repr=False)
_dependencies: list[dict[str, Any]] | None = field(default=None, repr=False)
_graph_state: dict[str, Any] | None = field(default=None, repr=False)
_gate_scope: str | None = field(default=None, repr=False)
@property
def steering_elements(self) -> frozenset[str]:
if self.eval_ctx:
return self.eval_ctx.steering_elements
return frozenset()
@property
def actions(self) -> list[dict[str, Any]]:
if self._actions is None:
if self.eval_ctx:
self._actions = self.eval_ctx.actions
else:
from services import actions as action_service
self._actions = action_service.list_actions_for_initiative(
tenant_id=self.tenant_ctx.tenant_id,
initiative_id=self.initiative_id,
)
return self._actions
@property
def blockers(self) -> list[dict[str, Any]]:
if self._blockers is None:
from services import blockers as blocker_service
self._blockers = blocker_service.list_blockers_for_initiative(
tenant_id=self.tenant_ctx.tenant_id, initiative_id=self.initiative_id
)
return self._blockers
@property
def roadmap_items(self) -> list[dict[str, Any]]:
if self._roadmap_items is None:
from services import roadmap as roadmap_service
self._roadmap_items = roadmap_service.list_roadmap_items_for_initiative(
tenant_id=self.tenant_ctx.tenant_id, initiative_id=self.initiative_id
)
return self._roadmap_items
@property
def gate_scope(self) -> str | None:
if self._gate_scope is None:
from steering.strategies.next_action.execution_ready import (
gate_horizon_scope_for_method,
)
self._gate_scope = gate_horizon_scope_for_method(
self.binding.primary_method_key,
self.tenant_ctx,
self.initiative_id,
)
return self._gate_scope
@property
def dependencies(self) -> list[dict[str, Any]]:
if self._dependencies is None:
from services.execution_plan import list_dependencies_for_initiative
self._dependencies = list_dependencies_for_initiative(
tenant_id=self.tenant_ctx.tenant_id, initiative_id=self.initiative_id
)
return self._dependencies
@property
def graph_state(self) -> dict[str, Any]:
if self._graph_state is None:
cached = self.read_models.get("execution_graph")
if cached is not None:
self._graph_state = cached
else:
from steering.graph.execution_engine import compute_execution_graph_state
self._graph_state = compute_execution_graph_state(
actions=self.actions,
dependencies=self.dependencies,
scope_roadmap_item_id=self.gate_scope,
)
return self._graph_state

View File

@ -0,0 +1 @@
"""Attention contributors package."""

View File

@ -0,0 +1,62 @@
"""Core attention contributors — blockers, blocked actions."""
from __future__ import annotations
from steering.attention.context import AttentionEvalContext
from steering.attention.contributors.registry import (
AttentionContributor,
register_attention_contributor,
)
def _open_blockers(ctx: AttentionEvalContext) -> list[dict]:
items: list[dict] = []
for blocker in ctx.blockers:
if blocker.get("status") not in ("open", "in_progress"):
continue
items.append(
{
"kind": "open_blocker",
"severity": "warning",
"title": blocker.get("title") or "Blocker",
"summary": "Offener Blocker im Vorhaben",
"scope_type": "blocker",
"scope_id": str(blocker["id"]),
"initiative_id": ctx.initiative_id,
"action_id": (
str(blocker["action_id"]) if blocker.get("action_id") else None
),
"blocker_id": str(blocker["id"]),
"reason_code": "path_blocked",
}
)
return items
def _blocked_actions(ctx: AttentionEvalContext) -> list[dict]:
items: list[dict] = []
for action in ctx.actions:
if action.get("status") != "blocked":
continue
items.append(
{
"kind": "blocked_action",
"severity": "critical",
"title": action.get("title") or "Arbeitspaket",
"summary": "Maßnahme ist blockiert",
"scope_type": "action",
"scope_id": str(action["id"]),
"initiative_id": ctx.initiative_id,
"action_id": str(action["id"]),
"reason_code": "path_blocked",
}
)
return items
register_attention_contributor(
AttentionContributor(key="open_blockers", contribute=_open_blockers)
)
register_attention_contributor(
AttentionContributor(key="blocked_actions", contribute=_blocked_actions)
)

View File

@ -0,0 +1,105 @@
"""Gate / execution graph attention — planning_debt, execution_waiting."""
from __future__ import annotations
from steering.attention.context import AttentionEvalContext
from steering.attention.contributors.registry import (
AttentionContributor,
register_attention_contributor,
)
from steering.graph.execution_engine import (
execution_waiting_to_attention_items,
planning_debt_to_attention_items,
)
_GATE_METHODS = frozenset(
{"sequential_dependency", "program_delivery", "maturity_progression"}
)
def _planning_debt(ctx: AttentionEvalContext) -> list[dict]:
if ctx.binding.primary_method_key not in _GATE_METHODS:
return []
debts = ctx.read_models.get("planning_debt")
if debts is None:
from steering.graph.execution_engine import compute_planning_debt
gate_scope = ctx.gate_scope
scoped_roadmap = ctx.roadmap_items
scoped_actions = ctx.actions
if gate_scope:
scoped_roadmap = [
ri for ri in ctx.roadmap_items if str(ri.get("id")) == str(gate_scope)
]
scoped_actions = [
a
for a in ctx.actions
if a.get("roadmap_item_id")
and str(a["roadmap_item_id"]) == str(gate_scope)
]
debts = compute_planning_debt(
actions=scoped_actions, roadmap_items=scoped_roadmap
)
return planning_debt_to_attention_items(
initiative_id=ctx.initiative_id, debts=debts or []
)
def _execution_waiting(ctx: AttentionEvalContext) -> list[dict]:
if "critical_path" not in ctx.steering_elements and "gate_fulfillment" not in ctx.steering_elements:
return []
return execution_waiting_to_attention_items(
initiative_id=ctx.initiative_id,
actions=ctx.actions,
graph_state=ctx.graph_state,
)
def _horizon_no_ready_work(ctx: AttentionEvalContext) -> list[dict]:
if ctx.next_work:
return []
if ctx.binding.primary_method_key not in _GATE_METHODS:
return []
if ctx.horizon.kind != "gate":
return []
debts = ctx.read_models.get("planning_debt") or []
has_planning = len(debts) > 0
has_waiting = bool(ctx.graph_state.get("blocked_actions"))
if not has_planning and not has_waiting and not ctx.graph_state.get("ready_actions"):
return [
{
"kind": "initiative_without_next_action",
"severity": "info",
"title": ctx.horizon.gate_title or "Aktiver Horizont",
"summary": "Keine ausführungsbereite Arbeit im aktiven Horizont",
"scope_type": "milestone",
"scope_id": ctx.horizon.gate_roadmap_item_id,
"initiative_id": ctx.initiative_id,
"reason_code": "horizon_no_ready_work",
}
]
return []
register_attention_contributor(
AttentionContributor(
key="planning_debt",
requires_elements=frozenset({"gate_fulfillment"}),
contribute=_planning_debt,
)
)
register_attention_contributor(
AttentionContributor(
key="execution_waiting",
requires_any_element=frozenset({"critical_path", "gate_fulfillment"}),
contribute=_execution_waiting,
)
)
register_attention_contributor(
AttentionContributor(
key="horizon_no_ready_work",
requires_elements=frozenset({"gate_fulfillment"}),
contribute=_horizon_no_ready_work,
)
)

View File

@ -0,0 +1,80 @@
"""Read-model / proposal derived attention."""
from __future__ import annotations
from steering.attention.context import AttentionEvalContext
from steering.attention.contributors.registry import (
AttentionContributor,
register_attention_contributor,
)
from steering.proposals.gate_next_actions import gate_next_actions_attention_items
from steering.proposals.intake_triage import intake_triage_attention_items
from steering.proposals.sprint_commit import sprint_commit_attention_items
from steering.read_models.epic_rollup import epic_rollup_attention_items
def _epic_rollup(ctx: AttentionEvalContext) -> list[dict]:
rollups = ctx.read_models.get("epic_rollup")
if not rollups:
return []
return epic_rollup_attention_items(
initiative_id=ctx.initiative_id, rollups=rollups
)
def _sprint_commit(ctx: AttentionEvalContext) -> list[dict]:
proposals = ctx.proposals.get("sprint_commit")
if not proposals:
return []
return sprint_commit_attention_items(
initiative_id=ctx.initiative_id,
proposals=proposals,
actions=ctx.actions,
)
def _gate_next_actions(ctx: AttentionEvalContext) -> list[dict]:
proposals = ctx.proposals.get("gate_next_actions")
if not proposals:
return []
return gate_next_actions_attention_items(
initiative_id=ctx.initiative_id, proposals=proposals
)
def _intake_triage(ctx: AttentionEvalContext) -> list[dict]:
proposals = ctx.proposals.get("intake_triage")
if not proposals:
return []
return intake_triage_attention_items(
initiative_id=ctx.initiative_id, proposals=proposals
)
register_attention_contributor(
AttentionContributor(
key="epic_rollup",
requires_elements=frozenset({"backlog_epic_hierarchy"}),
contribute=_epic_rollup,
)
)
register_attention_contributor(
AttentionContributor(
key="sprint_commit",
requires_elements=frozenset({"work_cycle_scope"}),
contribute=_sprint_commit,
)
)
register_attention_contributor(
AttentionContributor(
key="gate_next_actions",
requires_elements=frozenset({"gate_fulfillment"}),
contribute=_gate_next_actions,
)
)
register_attention_contributor(
AttentionContributor(
key="intake_triage",
contribute=_intake_triage,
)
)

View File

@ -0,0 +1,59 @@
"""Attention contributor registry — K-Ext-2."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Callable
from steering.attention.context import AttentionEvalContext
AttentionContribute = Callable[[AttentionEvalContext], list[dict]]
@dataclass(frozen=True)
class AttentionContributor:
key: str
requires_elements: frozenset[str] = field(default_factory=frozenset)
requires_any_element: frozenset[str] = field(default_factory=frozenset)
requires_primary_methods: frozenset[str] = field(default_factory=frozenset)
contribute: AttentionContribute = lambda _ctx: []
_CONTRIBUTORS: list[AttentionContributor] = []
def register_attention_contributor(contributor: AttentionContributor) -> None:
_CONTRIBUTORS.append(contributor)
def collect_attention_items(ctx: AttentionEvalContext) -> list[dict]:
elements = ctx.steering_elements
primary = ctx.binding.primary_method_key
items: list[dict] = []
for contributor in _CONTRIBUTORS:
if (
contributor.requires_elements
and not contributor.requires_elements <= elements
):
continue
if contributor.requires_any_element and not (
contributor.requires_any_element & elements
):
continue
if (
contributor.requires_primary_methods
and primary not in contributor.requires_primary_methods
):
continue
items.extend(contributor.contribute(ctx))
for item in items:
item.setdefault("data_source", "steering_kernel")
items.sort(
key=lambda x: {"critical": 0, "warning": 1, "info": 2}.get(
x.get("severity", "info"), 9
)
)
return items

View File

@ -5,16 +5,12 @@ from __future__ import annotations
from typing import Any from typing import Any
from steering.kernel.models import HorizonMarker, SteeringBinding from steering.kernel.models import HorizonMarker, SteeringBinding
from steering.strategies.next_action.execution_ready import gate_horizon_scope_for_method
from tenant_context import TenantContext from tenant_context import TenantContext
_GATE_METHODS = frozenset( # Side-effect: register contributors
{ import steering.attention.contributors.core # noqa: F401
"sequential_dependency", import steering.attention.contributors.gates # noqa: F401
"program_delivery", import steering.attention.contributors.read_models # noqa: F401
"maturity_progression",
}
)
def evaluate_attention( def evaluate_attention(
@ -29,161 +25,18 @@ def evaluate_attention(
eval_ctx=None, eval_ctx=None,
limit: int = 20, limit: int = 20,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
"""Kernel attention for a single initiative — no fake next when planning debt.""" """Kernel attention — contributors via registry (K-Ext-2)."""
from services import actions as action_service from steering.attention.context import AttentionEvalContext
from services import blockers as blocker_service from steering.attention.contributors.registry import collect_attention_items
from services import roadmap as roadmap_service
from services.execution_plan import list_dependencies_for_initiative
from steering.graph.execution_engine import (
compute_execution_graph_state,
compute_planning_debt,
execution_waiting_to_attention_items,
planning_debt_to_attention_items,
)
items: list[dict[str, Any]] = [] actx = AttentionEvalContext(
tenant_ctx=ctx,
blockers = blocker_service.list_blockers_for_initiative(
tenant_id=ctx.tenant_id, initiative_id=initiative_id
)
for blocker in blockers:
if blocker.get("status") not in ("open", "in_progress"):
continue
items.append(
{
"kind": "open_blocker",
"severity": "warning",
"title": blocker.get("title") or "Blocker",
"summary": "Offener Blocker im Vorhaben",
"scope_type": "blocker",
"scope_id": str(blocker["id"]),
"initiative_id": initiative_id,
"action_id": (
str(blocker["action_id"]) if blocker.get("action_id") else None
),
"blocker_id": str(blocker["id"]),
"reason_code": "path_blocked",
"data_source": "steering_kernel",
}
)
actions = action_service.list_actions_for_initiative(
tenant_id=ctx.tenant_id, initiative_id=initiative_id
)
for action in actions:
if action.get("status") != "blocked":
continue
items.append(
{
"kind": "blocked_action",
"severity": "critical",
"title": action.get("title") or "Arbeitspaket",
"summary": "Maßnahme ist blockiert",
"scope_type": "action",
"scope_id": str(action["id"]),
"initiative_id": initiative_id,
"action_id": str(action["id"]),
"reason_code": "path_blocked",
"data_source": "steering_kernel",
}
)
gate_scope = gate_horizon_scope_for_method(
binding.primary_method_key, ctx, initiative_id
)
roadmap_items = roadmap_service.list_roadmap_items_for_initiative(
tenant_id=ctx.tenant_id, initiative_id=initiative_id
)
scoped_roadmap = roadmap_items
scoped_actions = actions
if gate_scope:
scope = str(gate_scope)
scoped_roadmap = [ri for ri in roadmap_items if str(ri.get("id")) == scope]
scoped_actions = [
a
for a in actions
if a.get("roadmap_item_id") and str(a["roadmap_item_id"]) == scope
]
if binding.primary_method_key in _GATE_METHODS:
debts = compute_planning_debt(
actions=scoped_actions, roadmap_items=scoped_roadmap
)
items.extend(
planning_debt_to_attention_items(
initiative_id=initiative_id, debts=debts
)
)
dependencies = list_dependencies_for_initiative(
tenant_id=ctx.tenant_id, initiative_id=initiative_id
)
graph_state = compute_execution_graph_state(
actions=actions,
dependencies=dependencies,
scope_roadmap_item_id=gate_scope,
)
items.extend(
execution_waiting_to_attention_items(
initiative_id=initiative_id, initiative_id=initiative_id,
actions=actions, binding=binding,
graph_state=graph_state, horizon=horizon,
next_work=next_work,
read_models=read_models or {},
proposals=proposals or {},
eval_ctx=eval_ctx,
) )
) return collect_attention_items(actx)[:limit]
read_models = read_models or {}
proposals = proposals or {}
from steering.read_models.epic_rollup import epic_rollup_attention_items
from steering.proposals.sprint_commit import sprint_commit_attention_items
epic_rollups = read_models.get("epic_rollup")
if epic_rollups:
items.extend(
epic_rollup_attention_items(
initiative_id=initiative_id, rollups=epic_rollups
)
)
sprint_proposals = proposals.get("sprint_commit")
if sprint_proposals:
items.extend(
sprint_commit_attention_items(
initiative_id=initiative_id,
proposals=sprint_proposals,
actions=actions,
)
)
if (
not next_work
and binding.primary_method_key in _GATE_METHODS
and horizon.kind == "gate"
):
has_planning = any(i.get("reason_code") == "planning_debt" for i in items)
has_waiting = any(i.get("reason_code") == "execution_waiting" for i in items)
if not has_planning and not has_waiting and not graph_state.get("ready_actions"):
items.append(
{
"kind": "initiative_without_next_action",
"severity": "info",
"title": horizon.gate_title or "Aktiver Horizont",
"summary": "Keine ausführungsbereite Arbeit im aktiven Horizont",
"scope_type": "milestone",
"scope_id": horizon.gate_roadmap_item_id,
"initiative_id": initiative_id,
"reason_code": "horizon_no_ready_work",
"data_source": "steering_kernel",
}
)
for item in items:
item.setdefault("data_source", "steering_kernel")
items.sort(
key=lambda x: {"critical": 0, "warning": 1, "info": 2}.get(
x.get("severity", "info"), 9
)
)
return items[:limit]

View File

@ -0,0 +1,127 @@
"""Gate next actions proposal — sequential_dependency / program_delivery."""
from __future__ import annotations
from typing import Any
from steering.eval_context import SteeringEvalContext
COMMITTABLE = frozenset({"new", "triaged", "accepted"})
_PRIORITY = {"critical": 0, "high": 1, "normal": 2, "low": 3}
def _active_gate_id(ctx: SteeringEvalContext) -> str | None:
if ctx.horizon.kind == "gate" and ctx.horizon.gate_roadmap_item_id:
return str(ctx.horizon.gate_roadmap_item_id)
debts = []
return None
def _sort_key(item: dict[str, Any]) -> tuple:
return (
_PRIORITY.get(item.get("priority") or "normal", 9),
item.get("sort_order") if item.get("sort_order") is not None else 9999,
(item.get("title") or "").lower(),
)
def propose_gate_next_actions(
ctx: SteeringEvalContext,
*,
read_models: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
read_models = read_models or {}
gate_id = _active_gate_id(ctx)
proposals: list[dict[str, Any]] = []
rank = 0
for debt in read_models.get("planning_debt") or []:
rank += 1
gid = str(debt.get("roadmap_item_id"))
proposals.append(
{
"proposal_key": "gate_next_actions",
"scope_type": "roadmap_item",
"scope_id": gid,
"rank": rank,
"reason_code": "planning_debt",
"summary": debt.get("message") or "Gate ohne Durchführungsplan",
"title": debt.get("title") or "Zielzustand",
"roadmap_item_id": gid,
"initiative_id": ctx.initiative_id,
"ranker_key": "heuristic_v0",
"confidence": "heuristic",
"factors": [{"code": "planning_debt", "weight": 100, "label": "Planning Debt"}],
"dependency_refs": [],
"dependency_blocked": False,
"data_source": "steering_kernel",
}
)
vocabulary = ctx.backlog_vocabulary
convertible = frozenset(vocabulary.get("convertible_kinds") or ["story", "bug", "issue"])
candidates = [
item
for item in ctx.backlog_items
if item.get("status") in COMMITTABLE
and item.get("item_kind") != "epic"
and (item.get("item_kind") or "story") in convertible
and (not gate_id or str(item.get("roadmap_item_id") or "") == gate_id)
]
for item in sorted(candidates, key=_sort_key)[:15]:
rank += 1
proposals.append(
{
"proposal_key": "gate_next_actions",
"scope_type": "backlog_item",
"scope_id": str(item["id"]),
"rank": rank,
"reason_code": "gate_backlog_ready",
"summary": "Eingang-Item am aktiven Gate committen",
"title": item.get("title") or "Backlog-Item",
"item_kind": item.get("item_kind") or "story",
"priority": item.get("priority") or "normal",
"roadmap_item_id": (
str(item["roadmap_item_id"]) if item.get("roadmap_item_id") else gate_id
),
"initiative_id": ctx.initiative_id,
"ranker_key": "heuristic_v0",
"confidence": "heuristic",
"factors": [
{
"code": "gate_alignment",
"weight": 50,
"label": "Gate-Zuordnung",
}
],
"dependency_refs": [],
"dependency_blocked": False,
"data_source": "steering_kernel",
}
)
return proposals
def gate_next_actions_attention_items(
*,
initiative_id: str,
proposals: list[dict[str, Any]],
) -> list[dict[str, Any]]:
backlog_ready = [p for p in proposals if p.get("scope_type") == "backlog_item"]
if not backlog_ready:
return []
return [
{
"kind": "gate_commit_suggested",
"severity": "info",
"title": "Gate-Planung",
"summary": f"{len(backlog_ready)} Item(s) für Gate-Commit vorgeschlagen",
"scope_type": "initiative",
"scope_id": initiative_id,
"initiative_id": initiative_id,
"reason_code": "gate_commit_suggested",
"data_source": "gate_next_actions",
}
]

View File

@ -0,0 +1,93 @@
"""Intake triage proposal — queue_inbox / continuous backlog (no sprint)."""
from __future__ import annotations
from typing import Any
from steering.eval_context import SteeringEvalContext
COMMITTABLE = frozenset({"new", "triaged", "accepted"})
_PRIORITY = {"critical": 0, "high": 1, "normal": 2, "low": 3}
_QUEUE_KIND_BIAS = {"bug": 0, "issue": 1, "incident": 1, "story": 2, "idea": 3}
def propose_intake_triage(
ctx: SteeringEvalContext,
*,
read_models: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
vocabulary = ctx.backlog_vocabulary
convertible = frozenset(vocabulary.get("convertible_kinds") or ["story", "bug", "issue"])
queue_mode = "queue_inbox" in ctx.steering_elements
candidates = [
item
for item in ctx.backlog_items
if item.get("status") in COMMITTABLE
and item.get("item_kind") != "epic"
and (item.get("item_kind") or "story") in convertible
]
if not candidates:
return []
def sort_key(item: dict[str, Any]) -> tuple:
kind = item.get("item_kind") or "story"
kind_bias = _QUEUE_KIND_BIAS.get(kind, 5) if queue_mode else 5
return (
_PRIORITY.get(item.get("priority") or "normal", 9),
kind_bias,
item.get("sort_order") if item.get("sort_order") is not None else 9999,
)
proposals: list[dict[str, Any]] = []
for rank, item in enumerate(sorted(candidates, key=sort_key)[:20], start=1):
kind = item.get("item_kind") or "story"
proposals.append(
{
"proposal_key": "intake_triage",
"scope_type": "backlog_item",
"scope_id": str(item["id"]),
"rank": rank,
"reason_code": "intake_triage_candidate",
"summary": "Triage-Kandidat (Heuristik — Ranker austauschbar)",
"title": item.get("title") or "Backlog-Item",
"item_kind": kind,
"priority": item.get("priority") or "normal",
"initiative_id": ctx.initiative_id,
"ranker_key": "heuristic_v0",
"confidence": "heuristic",
"factors": [
{
"code": "priority",
"weight": _PRIORITY.get(item.get("priority") or "normal", 9),
"label": "Priorität",
}
],
"dependency_refs": [],
"dependency_blocked": False,
"data_source": "steering_kernel",
}
)
return proposals
def intake_triage_attention_items(
*,
initiative_id: str,
proposals: list[dict[str, Any]],
) -> list[dict[str, Any]]:
if not proposals:
return []
return [
{
"kind": "intake_triage_suggested",
"severity": "info",
"title": "Eingang triagieren",
"summary": f"{len(proposals)} Item(s) — Kernel schlägt Triage-Reihenfolge vor",
"scope_type": "initiative",
"scope_id": initiative_id,
"initiative_id": initiative_id,
"reason_code": "intake_triage_suggested",
"data_source": "intake_triage",
}
]

View File

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass, field
from typing import Any, Callable from typing import Any, Callable
from steering.eval_context import SteeringEvalContext from steering.eval_context import SteeringEvalContext
@ -16,6 +16,7 @@ class ProposalProvider:
requires_elements: frozenset[str] requires_elements: frozenset[str]
requires_data_slices: frozenset[str] requires_data_slices: frozenset[str]
compute: ProposalCompute compute: ProposalCompute
excludes_elements: frozenset[str] = field(default_factory=frozenset)
_PROVIDERS: list[ProposalProvider] = [] _PROVIDERS: list[ProposalProvider] = []
@ -37,6 +38,8 @@ def compute_proposals(
for provider in _PROVIDERS: for provider in _PROVIDERS:
if provider.requires_elements and not provider.requires_elements <= elements: if provider.requires_elements and not provider.requires_elements <= elements:
continue continue
if provider.excludes_elements and provider.excludes_elements & elements:
continue
if provider.requires_data_slices and not provider.requires_data_slices <= slices: if provider.requires_data_slices and not provider.requires_data_slices <= slices:
continue continue
result[provider.key] = provider.compute(ctx, read_models) result[provider.key] = provider.compute(ctx, read_models)
@ -45,6 +48,8 @@ def compute_proposals(
def _register_builtin_proposals() -> None: def _register_builtin_proposals() -> None:
from steering.proposals.gate_next_actions import propose_gate_next_actions
from steering.proposals.intake_triage import propose_intake_triage
from steering.proposals.sprint_commit import propose_sprint_commit from steering.proposals.sprint_commit import propose_sprint_commit
register_proposal( register_proposal(
@ -52,7 +57,30 @@ def _register_builtin_proposals() -> None:
key="sprint_commit", key="sprint_commit",
requires_elements=frozenset({"work_cycle_scope"}), requires_elements=frozenset({"work_cycle_scope"}),
requires_data_slices=frozenset({"backlog", "work_cycles"}), requires_data_slices=frozenset({"backlog", "work_cycles"}),
compute=lambda ctx, read_models: propose_sprint_commit(ctx, read_models=read_models), compute=lambda ctx, read_models: propose_sprint_commit(
ctx, read_models=read_models
),
)
)
register_proposal(
ProposalProvider(
key="gate_next_actions",
requires_elements=frozenset({"gate_fulfillment"}),
requires_data_slices=frozenset({"backlog", "roadmap", "actions"}),
compute=lambda ctx, read_models: propose_gate_next_actions(
ctx, read_models=read_models
),
)
)
register_proposal(
ProposalProvider(
key="intake_triage",
requires_elements=frozenset(),
requires_data_slices=frozenset({"backlog"}),
excludes_elements=frozenset({"work_cycle_scope"}),
compute=lambda ctx, read_models: propose_intake_triage(
ctx, read_models=read_models
),
) )
) )

View File

@ -0,0 +1,27 @@
"""Execution graph read model — critical_path / gate_fulfillment."""
from __future__ import annotations
from typing import Any
from steering.eval_context import SteeringEvalContext
from steering.graph.execution_engine import compute_execution_graph_state
def compute_execution_graph_read_model(ctx: SteeringEvalContext) -> dict[str, Any]:
from services.execution_plan import list_dependencies_for_initiative
from steering.strategies.next_action.execution_ready import (
gate_horizon_scope_for_method,
)
gate_scope = gate_horizon_scope_for_method(
ctx.binding.primary_method_key, ctx.tenant_ctx, ctx.initiative_id
)
dependencies = list_dependencies_for_initiative(
tenant_id=ctx.tenant_ctx.tenant_id, initiative_id=ctx.initiative_id
)
return compute_execution_graph_state(
actions=ctx.actions,
dependencies=dependencies,
scope_roadmap_item_id=gate_scope,
)

View File

@ -0,0 +1,36 @@
"""Planning debt read model — gate_fulfillment."""
from __future__ import annotations
from typing import Any
from steering.eval_context import SteeringEvalContext
from steering.graph.execution_engine import compute_planning_debt
def compute_planning_debt_read_model(ctx: SteeringEvalContext) -> list[dict[str, Any]]:
from services import roadmap as roadmap_service
from steering.strategies.next_action.execution_ready import (
gate_horizon_scope_for_method,
)
gate_scope = gate_horizon_scope_for_method(
ctx.binding.primary_method_key, ctx.tenant_ctx, ctx.initiative_id
)
roadmap_items = roadmap_service.list_roadmap_items_for_initiative(
tenant_id=ctx.tenant_ctx.tenant_id, initiative_id=ctx.initiative_id
)
actions = ctx.actions
scoped_roadmap = roadmap_items
scoped_actions = actions
if gate_scope:
scope = str(gate_scope)
scoped_roadmap = [ri for ri in roadmap_items if str(ri.get("id")) == scope]
scoped_actions = [
a
for a in actions
if a.get("roadmap_item_id") and str(a["roadmap_item_id"]) == scope
]
return compute_planning_debt(actions=scoped_actions, roadmap_items=scoped_roadmap)

View File

@ -16,6 +16,7 @@ class ReadModelProvider:
requires_elements: frozenset[str] requires_elements: frozenset[str]
requires_data_slices: frozenset[str] requires_data_slices: frozenset[str]
compute: ReadModelCompute compute: ReadModelCompute
requires_any_element: frozenset[str] = frozenset()
_PROVIDERS: list[ReadModelProvider] = [] _PROVIDERS: list[ReadModelProvider] = []
@ -34,6 +35,10 @@ def compute_read_models(ctx: SteeringEvalContext) -> dict[str, Any]:
for provider in _PROVIDERS: for provider in _PROVIDERS:
if provider.requires_elements and not provider.requires_elements <= elements: if provider.requires_elements and not provider.requires_elements <= elements:
continue continue
if provider.requires_any_element and not (
provider.requires_any_element & elements
):
continue
if provider.requires_data_slices and not provider.requires_data_slices <= slices: if provider.requires_data_slices and not provider.requires_data_slices <= slices:
continue continue
result[provider.key] = provider.compute(ctx) result[provider.key] = provider.compute(ctx)
@ -43,6 +48,8 @@ def compute_read_models(ctx: SteeringEvalContext) -> dict[str, Any]:
def _register_builtin_read_models() -> None: def _register_builtin_read_models() -> None:
from steering.read_models.epic_rollup import compute_epic_rollup from steering.read_models.epic_rollup import compute_epic_rollup
from steering.read_models.execution_graph import compute_execution_graph_read_model
from steering.read_models.planning_debt import compute_planning_debt_read_model
register_read_model( register_read_model(
ReadModelProvider( ReadModelProvider(
@ -55,6 +62,23 @@ def _register_builtin_read_models() -> None:
), ),
) )
) )
register_read_model(
ReadModelProvider(
key="planning_debt",
requires_elements=frozenset({"gate_fulfillment"}),
requires_data_slices=frozenset({"roadmap", "actions"}),
compute=compute_planning_debt_read_model,
)
)
register_read_model(
ReadModelProvider(
key="execution_graph",
requires_elements=frozenset(),
requires_any_element=frozenset({"critical_path", "gate_fulfillment"}),
requires_data_slices=frozenset({"actions"}),
compute=compute_execution_graph_read_model,
)
)
_register_builtin_read_models() _register_builtin_read_models()

View File

@ -0,0 +1,93 @@
"""Unit tests — gate_next_actions proposal."""
from __future__ import annotations
from steering.eval_context import SteeringEvalContext
from steering.kernel.models import HorizonMarker, LifecycleContext, SteeringBinding
from steering.proposals.gate_next_actions import propose_gate_next_actions
from tenant_context import TenantContext
def _ctx(*, backlog_items, read_models=None, gate_id="gate-1"):
binding = SteeringBinding(
primary_method_key="sequential_dependency",
composition_modifier=None,
next_work_strategy_key="sequential_dependency",
)
lifecycle = LifecycleContext(
current_state="execution",
current_state_label="Ausführung",
slot_map={},
active_slots=(),
operational_slots=(),
)
horizon = HorizonMarker(
kind="gate",
gate_roadmap_item_id=gate_id,
gate_title="Gate A",
)
eval_ctx = SteeringEvalContext(
tenant_ctx=TenantContext(
user_id="u1",
email="t@example.com",
display_name="Test",
portal_role="user",
tenant_id="t1",
tenant_slug="t",
tenant_name="T",
tenant_role="admin",
actor_id=None,
actor_type=None,
session_token="test",
capabilities=frozenset(),
),
initiative_id="init-1",
binding=binding,
horizon=horizon,
lifecycle=lifecycle,
operating_context={
"steering_elements": ["gate_fulfillment", "critical_path"],
"data_slices": ["backlog", "roadmap", "actions"],
"backlog_vocabulary": {"convertible_kinds": ["story", "bug", "issue"]},
},
)
eval_ctx._backlog_items = backlog_items
eval_ctx._actions = []
eval_ctx._work_cycles = []
return eval_ctx, read_models or {}
def test_gate_proposal_includes_planning_debt():
eval_ctx, read_models = _ctx(
backlog_items=[],
read_models={
"planning_debt": [
{
"roadmap_item_id": "gate-1",
"title": "Gate A",
"message": "Kein Plan",
}
]
},
)
result = propose_gate_next_actions(eval_ctx, read_models=read_models)
assert result[0]["scope_type"] == "roadmap_item"
assert result[0]["reason_code"] == "planning_debt"
def test_gate_proposal_backlog_at_gate():
eval_ctx, read_models = _ctx(
backlog_items=[
{
"id": "b1",
"title": "Story",
"item_kind": "story",
"status": "accepted",
"roadmap_item_id": "gate-1",
"priority": "normal",
"sort_order": 0,
}
],
)
result = propose_gate_next_actions(eval_ctx, read_models=read_models)
assert any(p["scope_type"] == "backlog_item" for p in result)

View File

@ -0,0 +1,57 @@
"""Unit tests — intake_triage proposal."""
from __future__ import annotations
from steering.eval_context import SteeringEvalContext
from steering.kernel.models import HorizonMarker, LifecycleContext, SteeringBinding
from steering.proposals.intake_triage import propose_intake_triage
from tenant_context import TenantContext
def test_intake_triage_proposes_backlog_items():
binding = SteeringBinding(
primary_method_key="sequential_dependency",
composition_modifier=None,
next_work_strategy_key="sequential_dependency",
)
eval_ctx = SteeringEvalContext(
tenant_ctx=TenantContext(
user_id="u1",
email="t@example.com",
display_name="Test",
portal_role="user",
tenant_id="t1",
tenant_slug="t",
tenant_name="T",
tenant_role="admin",
actor_id=None,
actor_type=None,
session_token="test",
capabilities=frozenset(),
),
initiative_id="init-1",
binding=binding,
horizon=HorizonMarker(kind="none"),
lifecycle=LifecycleContext(
current_state="execution",
current_state_label="Ausführung",
slot_map={},
active_slots=(),
operational_slots=(),
),
operating_context={
"steering_elements": ["gate_fulfillment"],
"data_slices": ["backlog"],
"backlog_vocabulary": {"convertible_kinds": ["story", "bug", "issue"]},
},
)
eval_ctx._backlog_items = [
{"id": "b1", "title": "Item", "item_kind": "story", "status": "new", "priority": "normal", "sort_order": 0},
]
eval_ctx._actions = []
eval_ctx._work_cycles = []
result = propose_intake_triage(eval_ctx, read_models={})
assert len(result) == 1
assert result[0]["proposal_key"] == "intake_triage"
assert result[0]["ranker_key"] == "heuristic_v0"

View File

@ -0,0 +1,134 @@
# ADP — Steering Kernel Coding Rules v0.1
**Status:** PO-freigegeben — **verbindlich für alle Coding-Agenten**
**Stand:** 2026-07-27
**Bezug:** ADP Steering Kernel Spine v0.1, ADP Steering Kernel Extension Model v0.1, ADP AP2.4, `.cursor/rules/kairo-steering-kernel.mdc`
---
## 1. Geltungsbereich
Diese Regeln gelten für **jede** Änderung an:
- `backend/steering/**` (Kernel, Registries, Provider, Ranker, Contributors)
- `backend/data_layer/initiative_snapshot.py` (Steuerungs-Read-Models)
- Frontend-Steuerungs-UI (`SteeringProposalsPanel`, Operating Context, Snapshot-Konsum)
**Ziel:** Ein gemeinsamer Steuerungskern für alle Methoden — keine Agile-Sonderlogik in Pages, kein Methoden-Fork im Kernel.
---
## 2. Herzmaschine (Stop-the-line)
| Regel | Detail |
|-------|--------|
| **SK-01** | Initiative-Steuerung (Next Work, Attention, Read Models, Proposals) **nur** über `steering.kernel.evaluate_steering()`. |
| **SK-02** | Snapshot/API spiegeln Kernel-Output — **keine** parallele Berechnung in `initiative_snapshot.py` (Ausnahme: Legacy-Signals bis Migration). |
| **SK-03** | Keine Steuerungsheuristik in Routers, Services außerhalb `backend/steering/`, oder React-Pages. |
---
## 3. Extension Registry (Pflicht-Muster)
Neue Steuerungsfähigkeit = **Provider registrieren**, nicht inline codieren.
| Typ | Registry | Aktivierung |
|-----|----------|-------------|
| Read Model | `steering/read_models/registry.py` | `requires_elements`, optional `requires_any_element`, `requires_data_slices` |
| Proposal | `steering/proposals/registry.py` | wie oben + optional `excludes_elements` |
| Attention | `steering/attention/contributors/registry.py` | `requires_elements` / `requires_any_element` |
| Ranker (Proposal) | `register_sprint_commit_ranker()` o. Ä. | Operating Context `proposal_rankers.{key}` |
**SK-04:** Provider-Schlüssel sind stabil (`sprint_commit`, `gate_next_actions`, `intake_triage`) — UI bindet an Keys, nicht an Archetyp.
**SK-05:** Keine feste Produktpolitik in Rankern (z.B. „Bugs immer zuerst“). Heuristik = Fallback mit `ranker_key: heuristic_v0`; Agent = `agent_v1` nach Principle Gate.
---
## 4. Proposal-DTO (Minimalvertrag)
Jeder Proposal-Eintrag liefert mindestens:
```python
{
"proposal_key": str,
"scope_type": "backlog_item" | "roadmap_item" | "action" | ...,
"scope_id": str,
"rank": int,
"reason_code": str,
"summary": str,
"ranker_key": str,
"confidence": "heuristic" | "agent",
"factors": [{"code", "weight", "label"}],
"dependency_refs": [...],
"dependency_blocked": bool,
"data_source": "steering_kernel",
}
```
**SK-06:** Proposals **mutieren nie** OM — Accept erfolgt explizit in UI/API.
**SK-07:** Abhängigkeiten (`parent_action_id`, Execution Graph, Epic) in `dependency_refs` / `dependency_blocked` — nicht stillschweigend ignorieren.
---
## 5. Methoden-Parität
| Methode / Element | Read Models | Proposals |
|-------------------|-------------|-----------|
| `work_cycle_scope` | epic_rollup (mit backlog_epic_hierarchy) | `sprint_commit` |
| `gate_fulfillment` | `planning_debt`, `execution_graph` | `gate_next_actions` |
| `backlog` ohne Sprint | — | `intake_triage` (`excludes work_cycle_scope`) |
| `queue_inbox` | — | `intake_triage` (Queue-Profil) |
| `critical_path` | `execution_graph` | (später path-basiert) |
**SK-08:** Neue Methode = Elemente im `MethodDefinition` + Provider — **kein** `if method_key == …` in Kernel.
**SK-09:** `agile_iteration` ist **Modifier**, keine Parallel-Welt — komponiert mit Primary (Product, Linear, …).
---
## 6. Frontend
**SK-10:** Steuerungs-UI liest `steeringSnapshot.steering_kernel.proposals` / `read_models` — nicht selbst sortieren.
**SK-11:** `SteeringProposalsPanel` + `PROPOSAL_UI_CONFIG` erweitern — **keine** neue Page pro Proposal-Typ.
**SK-12:** Capabilities aus `operatingContext.steering_elements` / `hasSteeringElement` — keine Archetyp-Ifs.
---
## 7. KI / Agent (eingefroren bis Principle Gate)
**SK-13:** Keine Prompts hardcodieren. Agent-Ranker implementieren als `ranker_key: agent_v1` + Actor-Slot — Kontext aus Snapshot/Operating Context.
**SK-14:** Bis Freigabe: Fallback auf `heuristic_v0` mit transparentem `factors[]`.
---
## 8. Tests & Abnahme
**SK-15:** Jeder neue Provider: Unit-Test (reine Logik) + Integration über Snapshot wenn DB verfügbar.
**SK-16:** Truth Table + ADP Extension Model §6 aktualisieren.
---
## 9. Verboten (Anti-Patterns)
- Bug-zuerst / Story-zuerst als globale Sortierregel ohne `ranker_key`-Kennzeichnung
- Proposal-Logik in `BacklogSection` / `PlanSprintPage`
- Duplicate Read Model (Snapshot + Kernel)
- Neue OM-Tabelle für Vorschläge
- Methoden-spezifische Kernel-Forks
---
## 10. Referenzen
| Dokument | Pfad |
|----------|------|
| Kernel Extension Model | `docs/architecture/ADP_Steering_Kernel_Extension_Model_v0.1.md` |
| Kernel Spine | `docs/architecture/ADP_Steering_Kernel_Spine_v0.1.md` |
| Cursor Rule | `.cursor/rules/kairo-steering-kernel.mdc` |
| Plugin-Architektur | `.cursor/rules/kairo-plugin-architecture.mdc` |

View File

@ -210,7 +210,9 @@ class AgentSlotProvider:
| Extension | agile_iteration | continuous_product | sequential_dependency | program_delivery | care_navigation | | Extension | agile_iteration | continuous_product | sequential_dependency | program_delivery | care_navigation |
|-----------|-----------------|--------------------|-----------------------|------------------|-----------------| |-----------|-----------------|--------------------|-----------------------|------------------|-----------------|
| `epic_rollup` | ✓ | — | — | ◐ (Epic=Liefercontainer) | — | | `epic_rollup` | ✓ | — | — | ◐ (Epic=Liefercontainer) | — |
| `sprint_commit` proposal | ✓ | — | — | — | — | | `sprint_commit` proposal | ✓ (work_cycle_scope) | ✓ | — | ◐ | ◐ |
| `gate_next_actions` proposal | — | ◐ | ✓ | ✓ | — |
| `intake_triage` proposal | — | ✓ (excl. sprint) | ✓ | ◐ | ✓ |
| `planning_debt` | ◐ | ◐ | ✓ | ✓ | — | | `planning_debt` | ◐ | ◐ | ✓ | ✓ | — |
| `execution_graph` | ◐ | ◐ | ✓ | ✓ | ◐ | | `execution_graph` | ◐ | ◐ | ✓ | ✓ | ◐ |
| `intake_triage` proposal | ✓ | ✓ | ◐ | ◐ | ✓ | | `intake_triage` proposal | ✓ | ✓ | ◐ | ◐ | ✓ |
@ -226,7 +228,7 @@ class AgentSlotProvider:
| Phase | Inhalt | Entkoppelt | | Phase | Inhalt | Entkoppelt |
|-------|--------|------------| |-------|--------|------------|
| **K-Ext-1** | `read_models/registry.py` + Kernel ruft Provider; P4 `epic_rollup` migriert | ✓ Kernel v0.3 | | **K-Ext-1** | `read_models/registry.py` + Kernel ruft Provider; P4 `epic_rollup` migriert | ✓ Kernel v0.3 |
| **K-Ext-2** | `attention/contributors/registry.py`; inline-Attention refactoren | offen | | **K-Ext-2** | `attention/contributors/registry.py`; inline-Attention refactoren | ✓ AP2.2j |
| **K-Ext-3** | `proposals/registry.py` + `SteeringEvaluation.proposals` | ✓ AP2.2i (P6) | | **K-Ext-3** | `proposals/registry.py` + `SteeringEvaluation.proposals` | ✓ AP2.2i (P6) |
| **K-Ext-4** | `agent_slots/registry.py` + Operating Context Feld `agent_slots` | P8 Reviews; Principle Gate für KI | | **K-Ext-4** | `agent_slots/registry.py` + Operating Context Feld `agent_slots` | P8 Reviews; Principle Gate für KI |
| **K-Ext-5** | Portfolio-Ebene: Workspace aggregiert Attention/Proposals über Initiativen | Program Director multi-initiative | | **K-Ext-5** | Portfolio-Ebene: Workspace aggregiert Attention/Proposals über Initiativen | Program Director multi-initiative |

View File

@ -228,7 +228,8 @@ Details: `Kairo_Status_Review_and_Next_Steps_v0.1.md` §2.1
|----|-----------| |----|-----------|
| AP2.3/4 | ✓ Plugin-Architektur + Element-Registry (2026-07-25) | | AP2.3/4 | ✓ Plugin-Architektur + Element-Registry (2026-07-25) |
| AP2.2a | Starter-Kits ◐→✓ | | AP2.2a | Starter-Kits ◐→✓ |
| ADP Backlog/Epic | P1P4 ✓ · P6 Sprint-Vorschlag ✓ · P5 Agent-Tasks offen · K-Ext-2…4 offen | | ADP Backlog/Epic | P1P4 ✓ · P6 Sprint-Vorschlag ✓ · K-Ext-1/2/3 ✓ · Agent-Slots offen |
| Steering Kernel Proposals | ◐ | sprint_commit · gate_next_actions · intake_triage (Kernel v0.3) |
| AP2.2be | Referenz-Archetypen End-to-End ◐→✓ | | AP2.2be | Referenz-Archetypen End-to-End ◐→✓ |
| AP2.1 | MVP-Abnahfe ✗→✓ | | AP2.1 | MVP-Abnahfe ✗→✓ |
| AP1.7b | Op-API Parität | | AP1.7b | Op-API Parität |

View File

@ -0,0 +1,107 @@
import {
PROPOSAL_UI_CONFIG,
filterSprintProposals,
listProposalKeys,
proposalReasonLabel,
} from '../utils/steeringProposals.js'
import { backlogKindLabel, resolveBacklogVocabulary } from '../utils/resolveBacklogVocabulary.js'
import { PriorityBadge } from './PriorityBadge.jsx'
export function SteeringProposalsPanel({
proposalsByKey = {},
proposalKeys = null,
backlogVocabulary = null,
canManage = false,
onAcceptBacklogProposal,
busy = false,
filterContext = {},
}) {
const vocabulary = resolveBacklogVocabulary(backlogVocabulary)
const keys = proposalKeys || listProposalKeys(proposalsByKey)
if (!keys.length) {
return null
}
return (
<>
{keys.map((proposalKey) => {
const config = PROPOSAL_UI_CONFIG[proposalKey] || {
title: proposalKey,
lead: 'Steuerungsvorschlag',
acceptLabel: 'Übernehmen',
scopeTypes: ['backlog_item'],
}
let items = proposalsByKey[proposalKey] || []
if (proposalKey === 'sprint_commit' && filterContext.workCycleId) {
items = filterSprintProposals(items, filterContext.workCycleId)
}
if (!items.length) return null
const ranker = items[0]?.ranker_key || 'heuristic_v0'
return (
<section key={proposalKey} className="card steering-proposals">
<div className="section-header">
<div>
<h2>{config.title}</h2>
<p className="section-lead muted">
{config.lead} Ranker: {ranker}.
</p>
</div>
</div>
<ol className="item-list steering-proposals__list">
{items.map((proposal) => (
<li
key={`${proposalKey}-${proposal.scope_type}-${proposal.scope_id}`}
className="list-item card-list-item steering-proposal"
>
<div className="list-item-main">
<strong>
{proposal.rank}. {proposal.title}
</strong>
<p className="list-item-sub muted">{proposalReasonLabel(proposal)}</p>
{proposal.dependency_blocked && (
<p className="list-item-sub muted steering-proposal__blocked">
Abhängigkeit offen
</p>
)}
{proposal.scope_type === 'roadmap_item' && (
<p className="list-item-sub muted">
Gate ohne Plan Arbeitspaket anlegen oder Eingang committen
</p>
)}
</div>
<div className="list-item-meta action-controls">
{proposal.item_kind && (
<span className="badge badge--kind muted">
{backlogKindLabel(vocabulary, proposal.item_kind)}
</span>
)}
{proposal.priority && <PriorityBadge priority={proposal.priority} />}
{canManage
&& proposal.scope_type === 'backlog_item'
&& config.scopeTypes.includes('backlog_item') && (
<button
type="button"
className="btn btn-primary btn-sm"
disabled={busy || proposal.dependency_blocked}
onClick={() =>
onAcceptBacklogProposal?.(proposal.scope_id, {
work_cycle_id: proposal.work_cycle_id,
proposal_key: proposalKey,
})
}
>
{config.acceptLabel}
</button>
)}
</div>
</li>
))}
</ol>
</section>
)
})}
</>
)
}

View File

@ -1,5 +1,7 @@
import { useEffect, useMemo } from 'react'
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx' import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
import { BacklogSection } from '../../components/BacklogSection.jsx' import { BacklogSection } from '../../components/BacklogSection.jsx'
import { SteeringProposalsPanel } from '../../components/SteeringProposalsPanel.jsx'
import { LoadingState } from '../../components/LoadingState.jsx' import { LoadingState } from '../../components/LoadingState.jsx'
export function InitiativeInboxPage() { export function InitiativeInboxPage() {
@ -21,8 +23,20 @@ export function InitiativeInboxPage() {
handleConvertBacklog, handleConvertBacklog,
handleBulkConvertBacklog, handleBulkConvertBacklog,
handleDeleteBacklog, handleDeleteBacklog,
reloadSlices,
steeringSnapshot,
operatingContext,
} = useInitiativeOperations() } = useInitiativeOperations()
useEffect(() => {
reloadSlices(['steering_snapshot'])
}, [reloadSlices])
const kernelProposals = useMemo(
() => steeringSnapshot?.steering_kernel?.proposals || {},
[steeringSnapshot],
)
if (!capabilities.has('kairo.initiative.read')) { if (!capabilities.has('kairo.initiative.read')) {
return null return null
} }
@ -34,6 +48,17 @@ export function InitiativeInboxPage() {
return ( return (
<> <>
{error && <p className="error">{error}</p>} {error && <p className="error">{error}</p>}
<SteeringProposalsPanel
proposalsByKey={kernelProposals}
proposalKeys={['intake_triage']}
backlogVocabulary={operatingContext?.backlog_vocabulary}
canManage={capabilities.has('kairo.backlog.manage')}
onAcceptBacklogProposal={async (itemId) => {
await handleConvertBacklog(itemId, {})
await reloadSlices(['steering_snapshot', 'actions', 'backlog'])
}}
busy={formBusy}
/>
<BacklogSection <BacklogSection
items={backlogItems} items={backlogItems}
initiativeId={initiativeId} initiativeId={initiativeId}

View File

@ -1,5 +1,7 @@
import { useEffect, useMemo } from 'react'
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx' import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
import { GatesPlanPanel } from '../../components/GatesPlanPanel.jsx' import { GatesPlanPanel } from '../../components/GatesPlanPanel.jsx'
import { SteeringProposalsPanel } from '../../components/SteeringProposalsPanel.jsx'
export function InitiativePlanPage() { export function InitiativePlanPage() {
const { const {
@ -10,9 +12,22 @@ export function InitiativePlanPage() {
handleCreateRoadmapItem, handleCreateRoadmapItem,
handleReorderRoadmapItems, handleReorderRoadmapItems,
handleDeleteRoadmapItem, handleDeleteRoadmapItem,
handleConvertBacklog,
reloadSlices,
steeringSnapshot,
operatingContext,
error, error,
} = useInitiativeOperations() } = useInitiativeOperations()
useEffect(() => {
reloadSlices(['steering_snapshot'])
}, [reloadSlices])
const kernelProposals = useMemo(
() => steeringSnapshot?.steering_kernel?.proposals || {},
[steeringSnapshot],
)
if (!capabilities.has('kairo.initiative.read')) { if (!capabilities.has('kairo.initiative.read')) {
return null return null
} }
@ -20,6 +35,17 @@ export function InitiativePlanPage() {
return ( return (
<> <>
{error && <p className="error">{error}</p>} {error && <p className="error">{error}</p>}
<SteeringProposalsPanel
proposalsByKey={kernelProposals}
proposalKeys={['gate_next_actions']}
backlogVocabulary={operatingContext?.backlog_vocabulary}
canManage={capabilities.has('kairo.backlog.manage')}
onAcceptBacklogProposal={async (itemId) => {
await handleConvertBacklog(itemId, {})
await reloadSlices(['steering_snapshot', 'actions', 'backlog'])
}}
busy={formBusy}
/>
<GatesPlanPanel <GatesPlanPanel
initiativeId={initiativeId} initiativeId={initiativeId}
items={roadmapItems} items={roadmapItems}

View File

@ -2,7 +2,7 @@ import { useEffect, useMemo } from 'react'
import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx' import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
import { ScopedInitiativeProvider } from '../../layout/ScopedInitiativeProvider.jsx' import { ScopedInitiativeProvider } from '../../layout/ScopedInitiativeProvider.jsx'
import { WorkCyclesPanel } from '../../components/WorkCyclesPanel.jsx' import { WorkCyclesPanel } from '../../components/WorkCyclesPanel.jsx'
import { SprintCommitProposalPanel } from '../../components/SprintCommitProposalPanel.jsx' import { SteeringProposalsPanel } from '../../components/SteeringProposalsPanel.jsx'
import { InitiativeActionsHub } from '../../components/InitiativeActionsHub.jsx' import { InitiativeActionsHub } from '../../components/InitiativeActionsHub.jsx'
import { LoadingState } from '../../components/LoadingState.jsx' import { LoadingState } from '../../components/LoadingState.jsx'
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx' import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
@ -58,11 +58,8 @@ function PlanSprintInner() {
reloadSlices(['steering_snapshot']) reloadSlices(['steering_snapshot'])
}, [reloadSlices]) }, [reloadSlices])
const sprintCommitProposals = useMemo( const kernelProposals = useMemo(
() => () => steeringSnapshot?.steering_kernel?.proposals || {},
steeringSnapshot?.sprint_commit_proposals ||
steeringSnapshot?.steering_kernel?.proposals?.sprint_commit ||
[],
[steeringSnapshot], [steeringSnapshot],
) )
@ -110,12 +107,13 @@ function PlanSprintInner() {
busy={formBusy} busy={formBusy}
/> />
{selectedWorkCycleId && ( {selectedWorkCycleId && (
<SprintCommitProposalPanel <SteeringProposalsPanel
proposals={sprintCommitProposals} proposalsByKey={kernelProposals}
selectedWorkCycleId={selectedWorkCycleId} proposalKeys={['sprint_commit']}
filterContext={{ workCycleId: selectedWorkCycleId }}
backlogVocabulary={operatingContext?.backlog_vocabulary} backlogVocabulary={operatingContext?.backlog_vocabulary}
canManage={capabilities.has('kairo.backlog.manage')} canManage={capabilities.has('kairo.backlog.manage')}
onAcceptProposal={async (itemId, options) => { onAcceptBacklogProposal={async (itemId, options) => {
await handleConvertBacklog(itemId, options) await handleConvertBacklog(itemId, options)
await reloadSlices(['steering_snapshot', 'actions', 'backlog']) await reloadSlices(['steering_snapshot', 'actions', 'backlog'])
}} }}

View File

@ -1716,10 +1716,28 @@
gap: 1rem; gap: 1rem;
} }
.sprint-commit-proposal__blocked { .sprint-commit-proposal__blocked,
.steering-proposal__blocked {
color: var(--jk-warning-text, #7a4b08); color: var(--jk-warning-text, #7a4b08);
} }
.steering-proposals {
margin-top: 1rem;
}
.steering-proposals__list {
list-style: none;
padding: 0;
margin: 0;
}
.steering-proposal {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}
.archetype-badge { .archetype-badge {
background: var(--jk-surface-muted, #eef2f6); background: var(--jk-surface-muted, #eef2f6);
color: var(--jk-text-secondary, #445); color: var(--jk-text-secondary, #445);

View File

@ -0,0 +1,51 @@
/** Generic steering proposal UI config (Kernel v0.3). */
export const PROPOSAL_UI_CONFIG = {
sprint_commit: {
title: 'Sprint-Vorschläge',
lead: 'Ranker-gestützte Commit-Vorschläge — Accept committet ins Sprint-Backlog.',
acceptLabel: 'In Sprint planen',
scopeTypes: ['backlog_item'],
},
gate_next_actions: {
title: 'Gate-Vorschläge',
lead: 'Planning Debt und Eingang am aktiven Gate — committen oder AP planen.',
acceptLabel: 'Committen',
scopeTypes: ['backlog_item'],
},
intake_triage: {
title: 'Triage-Vorschläge',
lead: 'Priorisierte Eingangs-Reihenfolge — Heuristik oder später Agent-Ranker.',
acceptLabel: 'Committen',
scopeTypes: ['backlog_item'],
},
}
export function listProposalKeys(proposalsByKey = {}) {
return Object.keys(PROPOSAL_UI_CONFIG).filter(
(key) => Array.isArray(proposalsByKey[key]) && proposalsByKey[key].length > 0,
)
}
export function proposalReasonLabel(proposal) {
const labels = {
priority_critical: 'Dringlichkeit critical',
priority_high: 'Hohe Priorität',
candidate_bug_or_issue: 'Kandidat (Heuristik)',
candidate_ready: 'Commit-bereit (Heuristik)',
waiting_on_feature_action: 'Wartet auf Feature-AP',
missing_parent_action: 'Feature-Referenz fehlt',
dependency_blocked: 'Abhängigkeit offen',
gate_backlog_ready: 'Gate-Eingang committen',
planning_debt: 'Planning Debt — Plan ergänzen',
intake_triage_candidate: 'Triage-Kandidat',
}
return labels[proposal?.reason_code] || proposal?.summary || 'Vorschlag'
}
export function filterSprintProposals(proposals, workCycleId) {
if (!workCycleId) return proposals || []
return (proposals || []).filter(
(p) => !p.work_cycle_id || String(p.work_cycle_id) === String(workCycleId),
)
}