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>
91 lines
2.9 KiB
Go
91 lines
2.9 KiB
Go
package domain
|
|
|
|
import "fmt"
|
|
|
|
// WorkPhase is the cognitive phase of a task. It is orthogonal to TaskState:
|
|
// a task can be leased in any phase, and a phase change is not a lifecycle
|
|
// transition. Keeping them separate is what stops a rotation from looking
|
|
// like progress and a failed experiment from looking like a failed task.
|
|
type WorkPhase string
|
|
|
|
const (
|
|
WorkPhaseFrame WorkPhase = "frame"
|
|
WorkPhaseResearch WorkPhase = "research"
|
|
WorkPhasePlan WorkPhase = "plan"
|
|
WorkPhaseImplement WorkPhase = "implement"
|
|
WorkPhaseReview WorkPhase = "review"
|
|
)
|
|
|
|
// EventWorkPhaseChanged is emitted by Orchestra, never by an agent. An agent
|
|
// asks for a phase change through the approval surface and Orchestra decides.
|
|
const EventWorkPhaseChanged = "WorkPhaseChanged"
|
|
|
|
func (p WorkPhase) Valid() bool {
|
|
switch p {
|
|
case WorkPhaseFrame, WorkPhaseResearch, WorkPhasePlan, WorkPhaseImplement, WorkPhaseReview:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// legalPhaseTransitions is the full set of moves Orchestra may make. A
|
|
// project's declared path is a subset of this, checked where the registry is
|
|
// visible. Skipping ahead is allowed, going backwards is not, except for
|
|
// review sending work back to implement.
|
|
var legalPhaseTransitions = map[WorkPhase][]WorkPhase{
|
|
WorkPhaseFrame: {WorkPhaseResearch, WorkPhaseImplement},
|
|
WorkPhaseResearch: {WorkPhasePlan, WorkPhaseImplement},
|
|
WorkPhasePlan: {WorkPhaseImplement},
|
|
WorkPhaseImplement: {WorkPhaseReview},
|
|
WorkPhaseReview: {WorkPhaseImplement},
|
|
}
|
|
|
|
// CanTransitionPhase reports whether Orchestra may move from one phase to
|
|
// another. An empty from is treated as frame, the phase every task starts in.
|
|
func CanTransitionPhase(from, to WorkPhase) bool {
|
|
if from == "" {
|
|
from = WorkPhaseFrame
|
|
}
|
|
if !from.Valid() || !to.Valid() {
|
|
return false
|
|
}
|
|
for _, allowed := range legalPhaseTransitions[from] {
|
|
if allowed == to {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ValidateWorkPhaseChanged checks the payload shape. Whether the transition
|
|
// is legal from the task's current phase is checked at the append boundary,
|
|
// where the current phase is visible.
|
|
func ValidateWorkPhaseChanged(p map[string]any) error {
|
|
phase, _ := p["phase"].(string)
|
|
if !WorkPhase(phase).Valid() {
|
|
return fmt.Errorf("%w: phase invalid", ErrInvalid)
|
|
}
|
|
if v, ok := p["from"]; ok {
|
|
s, ok := v.(string)
|
|
if !ok || !WorkPhase(s).Valid() {
|
|
return fmt.Errorf("%w: from invalid", ErrInvalid)
|
|
}
|
|
}
|
|
// A sealed artifact is what makes the next phase's context cheap. It is
|
|
// required when leaving research or plan, because those phases exist to
|
|
// produce one.
|
|
if v, ok := p["result_sha"]; ok {
|
|
s, ok := v.(string)
|
|
if !ok || len(s) != 40 {
|
|
return fmt.Errorf("%w: result_sha invalid", ErrInvalid)
|
|
}
|
|
}
|
|
if v, ok := p["artifact_ref"]; ok {
|
|
s, _ := v.(string)
|
|
if err := requiredHash(map[string]any{"artifact_ref": s}, "artifact_ref"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|