Reconcile docs with reality; fix module graph, token compare, health #1

Open
kami wants to merge 216 commits from webui-and-audit-reconciliation into master
4 changed files with 175 additions and 36 deletions
Showing only changes of commit 86cc0b9276 - Show all commits
+37
View File
@@ -910,6 +910,43 @@ a threshold comparison, not attempted this pass.
`go build ./...`, `go vet ./...`, `go test ./...` all pass.
## S11 — agent-initiated ROTATE, landed 2026-07-27
The last of S11's four pieces that fits an in-band signal (milestone and
thrash both need transcript/tool-call introspection this repo has no source
for — still open). §5.3: *"agent-initiated `ROTATE` → emitted when a coherent
unit finishes and the next is independent."* `continuity.Handoff`'s schema
already anticipated this — `reasons["manual"]` was valid since B5 landed, but
nothing ever checked for it.
New `handoffReason(worktree)` (`internal/orchestrator/orchestrator.go`) reads
`HandoffFile` if present and returns its decoded `meta.reason`, or `""` if
absent/invalid — never `"manual"` on a bad read, so a malformed handoff can't
accidentally short-circuit rotation. Both `rotate()` and `TurnDecision` check
this first: if the agent already wrote a handoff with `reason=manual`, that
**is** the boundary signal, so occupancy and the turn-boundary probe are
skipped entirely and release proceeds straight away with `reason: "manual"`
in the emitted `TaskReleased`. Below that check, the existing
threshold/soft/hard logic is unchanged.
Extracted the release tail (`Release` → anchor certification → `TaskReleased`
append) shared between the threshold and manual paths in `TurnDecision` into
`Coordinator.finishRelease`, since the manual path needed to reach the exact
same anchor-safety logic (never emit a payload with an uncertifiable anchor)
without going through occupancy/boundary gating first.
Covered by `TestTurnDecision/"manual reason bypasses occupancy and turn
boundary"` (`internal/orchestrator/rotation_test.go`): occupancy=0,
boundary=false (both would refuse/continue under every other path), a
`.orchestra-handoff.json` with `reason=manual` written directly to the
worktree, and asserts `TurnRotateNow` + `Release` invoked + task state
`StateQueued`.
**Still open from S11:** milestone rotation and thrash detection — both need
a source of transcript/tool-call data this repo doesn't have yet.
`go build ./...`, `go vet ./...`, `go test ./...` all pass.
### Design consequences (not yet implemented)
1. **Percentages are a level, not a delta.**
+76 -35
View File
@@ -536,6 +536,22 @@ func (c *Coordinator) expire(ctx context.Context) ([]domain.Event, error) {
}
return events, nil
}
// handoffReason reads HandoffFile from the worktree, if present, and returns
// its meta.reason ("threshold|milestone|thrash|manual" per §6.1). An unread­
// able or invalid file returns "" — callers treat that as "no signal yet",
// never as "manual".
func handoffReason(worktree string) string {
b, err := os.ReadFile(filepath.Join(worktree, herdr.HandoffFile))
if err != nil {
return ""
}
h, err := continuity.Decode(b)
if err != nil {
return ""
}
return h.Meta.Reason
}
func (c *Coordinator) rotate(ctx context.Context, hard float64) {
c.loadSessions()
c.mu.Lock()
@@ -554,45 +570,55 @@ func (c *Coordinator) rotate(ctx context.Context, hard float64) {
continue
}
reason := "threshold"
occupancy, err := a.Occupancy(session)
if err != nil || occupancy < c.soft() {
continue
}
if occupancy < hard {
// Soft threshold (§5.3): request a handoff early, advisory only
// — no release, no turn-boundary requirement.
if requester, ok := a.(herdr.HandoffRequester); ok {
if _, statErr := os.Stat(filepath.Join(session.Worktree, herdr.HandoffFile)); statErr != nil && !session.HandoffRequested {
if reqErr := requester.RequestHandoff(ctx, session); reqErr == nil {
session.HandoffRequested = true
c.mu.Lock()
c.sessions[taskID] = session
_ = c.saveSessionsLocked()
c.mu.Unlock()
// Agent-initiated ROTATE (§5.3): the agent itself wrote a handoff
// with reason=manual — "a coherent unit finished and the next is
// independent". That is the boundary signal in its own right; skip
// occupancy and the turn-boundary probe and go straight to release.
manual := handoffReason(session.Worktree) == "manual"
if manual {
reason = "manual"
} else {
occupancy, err := a.Occupancy(session)
if err != nil || occupancy < c.soft() {
continue
}
if occupancy < hard {
// Soft threshold (§5.3): request a handoff early, advisory
// only — no release, no turn-boundary requirement.
if requester, ok := a.(herdr.HandoffRequester); ok {
if _, statErr := os.Stat(filepath.Join(session.Worktree, herdr.HandoffFile)); statErr != nil && !session.HandoffRequested {
if reqErr := requester.RequestHandoff(ctx, session); reqErr == nil {
session.HandoffRequested = true
c.mu.Lock()
c.sessions[taskID] = session
_ = c.saveSessionsLocked()
c.mu.Unlock()
}
}
}
continue
}
continue
}
// Face B is treated as required, not best-effort (spec §5.2/§5.3):
// an adapter that supports the turn-boundary probe but fails to
// answer it blocks this tick's release rather than silently
// proceeding as if mid-turn interruption were safe. Only an
// adapter that genuinely does not implement TurnBoundary at all
// falls back to occupancy-only thresholding, and that fallback is
// recorded so it is observable (MonitorHealth.TurnBoundaryDegraded)
// instead of invisible.
if boundary, ok := a.(herdr.TurnBoundary); ok {
atBoundary, boundaryErr := boundary.AtTurnBoundary(ctx, session)
if boundaryErr != nil {
// Face B is treated as required, not best-effort (spec
// §5.2/§5.3): an adapter that supports the turn-boundary probe
// but fails to answer it blocks this tick's release rather than
// silently proceeding as if mid-turn interruption were safe.
// Only an adapter that genuinely does not implement
// TurnBoundary at all falls back to occupancy-only
// thresholding, and that fallback is recorded so it is
// observable (MonitorHealth.TurnBoundaryDegraded) instead of
// invisible.
if boundary, ok := a.(herdr.TurnBoundary); ok {
atBoundary, boundaryErr := boundary.AtTurnBoundary(ctx, session)
if boundaryErr != nil {
c.recordTurnBoundaryDegraded()
continue
}
if !atBoundary {
continue
}
} else {
c.recordTurnBoundaryDegraded()
continue
}
if !atBoundary {
continue
}
} else {
c.recordTurnBoundaryDegraded()
}
if requester, ok := a.(herdr.HandoffRequester); ok {
if _, statErr := os.Stat(filepath.Join(session.Worktree, herdr.HandoffFile)); statErr != nil {
@@ -668,6 +694,12 @@ func (c *Coordinator) TurnDecision(ctx context.Context, taskID string) (string,
if err != nil {
return "", fmt.Errorf("orchestrator: adapter: %w", err)
}
// Agent-initiated ROTATE (§5.3): a handoff already written with
// reason=manual is itself the boundary signal — skip occupancy and the
// turn-boundary probe and release immediately.
if handoffReason(session.Worktree) == "manual" {
return c.finishRelease(ctx, taskID, task, session, a, "manual")
}
occupancy, err := a.Occupancy(session)
if err != nil {
return "", fmt.Errorf("orchestrator: occupancy: %w", err)
@@ -720,6 +752,15 @@ func (c *Coordinator) TurnDecision(ctx context.Context, taskID string) (string,
return TurnPrepareHandoff, nil
}
}
return c.finishRelease(ctx, taskID, task, session, a, "threshold")
}
// finishRelease runs the common release tail shared by TurnDecision's
// threshold path and its agent-initiated-ROTATE (reason=manual) shortcut:
// call Adapter.Release, certify the anchor against the real worktree HEAD,
// and emit TaskReleased. Any failure refuses rather than emitting a
// TaskReleased payload that would fail validation and strand the session.
func (c *Coordinator) finishRelease(ctx context.Context, taskID string, task domain.Task, session herdr.Session, a herdr.Adapter, reason string) (string, error) {
ref, err := a.Release(ctx, session)
if err != nil || ref == "" {
return TurnRefuse, nil
@@ -730,7 +771,7 @@ func (c *Coordinator) TurnDecision(ctx context.Context, taskID string) (string,
// invalid TaskReleased payload, same as rotate()'s bare continue.
return TurnRefuse, nil
}
b, _ := json.Marshal(map[string]string{"handoff_ref": ref, "reason": "threshold", "anchor_sha": anchorSHA})
b, _ := json.Marshal(map[string]string{"handoff_ref": ref, "reason": reason, "anchor_sha": anchorSHA})
e := domain.Event{ID: domain.NewID(), Type: "TaskReleased", TaskID: taskID, Version: task.Version + 1, Payload: b, Surface: string(authz.System)}
if err := c.Store.Append(e); err != nil {
return TurnRefuse, nil
+48
View File
@@ -257,6 +257,54 @@ func TestTurnDecision(t *testing.T) {
t.Fatalf("task state=%v ok=%v, want leased", got.State, ok)
}
})
// Agent-initiated ROTATE (spec §5.3): a handoff the agent wrote with
// reason=manual is itself the boundary signal ("a coherent unit
// finished and the next is independent") — it must release immediately,
// bypassing occupancy and the turn-boundary probe entirely, not wait for
// either to agree.
t.Run("manual reason bypasses occupancy and turn boundary", func(t *testing.T) {
a := &fakeAdapter{occupancy: 0, boundary: false}
c, st, task := newCoordinator(a)
artifactRef, err := st.PutArtifact([]byte("handoff"))
if err != nil {
t.Fatal(err)
}
a.ref = artifactRef
head, err := herdr.HeadSHA(repo)
if err != nil {
t.Fatal(err)
}
handoff := map[string]any{
"meta": map[string]any{"id": "h2", "reason": "manual", "rotation_index": 0},
"anchor": map[string]any{"git_sha": head, "branch": "orchestra/t1"},
"goal": "g", "done_when": []string{"x"}, "action": "prompt", "command": "go test ./...",
}
b, err := json.Marshal(handoff)
if err != nil {
t.Fatal(err)
}
handoffPath := repo + "/" + herdr.HandoffFile
if err := os.WriteFile(handoffPath, b, 0o644); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { os.Remove(handoffPath) })
decision, err := c.TurnDecision(context.Background(), task.ID)
if err != nil {
t.Fatal(err)
}
if decision != orchestrator.TurnRotateNow {
t.Fatalf("decision=%q want %q", decision, orchestrator.TurnRotateNow)
}
if a.releases == 0 {
t.Fatal("Release was never invoked for an agent-initiated manual rotate")
}
got, ok := st.Task(task.ID)
if !ok || got.State != domain.StateQueued {
t.Fatalf("task state=%v ok=%v, want queued", got.State, ok)
}
})
}
// TestStartBlocksOnInvalidPickup guards AUDIT.md's B6/Phase 4 item 4:
+14 -1
View File
@@ -282,8 +282,21 @@ Fixed so far:
rotation, thrash detection, agent-initiated `ROTATE` — see AUDIT.md's S11
section for why those are bigger than a threshold check.
- **S11 (agent-initiated ROTATE)** — the schema already accepted
`meta.reason: "manual"` (since B5) but nothing checked for it. New
`handoffReason(worktree)` reads a written `.orchestra-handoff.json` and, if
its reason is `"manual"`, both `rotate()` and `TurnDecision` skip occupancy
and the turn-boundary probe entirely and release immediately — the agent's
own handoff *is* the boundary signal per §5.3 ("a coherent unit finished and
the next is independent"). Extracted the shared release-and-certify tail
into `Coordinator.finishRelease` so the manual path reaches the same
anchor-safety guarantee as the threshold path. Covered by a new
`TestTurnDecision` subtest (occupancy=0, boundary=false — every other path
would refuse or continue — asserting rotation happens anyway once a
`reason=manual` handoff exists).
Not yet started: Codex/opencode Stop-hook-equivalent scripts, S8, S11's
milestone/thrash/agent-`ROTATE` pieces. See `AUDIT.md` for the full plan.
milestone/thrash pieces. See `AUDIT.md` for the full plan.
**Phase 0 done (2026-07-27):** this box has live TCP reachability to the real
herdr instance at `192.168.1.105:9245` — verified by hand (raw JSON-RPC