diff --git a/.cursor/rules/kairo-plugin-architecture.mdc b/.cursor/rules/kairo-plugin-architecture.mdc
index 44324a4..211e41c 100644
--- a/.cursor/rules/kairo-plugin-architecture.mdc
+++ b/.cursor/rules/kairo-plugin-architecture.mdc
@@ -6,7 +6,7 @@ alwaysApply: true
# 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
diff --git a/.cursor/rules/kairo-steering-kernel.mdc b/.cursor/rules/kairo-steering-kernel.mdc
new file mode 100644
index 0000000..2234f0f
--- /dev/null
+++ b/.cursor/rules/kairo-steering-kernel.mdc
@@ -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
diff --git a/CLAUDE.md b/CLAUDE.md
index 0f5d37a..483d7a4 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -50,6 +50,7 @@ Lies bei Projektstart in dieser Reihenfolge:
8. `docs/architecture/Kairo_Tenant_Invariants_v0.1.md`
9. `docs/architecture/Kairo_Sprint0_Principle_Gate_v0.1.md`
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**)
12. `docs/architecture/ADP_AP2_4_Steering_Elements_and_Method_Contract_v0.1.md` (AP2.4 — **geliefert**)
13. `.cursor/rules/kairo-plugin-architecture.mdc`
diff --git a/backend/data_layer/initiative_snapshot.py b/backend/data_layer/initiative_snapshot.py
index ddbb0b3..415cd7e 100644
--- a/backend/data_layer/initiative_snapshot.py
+++ b/backend/data_layer/initiative_snapshot.py
@@ -362,6 +362,8 @@ def get_initiative_steering_snapshot(
"active_work_cycle": active_work_cycle,
"epic_rollup": epic_rollup,
"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": {
"actions_open": open_actions,
"actions_blocked": blocked_actions,
diff --git a/backend/steering/attention/__init__.py b/backend/steering/attention/__init__.py
new file mode 100644
index 0000000..6ff5ba0
--- /dev/null
+++ b/backend/steering/attention/__init__.py
@@ -0,0 +1 @@
+"""Attention contributors — K-Ext-2."""
diff --git a/backend/steering/attention/context.py b/backend/steering/attention/context.py
new file mode 100644
index 0000000..d46077c
--- /dev/null
+++ b/backend/steering/attention/context.py
@@ -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
diff --git a/backend/steering/attention/contributors/__init__.py b/backend/steering/attention/contributors/__init__.py
new file mode 100644
index 0000000..420274c
--- /dev/null
+++ b/backend/steering/attention/contributors/__init__.py
@@ -0,0 +1 @@
+"""Attention contributors package."""
diff --git a/backend/steering/attention/contributors/core.py b/backend/steering/attention/contributors/core.py
new file mode 100644
index 0000000..e3a8d7d
--- /dev/null
+++ b/backend/steering/attention/contributors/core.py
@@ -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)
+)
diff --git a/backend/steering/attention/contributors/gates.py b/backend/steering/attention/contributors/gates.py
new file mode 100644
index 0000000..42426dd
--- /dev/null
+++ b/backend/steering/attention/contributors/gates.py
@@ -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,
+ )
+)
diff --git a/backend/steering/attention/contributors/read_models.py b/backend/steering/attention/contributors/read_models.py
new file mode 100644
index 0000000..e525d7a
--- /dev/null
+++ b/backend/steering/attention/contributors/read_models.py
@@ -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,
+ )
+)
diff --git a/backend/steering/attention/contributors/registry.py b/backend/steering/attention/contributors/registry.py
new file mode 100644
index 0000000..c11ba46
--- /dev/null
+++ b/backend/steering/attention/contributors/registry.py
@@ -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
diff --git a/backend/steering/kernel/attention.py b/backend/steering/kernel/attention.py
index 4b5a882..4b52e89 100644
--- a/backend/steering/kernel/attention.py
+++ b/backend/steering/kernel/attention.py
@@ -5,16 +5,12 @@ from __future__ import annotations
from typing import Any
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
-_GATE_METHODS = frozenset(
- {
- "sequential_dependency",
- "program_delivery",
- "maturity_progression",
- }
-)
+# Side-effect: register contributors
+import steering.attention.contributors.core # noqa: F401
+import steering.attention.contributors.gates # noqa: F401
+import steering.attention.contributors.read_models # noqa: F401
def evaluate_attention(
@@ -29,161 +25,18 @@ def evaluate_attention(
eval_ctx=None,
limit: int = 20,
) -> list[dict[str, Any]]:
- """Kernel attention for a single initiative — no fake next when planning debt."""
- from services import actions as action_service
- from services import blockers as blocker_service
- 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,
+ """Kernel attention — contributors via registry (K-Ext-2)."""
+ from steering.attention.context import AttentionEvalContext
+ from steering.attention.contributors.registry import collect_attention_items
+
+ actx = AttentionEvalContext(
+ tenant_ctx=ctx,
+ initiative_id=initiative_id,
+ binding=binding,
+ horizon=horizon,
+ next_work=next_work,
+ read_models=read_models or {},
+ proposals=proposals or {},
+ eval_ctx=eval_ctx,
)
-
- items: list[dict[str, Any]] = []
-
- 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,
- actions=actions,
- graph_state=graph_state,
- )
- )
-
- 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]
+ return collect_attention_items(actx)[:limit]
diff --git a/backend/steering/proposals/gate_next_actions.py b/backend/steering/proposals/gate_next_actions.py
new file mode 100644
index 0000000..3820d33
--- /dev/null
+++ b/backend/steering/proposals/gate_next_actions.py
@@ -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",
+ }
+ ]
diff --git a/backend/steering/proposals/intake_triage.py b/backend/steering/proposals/intake_triage.py
new file mode 100644
index 0000000..1332fa9
--- /dev/null
+++ b/backend/steering/proposals/intake_triage.py
@@ -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",
+ }
+ ]
diff --git a/backend/steering/proposals/registry.py b/backend/steering/proposals/registry.py
index 4011629..76a102d 100644
--- a/backend/steering/proposals/registry.py
+++ b/backend/steering/proposals/registry.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from dataclasses import dataclass
+from dataclasses import dataclass, field
from typing import Any, Callable
from steering.eval_context import SteeringEvalContext
@@ -16,6 +16,7 @@ class ProposalProvider:
requires_elements: frozenset[str]
requires_data_slices: frozenset[str]
compute: ProposalCompute
+ excludes_elements: frozenset[str] = field(default_factory=frozenset)
_PROVIDERS: list[ProposalProvider] = []
@@ -37,6 +38,8 @@ def compute_proposals(
for provider in _PROVIDERS:
if provider.requires_elements and not provider.requires_elements <= elements:
continue
+ if provider.excludes_elements and provider.excludes_elements & elements:
+ continue
if provider.requires_data_slices and not provider.requires_data_slices <= slices:
continue
result[provider.key] = provider.compute(ctx, read_models)
@@ -45,6 +48,8 @@ def compute_proposals(
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
register_proposal(
@@ -52,7 +57,30 @@ def _register_builtin_proposals() -> None:
key="sprint_commit",
requires_elements=frozenset({"work_cycle_scope"}),
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
+ ),
)
)
diff --git a/backend/steering/read_models/execution_graph.py b/backend/steering/read_models/execution_graph.py
new file mode 100644
index 0000000..afbbe9e
--- /dev/null
+++ b/backend/steering/read_models/execution_graph.py
@@ -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,
+ )
diff --git a/backend/steering/read_models/planning_debt.py b/backend/steering/read_models/planning_debt.py
new file mode 100644
index 0000000..4032057
--- /dev/null
+++ b/backend/steering/read_models/planning_debt.py
@@ -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)
diff --git a/backend/steering/read_models/registry.py b/backend/steering/read_models/registry.py
index f26ad3c..c39b3f5 100644
--- a/backend/steering/read_models/registry.py
+++ b/backend/steering/read_models/registry.py
@@ -16,6 +16,7 @@ class ReadModelProvider:
requires_elements: frozenset[str]
requires_data_slices: frozenset[str]
compute: ReadModelCompute
+ requires_any_element: frozenset[str] = frozenset()
_PROVIDERS: list[ReadModelProvider] = []
@@ -34,6 +35,10 @@ def compute_read_models(ctx: SteeringEvalContext) -> dict[str, Any]:
for provider in _PROVIDERS:
if provider.requires_elements and not provider.requires_elements <= elements:
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:
continue
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:
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(
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()
diff --git a/backend/tests/test_gate_next_actions_unit.py b/backend/tests/test_gate_next_actions_unit.py
new file mode 100644
index 0000000..22e25be
--- /dev/null
+++ b/backend/tests/test_gate_next_actions_unit.py
@@ -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)
diff --git a/backend/tests/test_intake_triage_unit.py b/backend/tests/test_intake_triage_unit.py
new file mode 100644
index 0000000..48c6e4f
--- /dev/null
+++ b/backend/tests/test_intake_triage_unit.py
@@ -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"
diff --git a/docs/architecture/ADP_Steering_Kernel_Coding_Rules_v0.1.md b/docs/architecture/ADP_Steering_Kernel_Coding_Rules_v0.1.md
new file mode 100644
index 0000000..be2a20d
--- /dev/null
+++ b/docs/architecture/ADP_Steering_Kernel_Coding_Rules_v0.1.md
@@ -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` |
diff --git a/docs/architecture/ADP_Steering_Kernel_Extension_Model_v0.1.md b/docs/architecture/ADP_Steering_Kernel_Extension_Model_v0.1.md
index 83dc04f..6a866e7 100644
--- a/docs/architecture/ADP_Steering_Kernel_Extension_Model_v0.1.md
+++ b/docs/architecture/ADP_Steering_Kernel_Extension_Model_v0.1.md
@@ -210,7 +210,9 @@ class AgentSlotProvider:
| Extension | agile_iteration | continuous_product | sequential_dependency | program_delivery | care_navigation |
|-----------|-----------------|--------------------|-----------------------|------------------|-----------------|
| `epic_rollup` | ✓ | — | — | ◐ (Epic=Liefercontainer) | — |
-| `sprint_commit` proposal | ✓ | — | — | — | — |
+| `sprint_commit` proposal | ✓ (work_cycle_scope) | ✓ | — | ◐ | ◐ |
+| `gate_next_actions` proposal | — | ◐ | ✓ | ✓ | — |
+| `intake_triage` proposal | — | ✓ (excl. sprint) | ✓ | ◐ | ✓ |
| `planning_debt` | ◐ | ◐ | ✓ | ✓ | — |
| `execution_graph` | ◐ | ◐ | ✓ | ✓ | ◐ |
| `intake_triage` proposal | ✓ | ✓ | ◐ | ◐ | ✓ |
@@ -226,7 +228,7 @@ class AgentSlotProvider:
| Phase | Inhalt | Entkoppelt |
|-------|--------|------------|
| **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-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 |
diff --git a/docs/product/Kairo_Implementation_Truth_Table_v0.1.md b/docs/product/Kairo_Implementation_Truth_Table_v0.1.md
index 4666cbf..f4aad2f 100644
--- a/docs/product/Kairo_Implementation_Truth_Table_v0.1.md
+++ b/docs/product/Kairo_Implementation_Truth_Table_v0.1.md
@@ -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.2a | Starter-Kits ◐→✓ |
-| ADP Backlog/Epic | P1–P4 ✓ · P6 Sprint-Vorschlag ✓ · P5 Agent-Tasks offen · K-Ext-2…4 offen |
+| ADP Backlog/Epic | P1–P4 ✓ · 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.2b–e | Referenz-Archetypen End-to-End ◐→✓ |
| AP2.1 | MVP-Abnahfe ✗→✓ |
| AP1.7b | Op-API Parität |
diff --git a/frontend/src/components/SteeringProposalsPanel.jsx b/frontend/src/components/SteeringProposalsPanel.jsx
new file mode 100644
index 0000000..d1c3d09
--- /dev/null
+++ b/frontend/src/components/SteeringProposalsPanel.jsx
@@ -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 (
+
+ {config.lead} Ranker: {ranker}.
+ {proposalReasonLabel(proposal)}
+ Abhängigkeit offen
+
+ Gate ohne Plan — Arbeitspaket anlegen oder Eingang committen
+ {config.title}
+
+ {items.map((proposal) => (
+
+
{error}
} +{error}
} +