Research

The Never-Sleeping Harness

12 August 2026 Tom Küstner Reading time 38 min

What this note is, and what it is not

It describes a pattern that emerged over several months of production use: how to let a coding agent work autonomously for hours without a human sitting next to it, and above all which failures occur along the way and how to recognize them.

It is deliberately setup agnostic. Every path, name and host appears as a placeholder in angle brackets; the concrete topology of an installation belongs in internal operations documentation, not here. What follows holds regardless of operating system, hosting and project structure.

It is not a product promise. Every statement comes from measurement in one concrete installation. Where something is merely plausible but unverified, that is stated explicitly. This distinction is half the value of the document.

A glossary of the technical terms appears as Appendix D at the end (section 15). If a term gives you trouble, you will find it there with a short definition and the reason it matters here.

Placeholders used throughout: <PROJECT> project directory · <AGENT-USER> unprivileged account the agent runs under · <CLI> the agent command line · <REPO> the version control remote · <PUBLISH-CMD> the hardened publication tool.


The problem

Coding agents with a command line interface can work through longer tasks on their own. In practice they run into three walls:

  1. Turn limits. A run ends after a fixed number of tool rounds, whether or not the task is finished.
  2. Usage limits. Providers cap consumption per time window. The agent stalls until the window resets, typically for hours.
  3. Context limits. Long conversations fill up; the agent loses the thread or aborts.

A human sitting next to it, typing “keep going” over and over, solves all three. The harness automates exactly that human, nothing more. Hence the name: it does not sleep when the model has to.


Basic architecture

   +----------------------------------------------------------+
   |  Watcher (shell script, runs in the background)          |
   |                                                          |
   |   while true:                                            |
   |     |- Poke: <CLI> in print mode, ONE JSON returned      |
   |     |- Evaluate JSON (subtype, is_error, result)         |
   |     |- Progress?  -> publish                             |
   |     |- Limit?     -> sleep until reset                   |
   |     |- Sentinel?  -> exit cleanly                        |
   |     +- Stalled?   -> abort (do not poke forever)         |
   +----------------------------------------------------------+
Standing command identical on every poke Poke fresh process · session is resumed Response: one JSON subtype · is_error · result Error flag set? check this first Sentinel in text? New revision? commit id before / after Stall counter +1 N reached → stop Waiting case, not a stall reset time from plain text · sleep · new session Clean exit exit 0 — nothing left in reach Publish hardened tool · counter back to 0 Stop exit 2 Turn cap hit keep poking yesno yesno yesno next poke
Fig. M27 · The poke cycle. The order of checks is the content: error flag first, then the sentinel, then progress. Orange marks the disguised limit, the path that cost Section 6.4.

The central trick: every poke is a fresh process, but the conversation outlives it. Most agent command lines persist sessions and can resume them. The agent therefore keeps thinking about the same task across process boundaries while the watcher restarts as often as it likes.

Why print mode (headless) and not the interactive interface: a script needs three things that only the non-interactive mode provides, namely programmatic startup, a machine readable ending (structured output instead of screen text) and a turn budget. An interactive session would have to be scraped off the screen, which is fragile and not auditable.

Skeleton of a poke (schematic):

