Files
orchestra/internal/domain/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

136 lines
5.0 KiB
Go

package domain
import (
"fmt"
"strings"
"time"
)
// EventPlanPhaseVerified records that Orchestra ran a plan phase's automated
// verification and what happened. The implementer never emits it: an agent may
// request verification, and only the plane can establish it.
const EventPlanPhaseVerified = "PlanPhaseVerified"
// PlanPhaseStatus is what Orchestra established about one phase.
type PlanPhaseStatus string
const (
// PlanPhaseInProgress is the default and the outcome of a failed run. It
// is never written by a request, only left in place by one.
PlanPhaseInProgress PlanPhaseStatus = "in_progress"
// PlanPhaseAwaitingManual means every automated check passed and manual
// steps remain. A human signs those off; the agent cannot.
PlanPhaseAwaitingManual PlanPhaseStatus = "awaiting_manual_verification"
// PlanPhaseVerified means nothing further is required for this phase.
PlanPhaseVerified PlanPhaseStatus = "verified"
)
func (s PlanPhaseStatus) Valid() bool {
switch s {
case PlanPhaseInProgress, PlanPhaseAwaitingManual, PlanPhaseVerified:
return true
}
return false
}
// PlanPhaseRequestStatus is the single value an implementer may write. Every
// other status is a conclusion Orchestra reaches, so allowing an agent to
// assert one would let it declare its own work verified.
const PlanPhaseRequestStatus = "ready_for_verification"
// PlanPhaseRecord is one verification run, bound to the plan it belongs to and
// the commit it ran against.
//
// Both bindings are load-bearing. Without PlanRef, a phase verified under plan
// A survives into plan B, which is the same class of bug as a review that
// outlives the commit it examined. Without AtSHA, "verified" outlives the code
// that made it true.
type PlanPhaseRecord struct {
PlanRef string `json:"plan_ref"`
PhaseID string `json:"phase_id"`
Status PlanPhaseStatus `json:"status"`
Commands [][]string `json:"commands,omitempty"`
ExitCodes []int `json:"exit_codes,omitempty"`
AtSHA string `json:"at_sha"`
// EvidenceRef is the CAS ref of the captured command output.
EvidenceRef string `json:"evidence_ref,omitempty"`
At time.Time `json:"at"`
}
// Stale reports whether the tree has moved since this phase was verified. A
// stale record is retained and labelled rather than discarded: it is still
// true that the phase passed at that commit, and hiding it would lose the
// provenance. What it must never do is read as current.
func (r PlanPhaseRecord) Stale(headSHA string) bool {
return headSHA != "" && r.AtSHA != "" && r.AtSHA != headSHA
}
// PlanProgress is the durable verification state for one accepted plan.
type PlanProgress struct {
// PlanRef is the plan these records belong to. A record from a superseded
// plan is never counted, so a replan cannot inherit progress it did not
// earn.
PlanRef string `json:"plan_ref"`
Phases []PlanPhaseRecord `json:"phases,omitempty"`
}
// PlanPhases returns the records that belong to the currently accepted plan.
// A task whose plan was superseded reports none, whatever the log still holds.
func (t Task) PlanPhases() []PlanPhaseRecord {
if t.PlanProgress == nil || t.PlanRef == "" || t.PlanProgress.PlanRef != t.PlanRef {
return nil
}
return t.PlanProgress.Phases
}
// PlanPhase returns the record for one phase of the accepted plan.
func (t Task) PlanPhase(id string) (PlanPhaseRecord, bool) {
for _, r := range t.PlanPhases() {
if r.PhaseID == id {
return r, true
}
}
return PlanPhaseRecord{}, false
}
// PlanPhaseSubject is the decision subject a manual sign-off carries. The key
// binds the approval to one phase of one plan, so a later "looks good" on an
// unrelated thread cannot satisfy a gate nobody was talking about.
func PlanPhaseSubject(planRef, phaseID string) string {
return "plan_phase_verification:" + planRef + ":" + phaseID
}
const maxVerificationCommands = 16
func ValidatePlanPhaseVerified(p map[string]any) error {
planRef, _ := p["plan_ref"].(string)
if strings.TrimSpace(planRef) == "" {
return fmt.Errorf("%w: plan_ref required", ErrInvalid)
}
phaseID, _ := p["phase_id"].(string)
if strings.TrimSpace(phaseID) == "" {
return fmt.Errorf("%w: phase_id required", ErrInvalid)
}
status, _ := p["status"].(string)
if !PlanPhaseStatus(status).Valid() {
return fmt.Errorf("%w: status %q is not a plan phase status", ErrInvalid, status)
}
if sha, _ := p["at_sha"].(string); len(sha) != 40 {
return fmt.Errorf("%w: at_sha must be a full commit sha", ErrInvalid)
}
codes, _ := p["exit_codes"].([]any)
if len(codes) > maxVerificationCommands {
return fmt.Errorf("%w: %d exit codes exceeds the %d command bound", ErrInvalid, len(codes), maxVerificationCommands)
}
// A verified phase whose commands failed would be a contradiction the
// reducer could not detect later.
if PlanPhaseStatus(status) != PlanPhaseInProgress {
for _, c := range codes {
if code, ok := c.(float64); !ok || code != 0 {
return fmt.Errorf("%w: status %q cannot carry a non-zero exit code", ErrInvalid, status)
}
}
}
return nil
}