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
@@ -308,6 +308,28 @@ public sealed class OrgAdministrationTests : IDisposable
Unit("marketing").Inspection.ShouldBeNull();
}
// ═══════════════════ votes (#702) ═══════════════════
[Fact]
public void A_human_puts_operations_to_a_vote_with_room_for_a_human_between_the_thresholds()
{
BuildMarketing();
Org.SetVotes("marketing", ["AgentEditor.update_soul"], ["ads", "listing"]).Message.ShouldContain("always stays with a human");
Org.SetVotes("marketing", ["Database.insert"], ["ads"]).Ok.ShouldBeFalse("one voter is a lead's approval");
Org.SetVotes("marketing", ["Database.insert"], ["ads", "nobody"]).Message.ShouldContain("nobody");
Org.SetVotes("marketing", ["Database.insert"], ["ads", "listing", "specialist"], approveAt: 2, rejectAt: 2).Ok
.ShouldBeFalse("rejection must lie below approval");
var set = Org.SetVotes("marketing", ["Database.insert"], ["ads", "listing", "specialist"], approveAt: 2, rejectAt: 0, reason: "orders");
set.Ok.ShouldBeTrue(set.Message);
Unit("marketing").Votes!.Voters.ShouldBe(["ads", "listing", "specialist"]);
Unit("marketing").Votes!.EffectiveQuorum.ShouldBe(2);
Org.SetVotes("marketing", [], null).Ok.ShouldBeTrue();
Unit("marketing").Votes.ShouldBeNull();
}
// ═══════════════════ the top (#690) ═══════════════════
private void MakeDecider(string id)