v3 workflow: intent, phases, review, submission, enforcement, burn-in
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>
This commit is contained in:
@@ -117,6 +117,49 @@ func (c Client) Tasks(ctx context.Context) ([]domain.Task, error) {
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// TurnDecision is the coordinator's answer at a worker's turn boundary: the
|
||||
// verdict the worker reported, plus the human decisions this session has not
|
||||
// been shown. Decisions are present only when the verdict is continue.
|
||||
type TurnDecision struct {
|
||||
Verdict string `json:"verdict"`
|
||||
Decisions []domain.HumanDecision `json:"decisions,omitempty"`
|
||||
}
|
||||
|
||||
// Turn reports a verified turn boundary and collects any newer human
|
||||
// decisions. The worker evaluates rotation locally, because only it can see
|
||||
// the pane; authority stays with the coordinator.
|
||||
func (c Client) Turn(ctx context.Context, taskID, epoch, verdict string, delivered []string) (TurnDecision, error) {
|
||||
resp, err := c.request(ctx, http.MethodPost, "/v1/federation/turn", map[string]any{
|
||||
"task_id": taskID, "lease_epoch": epoch, "verdict": verdict, "delivered_decisions": delivered,
|
||||
})
|
||||
if err != nil {
|
||||
return TurnDecision{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var out TurnDecision
|
||||
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
||||
return TurnDecision{}, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Intent fetches the reduced authority for one task: its contract plus the
|
||||
// human decisions still standing. A worker renders its launch instruction
|
||||
// from this, never from handoff prose.
|
||||
func (c Client) Intent(ctx context.Context, taskID string) (domain.EffectiveIntent, error) {
|
||||
resp, err := c.request(ctx, http.MethodGet, "/v1/tasks/"+url.PathEscape(taskID)+"/intent", nil)
|
||||
if err != nil {
|
||||
return domain.EffectiveIntent{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var intent domain.EffectiveIntent
|
||||
if err := json.NewDecoder(resp.Body).Decode(&intent); err != nil {
|
||||
return domain.EffectiveIntent{}, err
|
||||
}
|
||||
return intent, nil
|
||||
}
|
||||
|
||||
func (c Client) Ack(ctx context.Context, cursor uint64) error {
|
||||
resp, err := c.request(ctx, http.MethodPost, "/v1/federation/events/ack", map[string]uint64{"cursor": cursor})
|
||||
if resp != nil {
|
||||
|
||||
@@ -27,12 +27,13 @@ type Worker struct {
|
||||
Token string `json:"-"`
|
||||
}
|
||||
|
||||
// WorkerHealth is reported by the worker that owns the local herdr socket.
|
||||
// WorkerHealth is reported by the worker that owns the local execution backend.
|
||||
// It intentionally does not reuse coordinator TCP-probe state: a remote
|
||||
// socket is meaningful only from the machine where the worker and checkout
|
||||
// live.
|
||||
// pane backend is meaningful only from the machine where the worker and
|
||||
// checkout live. HerdrStatus keeps its wire name for compatibility.
|
||||
type WorkerHealth struct {
|
||||
HerdrStatus string `json:"herdr_status"` // reachable, unreachable, or unknown
|
||||
Backend string `json:"backend,omitempty"` // herdr or tmux
|
||||
HerdrStatus string `json:"herdr_status"` // reachable, unreachable, or unknown
|
||||
CheckedAt time.Time `json:"checked_at,omitempty"`
|
||||
ActiveTask string `json:"active_task_id,omitempty"`
|
||||
ActivePane string `json:"active_pane_id,omitempty"`
|
||||
@@ -422,7 +423,8 @@ func (r *Registry) Available(id string) bool {
|
||||
}
|
||||
// A heartbeat merely proves the worker process can reach the coordinator.
|
||||
// Lease admission additionally requires a fresh probe of the worker's
|
||||
// local herdr; otherwise a partitioned/down herdr still attracts work.
|
||||
// local execution backend; otherwise a partitioned/down backend still
|
||||
// attracts work.
|
||||
w.Online = time.Since(w.LastSeen) <= r.TTL && w.Health.HerdrStatus == "reachable" && !w.Health.CheckedAt.IsZero() && time.Since(w.Health.CheckedAt) <= r.TTL
|
||||
r.workers[id] = w
|
||||
return w.Online
|
||||
|
||||
Reference in New Issue
Block a user