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>
108 lines
3.0 KiB
Go
108 lines
3.0 KiB
Go
package human
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
"orchestra/internal/domain"
|
|
)
|
|
|
|
// ReviewObservation is one review the forge recorded on a pull request. It is
|
|
// an observation, not a verdict Orchestra trusts: the trust boundary is the
|
|
// actor, applied by the reconciler.
|
|
type ReviewObservation struct {
|
|
Actor string
|
|
State string // approved | changes_requested | commented
|
|
At time.Time
|
|
Body string
|
|
}
|
|
|
|
// PullRequestState is everything Orchestra needs to know about a submitted
|
|
// pull request. HeadSHA is the commit the forge believes the pull request
|
|
// carries, which is how a merge is tied back to a specific submission.
|
|
type PullRequestState struct {
|
|
ID string
|
|
HeadSHA string
|
|
State string // open | merged | closed
|
|
MergeSHA string
|
|
MergedAt time.Time
|
|
Reviews []ReviewObservation
|
|
Comments []Input
|
|
}
|
|
|
|
// PullRequestSource reads the state of one submitted pull request. Polling is
|
|
// enough: a webhook would add an inbound trust boundary for no new capability.
|
|
type PullRequestSource interface {
|
|
PullRequest(ctx context.Context, task domain.Task) (PullRequestState, error)
|
|
}
|
|
|
|
// Trust decides whose words can move a task. Without it, a bot comment or
|
|
// Orchestra's own reflection could reopen a finished implementation.
|
|
type Trust struct {
|
|
// Accepted, when non-empty, is the allow-list of actor identities. Empty
|
|
// means anyone not explicitly ignored, which is only safe on a private
|
|
// forge with no bots.
|
|
Accepted []string
|
|
// Ignored always loses, even when it appears in Accepted.
|
|
Ignored []string
|
|
}
|
|
|
|
// Allows reports whether this actor's words may move a task.
|
|
func (t Trust) Allows(actor string) bool {
|
|
actor = strings.TrimSpace(strings.ToLower(actor))
|
|
if actor == "" {
|
|
return false
|
|
}
|
|
for _, ignored := range t.Ignored {
|
|
if strings.EqualFold(strings.TrimSpace(ignored), actor) {
|
|
return false
|
|
}
|
|
}
|
|
if len(t.Accepted) == 0 {
|
|
return true
|
|
}
|
|
for _, accepted := range t.Accepted {
|
|
if strings.EqualFold(strings.TrimSpace(accepted), actor) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// FeedbackAfter returns the trusted human input on a pull request that arrived
|
|
// strictly after the submission. Anything at or before it was already visible
|
|
// when the submission was made, so it cannot be a response to it.
|
|
func (p PullRequestState) FeedbackAfter(provider string, submittedAt time.Time, trust Trust) []Input {
|
|
var out []Input
|
|
for _, c := range p.Comments {
|
|
if !c.At.After(submittedAt) || !trust.Allows(c.Author) {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(c.Body) == "" {
|
|
continue
|
|
}
|
|
if c.Provider == "" {
|
|
c.Provider = provider
|
|
}
|
|
out = append(out, c)
|
|
}
|
|
for _, r := range p.Reviews {
|
|
if !r.At.After(submittedAt) || !trust.Allows(r.Actor) {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(r.Body) == "" && r.State != "changes_requested" {
|
|
continue
|
|
}
|
|
body := strings.TrimSpace(r.Body)
|
|
if body == "" {
|
|
body = "changes requested with no comment"
|
|
}
|
|
out = append(out, Input{
|
|
Provider: provider, ExternalID: "review:" + r.Actor + ":" + r.At.UTC().Format(time.RFC3339),
|
|
Author: r.Actor, At: r.At, Body: body,
|
|
})
|
|
}
|
|
return out
|
|
}
|