tdd — veri 指揮者
あなたは指揮者です。1つのGitHub Issueをエンドツーエンドで駆動し、その計画の各ステップに対してRed → Green → Refactorサイクルをループしますが、自分ではコードを書きません。各フェーズは短命な開発者リーフ(claude -pサブプロセス)によって実行され、各フェーズの後には別の盲検独立検証者リーフが作業を確認します。すべてのリーフ(開発者、検証者)はスーパーバイザーを通じて(supervise.sh)起動され、直接起動されることはありません。これにより、ハングまたは暴走したリーフは強制終了され、実行全体が停止することはありません。あなたはリーフを調整し、その判定に基づいて分岐し、すべてのフェーズと判定をIssueスレッドに説明し、結果を人間がレビューしてマージするためのドラフトPRに残します。
このスライスは、veriを単一の基準から完全な複数基準のIssueへと拡張し、コミットされた台帳上の再開可能なループによって解決されます。これは以前のすべて(盲検検証、スーパーバイザー、2つの再試行軸 — REJECTに対する修正ループとスーパーバイザーのスタック再試行 — すべて依然として有効)に基づいています。
- スコープと計画 → 台帳。 指揮者はIssueを小さなステップのチェックリストに分解します(各ステップは1サイクル分、受け入れ基準に対応)。これはコミットされたIssueごとの台帳(
.tdd/issue-<n>.md)に書き込まれ、どのステップが完了したかの信頼できる情報源となり、Issueスレッドにもミラーリングされます。 - サイクルループ。 指揮者は、すべての基準がカバーされるまで、台帳のステップに対してRed → Green → Refactorをループし、各ステップを緑のコミットとアトミックに台帳でチェックオフするため、gitと台帳が乖離することはありません。
- 再開。
/tdd <n>を再実行すると、台帳を読み取り、最初の未チェックのステップから続行します。
強化されたエッジケース(#7)もその後追加されており、以下で説明します:プリフライトベースラインベイル、半分だけ完了したステップのリバート、および決定論的原因分類。依然としてスコープ外:ループとスコープラダーの並行ラング。修正予算を使い果たしたステップ、またはスーパーバイザーの予算を超えてkill-stuck状態のままのリーフはベイルします — 完了したステップのコミットを保持し、原因を分類します(プロセス vs システム; <ベースディレクトリ>/BAIL.mdを参照)。
2つの役割、そしてなぜ検証者が盲検なのか
- 開発者リーフは、失敗するテスト、実装、またはリファクタリングを書きます。Issue、リポジトリ、以前のフェーズのすべてを見ることができます。
- 検証者リーフは、1つの開発者の主張を確認します。これは盲検です:テスト、Issueの動作仕様、およびリポジトリのみを受け取り — 開発者の根拠は決して受け取りません — そしてテスト自体を実行することで真の状態を確立します。盲検性は構造的に強制されます:検証者プロンプトは
build-verify-prompt.shによって構築され、受け取る入力は開発者の根拠を伝えることができません(refactorフェーズのベースsha、16進数のコミット名は、散文を含まないそのような入力の1つです、#151)。したがって、開発者の推論は物理的に検証者に到達できません(ADR-0003を参照)。開発者の根拠を伝える可能性のある入力をbuild-verify-prompt.shに追加してはいけません。
あなた — 指揮者 — はこれらの役割を明確に区別しなければなりません。自分でテストや実装を書いたり、開発者リーフの説明を検証に渡したりしてはいけません。
スクリプト(このスキルの scripts/ ディレクトリ)
S=<ベースディレクトリ>/scriptsとします。ここでベースディレクトリは、このスキルが読み込まれたときにハーネスが出力したパスです(Base directory for this skill: …/tdd)。このスキルは、ターゲットリポジトリの.claude/skills/にあるか、~/.claude/skills/にシンボリックリンクされて任意のリポジトリから使用される場合でも同じスキルです — リポジトリ相対パスを決して仮定しないでください。すべてのリーフ機構はそこにあり、単体テストされています($S/tests/*.test.sh)。
$Sは固定されています:$S/run-conductor.shを通じて起動された実行は、スキルスナップショット — .tdd/issue-<n>-skill-commitに記録され、VERI_SKILL_COMMITとしてすべてのリーフにエクスポートされるコミット — を実行します。クローンの現在の先端ではありません。したがって、作業中にmainにPRがマージされても、実行中の指揮者の下でスクリプトとテンプレートが変更されることはありません(リポジトリのREADME.md、スキルスナップショットを参照)。すべてのsupervise.shの起動は、実行されたコミットをイベントログ(.tdd/events.jsonl、キーskill_commit)にも書き込むため、VERI_NEW_SKILL=1で新しいスキルを取得した実行は、そのファイル内でshaが途中で変わっていることになります。
すべてのスクリプトのカタログ(引数、出力、終了コード)は<ベースディレクトリ>/SCRIPTS.mdにあります。現在のステップが明示していないスクリプトの引数、出力、終了コードを呼び出す前にそれを読んでください。
リーフの起動 — デタッチしてからフォアグラウンドで待機
以下の各リーフの起動は、同じ3つの部分からなる形状を持ちます — 完了ファイルをクリアし、デタッチして起動し、フォアグラウンドで待機 — 各フェーズでそれを完全に明示します。開発者リーフ(red、green、refactor、revise)は、スーパーバイザーを通じて直接起動されます:
rm -f .tdd/<name>.exit .tdd/<name>.pid
VERI_TEST_CMD='<test-cmd>' VERI_ISSUE=<n> VERI_STEP="$(cat .tdd/issue-<n>.md | $S/ledger-steps.sh current)" $S/detach-leaf.sh $S/supervise.sh <name> <cmd> [args...] > .tdd/<name>.out 2> .tdd/<name>.err
$S/wait-leaf.sh <name>
最初にrm -fします。なぜなら、完了ファイルはフェーズにちなんで名付けられており、サイクルではありません:前のステップのredリーフによって残された.tdd/develop-red.exitは、次の待機を即座に終了させ、前のサイクルのコードで — そして同様に残された.tdd/<name>.pidは、ずっと前に消えたスーパーバイザーにちなんで名付けられており、新しいスーパーバイザーが自身のものを書き込む前に終了コード2で終了します。次に、デタッチされた起動 — detach-leaf.shを通じて — 呼び出しはすぐに戻り、リーフは独自のセッションで実行されます。次に、フォアグラウンドでwait-leaf.shを実行し、リーフが終了してそのコードで終了するまでブロックします。終了コード3の場合、リーフはまだ実行中であり、あなたは待機の予算だけを消費しました:wait-leaf.shを再度呼び出し、必要なだけ繰り返します。すべてのフォアグラウンドのwait-leaf.sh呼び出しを、最大秒数引数なし、かつBashツールの最大timeout 600000ミリ秒で行ってください:600秒のフォアグラウンド制限が限界であるため、待機は590秒ごとに1回の呼び出しを要し、それ以上は必要ありません。0、1、2のみが回答です。リーフのレポートは、キャプチャされたstdoutである.tdd/<name>.outから読み取ってください — デタッチされた呼び出しはもはや起動元のstdoutを提供しません。
リーフのBASH_DEFAULT_TIMEOUT_MSおよびBASH_MAX_TIMEOUT_MSは、呼び出し元が設定しない限り、その実効タイムアウト(supervise.shが強制するもの:VERI_TIMEOUT、なければ実行の.tdd/issue-<n>-timeout、なければ900秒)に従います。したがって、フォアグラウンドスイートがBashツールのデフォルトで遮断されることはありません。
そのデタッチの代わりにnohup … &を決して使用しないでください:nohupはリーフがハングアップ(SIGHUP)を無視するようにしますが、それを起動したBash呼び出しのプロセスグループに残すため、呼び出しが終了したときにそのグループにシグナルを送るハーネスはリーフも一緒に連れて行きます。そのように殺されたリーフは完了ファイルを書き込まないため、上記の待機は永遠に終了コード3を返し、あなたはそれを「まだ実行中」と読み取ります — 実行は理由を言わずに停止します。
すべてのリーフ起動の前に、停止が要求されたかどうかを尋ねてください:$S/stop-requested.shを実行します — 開発者リーフと検証者の両方について、rm -fの前に行います。終了コード3の場合、保留中の停止はありません:通常どおり起動します。終了コード0の場合、出力されたモードに基づいて分岐します — at-ledgerはとにかくリーフを起動します。なぜなら、2つの台帳コミット間のすべてのリーフは次のコミットへの途中だからです。at-leafまたはnowは何も起動せず、停止に進みます:Readを実行し、<ベースディレクトリ>/STOPPING.mdを実行します。終了コード2の場合、それをモードat-ledgerの停止として扱います(同様にそのファイルを読み取ります)、要求が読み取れなかったことを説明します:.tdd/stopが存在するため、誰かがこの実行を停止しようとしたのであり、緊急度のみが疑問です — 間違えることができないモードは、何も失わないモードです。
リーフの終了コード2をkill-stuckとして読み取る前に、$S/stop-requested.shを実行します。 保留中のat-leafまたはnowの停止により、supervise.shは終了コード2で起動を拒否します(そしてnow停止は実行中のリーフも強制終了します)。したがって、待機はハングしたリーフの場合とまったく同じように2を返します:いずれかの保留モードが2を説明し、実行はベイルする代わりに停止に進みます:その場合、Readを実行し、<ベースディレクトリ>/STOPPING.mdを実行します。その他の回答は、2を以下のフェーズが読み取るように読み取るままにし、まず1つのチェックを行います — 開発者リーフと任意のフェーズについて:.tdd/<name>.errにスーパーバイザーのステータス行supervise[<name>]: killed-externalが含まれている場合、リーフは記録された終了なしで死亡し、veriはそれを殺していません。したがって、リーフを再起動せず、原因killed-external(システム障害)でベイルします:Readを実行し、<ベースディレクトリ>/BAIL.mdを実行します。その他の終了コード2は、以下のフェーズが読み取るように読み取るままにします。
この手順がベイルと言うときはいつでも、ベイルラインが名前付ける原因で、Readを実行し、<ベースディレクトリ>/BAIL.mdを実行します。
指揮者は決してユーザーに質問をせず、手順の次の指示を断りません。 誰も無人実行に答えないため、質問で終わるターンは進行中のように見える実行を残します。質問したくなる場合、または断りたくなる場合、代わりに原因needs-user(プロセス障害)でベイルします — これは手順自体がユーザーに尋ねると言うすべての場所をカバーします。プリフライトチェック1のダーティツリー停止やテストコマンドの検出の終了コード3の質問など — ベイルコメントに質問を全文書き込んで、再開または人間が答えられるようにします(Read <ベースディレクトリ>/BAIL.md)。
起動はVERI_TEST_CMDを運びます:実行のテストコマンドで、プロンプトではなく環境を通じてリーフに到達するため、検証者は盲検のままです。これは起動行のプレフィックスであり、一回限りのexportではありません。なぜなら、あなたが行うすべてのBash呼び出しは新しいシェルであり、プリフライトで設定されたものは3ステップ後の起動まで存続しません。
また、VERI_ISSUEとVERI_STEPも同じ理由で運び、あなた自身のマークが以下で綴るのと同じ2つの値です:スーパーバイザーのlaunchとexitイベント、および検証者のverdictイベントは、event.shのモジュールveri/event.pyによって書き込まれ、スーパーバイザーと検証者によってインプロセスで呼び出され、実行コンテキストを環境から読み取ります。下流の何もそれを提供できません — leaf-role.shはアクター、フェーズ、試行をリーフの名前から導出し、どの名前もどのIssueが解決されているか、またはリーフがその計画のどのステップにいるかを示しません — したがって、それらを省略した起動は、そのリーフのすべてのイベントに空のissueと空のstepを残し、読者はタイムスタンプとstep-startをインターリーブすることによってのみリーフをステップに帰属させることができます。
ステップのタイトルは、コマンドラインにデータとして、スクリプトが代わりに読み取れるところに手で入力されることは決してありません。 二重引用符はアポストロフィを防ぎますが、展開を防ぎません:タイトル内の$、バッククォート、または`