harness について
harness は、長期にわたるプロジェクト作業を複数セッションで続けるためのスキルです。進捗とタスク状態を保存し、復旧、依存関係の処理、再試行、ロールバックを支援します。
主な機能
- 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 — 長時間実行エージェントフレームワーク
進捗の自動復旧、タスク依存関係の解決、失敗時のロールバック、標準化されたエラー処理を備え、あらゆるエージェントタスクを複数のセッションにわたって継続実行できるようにする実行可能なプロトコルです。
設計原則
- 人間ではなくエージェント向けに設計する — テスト出力、ドキュメント、タスク構成がエージェントの主要なインターフェースです
- 進捗ファイルがコンテキストそのものです — コンテキストウィンドウがリセットされた場合、進捗ファイルと 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- すべてのタスクが完了したら(保留中、実行中、再試行可能なタスクがなくなったら)、削除します:
rm <project-path>/.harness-active - このマーカーがない場合、すべてのフックは何もせず、直ちに 0 を終了コードとして返します
進捗の永続化(2 ファイル方式)
プロジェクトの作業ディレクトリに、次の 2 つのファイルを維持します。
harness-progress.txt(追記専用ログ)
セッションをまたいだエージェントのすべての操作を記録する自由形式のログです。切り詰めてはいけません。
[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 タイムスタンプ)のクレームメタデータが含まれる場合もあります。
セッション境界:エージェントがセッション開始プロトコルの実行を開始した時点でセッションが始まり、停止条件を満たした時点、またはコンテキストウィンドウがリセットされた時点で終了します。各セッションには一意の 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ハンドラーによって自動的に解放されます)。同じ状態ルートで 2 つ目のセッションを開始すると、直ちに失敗します。 - 並行(
session_config.concurrency_mode: "concurrent"でオプトイン):これは状態トランザクションロックとして扱います。harness-tasks.jsonの読み取り、変更、書き込み(.bak/.tmpを含む)とharness-progress.txtへの追記を行う間だけロックを保持し、実作業を始める直前に解放します。
並行モードの不変条件:
- すべてのワーカーが同じ状態ルート(
harness-tasks.jsonを含むディレクトリ)を指定する必要があります。別々の worktree/クローンを使用する場合は、明示的に固定してください(例:HARNESS_STATE_ROOT=/abs/path/to/state-root)。 - タスクの選択は参考情報であり、実際のゲートとなるのはロック取得中のアトミックなクレームです:
status="in_progress"を設定し、claimed_by(HARNESS_WORKER_IDなどの安定したワーカー ID)とlease_expires_atを設定します。クレームに失敗した場合(有効なリース付きで既にin_progressの場合)、別の実行可能なタスクを選んで再試行します。 - 同じ git 作業ディレクトリで 2 つのワーカーを実行してはいけません。別々の worktree/クローンを使用してください。そうしないと、ロールバック(
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を再帰的にたどります。いずれかのタスクが自身の依存関係チェーンに含まれている場合、そのタスクを[DEPENDENCY] Circular dependency detected: task-A -> task-B -> task-Aを付けてfailedに設定します。自身への参照(depends_onに自身の ID が含まれている場合)も循環依存です。 - ブロック状態の伝播:タスクの
depends_onに、再試行されることがないfailed状態のタスク(attempts >= max_attempts、またはerror_logに[DEPENDENCY]エントリが含まれているタスク)がある場合、ブロックされたタスクを[DEPENDENCY] Blocked by failed task-XXXを付けてfailedに設定します。伝播するタスクがなくなるまで繰り返します。
次の優先順位で次のタスクを選択します。
status: "pending"で、すべてのdepends_onタスクがcompletedのタスク —priority(P0 > P1 > P2)、次にid(小さい順)でソートstatus: "failed"、attempts < max_attemptsで、すべてのdepends_onがcompletedのタスク — 優先度、次に失敗した日時が古い順でソート- 選択可能なタスクが残っていない場合 → 最終 STATS を記録 → 停止
タスク実行サイクル
各タスクについて、次の手順を正確に実行します。
- 確保(ロック下でアトミックに実行):現在の HEAD ハッシュを
started_at_commitに記録します。ステータスを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 で終了 → PASS
- コマンドが 0 以外で終了 → FAIL
- コマンドがタイムアウトを超過 → TIMEOUT
- 結果を記録:
- 成功:status=
completedに設定し、completed_atを設定して、Completed [<task-id>] (commit <hash>)を記録し、git commit を実行します。 - 失敗:
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>を記録します。status をfailedに設定します(attempts < max_attemptsの場合、タスク選択アルゴリズムの手順 2 で再試行されます)。
- 成功:status=
- 記録する:セッションごとのタスクカウンターをインクリメントします。
max_tasks_per_sessionに達した場合は STATS を記録して停止します。 - 続行する:直ちに次のタスクを選択します(待機時間なし)。
停止条件
- すべてのタスクが
completed - 残りのすべてのタスクが、max_attempts に達したか、失敗した依存関係によってブロックされて
failed - このセッションで
session_config.max_tasks_per_sessionに到達した - すべてのセッションを通じて
session_config.max_sessionsに到達した - ユーザーが中断した
コンテキストウィンドウの復旧プロトコル
新しいセッションの開始時に status: "in_progress" のタスクが見つかった場合:
- 排他モード:前のセッションが中断されたものとして扱い、以下の復旧プロトコルを実行します。
- 並行モード:このワーカーに
claimed_byが一致する場合、または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配列を読み、最後に完了した手順を特定します - 判断マトリクス(コミットメッセージで task-id を確認し、最近のコミットがこのタスクに属することを検証します):
| 未コミットの変更? | 最近のタスク関連コミット? | チェックポイント? | 対応 |
|---|---|---|---|
| いいえ | いいえ | なし | [SESSION_TIMEOUT] No progress detected で failed にし、試行回数を増やします |
| いいえ | いいえ | あり | ファイルの状態がチェックポイントの記録と一致するか確認します。ファイルにチェックポイントの進捗が反映されている場合は、最後の手順から再開します。反映されていない場合は、作業が失われたものとして 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>"
エラー処理と復旧戦略
各エラーカテゴリには、デフォルトの復旧戦略があります:
| カテゴリ | デフォルトの復旧方法 | エージェントの対応 |
|---|---|---|
ENV_SETUP | 初期化を再実行し、それでも失敗する場合は停止 | 直ちに harness-init.sh を再実行します。2 回実行しても失敗する場合は、記録して停止します。環境が破損しています |
CONFIG | 停止(人による修正が必要) | 設定エラーを正確に(ファイルとフィールドを含めて)記録し、停止します。推測したり、タスクのメタデータを自動変更したりしないでください |
TASK_EXEC | git reset --hard <started_at_commit> でロールバックし、再試行 | started_at_commit が存在することを確認します(git cat-file -t <hash>)。存在しない場合は、max_attempts に達したものとして failed にします。それ以外の場合はリセットし、定義されていれば 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 で検索しやすいように1行形式にします。
[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 に恒久的に失敗したタスクを含む、保留中のタスク数です。保存されるステータス値ではありません。
Init コマンド(/harness init)
- 初期化エントリを含む
harness-progress.txtを作成します - 空のタスクリストとデフォルトの
session_configを含むharness-tasks.jsonを作成します - 任意で
harness-init.shのテンプレートを作成します(chmod +x) - harness ファイルを
.gitignoreに追加するかユーザーに尋ねます
Status コマンド(/harness status)
harness-tasks.json と harness-progress.txt を読み取り、次を表示します。
- タスクの概要:ステータス別の件数(completed、failed、pending、blocked)。
blockedは、depends_onに恒久的に失敗したタスクを含む保留中のタスク数です(保存されるステータスではなく、計算されます)。 - タスクごとの1行表示:
[status] task-id: title (attempts/max_attempts) harness-progress.txtの最後の5行- セッション数と最終セッションのタイムスタンプ
ロックは取得しません(読み取り専用の操作です)。
Add コマンド(/harness add)
自動採番された ID(task-NNN)、ステータス pending、デフォルトの max_attempts: 3、空の depends_on、およびタスク完了前に必須となる検証コマンドなしで、新しいタスクを harness-tasks.json に追加します。任意のフィールドとして priority、depends_on、validation.command、timeout_seconds をユーザーに入力してもらいます。ロックの取得が必要です(JSON を変更するため)。
ツールの依存関係
必要なもの:Bash、ファイルの読み書き、git。すべてのハーネス操作はプロジェクトのルートディレクトリから実行する必要があります。
不要なもの:特定の MCP サーバー、プログラミング言語、テストフレームワーク。
並行モードでは、分離された作業ディレクトリ(git worktree または別々のクローン)が必要です。同じ作業ツリーで並行ワーカーを実行しないでください。