agent: director heartbeat + interactive-surface core (LLM-in-the-loop, surfaces on drain instead of stopping) #351

Closed
opened 2026-06-25 22:39:58 +00:00 by coilysiren · 3 comments
Owner

First build slice of the unified managed-shell director (design anchor: see the tracking issue). Prototype the heartbeat re-entry + interactive-surface spine - the one genuinely new mechanism. Builds on ward#346 (the deterministic director loop, whose ledger/ranking/WARD-OUTCOME parsing this repurposes) and the architect/scratch-session flow.

Goal

Transform ward agent director from #346's deterministic autonomous loop into an LLM-in-the-loop heartbeat that, when the headless lane drains, surfaces to an interactive session instead of exiting. Keep it attached/interactive only (no --detach).

Behavior

warded director [--repo a/b,c/d] [--max-parallel N] attaches an interactive session and runs:

  1. Tick (heartbeat):
    • Poll + reconcile in-flight engineers deterministically - reuse #346's ledger + WARD-OUTCOME parsing to classify done/blocked/failed and update state.
    • Invoke the LLM one-shot (the --driver harness) with: the current ranked backlog (reuse #346 ranking/lanes), the ledger state, and the latest outcomes. The LLM chooses which queued headless-lane issues to dispatch (up to --max-parallel) and interprets blockers/failures (re-queue, park, or surface).
    • Dispatch the chosen issues via the existing path (warded engineer #N / launchAgentContainer), honoring engineer's existing 2h issue reservation so nothing double-dispatches.
    • Sleep cheaply until the next heartbeat. The LLM must NOT be alive/thinking while sleeping - cost is ~zero between ticks. (Mechanism is yours to choose: a wrapper that sleeps and re-invokes the driver one-shot per tick is the expected shape; the ledger persists state across ticks so each invocation is stateless-but-informed.)
  2. Drain -> surface: when nothing is queued AND nothing is in-flight, do not exit (the #346 bug). Surface to an interactive LLM session: report "headless backlog drained" + a summary (done / parked-blocked / parked-failed), then hand to the human for direction. New direction (issues filed, scope, refocus) refills the queue; the next tick resumes the heartbeat.

Reuse, don't rebuild

  • #346's ledger (durable per-repo state, resumability), ranking (tier/mode lanes), and WARD-OUTCOME parsing are the deterministic state + poll layer - drive them from the heartbeat, do not delete them.
  • The dispatch path (runAgentWork/launchAgentContainer) and reservation are reused as-is.
  • The interactive surface reuses the scratch-session/architect bring-up shape (an attached LLM session in the same container).

Constraints

  • Attached/interactive only - no --detach. A detached director is explicitly out (runaway-dispatch risk).
  • Token-cheap between ticks - the heartbeat sleeps without an LLM alive; the per-tick LLM call is a bounded one-shot.
  • Honor the --max-parallel cap and engineer's issue reservation (no double-dispatch).

Out of scope (follow-ups, do not do here)

  • Collapsing the standalone architect command into director's surface phase (a later slice; architect stays for now).
  • Manager-skill richness, mid-drain steering UX, naming, and the roster shrink to 3 roles.

Acceptance

  • warded director attaches, and autonomously dispatches + polls + drains the headless lane via per-tick LLM judgment over the #346 ledger, costing ~nothing while asleep between ticks.
  • On drain it surfaces to an interactive session (does not exit); giving it new direction resumes the heartbeat.
  • A smoke run: with one actionable headless issue in scope, director dispatches an engineer for it, reconciles its WARD-OUTCOME on a later tick, then surfaces to interactive with a correct summary.
  • No --detach. make build, make test, make vet, make lint green before push.

Context to front-load

cmd/ward/agent_director.go (the #346 loop + ledger/ranking/outcome-parse to transform + repurpose), cmd/ward/agent_architect.go + runScratchSession (the interactive surface bring-up), cmd/ward/agent.go (runAgentWork/launchAgentContainer dispatch + the --driver one-shot invocation, e.g. how claude -p is run), cmd/ward/agent_reserve.go (reservation), docs/agent-director.md. Read each before editing.

References

The director design anchor (tracking), ward#346 (the loop this transforms), ward#347 (architect/roster), ward#310 (the WARD-OUTCOME seed marker the poll reads).

First build slice of the unified managed-shell director (design anchor: see the tracking issue). Prototype the **heartbeat re-entry + interactive-surface** spine - the one genuinely new mechanism. Builds on ward#346 (the deterministic director loop, whose ledger/ranking/`WARD-OUTCOME` parsing this repurposes) and the architect/scratch-session flow. ## Goal Transform `ward agent director` from #346's deterministic autonomous loop into an **LLM-in-the-loop heartbeat** that, when the headless lane drains, **surfaces to an interactive session instead of exiting**. Keep it attached/interactive only (no `--detach`). ## Behavior `warded director [--repo a/b,c/d] [--max-parallel N]` attaches an interactive session and runs: 1. **Tick** (heartbeat): - **Poll + reconcile** in-flight engineers deterministically - reuse #346's ledger + `WARD-OUTCOME` parsing to classify done/blocked/failed and update state. - **Invoke the LLM one-shot** (the `--driver` harness) with: the current ranked backlog (reuse #346 ranking/lanes), the ledger state, and the latest outcomes. The LLM chooses which queued headless-lane issues to dispatch (up to `--max-parallel`) and interprets blockers/failures (re-queue, park, or surface). - **Dispatch** the chosen issues via the existing path (`warded engineer #N` / `launchAgentContainer`), honoring engineer's existing 2h issue reservation so nothing double-dispatches. - **Sleep cheaply** until the next heartbeat. The LLM must NOT be alive/thinking while sleeping - cost is ~zero between ticks. (Mechanism is yours to choose: a wrapper that sleeps and re-invokes the driver one-shot per tick is the expected shape; the ledger persists state across ticks so each invocation is stateless-but-informed.) 2. **Drain -> surface**: when nothing is queued AND nothing is in-flight, do **not** exit (the #346 bug). Surface to an **interactive** LLM session: report "headless backlog drained" + a summary (done / parked-blocked / parked-failed), then hand to the human for direction. New direction (issues filed, scope, refocus) refills the queue; the next tick resumes the heartbeat. ## Reuse, don't rebuild - #346's **ledger** (durable per-repo state, resumability), **ranking** (tier/mode lanes), and **`WARD-OUTCOME` parsing** are the deterministic state + poll layer - drive them from the heartbeat, do not delete them. - The **dispatch** path (`runAgentWork`/`launchAgentContainer`) and **reservation** are reused as-is. - The **interactive surface** reuses the scratch-session/architect bring-up shape (an attached LLM session in the same container). ## Constraints - **Attached/interactive only** - no `--detach`. A detached director is explicitly out (runaway-dispatch risk). - **Token-cheap between ticks** - the heartbeat sleeps without an LLM alive; the per-tick LLM call is a bounded one-shot. - Honor the `--max-parallel` cap and engineer's issue reservation (no double-dispatch). ## Out of scope (follow-ups, do not do here) - Collapsing the standalone `architect` command into director's surface phase (a later slice; architect stays for now). - Manager-skill richness, mid-drain steering UX, naming, and the roster shrink to 3 roles. ## Acceptance - `warded director` attaches, and autonomously dispatches + polls + drains the headless lane via **per-tick LLM judgment over the #346 ledger**, costing ~nothing while asleep between ticks. - On drain it **surfaces to an interactive session** (does not exit); giving it new direction resumes the heartbeat. - A smoke run: with one actionable headless issue in scope, director dispatches an engineer for it, reconciles its `WARD-OUTCOME` on a later tick, then surfaces to interactive with a correct summary. - No `--detach`. `make build`, `make test`, `make vet`, `make lint` green before push. ## Context to front-load `cmd/ward/agent_director.go` (the #346 loop + ledger/ranking/outcome-parse to transform + repurpose), `cmd/ward/agent_architect.go` + `runScratchSession` (the interactive surface bring-up), `cmd/ward/agent.go` (`runAgentWork`/`launchAgentContainer` dispatch + the `--driver` one-shot invocation, e.g. how `claude -p` is run), `cmd/ward/agent_reserve.go` (reservation), `docs/agent-director.md`. Read each before editing. ## References The director design anchor (tracking), ward#346 (the loop this transforms), ward#347 (architect/roster), ward#310 (the `WARD-OUTCOME` seed marker the poll reads).
Author
Owner

First build slice of the unified managed-shell director design: ward#350. This is the heartbeat + interactive-surface spine; collapsing architect into the surface phase and the polish are later slices tracked there.

First build slice of the unified managed-shell director design: ward#350. This is the heartbeat + interactive-surface spine; collapsing architect into the surface phase and the polish are later slices tracked there.
Author
Owner

🔒 Reserved by ward agent --driver claude — container ward-ward-issue-351-claude-12308859 on host 4f2062d4fed1 is carrying this issue (reserved 2026-06-25T22:40:24Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `ward-ward-issue-351-claude-12308859` on host `4f2062d4fed1` is carrying this issue (reserved 2026-06-25T22:40:24Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

Retro from the container that carried this:

The heartbeat spine came together cleanly — the per-tick LLM decision is just the existing claude -p preflight one-shot pattern pointed at a new prompt, so it slotted in without inventing machinery, and threading it through a directorBackend seam meant the whole tick→dispatch→reconcile→drain→surface cycle drops into an in-memory smoke test with no docker/forgejo/LLM. Deciding to fail-open to the #346 rank floor (no host one-shot, no binary, timed-out read, or no clear verdict all just dispatch by rank) felt right — the LLM stays judgment layered over a working deterministic floor, never a gate that can strand the lane.

What actually fought back wasn't the Go — it was the gate. Most of the fiddly time went to the doc-size caps (trimming agent-director.md back under 4000 chars after expanding it) and the 2-line contiguous-comment rule, and trufflehog wasn't installed in this container so the commit hook blocked until I fetched the release binary by hand.

Confidence: high on the pure logic and the loop's control flow (well covered), lower on the genuinely-interactive bits — the live architect surface and the real LLM decision live behind the seam, validated by construction rather than a live run. Rough edges worth a follow-up: an actual end-to-end live drain→surface→resume exercise, and a reservation conflict mid-dispatch still parks the issue failed (inherited from #346, not great). Mid-drain steering UX is explicitly the next slice.

Retro from the container that carried this: The heartbeat spine came together cleanly — the per-tick LLM decision is just the existing `claude -p` preflight one-shot pattern pointed at a new prompt, so it slotted in without inventing machinery, and threading it through a `directorBackend` seam meant the whole tick→dispatch→reconcile→drain→surface cycle drops into an in-memory smoke test with no docker/forgejo/LLM. Deciding to fail-open to the #346 rank floor (no host one-shot, no binary, timed-out read, or no clear verdict all just dispatch by rank) felt right — the LLM stays judgment layered over a working deterministic floor, never a gate that can strand the lane. What actually fought back wasn't the Go — it was the gate. Most of the fiddly time went to the doc-size caps (trimming agent-director.md back under 4000 chars after expanding it) and the 2-line contiguous-comment rule, and trufflehog wasn't installed in this container so the commit hook blocked until I fetched the release binary by hand. Confidence: high on the pure logic and the loop's control flow (well covered), lower on the genuinely-interactive bits — the live architect surface and the real LLM decision live behind the seam, validated by construction rather than a live run. Rough edges worth a follow-up: an actual end-to-end live drain→surface→resume exercise, and a reservation conflict mid-dispatch still parks the issue `failed` (inherited from #346, not great). Mid-drain steering UX is explicitly the next slice.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
coilyco-flight-deck/ward#351
No description provided.