Skip to content

Running the loop

GoalStart, pause, resume and stop a loop, and read its exit codes.

How the loop is built

The loop is two programs inside each other. The supervisor runs for hours; the runner runs one unit and exits.

LayerLives forDoes
supervisorthe whole runtakes the lock, runs batches of units, reads PAUSE and STOP between units, reacts to exit codes, waits out usage limits
runnerone unitpicks a task, takes a snapshot, starts the coordinator, checks the contracts, records the result, exits with a code
coordinatorone unitthe claude -p process: reads the task and calls specialists
specialistspart of a unitdo one narrow job each and leave a verdict
contractsthe end of a unitscope, task file, trace, gate, closing: code that decides whether the unit counts

Step through five runs to see which layer is busy, what the trace folder holds, and how pause, stop, a hard stop and a usage limit play out.

The loop: supervisor, runner, coordinator, specialists and contracts
supervisorthe whole runrunning
runnerone unit
coordinatorclaude -p
specialistsone job each
contractscode checks

Units

    .harness/trace/

    • supervisor.lock
    harness run starts the supervisor. It takes the lock .harness/trace/supervisor.lock, so a second loop on this checkout refuses to start.
    Step 1 of 6

    Every command below takes --project, with a path, or the id or name the project was registered under. Without it, the command uses the current folder, which must contain a .harness/ folder. The examples use the demo's sample project, created and registered with harness example ~/code/bookmarks; the project name defaults to the folder name.

    Start

    From the dashboard, press Start. It spawns harness run --project <path> as a detached process, with the dashboard's own environment and its output in .harness/trace/supervisor.out. The dashboard's Pause, Resume and Stop write the same files as the CLI; the dashboard never signals or kills a process. Its dock shows the supervisor as Running, Paused, Stopping (STOP is written, the unit is finishing) or Not running. The button names who a Start runs: Start · claude, or Start · stand-in when AGENT_CMD or agent.command is set; the dashboard's first line in the terminal says the same.

    From a terminal:

    harness run --project bookmarks
    

    By default the run does not end when the queue is empty: it checks for new work every supervisor.idlePollSeconds (30 seconds by default) until you stop it. A human action wakes it at once: a submit, an answer or an approval, from the CLI or the dashboard, lands in the human-write ledger, and the idle supervisor watches that file every supervisor.pausePollSeconds (2 seconds). These options change that, and harness unit runs a single unit to try things out:

    harness run --project bookmarks --deadline 120   # a time limit for the run, in minutes
    harness run --project bookmarks --max-units 5    # at most five units
    harness run --project bookmarks --once-through   # exit as soon as nothing is left to take
    harness unit --project bookmarks                 # exactly one unit; exits with the runner's code
    

    Two commands are worth running first, and neither spends a token:

    harness agents --check --project bookmarks   # the registry, standing-rules drift, the model split
    harness preview T-002 --project bookmarks    # the exact command and prompt of the next unit
    

    Before the first unit, the supervisor checks two things and refuses to start if either fails:

    • No other loop runs on this checkout. It holds the lock file .harness/trace/supervisor.lock for the whole run.
    • The base branch is clean. It refuses a dirty tree it did not make, because the scope contract would blame your changes on the agent.

    Right after its start line, it logs one line, supervisor · billing: …, that says how the run is billed, from the environment it starts in:

    The line saysWhen
    billing: subscription login — usage limits, not a per-token bill; costs shown are API-equivalentclaude is logged in with a subscription
    billing: ANTHROPIC_API_KEY is set — claude -p bills the API per tokenthe environment has ANTHROPIC_API_KEY
    billing: a stand-in replaces claude — no tokensAGENT_CMD or agent.command is set

    On a subscription, the costs on the dashboard and in harness stats are what the same tokens would cost on the API. They are not a bill.

    Pause and resume

    harness pause --project bookmarks
    harness resume --project bookmarks
    

    harness pause writes the file .harness/trace/PAUSE. harness resume deletes it. The dashboard's Pause and Resume buttons write the same file.

    The pause does not interrupt the unit that is running. The supervisor reads the file between units, so the unit in flight finishes, its contracts run, and then the loop waits.

    Stop

    harness stop --project bookmarks
    

    harness stop writes .harness/trace/STOP. The supervisor finishes the current unit, exits, and does not come back until you start it again.

    If you have to stop it at once, stop the supervisor process you started, by its PID. Because the disk is the state, you lose only the unit that was running. Never kill processes by pattern (pkill -f, killall): the pattern matches far more than you think, including other Claude sessions.

    Exit codes

    Every unit ends with an exit code from the runner. The supervisor reads it and decides what to do next.

    Runner codeMeaningWhat happens next
    0the unit is donethe supervisor takes the next unit
    1a hard stop: scope, task file, trace, gate, or the agent crashedthe unit's work goes into a named stash, the task is parked, the supervisor continues
    2the configuration is broken: config.json, drifted standing rules, or a judge on the implementer's or the tester's modelthe supervisor stops: waiting will not fix it
    3nothing is left to takethe supervisor polls for new work; with --once-through it ends the run
    4the agent could not run: usage limit, network, loginthe supervisor waits, then retries

    A hard stop throws nothing away. The runner moves the unit's changes into a git stash named harness quarantine: T-NNN unit N, restores the tree, and writes the reason into the task. You can inspect the stash before you reopen the task.

    The supervisor has its own exit codes, for when the whole run ends. The numbers repeat, but the meanings differ: runner code 3 is an empty queue, and supervisor code 3 is a stuck loop.

    Supervisor codeWhy the run ended
    0it was stopped, the queue drained under --once-through, or the deadline or --max-units was reached
    2the configuration is broken, or deliver.pr is on and a remote, gh, its login or the base branch on the remote is missing
    3barren: three units in a row moved nothing
    4it gave up waiting for the agent

    The barren rule keeps a stuck loop from spinning: three units in a row that moved nothing stop it. Progress means new commits or new handoffs, not just one of them.

    Usage limits and outages

    Exit code 4 means the agent never got to work, so the task is untouched. The supervisor waits and tries again: it probes the network every 30 seconds, and when the network is fine but the agent is still refused, for example by a usage limit, it naps for 30 minutes before the next try. When the refusal names a reset time, such as "You've hit your session limit · resets 9:10pm (Europe/Kiev)", it waits until that time plus one minute instead, and logs agent could not run (…) — waiting until <time>. Session and weekly limits count as usage limits too. After six hours of waiting it gives up and stops with code 4. The timings are supervisor.usageProbeSeconds, supervisor.usageNapSeconds and supervisor.usageCeilingSeconds in config.json.

    A project can add its own probe: command to config.json, for example a check that the dev server answers. When the probe fails, the unit ends with code 4 and nothing is touched.

    Do not edit while a unit runs

    The scope contract compares the whole working tree before and after a unit. It cannot tell your edit from the agent's. If you save a file in the project while a unit runs, the unit fails, and the task is parked for something you did.

    The safe order:

    1. Pause.
    2. Wait until the running unit finishes. The dashboard shows when nothing is in flight.
    3. Make your edit and commit it.
    4. Resume.

    Task actions are the exception. Approve, answer, revise and the rest, from the dashboard or the CLI, go through a ledger: the harness records each human write to a task file in .harness/trace/human-writes.jsonl, and the scope contract excuses a task file whose content matches that record. An approve made while another task's unit runs is kept. Inside a unit, the human CLI actions refuse to run, so an agent cannot pose as you. The ledger stops mistakes, not an agent that forges the ledger itself.

    The same reason allows only one loop per checkout. Running tasks side by side needs one git worktree per task, which is planned after the first version.

    Watching a run

    harness status --project bookmarks
    harness status --project bookmarks --json   # the same, for scripts
    harness stats --project bookmarks           # units, time, tokens and API-equivalent cost per stage and task
    harness stats --project bookmarks --json    # the same, for scripts
    

    The dashboard's Live view shows more: the supervisor's state, the unit in flight, its agents with their role, elapsed time and current tool, the pace, and the supervisor log. The snapshot behind it carries role names, tool names and file names only, never tool arguments or message text.

    Live trace during unit 12. Durations are real; the costs are demo numbers. Walkthrough on the Dashboard page →
    The Live trace screen: the unit list, the running qa unit with its agents, and the supervisor log.

    Telegram notifications

    The harness can tell you in Telegram when a task needs you (harness/notify.ts). It needs two keys, TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID, and looks for them in this order. The first place that defines the token decides:

    1. the environment;
    2. <project>/.env;
    3. $HARNESS_HOME/.env, which is ~/.harness/.env by default.

    An empty token switches notifications off. harness projects add puts .env into the project's .gitignore, so the keys stay out of git.

    EventWhen it fires
    needs-inputa task waits for your answers
    planneda plan waits for your approval (only with autonomy.mode: gated)
    prototypea prototype round waits for your review (only with gated)
    parkeda task is parked, with the reason
    donea task is confirmed
    delivereda pull request is open, with its link
    supervisora run ended by itself, or the first usage-limit wait began

    All events are on by default; notify.events in config.json narrows the list. A message carries the project name, the task id and title, the event and the pull request link. It never links to the local dashboard. Notifications go one way: the harness only sends. A failed send never fails a unit; it adds one notify · … not sent line to progress.log, with the token removed.

    Telegram gets each message once. A repeat, with the same project and text after ids and times are set aside, is held back for six hours, and the whole chat gets at most six messages a minute. A held-back message logs notify · <event> held back: <why>, and the record lives in $HARNESS_HOME/notify-sent.json. A project folder that no longer exists sends nothing, and an agent outage is reported once.

    harness notify --project bookmarks          # where the keys come from; never prints the token
    harness notify --project bookmarks --test   # send one test message
    

    What is in the trace folder

    Everything the loop knows about itself is in .harness/trace/. The project's .gitignore keeps trace/, artifacts/, tasks/ and JOURNAL.md out of git; only config.json is versioned. Task files are ignored on purpose: the runner rewrites them while it switches between the base branch and the task branch, and a versioned task file that differs between branches would make the checkout fail. A task's history lives in its ## Log and in handoffs.jsonl.

    FileWhat it is
    supervisor.lockheld by the running supervisor; a second loop refuses to start
    PAUSEpresent = pause at the next unit boundary
    STOPpresent = stop at the next unit boundary
    running.jsonthe task a unit is working on right now
    progress.logthe supervisor's log
    supervisor.outthe supervisor's output when the dashboard started the run
    handoffs.jsonlwhat each unit handed to the next: start time, verdict, tokens, cost and any permission denials
    unit-seqthe counter that numbers units
    unit-NNNN.jsonlone file per unit: the agent's stream-json output, each line stamped with harness_ts; the trace contract finds the verdict here
    human-writes.jsonlevery human write to a task file, with its hash; the scope contract excuses a matching file
    unit-NNNN.agents.jsonthe specialists passed to that unit with --agents
    unit-NNNN.app.jsonthe app the runner started for that unit: its pid, port and owner; removed once the app stops
    unit-NNNN.app.logthat app's output
    unit-NNNN.mcp.jsonthe MCP config a browser unit got: the Playwright server and its flags
    preview.mcp.jsonthe same config, written by harness preview for an app stage
    NextContracts→
    Guide10 chapters · ~55 min
    1. 1Concepts
    2. 2Roles
    3. 3Coordination
    4. 4Task lifecycle
    5. 5Running the loopyou are here
    6. 6Contracts
    7. 7Dashboard
    8. 8Quickstart
    9. 9Your own project
    10. 10Lessons
    OverviewDemoExampleGitHub