Harness — Long-Running Agent Framework
Executable protocol enabling any agent task to run continuously across multiple sessions with automatic progress recovery, task dependency resolution, failure rollback, and standardized error handling.Design Principles
- Design for the agent, not the human — Test output, docs, and task structure are the agent’s primary interface
- Progress files ARE the context — When context window resets, progress files + git history = full recovery
- Premature completion is the #1 failure mode — Structured task lists with explicit completion criteria prevent declaring victory early
- Standardize everything grep-able — ERROR on same line, structured timestamps, consistent prefixes
- Fast feedback loops — Pre-compute stats, run smoke tests before full validation
- Idempotent everything — Init scripts, task execution, environment setup must all be safe to re-run
- Fail safe, not fail silent — Every failure must have an explicit recovery strategy
Commands
Activation Marker
Hooks only take effect when.harness-active marker file exists in the harness root (same directory as harness-tasks.json).
/harness initand/harness runMUST create this marker:touch <project-path>/.harness-active- When all tasks complete (no pending/in_progress/retryable left), remove it:
rm <project-path>/.harness-active - Without this marker, all hooks are no-ops — they exit 0 immediately
Progress Persistence (Dual-File System)
Maintain two files in the project working directory:harness-progress.txt (Append-Only Log)
Free-text log of all agent actions across sessions. Never truncate.harness-tasks.json (Structured State)
pending → in_progress (transient, set only during active execution) → completed or failed. A task found as in_progress at session start means the previous session was interrupted — handle via Context Window Recovery Protocol.
In concurrent mode (see Concurrency Control), tasks may also carry claim metadata: claimed_by and lease_expires_at (ISO timestamp).
Session boundary: A session starts when the agent begins executing the Session Start protocol and ends when a Stopping Condition is met or the context window resets. Each session gets a unique SESSION-N identifier (N = session_count after increment).
Concurrency Control
Before modifyingharness-tasks.json, acquire an exclusive lock using portable mkdir (atomic on all POSIX systems, works on both macOS and Linux):
[timestamp] [SESSION-N] LOCK acquired (pid=<PID>)
Log lock release: [timestamp] [SESSION-N] LOCK released
Modes:
- Exclusive (default): hold the lock for the entire session (the
trap EXIThandler releases it automatically). Any second session in the same state root fails fast. - Concurrent (opt-in via
session_config.concurrency_mode: "concurrent"): treat this as a state transaction lock. Hold it only while reading/modifying/writingharness-tasks.json(including.bak/.tmp) and appending toharness-progress.txt. Release it immediately before doing real work.
- All workers MUST point at the same state root (the directory that contains
harness-tasks.json). If you are using separate worktrees/clones, pin it explicitly (e.g.,HARNESS_STATE_ROOT=/abs/path/to/state-root). - Task selection is advisory; the real gate is atomic claim under the lock: set
status="in_progress", setclaimed_by(stable worker id, e.g.,HARNESS_WORKER_ID), setlease_expires_at. If claim fails (alreadyin_progresswith a valid lease), pick another eligible task and retry. - Never run two workers in the same git working directory. Use separate worktrees/clones. Otherwise rollback (
git reset --hard/git clean -fd) will destroy other workers.
Infinite Loop Protocol
Session Start (Execute Every Time)
- Read state: Read last 200 lines of
harness-progress.txt+ fullharness-tasks.json. If JSON is unparseable, see JSON corruption recovery in Error Handling. - Read git: Run
git log --oneline -20andgit diff --statto detect uncommitted work - Acquire lock (mode-dependent): Exclusive mode fails if another session is active. Concurrent mode uses the lock only for state transactions.
- Recover interrupted tasks (see Context Window Recovery below)
- Health check: Run
harness-init.shif it exists - Track session: Increment
session_countin JSON. Checksession_countagainstmax_sessions— if reached, log STATS and STOP. Initialize per-session task counter to 0. - Pick next task using Task Selection Algorithm below
Task Selection Algorithm
Before selecting, run dependency validation:- Cycle detection: For each non-completed task, walk
depends_ontransitively. If any task appears in its own chain, mark itfailedwith[DEPENDENCY] Circular dependency detected: task-A -> task-B -> task-A. Self-references (depends_onincludes own id) are also cycles. - Blocked propagation: If a task’s
depends_onincludes a task that isfailedand will never be retried (eitherattempts >= max_attemptsOR itserror_logcontains a[DEPENDENCY]entry), mark the blocked task asfailedwith[DEPENDENCY] Blocked by failed task-XXX. Repeat until no more tasks can be propagated.
- Tasks with
status: "pending"where ALLdepends_ontasks arecompleted— sorted bypriority(P0 > P1 > P2), then byid(lowest first) - Tasks with
status: "failed"whereattempts < max_attemptsand ALLdepends_onarecompleted— sorted by priority, then oldest failure first - If no eligible tasks remain → log final STATS → STOP
Task Execution Cycle
For each task, execute this exact sequence:- Claim (atomic, under lock): Record
started_at_commit= current HEAD hash. Set status toin_progress, setclaimed_by, setlease_expires_at, logStarting [<task-id>] <title> (base=<hash>). If the task is already claimed (in_progresswith a valid lease), pick another eligible task and retry. - Execute with checkpoints: Perform the work. After each significant step, log:
Also append to the task’s
checkpointsarray:{ "step": M, "total": N, "description": "...", "timestamp": "ISO" }. In concurrent mode, renew the lease at each checkpoint (pushlease_expires_atforward). - Validate: Run the task’s
validation.commandwith a timeout wrapper (prefertimeout; on macOS usegtimeoutfrom coreutils). Ifvalidation.commandis empty/null, logERROR [<task-id>] [CONFIG] Missing validation.commandand STOP — do not declare completion without an objective check. Before running, verify the command exists (e.g.,command -v <binary>) — if missing, treat asENV_SETUPerror.- Command exits 0 → PASS
- Command exits non-zero → FAIL
- Command exceeds timeout → TIMEOUT
- Record outcome:
- Success: status=
completed, setcompleted_at, logCompleted [<task-id>] (commit <hash>), git commit - Failure: increment
attempts, append error toerror_log. Verifystarted_at_commitexists viagit cat-file -t <hash>— if missing, mark failed at max_attempts. Otherwise executegit reset --hard <started_at_commit>andgit clean -fdto rollback ALL commits and remove untracked files. Executeon_failure.cleanupif defined. LogERROR [<task-id>] [<category>] <message>. Set status=failed(Task Selection Algorithm pass 2 handles retries when attempts < max_attempts)
- Success: status=
- Track: Increment per-session task counter. If
max_tasks_per_sessionreached, log STATS and STOP. - Continue: Immediately pick next task (zero idle time)
Stopping Conditions
- All tasks
completed - All remaining tasks
failedat max_attempts or blocked by failed dependencies session_config.max_tasks_per_sessionreached for this sessionsession_config.max_sessionsreached across all sessions- User interrupts
Context Window Recovery Protocol
When a new session starts and finds a task withstatus: "in_progress":
- Exclusive mode: treat this as an interrupted previous session and run the Recovery Protocol below.
- Concurrent mode: only recover a task if either (a)
claimed_bymatches this worker, or (b)lease_expires_atis in the past (stale lease). Otherwise, treat it as owned by another worker and do not modify it.
- Check git state:
- Check checkpoints: Read the task’s
checkpointsarray to determine last completed step - Decision matrix (verify recent commits belong to this task by checking commit messages for the task-id):
- Log recovery:
[timestamp] [SESSION-N] RECOVERY [task-id] action="<action taken>" reason="<reason>"
Error Handling & Recovery Strategies
Each error category has a default recovery strategy:
JSON corruption: If
harness-tasks.json cannot be parsed, check for harness-tasks.json.bak (written before each modification). If backup exists and is valid, restore from it. If no valid backup, log ERROR [ENV_SETUP] harness-tasks.json corrupted and unrecoverable and STOP — task metadata (validation commands, dependencies, cleanup) cannot be reconstructed from logs alone.
Backup protocol: Before every write to harness-tasks.json, copy the current file to harness-tasks.json.bak. Write updates atomically: write JSON to harness-tasks.json.tmp then mv it into place (readers should never see a partial file).
Environment Initialization
Ifharness-init.sh exists in the project root, run it at every session start. The script must be idempotent.
Example harness-init.sh:
Standardized Log Format
All log entries use grep-friendly format on a single line:[task-id] and [category] are included when applicable (task-scoped entries). Session-level entries (INIT, LOCK, STATS) omit them.
Types: INIT, Starting, Completed, ERROR, CHECKPOINT, ROLLBACK, RECOVERY, STATS, LOCK, WARN
Error categories: ENV_SETUP, CONFIG, TASK_EXEC, TEST_FAIL, TIMEOUT, DEPENDENCY, SESSION_TIMEOUT
Filtering:
Session Statistics
At session end, updateharness-tasks.json: set last_session to current timestamp. (Do NOT increment session_count here — it is incremented at Session Start.) Then append:
blocked is computed at stats time: count of pending tasks whose depends_on includes a permanently failed task. It is not a stored status value.
Init Command (/harness init)
- Create
harness-progress.txtwith initialization entry - Create
harness-tasks.jsonwith empty task list and defaultsession_config - Optionally create
harness-init.shtemplate (chmod +x) - Ask user: add harness files to
.gitignore?
Status Command (/harness status)
Read harness-tasks.json and harness-progress.txt, then display:
- Task summary: count by status (completed, failed, pending, blocked).
blocked= pending tasks whosedepends_onincludes a permanently failed task (computed, not a stored status). - Per-task one-liner:
[status] task-id: title (attempts/max_attempts) - Last 5 lines from
harness-progress.txt - Session count and last session timestamp
Add Command (/harness add)
Append a new task to harness-tasks.json with auto-incremented id (task-NNN), status pending, default max_attempts: 3, empty depends_on, and no validation command (required before the task can be completed). Prompt user for optional fields: priority, depends_on, validation.command, timeout_seconds. Requires lock acquisition (modifies JSON).
Tool Dependencies
Requires: Bash, file read/write, git. All harness operations must be executed from the project root directory. Does NOT require: specific MCP servers, programming languages, or test frameworks. Concurrent mode requires isolated working directories (git worktree or separate clones). Do not run concurrent workers in the same working tree.