output=$(su - <AGENT-USER> -c "
    cd '<PROJECT>' &&
    timeout $POKE_TIMEOUT <CLI> -p '$STANDING_CMD' \
        $RESUME_FLAG \
        --max-turns $MAX_TURNS \
        --output-format json
") || exit_code=$?

From the output the loop reads: result type, error flag, result text and the session identifier (to resume on the next poke).


The standing command

The watcher sends the same instruction on every poke. It is the only lever through which the agent learns what it is allowed to do. Four parts:

  1. Where the work comes from, meaning the task list in the project and its format.
  2. Which tasks are eligible, expressed as a sharp predicate (section 4).
  3. What is forbidden, phrased as a prohibition on touching, not on completing (section 6.2, the most expensive lesson).
  4. When to stop, meaning an unambiguous sentinel string the watcher looks for.

Wording rules that have proven themselves:

  • The text must be free of quotes, backticks and dollar signs. It crosses several shell layers; every special character is a risk of breakage.
  • The sentinel must be demanded as the only output (“output IMMEDIATELY, as your only output, exactly: …”). Otherwise the agent writes an essay about why it is finished and the watcher cannot reliably find the string.
  • The instruction must stay project independent. Project specifics belong in the agent instruction file inside the project directory, which the command line loads anyway.

The selection predicate, the single most important design decision

The agent must not do “something sensible” but only clearly delimited work. A two dimensional predicate in the task list has proven itself:

DimensionPurpose
StatusIs the task actionable right now?
Working modeMay it be handled without a human?
eligible  <=>  status in {open, in progress}  AND  mode in {autonomous modes}

The second dimension is the decisive one. Without it you have to infer from the task text whether something is delicate. With it the answer is a field value.

What belongs in the non autonomous modes (experience): schema and database changes, architectural decisions, anything that opens write paths to the outside, anything with an unclear specification. Rule of thumb: if a mistake would not be caught by a test, the task does not belong in autonomous mode.

An additional status such as “structured but not yet actionable” keeps the list clean: tasks can sit there fully described without the loop touching them.


When does a task count as done?

This is the core question of any automation involving a non deterministic actor. The answer: the agent’s own claim is not the criterion. There are four levels, and only two of them are deterministic:

LevelWho judgesdeterministic?
Status change in the task listthe agentno, it is a claim
Automated gates (tests, type check, linter, build)programsyes
Acceptance criteria of the taskphrased as a testyes, if written correctly
Sign off by a humanhumanno, and that is exactly the point

5.1 Phrase acceptance as a test, not as an intention

This is where determinism comes from, not from better prompts:

unusable (intention)usable (test)
“The list should be tenant separated""Access across a tenant boundary: test runs red against the previous version"
"Deleting should be safe""Deleting an item that still has references is rejected, visibly, no silent failure"
"Accessibility is fine”(no measurement without a number, not admissible)
“Path traversal is protected""Traversal fails at all three boundaries individually (three separate red proofs)”

Two rules follow: no measurement without a number, and a regression test is only worth something if it ran red against the old version and that was reported.

5.2 What the watcher itself understands (purely mechanical)

The watcher has no notion of “done”. It knows three signals:

  1. Sentinel, the agreed string in the result text, leading to a clean exit.
  2. Progress equals a new revision in version control (comparing the commit identifier before and after the poke). “The agent wrote something friendly” is not progress.
  3. Error flag, which separates genuine success from disguised failures (section 6.4).

Consequence for task planning: a task with no committable outcome is indistinguishable from idling as far as the watcher is concerned. Pure documentation tasks either need a commit as their conclusion, or they do not belong in autonomous mode.

5.3 Gates are necessary, not sufficient

In one production run several tasks completed with entirely green gates, and the subsequent hands on check by a human still found two real defects: an input limit that was too tight combined with a silent error message, and a write path that did not work at all. Human sign off is not a formality, it finds a different class of defect than any test.


Failure modes, the expensive lessons

This section is the actual value of the note. Every item was experienced and measured first hand.

6.1 The loop that never ran

A script that had been edited in one place was missing a quote, which meant a syntax error, and the script never started. For weeks the documentation claimed it was “operational”.

Rule: every change to a control script goes through a syntax check (bash -n or equivalent) before it is rolled out. And: “operational” is a measurement, not a statement of intent.

6.2 “Skip” does not mean “do not touch” to an agent

The standing command instructed the agent to skip non autonomous tasks. The agent read this as a prohibition on building and instead did helpful preparatory work: draft plans for locked tasks, a newly assigned task identifier (effectively a structural change), and the overwriting of an existing file that was not under version control.

Remarkably, it simultaneously kept other rules perfectly: not a single commit over the entire run, because its instruction file prescribed a commit gate for that class of task. The safety rings held; the interpretation of scope did not.

Rule: prohibitions for autonomous agents must forbid touching, not completing. The wording must explicitly rule out drafts, notes, new files, new identifiers, and preparing work for later. Helpfulness fills every gap a formulation leaves open.

6.3 Idling with no stop condition

After finishing all eligible tasks the agent did not emit the sentinel but explained, across more than sixty consecutive pokes, that it was waiting. Its reading: there are still open tasks, just none for it. The watcher kept poking because it had no abort criterion. Overnight this would have burned hours of quota for no result whatsoever.

Rule: a stall guard is mandatory. N consecutive successful pokes with neither a sentinel nor a new revision must abort cleanly with a dedicated exit code. An overall emergency cap is advisable on top.

6.4 Limits arrive in disguise

The single most important measurement: the usage limit was not reported as an error type but as a successful run with the error flag set, and the reset time appeared only as plain text in the result (”… resets

The consequence was bitterly ironic: the freshly built stall guard from section 6.3 aborted the run, because it could not distinguish “agent is spinning” from “agent is locked out and waiting”, since both arrived as success.

Rule: check the error flag before counting stalls. If it is set and the text carries a limit pattern, this is a waiting case, not a stall: parse the reset time from the plain text, sleep, start a fresh session, continue. For parsing: fail safe, meaning that if it fails you use a fixed wait rather than aborting.

And more generally: do not rely on assumed field names in API responses. Write every response to an append only file and read, at the first real incident, what actually arrives. An assumption documented as fact survives undetected for months.

6.5 Choose test parameters to match the test purpose

An idle test with a very small turn budget failed because the agent used up the budget merely reading the task list and never reached the sentinel. What was tested was therefore not what was intended.

Rule: a test of idling needs a budget that makes completion reachable at all. Small budgets test the resume path, not the completion path.

6.6 Small budgets are expensive

Measured: with a very small turn budget the loop consumed roughly 2.4 percent of the session quota per minute, because every poke has startup cost (reloading context, rereading the task list). Larger budgets get more real work done per startup.

Rule: generous turn budgets for production runs, small ones only to test the resume path.

6.7 The publication step needs its own verification

“Committed” is not “published”. An agent can truthfully report a commit while the remote remains unchanged. Conversely the local reference to the remote can be stale, so that an agent wrongly reports that something was not published (experienced first hand).

Rule: the state of the remote is queried independently (for instance via a remote listing command), never derived from local state, neither by the agent nor by the human checking.


Security architecture

An autonomously running agent is a process that writes files without asking. The following rings have proven themselves, and they held even when the scope rule from section 6.2 was violated:

  1. A dedicated unprivileged user. The agent never runs under the human’s account; its rights end at the project directory.
  2. A hardened publication tool instead of raw push rights. The agent calls a script with target and branch wired in, which rejects foreign targets. Raw push commands are blocked.
  3. Anything irreversible belongs in configuration, not in the prompt. A boundary set in conversation (“do not publish”, “wait for my review”) only holds while the message is in context and disappears with compaction. It is not a basis for authorization. Everything irreversible (publishing, deleting, migrating, installing) belongs in the permission configuration.
  4. Append only remotes. No force push, no branch deletion. The agent cannot rewrite history, not even by accident.
  5. A human as the gate for publication and sign off. Which steps stay bound to a human is a risk decision; that there are such steps is not.

These rings are not theoretical. In the incident from section 6.2 the agent broke rules about scope, yet over the entire run it produced not a single commit and no publication. The damage stayed inside the working tree and was undone with one command.


Observability: four layers

A loop without a screen is not opaque, it is inspectable with a delay:

  1. Watcher log, one line per poke: result type, error flag, session identifier, snippet of the result, and the watcher’s decision.
  2. Response history, every complete response appended to a running file (one JSON object per line). Do not overwrite: that is exactly how we lost the response in which the agent documented its own mistake.
  3. Version history, one task, one commit, with the identifier in the subject. This is the real audit, because it can be verified independently.
  4. Retrospective session view. Most command lines can reopen a headless session by its identifier in interactive mode, giving a complete, scrollable record of every tool call. Ideal for review, but only after the run has ended (two writers on one session is a bad idea).

Also recommended: a log of the instructions issued, kept inside the project by the agent itself. In our case it maintained this diligently even while violating other rules.

8.1 Instances know nothing about each other

Several agent sessions on the same project share no memory. They share only the file system and version control. An interactive session opened in parallel notices nothing of the loop until it reads the history. Coordination happens exclusively through artifacts, never through awareness.


Suggested build order

  1. Define the task format and write it down: status vocabulary, mode vocabulary, acceptance as a test, counting. First, because everything else depends on it.
  2. Automate the consistency check. A test that compares status locations against the counters and turns red when they diverge. Without it the list drifts silently.
  3. Set up the gates before anything runs autonomously. Without them a status change is a pure claim.
  4. Build the safety rings from section 7, before the first unsupervised run.
  5. Build the watcher, with stall guard and error flag check from the start. Retrofitting either one costs you a failed run.
  6. Idle test: the loop must prove that it does nothing when there is nothing to do, and that it ends cleanly. Only then does it get work.
  7. Supervised short run with one real task: resume after the turn cap, publication, completion.
  8. Unsupervised run, inside a detachable terminal session so that a dropped connection does not end it.

On operational robustness: a terminal session does not survive a reboot of the machine. Anyone who wants to run the loop permanently needs a service unit with a restart policy. For runs of a few hours the simple variant is enough.


Honest limitations

  • The agent remains non deterministic. The harness does not make it more reliable, it makes it more verifiable. Anyone without automated gates simply gets more unchecked code faster, which is not a desirable outcome.
  • Human sign off does not go away (section 5.3).
  • Provider behavior changes. Response formats and limit semantics are not contractually guaranteed. Exactly for that reason: log every response and read it at the first deviation.
  • Cutting tasks well stays human work. The loop works through what is well described. A poorly specified task does not get better through automation, it gets implemented wrongly faster.
  • Evidence base: one installation, one project, several months. The failure modes in section 6 are documented; their completeness is not.

Quick reference, the twelve rules

  1. Syntax check before every rollout of a control script.
  2. “Operational” is a measurement, not an intention.
  3. Phrase prohibitions as a ban on touching, not on completing.
  4. Demand the sentinel as the only output.
  5. Stall guard from the start; progress equals a new revision.
  6. Check the error flag before the stall counter.
  7. Read the reset time from the actual response, not from assumed fields.
  8. Log every response, appending rather than overwriting.
  9. Phrase acceptance as a test; no measurement without a number.
  10. Gates are necessary, not sufficient. Humans sign off.
  11. Measure the remote state independently, never infer it from local state.
  12. Irreversible things belong in configuration, not in the prompt.

Appendix A: Reference implementation of the watcher

The following script is the version in production use, cleaned of setup specific details. It passes bash -n, but it is a template, not turnkey software: the placeholders from the legend must be replaced and the standing command adapted to your own task format.

12.1 Legend of the placeholders

Placeholder in the scriptMeaningReplace with
AGENTUSERunprivileged account the agent runs underyour own service user
agent-clithe command line of the coding agentthe invocation name of your CLI
publish-toolhardened publication tool (target and branch wired in)your own script, see section 7 item 2
meinprojektdirectory name of the project, drives the case selectionyour own project name
GATE1, GATE2, GATE3the automated checksyour own test, linter, type check and build commands
T-NNNidentifier format of the tasksyour own format
CAP-NNNreference to an internal finding entrydrop it or use your own reference

Also deliberately kept neutral: the dates in the comments, the path for temporary files, and the wording of project specific extra rules.

12.2 The script

#!/bin/bash
# watcher.sh - Never-Sleeping Harness, reference implementation
#   B6  Stall-Guard: STALL_LIMIT aufeinanderfolgende success-Pokes ohne Sentinel UND
#       ohne neuen Commit (HEAD-Vergleich) -> Stopp exit 2. Der Leertest lief 60+
#     so the text is kept free of quotes, backticks and dollar signs.
#       default 0 = aus) -> exit 3.
#   B2b: the scope wording was hardened. "Skip" had been read as "do not build",
#     not as "do not touch", which produced draft plans, a newly assigned task id
#     and one overwritten untracked file. See section 6.2.
#   B2b: the scope wording was hardened. "Skip" had been read as "do not build",
#     not as "do not touch", which produced draft plans, a newly assigned task id
#     and one overwritten untracked file. See section 6.2.
#   B7: quoting fragility removed. The standing command crosses two shell layers,
#   Logging: a no-op publish is now logged as such, not as a successful publish.
#   B8: session limits arrive as subtype=success WITH is_error=true and the reset
#     time only in the plain text of the result. The flag is now read first, the
#     time parsed from the text, then the loop sleeps until the reset.
#   B6: stall guard. N successful pokes without sentinel and without a new commit
#     end the run with a dedicated exit code instead of looping forever.
# Exit codes: 0 = sentinel, 1 = unknown subtype, 2 = stall, 3 = poke limit.
set -euo pipefail

PROJECT="${1:?project path missing}"
MAX_TURNS="${2:-12}"
SENTINEL="ALL-TASKS-DONE"
POKE_TIMEOUT="${POKE_TIMEOUT:-600}"
RESET_MARGIN=120
STALL_LIMIT="${STALL_LIMIT:-3}"
POKE_LIMIT="${POKE_LIMIT:-0}"
SESSION_FILE="/tmp/watcher-session-$$.txt"
LOG="/tmp/watcher-$$.log"
JSON_HIST="/tmp/watcher-history-$$.jsonl"

case "$(basename "$PROJECT")" in
  meinprojekt)
    STANDING_CMD="Continue working on docs/tasks.md (format: ID T-NNN, status vocabulary, field Mode). SCOPE: You may work ONLY on tasks that RIGHT NOW carry status open or in progress AND whose Mode field is Direct, Loop or Goal. EVERYTHING ELSE IS OFF LIMITS. Do not touch it, not even preparatorily: no draft plans, no notes, no new files, NO new task IDs, no schema, architecture or structural changes, and no work on tasks with status planned or a human-gated mode. Those need a human, and preparing work for a human is off limits too. If you find no eligible task, change NOTHING and output IMMEDIATELY, as your only output, exactly: ALL-TASKS-DONE. Otherwise: build against the task spec. Make ALL gates pass: GATE1, GATE2, GATE3. Commit atomically (never git add -A). When flipping a task to done, keep both status locations and the count line consistent. Do NOT publish (the watcher publishes). Set the status, then stop."
    PUSH_CMD="publish-tool "$PROJECT""
    ;;
  *)
    STANDING_CMD="Continue working on docs/TASKS.md: if a task is IN-PROGRESS, finish it (run the gates, set the status). Otherwise take the next OPEN task. Write the status (DONE/BLOCKED) into TASKS.md, then stop. If no OPEN tasks remain, output exactly: ALL-TASKS-DONE"
    PUSH_CMD="git push origin main 2>/dev/null"
    ;;
