Files
orchestra/internal/domain/submission.go
T
kami 7f12c7fc37 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>
2026-08-26 18:31:20 +04:00

175 lines
5.8 KiB
Go

package domain
import (
"fmt"
"strings"
"time"
)
// EventTaskSubmitted records that a reviewed change reached the human. It is
// deliberately not a completion: submission means the work is in the human's
// hands, and completion means the change shipped.
const EventTaskSubmitted = "TaskSubmitted"
// EventTaskChangesRequested records that the human sent a submitted change
// back. The submission it names is not removed: sha A was reviewed, submitted,
// and rejected, and that history is what explains sha B.
const EventTaskChangesRequested = "TaskChangesRequested"
// CompletionReceipt is the evidence that a submission shipped. Merge strategy
// varies, so a squash or merge commit means MergeSHA rarely equals
// SubmittedSHA. What establishes completion is that the bound pull request
// merged while carrying the submitted commit, not sha equality.
type CompletionReceipt struct {
SubmissionRef string `json:"submission_ref"`
PR ExternalRef `json:"pr"`
SubmittedSHA string `json:"submitted_sha"`
MergeSHA string `json:"merge_sha,omitempty"`
MergedAt time.Time `json:"merged_at"`
}
// GateResult is one quality-gate run, bound to the commit it ran against. A
// gate result with no commit is a claim, not evidence.
type GateResult struct {
Command string `json:"command"`
ExitCode int `json:"exit_code"`
SHA string `json:"sha"`
Output string `json:"output,omitempty"`
}
func (g GateResult) Passed() bool { return g.ExitCode == 0 && len(g.SHA) == 40 }
// ExternalRef identifies a pull request in the forge that holds it.
type ExternalRef struct {
Provider string `json:"provider"`
ID string `json:"id"`
URL string `json:"url,omitempty"`
}
// SubmissionRef is the durable record of what was submitted. Every field binds
// the submission to one commit, so a later change cannot inherit it.
type SubmissionRef struct {
ResultSHA string `json:"result_sha"`
RemoteRef string `json:"remote_ref"`
PR ExternalRef `json:"pr"`
GateRef string `json:"gate_ref,omitempty"`
ReviewRef string `json:"review_ref,omitempty"`
PacketRef string `json:"packet_ref,omitempty"`
}
// SubmissionCheck is why a task may or may not be submitted. Reasons are
// listed rather than summarised: "not eligible" alone sends an operator
// reading code.
type SubmissionCheck struct {
Eligible bool `json:"eligible"`
Reasons []string `json:"reasons,omitempty"`
}
// CheckSubmission is the whole eligibility rule, as one pure function of the
// task, the current commit, and the gate run.
//
// The invariant that matters most: gate sha, review sha, and head sha must be
// the same commit. Anything changing after review makes submission ineligible
// immediately, with no state to clear and no flag to go stale.
func CheckSubmission(task Task, headSHA string, gate GateResult) SubmissionCheck {
var reasons []string
add := func(format string, args ...any) { reasons = append(reasons, fmt.Sprintf(format, args...)) }
phase := task.WorkPhase
if phase == "" {
phase = WorkPhaseFrame
}
if phase != WorkPhaseReview {
add("work phase is %s, not review", phase)
}
if task.State == StateBlocked || task.State == StateNeedsAttention {
add("task is %s (%s)", task.State, task.BlockReason)
}
if task.State == StateCompleted || task.State == StateFailed {
add("task is already %s", task.State)
}
if task.DecisionRequest != nil {
add("a human decision is still outstanding")
}
if len(headSHA) != 40 {
add("head commit is not anchored")
}
if !gate.Passed() {
add("quality gate %q exited %d", gate.Command, gate.ExitCode)
} else if gate.SHA != headSHA {
add("quality gate ran against %s, not the current head", short(gate.SHA))
}
switch {
case task.Review == nil:
add("no independent review has been recorded")
case task.Review.ResultSHA != headSHA:
add("the review is for %s, not the current head", short(task.Review.ResultSHA))
case task.Review.Blocking > 0:
add("%d unresolved blocker or important review findings", task.Review.Blocking)
}
return SubmissionCheck{Eligible: len(reasons) == 0, Reasons: reasons}
}
// RequirePhaseArtifacts reports the project-policy half of eligibility: a
// project whose path includes research or plan must have sealed them.
func (t Task) RequirePhaseArtifacts(path []WorkPhase) []string {
var missing []string
for _, phase := range path {
switch phase {
case WorkPhaseResearch:
if t.ResearchRef == "" {
missing = append(missing, "the project's path includes research but none was sealed")
}
case WorkPhasePlan:
if t.PlanRef == "" {
missing = append(missing, "the project's path includes plan but none was sealed")
}
}
}
return missing
}
// Submitted reports whether this task already has a submission for exactly
// this commit, which is what makes a repeated submission idempotent.
func (t Task) Submitted(headSHA string) bool {
return t.Submission != nil && t.Submission.ResultSHA == headSHA
}
func ValidateTaskSubmitted(p map[string]any) error {
if v, ok := p["result_sha"].(string); !ok || len(v) != 40 {
return fmt.Errorf("%w: result_sha invalid", ErrInvalid)
}
if v, ok := p["remote_ref"].(string); !ok || strings.TrimSpace(v) == "" {
return fmt.Errorf("%w: remote_ref required", ErrInvalid)
}
pr, ok := p["pr"].(map[string]any)
if !ok {
return fmt.Errorf("%w: pr required", ErrInvalid)
}
for _, k := range []string{"provider", "id"} {
if v, ok := pr[k].(string); !ok || strings.TrimSpace(v) == "" {
return fmt.Errorf("%w: pr.%s required", ErrInvalid, k)
}
}
for _, k := range []string{"gate_ref", "review_ref", "packet_ref"} {
if v, ok := p[k]; ok {
if s, _ := v.(string); s != "" {
if err := requiredHash(map[string]any{k: s}, k); err != nil {
return err
}
}
}
}
return nil
}
func short(sha string) string {
if len(sha) > 12 {
return sha[:12]
}
if sha == "" {
return "an unknown commit"
}
return sha
}