Doctrine: die Leitung steuert ihre Einheit, ohne jeden Auftragstext anzufassen (#698)

- SqliteDoctrineStore: je Einheit versioniert (Tabelle Doctrines: Text, Autor, Lauf, Grund),
  hoechstens 2000 Zeichen, leer entfernt; im Speicher gehalten fuer jeden Lauf
- OrgBriefing: die Doctrine der eigenen Einheit, nur eine Ebene tief, mit dem Satz,
  dass ein Widerspruch zur Aufgabe eskaliert wird; Leitungen erfahren doctrine_set
- Escalation: doctrine_read, doctrine_set (nur die Leitung, Grund Pflicht)
- IAgentTool.Vorgabe(operation, context): Einstufung mit dem Aufrufkontext; eine Leitung
  der Fremdinhalt-Stufe legt doctrine_set zur Freigabe vor
- API GET/PUT /organisation/units/{id}/doctrine; WebUI-Karte mit Verlauf und Markierung
  haeufiger Aenderungen; i18n en/de
- Tests: DoctrineTests (5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-10-01 09:38:44 +02:00
co-authored by Claude Opus 5.5
parent c608af1abb
commit 679333b382
15 changed files with 395 additions and 6 deletions
@@ -27,7 +27,9 @@ public static class OrgBriefing
/// <param name="atTop">#690: The agent is the deciding instance at the top — it gets that
/// section instead, with or without units.</param>
public static string? For(OrgChart org, string agentId, DateTime nowUtc, AgentTrust? trust = null, bool canEscalate = false, bool atTop = false)
/// <param name="doctrine">#698: The doctrine of a unit, if it has one.</param>
public static string? For(OrgChart org, string agentId, DateTime nowUtc, AgentTrust? trust = null, bool canEscalate = false, bool atTop = false,
Func<string, Doctrine?>? doctrine = null)
{
if (atTop && org.Agent(agentId) is not null)
return ForTop(org);
@@ -55,6 +57,14 @@ public static class OrgBriefing
if (!string.IsNullOrWhiteSpace(unit.Goal))
sb.AppendLine($"Goal of your unit: {unit.Goal.Trim()}");
// #698: Only the own unit's doctrine — one level deep, never the units above.
if (doctrine?.Invoke(unit.Id) is { IsEmpty: false } holds)
{
sb.AppendLine($"Doctrine of your unit — what holds here now (set by {holds.SetBy} on {holds.SetAt:yyyy-MM-dd}). " +
"It frames your tasks; if a task contradicts it, do not choose silently — escalate:");
sb.AppendLine(holds.Text.Trim());
}
}
var lead = org.EffectiveLeaderOf(agentId);
@@ -66,6 +76,9 @@ public static class OrgBriefing
if (org.LedBy(agentId) is { } led)
{
sb.AppendLine($"You lead the unit \"{led.Name}\". Delegate within it, and hand what goes beyond it to your own lead.");
if (canEscalate)
sb.AppendLine("You set your unit's doctrine (Escalation tool, doctrine_set): what holds in it now, short, changed seldom. " +
"It reaches the members of your unit only — sub-units have their own.");
// #687: What the unit lets its lead approve — told, so an approval task is no surprise.
if (led.Approvals is { Operations.Count: > 0 } approvals && canEscalate)
sb.AppendLine($"Your unit lets you approve, for the agents below you: {string.Join(", ", approvals.Operations)} " +