Files
orchestra/internal/operations/planprogress.go
T
kami a221502356 Let Orchestra establish plan progress instead of the implementer asserting it
A detailed plan that nothing enforces is a document. This makes the phases
executable: the implementer may write exactly one status, and every other
status is a conclusion Orchestra reaches by running the plan's own commands.

    agent may request:  ready_for_verification
    agent may not assert: verified, awaiting_manual_verification, failed, skipped

The worker resolves commands from the coordinator, never from the request, so a
request cannot smuggle in a command the planner did not write. They run as argv
through exec with Dir set to the worktree, which is the quality gate's existing
envelope and not a weaker one. There is no shell, so a pipe is a literal
argument.

Project policy decides executable reach. registry.Project.Verification matches
argv positionally, and an absent policy refuses everything: a plan command is
agent-authored, so inheriting the operator-authored gate's reach by default
would be the wrong direction to fail in. A refused command is refused before
anything runs, and the refusal names the project and the command so the planner
learns its real reach.

Two bindings make the record mean something later. PlanRef, so progress earned
under plan A cannot survive into plan B. AtSHA, so "verified" does not outlive
the code that made it true: a record whose commit has moved is retained as
provenance and rendered as stale, never as a claim about the current tree.
Both are the same failure this codebase already fixed for reviews, which bind
to the commit they examined.

Manual steps hold a phase at awaiting_manual_verification. The sign-off is an
ordinary human decision whose subject carries the plan ref and the phase id, so
a later "looks good" on an unrelated thread cannot satisfy a gate nobody was
discussing.

A plan sealed before plan.md declares no executable unit, and says so: the
implement context states that phase progress is unavailable and the work
continues under the old semantics. Inventing phases it never had would be worse
than admitting it has none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 11:59:39 +04:00

172 lines
6.5 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
}
if record.Status != domain.PlanPhaseInProgress && manuallySignedOff(s, t, phaseID) {
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,
}
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. The subject carries both, so a later "looks good"
// on an unrelated thread cannot satisfy a gate nobody was discussing.
func manuallySignedOff(s *store.Store, t domain.Task, phaseID string) bool {
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
}