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>
92 lines
2.6 KiB
Go
92 lines
2.6 KiB
Go
package provider
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"orchestra/internal/domain"
|
|
"orchestra/internal/human"
|
|
"orchestra/internal/store"
|
|
)
|
|
|
|
// GiteaComments reads issue comments as human input. It is a separate type
|
|
// from Gitea so the ingestion path and the authority path cannot be confused
|
|
// for each other: this one never creates or closes a task.
|
|
type GiteaComments struct{ Gitea }
|
|
|
|
type giteaComment struct {
|
|
ID int64 `json:"id"`
|
|
Body string `json:"body"`
|
|
User struct {
|
|
Login string `json:"login"`
|
|
} `json:"user"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// FetchAfter returns the comments on the task's issue with an id above the
|
|
// cursor, oldest first. The cursor is the highest comment id already
|
|
// reconciled; comment ids are monotonic per repo, which makes them a usable
|
|
// resume point even when a comment is edited later.
|
|
func (g GiteaComments) FetchAfter(ctx context.Context, task domain.Task, cursor store.SourceCursor) ([]human.Input, store.SourceCursor, error) {
|
|
next := store.SourceCursor{TaskID: task.ID, Provider: g.SourceName(), Cursor: cursor.Cursor}
|
|
if task.ExternalID == "" || task.Source != g.SourceName() {
|
|
return nil, next, nil
|
|
}
|
|
var after int64
|
|
if cursor.Cursor != "" {
|
|
v, err := strconv.ParseInt(cursor.Cursor, 10, 64)
|
|
if err != nil {
|
|
return nil, next, fmt.Errorf("gitea comments: bad cursor %q: %w", cursor.Cursor, err)
|
|
}
|
|
after = v
|
|
}
|
|
u := strings.TrimRight(g.BaseURL, "/") + "/api/v1/repos/" + g.Owner + "/" + g.Repo + "/issues/" + task.ExternalID + "/comments"
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
|
if err != nil {
|
|
return nil, next, err
|
|
}
|
|
if g.Token != "" {
|
|
req.Header.Set("Authorization", "token "+g.Token)
|
|
}
|
|
resp, err := g.client().Do(req)
|
|
if err != nil {
|
|
return nil, next, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode/100 != 2 {
|
|
return nil, next, fmt.Errorf("gitea comments: %s", resp.Status)
|
|
}
|
|
var comments []giteaComment
|
|
if err := json.NewDecoder(resp.Body).Decode(&comments); err != nil {
|
|
return nil, next, err
|
|
}
|
|
sort.Slice(comments, func(a, b int) bool { return comments[a].ID < comments[b].ID })
|
|
var out []human.Input
|
|
highest := after
|
|
for _, c := range comments {
|
|
if c.ID <= after {
|
|
continue
|
|
}
|
|
out = append(out, human.Input{
|
|
Provider: g.SourceName(),
|
|
ExternalID: strconv.FormatInt(c.ID, 10),
|
|
Author: c.User.Login,
|
|
At: c.CreatedAt,
|
|
Body: c.Body,
|
|
})
|
|
if c.ID > highest {
|
|
highest = c.ID
|
|
}
|
|
}
|
|
if highest > after {
|
|
next.Cursor = strconv.FormatInt(highest, 10)
|
|
}
|
|
return out, next, nil
|
|
}
|