carin: autonomous iterative dev loop #1

Merged
starsetbyte merged 23 commits from autonomous-iterative-loop into main 2026-07-04 15:39:40 +00:00
Owner

Summary

Implementation of the autonomous iterative dev loop plan
(docs/superpowers/plans/2026-07-03-autonomous-iterative-loop.md), built
task-by-task via subagent-driven development (fresh implementer + task
reviewer per task, all findings verified against diffs before merge), then
validated end-to-end against a live scratch instance with real DeepSeek API
calls, then given a final whole-branch review.

All 14 tasks complete, reviewed, and merged with main. Two live smoke-test
pipelines run successfully end-to-end. Final whole-branch review's one
gating issue fixed and re-reviewed.

Done (reviewed, tests passing)

Phase A — reliability fixes

  • Task 1: StaleRuns now honors last_heartbeat_at, not just started_at
  • Task 2: real backoff (backoff_until), priority ordering, per-project
    serialization via new NextQueuedRun
  • Task 3: dispatcher heartbeats external agent processes while they run
  • Task 4: waiting → queued is now a legal resume transition

Phase B — agent protocol

  • Task 5: client helpers HeartbeatRun, LinkRunThread, handoff fields on RunParams
  • Task 6: carin agent say|block|post-artifact|heartbeat CLI verbs

Phase C — personas, threads, protocol-aware spawns

  • Task 7: persona config (internal/dispatch/personas.go, -agents flag)
  • Task 8: every dispatched run gets a mailbox thread; handoffs inherit it
  • Task 9: persona-driven spawns, protocol-aware prompts (buildAgentPrompt),
    status-aware completion (dispatcher no longer stomps over an agent's own
    complete/fail/block)
  • Task 10: blocker resume (re-spawn pi with prior session + resume prompt).
    Went through three review-driven fix rounds to get the resume-gating logic
    right (crash-retry vs. genuine block, compound block→resume→crash→retry
    sequences) plus a fix (found via live testing) for priorSessionID
    returning the oldest rather than most recent session artifact.

Phase D — iterative pipeline

  • Task 11: iteration fields (parent_run_id, iteration, max_iterations,
    handoff_mode), generalized handoff via HasChildRun (replacing a fragile
    context_summary string-scan dedup)
  • Task 12: review verdict loop (processVerdict): approve/revise/iteration-cap
    escalation
  • Task 13: carin pipeline-create — one command kicks off the whole loop
  • Task 14: office view live conversation feed + persona colors, end-to-end
    smoke test against real pipeline runs

Merge with main

Main had advanced 11 commits past this branch's fork point (knowledge engine,
UI redesign, a GitHub client for commit statuses/PR comments) before this
branch was ready — real conflicts in cmd/carin/main.go and
internal/dispatch/dispatch.go (both added Dispatcher fields/flags in the
same spots). Resolved additively (commit c0f7c29) — kept both the
GitHub-client wiring and the persona-config wiring. Build/vet/test green
after merge; PR confirmed mergeable: true.

End-to-end smoke test (Task 14 Step 6)

Ran two full pipelines against a scratch instance using a real DeepSeek API
key:

  1. "hello loop" — architect → builder → reviewer(revise) → fix →
    reviewer(revise) → fix → reviewer(hit iteration cap) → escalated to
    human inbox. Real files created and committed by the builder persona in a
    real sandbox git repo.
  2. "escalation check" — same shape, also hit the cap and escalated
    correctly. (This run was also meant to test spontaneous
    carin agent block usage from an ambiguous goal; the persona chose to
    decide autonomously instead rather than blocking — a prompt-tuning gap,
    not a code defect; the block/resume mechanism was separately verified
    deterministically via direct CLI manipulation: draft→running→block→
    waiting→reply→queued→cancelled all worked correctly.)

This live testing found and fixed two real bugs beyond the three review
rounds above:

  • processHandoffs was seeding the next stage's prompt from
    artifacts[0] (oldest artifact — always the pi-session hash, since
    every fresh spawn posts that before its real output) instead of the most
    recent real-output artifact. Fixed in 22a709d, pre-existing bug
    (predates this whole plan, git blame de3c191), directly undermined the
    handoff-chain goal.
  • The office view's SSE-triggered live refresh did an outerHTML swap onto
    a node that wasn't the partial's own root, nesting duplicate page headers
    on every update. Fixed in 18034b4.

