M5: action gate + review inbox — edit distance captured for real
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>
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
namespace TeamUp.SharedKernel.Access;
|
||||
|
||||
/// <summary>
|
||||
/// Risk lives on the action, not the agent. The action gate (Governance, M5) compares it to the
|
||||
/// seat's autonomy to decide execute-vs-hold. Destructive always holds for a human.
|
||||
/// </summary>
|
||||
public enum ActionRisk
|
||||
{
|
||||
Read,
|
||||
Draft,
|
||||
Publish,
|
||||
Destructive,
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace TeamUp.SharedKernel.Access;
|
||||
|
||||
/// <summary>
|
||||
/// The action-gate decision matrix: seat autonomy × action risk → execute or hold. Pure policy —
|
||||
/// persistence/execution live in Governance. Destructive ALWAYS holds for a human, whatever the
|
||||
/// autonomy (the prompt-injection backstop). Read-level actions never hold. Draft/Publish execute
|
||||
/// only on an Autonomous seat; DraftOnly and Gated behave alike on V1's internal action set (the
|
||||
/// distinction becomes meaningful with per-agent MCP tool-calls in Phase 1).
|
||||
/// </summary>
|
||||
public static class GatePolicy
|
||||
{
|
||||
public static bool ShouldHold(Autonomy autonomy, ActionRisk risk) => risk switch
|
||||
{
|
||||
ActionRisk.Read => false,
|
||||
ActionRisk.Destructive => true,
|
||||
_ => autonomy != Autonomy.Autonomous,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using TeamUp.SharedKernel.Access;
|
||||
|
||||
namespace TeamUp.SharedKernel.Ai;
|
||||
|
||||
/// <summary>An agent's proposed action coming off a completed run, handed to the action gate.</summary>
|
||||
public sealed record AgentActionProposal(
|
||||
Guid AgentRunId,
|
||||
Guid SeatId,
|
||||
Guid AgentId,
|
||||
Guid WorkItemId,
|
||||
Guid TeamId,
|
||||
Guid OrganizationId,
|
||||
Autonomy Autonomy,
|
||||
string ActionKind,
|
||||
string Risk,
|
||||
string Title,
|
||||
string Content,
|
||||
IReadOnlyList<string> ChildTitles,
|
||||
string? Trace);
|
||||
|
||||
public enum GateOutcome
|
||||
{
|
||||
Executed,
|
||||
Held,
|
||||
}
|
||||
|
||||
public sealed record GateResult(GateOutcome Outcome, Guid? ReviewItemId);
|
||||
|
||||
/// <summary>
|
||||
/// The action gate: autonomy vs risk → execute now or hold for review. Implemented by Governance;
|
||||
/// called by the assembler when a run completes. Destructive always holds.
|
||||
/// </summary>
|
||||
public interface IActionGate
|
||||
{
|
||||
Task<GateResult> EvaluateAsync(AgentActionProposal proposal, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace TeamUp.SharedKernel.Board;
|
||||
|
||||
public sealed record ChildTaskSpec(string Title, string Type);
|
||||
|
||||
/// <summary>
|
||||
/// Lets Governance execute an approved agent action onto the board (create the child tasks)
|
||||
/// without referencing OrgBoard's tables. Implemented by OrgBoard.
|
||||
/// </summary>
|
||||
public interface IBoardWriter
|
||||
{
|
||||
Task<int> CreateChildTasksAsync(
|
||||
Guid teamId,
|
||||
Guid parentWorkItemId,
|
||||
IReadOnlyList<ChildTaskSpec> children,
|
||||
Guid? createdByMemberId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Writes an approved artifact (spec / test plan) onto the work item.</summary>
|
||||
Task AttachArtifactAsync(Guid workItemId, string content, CancellationToken cancellationToken = default);
|
||||
}
|
||||
Reference in New Issue
Block a user