esac

log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG" >&2; }

repo_head() {
    su - AGENTUSER -c "cd '$PROJECT' && git rev-parse HEAD" 2>/dev/null || echo ""
}

poke() {
    local sid_flag=""
    if [[ -f "$SESSION_FILE" ]]; then
        local sid
        sid=$(cat "$SESSION_FILE")
        [[ -n "$sid" ]] && sid_flag="--resume $sid"
    fi
    local out
    local exit_code=0
    out=$(su - AGENTUSER -c "
        cd '$PROJECT' &&
        timeout $POKE_TIMEOUT agent-cli -p '$STANDING_CMD' \
            $sid_flag \
            --max-turns $MAX_TURNS \
            --permission-mode acceptEdits \
            --output-format json \
            2>/tmp/watcher-poke-err.txt
    ") || exit_code=$?
    printf '%s\n' "$out" >> "$JSON_HIST" 2>/dev/null || true
    local subtype result new_sid
    subtype=$(echo "$out" | jq -r '.subtype // "unknown"' 2>/dev/null || echo "unknown")
    is_error=$(echo "$out" | jq -r '.is_error // false' 2>/dev/null || echo "false")
    result=$(echo "$out" | jq -r '.result // ""' 2>/dev/null || echo "")
    new_sid=$(echo "$out" | jq -r '.session_id // ""' 2>/dev/null || echo "")
    [[ -n "$new_sid" ]] && echo "$new_sid" > "$SESSION_FILE"
    log "exit=$exit_code subtype=$subtype is_error=$is_error sid=${new_sid:0:8}..."
    [[ -n "$result" ]] && log "result: ${result:0:120}"
    # field separator \x1f so free text in the result cannot break parsing
    printf '%s\x1f%s\x1f%s' "$subtype" "$is_error" "$result"
}

parse_reset_epoch() {
    # Uses the local system time zone. On failure return empty, caller falls back to a fixed wait.
    local text="$1" clock ampm hm target_h target_m today epoch now_epoch
    clock=$(printf '%s' "$text" | grep -oiE 'resets ([0-9]{1,2}(:[0-9]{2})?)(am|pm)' | head -1 | sed 's/[Rr]esets //')
    [[ -z "$clock" ]] && return 0
    ampm=$(printf '%s' "$clock" | grep -oiE '(am|pm)$' | tr 'A-Z' 'a-z')
    hm=${clock%$ampm}
    target_h=${hm%%:*}
    if [[ "$hm" == *:* ]]; then target_m=${hm##*:}; else target_m=0; fi
    target_h=$((10#$target_h)); target_m=$((10#$target_m))
    [[ "$ampm" == "pm" && "$target_h" -ne 12 ]] && target_h=$((target_h+12))
    [[ "$ampm" == "am" && "$target_h" -eq 12 ]] && target_h=0
    today=$(date +%Y-%m-%d)
    epoch=$(date -d "$today $target_h:$target_m" +%s 2>/dev/null) || return 0
    now_epoch=$(date +%s)
    (( epoch <= now_epoch )) && epoch=$((epoch + 86400))
    echo "$epoch"
}

get_reset_epoch() {
    # documented candidate but was EMPTY in every measurement. ASSUMPTION until
    # verified against a real limit event in $JSON_HIST.
    [[ -f "$JSON_HIST" ]] || return 0
    local raw
    raw=$(tail -1 "$JSON_HIST" | jq -r '.rate_limits.five_hour.resets_at // empty' 2>/dev/null || true)
    [[ -z "$raw" ]] && return 0
    if [[ "$raw" =~ ^[0-9]+$ ]]; then
        echo "$raw"
    else
        date -d "$raw" +%s 2>/dev/null || true
    fi
}

log "=== Watcher start: $PROJECT (V1.5) ==="
log "max_turns=$MAX_TURNS timeout=${POKE_TIMEOUT}s stall_limit=$STALL_LIMIT poke_limit=$POKE_LIMIT json_hist=$JSON_HIST"

stall_count=0
poke_count=0
last_head=$(repo_head)
log "Baseline HEAD: ${last_head:0:7}"

while true; do
    poke_count=$((poke_count + 1))
    if (( POKE_LIMIT > 0 && poke_count > POKE_LIMIT )); then
        log "POKE_LIMIT ($POKE_LIMIT) reached, stopping (exit 3)."
        exit 3
    fi

    result_line=$(poke)
    IFS=$'\x1f' read -r subtype is_error result_text <<< "$result_line"

    case "$subtype" in
        success)
            if [[ "$is_error" == "true" ]]; then
                # Disguised limit or error (subtype=success WITH is_error=true). Most common
                # case: a session limit. This is NEITHER a stall NOR progress.
                if echo "$result_text" | grep -qiE "session limit|hit your.*limit|resets [0-9]"; then
                    reset_epoch=$(parse_reset_epoch "$result_text")
                    if [[ -n "$reset_epoch" ]]; then
                        now=$(date +%s); sleep_secs=$(( reset_epoch - now + RESET_MARGIN ))
                        (( sleep_secs < 0 )) && sleep_secs=0
                        log "Session limit ($result_text), sleeping ${sleep_secs}s until reset."
                        sleep "$sleep_secs"
                    else
                        log "Session limit detected but reset time not parseable ($result_text), sleeping 3600s."
                        sleep 3600
                    fi
                    rm -f "$SESSION_FILE"   # start a fresh session after the reset
                    log "Reset elapsed, next poke."
                else
                    log "is_error=true but no limit pattern ($result_text), next poke."
                fi
            elif echo "$result_text" | grep -q "$SENTINEL"; then
                log "ALL-TASKS-DONE -- finished."
                rm -f "$SESSION_FILE"
                exit 0
            elif echo "$result_text" | grep -qi "prompt is too long\|context.*too long\|context window"; then
                log "Context too long, dropping session id and poking fresh."
                rm -f "$SESSION_FILE"
            else
                new_head=$(repo_head)
                if [[ -n "$new_head" && "$new_head" == "$last_head" ]]; then
                    stall_count=$((stall_count + 1))
                    log "Turn finished, no sentinel, no new commit (stall $stall_count/$STALL_LIMIT)."
                    if (( stall_count >= STALL_LIMIT )); then
                        log "STALL: $STALL_LIMIT successful pokes without sentinel and without a commit, stopping (exit 2). Last responses: $JSON_HIST"
                        exit 2
                    fi
                else
                    stall_count=0
                    last_head="$new_head"
                    log "Turn finished, new commit ${new_head:0:7}, publishing."
                fi
                push_rc=0
                push_out=$(su - AGENTUSER -c "cd '$PROJECT' && $PUSH_CMD" 2>&1) || push_rc=$?
                push_tail=$(printf '%s\n' "$push_out" | tail -1)
                if (( push_rc == 0 )); then
                    if printf '%s' "$push_out" | grep -qi "Nichts zu pushen"; then
                        log "Publish: no-op (nothing new to publish)."
                    else
                        log "Publish OK: $push_tail"
                    fi
                else
                    log "Publish failed (not a blocker): $push_tail"
                fi
            fi
            ;;
        error_max_turns)
            log "Turn cap reached, next poke."
            ;;
        error_rate_limited|rate_limited)
            log "Rate limit reached."
            reset_epoch=$(get_reset_epoch)
            if [[ -n "$reset_epoch" ]]; then
                now=$(date +%s)
                sleep_secs=$(( reset_epoch - now + RESET_MARGIN ))
                if (( sleep_secs > 0 )); then
                    log "Sleeping ${sleep_secs}s until reset."
                    sleep "$sleep_secs"
                fi
            else
                log "ASSUMPTION: no reset field found in the response. Inspect 'tail -1 $JSON_HIST' and adjust get_reset_epoch. Falling back to sleep 3600s."
                sleep 3600
            fi
            log "Reset elapsed, next poke."
            ;;
        *)
            log "Unknown subtype '$subtype' -- stopping."
            cat /tmp/watcher-poke-err.txt >> "$LOG" 2>/dev/null || true
            exit 1
            ;;
    esac

    sleep 2
