Files
Teamup/src/Modules/TeamUp.Modules.OrgBoard/Domain/WorkItem.cs
T
soroush.asadi d83ad87151 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>
2026-06-10 07:45:35 +03:30

74 lines
2.2 KiB
C#

using TeamUp.SharedKernel.Domain;
namespace TeamUp.Modules.OrgBoard.Domain;
/// <summary>A board task. Humans and AI share this one model — the assignee is a member or an agent.</summary>
internal sealed class WorkItem : Entity
{
public Guid TeamId { get; private set; }
public string Title { get; private set; } = null!;
public string? Description { get; private set; }
public WorkItemType Type { get; private set; }
public WorkItemStatus Status { get; private set; }
public AssigneeKind AssigneeKind { get; private set; }
public Guid? AssigneeId { get; private set; }
public Guid? ParentId { get; private set; }
public Guid CreatedByMemberId { get; private set; }
public DateTimeOffset CreatedAtUtc { get; private set; }
public DateTimeOffset UpdatedAtUtc { get; private set; }
private WorkItem()
{
}
public WorkItem(
Guid teamId,
string title,
string? description,
WorkItemType type,
Guid createdByMemberId,
DateTimeOffset nowUtc,
Guid? parentId = null)
{
TeamId = teamId;
Title = title;
Description = description;
Type = type;
Status = WorkItemStatus.Backlog;
AssigneeKind = AssigneeKind.Unassigned;
CreatedByMemberId = createdByMemberId;
ParentId = parentId;
CreatedAtUtc = nowUtc;
UpdatedAtUtc = nowUtc;
}
public void MoveTo(WorkItemStatus status, DateTimeOffset nowUtc)
{
Status = status;
UpdatedAtUtc = nowUtc;
}
public void AssignToMember(Guid memberId, DateTimeOffset nowUtc)
{
AssigneeKind = AssigneeKind.Member;
AssigneeId = memberId;
UpdatedAtUtc = nowUtc;
}
public void Unassign(DateTimeOffset nowUtc)
{
AssigneeKind = AssigneeKind.Unassigned;
AssigneeId = null;
UpdatedAtUtc = nowUtc;
}
/// <summary>Appends an approved agent artifact (spec / test plan) to the task.</summary>
public void AttachArtifact(string content, DateTimeOffset nowUtc)
{
Description = string.IsNullOrWhiteSpace(Description)
? content
: Description + "\n\n---\n\n" + content;
UpdatedAtUtc = nowUtc;
}
}