Harvey /loop — Implementation Plan

See loop-design.md for the full design rationale.

New Module Dependencies

None. Implemented with existing Harvey packages, time, and the standard library signal-handling pattern already used three times in terminal.go.

Files to Create

File Purpose
harvey/loop.go cmdLoop, argument parsing, the iteration loop, sleep-with-cancellation
harvey/loop_test.go Unit tests for parsing and the iteration loop (with mockLLMClient)

Files to Modify

File Change
harvey/terminal.go Factor the inline chat block (~lines 635-820) into (a *Agent) runChatTurn(ctx, input, out) (reply string, stats ChatStats, err error); REPL loop becomes a thin wrapper around it
harvey/commands.go Register "loop" in registerCommands(); add a "loop" case to cmdHelp’s topic switch and the two topic-list strings (commands.go:566 and 596-600)
harvey/helptext.go Add LoopHelpText constant

Implementation Phases

Phase 1 — Extract runChatTurn from the REPL chat block

Pull the body of the REPL’s plain-chat branch (RAG augmentation through RecordTurnWithStats, roughly terminal.go:681-830) into:

func (a *Agent) runChatTurn(ctx context.Context, input string, out io.Writer) (reply string, stats ChatStats, err error)

Phase 2 — Argument Parsing

func parseLoopArgs(args []string) (interval time.Duration, count int, rest string, err error)

Phase 3 — Iteration Dispatch

func runLoopIteration(ctx context.Context, a *Agent, rest string, out io.Writer) (exitRequested bool, err error)

Phase 4 — Sleep With Cancellation

func sleepInterruptible(ctx context.Context, d time.Duration) (cancelled bool)

Phase 5 — Orchestrator

func cmdLoop(a *Agent, args []string, out io.Writer) error
  1. parseLoopArgs — on error, print usage and return nil (matches other commands’ “print usage, don’t error” convention, e.g. /pipeline with no threshold)
  2. Print plan summary: Looping every %s, up to %d time(s): %s
  3. Build one cancellable context.Context + SIGINT watcher for the whole run
  4. Loop i := 1..count:
    • runLoopIteration
    • if it reports exitRequested, print loop: stopping — %q would exit Harvey and break
    • if cancelled, break
    • if i < count, sleepInterruptible; if it reports cancellation, break
  5. Print summary: Loop finished after %d/%d iteration(s) or Loop cancelled after %d/%d iteration(s)

Phase 6 — Help Text & Registration

Phase 7 — Tests (loop_test.go)

Test Covers
TestParseLoopArgs_valid 5m, 30s, 300, with and without --count
TestParseLoopArgs_invalid Bad duration, zero/negative interval, --count out of range or non-numeric, empty prompt
TestCmdLoop_chatMode N iterations of a plain prompt against mockLLMClient; asserts a.History grows by 2×N messages
TestCmdLoop_commandMode Loops a harmless built-in (/status) N times; asserts no chat call is made
TestCmdLoop_exitSentinel Looping /exit stops the loop without exiting Harvey
TestCmdLoop_countCap --count 0 and --count 101 rejected with usage messages
TestSleepInterruptible_cancel Cancelling the context returns promptly without waiting the full duration
TestRunChatTurn_* Coverage for the extracted helper — add minimal tests in Phase 1 if the inline block had none

Acceptance Criteria