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
+54
View File
@@ -63,6 +63,60 @@ public sealed class UnitConfig
/// </summary>
[JsonPropertyName("inspection")]
public UnitInspection? Inspection { get; set; }
/// <summary>
/// #702: Operations that are put to a blind vote instead of the lead; <c>null</c> means none.
/// Set only by a human — a vote is a deliberate loosening, not a default.
/// </summary>
[JsonPropertyName("votes")]
public UnitVotes? Votes { get; set; }
}
/// <summary>
/// #702: A vote — several agents check independently and vote blind before something happens.
/// Not opinions are counted but independent checks enforced: every voter looks at its own data.
/// </summary>
public sealed class UnitVotes
{
/// <summary>The operations put to the vote: <c>Tool.operation</c>, or <c>Tool</c> for all of it.</summary>
[JsonPropertyName("operations")]
public List<string> Operations { get; set; } = [];
/// <summary>The agents that vote. Whoever proposes the call does not vote on it.</summary>
[JsonPropertyName("voters")]
public List<string> Voters { get; set; } = [];
/// <summary>Yes votes from which the call is approved.</summary>
[JsonPropertyName("approveAt")]
public int ApproveAt { get; set; } = 3;
/// <summary>At this many yes votes or fewer, the call is rejected. Between the two, a human decides.</summary>
[JsonPropertyName("rejectAt")]
public int RejectAt { get; set; } = 1;
/// <summary>Votes that must be cast for any result; 0 means <see cref="ApproveAt"/>. A missing vote is no yes.</summary>
[JsonPropertyName("quorum")]
public int Quorum { get; set; }
/// <summary>How long the voters have; after that, what was cast is counted.</summary>
[JsonPropertyName("hours")]
public double Hours { get; set; } = 4;
/// <summary>Votes a day for the unit — every vote costs a run per voter; beyond, the lead or a human decides.</summary>
[JsonPropertyName("perDay")]
public int PerDay { get; set; } = 10;
[JsonIgnore]
public TimeSpan Window => TimeSpan.FromHours(Math.Clamp(Hours, 0.25, 72));
[JsonIgnore]
public int EffectiveQuorum => Quorum > 0 ? Quorum : ApproveAt;
/// <summary>Whether the vote covers the operation.</summary>
public bool Covers(string tool, string? operation)
=> Operations.Any(o => string.Equals(o, tool, StringComparison.OrdinalIgnoreCase)
|| (!string.IsNullOrWhiteSpace(operation)
&& string.Equals(o, $"{tool}.{operation}", StringComparison.OrdinalIgnoreCase)));
}
/// <summary>