fix(harness): add automatic TaskCompleted producer (B3, partial)
POST /v1/harness/complete lets a Claude Code Stop hook report task completion instead of relying on a human hitting the manual endpoint. The hook only fires on an explicit .orchestra-report.md marker (not every turn boundary); the server builds the receipt itself from the real transcript via herdr.ClaudeUsage rather than trusting a self-reported number. Codex/opencode producers and the turn-decision endpoint are still unbuilt — see AUDIT.md/progress.md for scope. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
This commit is contained in:
@@ -242,6 +242,68 @@ func main() {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]string{"ref": ref})
|
||||
})
|
||||
// /v1/harness/complete is the automatic TaskCompleted producer (AUDIT.md
|
||||
// B3): a harness-side hook posts here when the agent has declared the
|
||||
// task done (see deploy/hooks/orchestra-stop.sh), not on every turn
|
||||
// boundary. It reads the transcript locally to build an honest receipt —
|
||||
// same session-file assumption as CLIAdapter.Occupancy — rather than
|
||||
// trusting a self-reported number.
|
||||
harnessToken := os.Getenv("ORCHESTRA_HARNESS_TOKEN")
|
||||
mux.HandleFunc("/v1/harness/complete", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if harnessToken != "" && r.Header.Get("Authorization") != "Bearer "+harnessToken {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
TaskID string `json:"task_id"`
|
||||
TranscriptPath string `json:"transcript_path"`
|
||||
Report string `json:"report"`
|
||||
}
|
||||
if json.NewDecoder(r.Body).Decode(&p) != nil || p.TaskID == "" || p.Report == "" || p.TranscriptPath == "" {
|
||||
http.Error(w, "task_id, transcript_path, and report are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
t, ok := s.Task(p.TaskID)
|
||||
if !ok {
|
||||
http.Error(w, "task not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
usage, err := herdr.ClaudeUsage(p.TranscriptPath)
|
||||
if err != nil {
|
||||
http.Error(w, "reading transcript: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ref, err := s.PutArtifact([]byte(p.Report))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"report_ref": ref,
|
||||
"receipt": map[string]any{
|
||||
"input_tokens": usage.Input,
|
||||
"cache_read_tokens": usage.CacheRead,
|
||||
"cache_write_tokens": usage.CacheWrite,
|
||||
"output_tokens": usage.Output,
|
||||
"numerator": usage.Numerator(),
|
||||
},
|
||||
})
|
||||
e := domain.Event{ID: id(), Type: "TaskCompleted", TaskID: p.TaskID, Version: t.Version + 1, Payload: payload, Surface: string(authz.System)}
|
||||
if err := s.Append(e); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusConflict)
|
||||
return
|
||||
}
|
||||
if rt != nil {
|
||||
if _, routeErr := rt.HandleEvent(e); routeErr != nil {
|
||||
log.Printf("route task: %v", routeErr)
|
||||
}
|
||||
}
|
||||
json.NewEncoder(w).Encode(e)
|
||||
})
|
||||
mux.HandleFunc("/v1/brief", func(w http.ResponseWriter, r *http.Request) {
|
||||
to := time.Now().UTC()
|
||||
from := to.Add(-12 * time.Hour)
|
||||
|
||||
Reference in New Issue
Block a user