done

12.3 The three places where most of the work sits

  1. The standing command (the line with STANDING_CMD). It carries the selection predicate, the prohibition on touching and the sentinel instruction. Roughly half of all corrections concerned this one string.
  2. The success branch with its ordering: first check the error flag, then the sentinel, then progress against the revision identifier. That order is not arbitrary, it is the result of section 6.4.
  3. parse_reset_epoch. The time arrives as plain text and the time zone is that of the system. The fallback path (a fixed wait) is mandatory, otherwise the loop hangs on an unexpected format.

Appendix B: Example task list

The file lives in the project and is the only source of work for the loop. It shows three tasks in three different states so that the predicate from section 4 becomes visible.

# tasks.md

Table row format: `| STATUS | ID | Title |`
Status vocabulary: `open` `in progress` `done` `planned` `dropped`
Mode vocabulary: `Plan-Mode` `Auto + human stop` `Direct` `Loop` `Goal`
Eligible for autonomous work is exclusively: status `open` or `in progress`
combined with mode `Direct`, `Loop` or `Goal`.

## Overview

| Status | ID    | Title                                          |
|--------|-------|------------------------------------------------|
| done   | T-101 | Data access layer for the customer list        |
| open   | T-102 | Customer list as an overview page              |
| open   | T-103 | Tenant separation in the data model            |

