Files
orchestra/internal/operations/review.go
T
kami 7f12c7fc37 v3 workflow: intent, phases, review, submission, enforcement, burn-in
The v3 stack, previously an uncommitted working tree, plus this session's two
units and the burn-in instrument. This commit is the burn-in build identity:
coordinator and worker must both report this revision before a task is created.

Workflow (earlier sessions, uncommitted until now): human decision events and
reduction, source cursors and reconcile-before-launch, turn-boundary
reconciliation, internal/agentctx as the single renderer, ace-fca phases with
sealed artifacts, the trajectory gate, bounded grilling, independent review,
task pr enforcement, and human review reflection.

Capability restrictions at the agent boundary: an authz.Agent surface at
GatedWrite may ask and may not act. It also fixes two bugs the unit exposed --
gated surfaces could not reach the two endpoints written for them, and
RequestHumanDecision would block an unowned task while rejecting a question
from the session that did own it.

Turn-boundary reconcile-failure escalation: a streak of consecutive failures
asks the session to hand off, fenced on the lease epoch, with reconcile_failure
as a real handoff reason. The worker was dropping the coordinator's verdict on
the floor; it now acts on it.

Burn-in: herdr.WriteLaunchContext dumps the exact agentctx.Build result to
<worktree>/.orchestra/launch.md at every launch, local and federated. BURNIN.md
is the runbook. deploy/build.sh stamps both binaries from one commit.

go build, go vet and go test ./... pass, 20 packages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 18:31:20 +04:00

123 lines
4.5 KiB
Go

package operations
import (
"encoding/json"
"errors"
"fmt"
"orchestra/internal/authz"
"orchestra/internal/domain"
"orchestra/internal/registry"
"orchestra/internal/review"
"orchestra/internal/store"
)
// ErrReviewNotEligible reports that the entry conditions for review are not
// met. It names which one, because "not eligible" alone sends an operator
// reading code.
var ErrReviewNotEligible = errors.New("not eligible for review")
// EnterReview checks the entry conditions and moves the task to the review
// phase, which is what makes the next session a reviewing session.
//
// The conditions exist so a reviewer is never handed an unfinished or
// unanchored change: reviewing a tree that nobody can reproduce produces
// findings nobody can act on.
func EnterReview(s *store.Store, project registry.Project, taskID string, ev review.Evidence) (domain.Event, error) {
t, ok := s.Task(taskID)
if !ok {
return domain.Event{}, domain.ErrNotFound
}
if current(t) != domain.WorkPhaseImplement {
return domain.Event{}, fmt.Errorf("%w: work phase is %s, not implement", ErrReviewNotEligible, current(t))
}
if t.State == domain.StateBlocked || t.State == domain.StateNeedsAttention {
return domain.Event{}, fmt.Errorf("%w: task is %s (%s)", ErrReviewNotEligible, t.State, t.BlockReason)
}
if t.DecisionRequest != nil {
return domain.Event{}, fmt.Errorf("%w: an unresolved human decision is outstanding", ErrReviewNotEligible)
}
if len(ev.ResultSHA) != 40 || len(ev.BaseSHA) != 40 {
return domain.Event{}, fmt.Errorf("%w: base and result commits must both be anchored", ErrReviewNotEligible)
}
if ev.Diff == "" {
return domain.Event{}, fmt.Errorf("%w: there is no diff to review", ErrReviewNotEligible)
}
if ev.GateCommand != "" && ev.GateExit != 0 {
return domain.Event{}, fmt.Errorf("%w: quality gate %q exited %d", ErrReviewNotEligible, ev.GateCommand, ev.GateExit)
}
if project.QualityGate != "" && ev.GateCommand == "" {
return domain.Event{}, fmt.Errorf("%w: project requires the quality gate to have run", ErrReviewNotEligible)
}
return advanceWorkPhase(s, project, taskID, nil, map[string]any{"result_sha": ev.ResultSHA})
}
// RecordReview seals a review against the exact commit it examined, then acts
// on it. Blocking findings return the task to implementation with the findings
// in hand. Minor findings are recorded and left alone.
//
// The reviewing session supplies findings and nothing else. It does not decide
// the phase, and it never edits code.
func RecordReview(s *store.Store, project registry.Project, taskID string, result review.Result) (domain.Event, error) {
if err := result.Validate(); err != nil {
return domain.Event{}, fmt.Errorf("%w: %s", domain.ErrInvalid, err)
}
t, ok := s.Task(taskID)
if !ok {
return domain.Event{}, domain.ErrNotFound
}
if current(t) != domain.WorkPhaseReview {
return domain.Event{}, fmt.Errorf("%w: work phase is %s, not review", domain.ErrInvalid, current(t))
}
// A review of a different commit is not a review of this work. Catching it
// here beats discovering it at completion, when the reviewing session is
// already gone.
if t.ReviewTargetSHA != "" && result.ResultSHA != t.ReviewTargetSHA {
return domain.Event{}, fmt.Errorf("%w: review is for %s but this phase was entered against %s", domain.ErrInvalid, result.ResultSHA, t.ReviewTargetSHA)
}
sealed, err := review.Encode(result)
if err != nil {
return domain.Event{}, err
}
ref, err := s.PutArtifact(sealed)
if err != nil {
return domain.Event{}, err
}
blocking := len(result.Blocking())
b, err := json.Marshal(map[string]any{
"artifact_ref": ref, "result_sha": result.ResultSHA, "blocking": blocking,
})
if err != nil {
return domain.Event{}, err
}
e := domain.Event{ID: domain.NewID(), Type: domain.EventReviewRecorded, TaskID: taskID, Version: t.Version + 1, Payload: b, Surface: string(authz.System)}
if err := s.Append(e); err != nil {
return domain.Event{}, err
}
if blocking == 0 {
return e, nil
}
// Back to implementation, with the findings as the reason.
if _, err := AdvanceWorkPhase(s, project, taskID, nil); err != nil {
return e, err
}
return e, nil
}
// TaskReview loads the sealed findings for a task, for the implementation
// context that has to act on them.
func TaskReview(s *store.Store, t domain.Task) (*review.Result, error) {
if t.Review == nil {
return nil, nil
}
b, err := s.Artifact(t.Review.ArtifactRef)
if err != nil {
return nil, err
}
r, err := review.Decode(b)
if err != nil {
return nil, err
}
return &r, nil
}