It also surfaced, empirically, that parseVerdict's strict suffix-matching
(must end with exactly "VERDICT: approve/revise") never once matched
across 6 real reviewer completions — models always add trailing prose or
markdown formatting after the token. See below.

Final whole-branch review

Verdict: ready to merge, with one fixparseVerdict's strict
suffix-matching (confirmed by the live testing above) meant the pipeline's
clean self-approval path had never actually fired against a real model;
every run fell through to defensive-revise and always terminated via
human escalation instead. Fixed in 915771d: last-occurrence verdict:
scan, markdown/whitespace stripping, prefix match (not Contains, to avoid
false-positiving on e.g. "VERDICT: revise — does not meet the approve bar"), with the safety bias (ambiguity → revise/escalate, never a silent
false-approve) explicitly preserved and tested. Re-reviewed and approved —
every adversarial case hand-traced plus independent extra hostile inputs,
zero false-approve outcomes.

Non-blocking items explicitly assessed as ship-and-track by the final
review (carried forward, not fixed here):

  • A pre-existing TOCTOU race in tick()'s per-project run serialization
    (real, but requires 2+ concurrent same-project runs, which normal
    sequential pipelines never produce). Suggested fix if revisited: fold the
    claim into NextQueuedRun as one atomic SELECT+UPDATE.
  • Personas not spontaneously using carin agent block (RolePrompt tuning,
    see smoke test above).
  • Minor: escalateIterationCap's verdict-artifact-before-notify ordering;
    dead internal/web/templates/office.html; processHandoffs's
    Limit: 50 (latent durability edge at higher throughput); a swallowed
    GetRun re-fetch error in spawnExternal.

Test plan

  • go build ./..., go vet ./..., go test ./... — green at every
    commit, and at the final HEAD (915771d)
  • Tasks 1–14 implemented and reviewed (fresh implementer + fresh
    reviewer per task, iterating to approval)
  • End-to-end smoke test — two full pipelines run live against a real
    DeepSeek-backed scratch instance, both reaching the iteration cap and
    escalating correctly; block/resume mechanics verified deterministically
  • Final whole-branch review — one gating issue found and fixed
    (parseVerdict), re-reviewed and approved

Remaining (post-merge)

  • Deploy: rebuild/restart the production process with -agents,
    delete stale carin.old in the repo root, update README/AGENTS.md