*3 tasks, 1 `done`, 2 `open`.*

---

### T-101: Data access layer for the customer list

- **Status:** done (date)
- **Mode:** Loop
- **Intent:** Bundle all read and write access to the customer table.
- **Deps:** none
- **Acceptance:** every query filters by tenant, proven by a test that runs red
  against the unfiltered version; all gates green.
- **Outcome (date):** access layer with six test cases, each checked against a
  second tenant. Not included: the user interface, that is T-102.

### T-102: Customer list as an overview page

- **Status:** open
- **Mode:** Loop
- **Intent:** Display, create, edit and delete the customers of the active tenant.
- **Deps:** T-101 (done)
- **Write path:** through the access layer from T-101, no direct access.
- **Acceptance:** the list shows only customers of the active tenant (test red
  against unfiltered); creating and editing survive a reload; deleting a customer
  with open documents is rejected and the rejection is visible, no silent failure;
  all gates green.

### T-103: Tenant separation in the data model

- **Status:** open
- **Mode:** Plan-Mode
- **Intent:** Tenant column in all tables, migration, foreign keys.
- **Deps:** none
- **Why not autonomous:** schema change with a migration. A mistake here is not
  caught by a test but shows up later in the data. Design and approval by a human,
  implementation afterwards.
- **Acceptance:** the migration is forward only and loses no data; every table
  carries the tenant column; access across tenant boundaries fails, proven by one
  test per table.

