Files
orchestra/internal/operations/planprogress.go
T
kami b5c37f693b Bind a manual sign-off to the tree it was given against
F63, found live on run 19. A manual check on these projects is a human
reading what the code prints. RecordPlanPhaseVerification asked only
whether a sign-off for that plan and phase existed, and one exists forever,
so rerunning a phase's automated checks at a new commit carried the human
half along with it. The rig proved it twice: two operator commits and two
re-verification requests, each coming back verified without anyone looking.

The reducer now records which tree the human confirmed, the record carries
it forward as provenance, and a run whose commit does not match it waits
for the human again. A sign-off given before any run has no confirmed tree
and still counts, so the ordinary ordering is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-29 19:41:50 +04:00

188 lines
7.3 KiB
Go

package operations
import (
"encoding/json"
"fmt"
"strings"
"time"
"orchestra/internal/authz"
"orchestra/internal/domain"
"orchestra/internal/registry"
"orchestra/internal/store"
"orchestra/internal/workphase"
)
// ErrPlanPhase reports that a verification request cannot proceed. The reason
// is always specific, because it is delivered to a live implementer that has
// to act on it.
var ErrPlanPhase = fmt.Errorf("plan phase verification refused")
// VerificationRun is the outcome of one command the worker executed.
type VerificationRun struct {
Command []string `json:"command"`
ExitCode int `json:"exit_code"`
Output string `json:"output,omitempty"`
}
// PlanPhaseCommands resolves the commands a phase's verification will run.
//
// The commands come from the accepted plan, never from the request. An
// implementer asks to verify a phase; what that phase is checked with was
// settled when the plan was sealed, so a request cannot smuggle in a command
// the planner did not write.
//
// Every command is checked against the project's policy before any of them
// runs. A partial execution followed by a refusal would leave side effects
// behind with nothing recording them.
func PlanPhaseCommands(s *store.Store, project registry.Project, taskID, phaseID string) (workphase.PlanPhase, error) {
t, ok := s.Task(taskID)
if !ok {
return workphase.PlanPhase{}, domain.ErrNotFound
}
if t.PlanRef == "" {
return workphase.PlanPhase{}, fmt.Errorf("%w: this task has no accepted plan", ErrPlanPhase)
}
raw, err := s.Artifact(t.PlanRef)
if err != nil {
return workphase.PlanPhase{}, fmt.Errorf("read accepted plan: %w", err)
}
doc, err := workphase.DecodeStoredPlan(raw)
if err != nil {
return workphase.PlanPhase{}, fmt.Errorf("read accepted plan: %w", err)
}
if len(doc.Phases) == 0 {
// A plan sealed before plan.md names no executable unit. Saying so is
// the honest answer; inventing a phase would make progress against a
// plan that never had any.
return workphase.PlanPhase{}, fmt.Errorf("%w: the accepted plan predates plan.md and declares no phases, so phase progress does not apply to it", ErrPlanPhase)
}
phase, ok := doc.Phase(phaseID)
if !ok {
return workphase.PlanPhase{}, fmt.Errorf("%w: the accepted plan has no %s; it has %s", ErrPlanPhase, phaseID, phaseNames(doc))
}
for _, argv := range phase.Automated {
if allowed, why := project.Verification.Allows(argv); !allowed {
// Refused before anything ran. The planner wrote a command the
// project does not permit, and the implementer is told rather
// than left retrying a phase that can never verify.
return workphase.PlanPhase{}, fmt.Errorf("%w: %s declares a command %s cannot run: %s", ErrPlanPhase, phaseID, project.ID, why)
}
}
return phase, nil
}
func phaseNames(doc workphase.PlanDoc) string {
out := make([]string, 0, len(doc.Phases))
for _, p := range doc.Phases {
out = append(out, p.ID)
}
return strings.Join(out, ", ")
}
// RecordPlanPhaseVerification establishes what the runs prove, and appends the
// durable record.
//
// The status is derived here and never taken from the caller. An implementer
// may request verification; only this function decides whether a phase is
// verified, awaiting a human, or still in progress.
func RecordPlanPhaseVerification(s *store.Store, project registry.Project, taskID, phaseID, atSHA string, runs []VerificationRun) (domain.Event, error) {
t, ok := s.Task(taskID)
if !ok {
return domain.Event{}, domain.ErrNotFound
}
if len(atSHA) != 40 {
return domain.Event{}, fmt.Errorf("%w: verification must name the commit it ran against", ErrPlanPhase)
}
phase, err := PlanPhaseCommands(s, project, taskID, phaseID)
if err != nil {
return domain.Event{}, err
}
if len(runs) != len(phase.Automated) {
return domain.Event{}, fmt.Errorf("%w: %s declares %d automated commands but %d results were reported", ErrPlanPhase, phaseID, len(phase.Automated), len(runs))
}
record := domain.PlanPhaseRecord{
PlanRef: t.PlanRef, PhaseID: phaseID, AtSHA: atSHA,
Status: domain.PlanPhaseVerified, At: time.Now().UTC(),
}
for i, r := range runs {
// The result must describe the command the plan named. A reordered or
// substituted result would attribute one command's exit code to
// another.
if strings.Join(r.Command, "\x00") != strings.Join(phase.Automated[i], "\x00") {
return domain.Event{}, fmt.Errorf("%w: result %d reports %q but the plan declares %q", ErrPlanPhase, i, strings.Join(r.Command, " "), strings.Join(phase.Automated[i], " "))
}
record.Commands = append(record.Commands, r.Command)
record.ExitCodes = append(record.ExitCodes, r.ExitCode)
if r.ExitCode != 0 {
record.Status = domain.PlanPhaseInProgress
}
}
if record.Status == domain.PlanPhaseVerified && len(phase.Manual) > 0 {
// Automated checks passing is not the whole phase. A human owns the
// manual steps, and the phase waits rather than claiming more than
// was established.
record.Status = domain.PlanPhaseAwaitingManual
}
// Carry the confirmed tree forward as provenance. Without it a second
// rerun would compare against nothing and re-inherit the sign-off.
if prior, ok := t.PlanPhase(phaseID); ok {
record.ManualAtSHA = prior.ManualAtSHA
}
if record.Status != domain.PlanPhaseInProgress && manuallySignedOff(s, t, phaseID, record) {
record.Status = domain.PlanPhaseVerified
}
if ref, err := s.PutArtifact(verificationEvidence(runs)); err == nil {
record.EvidenceRef = ref
} else {
return domain.Event{}, fmt.Errorf("store verification evidence: %w", err)
}
payload := map[string]any{
"plan_ref": record.PlanRef, "phase_id": record.PhaseID, "status": string(record.Status),
"commands": record.Commands, "exit_codes": record.ExitCodes, "at_sha": record.AtSHA,
"evidence_ref": record.EvidenceRef, "at": record.At, "manual_at_sha": record.ManualAtSHA,
}
if t.Lease != nil {
payload["harness_id"], payload["lease_epoch"] = t.Lease.HarnessID, t.Lease.Epoch
}
b, err := json.Marshal(payload)
if err != nil {
return domain.Event{}, err
}
e := domain.Event{ID: domain.NewID(), Type: domain.EventPlanPhaseVerified, TaskID: taskID, Version: t.Version + 1, Payload: b, Surface: string(authz.System)}
return e, s.Append(e)
}
// manuallySignedOff reports whether a human has already approved this exact
// phase of this exact plan, against the tree this run examined. The subject
// carries plan and phase, so a later "looks good" on an unrelated thread
// cannot satisfy a gate nobody was discussing.
//
// The tree matters as much as the subject (F63). A sign-off is a human saying
// they read what this code prints; an edit afterwards can change exactly that.
// A record whose ManualAtSHA names a different commit is therefore not signed
// off, and waits for the human again. A sign-off given before any run has no
// confirmed tree to compare against and still counts, which keeps the ordinary
// ordering unchanged.
func manuallySignedOff(s *store.Store, t domain.Task, phaseID string, record domain.PlanPhaseRecord) bool {
if record.ManualAtSHA != "" && record.ManualAtSHA != record.AtSHA {
return false
}
intent, err := s.EffectiveIntent(t.ID)
if err != nil {
return false
}
subject := domain.PlanPhaseSubject(t.PlanRef, phaseID)
for _, d := range intent.Decisions {
if d.Subject == subject {
return true
}
}
return false
}
func verificationEvidence(runs []VerificationRun) []byte {
b, _ := json.Marshal(runs)
return b
}