7f12c7fc37
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>
123 lines
4.5 KiB
Go
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
|
|
}
|