How the loop reads this list: exactly T-102 is eligible. T-101 is done, so it is finished. T-103 carries status open but mode Plan-Mode, so it is locked for autonomous operation and stays untouched. Once T-102 is complete the agent finds no eligible task, emits the sentinel, and the loop ends.

The three counting levels (status in the table, detail block, count line) must agree. An automated consistency check that compares exactly these and turns red on divergence is strongly recommended: numbers in a count line drift silently, and a status word the parser does not recognize makes a task disappear unnoticed from the checking view.


Appendix C: Starting the loop

14.1 Supervised short run for testing

# Step 1: syntax check, always first
bash -n ./watcher.sh && echo PARSE-OK

# Step 2: idle test. Expectation: sentinel and exit without any change.
# A small turn budget is not always enough here, see section 6.5:
# the budget must let the agent read the list AND answer.
./watcher.sh /path/to/project 12

# Step 3: first real run with one task, supervised.
# A small budget deliberately forces several resumes here,
# so that the resume path gets observed once:
POKE_TIMEOUT=3600 ./watcher.sh /path/to/project 2

14.2 Unsupervised run

# Start inside a detachable terminal session so that a dropped
# connection does not end the run:
tmux new -s harness

POKE_TIMEOUT=3600 STALL_LIMIT=5 POKE_LIMIT=0 \
  ./watcher.sh /path/to/project 12 2>&1 | tee /tmp/harness-run.log