## Summary Implementation of the autonomous iterative dev loop plan (`docs/superpowers/plans/2026-07-03-autonomous-iterative-loop.md`), built task-by-task via subagent-driven development (fresh implementer + task reviewer per task, all findings verified against diffs before merge), then validated end-to-end against a live scratch instance with real DeepSeek API calls, then given a final whole-branch review. **All 14 tasks complete, reviewed, and merged with main. Two live smoke-test pipelines run successfully end-to-end. Final whole-branch review's one gating issue fixed and re-reviewed.** ## Done (reviewed, tests passing) **Phase A — reliability fixes** - Task 1: `StaleRuns` now honors `last_heartbeat_at`, not just `started_at` - Task 2: real backoff (`backoff_until`), priority ordering, per-project serialization via new `NextQueuedRun` - Task 3: dispatcher heartbeats external agent processes while they run - Task 4: `waiting → queued` is now a legal resume transition **Phase B — agent protocol** - Task 5: client helpers `HeartbeatRun`, `LinkRunThread`, handoff fields on `RunParams` - Task 6: `carin agent say|block|post-artifact|heartbeat` CLI verbs **Phase C — personas, threads, protocol-aware spawns** - Task 7: persona config (`internal/dispatch/personas.go`, `-agents` flag) - Task 8: every dispatched run gets a mailbox thread; handoffs inherit it - Task 9: persona-driven spawns, protocol-aware prompts (`buildAgentPrompt`), status-aware completion (dispatcher no longer stomps over an agent's own `complete`/`fail`/`block`) - Task 10: blocker resume (re-spawn pi with prior session + resume prompt). Went through three review-driven fix rounds to get the resume-gating logic right (crash-retry vs. genuine block, compound block→resume→crash→retry sequences) plus a fix (found via live testing) for `priorSessionID` returning the oldest rather than most recent session artifact. **Phase D — iterative pipeline** - Task 11: iteration fields (`parent_run_id`, `iteration`, `max_iterations`, `handoff_mode`), generalized handoff via `HasChildRun` (replacing a fragile `context_summary` string-scan dedup) - Task 12: review verdict loop (`processVerdict`): approve/revise/iteration-cap escalation - Task 13: `carin pipeline-create` — one command kicks off the whole loop - Task 14: office view live conversation feed + persona colors, end-to-end smoke test against real pipeline runs ## Merge with main Main had advanced 11 commits past this branch's fork point (knowledge engine, UI redesign, a GitHub client for commit statuses/PR comments) before this branch was ready — real conflicts in `cmd/carin/main.go` and `internal/dispatch/dispatch.go` (both added Dispatcher fields/flags in the same spots). Resolved additively (commit `c0f7c29`) — kept both the GitHub-client wiring and the persona-config wiring. Build/vet/test green after merge; PR confirmed `mergeable: true`. ## End-to-end smoke test (Task 14 Step 6) Ran two full pipelines against a scratch instance using a real DeepSeek API key: 1. "hello loop" — architect → builder → reviewer(revise) → fix → reviewer(revise) → fix → reviewer(hit iteration cap) → escalated to human inbox. Real files created and committed by the builder persona in a real sandbox git repo. 2. "escalation check" — same shape, also hit the cap and escalated correctly. (This run was also meant to test spontaneous `carin agent block` usage from an ambiguous goal; the persona chose to decide autonomously instead rather than blocking — a prompt-tuning gap, not a code defect; the block/resume *mechanism* was separately verified deterministically via direct CLI manipulation: draft→running→block→ waiting→reply→queued→cancelled all worked correctly.) This live testing found and fixed two real bugs beyond the three review rounds above: - `processHandoffs` was seeding the next stage's prompt from `artifacts[0]` (oldest artifact — always the `pi-session` hash, since every fresh spawn posts that before its real output) instead of the most recent real-output artifact. Fixed in `22a709d`, pre-existing bug (predates this whole plan, git blame `de3c191`), directly undermined the handoff-chain goal. - The office view's SSE-triggered live refresh did an `outerHTML` swap onto a node that wasn't the partial's own root, nesting duplicate page headers on every update. Fixed in `18034b4`. It also surfaced, empirically, that `parseVerdict`'s strict suffix-matching (`must end with exactly "VERDICT: approve/revise"`) never once matched across 6 real reviewer completions — models always add trailing prose or markdown formatting after the token. See below. ## Final whole-branch review Verdict: **ready to merge, with one fix** — `parseVerdict`'s strict suffix-matching (confirmed by the live testing above) meant the pipeline's clean self-approval path had never actually fired against a real model; every run fell through to defensive-revise and always terminated via human escalation instead. Fixed in `915771d`: last-occurrence `verdict:` scan, markdown/whitespace stripping, prefix match (not `Contains`, to avoid false-positiving on e.g. `"VERDICT: revise — does not meet the approve bar"`), with the safety bias (ambiguity → revise/escalate, never a silent false-approve) explicitly preserved and tested. Re-reviewed and approved — every adversarial case hand-traced plus independent extra hostile inputs, zero false-approve outcomes. Non-blocking items explicitly assessed as ship-and-track by the final review (carried forward, not fixed here): - A pre-existing TOCTOU race in `tick()`'s per-project run serialization (real, but requires 2+ concurrent same-project runs, which normal sequential pipelines never produce). Suggested fix if revisited: fold the claim into `NextQueuedRun` as one atomic SELECT+UPDATE. - Personas not spontaneously using `carin agent block` (RolePrompt tuning, see smoke test above). - Minor: `escalateIterationCap`'s verdict-artifact-before-notify ordering; dead `internal/web/templates/office.html`; `processHandoffs`'s `Limit: 50` (latent durability edge at higher throughput); a swallowed `GetRun` re-fetch error in `spawnExternal`. ## Test plan - [x] `go build ./...`, `go vet ./...`, `go test ./...` — green at every commit, and at the final HEAD (`915771d`) - [x] Tasks 1–14 implemented and reviewed (fresh implementer + fresh reviewer per task, iterating to approval) - [x] End-to-end smoke test — two full pipelines run live against a real DeepSeek-backed scratch instance, both reaching the iteration cap and escalating correctly; block/resume mechanics verified deterministically - [x] Final whole-branch review — one gating issue found and fixed (`parseVerdict`), re-reviewed and approved ## Remaining (post-merge) - [ ] Deploy: rebuild/restart the production process with `-agents`, delete stale `carin.old` in the repo root, update README/AGENTS.md
- model: Add RunModeAutonomous='autonomous' to valid run modes
- webhook: Dispatch run.assigned event when run enters queued status
- store: Add ClaimRun with ErrNotAssigned guard — only assigned agent can claim
- api: Add POST /runs/{id}/claim endpoint, ErrNotAssigned→409 mapping
- client: Add ClaimRun helper and CLI 'run-claim' subcommand
- web: Add autonomous to UI mode select
- docs: Update README with new mode, API route, and CLI command
- skill: Rewrite carin-client skill with autonomous protocol workflow
- agent poll: finds next queued run, claims it, prints full context
- agent complete <id> <summary>: posts summary artifact, marks done
- agent fail <id> <reason>: posts failure artifact, marks failed
- Add AddArtifact + ArtifactParams to Go client
- Allow queued→failed transition in run state machine
- New /ui/office page showing all agents as animated desk cards
- CSS keyframe animations: green pulse (working), amber blink (queued), grey (idle)
- SSE-powered real-time refresh via existing /stream EventSource
- Agent data derived from runs table (no separate agent registry needed)
- Desk cards show: agent name, run count, current run title + mode pill
- Dark theme matching night-signal-desk vocabulary
- No new dependencies — pure Go html/template + CSS
- Nav link added to dashboard header
Dispatch loop now lives inside the Carin server process — no cron, no bash
scripts, no Hermes scheduler middleman. One goroutine polls every 30s,
claims queued runs, spawns agents as subprocesses, and updates the ledger.

