mcpField
H
harness

harness

github.com/stellarlinkcoAgent 工作流2.8k154 次安装26 周前更新
代码仓库

关于 harness

harness 是面向长时间项目工作的技能,可让 Agent 跨会话持续执行任务。它通过持久化进度和任务状态,支持恢复、依赖处理、重试与回滚。

主要功能

  1. 01跨会话执行任务
  2. 02进度日志与结构化状态
  3. 03依赖校验与排序
  4. 04恢复中断任务
  5. 05失败回滚与重试

适用场景

  1. 01上下文重置后继续项目工作
  2. 02按依赖关系执行并检查任务
  3. 03恢复并重试失败的任务

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/stellarlinkco/myclaude --skill harness

安装后在 Claude Code 中输入/harness确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

hooks/
_harness_common.py
harness-claim.py
harness-renew.py
harness-sessionstart.py
harness-stop.py
harness-subagentstop.py
harness-teammateidle.py
hooks.json
self-reflect-stop.py
tests/
e2e-100tasks.sh
test_hooks.py

详细介绍

Harness — 长时间运行的 Agent 框架

可执行协议,使任何 Agent 任务都能跨多个会话持续运行,并支持自动恢复进度、解决任务依赖关系、失败回滚和标准化错误处理。

设计原则

  1. 为 Agent 而非人设计 — 测试输出、文档和任务结构是 Agent 的主要接口
  2. 进度文件就是上下文 — 上下文窗口重置时,进度文件 + git 历史记录 = 完整恢复
  3. 过早完成是头号失败模式 — 结构化任务列表和明确的完成标准可防止过早宣告胜利
  4. 将所有内容标准化,便于 grep 搜索 — ERROR 位于同一行,时间戳采用结构化格式,前缀保持一致
  5. 快速反馈循环 — 预先计算统计数据,在完整验证前运行冒烟测试
  6. 所有操作都应具备幂等性 — 初始化脚本、任务执行和环境设置都必须能够安全地重复运行
  7. 安全失败,而非静默失败 — 每种失败都必须有明确的恢复策略

命令

/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)会销毁其他工作进程的内容。

无限循环协议

会话开始(每次执行)
  1. 读取状态:读取 harness-progress.txt 的最后 200 行,以及完整的 harness-tasks.json。如果 JSON 无法解析,请参阅错误处理中的 JSON 损坏恢复。
  2. 读取 Git 状态:运行 git log --oneline -20 和 git diff --stat,检测未提交的工作。
  3. 获取锁(取决于模式):独占模式下,如果另一会话处于活动状态,则失败。并发模式仅在状态事务期间使用锁。
  4. 恢复中断的任务(参见下文的上下文窗口恢复)
  5. 健康检查:如果 harness-init.sh 存在,则运行它。
  6. 跟踪会话:在 JSON 中递增 session_count。检查 session_count 是否达到 max_sessions;如果已达到,则记录 STATS 并停止。将每会话任务计数器初始化为 0。
  7. 使用下方的任务选择算法选择下一个任务。
任务选择算法

选择任务前,先运行依赖项验证:

  1. 循环检测:对于每个未完成的任务,沿 depends_on 递归遍历。如果某个任务出现在自身的依赖链中,则将其标记为 failed,并附加 [DEPENDENCY] Circular dependency detected: task-A -> task-B -> task-A。自引用(depends_on 包含自身 ID)也属于循环。
  2. 阻塞传播:如果某个任务的 depends_on 包含一个失败且不会重试的任务(即 attempts >= max_attempts,或其 error_log 包含 [DEPENDENCY] 条目),则将被阻塞的任务标记为 failed,并附加 [DEPENDENCY] Blocked by failed task-XXX。重复此步骤,直到无法继续传播。

然后按以下优先级顺序选择下一个任务:

  1. status: "pending" 且所有 depends_on 任务均为 completed 的任务——先按 priority 排序(P0 > P1 > P2),再按 id 排序(从低到高)
  2. status: "failed"、attempts < max_attempts 且所有 depends_on 均为 completed 的任务——先按优先级排序,再按失败时间从早到晚排序
  3. 如果没有符合条件的任务,则记录最终 STATS 并停止
任务执行周期

对每个任务,严格按以下顺序执行:

  1. 认领(在锁内原子执行):记录 started_at_commit = 当前 HEAD 哈希。将状态设为 in_progress,设置 claimed_by 和 lease_expires_at,记录 Starting [<task-id>] <title> (base=<hash>)。如果任务已被认领(状态为 in_progress 且租约有效),则选择另一个符合条件的任务并重试。
  2. 执行并设置检查点:执行工作。每完成一个重要步骤后,记录:
    [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)。
  3. 验证:使用超时包装器运行任务的 validation.command(优先使用 timeout;在 macOS 上使用 coreutils 中的 gtimeout)。如果 validation.command 为空或为 null,则记录 ERROR [<task-id>] [CONFIG] Missing validation.command 并停止——未经客观检查,不得声明任务完成。运行前,验证命令是否存在(例如 command -v <binary>);如果不存在,则视为 ENV_SETUP 错误。
    • 命令退出码为 0 → 通过
    • 命令退出码非 0 → 失败
    • 命令超时 → 超时
  4. 记录结果:
    • 成功:将状态设为 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 时处理重试)。
  5. 跟踪:递增每会话任务计数器。如果达到 max_tasks_per_session,则记录 STATS 并停止。
  6. 继续:立即选择下一个任务(不留空闲时间)。
停止条件
  • 所有任务均已 completed
  • 所有剩余任务均已达到最大尝试次数或因依赖项失败而受阻,状态为 failed
  • 本次会话已达到 session_config.max_tasks_per_session
  • 所有会话已达到 session_config.max_sessions
  • 用户中断

上下文窗口恢复协议

新会话启动时,如果发现某个任务的状态为 in_progress:

  • 独占模式:将其视为上一个会话中断,并执行以下恢复协议。
  • 并发模式:仅当 (a) claimed_by 与此工作进程匹配,或 (b) lease_expires_at 已过期(租约过期)时,才恢复任务。否则,将其视为由其他工作进程持有,不要对其进行修改。
  1. 检查 git 状态:
    git diff --stat          # Uncommitted changes?
    git log --oneline -5     # Recent commits since task started?
    git stash list           # Any stashed work?
    
  2. 检查检查点:读取任务的 checkpoints 数组,以确定最后完成的步骤
  3. 决策矩阵(通过检查提交消息中的任务 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
  1. 记录恢复日志:[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)

  1. 创建 harness-progress.txt 并写入初始化条目
  2. 创建 harness-tasks.json,其中包含空任务列表和默认 session_config
  3. 可选:创建 harness-init.sh 模板(chmod +x)
  4. 询问用户是否将 harness 文件添加到 .gitignore?

状态命令(/harness status)

读取 harness-tasks.json 和 harness-progress.txt,然后显示:

  1. 任务摘要:按状态统计数量(已完成、失败、待处理、受阻)。blocked = depends_on 中包含永久失败任务的待处理任务(计算得出,不是存储的状态)。
  2. 每个任务一行:[status] task-id: title (attempts/max_attempts)
  3. harness-progress.txt 的最后 5 行
  4. 会话次数和上次会话时间戳

不获取锁(只读操作)。

添加命令(/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 或单独克隆)。不要在同一个工作树中运行并发工作进程。

评论

支持 Markdown