# Ctrl-b d detaches the session, the loop keeps running.
# To look at it later:  tmux attach -t harness

14.3 The tuning knobs

QuantityEffectRecommendation
second argument (MAX_TURNS)tool rounds per pokegenerous in production, small only to test the resume path
POKE_TIMEOUTabort of a single pokegenerous, otherwise a running build step gets killed
STALL_LIMITsuccessful pokes without progress before abortingthree to five
POKE_LIMIThard ceiling on all pokes0 (off) in normal operation, set during experiments

14.4 During and after the run

While the run is going, the watcher log and the response history tell you what is happening. Afterwards the session can be reopened by its identifier, which appears in every log line, in the interactive mode of the command line and reviewed in full. While the loop is running you should not do this.


Appendix D: Glossary

The terms are grouped by theme rather than alphabetically: if you want to understand the loop, read from top to bottom. Each card gives the meaning, the reason the term matters in this document, and where relevant the most common confusion.

15.1 Operating modes and basic mechanics

Headless

Short: running a program without a screen interface, controlled purely through invocation parameters and return values. Why it matters: only this lets a script start an agent and evaluate its result. An interactive interface would have to be read off the screen, which is not auditable. Common confusion: headless does not mean unobservable. The loop is fully inspectable with a delay, see section 8.

Short: the operating mode of an agent command line in which an instruction is passed in, worked through, and one structured response is returned, after which the process ends. Why it matters: it provides the three things a loop needs: programmatic startup, a machine readable ending, and a turn budget.

Poke

Short: a single nudge of the agent by the watcher, meaning one complete start, work, end cycle. Why it matters: the poke is the unit of account of the loop. Every metric (consumption, progress, stalling) is measured per poke. Mnemonic: a poke is the automated equivalent of a human typing “keep going”.

Standing command

Short: the always identical instruction text the watcher passes on every poke. Why it matters: it is the only place where what the agent may do is decided. Roughly half of all corrections concerned this text. See: section 3.

Sentinel

Short: an agreed string the agent emits when there is nothing left to do, and which the watcher checks for. Why it matters: it is the only completion signal the loop understands. That is why it must be demanded as the only output, otherwise it drowns in an explanation.

15.2 Time, quota and limits

Turn

Short: one tool round of the agent, such as reading a file, running a test, writing a file. Common confusion: a turn is not a conversational step with a human. A single task often consumes dozens of turns.

Turn budget

Short: the ceiling on tool rounds per poke. Why it matters: set too small, the agent gets cut off before it can answer, and a test then measures something other than what was intended. Too small is also expensive, because every poke has startup cost. See: sections 6.5 and 6.6.

Rate limit

Short: a provider side cap on consumption within a time window. Why it matters: it is the reason this loop exists.

