Entscheidungsinstanz an der Spitze: Mensch, Agent oder Mensch zuerst mit Frist (#690)

- cases.top in instance.json: Human | Agent | HumanFirst, agentId, humanFirstHours
- TopRole: der entscheidende Agent steht in keiner Einheit, liest keine Fremdinhalte
  und hat nur Taskboard, Escalation, Memory, FileRW, AgentComm; geprueft beim Setzen,
  bei jeder Zustellung und vor jeder Entscheidung
- CaseDesk: was bisher an der Spitze beim Menschen landete, geht nach Einstellung an
  den Agenten; Tagesdeckel und Frist gehen weiter an den Menschen. handle verwehrt,
  forward geht an den Menschen. Mensch zuerst: Uebergabe nach Ablauf, eigene Frist.
  Der Mensch kann einen Fall des Agenten selbst entscheiden.
- Beschluesse des Agenten: ByTopAgent, zum Lesen vorgelegt; jeder Beschluss laesst
  sich mit Begruendung zuruecknehmen und bindet dann niemanden mehr
- Briefing fuer den Agenten an der Spitze, auch ohne Einheiten
- OrgAdministration.SetTop mit Audit; Werkzeugsperre und Verschiebesperre
- WebUI Vorgaenge: Reiter Spitze, Lesen/Zuruecknehmen, Zaehler; i18n en/de
- Tests: TopRoleTests (15), OrgAdministrationTests (+2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-29 09:27:26 +02:00
co-authored by Claude Opus 5.5
parent e22edb418a
commit e8964e3c74
16 changed files with 1020 additions and 26 deletions
+64
View File
@@ -0,0 +1,64 @@
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Organisation;
namespace ClawdDotNet.Core.Cases;
/// <summary>
/// #690: The deciding instance at the top of the tree (Organisation-Konzept, section 6).
///
/// <para><b>A role, not a person.</b> A human, an agent, or the human first with the agent
/// behind — otherwise the kit would fix one picture of a company.</para>
///
/// <para><b>What a deciding agent may not do matters more than what it may.</b> It decides
/// questions and does not carry them out; it has no tool with an effect outside; it does not
/// change the structure. Otherwise the top of the organisation is its largest hole — a model
/// with full power and nobody opposite. So it holds only the tools of <see cref="AllowedTools"/>
/// and stands outside the units; an agent that does not fit is not given a case — the human is.</para>
///
/// <para><b>It makes the organisation faster, not more right.</b> Its data comes from the same
/// instance, with the same possible errors. Every decision it takes is therefore presented to
/// the human to read (<see cref="Decision.ByTopAgent"/>), and the deadline holds for it too.</para>
/// </summary>
public static class TopRole
{
/// <summary>
/// What a deciding agent may hold: tasks for the units that carry a decision out, its own
/// case tool, memory, files inside the instance, and talking to agents. Nothing with an
/// effect outside (Mail, Telegram, social media, FTP, APIs), nothing that reads foreign
/// content into the top, no database, nothing that creates or edits agents.
/// </summary>
public static readonly IReadOnlySet<string> AllowedTools = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Taskboard", CaseDesk.ToolName, "Memory", "FileRW", "AgentComm"
};
/// <summary>
/// Why this agent cannot take the top, or <c>null</c>. Checked when it is set and again
/// every time a case would go to it — the configuration may have changed in between.
/// </summary>
public static string? Unfit(AgentConfig? agent, string agentId)
{
if (agent is null)
return $"There is no agent '{agentId}'.";
if (!string.IsNullOrWhiteSpace(agent.UnitId))
return $"'{agent.AgentId}' belongs to a unit. The deciding agent stands above all units, not in one — " +
"move it to the top level first.";
if (agent.TreatAsForeignReader)
return $"'{agent.AgentId}' is set to read content from outside. Whoever decides for the whole instance must not.";
var beyond = agent.Tools.Keys.Where(t => !AllowedTools.Contains(t)).OrderBy(t => t, StringComparer.OrdinalIgnoreCase).ToList();
if (beyond.Count > 0)
return $"'{agent.AgentId}' holds tools a deciding agent may not have: {string.Join(", ", beyond)}. " +
$"It decides, it does not carry out — allowed are {string.Join(", ", AllowedTools.Order(StringComparer.OrdinalIgnoreCase))}.";
return null;
}
/// <summary>The deciding agent, if the settings name one and it fits; otherwise <c>null</c> and why.</summary>
public static (AgentConfig? Agent, string? Problem) Resolve(TopSettings top, OrgChart? org)
{
if (!top.UsesAgent)
return (null, null);
var agent = org?.Agent(top.AgentId!.Trim());
var problem = Unfit(agent, top.AgentId!.Trim());
return problem is null ? (agent, null) : (null, problem);
}
}