Files
orchestra/internal/human/pullrequest.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

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
}