Session limit

Short: a particular form of usage cap that stops the running session until a reset time is reached. Why it matters: in measurement it arrived in disguise, namely as a successful run with the error flag set, and the time appeared only in plain text. Anyone checking only for an error type will miss it. See: section 6.4.

Reset

Short: the point in time at which a consumed quota becomes available again. Why it matters: the loop sleeps until then instead of aborting. Exactly this bridge distinguishes it from a simple retry loop.

Context compaction

Short: the summarizing of older parts of a conversation when a model’s context fills up. Why it matters: it is the reason a boundary set in conversation is not a basis for authorization. What has been summarized no longer reliably applies. See: section 7 item 3.

15.3 Control and safeguards

Predicate (selection predicate)

Short: the logical condition that decides which tasks the agent may touch. Why it matters: phrased in two dimensions (status and mode) it turns a judgment call into a field value. See: section 4.

Stall guard

Short: an abort condition that triggers when several successful pokes in a row deliver neither the sentinel nor a new revision. Why it matters: without it an idle loop runs on indefinitely and burns quota for no result. Common confusion: it must not fire while the agent is merely waiting for a reset. That is why the error flag is checked first.

Fail safe

Short: behavior that falls into the harmless state on failure instead of blocking or aborting. Example here: if the reset time cannot be read from the text, the loop waits a fixed period rather than ending.

Exit code

Short: the return value of a program, where zero means success. Why it matters: different abort reasons get different values so that monitoring can tell whether the loop finished or gave up.

Sandbox

Short: a restricted execution environment in which a process can only reach what it needs. Why it matters: the agent runs under its own unprivileged account. When a scope rule was violated, this kept the damage confined to the working tree.

15.4 Version control and publication

Working tree

Short: the state of the files in the project directory, meaning what has not yet been recorded in version history. Why it matters: changes in the working tree can be undone with one command. That is precisely what makes a commit gate an effective safeguard.

Commit

Short: a recorded, named state in local version history. Why it matters: a new commit is the only progress signal the watcher checks mechanically.

Publishing

Short: transferring local commits to the shared remote. Common confusion: “committed” is not “published”. Both directions of this error have occurred, see section 6.7.

Remote

Short: the shared store of version history outside the workstation. Why it matters: its state is queried independently and never inferred from local state.

Append only

Short: a store that permits only additions, no overwriting and no deletion of history. Why it matters: the agent cannot rewrite the past, not even accidentally. Corrections are additional entries.

Force push

Short: a publication operation that overwrites history on the remote. Why it matters: it is the most dangerous single command in the vicinity of an autonomous agent and is blocked server side, not by agreement.

15.5 Quality and evidence

Gate

Short: an automated check that must pass before a state may count as finished, such as tests, type checking, linting, build. Why it matters: gates are the deterministic part of the done decision. Programs judge, not phrasing. Common confusion: necessary but not sufficient. See 5.3.

Acceptance criterion

Short: the condition under which a task counts as fulfilled. Why it matters: only when phrased as a test does an intention become a checkable fact. See: section 5.1.

Regression test

Short: a test that makes sure a fixed defect does not come back. Why it matters: it is only worth something if it ran red against the old version and that was reported. A test that was never red proves nothing.

Determinism

Short: same input, same result. Why it matters: a language model is not deterministic. The solution is not to make it so, but to attach the decision “done” to deterministic authorities.

Drift

Short: the slow divergence of two statements that ought to agree, for instance a count line and the actual distribution. Why it matters: drift is silent. Only an automated consistency check makes it visible.

Audit trail

Short: the complete, retrospectively checkable record of what happened. Why it matters: in autonomous work it replaces real time observation. Four layers, see section 8.

15.6 Formats and tools

JSON

Short: a text based data format made of named fields. Why it matters: every poke response arrives this way, which is what allows machine evaluation instead of text search across screen output.

JSONL

Short: a file that holds one complete JSON object per line. Why it matters: ideal for a running response history, because appending is enough. Overwriting instead of appending cost us exactly the response in which a mistake had been recorded.

Epoch time

Short: a point in time expressed as seconds since a fixed zero point. Why it matters: arithmetic on time becomes trivial. The loop first converts the plain text time it read into this format.

Detachable terminal session

Short: a session that keeps running when the connection drops and can be reentered later. Why it matters: without it a dropped connection ends the loop. Limitation: it does not survive a reboot of the machine. Permanent operation needs a service unit with a restart policy.

Idempotence

Short: an operation that, executed several times, yields the same result as executing it once. Why it matters: the publication step runs after every poke. Without idempotence that would be dangerous; as it is, it has no consequence when there is nothing to publish.


This note describes an operating pattern, not a piece of software. It deliberately contains no details about hosts, accounts, addresses or directory structures of any concrete installation; such details belong in internal operations documentation.

Don't miss a thing

New research articles by email, as soon as they appear.