Give the reviewed change a path to the human

The completion tail ended at TaskCompleted with no pull request. Nothing in
the running system ever called the review or submission endpoints: the whole
event log holds zero ReviewRecorded and zero TaskSubmitted, so the merge
reflection, the publisher and the human trust boundary had no entry point.

Four links, in the order the tail needs them:

- finalize commits first and runs the quality gate against the committed
  tree, so the gate result is bound to the commit being submitted.
  CheckSubmission requires gate sha, review sha and head sha to be one
  commit, which a gate run on the pre-commit tree can never satisfy.
- The worker seals the reviewer's findings and submits, through a new
  /v1/federation/workers/<id>/submit. A blocking review returns the task to
  implementation instead; a project with no forge still completes directly.
- The reviewing session is told where findings go. The brief asked for
  findings and named no file, and it described a diff nobody supplied.
- GiteaPublisher.Push asks the forge what the branch holds before reaching
  for a local checkout. A worker-owned worktree is on another machine and
  has already pushed the commit; the coordinator has no such directory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
This commit is contained in:
2026-08-28 10:28:50 +04:00
parent d9a5a61965
commit e8d04d719d
6 changed files with 259 additions and 26 deletions
+35
View File
@@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"orchestra/internal/domain"
"orchestra/internal/review"
"strings"
)
@@ -291,6 +292,40 @@ func (c Client) Complete(ctx context.Context, taskID, reportRef, resultSHA, bran
return err
}
// Submit outcomes. Sealing a review and submitting are one call because a
// review that blocks has no submission to make, and splitting them would let a
// lost response leave a sealed review with nothing acting on it.
const (
// SubmitSubmitted: the pull request is open and TaskSubmitted is recorded.
SubmitSubmitted = "submitted"
// SubmitChangesRequested: the review blocked and the task is back in
// implementation.
SubmitChangesRequested = "changes_requested"
// SubmitNoPublisher: the project has no forge, so the caller completes the
// task directly instead.
SubmitNoPublisher = "no_publisher"
)
// Submit seals the review against resultSHA and submits that commit for human
// review. The coordinator owns both events; the worker supplies evidence.
func (c Client) Submit(ctx context.Context, taskID, epoch string, expectedVersion int, resultSHA, remote string, result review.Result, gate domain.GateResult) (string, error) {
resp, err := c.request(ctx, http.MethodPost, "/v1/federation/workers/"+url.PathEscape(c.WorkerID)+"/submit", map[string]any{
"task_id": taskID, "lease_epoch": epoch, "expected_version": expectedVersion,
"result_sha": resultSHA, "remote": remote, "review": result, "gate": gate,
})
if err != nil {
return "", err
}
defer resp.Body.Close()
var out struct {
Status string `json:"status"`
}
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return "", err
}
return out.Status, nil
}
func (c Client) PublishCapture(ctx context.Context, capture Capture) (Capture, error) {
resp, err := c.request(ctx, http.MethodPost, "/v1/federation/workers/"+url.PathEscape(c.WorkerID)+"/captures", capture)
if err != nil {
+24 -1
View File
@@ -32,9 +32,18 @@ type GiteaPublisher struct {
}
func (g GiteaPublisher) Push(ctx context.Context, remote, branch, sha string) (string, error) {
// A worker-owned checkout lives on another machine, and it has already
// pushed the commit it is submitting. Ask the forge what the branch holds
// before reaching for a local repository the coordinator may not have: if
// the remote is already at the submitted commit, publishing is done. This
// is the same read the caller performs afterwards, so nothing is taken on
// trust that a push would have proven.
if head, err := g.branchHead(ctx, branch); err == nil && head == sha {
return head, nil
}
root := g.Root
if strings.TrimSpace(root) == "" {
return "", fmt.Errorf("gitea publisher: worktree root is required")
return "", fmt.Errorf("gitea publisher: %s does not hold %s and there is no local checkout to push from", branch, sha)
}
if out, err := exec.CommandContext(ctx, "git", "-C", root, "push", remote, sha+":refs/heads/"+branch).CombinedOutput(); err != nil {
return "", fmt.Errorf("%s: %w", strings.TrimSpace(string(out)), err)
@@ -52,6 +61,20 @@ func (g GiteaPublisher) Push(ctx context.Context, remote, branch, sha string) (s
return fields[0], nil
}
// branchHead reports the commit the forge holds for a branch, or an error if
// the branch does not exist.
func (g GiteaPublisher) branchHead(ctx context.Context, branch string) (string, error) {
var out struct {
Commit struct {
ID string `json:"id"`
} `json:"commit"`
}
if err := g.get(ctx, "/branches/"+branch, &out); err != nil {
return "", err
}
return out.Commit.ID, nil
}
type giteaPR struct {
Number int `json:"number"`
State string `json:"state"`
+47
View File
@@ -0,0 +1,47 @@
package provider
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
// A worker-owned checkout lives on another machine and has already pushed the
// commit. The coordinator must publish by verifying the forge, not by running
// git in a directory it does not have.
func TestPushAcceptsACommitTheRemoteAlreadyHolds(t *testing.T) {
const sha = "1111111111111111111111111111111111111111"
var asked string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
asked = r.URL.Path
json.NewEncoder(w).Encode(map[string]any{"commit": map[string]string{"id": sha}})
}))
defer srv.Close()
// Root is deliberately empty: a push would have nowhere to run.
p := GiteaPublisher{Gitea: Gitea{BaseURL: srv.URL, Owner: "kami", Repo: "test-e2e"}}
got, err := p.Push(context.Background(), "origin", "orchestra/T1", sha)
if err != nil {
t.Fatalf("push: %v", err)
}
if got != sha {
t.Fatalf("got %q, want %q", got, sha)
}
if asked != "/api/v1/repos/kami/test-e2e/branches/orchestra/T1" {
t.Fatalf("unexpected branch read %q", asked)
}
}
func TestPushRefusesWhenTheRemoteHoldsAnotherCommitAndThereIsNoCheckout(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]any{"commit": map[string]string{"id": "2222222222222222222222222222222222222222"}})
}))
defer srv.Close()
p := GiteaPublisher{Gitea: Gitea{BaseURL: srv.URL, Owner: "kami", Repo: "test-e2e"}}
if _, err := p.Push(context.Background(), "origin", "orchestra/T1", "1111111111111111111111111111111111111111"); err == nil {
t.Fatal("expected a refusal, got a published commit")
}
}
+19 -2
View File
@@ -165,7 +165,11 @@ func Decode(b []byte) (Result, error) {
// Instructions is the reviewer's whole brief. It is narrow on purpose: an open
// invitation produces a list of ways the reviewer would have written it
// instead, which is not review.
const Instructions = `Review the supplied diff against, in order:
// The diff is named rather than supplied: nothing populates agentctx.Evidence
// on the federated path, so a brief that said "the supplied diff" described
// material the reviewing session never received.
const Instructions = `Read the change under review with git in this worktree,
then review it against, in order:
1. the task goal and acceptance criteria
2. the human decisions and constraints
3. the repository rules
@@ -180,4 +184,17 @@ defect or contradicts a decision, minor otherwise.
Do not redesign the solution. Do not suggest optional refactors. Do not edit
any file. Do not report style preferences unless they violate a repository
rule. You are not implementing this task and you do not decide its lifecycle.`
rule. You are not implementing this task and you do not decide its lifecycle.
Write your findings to .orchestra/review.json before you finish, even when you
found nothing:
{"findings": [
{"id": "f1", "severity": "blocker", "file": "path/to/file.go",
"line": 42, "claim": "what is wrong", "evidence": "why, from the diff"}
]}
An empty findings list is how you report a clean review. Do not set a commit
sha: Orchestra binds the review to the commit it seals, because the commit is
not something you can observe. A review with no file is a review nobody can
act on, so the file is what finishes the phase, not the text in your pane.`