Abstimmung: mehrere Agenten pruefen unabhaengig und stimmen blind ab (#702)

A unit can put operations to a blind vote instead of its lead. Not opinions
are counted but independent checks enforced.

- UnitConfig.votes: operations, voters, approveAt, rejectAt, quorum, hours,
  perDay - set only by a human (OrgAdministration.SetVotes).
- Core/Staging/Votes: routes a covered staged call before the lead; one task
  per voter with the frozen call and nothing else; cast_vote needs reasoning
  and evidence. Counted as soon as the result is settled; in between and
  without the quorum a human decides. The proposer does not vote, voters that
  read foreign content get no ballot, changes to agents never go to a vote.
- Escalation tool: list_ballots, cast_vote.
- Approvals view shows each vote with every ballot and the voters' costs;
  organisation view sets the rule and lists the votes held.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-10-01 12:04:33 +02:00
co-authored by Claude Opus 5.5
parent d834e5ac38
commit a5dd503d9b
23 changed files with 1238 additions and 11 deletions
+18 -2
View File
@@ -653,7 +653,7 @@ public sealed class AppHost : IAsyncDisposable
var (scanner, staging, board) = BuildTaskboard(
instance, taskRepository, engine, tools, stateStore, audit,
stagingRepository, dauerfreigaben, stagingGate, cases, loggerFactory, logger);
stagingRepository, dauerfreigaben, stagingGate, cases, storage, loggerFactory, logger);
var host = new AppHost
{
@@ -732,6 +732,13 @@ public sealed class AppHost : IAsyncDisposable
spotChecks.Start();
host._shutdown.Add(spotChecks.DisposeAsync);
// #702: votes whose time is up are counted without waiting for the next ballot.
if (engine.Votes is Core.Staging.Votes votes)
{
votes.Start();
host._shutdown.Add(votes.DisposeAsync);
}
// #699: costs per unit, and what changed in them.
var costWatch = new Core.Accounting.CostWatch(
storage, usage, () => Core.Organisation.OrgChart.From(instance), () => instance.CostWatch, board,
@@ -838,6 +845,7 @@ public sealed class AppHost : IAsyncDisposable
SqliteDauerfreigabeRepository dauerfreigaben,
StagingGate stagingGate,
Core.Cases.ICaseRepository cases,
SqliteStorage storage,
ILoggerFactory loggerFactory,
ILogger logger)
{
@@ -858,9 +866,17 @@ public sealed class AppHost : IAsyncDisposable
stagingRepository, board, () => engine.OrganisationProvider?.Invoke(),
agent => Core.Organisation.ReportingLine.TrustOf(agent, tools.Get),
loggerFactory.CreateLogger("ClawdDotNet.Core.Staging.Leads")) { Service = staging };
stagingGate.Route = leadApprovals.RouteAsync;
engine.LeadApprovals = leadApprovals;
// #702: What a unit puts to a blind vote goes there first; otherwise to the lead.
var votes = new Votes(
storage, stagingRepository, board, () => engine.OrganisationProvider?.Invoke(),
agent => Core.Organisation.ReportingLine.TrustOf(agent, tools.Get),
loggerFactory.CreateLogger("ClawdDotNet.Core.Staging.Votes")) { Service = staging, Audit = audit };
stagingGate.Route = async (call, ct) => await votes.RouteAsync(call, ct) ?? await leadApprovals.RouteAsync(call, ct);
engine.Votes = votes;
var dispatcher = new EngineTaskDispatcher(
engine, () => instance.Agents, instance.InstanceId, tools, stateStore, loggerFactory,
tasks: taskRepository);