d83ad87151
SharedKernel:
- ActionRisk (risk lives on the action) + GatePolicy (the pure autonomy x risk matrix:
Read never holds, Draft/Publish hold unless Autonomous, Destructive ALWAYS holds).
- IActionGate (AgentActionProposal -> execute|hold) and IBoardWriter.AttachArtifactAsync.
Governance:
- ReviewItem (held action: artifact, child titles, trace, decision, edit distance) in a new
review_items table (AddReviewItems migration).
- ActionGate: hold -> ReviewItem + "action.held" audit; autonomous -> execute + audit.
- HeldActionExecutor: writes the artifact onto the task and creates the child tasks via
IBoardWriter (implemented by OrgBoard — no cross-module table access).
- Review inbox API: GET /api/governance/reviews (scope-filtered to where the caller may
approve), POST /reviews/{id}/approve (optional edited content/children -> normalized
edit distance recorded — the north-star metric), POST /reviews/{id}/sendback. Deciding
twice is 409; Members are 403.
Assembler:
- OutputParser (numbered-list child titles, conservative) and the executor now hands every
completed run's proposal to the gate.
OrgBoard: WorkItem.AttachArtifact + BoardWriter.AttachArtifactAsync.
Verified: build green; ArchitectureTests 8/8; IntegrationTests 41/41 incl. the full M5
acceptance — Aria (gated) proposes a spec, it waits in the inbox with its trace, a Member is
403'd, the owner edits-and-approves, the spec + four child stories land on the board, edit
distance > 0 is recorded and audited; Quill (autonomous) executes straight to the board;
destructive holds even for an autonomous seat and can be sent back. Plus the GatePolicy matrix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
27 lines
1.2 KiB
C#
27 lines
1.2 KiB
C#
using TeamUp.SharedKernel.Access;
|
|
using Xunit;
|
|
|
|
namespace TeamUp.IntegrationTests;
|
|
|
|
/// <summary>The gate decision matrix — pure policy, no database.</summary>
|
|
public sealed class GatePolicyTests
|
|
{
|
|
[Theory]
|
|
// Read never holds.
|
|
[InlineData(Autonomy.DraftOnly, ActionRisk.Read, false)]
|
|
[InlineData(Autonomy.Gated, ActionRisk.Read, false)]
|
|
[InlineData(Autonomy.Autonomous, ActionRisk.Read, false)]
|
|
// Draft/Publish hold unless the seat is Autonomous.
|
|
[InlineData(Autonomy.DraftOnly, ActionRisk.Draft, true)]
|
|
[InlineData(Autonomy.Gated, ActionRisk.Draft, true)]
|
|
[InlineData(Autonomy.Autonomous, ActionRisk.Draft, false)]
|
|
[InlineData(Autonomy.Gated, ActionRisk.Publish, true)]
|
|
[InlineData(Autonomy.Autonomous, ActionRisk.Publish, false)]
|
|
// Destructive ALWAYS holds — whatever the autonomy. The backstop.
|
|
[InlineData(Autonomy.DraftOnly, ActionRisk.Destructive, true)]
|
|
[InlineData(Autonomy.Gated, ActionRisk.Destructive, true)]
|
|
[InlineData(Autonomy.Autonomous, ActionRisk.Destructive, true)]
|
|
public void Gate_matrix(Autonomy autonomy, ActionRisk risk, bool shouldHold) =>
|
|
Assert.Equal(shouldHold, GatePolicy.ShouldHold(autonomy, risk));
|
|
}
|