diff --git a/docs/Organisation-Konzept.md b/docs/Organisation-Konzept.md
index eb74dc9..1598bbf 100644
--- a/docs/Organisation-Konzept.md
+++ b/docs/Organisation-Konzept.md
@@ -475,6 +475,49 @@ Ohne Regeln läuft so etwas heiß. Vier, die nicht verhandelbar sind:
4. **Die Spitze ist endlich.** Was der Mensch nicht entscheidet, bleibt offen — sichtbar
offen, nicht stillschweigend versandet.
+### Umsetzung (2026-09-25, #689)
+
+- **Vorgang und Beschluss** sind eigene Datensätze (`Cases`, `Decisions`). Der Vorgang
+ selbst wandert nicht über das Brett. Jeder Halter bekommt eine eigene Aufgabe in seinen
+ Eingang, die Regeln führt `CaseDesk`.
+- **Eskalation:** Das Werkzeug `Escalation` hat jeder Agent, ohne dass man es freischalten
+ muss. „Ich komme nicht weiter“ ist keine Berechtigung. Der Vorgang geht an die eigene
+ Leitung, eine Ebene je Schritt. Die Leitung hat drei Antworten:
+ - `decide`: mit Pflichtbegründung, gilt für den Bereich der Leitung
+ - `handle`: sie erledigt es selbst
+ - `forward`: sie reicht mit eigener Einschätzung weiter
+
+ Die Aufgabe, an der der Agent feststeckt, wartet (blockiert) und geht beim Abschluss
+ wieder an die Arbeit.
+- **Erst nachsehen:** Vor jeder Eskalation zeigt das Werkzeug die Beschlüsse, die den
+ Agenten binden. Dieselbe Frage aus derselben Einheit schließt sich dem offenen Vorgang
+ an, statt einen zweiten zu eröffnen.
+- **Konflikt:** Zuerst nimmt die Gegenseite einmal Stellung. Danach geht der Vorgang direkt
+ an die niedrigste gemeinsame Leitung. Die Leitungen dazwischen stehen als „vermerkt,
+ nicht befragt“ im Vorgang. Einen Konflikt kann man weder weiterreichen noch „selbst
+ erledigen“, er wird entschieden, und beide Seiten erfahren den Beschluss.
+- **Damit es endet:**
+ - Es gibt einen Tagesdeckel je Einheit (`cases.escalationsPerUnitPerDay`, Vorgabe 10).
+ Darüber geht die Eskalation direkt an den Menschen, mit einem Vermerk.
+ - Nach der Frist (`cases.deadlineHours`, Vorgabe 4 Stunden) wird der Mensch gerufen,
+ einmal. Der Weckruf hat eine Anschlussstelle für den Kanal aus #644. Bis dahin
+ erscheint der Vorgang in der neuen Ansicht „Vorgänge“, mit einem Zähler in der
+ Seitenleiste.
+- **Der Mensch** entscheidet dort mit Begründung (gilt für die ganze Instanz) oder fordert
+ von einer Seite eine Stellungnahme an. Das ist die einzige Bewegung nach unten, die den
+ Vorgang nicht schließt, und sie hat eine eigene Frist.
+- **Stille Post (#697) und Berichtsweg (#715):**
+ - Jede Ebene hängt ihren Beitrag an, jeder Beitrag trägt seinen Lauf.
+ - Beim Halter kommt jeder Beitrag gerahmt als Daten an.
+ - Wer fremde Inhalte liest, eskaliert ohne eigenen Text (nur mit dem Verweis auf die
+ Aufgabe) und eröffnet keinen Konflikt mit eigenem Standpunkt.
+- **Nebenbei behoben:** Der Scanner hat einen Status, den der Lauf selbst gesetzt hatte
+ (blockiert, abgebrochen), am Laufende mit „erledigt“ überschrieben. Damit liefen sowohl
+ das Eskalieren als auch der Meldeweg aus #715 ins Leere.
+- **Offen:** #676 (Schemaauskunft der Datenbank) ist die Vorbedingung dafür, dass „ich finde
+ kein Feld“ eine Feststellung ist und keine Vermutung. Der Mechanismus steht, der
+ Anwendungsfall braucht noch #676.
+
## 6 — Die Entscheidungsinstanz an der Spitze
Der CEO-Agent aus dem Vorschlag ist eine gute Idee mit einer Bedingung: Er ist eine
diff --git a/src/ClawdDotNet.App/AppHost.cs b/src/ClawdDotNet.App/AppHost.cs
index fe28cc3..8048fec 100644
--- a/src/ClawdDotNet.App/AppHost.cs
+++ b/src/ClawdDotNet.App/AppHost.cs
@@ -61,6 +61,12 @@ public sealed class AppHost : IAsyncDisposable
public required ILoggerFactory LoggerFactory { get; init; }
public required ToolRegistry Tools { get; init; }
+ /// #689: Escalations, conflicts and decisions — read by the interfaces, acted on through the desk.
+ public Core.Cases.ICaseRepository? Cases { get; init; }
+
+ /// #689: The desk a human decides at; null without a task board.
+ public Core.Cases.ICaseDesk? CaseDesk => Engine?.CaseDesk;
+
///
/// Zustandsspeicher und Audit-Log der Instanz. Nicht oeffentlich — sie gehoeren dem
/// Aufbau, nicht den Oberflaechen; gebraucht werden sie hier fuer die Sichtung
@@ -600,6 +606,7 @@ public sealed class AppHost : IAsyncDisposable
Board = board,
Usage = usage,
Transcripts = transcripts,
+ Cases = cases,
Insights = new RunInsights(audit, transcripts, board, staging,
agentId => instance.Agents.FirstOrDefault(a => a.AgentId == agentId) is { } agent
? engine.TranscriptRetentionFor(agent)
diff --git a/src/ClawdDotNet.Manager.Web/wwwroot/app.css b/src/ClawdDotNet.Manager.Web/wwwroot/app.css
index 8e36248..96240c3 100644
--- a/src/ClawdDotNet.Manager.Web/wwwroot/app.css
+++ b/src/ClawdDotNet.Manager.Web/wwwroot/app.css
@@ -781,6 +781,68 @@ button.klein {
gap: 12px;
}
+/* ─── #689: cases ─── */
+
+.faelle-liste {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ margin-top: 16px;
+}
+
+.fall-plaketten {
+ margin-bottom: 12px;
+}
+
+/* The two answers of the human wrap below the subject on a narrow screen instead of squeezing it. */
+.fall-karte .karte-kopf {
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.fall-karte .karte-kopf h2 {
+ flex: 1 1 240px;
+}
+
+.fall-beim-menschen {
+ border-left: 3px solid var(--warnung);
+}
+
+.fall-verlauf-titel {
+ margin: 16px 0 8px;
+ font-size: 14px;
+}
+
+.fall-verlauf {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.fall-eintrag {
+ padding: 8px 12px;
+ border-left: 2px solid var(--divider);
+ background: var(--bg);
+}
+
+.fall-eintrag-decided,
+.fall-eintrag-handled {
+ border-left-color: var(--akzent, var(--divider));
+}
+
+.fall-eintrag-called {
+ border-left-color: var(--warnung);
+}
+
+.fall-eintrag-text {
+ margin-top: 4px;
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+}
+
/* #715: marks a tool that brings outside text in — it changes the agent's reporting line. */
.werkzeug-fremd {
display: inline-block;
diff --git a/src/ClawdDotNet.Manager.Web/wwwroot/app.js b/src/ClawdDotNet.Manager.Web/wwwroot/app.js
index cc0fcc4..ca41b77 100644
--- a/src/ClawdDotNet.Manager.Web/wwwroot/app.js
+++ b/src/ClawdDotNet.Manager.Web/wwwroot/app.js
@@ -37,6 +37,7 @@ import { renderRunDetail } from "./views/run-detail.js";
import { renderPromptHistory } from "./views/prompt-history.js";
import { renderTeam, leaveTeam } from "./views/team.js";
import { renderOrganisation } from "./views/organisation.js";
+import { renderCases, holeMenschenfaelle } from "./views/cases.js";
// ─── 1. Routen registrieren ───
@@ -57,6 +58,7 @@ registriereRoute("/team", () => { verlasseAnsicht(); return renderTeam(); });
registriereRoute("/organisation", () => { verlasseAnsicht(); return renderOrganisation(); });
registriereRoute("/dienste", () => { verlasseAnsicht(); return rendereDienste(); });
registriereRoute("/staging", () => { verlasseAnsicht(); return rendereStaging(); });
+registriereRoute("/cases", () => { verlasseAnsicht(); return renderCases(); });
registriereRoute("/verbrauch", () => { verlasseAnsicht(); return rendereVerbrauch(); });
// #660: token usage sub-views and the prompt history
registriereRoute("/verbrauch/runs", () => { verlasseAnsicht(); return renderUsageRuns(); });
@@ -335,9 +337,19 @@ async function aktualisiereStagingZaehler() {
if (!holeAktiveInstanz()) {
const badge = document.getElementById("staging-badge");
if (badge) badge.hidden = true;
+ const faelle = document.getElementById("faelle-badge");
+ if (faelle) faelle.hidden = true;
return;
}
+ // #689: Cases that wait for a human are counted beside the approvals.
+ const faelleAnzahl = await holeMenschenfaelle();
+ const faelleBadge = document.getElementById("faelle-badge");
+ if (faelleBadge) {
+ faelleBadge.textContent = String(faelleAnzahl);
+ faelleBadge.hidden = faelleAnzahl === 0;
+ }
+
const anzahl = await holeWartendeAnzahl();
const badge = document.getElementById("staging-badge");
if (badge) {
diff --git a/src/ClawdDotNet.Manager.Web/wwwroot/i18n/de.json b/src/ClawdDotNet.Manager.Web/wwwroot/i18n/de.json
index 4e88872..84ef56f 100644
--- a/src/ClawdDotNet.Manager.Web/wwwroot/i18n/de.json
+++ b/src/ClawdDotNet.Manager.Web/wwwroot/i18n/de.json
@@ -222,6 +222,7 @@
"nav.group.system": "System",
"nav.instances": "Instanzen",
"nav.staging": "Freigaben",
+ "nav.cases": "Vorgänge",
"nav.logs": "Protokoll",
"nav.backup": "Sicherung",
"nav.settings": "Einstellungen",
@@ -851,5 +852,65 @@
"desktop.dialog.addService": "Dienst hinzufügen",
"desktop.dialog.restoreBackup": "Sicherung wiederherstellen",
"desktop.dialog.license": "Lizenz-Aktivierung",
- "desktop.dialog.instancePicker": "Instanz auswählen"
+ "desktop.dialog.instancePicker": "Instanz auswählen",
+
+ "cases.title": "Vorgänge",
+ "cases.refresh": "Aktualisieren",
+ "cases.intro": "Eskalationen und Konflikte, die die Agenten nicht unter sich lösen konnten, und die Beschlüsse, die sie geschlossen haben. Was bis zur Frist niemand löst, landet hier.",
+ "cases.tabOpen": "Offen",
+ "cases.tabAll": "Alle",
+ "cases.tabDecisions": "Beschlüsse",
+ "cases.none": "Keine offenen Vorgänge",
+ "cases.noneHint": "Nichts wartet auf eine Entscheidung.",
+ "cases.noDecisions": "Noch keine Beschlüsse",
+ "cases.noDecisionsHint": "Jeder entschiedene Vorgang hinterlässt hier einen Beschluss. Er gilt für den Bereich, für den er gefasst wurde, und wird gezeigt, bevor jemand dieselbe Frage erneut stellt.",
+ "cases.loadFailed": "Vorgänge konnten nicht geladen werden",
+ "cases.kindEscalation": "Eskalation",
+ "cases.kindConflict": "Konflikt",
+ "cases.status.open": "offen",
+ "cases.status.decided": "entschieden",
+ "cases.status.handled": "erledigt",
+ "cases.overdue": "Frist überschritten",
+ "cases.called": "Mensch gerufen",
+ "cases.id": "ID",
+ "cases.openedBy": "Eröffnet von",
+ "cases.holder": "Liegt bei",
+ "cases.human": "dir (Mensch)",
+ "cases.humanAuthor": "Mensch",
+ "cases.deadline": "Frist",
+ "cases.level": "Weitergereicht",
+ "cases.parties": "Seiten",
+ "cases.informed": "Vermerkt, nicht befragt",
+ "cases.waitingFor": "Wartet auf",
+ "cases.origin": "Herkunft",
+ "cases.trail": "Was jede Ebene gesagt hat",
+ "cases.entry.opened": "eröffnet",
+ "cases.entry.joined": "angeschlossen",
+ "cases.entry.forwarded": "weitergereicht",
+ "cases.entry.position": "Standpunkt",
+ "cases.entry.statement": "Stellungnahme",
+ "cases.entry.statement-requested": "Stellungnahme angefordert",
+ "cases.entry.called": "Mensch gerufen",
+ "cases.entry.decided": "entschieden",
+ "cases.entry.handled": "erledigt",
+ "cases.entry.note": "Vermerk",
+ "cases.decide": "Entscheiden",
+ "cases.decideTitle": "Vorgang {id} entscheiden",
+ "cases.decideHint": "Deine Entscheidung gilt für die ganze Instanz. Jeder Agent, der dieselbe Frage stellt, bekommt sie vorher angezeigt. Alle, die den Vorgang eröffnet haben oder daran beteiligt sind, erfahren davon.",
+ "cases.answer": "Entscheidung",
+ "cases.answerPlaceholder": "Was ab jetzt gilt",
+ "cases.reasoning": "Begründung",
+ "cases.reasoningPlaceholder": "Warum. Ohne Begründung lässt sich eine Entscheidung unten nicht anwenden",
+ "cases.decided": "Beschluss gespeichert.",
+ "cases.requestStatement": "Stellungnahme anfordern",
+ "cases.statementTitle": "Stellungnahme zu Vorgang {id} anfordern",
+ "cases.statementFrom": "Von",
+ "cases.question": "Frage",
+ "cases.questionPlaceholder": "Was musst du wissen?",
+ "cases.statementHint": "Der Agent bekommt eine Aufgabe mit genau dieser Frage und einer eigenen Frist. Der Vorgang bleibt bei dir.",
+ "cases.statementRequested": "Stellungnahme angefordert.",
+ "cases.scope": "Gilt für",
+ "cases.scopeAll": "die ganze Instanz",
+ "cases.decidedBy": "Entschieden von",
+ "cases.failed": "Fehlgeschlagen: {error}"
}
diff --git a/src/ClawdDotNet.Manager.Web/wwwroot/i18n/en.json b/src/ClawdDotNet.Manager.Web/wwwroot/i18n/en.json
index f6a6c0e..760f11e 100644
--- a/src/ClawdDotNet.Manager.Web/wwwroot/i18n/en.json
+++ b/src/ClawdDotNet.Manager.Web/wwwroot/i18n/en.json
@@ -222,6 +222,7 @@
"nav.group.system": "System",
"nav.instances": "Instances",
"nav.staging": "Approvals",
+ "nav.cases": "Cases",
"nav.logs": "Logs",
"nav.backup": "Backup",
"nav.settings": "Settings",
@@ -851,5 +852,65 @@
"desktop.dialog.addService": "Add Service",
"desktop.dialog.restoreBackup": "Restore Backup",
"desktop.dialog.license": "License Activation",
- "desktop.dialog.instancePicker": "Choose Instance"
+ "desktop.dialog.instancePicker": "Choose Instance",
+
+ "cases.title": "Cases",
+ "cases.refresh": "Refresh",
+ "cases.intro": "Escalations and conflicts the agents could not solve among themselves, and the decisions that closed them. A case nobody solves by its deadline comes here.",
+ "cases.tabOpen": "Open",
+ "cases.tabAll": "All",
+ "cases.tabDecisions": "Decisions",
+ "cases.none": "No open cases",
+ "cases.noneHint": "Nothing waits for a decision.",
+ "cases.noDecisions": "No decisions yet",
+ "cases.noDecisionsHint": "Every decided case leaves a decision here. It binds the area it was made for and is shown before anyone asks the same again.",
+ "cases.loadFailed": "Cases could not be loaded",
+ "cases.kindEscalation": "Escalation",
+ "cases.kindConflict": "Conflict",
+ "cases.status.open": "open",
+ "cases.status.decided": "decided",
+ "cases.status.handled": "handled",
+ "cases.overdue": "past deadline",
+ "cases.called": "human called",
+ "cases.id": "ID",
+ "cases.openedBy": "Opened by",
+ "cases.holder": "With",
+ "cases.human": "you (human)",
+ "cases.humanAuthor": "human",
+ "cases.deadline": "Deadline",
+ "cases.level": "Passed on",
+ "cases.parties": "Sides",
+ "cases.informed": "Told, not asked",
+ "cases.waitingFor": "Waiting for",
+ "cases.origin": "Arose from",
+ "cases.trail": "What each level said",
+ "cases.entry.opened": "opened",
+ "cases.entry.joined": "joined",
+ "cases.entry.forwarded": "passed on",
+ "cases.entry.position": "position",
+ "cases.entry.statement": "statement",
+ "cases.entry.statement-requested": "statement requested",
+ "cases.entry.called": "human called",
+ "cases.entry.decided": "decided",
+ "cases.entry.handled": "handled",
+ "cases.entry.note": "note",
+ "cases.decide": "Decide",
+ "cases.decideTitle": "Decide case {id}",
+ "cases.decideHint": "Your decision binds the whole instance and is shown to every agent that raises the same question. Everyone who raised it or is a side of it hears it.",
+ "cases.answer": "Decision",
+ "cases.answerPlaceholder": "What holds from now on",
+ "cases.reasoning": "Reasoning",
+ "cases.reasoningPlaceholder": "Why — without reasons a decision cannot be applied below",
+ "cases.decided": "Decision recorded.",
+ "cases.requestStatement": "Request statement",
+ "cases.statementTitle": "Request a statement for case {id}",
+ "cases.statementFrom": "From",
+ "cases.question": "Question",
+ "cases.questionPlaceholder": "What do you need to know?",
+ "cases.statementHint": "The agent gets a task with exactly this question and its own deadline. The case stays with you.",
+ "cases.statementRequested": "Statement requested.",
+ "cases.scope": "Binds",
+ "cases.scopeAll": "the whole instance",
+ "cases.decidedBy": "Decided by",
+ "cases.failed": "Failed: {error}"
}
diff --git a/src/ClawdDotNet.Manager.Web/wwwroot/index.html b/src/ClawdDotNet.Manager.Web/wwwroot/index.html
index 8526081..1ddccd9 100644
--- a/src/ClawdDotNet.Manager.Web/wwwroot/index.html
+++ b/src/ClawdDotNet.Manager.Web/wwwroot/index.html
@@ -124,6 +124,13 @@
Approvals0
+
+
+ Cases
+ 0
+