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:
+50
-2
@@ -1378,7 +1378,7 @@ func main() {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
mux.HandleFunc("/v1/federation/workers/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || (!strings.HasSuffix(r.URL.Path, "/heartbeat") && !strings.HasSuffix(r.URL.Path, "/renew") && !strings.HasSuffix(r.URL.Path, "/start") && !strings.HasSuffix(r.URL.Path, "/nack") && !strings.HasSuffix(r.URL.Path, "/handoff") && !strings.HasSuffix(r.URL.Path, "/pickup") && !strings.HasSuffix(r.URL.Path, "/complete") && !strings.HasSuffix(r.URL.Path, "/captures")) {
|
||||
if r.Method != http.MethodPost || (!strings.HasSuffix(r.URL.Path, "/heartbeat") && !strings.HasSuffix(r.URL.Path, "/renew") && !strings.HasSuffix(r.URL.Path, "/start") && !strings.HasSuffix(r.URL.Path, "/nack") && !strings.HasSuffix(r.URL.Path, "/handoff") && !strings.HasSuffix(r.URL.Path, "/pickup") && !strings.HasSuffix(r.URL.Path, "/complete") && !strings.HasSuffix(r.URL.Path, "/submit") && !strings.HasSuffix(r.URL.Path, "/captures")) {
|
||||
http.Error(w, "not found", 404)
|
||||
return
|
||||
}
|
||||
@@ -1443,6 +1443,8 @@ func main() {
|
||||
FailureClass string `json:"failure_class"`
|
||||
LastError string `json:"last_error"`
|
||||
SessionEvidence domain.SessionEvidence `json:"session_evidence"`
|
||||
Review review.Result `json:"review"`
|
||||
Gate domain.GateResult `json:"gate"`
|
||||
}
|
||||
if json.NewDecoder(r.Body).Decode(&b) != nil || b.TaskID == "" {
|
||||
http.Error(w, "invalid lease body", 400)
|
||||
@@ -1465,10 +1467,56 @@ func main() {
|
||||
http.Error(w, "lease not owned", 409)
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(r.URL.Path, "/complete") && b.ExpectedVersion != t.Version {
|
||||
if (strings.HasSuffix(r.URL.Path, "/complete") || strings.HasSuffix(r.URL.Path, "/submit")) && b.ExpectedVersion != t.Version {
|
||||
http.Error(w, "lease version conflict", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(r.URL.Path, "/submit") {
|
||||
// Seal the review, then submit the commit it examined. Both events
|
||||
// are Orchestra's; the worker supplies verified evidence and the
|
||||
// coordinator decides what it means.
|
||||
project, ok := rr.Project(t.Project)
|
||||
if !ok {
|
||||
http.Error(w, "unknown project "+t.Project, 409)
|
||||
return
|
||||
}
|
||||
if len(b.ResultSHA) != 40 {
|
||||
http.Error(w, "verified result_sha required", 400)
|
||||
return
|
||||
}
|
||||
if !t.Submitted(b.ResultSHA) && (t.Review == nil || t.Review.ResultSHA != b.ResultSHA) {
|
||||
b.Review.ResultSHA = b.ResultSHA
|
||||
if _, err := operations.RecordReview(s, project, b.TaskID, b.Review); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusConflict)
|
||||
return
|
||||
}
|
||||
if !b.Review.Accepted() {
|
||||
// The review sent the work back to implementation. That is
|
||||
// an outcome, not a failure, and there is nothing to submit.
|
||||
json.NewEncoder(w).Encode(map[string]any{"status": federation.SubmitChangesRequested, "blocking": len(b.Review.Blocking())})
|
||||
return
|
||||
}
|
||||
}
|
||||
if submissionPublisher == nil {
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": federation.SubmitNoPublisher})
|
||||
return
|
||||
}
|
||||
plan, err := operations.PrepareSubmission(s, project, b.TaskID, b.ResultSHA, b.Gate, operations.Notes{})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusConflict)
|
||||
return
|
||||
}
|
||||
if b.Remote != "" {
|
||||
plan.Remote = b.Remote
|
||||
}
|
||||
e, err := operations.ExecuteSubmission(r.Context(), s, plan, submissionPublisher(plan), nil)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusConflict)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]any{"status": federation.SubmitSubmitted, "event": e})
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(r.URL.Path, "/start") {
|
||||
// A lost response after append is an idempotent start ACK, not a
|
||||
// reason to strand the running pane behind a stale version.
|
||||
|
||||
Reference in New Issue
Block a user