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>
195 lines
5.6 KiB
Go
195 lines
5.6 KiB
Go
// Package workphase holds the sealed output of a cognitive phase.
|
|
//
|
|
// A phase artifact is what survives a phase boundary. The conversation that
|
|
// produced it does not: the next phase starts from the sealed artifact, which
|
|
// is the whole point of separating research from planning from implementation.
|
|
//
|
|
// Implementation state is deliberately absent here. It already has a format,
|
|
// continuity.Handoff, and a third one would be a third thing to keep in sync.
|
|
package workphase
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Finding is one thing research established, with the evidence for it.
|
|
type Finding struct {
|
|
Claim string `json:"claim"`
|
|
Evidence string `json:"evidence"`
|
|
}
|
|
|
|
// CodePath is a location the next phase will need, and why.
|
|
type CodePath struct {
|
|
Path string `json:"path"`
|
|
Why string `json:"why"`
|
|
}
|
|
|
|
type DeadEnd struct {
|
|
Tried string `json:"tried"`
|
|
WhyFailed string `json:"why_failed"`
|
|
}
|
|
|
|
// Research is the sealed result of a research phase. It is bounded on
|
|
// purpose: an unbounded research artifact is a transcript with extra steps.
|
|
type Research struct {
|
|
Findings []Finding `json:"findings"`
|
|
Code []CodePath `json:"relevant_code,omitempty"`
|
|
Invariants []string `json:"invariants,omitempty"`
|
|
DeadEnds []DeadEnd `json:"dead_ends,omitempty"`
|
|
Unknowns []string `json:"unknowns,omitempty"`
|
|
}
|
|
|
|
// Change is one intended modification. Target names what changes, Intent says
|
|
// what it should do afterwards. Neither is a diff: a plan that carries the
|
|
// patch is an implementation, and reviewing it costs what reviewing code costs.
|
|
type Change struct {
|
|
Target string `json:"target"`
|
|
Intent string `json:"intent"`
|
|
}
|
|
|
|
// Plan is the sealed result of a planning phase.
|
|
type Plan struct {
|
|
Changes []Change `json:"changes"`
|
|
Verification []string `json:"verification,omitempty"`
|
|
Risks []string `json:"risks,omitempty"`
|
|
DecisionsNeeded []string `json:"human_decisions_needed,omitempty"`
|
|
}
|
|
|
|
const maxItems = 64
|
|
const maxLine = 500
|
|
|
|
func (r Research) Validate() error {
|
|
if len(r.Findings) == 0 {
|
|
return fmt.Errorf("research: at least one finding is required")
|
|
}
|
|
if err := bound("findings", len(r.Findings)); err != nil {
|
|
return err
|
|
}
|
|
for i, f := range r.Findings {
|
|
if err := line(fmt.Sprintf("findings[%d].claim", i), f.Claim, true); err != nil {
|
|
return err
|
|
}
|
|
if err := line(fmt.Sprintf("findings[%d].evidence", i), f.Evidence, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for i, c := range r.Code {
|
|
if err := line(fmt.Sprintf("relevant_code[%d].path", i), c.Path, true); err != nil {
|
|
return err
|
|
}
|
|
if strings.HasPrefix(c.Path, "/") {
|
|
return fmt.Errorf("research: relevant_code[%d].path must be repository-relative", i)
|
|
}
|
|
if err := line(fmt.Sprintf("relevant_code[%d].why", i), c.Why, false); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for i, d := range r.DeadEnds {
|
|
if err := line(fmt.Sprintf("dead_ends[%d].tried", i), d.Tried, true); err != nil {
|
|
return err
|
|
}
|
|
if err := line(fmt.Sprintf("dead_ends[%d].why_failed", i), d.WhyFailed, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := bound("relevant_code", len(r.Code)); err != nil {
|
|
return err
|
|
}
|
|
if err := bound("dead_ends", len(r.DeadEnds)); err != nil {
|
|
return err
|
|
}
|
|
return lists(map[string][]string{"invariants": r.Invariants, "unknowns": r.Unknowns})
|
|
}
|
|
|
|
func (p Plan) Validate() error {
|
|
if len(p.Changes) == 0 {
|
|
return fmt.Errorf("plan: at least one change is required")
|
|
}
|
|
if err := bound("changes", len(p.Changes)); err != nil {
|
|
return err
|
|
}
|
|
for i, c := range p.Changes {
|
|
if err := line(fmt.Sprintf("changes[%d].target", i), c.Target, true); err != nil {
|
|
return err
|
|
}
|
|
if err := line(fmt.Sprintf("changes[%d].intent", i), c.Intent, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return lists(map[string][]string{"verification": p.Verification, "risks": p.Risks, "human_decisions_needed": p.DecisionsNeeded})
|
|
}
|
|
|
|
func Encode(v interface{ Validate() error }) ([]byte, error) {
|
|
if err := v.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
return json.Marshal(v)
|
|
}
|
|
|
|
func DecodeResearch(b []byte) (Research, error) {
|
|
var r Research
|
|
if err := json.Unmarshal(b, &r); err != nil {
|
|
return Research{}, fmt.Errorf("research artifact: %w", err)
|
|
}
|
|
return r, r.Validate()
|
|
}
|
|
|
|
func DecodePlan(b []byte) (Plan, error) {
|
|
var p Plan
|
|
if err := json.Unmarshal(b, &p); err != nil {
|
|
return Plan{}, fmt.Errorf("plan artifact: %w", err)
|
|
}
|
|
return p, p.Validate()
|
|
}
|
|
|
|
func bound(field string, n int) error {
|
|
if n > maxItems {
|
|
return fmt.Errorf("%s: %d entries exceeds the %d item bound", field, n, maxItems)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// line rejects a value that is empty when required, over-long, or
|
|
// multi-line. A phase artifact is a set of short claims, not prose: the bound
|
|
// is what keeps a sealed artifact cheaper to read than the session that
|
|
// produced it.
|
|
func line(field, v string, required bool) error {
|
|
s := strings.TrimSpace(v)
|
|
if s == "" {
|
|
if required {
|
|
return fmt.Errorf("%s is required", field)
|
|
}
|
|
return nil
|
|
}
|
|
if len(s) > maxLine {
|
|
return fmt.Errorf("%s: %d characters exceeds the %d character bound", field, len(s), maxLine)
|
|
}
|
|
if strings.ContainsAny(s, "\n\r") {
|
|
return fmt.Errorf("%s must be a single line", field)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func lists(fields map[string][]string) error {
|
|
names := make([]string, 0, len(fields))
|
|
for name := range fields {
|
|
names = append(names, name)
|
|
}
|
|
// Deterministic error for the same input.
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
if err := bound(name, len(fields[name])); err != nil {
|
|
return err
|
|
}
|
|
for i, v := range fields[name] {
|
|
if err := line(fmt.Sprintf("%s[%d]", name, i), v, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|