New model fields (opencode):
- Priority, RetryCount, MaxRetries, BackoffUntil, LastHeartbeatAt,
  IdempotencyKey

Dispatcher features:
- Priority queue (highest priority first, then oldest)
- Stale run recovery (auto-fail/re-queue after 10m no heartbeat)
- Retry with exponential backoff (1m→2m→4m→30m cap, max 3 retries)
- Agent spawn: hermes-sub (fire-and-forget), pi, opencode, codex
- Output capture → artifact posting → status update

New API endpoints (pi):
- POST /runs/{id}/heartbeat — agents send heartbeats
- GET /runs?stale=10m — query stuck runs

Schema migration handles SQLite's ALTER TABLE UNIQUE limitation
by adding idempotency_key as TEXT then creating a unique index.

Agents proven: hermes-sub (research), pi (coding). Opencode deprecated.

All built autonomously via Carin runs #17-#26.
Inbound webhook handler (pi — run #27):
- POST /webhooks/forgejo accepts Forgejo/GitHub push events
- HMAC-SHA256 verification (X-Hub-Signature-256 / X-Forgejo-Signature)
- Unified payload parsing for both platforms
- Idempotency via delivery ID (X-GitHub-Delivery / X-Forgejo-Delivery)
- Auto-creates project from repo name if not found
- Creates review run for hermes-sub with commit details as prompt
- Priority 5 for main/master pushes, 0 for others
- FindRunByIdempotencyKey store method

Parallel dispatcher:
- Up to 3 concurrent external agent processes via semaphore
- Hermes agents are fire-and-forget (no slot consumed)
- External agents (pi, opencode, codex) take a slot until done
- Each tick fills available slots with queued runs, respects priority
When a run completes with handoff_agent set, the dispatcher auto-creates
a follow-up coding run seeded with the research artifact content.

New model fields:
- HandoffAgent (string) — agent name for the follow-up run
- HandoffPrompt (string) — prompt template; {artifact_summary} is replaced

Flow: research completes → dispatcher creates coding run (queued) →
next tick picks it up → coding agent processes it.

All features now complete for zero-intervention operation.
Handoffs now work for ALL agents (hermes-sub fire-and-forget + external).
processHandoffs() runs on every dispatcher tick, detecting completed runs
with handoff_agent set and auto-creating follow-up coding runs.

Proven: run #31 (hermes-sub research → #34 pi coding) and #32→#33
both completed end-to-end with zero human intervention.
- internal/forgejo/client.go — posts commit statuses via Forgejo/Gitea API
- Webhook handler now accepts Forgejo client, sets 'pending' status on push
- Context summary includes forgejo metadata for post-review status updates
- Dispatcher accepts Forgejo client for future post-review status posting
- Flags: -forgejo-token, -forgejo-url (or env vars FORGEJO_API_TOKEN, FORGEJO_URL)

Needs: API token from https://durandal.exe.xyz/user/settings/applications
(scopes: write:repository) to enable commit status checks.
Fix the StaleRuns query to check COALESCE(last_heartbeat_at, started_at) instead of only started_at.
This prevents long-running but alive and heartbeating runs from being incorrectly reaped.

- Updated StaleRuns WHERE clause to honor last_heartbeat_at
- Added TestStaleRunsHonorsHeartbeat to verify fresh heartbeats prevent staleness detection
- All tests pass

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VV1e8zMXrz6Ny5ktDwZrDL
RetryRun now takes a backoff duration and writes backoff_until instead of
requeuing immediately. New store.NextQueuedRun does priority/backoff/
per-project-serialization selection in one SQL query, replacing the
dispatcher's client-side nextQueuedRun sort over ListRuns.
Wrap cmd.Run() in spawnExternal with a background heartbeat goroutine
that calls UpdateRunHeartbeat every 60s. The goroutine is stopped via
context cancellation the moment the process exits, preventing Task 1's
stale-run reaper from killing mid-flight external agent processes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VV1e8zMXrz6Ny5ktDwZrDL
Allow runs to transition from waiting to queued status, enabling the
resume flow. Adds model.RunStatusQueued to the RunStatusWaiting entry
in the runTransitions map.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VV1e8zMXrz6Ny5ktDwZrDL
Implements client-side methods wrapping existing server endpoints:
- HeartbeatRun: POST /runs/{id}/heartbeat
- LinkRunThread: POST /runs/{id}/link-thread
- RunParams gains: Priority (already existed), HandoffAgent, HandoffPrompt, HandoffMode

All tests pass (new test: TestHeartbeatAndLinkThread).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VV1e8zMXrz6Ny5ktDwZrDL
processRun now creates/links a mailbox thread for each run right after
claim via the new ensureRunThread (idempotent), and the spawn switch
operates on the claimed (post-claim) run rather than the pre-claim
snapshot. createHandoffRun propagates the parent run's ThreadID so a
handoff pipeline shares one continuous, watchable thread.
processRun now looks up d.personas[AgentName] and dispatches on persona.Kind
instead of a hardcoded agent-name switch. spawnExternal takes a Persona,
builds each session's prompt via the new buildAgentPrompt (identity, Carin
CLI protocol incl. say/block/complete/fail/post-artifact, context, task),
and generates a crypto/rand session id for pi runs (empirically verified pi
0.80.3 accepts a fresh --session-id in --print mode, posted as artifact
pi-session for Task 10's resume). Completion is now status-aware: after the
process exits the dispatcher re-fetches the run and only stamps
completed/failed/retry if it is still "running", so agents that already
called carin agent complete/fail/block keep their own outcome. Also fixes
buildHermesPrompt's post-artifact argument order and lets postArtifact take
an explicit artifact name instead of deriving "<agent>-output.md" itself.
Reference doc for the in-progress dispatcher rework on this branch.
starsetbyte left a comment

Review: autonomous-iterative-loop

Verdict: Looks good — continue in the morning. Not merging yet, there are 4 tasks remaining. But the core is solid.


Architecture —

The three-layer design is right:

Layer What Why it works
Dispatcher In-process goroutine, 30s poll, zero-token idle Replaces cron bash gate cleanly
Agent protocol `carin agent poll say
Personas architect → builder → reviewer chain Handoff pipeline is declarative, extensible

The NextQueuedRun query with per-project serialization (NOT IN project_id WHERE status=running) is clever — same-project runs can't collide in the queue.


Task 10 — Correct

The shouldResumeSession + lastQueuedTransitionFrom logic correctly handles the compound sequence:

Block → answer → re-resume → crash → retry
 ^waiting          ^queued(←waiting)   ^queued(←running)

lastQueuedTransitionFrom finds the most recent queued-transition. After crash retry, that's running → queued (from RetryRun), not the stale waiting → queued. So shouldResumeSession returns false — spawns fresh, correctly.

TestShouldResumeSessionFalseAfterBlockThenCrashRetry covers exactly this. Good test.


Issues

1. TOCTOU race (acknowledged, accepted for now)

tick() spawns goroutines; NextQueuedRun filters by project in SQL, but ClaimRun (the actual status bump to running) happens inside the goroutine. Two same-project runs can pass the filter before either claims. Consequence is mild (one wastes a claim, other gets it). Track as a follow-up — fix would be moving claim into NextQueuedRun or a project mutex.

2. Swallowed error in status-aware completion

spawnExternal ~line 650:

current, gerr := d.store.GetRun(ctx, run.ID)
if gerr == nil && current.Status != model.RunStatusRunning {

If GetRun errors, we silently fall through and may overwrite the agent's self-reported status. Add a log:

if gerr != nil {
    log.Printf("[dispatch] GetRun for status-check on run #%d: %v", run.ID, gerr)
}

3. Minor: pi-session artifact kind

postArtifact hardcodes ArtifactMarkdown for everything including the session ID hex string. Harmless but untidy. Consider passing kind as a parameter.


Remaining (11–14)

Task Scope
11 parent_run_id, handoff_mode schema + generalized handoff
12 Review verdict loop — `VERDICT: approve
13 carin pipeline-create CLI
14 Full pipeline smoke test against sandbox

Summary

Area Status
Dispatcher core
Agent protocol
Personas + handoff
Session resume (Task 10)
Test coverage
TOCTOU race ⚠️ Track, follow-up
Swallowed error 🔧 One-line log
Remaining tasks 11–14

Continue in the morning. Solid foundation.

## Review: `autonomous-iterative-loop` **Verdict: Looks good — continue in the morning.** Not merging yet, there are 4 tasks remaining. But the core is solid. --- ### Architecture — ✅ The three-layer design is right: | Layer | What | Why it works | |---|---|---| | **Dispatcher** | In-process goroutine, 30s poll, zero-token idle | Replaces cron bash gate cleanly | | **Agent protocol** | `carin agent poll|say|block|complete|fail|post-artifact` | Agents self-report into the board; dispatcher only spawns | | **Personas** | architect → builder → reviewer chain | Handoff pipeline is declarative, extensible | The `NextQueuedRun` query with per-project serialization (`NOT IN project_id WHERE status=running`) is clever — same-project runs can't collide in the queue. --- ### Task 10 — ✅ Correct The `shouldResumeSession` + `lastQueuedTransitionFrom` logic correctly handles the compound sequence: ``` Block → answer → re-resume → crash → retry ^waiting ^queued(←waiting) ^queued(←running) ``` `lastQueuedTransitionFrom` finds the *most recent* queued-transition. After crash retry, that's `running → queued` (from `RetryRun`), not the stale `waiting → queued`. So `shouldResumeSession` returns false — spawns fresh, correctly. `TestShouldResumeSessionFalseAfterBlockThenCrashRetry` covers exactly this. Good test. --- ### Issues **1. TOCTOU race (acknowledged, accepted for now)** `tick()` spawns goroutines; `NextQueuedRun` filters by project in SQL, but `ClaimRun` (the actual status bump to `running`) happens inside the goroutine. Two same-project runs can pass the filter before either claims. Consequence is mild (one wastes a claim, other gets it). Track as a follow-up — fix would be moving claim into `NextQueuedRun` or a project mutex. **2. Swallowed error in status-aware completion** `spawnExternal` ~line 650: ```go current, gerr := d.store.GetRun(ctx, run.ID) if gerr == nil && current.Status != model.RunStatusRunning { ``` If `GetRun` errors, we silently fall through and may overwrite the agent's self-reported status. Add a log: ```go if gerr != nil { log.Printf("[dispatch] GetRun for status-check on run #%d: %v", run.ID, gerr) } ``` **3. Minor: `pi-session` artifact kind** `postArtifact` hardcodes `ArtifactMarkdown` for everything including the session ID hex string. Harmless but untidy. Consider passing kind as a parameter. --- ### Remaining (11–14) | Task | Scope | |---|---| | 11 | `parent_run_id`, `handoff_mode` schema + generalized handoff | | 12 | Review verdict loop — `VERDICT: approve|revise` → auto-create fix run | | 13 | `carin pipeline-create` CLI | | 14 | Full pipeline smoke test against sandbox | --- ### Summary | Area | Status | |---|---| | Dispatcher core | ✅ | | Agent protocol | ✅ | | Personas + handoff | ✅ | | Session resume (Task 10) | ✅ | | Test coverage | ✅ | | TOCTOU race | ⚠️ Track, follow-up | | Swallowed error | 🔧 One-line log | | Remaining tasks | 11–14 | Continue in the morning. Solid foundation.
# Conflicts:
#	cmd/carin/main.go
#	internal/dispatch/dispatch.go
A run that crashes and is retried before ever blocking gets a fresh pi
spawn (and a fresh "pi-session" artifact) on each retry, since there is
no uniqueness constraint on (run_id, name) in the artifacts table. If
that run later genuinely blocks and is answered, priorSessionID was
returning the oldest matching artifact (first-match-wins with an early
return) instead of the most recent one, resuming pi with a stale
session id. Fix: iterate all artifacts and keep the last match, since
store.Artifacts returns rows oldest-first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154Kr7ggbM9JhZqSWz3Mqy1
processHandoffs routes completed pipeline review runs (Mode==review with
a ParentRunID) into processVerdict: approve posts a high-priority result
to cassie and stops the chain, revise-under-cap spins up a fix run for
the builder persona (self-wiring its handoff back to review), and
revise-at-cap (or an unparseable verdict, treated as revise defensively)
escalates with an approval_request. Dedup via a verdict artifact plus
HasChildRun keeps repeat dispatch ticks from double-processing. Standalone
webhook-triggered review runs (no ParentRunID) are left untouched, as
before this feature existed.
Adds the one-command pipeline kickoff: pipeline-create takes a project,
title, and goal, then creates and queues a single root research run with
explicitly-set handoff fields (architect -> builder flow). Persona defaults
(Task 11) wire everything downstream: the dispatcher claims the research
run, applies the persona's child handoff rules, and boots the loop.

Changes:
- client/runs.go: Add MaxIterations to RunParams; wire it through CreateRun's body map
- cmd/carin/client_runs.go: Implement cmdPipelineCreate per brief specs
- cmd/carin/main.go: Add "pipeline-create" to client-mode switch
- cmd/carin/client.go: Add dispatch case for "pipeline-create"

Manual verification: created a run via pipeline-create on demo project;
confirmed via 'carin runs' that it appears queued with HandoffAgent,
HandoffMode, and MaxIterations populated as expected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgSaVzHV5DYK7iMvqFLmJz
The office view now shows a live mailbox feed (store.RecentActivity) beside
the desks grid, and agentColor gets a stable hash fallback (p1..p6) instead
of lumping every non-named persona (e.g. pi-spawned "pi-builder") into
"system". Desk cards link to their active run; feed entries link to their
thread. SSE-driven refresh of the office panel is debounced via the
dashboard shell's existing event stream, reusing the already-present
message.* event types.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154Kr7ggbM9JhZqSWz3Mqy1
Live end-to-end runs showed reviewer completions always have trailing
prose or markdown after "VERDICT: approve/revise" (e.g. "VERDICT: approve
— fix is correct..."), so the old strict-suffix match never fired and
every review fell through to the defensive revise path, eventually hitting
the iteration cap and escalating to a human even on a clean approval.

parseVerdict now finds the last case-insensitive "verdict:" occurrence,
strips leading whitespace/markdown wrapping, and prefix-matches
approve/revise on what follows — tolerating trailing commentary while
still resolving on the true final verdict (not an earlier instructional
mention) and never treating ambiguous/garbled text as approve.

Also gofmt -w on client/runs.go, internal/dispatch/dispatch.go, and
internal/dispatch/personas.go (pure struct/map-literal alignment
whitespace, no functional change) per a separate Minor finding from the
same review.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154Kr7ggbM9JhZqSWz3Mqy1
starsetbyte changed title from autonomous-iterative-loop to carin: autonomous iterative dev loop 2026-07-04 15:38:47 +00:00
Author
Owner

🔍 Carin Code Review

Run #219 completed by pi-dispatcher


cba605c4a4267712911188de26e14630


Posted by CarinView run

## 🔍 Carin Code Review *Run [#219](http://astraea1:8787/ui/runs/219) completed by `pi-dispatcher`* --- cba605c4a4267712911188de26e14630 --- *Posted by [Carin](http://astraea1:8787/ui/runs/219) • [View run](http://astraea1:8787/ui/runs/219)*
Author
Owner

🔍 Carin Code Review

Run #219 completed by pi-dispatcher


Dispatch Summary — Run #219

Task

Review PR #1 in starsetbyte/Carin (test/pr-pipeline → main, author pyrrha-test).
PR URL: #1
Diff URL: #1.diff

Routing decision

Single dispatch → pi-reviewer (run #223)

Why

  • The task is explicitly a code review: "review this pull request", "fetch the diff and provide a structured review."
  • Per routing rule #3: reviewing existing changes → pi-reviewer.
  • No implementation, no planning, no research, no creative work required — so no architect/builder/researcher/creative stage is warranted.
  • The PR is an automated end-to-end pipeline test, expected to be a small/synthetic change, so a single reviewer pass is the correct scope. No need for a chain.

Persona chosen

pi-reviewer (deepseek-v4-pro) — the dedicated review persona, capable of fetching the diff, running checks, and issuing a verdict.

Run created

Field Value
Run # 223
Agent pi-reviewer
Mode review
Thread #380
Status queued

The reviewer has been instructed to post its full review as review.md and end its completion summary with VERDICT: approve or VERDICT: revise, which Carin's pipeline will parse to drive the next step (merge or fix loop).

Not routed

  • pi-architect: no plan needed (this is review, not greenfield design).
  • pi-builder: no code to write.
  • pi-researcher: no external research/investigation beyond the diff itself.
  • Chain: a single review is a one-shot pass; a chain would add latency without value.

Handoff

Dispatch is complete. The pipeline will pick up run #223, spawn pi-reviewer, and act on its verdict.


Posted by CarinView run

## 🔍 Carin Code Review *Run [#219](http://astraea1:8787/ui/runs/219) completed by `pi-dispatcher`* --- # Dispatch Summary — Run #219 ## Task Review PR #1 in starsetbyte/Carin (test/pr-pipeline → main, author pyrrha-test). PR URL: https://durandal.exe.xyz/starsetbyte/Carin/pulls/1 Diff URL: https://durandal.exe.xyz/starsetbyte/Carin/pulls/1.diff ## Routing decision **Single dispatch → pi-reviewer** (run #223) ### Why - The task is explicitly a **code review**: "review this pull request", "fetch the diff and provide a structured review." - Per routing rule #3: *reviewing existing changes → pi-reviewer*. - No implementation, no planning, no research, no creative work required — so no architect/builder/researcher/creative stage is warranted. - The PR is an automated end-to-end pipeline test, expected to be a small/synthetic change, so a single reviewer pass is the correct scope. No need for a chain. ### Persona chosen `pi-reviewer` (deepseek-v4-pro) — the dedicated review persona, capable of fetching the diff, running checks, and issuing a verdict. ## Run created | Field | Value | |---|---| | Run # | 223 | | Agent | pi-reviewer | | Mode | review | | Thread | #380 | | Status | queued | The reviewer has been instructed to post its full review as `review.md` and end its completion summary with `VERDICT: approve` or `VERDICT: revise`, which Carin's pipeline will parse to drive the next step (merge or fix loop). ## Not routed - pi-architect: no plan needed (this is review, not greenfield design). - pi-builder: no code to write. - pi-researcher: no external research/investigation beyond the diff itself. - Chain: a single review is a one-shot pass; a chain would add latency without value. ## Handoff Dispatch is complete. The pipeline will pick up run #223, spawn pi-reviewer, and act on its verdict. --- *Posted by [Carin](http://astraea1:8787/ui/runs/219) • [View run](http://astraea1:8787/ui/runs/219)*
Author
Owner

🔍 Carin Code Review

Run #219 completed by pi-dispatcher


cba605c4a4267712911188de26e14630


Posted by CarinView run

## 🔍 Carin Code Review *Run [#219](http://astraea1:8787/ui/runs/219) completed by `pi-dispatcher`* --- cba605c4a4267712911188de26e14630 --- *Posted by [Carin](http://astraea1:8787/ui/runs/219) • [View run](http://astraea1:8787/ui/runs/219)*
Author
Owner

🔍 Carin Code Review

Run #219 completed by pi-dispatcher


cba605c4a4267712911188de26e14630


Posted by CarinView run

## 🔍 Carin Code Review *Run [#219](http://astraea1:8787/ui/runs/219) completed by `pi-dispatcher`* --- cba605c4a4267712911188de26e14630 --- *Posted by [Carin](http://astraea1:8787/ui/runs/219) • [View run](http://astraea1:8787/ui/runs/219)*
Author
Owner

🔍 Carin Code Review

Run #219 completed by pi-dispatcher


cba605c4a4267712911188de26e14630


Posted by CarinView run

## 🔍 Carin Code Review *Run [#219](http://astraea1:8787/ui/runs/219) completed by `pi-dispatcher`* --- cba605c4a4267712911188de26e14630 --- *Posted by [Carin](http://astraea1:8787/ui/runs/219) • [View run](http://astraea1:8787/ui/runs/219)*
Sign in to join this conversation.
No reviewers
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
starsetbyte/Carin!1
No description provided.