关于 harness
harness 是面向长时间项目工作的技能,可让 Agent 跨会话持续执行任务。它通过持久化进度和任务状态,支持恢复、依赖处理、重试与回滚。
主要功能
- 01跨会话执行任务
- 02进度日志与结构化状态
- 03依赖校验与排序
- 04恢复中断任务
- 05失败回滚与重试
适用场景
- 01上下文重置后继续项目工作
- 02按依赖关系执行并检查任务
- 03恢复并重试失败的任务
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ stellarlinkco/ myclaude --skill harness安装后在 Claude Code 中输入/harness确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
hooks/_harness_common.pyharness-claim.pyharness-renew.pyharness-sessionstart.pyharness-stop.pyharness-subagentstop.pyharness-teammateidle.pyhooks.jsonself-reflect-stop.pytests/e2e-100tasks.shtest_hooks.py详细介绍
Harness — 长时间运行的 Agent 框架
可执行协议,使任何 Agent 任务都能跨多个会话持续运行,并支持自动恢复进度、解决任务依赖关系、失败回滚和标准化错误处理。
设计原则
- 为 Agent 而非人设计 — 测试输出、文档和任务结构是 Agent 的主要接口
- 进度文件就是上下文 — 上下文窗口重置时,进度文件 + git 历史记录 = 完整恢复
- 过早完成是头号失败模式 — 结构化任务列表和明确的完成标准可防止过早宣告胜利
- 将所有内容标准化,便于 grep 搜索 — ERROR 位于同一行,时间戳采用结构化格式,前缀保持一致
- 快速反馈循环 — 预先计算统计数据,在完整验证前运行冒烟测试
- 所有操作都应具备幂等性 — 初始化脚本、任务执行和环境设置都必须能够安全地重复运行
- 安全失败,而非静默失败 — 每种失败都必须有明确的恢复策略
命令
/harness init <project-path> # Initialize harness files in project
/harness run # Start/resume the infinite loop
/harness status # Show current progress and stats
/harness add "task description" # Add a task to the list
激活标记
只有当 Harness 根目录(与 harness-tasks.json 位于同一目录)中存在 .harness-active 标记文件时,钩子才会生效。
/harness init和/harness run必须创建此标记:touch <project-path>/.harness-active- 所有任务完成后(不再有
pending、in_progress或可重试任务),移除此标记:rm <project-path>/.harness-active - 没有此标记时,所有钩子都不执行任何操作,会立即以状态码 0 退出
进度持久化(双文件系统)
在项目工作目录中维护两个文件:
harness-progress.txt(仅追加日志)
记录所有会话中 Agent 执行操作的自由文本日志。绝不截断。
[2025-07-01T10:00:00Z] [SESSION-1] INIT Harness initialized for project /path/to/project
[2025-07-01T10:00:05Z] [SESSION-1] INIT Environment health check: PASS
[2025-07-01T10:00:10Z] [SESSION-1] LOCK acquired (pid=12345)
[2025-07-01T10:00:11Z] [SESSION-1] Starting [task-001] Implement user authentication (base=def5678)
[2025-07-01T10:05:00Z] [SESSION-1] CHECKPOINT [task-001] step=2/4 "auth routes created, tests pending"
[2025-07-01T10:15:30Z] [SESSION-1] Completed [task-001] (commit abc1234)
[2025-07-01T10:15:31Z] [SESSION-1] Starting [task-002] Add rate limiting (base=abc1234)
[2025-07-01T10:20:00Z] [SESSION-1] ERROR [task-002] [TASK_EXEC] Redis connection refused
[2025-07-01T10:20:01Z] [SESSION-1] ROLLBACK [task-002] git reset --hard abc1234
[2025-07-01T10:20:02Z] [SESSION-1] STATS tasks_total=5 completed=1 failed=1 pending=3 blocked=0 attempts_total=2 checkpoints=1
harness-tasks.json(结构化状态)
{
"version": 2,
"created": "2025-07-01T10:00:00Z",
"session_config": {
"concurrency_mode": "exclusive",
"max_tasks_per_session": 20,
"max_sessions": 50
},
"tasks": [
{
"id": "task-001",
"title": "Implement user authentication",
"status": "completed",
"priority": "P0",
"depends_on": [],
"attempts": 1,
"max_attempts": 3,
"started_at_commit": "def5678",
"validation": {
"command": "npm test -- --testPathPattern=auth",
"timeout_seconds": 300
},
"on_failure": {
"cleanup": null
},
"error_log": [],
"checkpoints": [],
"completed_at": "2025-07-01T10:15:30Z"
},
{
"id": "task-002",
"title": "Add rate limiting",
"status": "failed",
"priority": "P1",
"depends_on": [],
"attempts": 1,
"max_attempts": 3,
"started_at_commit": "abc1234",
"validation": {
"command": "npm test -- --testPathPattern=rate-limit",
"timeout_seconds": 120
},
"on_failure": {
"cleanup": "docker compose down redis"
},
"error_log": ["[TASK_EXEC] Redis connection refused"],
"checkpoints": [],
"completed_at": null
},
{
"id": "task-003",
"title": "Add OAuth providers",
"status": "pending",
"priority": "P1",
"depends_on": ["task-001"],
"attempts": 0,
"max_attempts": 3,
"started_at_commit": null,
"validation": {
"command": "npm test -- --testPathPattern=oauth",
"timeout_seconds": 180
},
"on_failure": {
"cleanup": null
},
"error_log": [],
"checkpoints": [],
"completed_at": null
}
],
"session_count": 1,
"last_session": "2025-07-01T10:20:02Z"
}
任务状态:pending → in_progress(临时状态,仅在执行期间设置)→ completed 或 failed。如果会话开始时发现任务处于 in_progress 状态,说明上一个会话被中断 — 请按照上下文窗口恢复协议处理。
并发模式下(参见并发控制),任务还可以包含认领元数据:claimed_by 和 lease_expires_at(ISO 时间戳)。
会话边界:当 Agent 开始执行会话启动协议时,会话开始;满足停止条件或上下文窗口重置时,会话结束。每个会话都会获得一个唯一的 SESSION-N 标识符(N = 递增后的 session_count)。
并发控制
修改 harness-tasks.json 前,使用可移植的 mkdir 获取独占锁(在所有 POSIX 系统上都是原子操作,且在 macOS 和 Linux 上都可用):
# Acquire lock (fail fast if another agent is running)
# Lock key must be stable even if invoked from a subdirectory.
ROOT="$PWD"
SEARCH="$PWD"
while [ "$SEARCH" != "/" ] && [ ! -f "$SEARCH/harness-tasks.json" ]; do
SEARCH="$(dirname "$SEARCH")"
done
if [ -f "$SEARCH/harness-tasks.json" ]; then
ROOT="$SEARCH"
fi
PWD_HASH="$(
printf '%s' "$ROOT" |
(shasum -a 256 2>/dev/null || sha256sum 2>/dev/null) |
awk '{print $1}' |
cut -c1-16
)"
LOCKDIR="/tmp/harness-${PWD_HASH:-unknown}.lock"
if ! mkdir "$LOCKDIR" 2>/dev/null; then
# Check if lock holder is still alive
LOCK_PID=$(cat "$LOCKDIR/pid" 2>/dev/null)
if [ -n "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2>/dev/null; then
echo "ERROR: Another harness session is active (pid=$LOCK_PID)"; exit 1
fi
# Stale lock — atomically reclaim via mv to avoid TOCTOU race
STALE="$LOCKDIR.stale.$$"
if mv "$LOCKDIR" "$STALE" 2>/dev/null; then
rm -rf "$STALE"
mkdir "$LOCKDIR" || { echo "ERROR: Lock contention"; exit 1; }
echo "WARN: Removed stale lock${LOCK_PID:+ from pid=$LOCK_PID}"
else
echo "ERROR: Another agent reclaimed the lock"; exit 1
fi
fi
echo "$$" > "$LOCKDIR/pid"
trap 'rm -rf "$LOCKDIR"' EXIT
记录锁获取日志:[timestamp] [SESSION-N] LOCK acquired (pid=<PID>)
记录锁释放日志:[timestamp] [SESSION-N] LOCK released
模式:
- 独占(默认):整个会话期间都持有锁(
trap EXIT处理程序会自动释放)。同一状态根目录中的任何第二个会话都会快速失败。 - 并发(通过
session_config.concurrency_mode: "concurrent"启用):将其视为状态事务锁。仅在读取、修改或写入harness-tasks.json(包括.bak/.tmp)以及向harness-progress.txt追加内容时持有锁。开始实际工作前立即释放锁。
并发模式不变量:
- 所有工作进程都必须指向同一个状态根目录(即包含
harness-tasks.json的目录)。如果使用不同的工作树或克隆,请明确指定该目录(例如HARNESS_STATE_ROOT=/abs/path/to/state-root)。 - 任务选择仅供参考;真正的关卡是在锁的保护下进行原子认领:设置
status="in_progress",设置claimed_by(稳定的工作进程 ID,例如HARNESS_WORKER_ID),并设置lease_expires_at。如果认领失败(任务已处于in_progress状态且租约有效),请选择其他符合条件的任务并重试。 - 绝不能让两个工作进程在同一个 git 工作目录中运行。请使用不同的工作树或克隆。否则,回滚(
git reset --hard/git clean -fd)会销毁其他工作进程的内容。
无限循环协议
会话开始(每次执行)
- 读取状态:读取
harness-progress.txt的最后 200 行,以及完整的harness-tasks.json。如果 JSON 无法解析,请参阅错误处理中的 JSON 损坏恢复。 - 读取 Git 状态:运行
git log --oneline -20和git diff --stat,检测未提交的工作。 - 获取锁(取决于模式):独占模式下,如果另一会话处于活动状态,则失败。并发模式仅在状态事务期间使用锁。
- 恢复中断的任务(参见下文的上下文窗口恢复)
- 健康检查:如果
harness-init.sh存在,则运行它。 - 跟踪会话:在 JSON 中递增
session_count。检查session_count是否达到max_sessions;如果已达到,则记录 STATS 并停止。将每会话任务计数器初始化为 0。 - 使用下方的任务选择算法选择下一个任务。
任务选择算法
选择任务前,先运行依赖项验证:
- 循环检测:对于每个未完成的任务,沿
depends_on递归遍历。如果某个任务出现在自身的依赖链中,则将其标记为failed,并附加[DEPENDENCY] Circular dependency detected: task-A -> task-B -> task-A。自引用(depends_on包含自身 ID)也属于循环。 - 阻塞传播:如果某个任务的
depends_on包含一个失败且不会重试的任务(即attempts >= max_attempts,或其error_log包含[DEPENDENCY]条目),则将被阻塞的任务标记为failed,并附加[DEPENDENCY] Blocked by failed task-XXX。重复此步骤,直到无法继续传播。
然后按以下优先级顺序选择下一个任务:
status: "pending"且所有depends_on任务均为completed的任务——先按priority排序(P0 > P1 > P2),再按id排序(从低到高)status: "failed"、attempts < max_attempts且所有depends_on均为completed的任务——先按优先级排序,再按失败时间从早到晚排序- 如果没有符合条件的任务,则记录最终 STATS 并停止
任务执行周期
对每个任务,严格按以下顺序执行:
- 认领(在锁内原子执行):记录
started_at_commit= 当前 HEAD 哈希。将状态设为in_progress,设置claimed_by和lease_expires_at,记录Starting [<task-id>] <title> (base=<hash>)。如果任务已被认领(状态为in_progress且租约有效),则选择另一个符合条件的任务并重试。 - 执行并设置检查点:执行工作。每完成一个重要步骤后,记录:
同时将以下内容追加到任务的[timestamp] [SESSION-N] CHECKPOINT [task-id] step=M/N "description of what was done"checkpoints数组:{ "step": M, "total": N, "description": "...", "timestamp": "ISO" }。在并发模式下,每个检查点都要续租(延长lease_expires_at)。 - 验证:使用超时包装器运行任务的
validation.command(优先使用timeout;在 macOS 上使用 coreutils 中的gtimeout)。如果validation.command为空或为 null,则记录ERROR [<task-id>] [CONFIG] Missing validation.command并停止——未经客观检查,不得声明任务完成。运行前,验证命令是否存在(例如command -v <binary>);如果不存在,则视为ENV_SETUP错误。- 命令退出码为 0 → 通过
- 命令退出码非 0 → 失败
- 命令超时 → 超时
- 记录结果:
- 成功:将状态设为
completed,设置completed_at,记录Completed [<task-id>] (commit <hash>),并提交 Git。 - 失败:递增
attempts,将错误追加到error_log。通过git cat-file -t <hash>验证started_at_commit是否存在——如果不存在,则将任务标记为已达到最大尝试次数。否则,执行git reset --hard <started_at_commit>和git clean -fd,回滚所有提交并移除未跟踪文件。如果已定义on_failure.cleanup,则执行它。记录ERROR [<task-id>] [<category>] <message>。将状态设为failed(任务选择算法第 2 步会在attempts < max_attempts时处理重试)。
- 成功:将状态设为
- 跟踪:递增每会话任务计数器。如果达到
max_tasks_per_session,则记录 STATS 并停止。 - 继续:立即选择下一个任务(不留空闲时间)。
停止条件
- 所有任务均已
completed - 所有剩余任务均已达到最大尝试次数或因依赖项失败而受阻,状态为
failed - 本次会话已达到
session_config.max_tasks_per_session - 所有会话已达到
session_config.max_sessions - 用户中断
上下文窗口恢复协议
新会话启动时,如果发现某个任务的状态为 in_progress:
- 独占模式:将其视为上一个会话中断,并执行以下恢复协议。
- 并发模式:仅当 (a)
claimed_by与此工作进程匹配,或 (b)lease_expires_at已过期(租约过期)时,才恢复任务。否则,将其视为由其他工作进程持有,不要对其进行修改。
- 检查 git 状态:
git diff --stat # Uncommitted changes? git log --oneline -5 # Recent commits since task started? git stash list # Any stashed work? - 检查检查点:读取任务的
checkpoints数组,以确定最后完成的步骤 - 决策矩阵(通过检查提交消息中的任务 ID,确认最近的提交属于此任务):
| 有未提交更改? | 有最近的任务提交? | 有检查点? | 操作 |
|---|---|---|---|
| 否 | 否 | 无 | 标记为 failed,并写入 [SESSION_TIMEOUT] No progress detected,增加尝试次数 |
| 否 | 否 | 有 | 验证文件状态是否符合检查点声明。如果文件反映了检查点进度,则从最后一步恢复。如果不是,则标记为 failed——工作已丢失 |
| 否 | 有 | 任意 | 运行 validation.command。如果通过 → 标记为 completed。如果失败 → git reset --hard <started_at_commit>,然后标记为 failed |
| 有 | 否 | 任意 | 在保留未提交更改的情况下运行验证。如果通过 → 提交更改,并标记为 completed。如果失败 → git reset --hard <started_at_commit> + git clean -fd,然后标记为 failed |
| 有 | 有 | 任意 | 提交未提交的更改,运行 validation.command。如果通过 → 标记为 completed。如果失败 → git reset --hard <started_at_commit> + git clean -fd,然后标记为 failed |
- 记录恢复日志:
[timestamp] [SESSION-N] RECOVERY [task-id] action="<action taken>" reason="<reason>"
错误处理与恢复策略
每种错误类别都有默认恢复策略:
| 类别 | 默认恢复策略 | Agent 操作 |
|---|---|---|
ENV_SETUP | 重新运行初始化;如果仍然失败则停止 | 立即再次运行 harness-init.sh。如果连续两次失败,则记录日志并停止——环境已损坏 |
CONFIG | 停止(需要人工修复) | 精确记录配置错误(文件 + 字段),然后停止。不要猜测或自动修改任务元数据 |
TASK_EXEC | 通过 git reset --hard <started_at_commit> 回滚并重试 | 验证 started_at_commit 是否存在(git cat-file -t <hash>)。如果不存在,则在达到 max_attempts 时标记为失败。否则重置,运行 on_failure.cleanup(如果已定义),并在尝试次数 < max_attempts 时重试 |
TEST_FAIL | 通过 git reset --hard <started_at_commit> 回滚并重试 | 重置到 started_at_commit,分析测试输出以找出修复方法,再进行有针对性的修改并重试 |
TIMEOUT | 终止进程、执行清理并重试 | 使用 timeout <seconds> <command> 包装验证命令。超时时,运行 on_failure.cleanup 并重试(如果反复发生,可考虑拆分任务) |
DEPENDENCY | 跳过任务并标记为受阻 | 记录失败的依赖项,将任务标记为 failed,并注明依赖项导致的失败原因 |
SESSION_TIMEOUT | 使用上下文窗口恢复协议 | 新会话通过恢复协议评估部分进度——根据验证结果,任务可能完成,也可能失败 |
JSON 损坏:如果无法解析 harness-tasks.json,请检查 harness-tasks.json.bak(每次修改前写入)。如果备份存在且有效,则从中恢复。如果没有有效备份,则记录 ERROR [ENV_SETUP] harness-tasks.json corrupted and unrecoverable 并停止——仅凭日志无法重建任务元数据(验证命令、依赖项、清理操作)。
备份协议:每次写入 harness-tasks.json 前,将当前文件复制到 harness-tasks.json.bak。以原子方式写入更新:先将 JSON 写入 harness-tasks.json.tmp,然后通过 mv 将其移至目标位置(读取方不应看到不完整的文件)。
环境初始化
如果项目根目录中存在 harness-init.sh,请在每次会话开始时运行。该脚本必须具备幂等性。
harness-init.sh 示例:
#!/bin/bash
set -e
npm install 2>/dev/null || pip install -r requirements.txt 2>/dev/null || true
curl -sf http://localhost:5432 >/dev/null 2>&1 || echo "WARN: DB not reachable"
npm test -- --bail --silent 2>/dev/null || echo "WARN: Smoke test failed"
echo "Environment health check complete"
标准化日志格式
所有日志条目均使用便于 grep 检索的单行格式:
[ISO-timestamp] [SESSION-N] <TYPE> [task-id]? [category]? message
适用时(任务范围的条目)包含 [task-id] 和 [category]。会话级条目(INIT、LOCK、STATS)不包含这两项。
类型:INIT、Starting、Completed、ERROR、CHECKPOINT、ROLLBACK、RECOVERY、STATS、LOCK、WARN
错误类别:ENV_SETUP、CONFIG、TASK_EXEC、TEST_FAIL、TIMEOUT、DEPENDENCY、SESSION_TIMEOUT
筛选:
grep "ERROR" harness-progress.txt # All errors
grep "ERROR" harness-progress.txt | grep "TASK_EXEC" # Execution errors only
grep "SESSION-3" harness-progress.txt # All session 3 activity
grep "STATS" harness-progress.txt # All session summaries
grep "CHECKPOINT" harness-progress.txt # All checkpoints
grep "RECOVERY" harness-progress.txt # All recovery actions
会话统计
在会话结束时,更新 harness-tasks.json:将 last_session 设为当前时间戳。(此处不要递增 session_count——它会在会话开始时递增。)然后追加:
[timestamp] [SESSION-N] STATS tasks_total=10 completed=7 failed=1 pending=2 blocked=0 attempts_total=12 checkpoints=23
blocked 在生成统计时计算:统计 depends_on 中包含永久失败任务的待处理任务数量。它不是存储的状态值。
初始化命令(/harness init)
- 创建
harness-progress.txt并写入初始化条目 - 创建
harness-tasks.json,其中包含空任务列表和默认session_config - 可选:创建
harness-init.sh模板(chmod +x) - 询问用户是否将 harness 文件添加到
.gitignore?
状态命令(/harness status)
读取 harness-tasks.json 和 harness-progress.txt,然后显示:
- 任务摘要:按状态统计数量(已完成、失败、待处理、受阻)。
blocked=depends_on中包含永久失败任务的待处理任务(计算得出,不是存储的状态)。 - 每个任务一行:
[status] task-id: title (attempts/max_attempts) harness-progress.txt的最后 5 行- 会话次数和上次会话时间戳
不获取锁(只读操作)。
添加命令(/harness add)
向 harness-tasks.json 追加一个新任务,使用自动递增的 ID(task-NNN)、状态 pending、默认 max_attempts: 3、空的 depends_on,且不设置验证命令(任务完成前必须提供)。提示用户填写可选字段:priority、depends_on、validation.command、timeout_seconds。需要获取锁(会修改 JSON)。
工具依赖项
需要:Bash、文件读写、git。所有 harness 操作都必须从项目根目录执行。 不需要:特定的 MCP 服务、编程语言或测试框架。
并发模式需要使用隔离的工作目录(git worktree 或单独克隆)。不要在同一个工作树中运行并发工作进程。