diff --git a/docs/specifications/README.md b/docs/specifications/README.md index cfceb4199..6844855a0 100644 --- a/docs/specifications/README.md +++ b/docs/specifications/README.md @@ -18,6 +18,7 @@ | [cross-review-evidence-based.md](cross-review-evidence-based.md) | 証拠ベースのレビューと効果の測定。状態ファイルの契約と決定の理由。手順は `cross-review` の `SKILL.md` が正 | | [cross-review-launch-outcome.md](cross-review-launch-outcome.md) | 起動 1 回の結末の語彙(理由 9 語)と起動し直しの可否、利用上限と CLI の上限の検知、プロセスグループでの起動と停止。手順は `cross-review` の `SKILL.md` と `docs/` が正 | | [cross-review-participants-and-seats.md](cross-review-participants-and-seats.md) | 使える者だけで収束ループを始める共通層(認証の確認を止めない形・参加の母集合と足す者/外す者・毎ラウンド 2 席の埋め方・席の名前)と、再開で渡した引数の反映。手順は `cross-review` の `SKILL.md` と `docs/` が正 | +| [cross-review-writes-to-conductor.md](cross-review-writes-to-conductor.md) | GitHub と git への書き込み(レビューの投稿・返信・決着・修正のまとめ・修正の送信)をレビューを回す側だけが行うこと、担当が書く 2 つのファイルと改名の順序、二度書かない照合、差分の外を指す指摘の退避、途中で止まったときの立て直し、起動し直しを初回と同じ経路へ通すこと。手順は `cross-review` の `SKILL.md` と `docs/` が正 | | [cross-refactoring-apply-intake.md](cross-refactoring-apply-intake.md) | 担当が結果を残さない起動を 3 つの取り込みが同じ手順で受けること(範囲の確定・未検証のコミットの取り消し・結末の記録)、適用ラウンドの開き直しの判定と試行の上限 2 回、項目の無い適用ラウンドを作らないこと、帰属の段落の後ろから必須の記名を読むこと。手順は `cross-refactoring` の `SKILL.md` と `docs/` が正 | | [cross-refactoring-participants.md](cross-refactoring-participants.md) | cross-refactoring の参加者(codex / kiro とホストを既定に足す者/外す者で変える・確認を通らない者を外して続ける)、提案と適用を同じ参加者で回す輪番、再開で渡した引数の反映、呼び手の無くなった共通層の旧関数の削除。手順は `cross-refactoring` の `SKILL.md` と `docs/` が正 | | [ndf-cleanup-and-bundle-closing.md](ndf-cleanup-and-bundle-closing.md) | 後片付けが止まる条件(git の拒否だけ)、実行前確認の要否を決める 3 つの問い、まとまりの課題を終わりの工程で閉じる条件と結果の 4 値、配布の記録の形と読み方。手順は `merged` / `progress-tracking` / `release` の SKILL.md が正 | @@ -25,5 +26,6 @@ | [ndf-context-window-metrics.md](ndf-context-window-metrics.md) | 会話の記録から context window を 3 層で測る部品(`transcript_agents.py`)の値の取り方と、`skill-stats --agents` の 4 つの表と印。値の取り方はこの文書が正 | | [ndf-execution-plan-and-parallel-capacity.md](ndf-execution-plan-and-parallel-capacity.md) | 並列の実行計画(依存を工程の対で書く・重なりの 3 区分・開いている間はコミットしない)、マイルストーンの組、メモリで見る本数(`parallel-measure.py`)。手順は `issue-plan-strategy` と `development-workflow` の `references/` が正 | | [ndf-instruction-files-check.md](ndf-instruction-files-check.md) | エージェント向け指示書の検査(`instructions-check.py`)。宣言 `.ndf/instructions.json` で決まる判定の強さ、即時読み込みと出た版の段落の判定、扱いの印、観点の調べ直し。呼び方と宣言の書き方は `release` の `references/instruction-files.md` が正 | +| [test-monitor-env-isolation.md](test-monitor-env-isolation.md) | テストの実行中だけ監視の上限を指す環境変数(接頭辞 `MONITOR_`)をリポジトリの根の共通の前提で外すこと、外す時点と戻す時点、根の設定ファイルで基準のディレクトリを固定すること | Skill の挙動仕様はここに置かない。Skill に関する詳細は対象 Skill の `SKILL.md` を参照する。 diff --git a/docs/specifications/cross-review-evidence-based.md b/docs/specifications/cross-review-evidence-based.md index 80adeb343..51c0c3603 100644 --- a/docs/specifications/cross-review-evidence-based.md +++ b/docs/specifications/cross-review-evidence-based.md @@ -131,11 +131,12 @@ Pull Request では、検証手順を実行できなかった 3 件が立証不 ### 指摘の構造化と独立発見 担当が書き出す `payload.json` の `comments[]` は、**その担当が出した指摘の全件**である。 -投稿したインラインの写しではない。総評だけへ書いた指摘も載り、`path` と `line` はそのときも -埋める。投稿先は `posted_to`(`inline` / `body`)が持つ。 +投稿したインラインの写しではない。位置を持たない指摘も載る。送れた先(`posted_to` の +`inline` / `body`)は、投稿する側が控えへ書き戻す。 -**インラインの件数(`result.json` の `comments_count`)は変えない。** 進行側が GitHub 側の -実数と突き合わせる値であり、指摘の全件を入れると照合が常に食い違う。 +**記録のインラインの件数(`comments`)は、指摘の件数と一致しない。** 送れたインラインの数で +あり、総評へ入った指摘はそこに現れない。投稿の担い手と件数の取り方は +[書き込みを回す側へ集める仕様](cross-review-writes-to-conductor.md)にある。 **発見を終えるまで、参照してよい既存コメントは起動時のスナップショットに限る。** 同じ ラウンドの他の担当の投稿・結果ファイル・進捗ログは参照しない。スナップショットは前の diff --git a/docs/specifications/cross-review-writes-to-conductor.md b/docs/specifications/cross-review-writes-to-conductor.md new file mode 100644 index 000000000..5a0a89add --- /dev/null +++ b/docs/specifications/cross-review-writes-to-conductor.md @@ -0,0 +1,413 @@ +# cross-review: PR に出ている指摘が記録に残らず、同じ論点が 2 つのスレッドに分かれる → GitHub と git へ書くのはレビューを回す側だけになり、途中で止まっても同じものを二度書かない + +## 目的 + +**GitHub と git へ書くのは、レビューを回す側だけである。** レビューの担当は指摘の控えと +結果ファイルを書いて終わる。修正の担当はコミットまでで止まる。投稿・返信・スレッドの決着・ +修正のまとめ・修正の送信は、結果ファイルを取り込む側が行う。 + +**書き込みと記録が同じ手順の中で続けて起きる。** 担当が途中で止まっても、PR には指摘が +出ているのに記録には無い状態ができない。取り込みが途中で止まっても、やり直せば同じものを +二度書かない。 + +**修正を送ったという記録は、送り先のブランチの実物と一致する。** + +**投稿の本文は、取り込みのプロセスの中だけを通る。** 収束ループを駆動している側の応答に +載るのは、件数・参照・状態だけである。 + +**手順と表は +[`cross-review` の SKILL.md](../../plugins/ndf/skills/cross-review/SKILL.md)と +[`docs/`](../../plugins/ndf/skills/cross-review/docs/04-contracts.md)が正である。** +投稿の種別ごとの照合の鍵は +[状態ファイルと入出力の契約](../../plugins/ndf/skills/cross-review/docs/04-contracts.md)が持つ。 +この文書が扱うのは、そこに書かない決定の理由と、共通層の契約である。 + +## 用語 + +本文は左の業務用語で書く。識別子は表・コードブロック・業務用語の初出の括弧書きにだけ置く。 +担当・席・結末の語は +[起動 1 回の結末](cross-review-launch-outcome.md#用語)と +[使える者だけで始める収束ループ](cross-review-participants-and-seats.md#用語)と同じ意味で使う。 + +| 業務用語 | 識別子 | 何を指すか | +| --- | --- | --- | +| レビューを回す側 | `state.py` と、それを呼ぶ `SKILL.md` の骨組み | 担当を起動し、結果を取り込み、判定する側 | +| 担当 | `launch-reviewer.sh` が起動する CLI | 1 ラウンドで 1 席ぶんのレビューを行う | +| 修正の担当 | `/ndf:fix` を実行するサブエージェント | 指摘を直してコミットする | +| 結果ファイル | `<席>-review-pr<番号>-result.json` | 担当の判定の要約 | +| 指摘の控え | `<席>-review-pr<番号>-round-payload.json` | 担当が出した指摘の全件と総評 | +| 修正の結果ファイル | `fix-pr<番号>-result.json` / `sweep-pr<番号>-result.json` | 修正の担当が書く戻り値 | +| 投稿の待ち行列 | `plugins/ndf/scripts/lib/post_queue.py` | 送る前に積み、上限で送れなければ残す仕組み | +| 投稿を組み立てる層 | `plugins/ndf/scripts/lib/result_posts.py` | 結果ファイルから投稿を組み立て、送る共通層 | +| 指摘の取り込み | `state.py read-result` | 控えを読んでレビューを投稿し、記録する | +| 修正の取り込み | `state.py merge-fix` | 修正を送り、返信・決着・まとめを投稿し、記録する | +| 単独の修正の口 | `result_posts.py fix` | 修正を単独で行うときと最終スイープで呼ぶ部分命令 | +| 先客 | — | 送ろうとした投稿と同じものとして、すでに PR にある投稿 | +| 総評 | レビューの本文(`body`) | インラインではなく、レビュー本体に書く文章 | + +## 対象範囲 + +| 扱う | 扱わない | +| --- | --- | +| レビューの投稿(総評とインライン) | PR の巻き直しの締め・作り直し・再開(もとから回す側が行い、待ち行列に積まない) | +| 指摘への返信・スレッドの決着・修正のまとめ | 反証(もとから投稿せず、ファイルへ書くだけ) | +| 修正の送信と、送り先に載ったことの確認 | 収束の判定・指摘の区分と数え方([証拠ベースのレビュー](cross-review-evidence-based.md)) | +| 起動し直した担当を初回と同じ経路へ通すこと | 結末の語彙と起動し直しの可否([起動 1 回の結末](cross-review-launch-outcome.md)) | +| 修正を単独で行うときの書き込み | 席の決め方([使える者だけで始める収束ループ](cross-review-participants-and-seats.md)) | +| — | 単発の `/ndf:pr-review` が担当に直接投稿させる流れ | + +## 背景 + +**書き込みと記録を別の相手が行っていた。** 担当は GitHub へレビューを投稿し、その後に結果 +ファイルを書いていた。この 2 つの間で担当が止まると、投稿は残り、記録は残らない。回す側は +記録しか読まないため「結果なし」と判定し、同じ担当を同じラウンドで起動し直した。起動し +直した担当は同じ論点をもう一度投稿した(#583)。 + +**実例は PR #578 の round 1 である。** 1 回目の起動がインライン 4 件を投稿し、起動し直した +2 回目が 1 件を投稿した。記録に入ったのは 2 回目の 1 件だけだった。利用者は 1 つの論点に +2 つのスレッドを見て、修正の担当は両方へ返信した。 + +**待ち行列は担当の投稿を通していなかった。** 上限で拒まれた投稿を残して後から流す仕組みは +回す側にあった。担当が自分で送るため、上限に当たった投稿はその場で失われた。 + +**送信の報告を確かめる手段が無かった。** 修正の担当はブランチ名だけを指定して送信していた。 +作業ツリーが切り離された頭で作られていると、この指定は現在の頭を送らないまま終了コード 0 で +終わる。取り込む側はその報告をそのまま記録した。 + +**根本原因は、書き込みの担い手が記録の担い手と別であることにある(#730)。** +書き込みを回す側へ集めると、この 3 つが同じ場所で解ける。 + +## 決定と理由 + +| 決定 | 理由 | +| --- | --- | +| GitHub と git への書き込みを回す側だけが行う | 書き込みと記録が同じ手順で続けば、片方だけが残る状態を作れない | +| 担当の投稿の後に回す側が実物を照合して記録を補う形を採らない | 照合は問い合わせを増やす。問い合わせが上限で失敗すると同じ食い違いが残る | +| 担当に待ち行列へ積ませる形を採らない | 待ち行列は回す側の作業ツリーにある。置き場所を担当へ開くと、巻き直しで捨てる範囲が変わる | +| 投稿は結果ファイルを読んだプロセスの中で組み立てる | 本文を引数や標準入力で渡すと、収束ループを駆動する側の応答に本文が載る | +| 「読んで記録する」と「投稿する」を 1 つの取り込みに閉じる | 分けると「記録したが投稿していない」状態がもう 1 つ増える | +| 送信に成功した項目は記録より先に待ち行列から消える形を変えない | 取り込みをやり直せば同じ状態になる。流し方を変えると、巻き直しのコメントの契約まで変わる | +| 二度書かない照合を投稿の 4 種別すべてに掛ける | 途中で止まった取り込みをやり直しても、どの種別も増えない | +| レビューの照合の鍵に判定の語を含めない | 起動し直して判定が変わると、判定の語を含む鍵は別の投稿と読んで二重に送る | +| レビューの照合をラウンドの開始時刻より後のレビューに限る | ラウンドの番号は実行ごとに 1 から数え直す。回し直した PR では前の実行のレビューに一致する | +| 開始時刻かレビューの時刻を読めないときは、番号と席だけで照合する | 二重に送る側へ倒さない | +| 担当の申告件数と GitHub の実数の突き合わせをやめる | 投稿する側と記録する側が同じになり、確かめる対象が無い | +| 差分の外を指す指摘は、拒まれてから総評へ移す | 投稿の前に判定すると差分の範囲を別に取り寄せる必要があり、失敗の分岐が増える | +| 拒まれた要求のインラインは、1 件ずつでなくすべて総評へ移す | 応答はどのインラインが原因かを指さない(実測) | +| 位置を解決できない拒まれ方だけを退避の契機にする | 判定の値や基準のコミットの誤りまで退避すると、別の不具合が飲み込まれる | +| 修正は現在の頭を指定して送り、送った後に照合する | ブランチ名だけの指定は、切り離された頭で何も送らずに成功する | +| 修正の書き込みを共通層の 1 か所にまとめる | cross-review から呼ぶ経路と単独で使う経路で、担い手と実装が分かれない | +| 単独の口は新しい入口のスクリプトにせず、共通層の部分命令にする | 手順に書く行が 1 本で済み、実装が 1 つになる | +| 担当は一時の名前で書いてから改名する | 読めた状態が書き終えた状態になり、取り込みが書きかけを読まない | +| 自分の PR での判定の格下げは、投稿する側が送信の時点で行う | 格下げるのは送る形だけで、収束の判定が読む本来の判定は落とさない | +| 起動し直しを繰り返しの先頭へ戻す形にする | 経路が 1 本なら、根拠の検証と反証を飛ばす枝が構造として無くなる | + +## 仕様 + +### 常に成り立つ条件 + +| 条件 | 破れたときの扱い | +| --- | --- | +| 担当へ渡すプロンプトは、投稿の手順(投稿の呼び出し・判定の格下げ・インラインの組み立て)を持たない | プロンプトの検査が落ちる | +| 担当が控えを書かずに止まった実行では、PR のレビューは増えない | — | +| 同じラウンド・同じ席のレビューは、この実行の中で 1 件しか PR に出ない | 先客があれば送らず、先客の参照を記録する | +| 取り込みの標準出力に、総評とインラインの本文は出ない | — | +| 記録に入るレビューの参照は、送信の応答か先客から取る | — | +| 修正の記録は、報告されたコミットが送り先のブランチに載ってから書く | 載っていなければ記録も投稿もせずに止まる | + +### 構成要素と責務 + +| 構成要素 | 責務 | +| --- | --- | +| 担当の起動(`launch-reviewer.sh`) | 投稿の手順を持たないプロンプトを作る。前の起動の一時の名前のファイルも消す | +| 指摘の取り込み(`read-result`) | 残りを流し、この担当のレビューを組み立てて送り、応答を記録し、指摘を取り込む | +| 修正の取り込み(`merge-fix`) | 修正を送って照合し、記録し、返信・決着・まとめを送る | +| 投稿を組み立てる層(`result_posts.py`) | 控えと結果ファイルから投稿を組み立てる。退避・格下げ・送信・照合を行う | +| 投稿の待ち行列(`post_queue.py`) | 送る前に先客を照合する。拒まれ方が位置によるものかを見分けて返す | +| 骨組み(`SKILL.md`) | 起動し直しを繰り返しの先頭へ戻す。最終スイープの書き込みを単独の修正の口で行う | + +投稿を組み立てる層が外へ見せる関数は次の 5 つである。**どれも本文を引数に取らず、ファイルの +パスを受け取る。** + +| 関数 | 入力 | 出力 | +| --- | --- | --- | +| `review_posts` | 控えと結果ファイルのパス、リポジトリ、PR、ラウンド、席、頭のコミット、自分の PR か、開始時刻 | 待ち行列へ積む項目の列 | +| `post_review` | 待ち行列と `review_posts` の入力、投稿者 | 送った結果(参照・インライン数・総評へ移した数・残した数・指摘数・失敗・送った形・本来の判定) | +| `fix_posts` | 修正の結果ファイルのパス、リポジトリ、PR、ラウンド | 返信 → 決着 → まとめの順の項目の列 | +| `post_fix` | 待ち行列と `fix_posts` の入力、投稿者 | まとめの参照・返信数・決着数・残した数・失敗 | +| `push_fix` | 作業ツリー、送り先のブランチ、報告されたコミット | 成否・送ったか・載っているか・説明 | + +### 担当が書くもの + +担当は 2 つのファイルを書く。**どちらも一時の名前(末尾 `.tmp`)で書き終えてから、控え → +結果ファイルの順に正式の名前へ改名する。** 結果ファイルが正式の名前で現れたことが、2 つとも +書き終えた印になる。 + +| 状態 | 取り込みの扱い | +| --- | --- | +| 結果ファイルが正式の名前である | 控えを読んで投稿する | +| 控えだけが正式の名前で、結果ファイルが無い | 結果なし。投稿は 0 件 | +| どちらも正式の名前に無い | 結果なし。投稿は 0 件 | + +担当が書く項目と、投稿する側が埋める項目は次のとおりである。 + +| ファイル | 担当が書く | 投稿する側が埋める | +| --- | --- | --- | +| 結果ファイル | 本来の判定(`event`)、重要度ごとの件数(`by_severity`) | —(記録の側に書く) | +| 指摘の控え | 総評(`summary`)、指摘の全件(`comments[]`) | 各指摘の送れた先(`posted_to` の `inline` / `body`) | + +指摘の各項目が持つ値は +[状態ファイルと入出力の契約](../../plugins/ndf/skills/cross-review/docs/04-contracts.md)にある。 + +### 指摘の取り込み + +取り込み 1 回の中を、次の順に進める。 + +1. 待ち行列の残りを流す。残りは前の取り込みが上限で送れなかった投稿で、その担当の記録は + 積んだ時点で書いてある +2. 控えと結果ファイルから、この担当のレビューを 1 件組み立てて積む +3. 流す +4. 位置を解決できずに拒まれたら、インラインをすべて総評へ移して積み直し、もう 1 度流す +5. 送れたら、控えへ送れた先を書き戻す +6. 送信の応答を記録へ書き、指摘を取り込む + +```mermaid +sequenceDiagram + participant M as レビューを回す側 + participant A as 担当 + participant G as GitHub + M->>A: 起動(投稿の手順を持たないプロンプト) + A->>A: 控えと結果ファイルを書いて改名する + A-->>M: 終了 + M->>M: 控えを読み、レビューを待ち行列へ積む + M->>G: 先客を照合し、いなければ送る + G-->>M: レビューの参照 + M->>M: 参照と件数を記録し、指摘を取り込む +``` + +**組み立てるレビューの本文の先頭行は、ラウンド・席・本来の判定を持つ。** + +```text +## 🤖 cross-review | round | <席> | <本来の判定> +``` + +**位置を持つ指摘はインラインに、持たない指摘は総評に入る。** 位置を持つとは、ファイルが +あり、行が正の整数として読めることである。行が `"L42"` や `"40-45"` の形なら総評へ回す。 + +### 差分の外を指す指摘の退避 + +GitHub はレビューの作成を**要求ごとに全件拒む**。差分の外を指すインラインが 1 件あると、 +正しいインラインも総評も作られない。応答の誤りは語をつないだ 1 つの文字列で、どの +インラインが原因かを指さない。 + +| 送ったもの | 応答の誤り | 扱い | +| --- | --- | --- | +| 差分に無いファイルのインライン | `Path could not be resolved` | 総評へ移して送り直す | +| 差分にあるファイルの、塊の外の行のインライン | `Line could not be resolved` | 同上 | +| 判定の値に知らない語 | `Variable $event ... was provided invalid value` | 失敗として止まる | +| 基準のコミットに存在しない値 | `The commitOID is not part of the pull request` | 同上 | + +**退避の契機は、HTTP 422 で応答が `could not be resolved` を含むときだけである。** +**退避するのは、この取り込みが積んだ項目が拒まれたときだけである。** 先に積まれていた項目の +拒まれ方を取り違えると、先の項目を消して同じレビューを二重に積む。 + +総評へ移した指摘は、総評の末尾の見出し「差分の外を指す指摘」の下に、位置と重要度を添えて +並ぶ。1 ラウンドの指摘は多くて 10 件前後で、すべて移しても総評の上限(65536 文字)に収まる。 + +### 自分の PR での判定の格下げ + +GitHub は自分の PR への変更を求めるレビューを拒む。**投稿する側は、自分の PR のとき送る形 +だけを `COMMENT` へ落とす。** 本来の判定は本文の先頭行と記録の `intent` に残る。収束の判定は +`intent` を読むため、格下げがループの続き方を変えない。 + +### 二度書かない照合 + +待ち行列は送る前に、同じものが PR に先にあるかを照合する。**先客がいれば送らず、先客を +送信の応答の代わりに返す。** 呼び出し側は、送った場合と同じ経路で参照を記録する。 + +| 種別 | 照合の要点 | +| --- | --- | +| レビュー | 投稿者と、先頭行の席までの前方一致。判定の語を含めない。ラウンドの開始時刻より後のレビューに限る | +| 返信 | 返信先の指摘と、本文の先頭 80 文字 | +| スレッドの決着 | スレッドがすでに決着しているか | +| PR へのコメント(修正のまとめ) | 投稿者と、本文の先頭 80 文字。先頭行がラウンドとコミットを持つ | + +**照合を確かめられないときは送る側へ倒す。** 確かめられないのは GitHub へ届いていないとき +であり、送っても同じ失敗で積まれ直す。 + +**投稿者は、どの席でも同じ 1 つのアカウントである。** 作業環境の `gh` は 1 アカウントを +持ち、席ごとに認証を切り替える口は配布物に無い。そのため席の区別は先頭行が担う。 + +**すでに決着したスレッドをもう一度決着させても失敗しない(実測)。** 照合が送信を止め +損ねても、二重の決着は誤りにならない。 + +### 修正の取り込みと送信 + +修正の担当はコミットまでを行い、修正の結果ファイルを書く。取り込みは次の順に進む。 + +1. 現在の頭を送り先のブランチへ送る(`git push origin HEAD:<ブランチ名>`)。認証で落ちた + ときは、共通層の退避の値で 1 度だけやり直す +2. 送り先を取り寄せ、報告されたコミットがその履歴に含まれることを確かめる +3. 修正を記録する +4. 返信 → 決着 → まとめの順に積んで流す +5. まとめの参照を記録へ書き戻す + +**1 か 2 で失敗したら、記録も投稿もせずに止まる。** 同じ取り込みをやり直せば、同じ手順を +最初から通る。報告されたコミットが無いときは送らずに進む。 + +| 修正の結果ファイルの配列 | 送られるもの | +| --- | --- | +| 対応した指摘(`resolved_threads`) | 「対応しました(<コミット>)」の返信と、スレッドの決着 | +| 見送った指摘(`deferred`) | 見送りの理由の返信。項目が決着を求めるときだけ決着する | +| 採らない指摘(`rejected`) | 採らない理由の返信。同上 | +| — | 件数・決着数・CI の状態を載せた修正のまとめ | + +**返信を決着より先に送る。** 決着したスレッドは畳まれ、後から届いた返信を読み手が開かない。 + +### 単独の修正と最終スイープ + +修正を単独で行うときと、ループを抜けた後の最終スイープでは、単独の修正の口を 1 行で呼ぶ。 +**修正の取り込みと同じ関数を通る。** + +```bash +python3 "$SCRIPTS/lib/result_posts.py" fix --pr <番号> --result <修正の結果ファイル> \ + [--repo <所有者>/<リポジトリ>] [--head <ブランチ名>] [--worktree <作業ツリー>] [--round ] +``` + +| 引数を省いたとき | 決め方 | +| --- | --- | +| リポジトリと送り先のブランチ | 作業ツリーの中で `gh` に問い合わせる | +| 作業ツリー | いまいるディレクトリ | +| 修正の結果ファイル | 一時ディレクトリ(「状態ファイルの鍵」の末尾)の `fix-pr<番号>-result.json` | + +**送り先のブランチを決められないときは、返信へ進まず終了コード 1 で止まる。** 送っていない +修正へ「対応しました」と返信しないためである。出力は次の形で、本文を含まない。 + +```text +PUSHED=1 COMMIT_ON_HEAD=1 +POSTED summary_url=https://github.com/.../pull/<番号>#issuecomment-... +REPLIED=3 RESOLVED=2 QUEUED=0 +``` + +### 途中で止まったときの立て直し + +送信に成功した項目は、記録より先に待ち行列から消える。**立て直しは、待ち行列に項目が残る +ことに頼らない。** + +| 止まった場所 | 待ち行列の項目 | 立て直し | +| --- | --- | --- | +| 担当が控えか結果ファイルを書く前 | 無い | 結果なし。起動し直しても投稿は重ならない | +| 取り込みが送る前(上限で送れない) | 残る | 判定の終了コード 8 の枝が流し直し、参照を記録へ書き戻す | +| 取り込みが送った後・記録の前 | 残らない | 取り込みをもう一度呼ぶ。照合が先客を見つけ、先客の参照を記録する | + +```mermaid +graph TD + A[取り込みが投稿を送る] --> B{送れたか} + B -->|送れていない| D[待ち行列に残る] + D --> E[判定の 8 の枝が流し直す] + B -->|送れた| C{記録まで済んだか} + C -->|済んだ| J[次の段へ進む] + C -->|止まった| K[取り込みをもう一度呼ぶ] + E --> F{先客がいるか} + K --> F + F -->|いる| G[送らずに
先客の参照を記録する] + F -->|いない| H[送って記録する] +``` + +**送れた後に控えへ送れた先を書けなければ、取り込みは止まる。** 控えは原子的に書くため、 +書きかけは残らない。やり直すと、照合が先客を見つけて同じ参照を記録する。 + +### 起動し直しの経路 + +骨組みのレビューの段は「起動 → 監視 → 取り込み → 根拠の検証 → 反証 → 判定」の繰り返しで +ある。**判定が結果なし(終了コード 7)を返したとき、名前の出た担当だけを入れて繰り返しの +先頭へ戻る。** 起動し直しは同じラウンドで 1 度だけである。 + +| 判定の終了コード | 扱い | +| --- | --- | +| 8(待ち行列に残りがある) | 流してから判定し直す。**7 より先に見る** | +| 7(結果なし)で、まだ起動し直していない | 名前の出た担当で繰り返しの先頭へ戻る | +| それ以外 | 繰り返しを抜ける | + +**経路が 1 本であるため、起動し直した担当の指摘も根拠の検証と反証を通る。** +起動し直せない結末(利用上限)の扱いは[起動 1 回の結末](cross-review-launch-outcome.md)が持つ。 + +## データ・設定 + +### 状態ファイルの鍵 + +ラウンドごとの担当の欄のうち、この仕様が決める鍵は次のとおりである。収束の判定が読む鍵 +(本来の判定・送った形・重要度ごとの件数)は形を変えない。 + +| 鍵 | 何が入るか | +| --- | --- | +| `review_url` | 送信の応答が返した参照。先客が見つかったときは先客の参照 | +| `queued` | 上限で送れず待ち行列に残っているとき真。流した直後に参照を書き戻して偽にする | +| `posted_inline` | インラインとして送れた件数 | +| `posted_body` | 総評へ入れた件数(位置を持たない指摘と、退避した指摘) | +| `comments` | `posted_inline` と同じ値 | +| `posted_as` | 送った形。自分の PR では `COMMENT` | + +修正の記録は、まとめの参照(`summary_comment_url`)を持つ。**投稿済みのレビューを探す鍵は +作らない。** 担当が投稿しないため、探す対象が無い。 + +レビューの照合は、ラウンドの記録が持つ開始時刻(`started_at`)を読む。 + +**待ち行列の置き場所は変えない。** 状態ファイルと同じ一時ディレクトリの `pending/` にあり、 +巻き直しのときは作業ツリーごと捨てられる。単独の修正の口は、一時ディレクトリを環境変数 +`CROSS_REVIEW_TMP_DIR` から、無ければ作業ツリーの `.cross_review/` から決める。 + +### 取り込みの出力と終了コード + +指摘の取り込みは、標準出力へ次の 3 行を出す。本文は出さない。 + +```text +POSTED review_url=https://github.com/.../pull/<番号>#pullrequestreview-... +INLINE=7 BODY=1 QUEUED=0 +FINDINGS=8 +``` + +| 行 | 意味 | +| --- | --- | +| `POSTED` | 送れたときだけ出る。記録した参照 | +| `INLINE` / `BODY` / `QUEUED` | 送れたインライン数・総評へ入れた数・待ち行列に残した数 | +| `FINDINGS` | 取り込んだ指摘の件数。判定が出す新しい指摘の数とは別の値 | + +修正の取り込みは、単独の修正の口と同じ形の行(送信・照合・まとめの参照・返信数・決着数・ +残した数)を出す。 + +**取り込み・判定・報告の終了コードは変えない。** 収束の判定と報告が読む変数 +(`REVIEWER_INTENTS` / `NEW_FINDINGS` / `CARRIED_OVER_THREADS` / `PENDING_POSTS`)も変えない。 +送れない・控えを書けない・修正が送り先に載らないときは、終了コード 1 で止まる。上限で送れなかったときは +止まらず、待ち行列に残して判定の 8 の枝へ渡す。 + +## テスト観点 + +| 観点 | 確かめ方 | +| --- | --- | +| 担当のプロンプトに投稿の手順が無く、2 つのファイルを一時の名前で書いて控え → 結果ファイルの順に改名させること | `plugins/ndf/skills/cross-review/tests/test_launch_reviewer_prompt_context.py` | +| 取り込みがレビューを送って応答を記録し、標準出力に本文を出さないこと | 同 `tests/test_read_result_posts.py` | +| 控えだけの状態と、何も書かなかった担当で、投稿が 0 件であること | 同上 | +| 位置を持つ指摘が 0 件でも、指摘があれば結果なしにならないこと | 同上 | +| 取り込みを 2 度呼んでもレビューが増えないこと | 同上 | +| 自分の PR で、送る形だけが `COMMENT` へ落ち、本来の判定が残ること | 同上と `plugins/ndf/scripts/tests/test_result_posts.py` | +| 位置を解決できない拒まれ方だけで総評へ移し、他の 422 と先の項目の拒まれ方では移さないこと | `plugins/ndf/scripts/tests/test_result_posts.py` / `test_post_queue.py` | +| 回し直した実行で、前の実行の同じラウンド・同じ席のレビューを先客と取り違えないこと | `plugins/ndf/scripts/tests/test_result_posts.py` | +| 控えへ送れた先を書けないとき、控えを壊さず取り込みを止めること | 同上と `plugins/ndf/scripts/tests/test_statefile_emit.py` | +| 返信・決着・まとめが 4 種別の照合を通り、2 度積んでも増えないこと | `plugins/ndf/skills/cross-review/tests/test_queue_idempotency.py` / `plugins/ndf/scripts/tests/test_result_posts.py` | +| 修正を現在の頭で送り、報告されたコミットが送り先に無ければ止まること | 同 `test_result_posts.py` と `plugins/ndf/skills/cross-review/tests/test_merge_fix_posts.py` | +| 単独の修正の口が同じ層を使い、送り先のブランチを決められないとき返信へ進まないこと | `plugins/ndf/scripts/tests/test_result_posts.py` | +| 骨組みの起動し直しが繰り返しの先頭へ戻り、8 の枝を 7 より先に見ること | `plugins/ndf/skills/cross-review/tests/test_skill_layout.py` | +| 文書が投稿の担い手と種別ごとの契約を書いていること | `plugins/ndf/skills/cross-review/tests/test_writes_by_conductor_docs.py` | + +## 関連リンク + +- [issue #730](https://github.com/devbasex/ai-plugins/issues/730) — GitHub と git への書き込みを回す側へ移す +- [issue #583](https://github.com/devbasex/ai-plugins/issues/583) — 投稿済みで記録なしの状態と、同じ論点の重なり +- [PR #801](https://github.com/devbasex/ai-plugins/pull/801) — 実装 +- [起動 1 回の結末](cross-review-launch-outcome.md) — 結果なしの理由と起動し直しの可否 +- [証拠ベースのレビューと効果の測定](cross-review-evidence-based.md) — 指摘の構造化・区分・収束の判定 +- [使える者だけで始める収束ループ](cross-review-participants-and-seats.md) — 席の名前と決め方 +- [`cross-review` の状態ファイルと入出力の契約](../../plugins/ndf/skills/cross-review/docs/04-contracts.md) +- [`cross-review` の修正と巻き直し](../../plugins/ndf/skills/cross-review/docs/02-fix-and-rotation.md) +- [`fix` の手順](../../plugins/ndf/skills/fix/SKILL.md) diff --git a/docs/specifications/test-monitor-env-isolation.md b/docs/specifications/test-monitor-env-isolation.md new file mode 100644 index 000000000..ade97f37a --- /dev/null +++ b/docs/specifications/test-monitor-env-isolation.md @@ -0,0 +1,73 @@ +# テストの前提: 監視の上限を環境変数で延ばしたシェルでは既定値を前提にするテストが落ち、収束ループの初期化が止まる → テストの実行中は監視の環境変数が共通の前提で外れ、どのシェルから起動しても同じ結果になる + +## 目的 + +**テストの実行中だけ、監視の上限を指す環境変数を利用者の環境から切り離す。** 上限を延ばした +シェルから全体のテストを起動しても、延ばしていないシェルと同じ件数が通る。 + +**切り離しはリポジトリの根の共通の前提(`conftest.py`)の 1 か所が持つ。** テストごとに +同じ除去を書かない。 + +## 用語 + +| 業務用語 | 識別子 | 何を指すか | +| --- | --- | --- | +| 監視の環境変数 | 接頭辞 `MONITOR_` を持つ環境変数(`MONITOR_STALL_AGY` / `MONITOR_TIMEOUT` など) | 無進捗の許容と打ち切りの上限を、担当ごと・共通に延ばす指定 | +| 共通の前提 | リポジトリの根の `conftest.py` | 全体のテストに共通する前提を 1 か所で用意するファイル | +| 根の設定ファイル | リポジトリの根の `pytest.ini` | テストの基準のディレクトリ(rootdir)を根へ固定するための空の設定 | + +## 背景 + +**上限は環境変数で延ばせる。** 運用で延ばしたシェルから全体のテストを起動すると、表の既定値を +前提にするテストが延ばした値を読んで落ちた。原因はテストの実行環境にあり、変更の中身には無い。 + +2026-09-21 の実測では、監視の環境変数を 2 つ設定したシェルで 3 件が落ち、設定していない +シェルでは 0 件だった(収集は 4603 件で同じ)。 + +**収束ループの初期化は、着手前のテストが通ることを条件にする。** そのため上限を延ばした +シェルでは、cross-refactoring の初期化がそこで止まった。 + +## 仕様 + +| 項目 | 振る舞い | +| --- | --- | +| 外す対象 | 名前が接頭辞 `MONITOR_` で始まる環境変数すべて | +| 外す時点 | テストの収集より前(`pytest_configure`) | +| 戻す時点 | テストの実行が終わったとき(`pytest_unconfigure`)。外した値をそのまま戻す | +| 子プロセス | 環境変数を受け継ぐため、テストが起動する別プロセスにも切り離しが効く | +| テストが自分で設定する値 | 打ち消さない。`monkeypatch.setenv` も、別プロセスへ渡す上書きも、切り離しの後に効く | +| 読まれる範囲 | どの束のディレクトリを起点にしても読まれる。根の設定ファイルが基準のディレクトリを根へ固定する | + +**名前を並べず、接頭辞だけで一致させる。** 上限の種類が増えるたびに一覧へ足し忘れる形を +作らないためである。 + +**収集より前に外す。** テストの本体を読み込む時点で上限を決める実装があり、セッションの前提 +(fixture)では間に合わない。 + +**根の設定ファイルは設定値を持たない。** pytest は起点から上へ設定ファイルを探し、見つかった +ところを基準にする。根に 1 つも無いと、束のディレクトリを起点にした実行では共通の前提が +読まれない。`testpaths` などを書くと、既存の実行が対象にする範囲が変わる。 + +**変えないもの。** 上限の解決順(起動時の明示の指定(`--timeout` / `--stall-timeout`)→ 担当ごとの +環境変数 → 共通の環境変数 → 表の既定)、上限の表の値、 +監視の本体と起動スクリプトの振る舞いは変えない。変わるのはテストの前提だけである。 + +## テスト観点 + +| 観点 | 確かめ方 | +| --- | --- | +| 監視の環境変数を設定したシェルでも、全体のテストの失敗が 0 件で、設定しないシェルと件数が一致すること | `MONITOR_TIMEOUT_AGY=1800 MONITOR_STALL_AGY=1800 uv run --with pytest pytest scripts/tests plugins/ndf -q` と、設定しない同じコマンドを比べる | +| 実行中のテストに接頭辞 `MONITOR_` の環境変数が 1 つも残らないこと | `scripts/tests/test_root_conftest.py` | +| テストが自分で設定した値は観測できること | 同上 | +| 別プロセスへ受け継がれないこと | 同上 | +| 実行が終わった後に元の値が戻ること | 同上 | +| 束のディレクトリを起点にした実行でも切り離しが効くこと | 同上 | +| テストごとの同じ除去が残っていないこと | `grep -rn "MONITOR_" --include="*.py"` で、接頭辞での除去と 1 変数ずつの除去がテストに無いことを見る | + +## 関連リンク + +- [issue #678](https://github.com/devbasex/ai-plugins/issues/678) — 上限を延ばしたシェルでテストが落ちる +- [PR #797](https://github.com/devbasex/ai-plugins/pull/797) — 実装 +- [起動 1 回の結末](cross-review-launch-outcome.md) — 監視の上限と結末の語彙 +- [共通の前提](../../conftest.py) +- [根の設定ファイル](../../pytest.ini) diff --git a/issues/issue-598-537-limits-plan.md b/issues/issue-598-537-limits-plan.md index 070ceb9f1..18cb68400 100644 --- a/issues/issue-598-537-limits-plan.md +++ b/issues/issue-598-537-limits-plan.md @@ -5,7 +5,7 @@ - 要求と受け入れ条件: [issue-662-598-537-619-584-583-requirements.md](issue-662-598-537-619-584-583-requirements.md)(P2 は AC30〜AC42 と AC70〜AC72) - 設計文書: [issue-662-598-537-619-584-583-design.md](issue-662-598-537-619-584-583-design.md)(P2 は決定 10〜13。決定 10 の cross-refactoring の例外を含む) - 契約の文書: [issue-662-598-537-619-584-583-design-contracts.md](issue-662-598-537-619-584-583-design-contracts.md)(「上限の表(P2)」「`limits.py`(P2)」「`launch-cli.sh` の第 7 引数(P2)」「`bg-wait.sh`(P2)」) -- 境界: [issue-647-592-553-design.md](issue-647-592-553-design.md) の決定 13 と「他の設計との契約」の D-A(P2)の行 +- 境界: #647 #592 #553 の設計(決定 13 と「他の設計との契約」の D-A(P2)の行。確定仕様は [適用の取り込み](../docs/specifications/cross-refactoring-apply-intake.md)) - 前段: P1(#662、PR #677、develop の d11c473) - 課題: #598 / #537(マイルストーン 21) diff --git a/issues/issue-624-478-648-contracts.md b/issues/issue-624-478-648-contracts.md deleted file mode 100644 index 819cb2168..000000000 --- a/issues/issue-624-478-648-contracts.md +++ /dev/null @@ -1,203 +0,0 @@ -# #624 / #478 / #648: 状態ファイル・引数・関数の契約 - -[issue-624-478-648-design.md](issue-624-478-648-design.md) の続きである。決定の理由は設計文書の「決定の記録」にあり、 -この文書は形だけを書く。**P4 は状態ファイルの形も引数も変えない。** P4 が変えるのは区分の条件(設計文書の決定 3)と -印の外し方(決定 5)で、「`state.py` の内部関数」の表の末尾 2 行と「変わらない項目の意味の変化」の先頭 2 行に -当たる。それ以外の契約は P5 のものである。 - -**誰にも誤りを示されていない `major` を数える側へ改めるため、P4 の契約(`_classify_finding` の順 4 の条件と、`classification` の値)は [issue-732-624-706-design.md](issue-732-624-706-design.md) の「データ構造」「入出力の契約」が持つ。** 新しい設計は区分 `unrefuted` と項目 `unrefuted_reason` を足し、順 4 から根拠の条件を外す。印を外す契約(決定 5)はそのまま引き継がれた。以下の P4 の行は 2026-09-15 時点の記録として残す。 - -**P5 の契約は [issue-727-687-478-664-648-contracts.md](issue-727-687-478-664-648-contracts.md) が置き換える**(親 #727 の設計、2026-09-19)。置き換え先は、使える者だけで収束ループを開始し、再開で渡した引数を反映するための状態ファイル・引数・関数の形である。6 項目は 1 つの `participants` に畳まれ、`review_assign` は `review_seats` に、`check_auth` は `probe_auth` に替わる。この文書の P5 の節は記録として残し、書き換えない。 - -## データ構造(状態ファイル) - -### 増える項目 - -状態ファイル `cross-review-pr<番号>-state.json` の最上位に 6 項目が増える。**`version` の類は持たないため上げない。** -項目が無い状態ファイルは、この変更の前に始めた実行として読む(下の「移行」)。 - -| 項目 | 型 | 空を許すか | 意味 | -| --- | --- | --- | --- | -| `available_reviewers` | 文字列の配列 | 許さない(項目が無いことは許す) | 使える者。母集合(`review_pool(host)`)の順に並ぶ。`only` があれば `[only]`。項目が無いのは「この変更の前に始めた実行」で、「使える者が 0 者」ではない(0 者の状態ファイルは作らない) | -| `excluded_reviewers` | 文字列の配列 | 許す(空の配列) | `--exclude` で外した者。母集合の順。空は「外していない」 | -| `unavailable_reviewers` | オブジェクト(名前 → 理由の文字列) | 許す(空のオブジェクト) | 認証を通らなかった者と、`probe_auth` の `detail`。空は「全員が通った」か「確認を飛ばした」 | -| `require_all` | 真偽値 | 許さない | `--require-all` の値。新規の既定は `false` | -| `auth_skipped` | 真偽値 | 許さない | `NDF_SKIP_AUTH_CHECK` で確認を飛ばしたか。`unavailable_reviewers` が空である理由を区別する | -| `resume_changes` | オブジェクトの配列 | 許す(空の配列) | 再開で変えた値の記録。追記だけを行う | - -`resume_changes[]` の要素: - -| 項目 | 型 | 意味 | -| --- | --- | --- | -| `at` | 文字列(ISO 8601) | 再開した時刻(`_now()`) | -| `field` | 文字列 | 変えた項目の名前(`max_rounds` / `rotate_after` / `only` / `verify_commands` / `verify_exit_codes` / `excluded_reviewers` / `available_reviewers` / `unavailable_reviewers` / `require_all` / `auth_skipped`)。値が変わった項目だけを積む | -| `from` | 任意 | 変える前の値。項目が無かったときは `null` | -| `to` | 任意 | 変えた後の値 | - -### 変わらない項目の意味の変化 - -| 項目 | 変わること | -| --- | --- | -| `evidence_rounds` | P4 から、反証が揃わない取り込みで番号が**外れる**ことがある(決定 5)。**付く条件は変わらない** | -| `review_findings[].classification` | P4 から、`critiques` が空で単独の根拠付き `major` 以上が `needs_human_judgment` になる(決定 3) | -| `only` | P5 から、再開の `--only` で変わる。`--only none` で `null` へ戻る | -| `max_rounds` / `rotate_after` / `verify_commands` / `verify_exit_codes` | P5 から、再開で明示的に渡したときだけ変わる | -| `rounds[].reviewers` | 変わらない。**過去のラウンドの担当はこの記録が持ち、再開で書き換えない** | - -### 実体の関係 - -```mermaid -erDiagram - 状態ファイル ||--o{ ラウンド : rounds - 状態ファイル ||--o{ 再開で変えた値 : resume_changes - 状態ファイル ||--o{ 指摘 : review_findings - 指摘 ||--o{ 反証 : critiques - 状態ファイル { - string host - string only - array available_reviewers - array excluded_reviewers - object unavailable_reviewers - array evidence_rounds - } - ラウンド { - int round - array reviewers - } -``` - -### 機能とデータの対応 - -| 機能 | `available_reviewers` / `excluded_reviewers` / `unavailable_reviewers` | `only` / `max_rounds` など | `resume_changes` | `rounds[].reviewers` | `evidence_rounds` | `review_findings[].classification` | -| --- | --- | --- | --- | --- | --- | --- | -| F1 数える | — | — | — | R | R | U | -| F2 印を外す | — | — | — | R | U | — | -| F3〜F5 新規の `init` | C | C | C(空) | — | — | — | -| F5 `start-round` | R | R | — | C | — | — | -| F6 再開の `init` | U | U | U(追記) | R | — | — | -| F7 `report` | R | R | R | R | — | — | - -### 時系列の扱い - -`available_reviewers` などの値は上書きし、過去の値は `resume_changes` に事象として積む(決定 17)。ラウンドごとに -誰が担当したかは `rounds[].reviewers` が持つため、上書きで失われるのは「どの時点でどの一覧だったか」だけで、 -それを `resume_changes` が補う。 - -### 移行 - -**既存の状態ファイルは書き換えない。** 項目が無いときの読み方を決める。 - -| 項目が無いとき | 読み方 | -| --- | --- | -| `available_reviewers` | `host` があれば `review_pool(host)` の輪番(変更前と同じ)。`host` も無ければ `codex` / `agy` | -| `excluded_reviewers` / `unavailable_reviewers` / `resume_changes` | 空として読む | -| `require_all` / `auth_skipped` | `false` として読む | - -再開で担当に関わる引数を渡したときだけ、使える者を作り直して項目を書く(決定 15)。渡さない再開では書き足さない。 - -## 入出力の契約 - -### `state.py init` の引数 - -| 引数 | 型 | 既定(argparse) | 新規の経路 | 再開の経路 | 変更 | -| --- | --- | --- | --- | --- | --- | -| `pr` | 整数 | 必須 | — | — | 変わらない | -| `--max-rounds N` | 整数 | `None` | 無ければ 12 | 渡せば反映 | 既定を `None` へ | -| `--rotate-after K` | 整数 | `None` | 無ければ 8 | 渡せば反映 | 既定を `None` へ | -| `--only RUNTIME` | `claude`/`codex`/`agy`/`kiro`/`none` | `None` | `none` は無しと同じ | 渡せば反映。`none` で `null` | `none` を追加 | -| `--exclude NAMES` | カンマ区切りの名前。繰り返し可 | `None` | 外す | 渡せば置き換え。`none` で空 | **新設** | -| `--require-all` / `--no-require-all` | 真偽値 | `None` | 無ければ `false` | 渡せば反映 | **新設** | -| `--host RUNTIME` | 4 つの名前 | `None` | 無ければ推定 | 反映しない。違えば 1 行 | 再開での知らせを追加 | -| `--verify-command CMD` | 文字列。繰り返し可 | `None` | 無ければ空 | 渡せば置き換え | 再開で反映 | -| `--verify-exit-code N` | 整数。繰り返し可 | `None` | 無ければ空(判定側の既定 1) | 渡せば置き換え | 再開で反映 | -| `--worktree` / `--focus` / `--extra-instructions-file` | — | — | — | — | 変わらない | - -**`--exclude` の値の検査は 2 段に分かれる。** 名前の綴り(4 つの名前か `none`)は argparse の型が終了コード 2 で -弾く。母集合に含まれるか(ホスト自身でないか)は `init` がホストを確定した後に確かめ、終了コード 1 で弾く。 -`none` と他の名前を同時に渡したら終了コード 1 で弾く。 - -**再開で担当に関わる引数の一部だけを渡したとき、渡さなかった側は状態ファイルの値を使う。** 例えば `excluded_reviewers` が -`["agy"]` の状態ファイルへ `--only agy` だけを渡すと、状態ファイルの除外と矛盾するため終了コード 1 で弾く。 - -### `state.py init` の出力と終了コード - -標準出力の `KEY=VALUE`(`_print_init_result`)は変えない。**増えるのは標準エラーの行だけである。** - -| 場面 | 標準エラーに出るもの | 終了コード | 状態ファイル | -| --- | --- | --- | --- | -| 新規で全員が使える | 母集合と使える者の 1 行(現行の「ホスト / レビュワーの母集合」の行に続ける) | 0 | 作る | -| 新規で認証を通らない者がいる | 認証を通らなかった者と理由を 1 者 1 行 | 0 | 作る | -| 新規で使える者が 1 者 | 観点が 1 つになる警告 1 行 | 0 | 作る | -| 確認を飛ばした(`NDF_SKIP_AUTH_CHECK`) | 飛ばしたことを 1 行(`auth.py` の既存の文言) | 0 | 作る(`auth_skipped: true`) | -| 新規で使える者が 0 者 | 使える者がいない理由 | 1 | 作らない | -| `--require-all` で欠けがある | 変更前と同じ「認証されていない CLI があります」の文言 | 1 | 作らない | -| `--exclude` が母集合の外 / `--only` と矛盾 / `none` と名前の混在 | 何が矛盾したか | 1 | 作らない | -| 再開で引数を反映した | 反映した項目ごとに `<項目>: <旧> → <新>` の 1 行 | 0 | 書き換える | -| 再開で `--host` が状態と違う | 反映しないことを 1 行 | 0 | `host` は変えない | -| 再開で作り直した使える者が 0 者 / 欠けあり | 新規と同じ | 1 | 書き換えない | - -### `state.py report` の出力 - -現行の「PR 履歴」の後に、次の節を足す。 - -```text -## 参加した者 -- 使える者: codex / kiro -- --exclude で外した者: agy -- 認証を通らなかった者: なし -- 再開で変えた値: 2026-09-15T12:00:00 excluded_reviewers [] → ["agy"] -``` - -項目が無い状態ファイル(この変更の前に始めた実行)では「使える者: 記録なし」と出す。`auth_skipped` が真のときは、 -3 行目を「認証を通らなかった者: 確認を飛ばした(`NDF_SKIP_AUTH_CHECK`)」と出す。 - -### 共通層の関数(`lib/assignment.py`) - -| 関数 | 入力 | 出力 | 失敗の形 | 変更 | -| --- | --- | --- | --- | --- | -| `review_pool(host)` | ホスト名 | 母集合 3 者 | ホストでない名前で `AssignmentError` | 変わらない | -| `review_candidates(host, excluded)` | ホスト名、外す名前の集合 | 母集合から外した一覧(母集合の順) | 母集合に無い名前を含むと `AssignmentError`(名前を並べる) | **新設** | -| `review_assign(round_no, available)` | ラウンド番号、使える者の一覧(`list` / `tuple`) | 担当(3 者以上は 1 者を外す、2 者以下はそのまま) | `round_no < 1`、空の一覧、文字列を渡したときに `AssignmentError` | **第 2 引数をホスト名から一覧へ変える** | -| `assign(round_no, host)` | — | — | — | 変わらない | - -**`review_assign` の呼び出し側は `state.py` の `_round_reviewers` だけである。** `git grep -n review_assign` で -他に当たるのは `cross-refactoring` のテスト、`docs/05` の説明、`issues/old/` の記録だけである。文字列を弾くのは、変更前の呼び方(`review_assign(1, "claude")`)が -黙って 1 文字ずつの一覧として通るのを防ぐためである。 - -### 共通層の関数(`lib/auth.py`) - -| 関数 | 入力 | 出力 | 失敗の形 | 変更 | -| --- | --- | --- | --- | --- | -| `probe_auth(runtimes, *, info, env=None)` | 確かめる名前の一覧 | `(結果, 飛ばしたか)`。結果は名前 → `{"command", "ok", "detail"}` | 例外を上げない。コマンドが無い・時間切れは `ok: false` と `detail` | **新設** | -| `check_auth(runtimes, *, info, die, env=None)` | 変わらない | 変わらない(飛ばしたときは `{}`) | 1 件でも失敗すれば `die` を呼ぶ | **振る舞いを変えない**(中で `probe_auth` を使ってよい) | - -### `state.py` の内部関数 - -| 関数 | 契約 | 変更 | -| --- | --- | --- | -| `_resolve_reviewers(host, only, excluded, require_all)` | 設計文書の「使える者の解決」の 5 手順を行い、`{"available_reviewers", "excluded_reviewers", "unavailable_reviewers", "auth_skipped"}` を返す。失敗は `die(code=1)` | **新設**(`_auth_targets` を置き換える) | -| `_validate_only(only, host, excluded)` | `only` が母集合に無い、または `excluded` に含まれるとき `die` | 引数を足す | -| `_round_reviewers(st, round_no)` | 設計文書の決定 12 の順で返す | 順を変える | -| `_resume_from_state(pr, repo, worktree, manual_extra_review, args)` | 決定 13〜17 の反映を行う。反映が失敗したら状態ファイルを書き換えずに終了コード 1 | 引数を足す | -| `_apply_resume_args(st, args)` | 渡された引数を `st` へ書き、`resume_changes` に積み、出す行の一覧を返す。書き込みは呼び出し側が 1 回で行う | **新設** | -| `_guard_previous_round(st, prev)` | `_no_result_agents` と `_round_passes` に `prev["reviewers"]`(無ければ `_round_reviewers(st, prev["round"])`)を渡す | 担当を渡す | -| `_classify_finding(finding)` | 順 4 に「`critiques` が空」を足す(P4) | 条件を足す | -| `_handle_incomplete_critiques(pr, st, round_no, missing)` | 先頭でそのラウンドの印を外す(P4) | 印を外す | - -### 手順書の骨組み(`SKILL.md` / `docs/01`) - -```bash -INIT_VARS=$("$SCRIPTS/state.py" init "$STATE_PR" \ - ${MAX_ROUNDS:+--max-rounds "$MAX_ROUNDS"} ${ROTATE_AFTER:+--rotate-after "$ROTATE_AFTER"} \ - ${HOST:+--host "$HOST"} \ - ${ONLY:+--only "$ONLY"} ${EXCLUDE:+--exclude "$EXCLUDE"} \ - ...) || exit $? - -for r in $REVIEWERS; do "$SCRIPTS/launch-reviewer.sh" "$r" "$STATE_PR" "$ROUND"; done -"$SCRIPTS/monitor.py" "$STATE_PR" --agents "$REVIEWERS_CSV" || true -for r in $REVIEWERS; do "$SCRIPTS/state.py" read-result "$STATE_PR" "$r" || true; done -"$SCRIPTS/critique-round.sh" "$STATE_PR" "$ROUND" $REVIEWERS -``` - -**`--max-rounds` と `--rotate-after` も値があるときだけ渡す。** 現行の骨組みは `"$MAX_ROUNDS"` を常に渡すため、 -再開のたびに利用者が指定していない値で上書きする(決定 13 が防ぎたい形そのもの)。 diff --git a/issues/issue-624-478-648-design.md b/issues/issue-624-478-648-design.md deleted file mode 100644 index bf37c37ba..000000000 --- a/issues/issue-624-478-648-design.md +++ /dev/null @@ -1,420 +0,0 @@ -# #624 / #478 / #648: 反証する担当がいない指摘を数え、使える担当だけで回し、再開で引数を反映する - -要求と受け入れ条件は [issue-624-478-648-requirements.md](issue-624-478-648-requirements.md) にある。この文書は -「どう作るか」だけを扱う。 - -**実装は 2 本の Pull Request に分ける**(決定 1)。 - -| Pull Request | 課題 | 中身 | 受け入れ条件 | -| --- | --- | --- | --- | -| P4 | #624、#583 の収束の部分 | 反証を受けていない単独の指摘を数える。反証が揃わない取り込みで印を外す | AC1〜AC9、AC31、AC32 | -| P5 | #478、#648 | 使える者の一覧と `--exclude`。再開で明示的に渡した引数を反映する | AC10〜AC32 | - -全体の実装順は P1 → P2 → P3(いずれも D-A)→ P4 → P5 である。P4 / P5 は P1〜P3 が入った `state.py` の上に載せる。 - -## 機能一覧 - -| # | 機能 | 誰が使うか | Pull Request | -| --- | --- | --- | --- | -| F1 | 反証する担当がいない根拠付きの `major` を、新しい指摘として数える | 1 者で回す利用者、担当が起動し直されたループ | P4 | -| F2 | 反証が揃わなかったラウンドを、全件を数える扱いへ戻す | 収束ループ全般 | P4 | -| F3 | 認証できない担当を外し、使える者でループを始める | CLI の一部が導入・認証されていない利用者 | P5 | -| F4 | `--exclude` で担当を名指しで外す | 打ち切り・利用上限が分かっている担当を避けたい利用者 | P5 | -| F5 | 使える者の数に応じて担当を決める(3 者は輪番、2 者は固定、1 者は警告) | 収束ループ全般 | P5 | -| F6 | 再開で `--max-rounds` などの明示した引数を反映し、反映しない引数を知らせる | 中断したループを進め方を変えて再開する利用者 | P5 | -| F7 | 完了報告に、参加した者・外した者・認証を通らなかった者(理由つき)・再開で変えた値を出す | 収束の結果を読む人 | P5 | - -## 決定の記録 - -決定 1 は分け方、決定 2〜5 は P4 の数え方、決定 6〜12 は P5 の担当の決め方、決定 13〜19 は P5 の再開と骨組みを扱う。 - -### 決定 1: P4(#624 と #583 の収束の部分)と P5(#478 と #648)に分け、P4 を先に出す - -#478 は使える者が 1 者のときに 1 者で回す分岐を入れる。#624 を直さないまま入れると、その分岐が必ず誤った収束を -踏む。P4 は担当の決め方に依存しないため、先に単独で出せる。#478 と #648 は同じ引数(`--exclude` / `--only`)の -再開での扱いを一緒に決める必要があり、1 本にまとめる。 - -3 本に分ける形は採らない。#648 だけを先に出すと、`--exclude` を足したときに再開の規則をもう一度書き換える。 - -### 決定 2: 数えない判断は「反証を受けた単独の指摘」だけに掛け、判定は指摘の記録から導く - -区分 4 の `support` の条件は「別の担当が独立に確かめたか」を問う。**問えるのは、別の担当が反証を返した指摘だけ -である。** 反証を 1 件も受けていない単独の指摘とは、`critiques` が空で `origin_runtimes` が 1 者の指摘である。担当 1 者のラウンドと、 -反証を取り込んだ後に取り込まれた指摘(#583)に現れる。**状態ファイルに項目を足さない。** 取り込み -(`_collect_review_findings`)は、同じ担当の指摘を新しい要素で置き換える。起動し直した担当の指摘は、必ず反証を -持たない状態で入る。`collect-critiques` は、全対象の反証が揃ったときだけ印を付ける。 -そのため印の付いた 2 者のラウンドでは、単独の指摘はすべて反証を持つ(再現 C)。 - -採らない案と理由: - -- **反証が 0 件のラウンドは絞り込まない(issue の候補 1)。** ラウンド単位では、2 者のうち 1 件だけが後から入った - #583 の形を塞げない -- **担当 1 者なら intent に従う(候補 2)。** 1 者の分岐だけを塞ぎ、2 者の起動し直しの経路が残る -- **指摘へ「反証の対象になった」印を書く(候補 3 の字面どおり)。** 書く経路(取り込み・起動し直し・統合)ごとに - 付け忘れが起き、旧い状態ファイルには印が無い。記録から導けば経路に依存しない - -### 決定 3: 反証を受けていない単独の指摘は、区分 4 の `support` の条件だけを外す - -外すのは担当の構造上満たせない条件だけにする。根拠(`has_evidence`)と重大度(`major` 以上)の条件は残す。 -**同じ指摘の扱いが担当の数で変わらない。** 2 者のラウンドで数えない `minor` は、1 者のラウンドでも数えない。 -区分の順 1〜3(実行検証の再現・棄却、`refute`)はそのまま先に当たる。 - -```text -順 4: has_evidence かつ major 以上 かつ(support が 1 件以上 / origin_runtimes が 2 者以上 / critiques が空) -``` - -`critiques` が空で `origin_runtimes` が 2 者以上の指摘は、元の条件(2 者以上)で既に当たる。そのため条件は -「`critiques` が空」だけを足せば足りる。**2 者の通常のラウンドの判定は変わらない**(決定 3・5 を当てた試作で既存の cross-review の -テスト 773 件が期待値を変えずに通った。落ちた 1 件は複製先が git の作業ツリーでないことによる)。 - -採らない案: **反証を受けていない指摘は全件(`minor` 以下も)数える。** 1 者で回すと `minor` だけの -`REQUEST_CHANGES` でもラウンドが続き、2 者のときと収束の基準が食い違う。1 者でも intent が pass -(`APPROVE` / 重大な指摘の無い `COMMENT`)なら `round_passes` で収束する。この案の利点は `minor` を -数えることだけになる。 - -### 決定 4: 区分の名前を増やさない - -決定 3 で数える指摘は `needs_human_judgment` に入れる。修正の担当が読む意味(根拠を持つ `major`、判断は修正の担当) -は同じである。名前を足すと、`COUNTED_CLASSIFICATIONS` を持つ `state.py` と `measure.py` の 2 か所と、 -`docs/04` / `docs/06` の語彙を揃えて変えることになる。反証を受けていないことは `critiques` が空であることで -後から読める。 - -### 決定 5: 反証が揃わない取り込みでは、先に付いていた印を外す - -`collect-critiques` は揃わないとき印を付けないが、**既に付いている印は外さない**。 -再現 B2 では、取り直しの後も `evidence_rounds` が `[1]` のままだった。そのとき「印を付けないため、このラウンドは全件を数えます」の出力と実際の -数え方が食い違う。`_handle_incomplete_critiques` の先頭でそのラウンドの番号を `evidence_rounds` から除く。 -**印は「直近の取り込みで揃ったか」を表す。** 起動し直しの後に反証を取り直す経路(D-A の P3)でも、揃えば印が戻り、 -揃わなければ全件を数える。 - -採らない案: 決定 3 だけで足りるとして印を残す。出力の文言と `docs/06` の契約(揃わなければ全件)が偽のまま残る。 - -### 決定 6: 認証の確認を関門から把握へ変え、使える者を状態ファイルに持つ - -`init` は母集合から外した者を除いた全員を確かめる。通った者は `available_reviewers` へ、通らなかった者は理由とともに -`unavailable_reviewers` へ書く。**再開のたびに確かめ直さない**(決定 15 の場合を除く)。途中で担当が -入れ替わると、前のラウンドの記録と突き合わせられなくなる。 - -共通層 `lib/auth.py` に止めない確認 `probe_auth` を足す。`check_auth` は `cross-refactoring` が使うため、 -振る舞い(1 件でも失敗すれば `die` を呼ぶ)を変えない(決定 10)。 - -採らない案: 起動の直前にラウンドごとに確かめる。ラウンドごとに最大 3 回の確認(1 回あたり上限 120 秒)が増える。 - -### 決定 7: 外す指定は `--exclude`(繰り返し・カンマ区切り)にする - -外したい理由は「この担当が落ちる」であり、名指しするのは外す側である。`--exclude agy` と `--exclude agy,kiro` と -`--exclude agy --exclude kiro` を同じ意味にする。骨組みのシェル変数 1 つ(`${EXCLUDE:+--exclude "$EXCLUDE"}`)で -複数を渡せる。母集合の外(ホスト自身)は `init` が弾く(`--only` と同じ扱い)。 - -採らない案: 使う側を並べる `--reviewers codex,kiro`。ホストが変わると一覧を書き直す必要があり、#478 の提案の -形とも違う。 - -### 決定 8: 使える者の数で分け、`review_assign` は使える者の一覧を受け取る - -```python -def review_assign(round_no: int, available: Sequence[str]) -> list[str]: - # 3 者以上: (round_no - 1) % len を外す / 1〜2 者: そのまま / 0 者: AssignmentError -``` - -**3 者の結果は変わらない。** 試作で 4 つのホストとラウンド 1〜12 の全組を比べた。`review_assign(r, review_pool(host))` は -変更前の `review_assign(r, host)` とすべて一致した。2 者は毎ラウンド同じ 2 者になり、#631 の「1 者も見ていない -担当がいる」状態が起きない。1 者は `init` が警告して回し、P4 の規則で数える。0 者は `init` が失敗し、状態ファイルを -作らない。 - -採らない案: 使える者が 2 者のときも輪番で 1 者を外す。毎ラウンド 1 者だけのレビューになり、観点が減る。 - -### 決定 9: `--require-all` で従来の関門を選べるようにする - -全員が揃わないなら始めたくない運用(リリース前の最終レビューなど)のために残す。付けると、認証を通らない者が -1 者でもいれば従来の文言で失敗する。`--exclude` で外した者は揃っていなくてよい(外すことを明示しているため)。 -再開で `--no-require-all` を渡せるよう `BooleanOptionalAction` にし、既定は `None` とする(決定 13)。 - -### 決定 10: `cross-refactoring` には除外の引数を足さず、`assign()` と `check_auth()` を変えない - -`cross-refactoring` の `assign()` は実装担当を先に決めてから残りを選ぶ。使える者が 2 者のとき、実装担当を外すと -レビュー担当が 1 者になる。#478 のコメントは「著者を含めて回し、著者の `APPROVE` を収束に数えない」形を提案している。 -この形は判定とプロンプトに及び、D-C の範囲(`refactor_lib` の適用の骨組み)と重なる。この変更では共通層の `assign()` と -`check_auth()` の振る舞いを保ち、`cross-refactoring` の除外は別の課題へ切り出す(#664)。 - -採らない案: `cross-refactoring` の `init`(`refactor_lib/commands/setup.py`)に除外だけを足す。提案者と実装の -母集合は減らせても、レビュー担当が 1 者になる形が残る。 - -### 決定 11: `--only` は使える者を 1 者に絞る指定として扱う - -`--only X` のとき使える者は `[X]` で、認証を確かめるのも `X` だけである(現行の `_auth_targets` と同じ)。 -`X` が `--exclude` に含まれるときは矛盾として `init` が弾く。状態ファイルの `only` は残す(`_agent_intent` の -`SKIP` と旧い状態ファイルが読むため)。 - -### 決定 12: 担当の決め方をラウンドの記録から先に見る順へ変え、前ラウンドの検査も記録の担当を読む - -```text -ラウンドの reviewers → only → available_reviewers の輪番 → host の輪番(review_pool)→ codex / agy -``` - -現行は `only` をラウンドの記録より先に見る。再開で `only` を変えられるようにすると(決定 13)、過去のラウンドの -担当まで変わる。`_finding_keys` は前のラウンドの指摘を別の担当の payload から読むことになる。`start-round` はラウンドを開く -ときに `_round_reviewers` の結果を記録する。新しい状態ファイルでは、順を変えても結果は同じである。 - -`_guard_previous_round` も同じ理由で `prev["reviewers"]` を渡す。現行は担当を渡さず、旧来の `codex` / `agy` で数える。 -担当が `agy` + `kiro` のラウンドでは `codex` を結果なしと読み、修正の記録が無いまま次のラウンドへ通す(再現 I)。 - -### 決定 13: 引数の既定値を `None` にし、明示的に渡した引数だけを再開で反映する - -`build_parser` の 8 つの引数の既定を `None` にする。対象は `--max-rounds` / `--rotate-after` / `--only` / `--host` / -`--verify-command` / `--verify-exit-code` / `--exclude` / `--require-all` である。新規の経路は `None` を定数の既定(`DEFAULT_MAX_ROUNDS = 12` / -`DEFAULT_ROTATE_AFTER = 8`)へ置き換える。再開の経路は `None` でない引数だけを状態ファイルへ書き、変えた項目ごとに -`<項目>: <旧> → <新>` を 1 行出す。`--verify-command` と `--verify-exit-code` は置き換える。 - -採らない案: 既定値と同じ値なら渡していないとみなす。`--max-rounds 12` で 20 から 12 へ戻したい指定を区別できない。 -再現 G では `init 1` と `init 1 --max-rounds 12` がどちらも 12 になった。 - -### 決定 14: `--host` は再開で反映せず、違う値のときだけ知らせる - -ホストが変わると母集合が変わり、過去のラウンドの輪番を再現できない。状態ファイルと違う値が渡されたら -「`--host` は再開では反映しない(状態: claude、指定: codex)」を 1 行出す。同じ値なら出さない。 -`--worktree` は状態ファイルを探す場所の指定であり、反映の対象ではない。 - -### 決定 15: 担当に関わる引数を渡した再開でだけ、認証を確かめ直して使える者を作り直す - -`--only` / `--exclude` / `--require-all` のいずれかを渡した再開では、決定 6 と同じ手順で使える者を作り直す。 -対象は母集合から外した者を除いた全員(`--only` があればその 1 者)である。**いずれも渡さない再開では確かめ直さない。** -作り直した結果が 0 者、または `--require-all` で欠けがあるときは、状態ファイルを書き換えずに終了コード 1 で終わる。 - -採らない案: 新しく加わる者だけを確かめる。前回に認証を通らなかった者がログインした後も戻れない。 - -反映は再開の後に開くラウンドから効く。骨組みは `init` の直後に必ず `start-round` を呼び、`start-round` は常に -新しい番号のラウンドを開く。そのため、開いたまま中断したラウンドの担当は書き換えない(要求の前提 3)。 - -### 決定 16: 再開で指定を外す値は `none` にする - -`--only none` は `only` を `null` へ、`--exclude none` は除外を空へ戻す。空文字列は骨組みの -`${ONLY:+--only "$ONLY"}` が渡さないため、外す手段にならない。新規の経路で `none` を渡すと、渡さないのと同じになる。 - -### 決定 17: 再開で変えた値は `resume_changes` に積む - -`available_reviewers` や `max_rounds` は再開で上書きされる。上書きだけでは、どの時点で何を変えたかが失われ、 -完了報告で「途中から agy を外した」ことが読めない。変えた項目を `{"at", "field", "from", "to"}` の形で -追記し、書き換えない(事象の記録)。ラウンドごとの担当は `rounds[].reviewers` が既に持つ。 - -### 決定 18: 骨組みは `$ONLY` で絞らず、`start-round` が返す担当を使う - -`start-round` の `REVIEWERS` は `only` を反映済みである。シェル変数の `$ONLY` でもう一度絞ると、状態ファイルと -シェル変数がずれたときに起動も監視も誰にも当たらない。そのうえ起動し直しの分岐だけが担当を起動する(#648 の 3 段の経過)。 -`SKILL.md` と `docs/01` の 2 か所で、担当を `$REVIEWERS` / `$REVIEWERS_CSV` に揃える。 -対象は Step 2 の起動・監視・取り込みと、Step 2.5 の反証である。`$ONLY` は `init` へ渡す 1 行にだけ残す。 - -### 決定 19: 起動した後に分かる使えなさで、担当を自動的に外す仕組みは作らない - -利用上限(kiro の `Monthly request limit reached`)やモデルの 404 は、起動した後に分かる。 -認証の確認は通る(#478 のコメント)。分類は D-A の #619 が `usage_limit` などとして作る。自動で外すと、一時的な打ち切りでも以後の -ラウンドから恒久的に外れる。この変更は利用者が `--exclude` で外し、再開で反映できる入口までを作る。 - -## 実測 - -**再現は、状態ファイルを最小の形で組んで `state.py` の関数を直接呼んで確かめた**(develop `b4a9f69`)。 - -| 場面 | 状態 | 変更前 | 決定 3・5 の試作 | -| --- | --- | --- | --- | -| A(#624) | 担当 `codex` 1 者、印あり、根拠付き `major` 1 件、反証なし | `(0, True)`、収束 | `(1, True)`、収束しない | -| B(#583) | 担当 `agy` + `kiro`、印あり、`agy` の根拠付き `major` が反証なし | `(0, True)`、収束 | `(1, True)`、収束しない | -| B2 | B の後に `collect-critiques`、`kiro` の反証が `agy` の指摘を覆わない | 終了コード 7、印 `[1]` のまま | 終了コード 7、印 `[]`、全件を数える | -| C | 2 者、相手が `insufficient_evidence` | `(0, True)`、収束 | 変わらない | -| D | `origin_runtimes` 2 者の `minor`、反証なし | `(0, True)`、収束 | 変わらない | -| A の `minor` / 根拠なし | A の指摘を `minor` / `has_evidence: false` にする | `(0, True)` | 変わらない(決定 3) | - -P5 の前提: - -| 場面 | 結果 | -| --- | --- | -| F(担当の優先) | `only: "codex"` の状態で、記録が `agy` + `kiro` のラウンド 1 を `_round_reviewers` が `["codex"]` と返す | -| G(#648) | `init 1` と `init 1 --max-rounds 12` はどちらも `max_rounds == 12`。再開の `init` に `--only codex --max-rounds 20 --verify-command pytest --host codex` を渡すと、状態ファイルは `only: null` / `max_rounds: 12` / `host: claude` / `verify_commands: []` のまま、出力にも出ない | -| H(#478) | `check_auth` は 3 者のうち 1 者の失敗で `die` を呼ぶ。`review_assign(r, "claude")` はラウンド 1〜3 で 3 者から 1 者ずつ外す | -| I(前ラウンドの検査) | 担当 `agy` + `kiro`・両者 `REQUEST_CHANGES`・`verdict` なし・修正の記録なしのラウンドを、`_no_result_agents(prev, None)` が `["codex"]` と読み、検査を通す | -| 引数の形 | `action="append"` とカンマ区切りの型で、`--exclude agy --exclude kiro` と `--exclude agy,kiro` が同じ集合になる。知らない名前は argparse が終了コード 2 で弾く | - -## 構成要素 - -| 要素 | 責務 | Pull Request | -| --- | --- | --- | -| 区分の判定(`_classify_finding`) | 順 4 の条件に「反証を受けていない」を足す(決定 3) | P4 | -| 反証の不足の扱い(`_handle_incomplete_critiques`) | そのラウンドの印を外してから、取り直す担当を返す(決定 5) | P4 | -| 担当の決定(`lib/assignment.py`) | 母集合から外した者を除く一覧を返す。使える者の一覧から担当を選ぶ(決定 7・8) | P5 | -| 認証の把握(`lib/auth.py`) | 止めずに確かめ、担当ごとの結果を返す(決定 6) | P5 | -| 使える者の解決(`state.py` の新しい関数) | 母集合・`--only`・`--exclude`・認証・`--require-all` から、使える者と外した者を決める。新規と再開の両方が呼ぶ | P5 | -| 再開の反映(`_resume_from_state`) | 明示的に渡した引数を状態ファイルへ書き、`resume_changes` に積み、反映しない引数を知らせる(決定 13〜17) | P5 | -| 担当の読み出し(`_round_reviewers` / `_guard_previous_round`) | ラウンドの記録を先に見る(決定 12) | P5 | -| 完了報告(`cmd_report`) | 使える者・外した者・認証を通らなかった者・再開で変えた値を出す | P5 | -| 骨組みと文書(`SKILL.md` / `docs/01` / `docs/04` / `docs/05` / `docs/06`) | `$ONLY` で絞らない。引数・状態ファイル・区分の規則を書く | P4 / P5 | - -P4 の要素(判定): - -```mermaid -graph TD - CC[反証の取り込み] --> IC[反証の不足の扱い] - NF[新しい指摘の数] --> CL[区分の判定] -``` - -P5 の要素(初期化とラウンド): - -```mermaid -graph TD - IN[init の新規の経路] --> RV[使える者の解決] - RS[再開の反映] --> RV - RV --> AU[認証の把握] - RV --> AS[担当の決定] - SR[start-round] --> GD[前ラウンドの検査] - SR --> RR[担当の読み出し] - GD --> RR - RR --> AS -``` - -図の辺は呼び出しを表す。`init` の新規の経路・`start-round`・反証の取り込み・新しい指摘の数は、変える要素の呼び出し元として -置いた既存の要素である。完了報告は状態ファイルを読むだけで他の要素を呼ばないため、骨組みと文書とともに図に含めない。 - -**文脈と配置は変わらない。** 動くのは、ホストの CLI から起動される `state.py` の 1 プロセスである。 -外部との出入りは `gh`(GitHub)と、各 CLI の認証の確認コマンドだけである。認証の確認の呼び出し先(`AUTH_PROBES`)は変えない。 - -## 置き場所 - -```text -plugins/ndf/ -├── scripts/lib/ -│ ├── assignment.py # P5: review_candidates を新設、review_assign の引数を変更 -│ └── auth.py # P5: probe_auth を新設(check_auth は変えない) -└── skills/ - ├── cross-review/ - │ ├── SKILL.md # P5 - │ ├── docs/01-state-and-review.md # P5 - │ ├── docs/04-contracts.md # P5 - │ ├── docs/05-pool-and-convergence.md # P4 / P5 - │ ├── docs/06-evidence.md # P4 - │ ├── scripts/state.py # P4 / P5 - │ └── tests/ # P4: test_classify_findings.py / test_critiques.py に追記 - │ # P5: test_state_review_pool.py に追記、test_state_resume_args.py を新設 - └── cross-refactoring/tests/test_assignment.py # P5: review_assign の呼び方を変え、AC15・AC16 のテストを足す -``` - -`dev.kiro` / `dev.agy` は `skills/` を symlink で参照するため、書き写す配布物は無い。 -`bash scripts/build-runtime-plugins.sh --check` で食い違いが無いことだけを確かめる。 - -状態ファイルの形・引数・関数の契約は [issue-624-478-648-contracts.md](issue-624-478-648-contracts.md) にある。 - -## 処理の流れ - -### 新しい指摘の数え方(P4) - -```mermaid -graph TD - J[judge] --> K{payload を読めたか} - K -->|いいえ| U["(0, False) 全員 pass に従う"] - K -->|はい| M{ラウンドに印があるか} - M -->|いいえ| ALL[payload の全件を数える] - M -->|はい| C[指摘ごとに区分を決める] - C --> R{実行検証で再現・棄却 / refute} - R -->|当たる| V[順 1〜3 の区分] - R -->|当たらない| E{根拠あり かつ major 以上} - E -->|いいえ| IE[insufficient_evidence 数えない] - E -->|はい| S{support / 提案者 2 者 / 反証なし} - S -->|いずれか| NH[needs_human_judgment 数える] - S -->|どれも無い| IE -``` - -反証の取り込みで揃わないとき、`_handle_incomplete_critiques` がそのラウンドの印を外す。次に `judge` が数えるとき -「印があるか」が「いいえ」へ進む。取り直して揃えば `_mark_evidence_round` が印を戻す。 - -### 初期化と再開(P5) - -新規の経路: - -```mermaid -graph TD - N[PR の情報・作業ツリー・既存コメント] --> H[ホストを確定] - H --> RV[使える者の解決] - RV -->|0 者 / --require-all で欠け| F[終了コード 1 状態を作らない] - RV -->|1 者| W[観点が 1 つと警告] - RV -->|2〜3 者| S[状態ファイルを書く] - W --> S -``` - -再開の経路(状態ファイルがあり `final` が `null`): - -```mermaid -graph TD - A[明示的に渡した引数を集める] --> HO{--host が状態と違う} - HO -->|はい| HM[反映しないことを 1 行出す] - HO -->|いいえ| RA{--only / --exclude / --require-all を渡した} - HM --> RA - RA -->|はい| RV[使える者の解決] - RA -->|いいえ| W[変えた項目を書き resume_changes に積む] - RV -->|1 者以上| W - RV -->|0 者 / 欠けあり| F[終了コード 1 状態は書かない] -``` - -使える者の解決は次の順で決める。 - -1. `--only` があれば母集合に含まれるかを確かめ、`--exclude` に含まれていれば弾く。対象は `[only]` -2. `--only` が無ければ、`--exclude` の各名前が母集合に含まれるかを確かめ、対象は母集合から除いた者 -3. 対象の認証を `probe_auth` で確かめる。`NDF_SKIP_AUTH_CHECK` が立っていれば全員を通ったものとする -4. `--require-all` が真で通らない者がいれば、従来の文言で終了コード 1 -5. 通った者が 0 者なら終了コード 1。1 者なら警告を 1 行出す - -## 非機能の実現方式 - -| 大項目 | 要求の条件 | 実現方式 | 確かめ方 | -| --- | --- | --- | --- | -| 可用性 | 母集合の 1 者が使えないことで、収束ループを開始できない状態にならない | 認証の失敗を `unavailable_reviewers` へ記録し、使える者で続ける(決定 6) | AC10 のテスト | -| 性能・拡張性 | 認証の確認の回数は、新規の `init` で変更前を上回らない。再開で増えるのは担当に関わる引数を渡したときだけで、母集合の最大 3 者である | `--exclude` で外した者は確かめない。再開は決定 15 の条件でだけ確かめる | AC13・AC26 のテストで `probe_auth` の呼び出しを数える | -| 運用・保守性 | 担当が欠けたまま収束したことが `report` の出力だけで分かる。再開で反映しなかった引数が出力に出る | `report` が 3 つの一覧と `resume_changes` を出す。`--host` の不一致を 1 行出す | AC21・AC27 のテスト | - -## 申し送り(並行する設計との境界) - -| 相手 | 決めた契約 | -| --- | --- | -| D-A(P1〜P3) | **P4 の規則は、起動し直しの経路が証拠集約を通るかどうかに依存しない。** 通らなければ決定 3 が数え、通って反証が揃わなければ決定 5 が全件へ戻す。D-A が起動し直しの後に `critique-round.sh` を呼ぶときは、担当を `$REVIEWERS`(そのラウンドの担当)で渡し、`$ONLY` を使わない(決定 18) | -| D-A(P1〜P3) | `SKILL.md` の骨組みで D-B が触るのは Step 0 の `init` の引数、Step 2 の起動・監視・取り込みの 3 つのループ、Step 2.5 の `critique-round.sh` の引数である。Step 3 の `JUDGE_RC -eq 7` の分岐は D-A が持つ。P5 は P3 の後に載せるため、衝突は P5 の側で解く | -| D-A(P1〜P3) | `_no_result_agents` と `_handle_no_result_round` の中身(`NO_RESULT` の理由)は D-A が持つ。P5 は `_guard_previous_round` から呼ぶときに担当の一覧を渡すだけにする | -| D-A(#619) | 利用上限などの分類を、担当を外す判断へつなぐのは後続に回す(決定 19)。つなぐ場合の入口は再開の `--exclude` である | -| D-C | `refactor_lib` に触らない。共通層の `assign()` と `check_auth()` の振る舞いを変えない(決定 10)。`review_assign` のテストは共通層のテストの置き場所 `cross-refactoring/tests/test_assignment.py` にある。P5 はそこで既存の呼び出しを直し、AC15・AC16 のテストを足す。`refactor_lib` は `review_assign` を呼ばない | - -**D-A の決定に依存する点:** - -- P3 が起動し直しの後に `verify-findings` と `critique-round.sh` を通す形を採るか。どちらでも P4 は成り立つ。 - 変わるのは、#583 の起動し直しの指摘を `needs_human_judgment` として数えるか、全件として数えるかである -- P1 が `monitor.py` の結果をファイルに残す形と、`SKILL.md` の Step 2 の監視の行の書き方。P5 の `$ONLY` の削除と同じ行を触る -- P3 が `state.py` の `_resume_from_state` か `cmd_init` に手を入れるか。P5 は `_resume_from_state` の引数を増やす - -## テスト設計 - -置き場所は、`cross-refactoring/` で始まるもの以外は `plugins/ndf/skills/cross-review/tests/` の下である。 -`cross-refactoring/` で始まるものは `plugins/ndf/skills/` の下にある。 - -| 受け入れ条件 | 何で確かめるか | 置き場所 | -| --- | --- | --- | -| AC1〜AC4 | 状態ファイルを組み、`_new_finding_count` と `cmd_judge` の終了コードを見る(実測の A / B と、A の `minor` / 根拠なし) | `test_classify_findings.py` | -| AC5 | 印の付いた状態で `cmd_collect_critiques` を 2 回呼び、`evidence_rounds` と数え方を見る(実測の B2) | `test_critiques.py` | -| AC6・AC7 | `_classify_finding` に反証の値ごと・`origin_runtimes` 2 者の指摘を渡す | `test_classify_findings.py` | -| AC8 | 既存のテストを期待値を変えずに通す | 既存のまま | -| AC9 | `grep` の行を検査するテスト | `test_skill_layout.py` | -| AC10〜AC14 | `probe_auth` を差し替えて `_init_new_state` を呼ぶ(GitHub を呼ぶ関数は `conftest.py` の既存の差し替え)。状態ファイルの有無と終了コードを見る | `test_state_review_pool.py` | -| AC15・AC16 | `review_assign` を 4 ホスト × 12 ラウンドと 2 者の一覧で呼ぶ。AC15 は変更前の式を期待値として持つ | `cross-refactoring/tests/test_assignment.py` | -| AC17・AC18 | 使える者 1 者・0 者の `init` | `test_state_review_pool.py` | -| AC19 | `available_reviewers` を持たない状態 / `host` も持たない状態で `_round_reviewers` | `test_state_review_pool.py` | -| AC20 | 既存の `assign` の期待値と、`check_auth` の失敗で `die` が呼ばれるテスト | `cross-refactoring/tests/test_assignment.py` / `cross-refactoring/tests/test_init.py` | -| AC21 | 3 つの一覧と `resume_changes` を持つ状態ファイルで `cmd_report` の出力の 4 行を見る | `test_state_review_pool.py` | -| AC22〜AC27 | 状態ファイルを置いた作業ツリーを渡して `cmd_init` を呼び、状態ファイルと標準エラーを見る(実測の G) | `test_state_resume_args.py`(新設) | -| AC28 | 2 ファイルの `ONLY` を含む行を数えるテスト | `test_skill_layout.py` | -| AC29 | `verdict` の無い前ラウンドで `cmd_start_round` の終了コード 5 を見る(実測の I) | `test_state_round_guard.py` | -| AC30 | 文書の語を `grep` するテスト | `test_skill_layout.py` | -| AC31・AC32 | コマンドの終了コード | 継続的統合と手元 | - -## 未確認のまま残ること - -| 項目 | 内容 | いつ決まるか | -| --- | --- | --- | -| 1 者で回したときの収束までのラウンド数 | 決定 3 で `minor` と根拠の無い `major` を数えないため、1 者のループが早く収束しすぎないかは実測していない | P4 の後の運用で `measure.py` の出力を見る | -| 2 者固定(`codex` + `kiro`)の観点の偏り | `agy` を外して 2 者を固定したとき、3 者の輪番より指摘を見落とすかは測っていない | P5 の後の運用 | -| `cross-refactoring` の除外と、著者を含めて回す規則 | 決定 10 で切り出す。レビュー担当が 1 者になる形をどう扱うかは未決 | #664 | -| 起動した後に分かる使えなさで外す仕組み | 決定 19 で作らない。自動で外すか、利用者へ再開を促すかは未決 | D-A の #619 の後 | -| 再開で `--verify-command` を空へ戻す手段 | 持たない。要求が出たら `none` と同じ形で足せる | 要求が出たとき | -| 出力の文言 | 反映した行・反映しない行・警告の文言は、項目名と値を含むことだけを決めた | **実装で決める** | -| テストの置き場所 | テスト設計の表の置き場所は既存ファイルに合わせた目安である | **実装で決める** | diff --git a/issues/issue-624-478-648-requirements.md b/issues/issue-624-478-648-requirements.md deleted file mode 100644 index 02caf60f1..000000000 --- a/issues/issue-624-478-648-requirements.md +++ /dev/null @@ -1,289 +0,0 @@ -# #624 / #478 / #648: 反証する担当がいない指摘を数え、使える担当だけで回し、再開で引数を反映する - -設計は [issue-624-478-648-design.md](issue-624-478-648-design.md) にある。この文書は「何を満たすか」だけを扱う。 - -**3 つの課題は 2 本の Pull Request で直す**(設計文書の決定 1)。P4 は #624 と、#583 のうち収束の誤りの部分を -直す。P5 は #478 と #648 を直す。マージは P4 → P5 の順である。受け入れ条件も Pull Request ごとに分ける。 - -**P5(#478 / #648、AC10〜AC30)は [issue-727-687-478-664-648-requirements.md](issue-727-687-478-664-648-requirements.md) が置き換える**(親 #727 の設計、2026-09-19)。置き換え先は、参加する CLI が 1 者でも使えないと収束ループを開始できない形と、再開で渡した引数が黙って無視される形を、両 Skill が共有する共通層で直す要求である。対応は置き換え先の「既存の受け入れ条件との対応」にある。この文書の P5 の節は記録として残し、書き換えない。 - -## 目的 - -- 反証する担当がいない指摘が、数える区分から黙って落ちなくなる。1 者で回しても、起動し直した担当の指摘でも、 - 修正を要する指摘が残ったまま収束しない -- 認証できない担当や、打ち切りが分かっている担当を外し、使える担当だけでレビューを回せる -- 中断した収束ループを、引数で進め方を変えて再開できる。反映しなかった引数は出力で分かる - -## 対象範囲 - -含む: - -- `state.py` の収束の数え方(区分の判定、証拠集約の印の外し方)(P4) -- `lib/assignment.py` の `review_assign` と、除外・使える者の一覧の算出(P5) -- `lib/auth.py` に止めない認証の確認を足す(P5) -- `state.py` の `init` の新規と再開の経路、`start-round` の担当の決め方、前ラウンドの検査、`report`(P5) -- `cross-review` の `SKILL.md` と `docs/`(01 / 04 / 05 / 06)、テスト(P4 / P5) - -含まない: - -| 扱わないもの | 理由 | -| --- | --- | -| `cross-refactoring` の除外の引数 | 実装担当を外す規則と組むと、使える者が 2 者のときにレビュー担当が 1 者になる。著者を含めて回す規則(#478 のコメント)は判定とプロンプトに及び、D-C の範囲と重なる(設計文書の決定 10)。#664 へ切り出した | -| 起動した後に分かる使えなさ(利用上限・モデルの 404)で担当を自動で外すこと | 分類は D-A(#619)が作る。この変更は利用者が `--exclude` で外す入口だけを作る(決定 19) | -| 監視の上限・結果のファイル化・`NO_RESULT` の理由・`JUDGE_RC -eq 7` の分岐 | D-A(#662 #598 #537 #619 #584 #583)の範囲 | -| #583 の投稿の重なり | D-A の範囲。この変更が扱うのは #583 のうち収束の誤りの部分だけである | -| 担当 2 者で、相手が反証で `support` を返さない単独の指摘を数えないこと | #156 の設計どおりであり、#624 の対象外と issue に書かれている | -| 再開で `--verify-command` を空へ戻す手段 | 置き換えはできる。空へ戻す要求は出ていない | -| クラス図 | 型を追加・変更しない(関数と辞書で組んだ状態ファイルを扱う)。状態ファイルの形は契約文書のデータ構造の節が持つ | -| システムの文脈・配置の図 | 動くのは `state.py` の 1 プロセスで、外部との出入り(`gh` と認証の確認コマンド)は変わらない | -| `CHANGELOG.md` と版数 | 配布の工程が書く | - -## 受け入れ条件(P4: #624 と #583 の収束の部分) - -**誰にも誤りを示されていない `major` を数える側へ改めるため、この節の受け入れ条件(P4: AC1〜AC9)は [issue-732-624-706-requirements.md](issue-732-624-706-requirements.md) が持つ。** 根拠を欠く指摘(AC3)と相手が支持しなかった指摘(AC6)を数えない、としていた条件を数える側へ改めた(親 #732)。#583 の投稿の重なりは #730 の設計が持つ。以下は 2026-09-15 時点の記録として残す。 - -反証する担当がいない指摘を数える: - -- [ ] AC1: 担当が 1 者(`only: "codex"`)で、証拠集約の印が付いたラウンドがある。そのラウンドに根拠を持つ `major` の - 指摘が 1 件あり、反証は 0 件で、前のラウンドは無い。このとき `_new_finding_count` が `(1, True)` を返し、`judge` が終了コード 2 で - 終わる(変更前は `(0, True)` と終了コード 0。issue の再現) -- [ ] AC2: AC1 の指摘が `minor` のとき、`_new_finding_count` は `(0, True)` を返す。担当 2 者のときと同じく、 - `minor` 以下は数えない -- [ ] AC3: AC1 の指摘が根拠を持たないとき、`_new_finding_count` は `(0, True)` を返す。根拠を持たないとは、 - `evidence` か `falsification` が空であることをいう -- [ ] AC4: 担当が `agy` + `kiro` で印が付いたラウンドに、`agy` の根拠を持つ `major` が反証 0 件のまま入っている。 - 起動し直した担当の指摘が、反証を取り込んだ後に取り込まれた形である。このとき `_new_finding_count` が `(1, True)` を - 返す(#583 の収束の部分) - -反証の取り直しで揃わないときは印を外す: - -- [ ] AC5: 印が付いたラウンドで `collect-critiques` を実行し、反証が揃わない(終了コード 7)。このとき - `evidence_rounds` からそのラウンドの番号が消え、`_new_finding_count` は payload の全件を数える。取り直した後も - 揃わないとき(2 度目)も印は付かない - -退行しない: - -- [ ] AC6: 担当 2 者で、相手が単独の根拠を持つ `major` へ `insufficient_evidence` か `out_of_scope` を返した指摘は - 数えない。`support` なら数え、`refute` なら `rejected` になる -- [ ] AC7: `origin_runtimes` が 2 者の指摘の区分は変わらない(根拠を持つ `major` は数え、`minor` は数えない) -- [ ] AC8: 印を持たないラウンドの数え方(payload の全件)と、実行検証の区分(`reproduced` / `not_reproduced`)は - 変わらない。`tests/test_classify_findings.py` の既存のテストが、期待値を変えずに通る - -文書: - -- [ ] AC9: `docs/06-evidence.md` の区分の表の順 4 が、「反証を受けた単独の指摘だけが `support` を求められる」条件を - 書く。同じ文書の「走らせる順序」の節が、取り直しで揃わないときに印を外すことを書く。`docs/05-pool-and-convergence.md` の - 終了基準が、担当 1 者と起動し直した担当の指摘の数え方を書く。次の 2 つがそれぞれ 1 行以上を出す - - ```bash - grep -n "反証を受けた" plugins/ndf/skills/cross-review/docs/06-evidence.md - grep -n "印を外す" plugins/ndf/skills/cross-review/docs/06-evidence.md - ``` - -## 受け入れ条件(P5: #478 使える担当だけで回す) - -認証の確認を把握にする: - -- [ ] AC10: ホスト `claude` で `kiro` の認証の確認が失敗する。`init` は終了コード 0 で状態ファイルを作る。 - `available_reviewers` は `["codex", "agy"]` で、`unavailable_reviewers` は `kiro` と失敗の理由を持つ。 - 標準エラーに `kiro` を外したことが 1 行出る -- [ ] AC11: `--require-all` を付けると、AC10 と同じ状態で `init` が終了コード 1 で終わり、状態ファイルを作らない -- [ ] AC12: `NDF_SKIP_AUTH_CHECK=1` のとき、`available_reviewers` は母集合から `--exclude` で外した者を除いた全員に - なる。確認を飛ばしたことが出力に残る - -除外の引数: - -- [ ] AC13: ホスト `claude` で `--exclude agy` を渡す。`agy` の認証を確かめず、`excluded_reviewers` が `["agy"]`、 - `available_reviewers` が `["codex", "kiro"]` になる。`--exclude agy --exclude kiro` と `--exclude agy,kiro` は同じ - 状態ファイルを作る -- [ ] AC14: 母集合の外(ホスト自身)を `--exclude` に渡す。または `--only codex --exclude codex` を渡す。 - どちらも `init` が終了コード 1 で終わり、状態ファイルを作らない - -使える者の数で分ける: - -- [ ] AC15: 使える者が 3 者のとき、`start-round` が返す担当は変更前と同じ輪番になる。4 つのホストとラウンド 1〜12 - の全組で、`review_assign(r, review_pool(host))` が変更前の `review_assign(r, host)` と一致する -- [ ] AC16: 使える者が 2 者(`codex` / `kiro`)のとき、ラウンド 1〜4 の `start-round` がすべて `codex kiro` を返す -- [ ] AC17: 使える者が 1 者のとき、`init` は終了コード 0 で終わり、観点が 1 つになることを 1 行出す。 - `start-round` はその 1 者を返す -- [ ] AC18: 使える者が 0 者のとき、`init` が終了コード 1 で終わり、状態ファイルを作らない - -退行しない: - -- [ ] AC19: `available_reviewers` を持たない状態ファイルは、この変更の前に始めた実行である。このとき `start-round` が - 返す担当は変更前と同じになる。`host` も持たない状態ファイルは `codex` / `agy` のままである -- [ ] AC20: `cross-refactoring` の担当と認証の関門が変わらない。`cross-refactoring/tests/test_assignment.py` の `assign` の期待値 - (`EXPECTED_FOR_CLAUDE`)が変えずに通る。`check_auth` は失敗が 1 件でもあれば中断させる - -報告: - -- [ ] AC21: `state.py report` が、使える者・`--exclude` で外した者・認証を通らなかった者・再開で変えた値を、 - 1 行ずつ出す。認証を通らなかった者には理由を添える。4 つとも空でない状態ファイル(`resume_changes` が - 1 件以上)で 4 行とも出る - -## 受け入れ条件(P5: #648 再開で引数を反映する) - -明示的に渡した引数だけを反映する: - -- [ ] AC22: `max_rounds: 12` の状態ファイルで、再開の `init` に `--max-rounds 20` を渡す。`max_rounds` が 20 になり、 - 反映したことが `12 → 20` の形で 1 行出る。`resume_changes` には `field: "max_rounds"` / `from: 12` / `to: 20` の - 要素が 1 件積まれる。`--rotate-after` / `--verify-command` / `--verify-exit-code` も同じく反映される。 - `--verify-command` と `--verify-exit-code` は置き換え、継ぎ足さない -- [ ] AC23: 再開の `init` に `--max-rounds` などの引数を渡さない。このとき状態ファイルの次の 10 項目が変わらず、 - `max_rounds: 20` の状態ファイルが 12 へ戻らない - - | 区分 | 項目 | - | --- | --- | - | 進め方 | `max_rounds` / `rotate_after` / `verify_commands` / `verify_exit_codes` | - | 担当 | `only` / `excluded_reviewers` / `available_reviewers` / `unavailable_reviewers` / `require_all` / `auth_skipped` | -- [ ] AC24: `only: null` の状態ファイルで、再開の `init` に `--only codex` を渡す。`only` が `codex` になり、次の - `start-round` が `codex` だけを返す。記録を持つ過去のラウンドの `_round_reviewers` は記録のまま変わらない -- [ ] AC25: `only: "codex"` の状態ファイルで、再開の `init` に `--only none` を渡す。`only` が `null` になり、使える者が - 作り直される。`--exclude none` は `excluded_reviewers` を空にする -- [ ] AC26: 再開の `init` に `--exclude agy` を渡す。母集合から `agy` を除いた全員の認証を確かめ直し、 - `available_reviewers` から `agy` が消え、次の `start-round` が `agy` を返さない。担当に関わる引数(`--only` / - `--exclude` / `--require-all`)を渡さない再開では、認証を確かめ直さない -- [ ] AC27: `host: "claude"` の状態ファイルで、再開の `init` に `--host codex` を渡す。`host` は `claude` のまま変わらず、 - 反映しないことが 1 行出る。同じ値の `--host claude` では何も出ない - -手順書の骨組み: - -- [ ] AC28: `SKILL.md` と `docs/01-state-and-review.md` の骨組みが、`start-round` が返した担当をそのまま使う。 - 起動・監視・取り込み・反証の担当を `$ONLY` で絞らない。2 ファイルに対する `grep -n 'ONLY' ` の出力が、`init` へ - 引数を渡す行と引数の説明の行だけになる - -前のラウンドの検査: - -- [ ] AC29: 前のラウンドが `verdict` を持たず、担当 `agy` + `kiro` の両者が `REQUEST_CHANGES` で、修正の記録が無い。 - このとき `start-round` は終了コード 5 で止まる。変更前は旧来の 2 者 `codex` / `agy` で数え、`codex` を結果なしと - 読んで通していた - -文書: - -- [ ] AC30: 次の 4 ファイルが、それぞれの内容を書く - - | ファイル | 書く内容 | - | --- | --- | - | `SKILL.md` | 引数の表と `argument-hint` に `--exclude` と `--require-all` がある。`--only` の説明から「デバッグ用」が消える | - | `docs/05-pool-and-convergence.md` | 使える者の数による分岐と、除外 | - | `docs/04-contracts.md` | 状態ファイルの 6 項目(`available_reviewers` / `excluded_reviewers` / `unavailable_reviewers` / `require_all` / `auth_skipped` / `resume_changes`) | - | `docs/01-state-and-review.md` | 再開で反映する引数と、反映しない引数 | - -## 受け入れ条件(両方) - -- [ ] AC31: `uv run --with pytest pytest scripts/tests plugins/ndf -q` が通る -- [ ] AC32: 次の 3 つが終了コード 0 で終わる - - ```bash - bash scripts/build-runtime-plugins.sh --check - claude plugin validate . - python3 scripts/check-skill-frontmatter.py - ``` - -## 非機能の条件 - -| 大項目 | 条件 | -| --- | --- | -| 可用性 | 母集合の 1 者が使えないことで、収束ループを開始できない状態にならない(AC10) | -| 性能・拡張性 | 認証の確認の回数は、新規の `init` で変更前を上回らない(除外した者は確かめない)。再開で増えるのは担当に関わる引数を渡したときだけで、母集合の最大 3 者である | -| 運用・保守性 | 担当が欠けたまま収束したことが、`report` の出力だけで分かる(AC21)。再開で反映しなかった引数が出力に出る(AC27) | - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| 認証に失敗する CLI がある利用者 | `init` が止まらず、使える者で回る。従来の関門は `--require-all` で選べる | -| `--only` で回していた利用者 | 反証する担当がいない根拠付きの `major` が数えられ、最初のラウンドで収束しなくなる | -| 状態ファイルの形 | 最上位に 6 項目が増える。無い状態ファイルは従来の担当の決め方で読む | -| `init` の引数 | `--exclude` / `--require-all` が増え、`--only` が `none` を取る。既定値は変わらない(`--max-rounds 12` / `--rotate-after 8`) | -| 再開で修正の記録の無い前ラウンドがある実行 | 担当が `codex` / `agy` 以外のラウンドでも、前ラウンドの検査が止める(AC29) | -| `cross-refactoring` | 変わらない | - -## 前提 - -| # | 前提 | -| --- | --- | -| 1 | P1〜P3(D-A)が先に `develop` へ入る。P4 / P5 の `state.py` の変更はその上に載せる。P4 の規則は、起動し直しの経路が証拠集約を通る形(P3)でも通らない形でも成り立つように決める | -| 2 | 担当は最大 2 者である(`review_assign` が 3 者以上から 1 者を外すため)。反証を返せる担当は、提案者を除いて最大 1 者になる | -| 3 | 再開の `init` の後、骨組みは必ず `start-round` で新しいラウンドを開く。開いたまま中断したラウンドの担当を書き換える必要は無い | -| 4 | 認証の確認コマンド(`AUTH_PROBES`)とその判定は変えない | - -前提 3 の根拠: `SKILL.md` の骨組みは `init` の直後に `start-round` を呼ぶ。`start-round` は常に -`len(rounds) + 1` のラウンドを開く(`state.py` の `cmd_start_round`)。 - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| テスト | `uv run --with pytest pytest scripts/tests plugins/ndf -q` | -| 配布物の同期 | `bash scripts/build-runtime-plugins.sh --check` | -| 定義の検査 | `claude plugin validate .` と `python3 scripts/check-skill-frontmatter.py` | -| 手動確認 | P5 の後、`--exclude agy` を付けた `cross-review` を 1 本の Pull Request で回し、`agy` が 1 度も起動しないことを `report` で見る | - -## 前提とする取り決め - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | 担当の決め方は `plugins/ndf/scripts/lib/assignment.py`、認証は `lib/auth.py` に置く(`cross-refactoring` と共有する共通層)。判定は `state.py` に置き、骨組みは結果を使うだけにする | -| コーディング規約 | 状態ファイルを最小の形で組み、関数を直接呼んで確かめる(`AGENTS.md` の DO) | -| テスト戦略 | 既存の形(`tests/conftest.py` の `state_mod` で `state.py` を読み込み、GitHub を呼ぶ関数を差し替える) | - -## 境界 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 既存テストの実行、配布物の同期の検査 | -| 確認してから行う | `init` の既定を「認証の失敗で止める」から「使える者で回す」へ変えること(利用者の判断を仰ぐ) | -| 行わない | `cross-refactoring` の担当の決め方と `refactor_lib` の変更、監視と起動の変更 | - -## 用語 - -| 用語 | 意味 | -| --- | --- | -| 母集合 | 全ランタイム − ホストの 3 者(`review_pool(host)`) | -| 使える者 | 母集合から、`--exclude` で外した者と認証を通らなかった者を除いた一覧(`available_reviewers`) | -| 担当 | そのラウンドにレビューする者。`start-round` がラウンドへ記録する | -| 証拠集約の印 | `evidence_rounds` に載るラウンド番号。統合・実行検証・反証を通り切ったラウンドに付く | -| 反証を受けた指摘 | 提案者でない担当の有効な反証(`CRITIQUE_VERDICTS` のいずれか)が 1 件以上結ばれた指摘 | -| 単独の指摘 | `origin_runtimes` が 1 者の指摘 | -| 再開 | 状態ファイルが残り `final` が `null` のときの `init`(`_resume_from_state`) | - -## 依頼(原文) - -### #624 - -> `cross-review` を `--only codex` で 1 者だけにして回すと、codex が `REQUEST_CHANGES` で新しい指摘を投稿したラウンドでも、 -> `judge` が収束と判定する(ndf 10.10.1、2026-09-13)。 -> -> | 候補 | 内容 | -> | --- | --- | -> | 反証の担当がいないラウンドは絞り込まない | 反証が 0 件のときは全件を数える(`_evidence_completed` を偽に扱う) | -> | `--only` では intent に従う | 担当が 1 者なら、全員 pass(`round_passes`)だけを収束の条件にする | -> | 印を指摘単位で持つ | 証拠集約の印をラウンド単位ではなく指摘単位(反証の対象になったか)で持ち、反証の対象にならなかった指摘は絞り込まない。#583 の起動し直しの経路もまとめて塞がる | - -### #583(収束の部分) - -> **起動し直した担当の指摘も、反証を受けないまま数えられない。** 証拠集約の印(`_mark_evidence_round`)は、 -> 1 回目の経路で反証を取り込んだ時点でラウンドに付く。起動し直した担当の指摘はその後に取り込まれるため、 -> 反証の対象にならないまま `_new_finding_count` の絞り込みにかかり、単独の major は `insufficient_evidence` へ落ちる。 - -### #478 - -> **認証確認を「関門」から「導入状況の把握」へ変える。** 使える者でレビューし、使えない者は最初から数えない。 -> -> 1. `init` で母集合の各ランタイムの認証を確かめ、**通った者の一覧を `state.json` へ記録する**(`available_reviewers`) -> 2. **明示的に外す手段を用意する**(例: `--exclude agy`)。**認証は通るが実行で落ちる担当を外す用途でも使う。** -> 3. 使える者の数で分岐する(3 者: 現行どおり / 2 者: 毎ラウンドその 2 者 / 1 者: 警告して 1 者 / 0 者: 失敗) -> 4. `review_assign()` は「**使える者が 3 者以上のときだけ 1 者を外す**」に変える -> 5. 全員揃っていることを要求したい運用のために `--require-all` を用意する -> 6. 完了報告に「このループに参加したのは誰か」を出す - -### #648 - -> `/ndf:cross-review` を中断・再開すると、`state.py init` に渡した `--only` / `--max-rounds` / `--rotate-after` / -> `--verify-command` / `--verify-exit-code` / `--host` が**黙って無視される**。 -> -> 再開経路でも、**明示的に渡された引数だけ**を状態ファイルへ反映する。反映できないと決めた引数(例えば `--host`)は、 -> **渡されたら 1 行知らせる**。黙って捨てない。 - -(各 issue の本文から抜粋。全文は `gh issue view 624` / `583` / `478` / `648`) diff --git a/issues/issue-647-592-553-design.md b/issues/issue-647-592-553-design.md deleted file mode 100644 index b6dc51c9a..000000000 --- a/issues/issue-647-592-553-design.md +++ /dev/null @@ -1,499 +0,0 @@ -# #647 / #592 / #553: 適用ラウンドに試行の上限を置き、帰属行の後ろのトレーラーを読む - -要求と受け入れ条件は [issue-647-592-553-requirements.md](issue-647-592-553-requirements.md) にある。 -この文書は「どう作るか」だけを扱う。 - -**実装は 1 本の Pull Request(P6)にまとめる**(決定 1)。マイルストーン 21 の順序では P3 の後に載せ、 P4・P5 とは並行してよい。 - -## 機能一覧 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| 1 | 結果を残さない担当の群を、担当を替えて 1 回だけ開き直し、2 回目も残さなければ取り消す | cross-refactoring を回す進行側 | -| 2 | 採用 0 件の提案ラウンドと項目の無い群で、適用担当を起動せずに次へ進む | 同上 | -| 3 | 修正の結果を群の担当から読み、結果が無ければ修正ラウンドを 1 つ進める | 同上 | -| 4 | 帰属行の段落が後ろに付いたコミットから必須トレーラーを読む | 同上(claude が適用担当の群) | -| 5 | 適用・修正の監視が、テストの実行中の無出力で担当を打ち切らない | 同上 | - -## 決定の記録 - -決定 1〜8 は #647 と修正ラウンド、決定 9〜10 は #592、決定 11〜12 は #553、決定 13 は無進捗の打ち切りを扱う。 - -### 決定 1: 3 課題と `merge-fix` の担当の食い違いを 1 本の Pull Request で直す - -#647 と #592 は同じ `next-apply-round` → `merge-apply` の繰り返しで止まらず、#553 は同じ適用の検証で群を落とす。 -`merge-fix` の食い違いは、#647 を直した後に同じ形(結果を取り込めない担当で修正と検証が往復する)で残る。いずれも `refactor_lib/commands/apply.py` -と `SKILL.md` の骨組みを触るため、分けると同じ箇所を 2 度変える。 - -`merge-fix` を別の issue に起票する形は採らない。起票しても直す場所と時期が P6 と同じになる。 - -### 決定 2: 同じ群の試行の上限を 2 回の固定値にし、引数を足さない - -2 回目は別の担当が試すため(決定 3)、2 回とも結果を残さなければ担当ではなく群の側を疑える。3 回以上にしても、輪番の 4 者のうち壊れた者に当たる確率が上がるだけである。 -値は `vocabulary.py` の `MAX_APPLY_ATTEMPTS` に置く。 - -`--max-apply-attempts` を足す形は採らない。`SKILL.md` は上限を 2 つ置くとどちらで止まったかを読み解く必要が出るとしており、 -利用者が変えたい理由も見当たらない。止まった理由は群の記録(`drop_reason`)が持つ。 - -### 決定 3: 2 回目の試行は次の輪番の担当が行い、利用上限でも進行全体を止めない - -結果を残さない原因の多くは担当の CLI の側にある(rf646 の agy の STALLED 4 回、claude の 429 の 3729 回)。 -同じ担当で開き直しても直らない。担当を替えれば、壊れた CLI が 1 者でも他の者が群を適用できる。替える先は、`apply_seq` を 1 ずつ進めて輪番の担当を引き、 -その群で失敗した担当のどれとも違う担当が出た最初の番号の担当である。引く式は群を割り当てたときと同じものを通す(決定 8)。 - -1 つ進めるだけにしない。`apply_seq` は提案ラウンドの全群を割り当てた後の番号で、群自身の番号ではない。輪番は 4 者で 1 周するため、 -群が 4 つあり先頭の群が失敗すると、次の番号の担当は失敗した担当と同じになる(`assign(1)` と `assign(5)` はどちらも codex。 -「実測」の節)。 - -利用上限(429)で進行全体を止める形は採らない。止めると他の 3 者で進められる群まで止まり、再開すると同じ者にまた当たる。429 は監視が早期の致命として 15 秒前後で打ち切る(D-A -の P3)ため、担当を替える 1 回の費用は小さい。利用上限だった事実は理由の名前(`usage_limit`)として群の記録と見送りの理由に残る。 - -### 決定 4: 開き直しの判定は `merge-apply` が持ち、骨組みは監視の終了コードで分岐しない - -`merge-apply` は結果ファイルを読めたかどうかを自分で知っている。監視の終了コードで分岐しても、結果ファイルが後から書かれた場合(#584)や、 -監視は `OK` でも JSON が壊れている場合は結果ファイルの側で決めるしかない。判定を 1 か所に置くと、骨組みは `|| continue` のまま変わらない。 - -骨組みで監視の終了コードを受けて `merge-apply` へ渡す形は採らない。渡しても振る舞いは変わらず、使い道は理由の記録だけである。 -理由は D-A が P1 で残す監視の結果ファイルから読む(「他の設計との契約」)。 - -### 決定 5: 試行番号は `next-apply-round` が進め、前の試行が失敗で閉じたときだけ進める - -`next-apply-round` が `pending` の群を開くとき、失敗した試行の記録の数が試行番号と等しければ試行番号を 1 進める。 -等しくなければ(取り込みの前に進行が止まった再開)そのまま開く。 - -`merge-apply` は同じ試行番号の失敗の記録が既にあれば、結果ファイルを読まずに、記録を足さず同じ終了コード 2 を返す。読んでから判定する形は採らない。 -結果ファイルの名前(`{agent}-apply-r<提案ラウンド>`)は群の番号を持たず、消すのは `launch-cli.sh` の起動時だけである。 -担当を替えた直後に叩き直すと、替えた先の担当が同じ提案ラウンドの先行の群で残した結果を読み、検証へ進んで群を落とす。進行はどこで止まっても叩き直せることが前提である(`docs/02-apply-and-review.md`「叩き直しても同じ判定を返す」)。 -叩き直しを 2 回目の試行と数えると、1 回の失敗で群を落とす。 - -開いた回数だけを数える形は採らない。進行側が落ちて再開しただけで試行が進む。 - -### 決定 6: 結果を残さない試行のコミットは、開き直す前に取り消す - -`next-apply-round` は `pending` の群を開くたびに起点を HEAD へ置き直す(`apply.py:304-309`)。 -結果を残さない担当がコミットだけ作って止まると、そのコミットは次の試行の起点より前に入り、検証を受けないまま次の群の push で Pull Request に出る。 -範囲にコミットがあれば、既存の `_revert_unverified_apply_round`(`apply.py:555-598`)から取り消しの本体(`revert_item_commits` -と起点の更新)だけを切り出して共有し、起点を取り消し後の HEAD にする。関数をそのまま呼ぶ形は採らない。群を `dropped` にして項目を見送るため、 -1 回目の失敗で群が閉じる。 - -### 決定 7: 着手前テストの未確認と範囲の未確定は中断(4)にする - -`init` は着手前テストが失敗していれば止まるため、`merge-apply` で `green` でないのは状態ファイルが壊れたときだけである。 -範囲を確定できないのも git の状態が壊れたときである。どちらも群を替えても直らず、 `SKILL.md` の終了コードの表は既に「範囲を確定できない」を 4 と書いている。 - -試行の上限へ含める形は採らない。全ての群が 2 回ずつ同じ理由で落ち、全件が見送りになってから気づく。 - -### 決定 8: 作業を任せる担当の決定は `rounds.impl_for_seq` の 1 つを通す - -`rounds.impl_for_seq(state, seq)` を新設し、輪番の通し番号から担当を引く呼び出しはその中だけにする。呼ぶのは次の 3 か所で、 -いずれも `apply_seq` を進めて CLI に作業を任せる担当を決める。 - -| 呼び出し元 | 決めるもの | -| --- | --- | -| `apply._assign_apply_rounds_to_state` | 群を割り当てたときの担当 | -| `apply._close_failed_attempt` | 結果を残さなかった群の交代先 | -| `gate._final_fix_impl`(`gate.py:128` を置き換える) | 最終ゲートの修正担当 | - -置き場所を `rounds.py` にするのは、`apply.py` と `gate.py` の両方が読む層だからである(`commands` どうしの取り込みを作らない)。 -D-B が除外(#478)を足すとき、変える呼び出しは 1 か所で済む。`assignment.py` は変えない。 - -`setup.cmd_start_round`(`setup.py:467`)の `assignment.assign` は通さない。引くのは提案ラウンドの記録上の担当で、 -骨組みは適用の前に `next-apply-round` が返す群の担当で `IMPL` を上書きするため、この担当は CLI を起動しない。 - -### 決定 9: 採用 0 件の提案ラウンドでは群を作らない - -`rounds.apply_groups` は、`apply_rounds` が空の配列のときも古い版の状態として扱う(`rounds.py:108-110` の `if groups:`)。 -そのため、ラウンド全体を 1 つの群にしている。`merge-proposals` は採用 0 件で `apply_rounds = []` を書くため、 -ここで項目 0 件の群が生まれる。鍵が無い(`None`)ときだけ古い版として扱い、空の配列はそのまま返す。 - -`merge-proposals` がテスト整備の採用 0 件で終了コード 2 を返す形は採らない。2 は構造改善の繰り返しを終える合図で、 -テスト整備では構造改善へ進む前に抜けてしまう。 - -### 決定 10: 項目の無い群は開かずに取り消し、取り込み済みで採用 0 件の群も取り消しへ直す - -決定 9 の後も、既に項目の無い群を持つ状態ファイル(rf587)は残る。`next-apply-round` は `items` が空の `pending` の群を `dropped`(`drop_reason: empty`)にして次の群を探す。 -`merge-apply` の取り込み済みの判定で採用 0 件だったときは、群が `dropped` でなければ `dropped` にしてから終了コード 2 を返す。 - -### 決定 11: トレーラーは、末尾から続くトレーラーの段落を git の判定で読む - -`commit_trailers` はメッセージを空行で段落に分け、末尾の段落から前へ向かって、1 段落ずつ `git interpret-trailers --parse` に掛ける。 -git がトレーラーの段落と判定しなかった段落で止め、それまでに読んだ段落の値を合わせる。同じ鍵は末尾に近い段落の値を採る。**1 段落目(題名)は掛けない。** -掛けると、本文がトレーラーだけのコミットで `Refactor: …` の形の題名をトレーラーとして読む(「実測」の節)。 - -| issue の案 | 採否 | 理由(「実測」の節) | -| --- | --- | --- | -| `git interpret-trailers --parse` へ替える | 採らない | `%(trailers:only,unfold)` と同じく最後の段落しか読まない | -| 全文から `^: ` を拾う | 採らない | 散文の段落にある `Round: …` の形の行を拾う | -| 雛形に最後の段落へ置くよう書く | 補助として採る(決定 12) | 実装担当が従わなければ落ちる | -| 進行側が `--amend` でトレーラーを足し直す | 採らない | SHA が変わり、結果ファイルの申告との対応が切れる | - -段落ごとの判定を git に任せるため、トレーラーの定義(区切り文字・折り返し・25% の規則)を自前で持たない。散文の段落で止まるため、本文中の `Key: value` の形の行は読まない。 - -`claude -p` に `--settings` で帰属行を消させる形は採らない。帰属行を書くのはモデルで、利用者の `CLAUDE.md` の指示でも足される。 -共通層の `launch-cli.sh` は cross-review も使う。 - -### 決定 12: 雛形のコミットの規約にも、必須トレーラーを最後の段落に置くことを書く - -進行側の検証は決定 11 で通る。一方、人が `git log --format='%(trailers:key=Impl-Model,valueonly)'` で集計すると、 -最後の段落しか読まない。`docs/02-apply-and-review.md` は、この集計を理由にトレーラー形式を選んでいる。帰属行を同じ段落に続けて書けば、 -git の標準の読み方でも取れる。 - -### 決定 13: 無進捗の許容を `--test-timeout` + 900 秒にし、雛形に進捗マーカーを足す - -適用・修正の担当はテストを 1 回実行し、その間は何も出力しない。テストの上限は `--test-timeout`(既定 900)である。 claude は `--output-format json` -のため、完了まで出力しない(監視の既定の許容 900 秒はこのため)。 2 つを足した値が、正常に動いていても出力が無い最長の時間になる。`init` がこの値を `IMPL_STALL_TIMEOUT` -として出し、骨組みの適用・修正・最終ゲートの修正の監視が `--stall-timeout` に渡す。`monitor.py` は変えない。 - -**監視の上限(`--timeout`)は渡さない。** 上限は D-A の P2 が `--phase` と `lib/limits.py` で工程ごとに決め(既定 3600 秒)、修正と -最終ゲートの修正が 420 秒で打ち切られる件も P2 が直す。P6 は P2 が入れた `--phase` の呼び出しに `--stall-timeout` だけを足す。 - -雛形の進捗マーカーは、次に STALLED が出たときに担当が動いていたかを読むためにも足す。rf646 の agy のログは作業ツリーとともに消えており、 -この設計の時点では確かめられなかった(未確認 1)。 - -**`--test-timeout` は、`apply` / `fix` / `final-fix` の監視の上限 − 900 未満を前提にする**(P2 の既定 3600 秒なら 2700 未満)。 -監視の上限は `MONITOR_TIMEOUT_<担当>` / `MONITOR_TIMEOUT` で上書きされうる。この値以上にすると許容が監視の上限以上になり、監視の上限で -打ち切られ、P2 の監視が担当名と 2 つの値を警告する(D-A の AC33)。D-A の AC31 が固定する「無進捗の許容 < 監視の上限」は担当ごとの -既定の許容の組で、この許容(既定 1800 < 3600)はその順序を崩さない。監視の上限を許容から導く形は採らない。上限の表は P2 の -`limits.py` が 1 つだけ持ち、適用の所要の実測も無い(未確認 6)。 - -進捗マーカーだけで足りるとする形は採らない。テストの実行中はマーカーを書けない。 `MONITOR_STALL_AGY` などの環境変数に委ねる形も採らない。 -担当ごとに値を覚えさせることになり、cross-review にも効く。 - -## 実測 - -### #647: 結果を残さない群が開き直され続ける - -一時のテストファイルで `cmd_next_apply_round` → `cmd_merge_apply` を直接 4 回呼んだ(コミットしない)。 -補助は `tests/conftest.py` の `no_git` / `patch_lib` と、`test_merge_apply.py` の `_state_with_items` -/ `git_facts` である。 - -| 状態 | 開いた群 | `merge-apply` の終了コード | 群の `status` | -| --- | --- | --- | --- | -| 群 2 つ(agy / codex)、結果ファイルなし | `[1, 1, 1, 1]` | `[2, 2, 2, 2]` | `pending`, `pending` | -| 同上で着手前テストが `red`(2 回) | `[1, 1]` | `[2, 2]` | `pending`, `pending`(項目は `blocked`) | - -輪番の担当は `plugins/ndf/scripts/lib/assignment.py` の `assign` を `seq` 1〜8(ホスト claude)で引くと `codex, agy, kiro, claude, codex, agy, kiro, claude` -だった。4 で 1 周するため、`apply_seq` を 1 進めるだけでは失敗した担当へ戻ることがある(決定 3)。 - -### 範囲へ入れたもの: `merge-fix` が提案ラウンドの担当を読む - -群の担当 agy、提案ラウンドの担当 codex で `agy-fix-r1-result.json` を置き、`cmd_merge_fix` を 3 回呼んだ。 -3 回とも終了コード 2 で `❌ codex の結果ファイルがありません`、`fix_rounds` は 0 のままだった。 `merge-fix` は `entry["impl"]`(`converge.py:463`)を読み、 -骨組みは `launch-cli.sh "$IMPL" fix` で群の担当を起動する。 - -### #592: 採用 0 件で項目の無い群が開く - -テスト整備ラウンドで提案 0 件の状態から `cmd_merge_proposals`(終了コード 0)→ 上と同じ 4 回を呼んだ。 - -| 結果ファイル | 開いた群 | 終了コード | 群 | -| --- | --- | --- | --- | -| なし | `[1, 1, 1, 1]` | `[2, 2, 2, 2]` | `items: []`、`status: pending` | -| `{"items": []}` | `[1, 1, 1, 1]` | `[2, 2, 2, 2]` | `items: []`、`status: applied`(rf587 と同じ形) | - -`apply_groups` を「鍵が `None` のときだけ群を作る」に差し替えて、同じ手順を試した。`next-apply-round` は 1 回目で終了コード 1 を返し、 -`apply_rounds` は `[]` のままだった。 - -### #553: 段落ごとの読み取り - -git 2.53.0 の一時リポジトリでコミットを作り、2 つの読み方を比べた。1 つは `git log -1 --format='%(trailers:only,unfold)'` である。 -もう 1 つは決定 11 の読み方の試作で、Python から段落ごとに `git interpret-trailers --parse` を呼ぶ。 - -| メッセージの末尾 | `%(trailers:only,unfold)` | 試作 | -| --- | --- | --- | -| 必須 4 つ / 空行 / `Co-Authored-By` | `Co-Authored-By` だけ | 5 つ | -| 必須 4 つ / 空行 / `Co-Authored-By` + `Claude-Session` | 帰属行 2 つだけ | 6 つ | -| 必須 4 つと `Co-Authored-By` が同じ段落 | 5 つ | 5 つ | -| `Round: 本文の説明行` / 散文 / 必須 4 つ / 帰属行 | 帰属行だけ | 帰属行と必須 4 つ(`Round` は `4`) | -| 散文 + `Item-Id: R9-999` の段落 / 帰属行 | 帰属行だけ | 帰属行だけ | - -`printf 'Refactor: 重複を除く\n\nRefactor: 重複を除く\n' | git interpret-trailers --parse` は `Refactor: 重複を除く` -を返した。題名を段落として git に掛けると、題名をトレーラーとして読む。 - -`git log -1 --format=%B | git interpret-trailers --parse` は 1 行目と同じく帰属行だけを返した。 -`interpret-trailers --parse` へ替えるだけでは効かない。 `git interpret-trailers --parse` は題名の無い入力(段落 1 つだけ)ではトレーラーを返さなかったため、 -試作は `<題名>\n\n<段落>` の形で渡している。 - -## 構成要素 - -| 要素 | 責務 | 課題 | -| --- | --- | --- | -| `rounds.apply_groups`(変更) | 鍵が無いときだけ古い版として群を 1 つ作る。空の配列はそのまま返す | #592 | -| `rounds.current_group`(変更) | 群が 1 つも無いときは中断(4)する | #592 | -| `apply.cmd_next_apply_round`(変更) | 項目の無い `pending` の群を取り消して飛ばす。開くときに試行番号を進める(決定 5) | #647 #592 | -| `apply.cmd_merge_apply`(変更) | 取り込み済みで採用 0 件の群を取り消しへ直す。着手前テストと範囲の検査を結果の読み取りより前に置き、4 で中断する。結果を読めなければ `_close_failed_attempt` へ渡す | #647 #592 | -| `apply._close_failed_attempt`(新設) | 範囲のコミットを取り消し、失敗した試行を記録する。上限未満なら担当を替え、上限なら群を取り消して項目を見送る | #647 | -| `rounds.impl_for_seq`(新設) | 輪番の通し番号から担当と要求モデルを返す。作業を任せる担当を決める呼び出しはすべてこれを通す(決定 8) | #647 | -| `gate._final_fix_impl`(変更) | 最終ゲートの修正担当を `rounds.impl_for_seq` で引く | #647 | -| `apply._monitor_reason`(新設) | `monitor_outcome.read_outcome(tmp_dir, "-apply-r")` の `reason` が `timeout` / `stalled` / `early_error` / `usage_limit` / `cli_timeout` / `pidfile_bad` ならその値を返す。`ok` / `missing` / ファイルなしなら `load_result` の問題(`missing` / `unparsable`)を返す(D-A の AC55 と同じ規則) | #647 | -| `gitfacts.load_result`(新設) | 結果ファイルを読み、`(payload, problem)` を返す。問題は `missing`(無い)/ `unparsable`(JSON として読めない・オブジェクトでない)。中断しない | #647 | -| `gitfacts.read_result`(変更なし) | 最終ゲートの修正(`gate.py`)が引き続き使う。結果が無いときの扱いは #674 | — | -| `converge.cmd_merge_fix`(変更) | 群の担当の結果を読む。結果を読めなければ範囲を取り消し、修正ラウンドを 1 つ進めて 2 で終わる | 範囲へ入れたもの | -| `gitfacts.commit_trailers`(変更) | 末尾から続くトレーラーの段落を読む(決定 11) | #553 | -| `vocabulary.py`(変更) | `MAX_APPLY_ATTEMPTS = 2` と `IMPL_STALL_MARGIN = 900` | #647 | -| `setup._emit_init`(変更) | `IMPL_STALL_TIMEOUT` を出す | #647 | -| `prompts/apply.md` / `fix.md`(変更) | 必須トレーラーを最後の段落に置く。進捗マーカー | #553 #647 | -| `prompts/final-fix.md`(変更) | 進捗マーカー | #647 | -| `SKILL.md`(変更) | 語の表の適用ラウンドの上限、「別の上限を置かない」の段落の削除、骨組みの監視の引数と終了コード 2 の注記 | #647 | -| `docs/02-apply-and-review.md` / `docs/04-fix-and-report.md`(変更) | Step 4 と Step 6 の骨組みの監視の引数、開き直しの規則、トレーラーの読み方、修正の結果が無いとき | 全体 | - -```mermaid -graph TD - N[next-apply-round] --> G[rounds.apply_groups
current_group] - A[merge-apply] --> G - A --> LR[gitfacts.load_result] - F[merge-fix] --> LR - A --> C[_close_failed_attempt] - C --> S[rounds.impl_for_seq] - S --> AS[assignment.assign] -``` - -```mermaid -graph TD - I[init] -->|IMPL_STALL_TIMEOUT| M[monitor.py] - M --> MR[監視の結果ファイル
D-A の P1] - C[_close_failed_attempt] --> R[_monitor_reason] - R --> MR -``` - -上の図は群の判定、下の図は監視との関係を描く。雛形・文書と、`commit_trailers`(`merge-apply` の検証が呼ぶ 1 本だけ)は含めない。`assignment.assign` と監視の結果ファイルは変えない要素である。 - -### 文脈と配置 - -```mermaid -graph TD - 利用者 --> ホスト[ホストの CLI セッション] - ホスト -->|骨組みの bash| RF[refactor.py] - ホスト -->|launch-cli.sh| 担当[適用・修正の担当 CLI] - ホスト --> 監視[monitor.py] - RF --> WORK[work/ の git] - 担当 --> WORK - 担当 --> 結果[結果ファイル
progress.log] - 監視 --> 結果 - 監視 --> 記録[監視の結果ファイル] - RF --> 記録 -``` - -**配置は変えない。** すべて利用者の機械のホストのセッションから起動するプロセスで、常駐しない。図は状態ファイルと Pull Request への push を含めない。この変更で増える辺は、`refactor.py` が監視の結果ファイルを読む 1 本だけである。 - -### 変わるファイル - -```text -plugins/ndf/skills/cross-refactoring/ -├── SKILL.md # 語の表・段落の削除・骨組み -├── docs/02-apply-and-review.md # Step 4 の骨組み・開き直し・トレーラー -├── docs/04-fix-and-report.md # Step 6 の骨組みの監視の引数・修正の結果が無いとき -├── prompts/apply.md # トレーラーの段落・進捗マーカー -├── prompts/fix.md # 同上 -├── prompts/final-fix.md # 進捗マーカー -├── scripts/refactor_lib/ -│ ├── rounds.py # apply_groups / current_group / impl_for_seq -│ ├── gitfacts.py # load_result / commit_trailers -│ ├── vocabulary.py # MAX_APPLY_ATTEMPTS / IMPL_STALL_MARGIN -│ └── commands/ -│ ├── apply.py # next-apply-round / merge-apply / 補助 3 つ -│ ├── converge.py # merge-fix -│ ├── gate.py # _final_fix_impl -│ └── setup.py # _emit_init -└── tests/ - ├── test_apply_attempts.py # 新設: AC1〜AC12、AC16 - ├── test_apply_rounds.py # AC13〜AC15 - ├── test_abandon_items.py # AC17〜AC19 - ├── test_commit_trailers_git.py # 新設: AC20〜AC26(一時リポジトリ) - ├── test_init.py # AC28 - ├── test_merge_apply.py # AC34 - ├── test_final_fix.py # AC37 - └── test_skill_terms.py # AC27、AC29〜AC32 -# dev.kiro / dev.agy の配布物は bash scripts/build-runtime-plugins.sh で同期する -``` - -## データ構造(状態ファイルの群) - -状態ファイル(`cross-refactoring-rf<番号>-state.json`)の `rounds[].apply_rounds[]` に項目を足す。 -**版は上げない。** 足す項目が無い状態ファイルは、試行番号 0・失敗の記録なしとして読む。 - -| 項目 | 型 | 値 | 空のときの意味 | -| --- | --- | --- | --- | -| `attempt` | 整数 | いま開いている試行の番号。1 から | 鍵なし = 0(まだ開いていない)。`merge-apply` は 0 を 1 回目として記録し、値を 1 に書く(変更前の版で開いた群を再開したとき) | -| `failed_attempts` | 配列 | 結果を残さなかった試行。1 件 = `{attempt, impl, reason, at, reverted}` | 鍵なし = 失敗なし | -| `failed_attempts[].reason` | 文字列 | 監視の理由の名前(`stalled` など)。監視が `ok` / `missing` か結果ファイルが無ければ `load_result` の問題(`missing` / `unparsable`) | — | -| `failed_attempts[].reverted` | 整数 | その試行の範囲から取り消したコミットの数 | 0 = コミットなし | -| `drop_reason` | 文字列 | `no_result`(試行の上限)/ `empty`(項目なし) | 鍵なし = 既存の経路で取り消した、または取り消していない | -| `impl` / `impl_model` | 既存 | 担当を替えたときに書き換える。**前の担当は `failed_attempts[].impl` に残る** | — | - -`impl` を書き換えても、どの担当がどの試行で失敗したかは `failed_attempts` から読める。群を取り消したときの見送りの理由(`deferred_items[].defer_reason`)は、 -`実装担当が結果を残しませんでした(agy: stalled → codex: missing)` の形にし、改修計画の「見送った項目」の表にそのまま出る。 - -ラウンドの項目(`rounds[]`)は `fix_merged_keys` に `":missing"` の形の鍵を足す(AC19)。 - -### 群の状態の遷移 - -```mermaid -stateDiagram-v2 - [*] --> pending: merge-proposals - pending --> dropped: next-apply-round(items が空) - pending --> pending: merge-apply(結果を残さない・attempt < 2)
担当を替える - pending --> dropped: merge-apply(結果を残さない・attempt = 2) - pending --> dropped: merge-apply(未割当・検証の失敗) - pending --> applied: merge-apply(取り込んだ) - applied --> dropped: merge-apply(取り込み済み・採用 0 件) - applied --> verified: verify-round(通った) - applied --> dropped: abandon-items / merge-test-judgements - verified --> [*] - dropped --> [*] -``` - -**`pending` のまま同じ担当で開き直す遷移は無い。** この変更で足す遷移は 4 本で、`items` が空の取り消し、担当の交代、 -試行の上限での取り消し、取り込み済み・採用 0 件の取り消しである。 - -## 入出力の契約 - -### コマンドの終了コード - -| コマンド | 0 | 1 | 2 | 4 | -| --- | --- | --- | --- | --- | -| `next-apply-round` | 群を開いた(変更なし) | 残りの群が無い(変更なし。群が無いラウンドも含む) | — | — | -| `merge-apply` | 取り込んだ(変更なし) | — | **この群を取り消した、または担当を替えて開き直す**(意味を広げる) | 着手前テストが `green` でない・範囲を確定できない(2 から変更)・群が無い(新) | -| `merge-fix` | 取り込んだ(変更なし) | — | 範囲を確定できない(変更なし)・**結果を読めない(新。修正ラウンドは進む)** | 群が無い(新) | -| `verify-round` / `abandon-items` / `merge-test-judgements` | 変更なし | — | 変更なし | 群が無い(新) | - -「群が無い(新)」は、群が 1 つも無いラウンドで `current_group` を呼んだときの中断である。`next-apply-round` は `current_group` を呼ばない。 - -骨組みは `merge-apply` の 2 で `continue` し、`merge-fix` の終了コードを見ない。**どちらも変えない。** - -### `init` の出力 - -`IMPL_STALL_TIMEOUT=` を足す。`test_timeout` は状態ファイルの値で、 -再開時も同じ値を出す。 - -### 骨組み(`SKILL.md` の「実行」の差分) - -```bash - "$LIB/monitor.py" "$ID" --agents "$IMPL" --tmp-dir "$TMP_DIR" \ - --stem-template "{agent}-apply-r$ROUND" --phase apply \ - --stall-timeout "$IMPL_STALL_TIMEOUT" - # 終了コード 2 = この群を取り消した、または担当を替えて開き直す。修正ラウンドは回さない - rf merge-apply "$ID" "$ROUND" || continue -``` - -差分は `--stall-timeout` の 1 行だけで(`--phase apply` は P2 の形)、修正と最終ゲートの修正の監視にも同じ行を足す。`--timeout` は渡さない。 - -### 雛形に足す文 - -| 雛形 | 足す文 | -| --- | --- | -| `apply.md` / `fix.md` のコミットの規約 | 4 つのトレーラーは**メッセージの最後の段落**に置く。実行環境が帰属行(`Co-Authored-By:` など)を足すときは、空行を挟まず同じ段落に続ける | -| `apply.md` / `fix.md` / `final-fix.md` | 作業段階が進むたびに `$RF_STEM-progress.log` へ 1 行追記する(`start` / `edit` / `test` / `commit` / `done` と対象だけ。推論は書かない)。形は cross-review の `launch-reviewer.sh` の「進捗マーカー」に揃える | - -## 処理の流れ - -### `next-apply-round` - -```mermaid -graph TD - S[群を順に見る] --> P{status が pending か
applied の群がある} - P -->|無い| E1[終了コード 1] - P -->|ある| K{status} - K -->|applied| O[開き直す
起点・試行番号を動かさない] - K -->|pending| EM{items が空} - EM -->|はい| D[dropped
drop_reason: empty] --> S - EM -->|いいえ| Q{失敗の記録の数 ==
attempt} - Q -->|はい| INC[attempt を 1 進め
起点を HEAD にする] - Q -->|いいえ・再開| OUT[APPLY_ROUND / IMPL を出す] - INC --> OUT - O --> OUT -``` - -再開で試行番号を進めないときも、`apply_round` と `apply_base_sha` は群の値から書き直す。 - -### `merge-apply` - -```mermaid -graph TD - B[置き土産を捨てる / 取り消しと push の再開] --> G{取り込み済みか} - G -->|採用あり| R0[終了コード 0] - G -->|採用 0 件| DR[群が dropped でなければ dropped] --> R2[終了コード 2] - G -->|未取り込み| DUP{同じ attempt の失敗の記録がある} - DUP -->|はい| R2 - DUP -->|いいえ| BL{着手前テストが green} - BL -->|いいえ| A4[終了コード 4] - BL -->|はい| RG{範囲を確定できる} - RG -->|いいえ| A4 - RG -->|はい| LD{load_result が読めた} - LD -->|はい| V[既存の検証と取り込み] - LD -->|いいえ| CF[_close_failed_attempt] --> R2 -``` - -`_close_failed_attempt` は次の順で行う。 - -1. 範囲にコミットがあれば全範囲を新しい順に取り消し、群と記録の起点を取り消し後の HEAD にする(push の印を先に立てる) -2. `failed_attempts` へ `{attempt, impl, reason: _monitor_reason(...), at, reverted}` を足す(`attempt` が 0 なら 1 として記録する) -3. `attempt < MAX_APPLY_ATTEMPTS` なら、`apply_seq` を 1 ずつ進めて `rounds.impl_for_seq` を引き、`failed_attempts[].impl` のどれとも違う担当が出たらその担当と要求モデルで `impl` / `impl_model` を書き換える。4 回進めても出なければ(除外で候補が 1 者しかない)、手順 4 と同じく群を取り消す -4. `attempt >= MAX_APPLY_ATTEMPTS` なら群の項目を `abandoned` にして `deferred_items` へ入れる。群は `dropped`(`drop_reason: no_result`)にし、`apply.merged_at` を立て、局面を `phase_after_group` にする -5. 保存し、取り消しがあったときだけ push する(`push_with_retry_marker`) - -### `merge-fix` - -担当は `current_group(entry)["impl"]`(無ければ `entry["impl"]`)から読む。`fix_merged_keys` に `":missing"` が -あれば、結果ファイルを読まずに 2 を返す(`merge-apply` の決定 5 と同じ理由。欠落の後に遅れて書かれた結果を取り込まない)。 -無ければ `load_result` を呼び、読めなければ範囲(`fix_base_sha`..HEAD)にコミットがあるときだけ `revert_unverified_range` で -取り消し、鍵を足し、`fix_rounds` を 1 進めて保存し、2 を返す。範囲を確定できないときの既存の扱い(`_resolve_fix_range`)と同じ形である。 - -## 非機能の実現方式 - -| 大項目 | 要求の条件 | 実現方式 | 確かめ方 | -| --- | --- | --- | --- | -| 性能・拡張性 | 中断と再開を挟まない実行で、適用担当の起動が群の数 × 2 回、修正担当の起動が群の数 × `--max-fix-rounds` 回を超えない | 試行番号の上限(決定 2・5)と、結果が無い修正でも `fix_rounds` を進めること | AC3(開く回数)と AC18(`should-abandon` が上限で 0) | -| 運用・保守性 | 群を取り消した理由が改修計画の「見送った項目」から読める | `defer_reason` に担当と理由の名前を並べる。`plan.py` の表は `defer_reason` をそのまま出す | AC2 で `defer_reason` に `agy` と理由の名前が入ることを見る | - -## 他の設計との契約 - -| 相手 | 契約 | D-C の扱い | -| --- | --- | --- | -| D-A(P1) | 監視が担当ごとの結果を `-monitor.json` へ残し、`monitor_outcome.read_outcome(tmp_dir, stem)` で読める(D-A の契約の文書) | `_monitor_reason` だけが `read_outcome(tmp_dir, "-apply-r")` の `reason` を読む。`ok` / `missing` / ファイルなしは `load_result` の問題へ置き換える(D-A の AC55 と同じ規則。壊れた JSON は監視から見ると `ok`)。振る舞いは変えない。追記だけの `monitor-outcomes.jsonl` は読まない | -| D-A(P3) | 理由の名前は #619 の語彙を基本とし、claude の `"api_error_status":429` を `usage_limit` として早期に打ち切る | 名前を解釈しない。記録へ写すだけ | -| D-A(P1 の計測) | 工程の所要は監視の記録と `refactor.py` の `statefile.save` の差し込み口から組み立て、骨組みと `refactor_lib` の取り込みには足さない(D-A の決定 9) | 計測の呼び出しを置かない。`refactor.py` の差し込み口と `commands/report.py` の要約の行は D-A が足すため、P6 はその上に載せる。**要約の `apply_attempts` は、鍵 `"r<ラウンド>-g<群>"` → `{"attempts": , "failed": , "dropped_reason": }` を状態ファイルの群から作る**(D-A の AC14 の「群ごとの試行回数」の出どころ。監視の記録からは作らない)ことを前提 1 に含める | -| D-A(P2) | 監視の上限は `--phase` と `lib/limits.py` が工程で決め、cross-refactoring の 8 か所は `--timeout` を渡さない(D-A の AC37)。修正と最終ゲートの修正の 420 秒の打ち切りも P2 が直す。8 か所の引数は P2 が変え、D-C は上限の値を書かない(D-A の申し送り) | `--timeout` を足さず、`--phase` が `apply` / `fix` / `final-fix` の呼び出しに `--stall-timeout` だけを足す。**これは申し送りと D-A の決定 10(無進捗の許容は担当の軸だけで決め、既定値は `limits.py` の表だけが持つ)に対する例外である。** 工程(テスト 1 回を含む適用・修正)に依存する許容を、D-C が骨組みの引数で渡す。D-A の申し送りの行と決定 10 へ同じ例外を書き足すことを前提 1 に含める。許容が監視の上限以上になる組は P2 の警告(D-A の AC33)に任せる | -| D-A(全体) | 監視の終了コードの意味(2 / 3 / 4 / 5 / 6)と `--stall-timeout` の引数は変えない | 骨組みは終了コードで分岐しない(決定 4) | -| D-B(P4 / P5) | 除外(#478)は `assignment.assign` か、その呼び出し側に入る | 適用・交代・最終ゲートの修正の担当を決める呼び出しは `rounds.impl_for_seq` の 1 か所(決定 8)。D-B が先に入れば P6 がそこへ寄せ、P6 が先なら D-B がそこを変える。`setup.py:467` の提案ラウンドの担当は CLI を起動しないため対象外 | - -## テスト設計 - -実行は `uv run --with pytest pytest plugins/ndf/skills/cross-refactoring/tests -q`。 -状態の遷移は関数を直接呼ぶ既存の形(`no_git` / `patch_lib` / `git_facts`)、トレーラーは一時リポジトリで実際に git を実行する。 - -| 受け入れ条件 | 何で確かめるか | 置き場所 | -| --- | --- | --- | -| AC1、AC2、AC4、AC5 | 群 2 つの状態で結果ファイルを置かずに(AC4 は壊れた JSON と配列で)呼び、`status` / `impl` / `failed_attempts` / `apply_seq` / `deferred_items` を見る。AC5 は群 4 つ(`apply_seq` 4)で先頭の群を失敗させる | `test_apply_attempts.py` | -| AC3 | `next-apply-round` が 1 を返すまで繰り返し、呼び出し回数 5 と両群の `dropped` | `test_apply_attempts.py` | -| AC6、AC7 | `merge-apply` の 2 度呼び、`next-apply-round` の 2 度呼びで、記録の件数と `attempt` が変わらない。AC6 は替えた先の担当の古い結果ファイルを置いた状態でも確かめる | `test_apply_attempts.py` | -| AC8 | `commits_in_range` が 1 件返す状態で、`no_git` の記録に `git revert` が出て、群の `base_sha` が変わる | `test_apply_attempts.py` | -| AC9、AC10 | 着手前テスト `red`、`commits_in_range` が `None` で、`SystemExit` の値が 4 | `test_apply_attempts.py` | -| AC11 | 4 つの経路でパラメータ化し、終了コード 2 の後の群が `dropped` か失敗の記録付き `pending` | `test_apply_attempts.py` | -| AC12 | `-apply-r-monitor.json` に `reason: stalled` を置いた場合、ファイルが無い場合、`reason: ok` で結果ファイルが壊れた JSON の場合 | `test_apply_attempts.py` | -| AC13 | テスト整備ラウンドの採用 0 件から `merge-proposals` → `next-apply-round` が 1、`apply_rounds == []` | `test_apply_rounds.py` | -| AC14 | `apply_rounds` の鍵を消した状態で群が 1 つ作られる(既存のテストを確かめ直す) | `test_apply_rounds.py` | -| AC15 | rf587 の形の状態で `merge-apply` → 2、群 `dropped`、`next-apply-round` → 1 | `test_apply_rounds.py` | -| AC16 | `items: []` の `pending` の群と、項目のある群を並べて `next-apply-round` | `test_apply_attempts.py` | -| AC17〜AC19 | 群の担当と提案ラウンドの担当を分けた状態で `merge-fix`。AC18 は `should-abandon` まで回す | `test_abandon_items.py` | -| AC20〜AC24 | 一時リポジトリで各形のコミットを作り、`commit_trailers` の戻り値を比べる | `test_commit_trailers_git.py` | -| AC25 | 同じ一時リポジトリで `collect_commit_facts` → `verify_apply_round` が `None` | `test_commit_trailers_git.py` | -| AC26 | 同じ一時リポジトリで、題名が `Round: …` の形で本文がトレーラーの段落だけのコミットを作り、`Round` が題名の値にならない | `test_commit_trailers_git.py` | -| AC27、AC30 | 雛形の文言を `grep` で探す | `test_skill_terms.py` | -| AC28 | `_emit_init` の出力に `IMPL_STALL_TIMEOUT=1800`(`test_timeout` 900 の状態) | `test_init.py` | -| AC29、AC31 | `SKILL.md` の骨組みから `monitor.py` の呼び出し 4 つを取り出し、`--phase` が `apply` / `fix` / `final-fix` の 3 つが `--stall-timeout "$IMPL_STALL_TIMEOUT"` を持ち、`--timeout` を持たない。語の表の行と段落の有無 | `test_skill_terms.py` | -| AC32 | `docs/02-apply-and-review.md` の Step 4 と `docs/04-fix-and-report.md` の Step 6 の `monitor.py` の引数(`--phase` と `--stall-timeout`、`--timeout` なし)が、`SKILL.md` の同じ呼び出しと一致する | `test_skill_terms.py` | -| AC33 | トレーラーの節の記載をレビューで見る | 手動 | -| AC34 | 既存の `test_a_verified_apply_round_marks_every_item_applied` に `failed_attempts` が無いことを足す | `test_merge_apply.py` | -| AC35、AC36 | 全体のテスト、`bash scripts/build-runtime-plugins.sh --check`、`claude plugin validate .`、`python3 scripts/check-skill-frontmatter.py` | 手動 | -| AC37 | `impl_for_seq` を差し替え、群の割り当て・担当の交代・最終ゲートの修正担当の 3 つが差し替えた担当になる | `test_final_fix.py` / `test_apply_attempts.py` | - -## 未確認のまま残ること - -| # | 項目 | 内容 | 決める時点 | -| --- | --- | --- | --- | -| 1 | rf646 で agy が打ち切りの時点で動いていたか | ログは作業ツリーとともに消えていた(`find / -name 'agy-apply-r*'` で該当なし)。決定 13 は動いていた場合も止まっていた場合も成り立つ | 次の実行で `progress.log` を読む | -| 2 | 担当が雛形の進捗マーカーに従うか | cross-review の agy は従っている(heartbeat に作業段階が出る)。claude / codex / kiro の適用で従うかは起動して確かめていない。従わなくても決定 13 の許容で打ち切られないのはテスト 1 回分まで | 実装後の最初の実行 | -| 3 | 担当を替えた後の担当も結果を残さない割合 | 2 回目で救える群の数は測っていない。実行の要約の `apply_attempts`(群ごとの `attempt` と `failed_attempts` の件数)で数える | 配布後 | -| 4 | Claude Code 以外の担当が帰属行を足すか | codex / agy / kiro のコミットで帰属行の段落を見ていない。決定 11 は誰が足しても同じに読む | 確かめなくてよい | -| 5 | 帰属行を同じ段落に続ける指示に claude が従うか | 決定 12 は補助で、従わなくても決定 11 で検証は通る | 実装後の最初の実行 | -| 6 | `--test-timeout` が監視の上限 − 900(既定 2700)以上の利用 | 適用・修正の所要を測っていない。#662 の計測で工程ごとの所要が取れたら、P2 の上限の表と合わせて見直す | 配布後 | diff --git a/issues/issue-647-592-553-requirements.md b/issues/issue-647-592-553-requirements.md deleted file mode 100644 index 42ca0c45a..000000000 --- a/issues/issue-647-592-553-requirements.md +++ /dev/null @@ -1,264 +0,0 @@ -# #647 / #592 / #553: cross-refactoring の適用ラウンドを有限回で終わらせ、帰属行の後ろでもトレーラーを読む - -設計は [issue-647-592-553-design.md](issue-647-592-553-design.md) にある。この文書は「何を満たすか」だけを扱う。 - -**この文書は置き換えられた。** 親 #728(取り込みが結果なしを値で受け、取り消しと群の状態を 1 か所で決める)の要求 [issue-728-647-592-553-requirements.md](issue-728-647-592-553-requirements.md) と設計 [issue-728-647-592-553-design.md](issue-728-647-592-553-design.md) が受け入れ条件と決定を持つ。対応は新しい文書の末尾の表にある。以下は 2026-09-15 時点の記録として残す。 - -**3 つの課題は 1 本の Pull Request(P6)で直す。** #647 と #592 は入口が違うが、同じ -`next-apply-round` → `merge-apply` の繰り返しが止まらない。#553 は同じ適用の検証で群を落とす。 -マイルストーン 21 の実装順では P3(監視の結果の分類)の後に入る。 - -## 目的 - -- 実装担当が結果を残さなくても、適用ラウンドの繰り返しが有限回で終わり、後ろの群と収束の判定へ届く -- テスト整備ラウンドの採用が 0 件でも、項目の無い群を開かずに提案ラウンドへ進む -- 修正ラウンドも、結果を残さない担当で上限なしに繰り返さない -- Claude Code が帰属行を別の段落で足しても、必須トレーラーが読めて群が落ちない - -## 対象範囲 - -含む: - -- `merge-apply` が取り込みの前に抜ける 3 か所の扱い(#647) -- 同じ群の試行の上限と、2 回目の試行で担当を替えること(#647) -- 採用 0 件の提案ラウンドで群を作らないこと(#592) -- `merge-fix` が読む結果ファイルの担当と、結果が無いときの修正ラウンドの数え方(範囲内へ入れた。下の「範囲へ入れたもの」) -- `commit_trailers` の読み方と、適用・修正の雛形のコミットの規約(#553) -- 適用・修正・最終ゲートの修正の監視に渡す無進捗の許容と、雛形の進捗マーカー(#647 の STALLED 対策) -- `SKILL.md` の「この Skill で使う語」「適用ラウンドに別の上限を置かない」「実行」の骨組み -- `docs/02-apply-and-review.md` と `docs/04-fix-and-report.md` の対応箇所 - -含まない: - -| 扱わないもの | 理由 | -| --- | --- | -| `plugins/ndf/scripts/lib/monitor.py` の変更(429 の検知・理由の語彙・結果の記録) | D-A(#662 #619 #584 #583)が所有する。D-C は消費する側 | -| `plugins/ndf/scripts/lib/assignment.py` と除外の引数 | D-B(#624 #478 #648)が所有する | -| 工程ごとの所要時間の計測 | D-A(P1)が監視の記録と `statefile.save` の差し込み口から組み立てる。骨組みと `refactor_lib` の取り込みには足さない | -| 利用上限(429)で進行全体を止めること | 担当を替えて 1 回だけ再試行する扱いに含める(設計文書の決定 3) | -| 取り込み済みの群を開き直したときに適用担当を起動し直す無駄 | 回数は 1 回で有限。繰り返しにはならない | -| 骨組みの bash を `scripts/` へ出すこと | #560 | -| 最終ゲートの修正で担当が結果を残さないときのコミットの扱い | 繰り返しは `--max-fix-rounds` で既に止まり、残るのは未検証のコミットの扱いで主題が違う(#674) | -| `cross-review` 側の監視と担当 | 別の設計(D-A / D-B) | -| `CHANGELOG.md` と版数 | 配布の工程が書く | -| クラス図 | 型を変えない。変えるのは関数と、状態ファイルの辞書の項目だけで、群の項目は設計文書の「データ構造」が持つ | - -### 範囲へ入れたもの - -**`merge-fix` の担当の食い違い**(起票せず範囲内へ入れた。`out-of-scope` の 3 択の「範囲内へ入れる」)。 -`cmd_merge_fix` は提案ラウンドの担当(`entry["impl"]`)の結果ファイルを読むが、起動するのは群の担当 -(`next-apply-round` が返す `IMPL`)である。群の担当は群ごとの輪番で決まるため、2 つが一致しない群では -修正の結果を一度も取り込めない。`merge-fix` は結果が無いと `fix_rounds` を進めずに終了コード 2 で抜ける。 -骨組みは終了コードを見ないため、検証 → 修正 → 検証が上限なしに回る。一時テストで再現した。 -群の担当 agy・提案ラウンドの担当 codex で `merge-fix` を 3 回呼ぶと、3 回とも -`codex の結果ファイルがありません` で、`fix_rounds` は 0 のままだった。**#647 と同じ「結果を取り込めない -担当で繰り返しが止まらない」形で、適用の側だけ直すと修正の側が残る。** - -## 受け入れ条件(#647: 結果を残さない担当) - -試行の上限と担当の交代: - -- [ ] AC1: 群が 2 つ(1 つ目の担当 agy、2 つ目の担当 codex)の状態で、1 つ目の結果ファイルを置かずに - `next-apply-round` → `merge-apply` を呼ぶ。`merge-apply` は終了コード 2 で終わる。1 つ目の群は - `status: pending` のまま担当が agy 以外に替わり、失敗した試行の記録が 1 件(試行番号 1・担当 agy・理由)残る -- [ ] AC2: AC1 の後、替わった担当の結果ファイルも置かずにもう一度 `next-apply-round` → `merge-apply` を呼ぶ。 - 1 つ目の群は `status: dropped` になり、群の項目は `abandoned` になり、`deferred_items` に理由付きで入る -- [ ] AC3: 結果ファイルを 1 つも置かずに、`next-apply-round` が終了コード 1 を返すまで `next-apply-round` → - `merge-apply` を繰り返す。`next-apply-round` の呼び出しは 5 回(開く 4 回 + 尽きた 1 回)で終わり、 - 両方の群が `dropped` になる -- [ ] AC4: 結果ファイルが JSON として読めない場合と、JSON の配列の場合も、AC1 と同じ状態になる -- [ ] AC5: 群が 4 つ(`apply_seq` 4)あり、先頭の群(担当 codex)が結果を残さない。担当を替えた後の担当は codex - 以外で、`apply_seq` は進めた分だけ進み、他の群の担当は変わらない - -叩き直しと中断からの再開: - -- [ ] AC6: AC1 の直後に、`next-apply-round` を挟まず `merge-apply` をもう一度呼ぶ。替えた先の担当の結果ファイルが - 同じ提案ラウンドの先行の群のものとして残っていても、終了コード 2 で終わる。失敗した試行の記録は 1 件のまま、 - 担当と `apply_seq` も変わらない -- [ ] AC7: `next-apply-round` を `merge-apply` を挟まず 2 回呼ぶ(取り込みの前に進行が止まった再開)。 - 群の試行番号は 1 のまま進まない - -結果を残さない試行のコミット: - -- [ ] AC8: 結果ファイルが無く、群の起点から HEAD までにコミットが 1 件以上ある。`merge-apply` の後、 - そのコミットは取り消され、群の起点は取り消し後の HEAD になる(次に開いたときの範囲へ入らない) - -取り込みの前の他の 2 か所: - -- [ ] AC9: 着手前のテストの状態が `green` でない状態で `merge-apply` を呼ぶと、終了コード 4 で終わる -- [ ] AC10: 適用の範囲を確定できない(起点が無い、または git が範囲を返さない)状態で `merge-apply` を - 呼ぶと、終了コード 4 で終わる - -繰り返しが有限であること: - -- [ ] AC11: `merge-apply` が終了コード 2 で終わった後の群は、`dropped` か、失敗した試行の記録を持つ - `pending` のどちらかである。確かめる経路は 4 つ(結果を残さない / 未割当のコミット / 適用の検証の失敗 / - 取り込み済みで採用 0 件) - -理由の記録: - -- [ ] AC12: 監視の結果ファイル(D-A が P1 で足す)がその担当・その段の理由を持つとき、失敗した試行の記録と - 見送りの理由にその理由の名前(例 `stalled`)が入る。ファイルが無いときは `missing` が入る。監視の理由が `ok` で - 結果ファイルが壊れた JSON のときは `unparsable` が入り、`ok` は入らない - -## 受け入れ条件(#592: 採用 0 件と項目の無い群) - -- [ ] AC13: テスト整備ラウンドで提案が 0 件の状態で `merge-proposals` を呼んだ後、`next-apply-round` を - 呼ぶ。1 回目で終了コード 1 を返し、そのラウンドの `apply_rounds` は空の配列のままである -- [ ] AC14: `apply_rounds` の鍵を持たない状態ファイル(群を導入する前の版)では、`next-apply-round` が - 従来どおりラウンド全体を 1 つの群として開く -- [ ] AC15: rf587 で残った形の群で `merge-apply` を呼ぶと、終了コード 2 で終わり、群が `dropped` になる。 - 形は `status: applied`・`items: []`・`apply.merged_at` あり・`applied: []` である。続く `next-apply-round` は - 終了コード 1 を返す -- [ ] AC16: `status: pending`、`items: []` の群を持つ状態で `next-apply-round` を呼ぶ。その群は開かれずに - `dropped` になり、次の群があればそれを開き、無ければ終了コード 1 を返す - -## 受け入れ条件(範囲へ入れたもの: 修正ラウンド) - -- [ ] AC17: 群の担当が agy、提案ラウンドの担当が codex の状態で、`agy-fix-r1-result.json` を置いて - `merge-fix` を呼ぶ。agy の結果が取り込まれ、`fix_rounds` が 1 になる -- [ ] AC18: 修正の結果ファイルが無い状態で `merge-fix` を呼ぶと、終了コード 2 で終わり、`fix_rounds` が - 1 進む。`--max-fix-rounds` 回続けた後の `should-abandon` は終了コード 0(見送りへ移る)を返す -- [ ] AC19: AC18 の直後に、`verify-round` を挟まず `merge-fix` をもう一度呼んでも、`fix_rounds` は進まない。その間に修正の結果ファイルが現れても進まない - -## 受け入れ条件(#553: 帰属行の後ろのトレーラー) - -一時リポジトリで実際にコミットを作って確かめる: - -- [ ] AC20: 必須トレーラー 4 つの段落の後に、空行を挟んで `Co-Authored-By:` の段落が付いたコミットで、 - `commit_trailers` が 4 つとも値を返す -- [ ] AC21: AC20 の段落の後に `Co-Authored-By:` と `Claude-Session:` の 2 行の段落が付いても、4 つとも返す -- [ ] AC22: 必須トレーラーの段落と末尾の段落の間に散文の段落があるコミットで、散文より前にある - `Round: …` の形の行を読まない -- [ ] AC23: 末尾の段落に散文とトレーラーの形の行が混ざる(git がトレーラーの段落と判定しない)コミットで、 - その行を読まない -- [ ] AC24: 同じ鍵が 2 つの段落にあるとき、末尾に近い段落の値を返す -- [ ] AC25: AC20 の形のコミットを申告した適用ラウンドが、トレーラーの欠落で取り消されない -- [ ] AC26: 本文がトレーラーの段落 1 つだけで、題名が `Round: 本文の題名` の形のコミットで、題名を読まない - -雛形: - -- [ ] AC27: `prompts/apply.md` と `prompts/fix.md` のコミットの規約が、必須トレーラーをメッセージの - 最後の段落に置くことを書く - -## 受け入れ条件(無進捗の打ち切り) - -- [ ] AC28: `init` の出力に `IMPL_STALL_TIMEOUT` が入り、値が `--test-timeout` の値 + 900 である - (既定で 1800)。`--test-timeout` は `apply` / `fix` / `final-fix` の監視の上限 − 900 未満(P2 の既定なら 2700 未満)を前提とする -- [ ] AC29: `SKILL.md` の骨組みで、`--phase apply` / `fix` / `final-fix` の 3 つの `monitor.py` の呼び出しが - `--stall-timeout "$IMPL_STALL_TIMEOUT"` を持ち、`--timeout` を持たない -- [ ] AC30: 適用・修正・最終ゲートの修正の雛形(`prompts/apply.md` / `fix.md` / `final-fix.md`)が進捗マーカーを - 書く。作業段階ごとに `$RF_STEM-progress.log` へ 1 行追記する指示である - -## 受け入れ条件(文書) - -- [ ] AC31: `SKILL.md` の「この Skill で使う語」の適用ラウンドの行が、上限を決めるものとして同じ群の試行の - 上限(2 回)を書く。「適用ラウンドに別の上限を置かない」の段落は無くなる。 - `grep -n "別の上限を置かない\|別に置かない" SKILL.md` が何も出力しない -- [ ] AC32: `docs/02-apply-and-review.md` の Step 4 と `docs/04-fix-and-report.md` の Step 6 の骨組みが、`SKILL.md` の - 同じ呼び出しと同じ `monitor.py` の引数を持つ(`--phase` と `--stall-timeout "$IMPL_STALL_TIMEOUT"` を持ち、`--timeout` を持たない) -- [ ] AC33: `docs/02-apply-and-review.md` のトレーラーの節が 2 つを書く。`git log --format='%(trailers:…)'` が - 最後の段落しか読まないことと、進行側の読み方である - -## 受け入れ条件(退行しない) - -- [ ] AC34: 結果ファイルがあり検証を通る適用ラウンドは、変更前と同じく 1 回目の試行で取り込まれ、 - 失敗した試行の記録を持たない -- [ ] AC35: `uv run --with pytest pytest scripts/tests plugins/ndf -q` が通る -- [ ] AC36: 配布物の同期・定義・frontmatter の 3 つの検査が終了コード 0 で終わる(コマンドは「検証手段」の表) - -## 受け入れ条件(他の設計との契約) - -- [ ] AC37: `rounds.impl_for_seq` を差し替えると、群を割り当てたときの担当・結果を残さなかった群の交代先・最終ゲートの - 修正担当の 3 つが、差し替えた関数の返す担当になる - -## 非機能の条件 - -| 大項目 | 条件 | -| --- | --- | -| 性能・拡張性 | 中断と再開を挟まない実行で、1 つの提案ラウンドで適用担当を起動する回数が群の数 × 2 回を超えない。修正担当を起動する回数も群の数 × `--max-fix-rounds` 回を超えない。取り込み済みの群の開き直しと、`merge-apply` の前に止まった再開は、その回数だけ起動が増える | -| 運用・保守性 | 群を取り消した理由(担当 2 者と、監視の理由の名前)が、改修計画の「見送った項目」の表から読める | - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| `merge-apply` の終了コード | 着手前テストの未確認と範囲の未確定が 2 から 4(中断)へ変わる。`SKILL.md` の終了コードの表は既に 4 と書いている | -| 状態ファイル | 群に試行の記録が増える。既存の状態ファイルは記録が無いまま読める(試行 0 回として扱う) | -| 群の担当 | 結果を残さなかった群だけ、2 回目の試行で次の輪番の担当へ替わる | -| 無進捗の打ち切り | 適用・修正・最終ゲートの修正で、どの担当も既定の許容より長くなる(codex 180 秒 / agy・kiro 480 秒 / claude 900 秒 → 1800 秒) | -| トレーラーの読み取り | 最後の段落に加え、その直前に続くトレーラーの段落も読む。最終ゲートの修正にも同じ読み方が効く | - -## 前提 - -| # | 前提 | -| --- | --- | -| 1 | D-A の P1(監視の結果ファイルと `monitor_outcome.read_outcome`)、P2(`--phase` と `lib/limits.py` による監視の上限)、P3(理由の語彙)が先に `develop` へ入る。D-A の申し送り(cross-refactoring の監視の引数は P2 が変え、D-C は上限の値を書かない)と決定 10(無進捗の許容は担当の軸だけで決める)に、適用・修正・最終ゲートの修正の `--stall-timeout "$IMPL_STALL_TIMEOUT"` を D-C が渡す例外が書き足される。D-A の実行の要約の `apply_attempts` が、鍵 `"r<ラウンド>-g<群>"` → `{"attempts": 整数, "failed": 整数, "dropped_reason": 文字列または null}` を状態ファイルの群から作る。理由の名前は #619 の提案(`timeout` / `cli_timeout` / `usage_limit` / `early_error` / `stalled` / `not_posted` / `missing`)を基本とする | -| 2 | 監視の終了コード(2 = TIMEOUT / 3 = NO_RESULT / 4 = EARLY_ERROR / 5 = STALLED / 6 = PIDFILE_BAD)の意味は変わらない | -| 3 | `assignment.assign(seq, host)` の実装担当は 4 者の輪番で、`seq` を 1 ずつ進めれば 3 回以内に失敗した担当と別のランタイムが出る。D-B が除外を足した後も、除外されない者が 2 者以上いる | -| 4 | Claude Code が足す帰属行は、メッセージの末尾に独立した段落として付く(#553 の実測 `26a0fff`) | - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| テスト | `uv run --with pytest pytest scripts/tests plugins/ndf -q`(cross-refactoring だけなら `plugins/ndf/skills/cross-refactoring/tests`) | -| 配布物の同期 | `bash scripts/build-runtime-plugins.sh --check` | -| 定義の検査 | `claude plugin validate .` と `python3 scripts/check-skill-frontmatter.py` | -| 手動確認 | 次に cross-refactoring を回した実行で、`agy-apply-r*-progress.log` に作業段階が残るか(未確認のまま残ること 1) | - -## 前提とする取り決め - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | 状態の判定は `refactor_lib/` に置き、骨組みの bash は判定を持たない(`SKILL.md` の「実行」) | -| コーディング規約 | 外部コマンド(`git interpret-trailers`)の挙動は書く前に実行して確かめる(`AGENTS.md` の DO) | -| テスト戦略 | 状態の遷移は既存の形(`tests/conftest.py` の `no_git` / `patch_lib` と `test_merge_apply.py` の `git_facts`)で関数を直接呼ぶ。トレーラーの読み取りは一時リポジトリで実際に git を実行する | - -## 境界 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 既存テストの実行、配布物の同期の検査 | -| 確認してから行う | 同じ群の試行の上限を引数にすること(この変更では固定の 2 回) | -| 行わない | `monitor.py` / `assignment.py` の変更、骨組みを `scripts/` へ出すこと | - -## 用語 - -| 用語 | 意味 | -| --- | --- | -| 群 | 適用ラウンド。書き換えるファイルが重ならない項目の集まりで、状態の `apply_rounds[]` の 1 件 | -| 試行 | 1 つの群に対して適用担当を起動し、`merge-apply` で取り込もうとした 1 回 | -| 結果を残さない | 結果ファイルが無い、JSON として読めない、JSON オブジェクトでない、のいずれか | -| 監視の結果ファイル | D-A が P1 で足す `-monitor.json`。監視が担当 1 者ごとの状態と理由を残し、`monitor_outcome.read_outcome` で読む | -| 帰属行 | Claude Code がコミットメッセージへ足す `Co-Authored-By:` / `Claude-Session:` の行 | -| トレーラーの段落 | `git interpret-trailers --parse` がトレーラーとして読む段落 | - -## 依頼(原文) - -### #647 - -> `/ndf:cross-refactoring` で、同じ適用ラウンド(書き換えるファイルが重ならない改善項目の群)の適用が**上限なしに再試行される**。後ろの群へ順番が回らず、収束の判定と最終ゲートへ届かない。 -> -> - **同じ群の適用の試行に上限を置き**、超えたら項目単位で見送る(`abandon-items` と同じ扱い)。あわせて群の状態を進め、次の群と次の担当へ移る -> - `SKILL.md:49` / `:56` の「適用ラウンドに別の上限を置かない」を、開き直しの上限を持つ形へ改める -> - 骨組みで `monitor.py` の終了コードを受け取り、STALLED / TIMEOUT / EARLY_ERROR を区別して `merge-apply` へ渡すか、進行を止めて報告する -> - `EARLY_ERROR` のうち利用上限(429)は**再試行しても直らない**ため、その場で進行を止めて報告する。`EARLY_ERROR_FATAL` に claude の `api_error_status":429` の形を足す -> - cross-refactoring の適用・修正の雛形にも進捗マーカー(`$STEM-progress.log`)の指示を足すか、適用の監視に `--stall-timeout` を渡す。先に rf646 の agy のログで、打ち切りの時点で agy が動いていたかを確かめる - -### #592 - -> **テスト整備ラウンドの採用が 0 件のとき、項目の無い適用ラウンドが開き、上限なしに同じ群を繰り返す。** 手順書の骨組み(`SKILL.md` の「実行」)をそのまま回すと、提案ラウンドへ進まない。 - -### #553 - -> `cross-refactoring` の適用ラウンドで、**claude が実装担当のときだけ**必須トレーラー(`Item-Id` / `Round` / `Impl-Runtime` / `Impl-Model`)が読めず、群が丸ごと取り消される。 -> -> | 案 | 中身 | 気になる点 | -> | --- | --- | --- | -> | A | `commit_trailers()` を `git interpret-trailers --parse` へ変える | 同じく最終段落しか読まない。効かない | -> | B | メッセージ全文から `^: ` を正規表現で拾う | git のトレーラー定義から外れる。本文中の同名の行を拾いうる | -> | C | プロンプトへ「必須トレーラーを**最後の段落**に置く(帰属行より後ろ)」と書く | 実装担当の従い方に依存する | -> | D | 進行側が取り込みの直前に `git commit --amend` でトレーラーを足し直す | 実装担当のコミットを進行側が書き換える | - -(3 件の issue の本文から抜粋。全文は `gh issue view 647` / `gh issue view 592` / `gh issue view 553`) diff --git a/issues/issue-662-598-537-619-584-583-design-contracts.md b/issues/issue-662-598-537-619-584-583-design-contracts.md index 592dfda83..e8d6f928d 100644 --- a/issues/issue-662-598-537-619-584-583-design-contracts.md +++ b/issues/issue-662-598-537-619-584-583-design-contracts.md @@ -122,7 +122,7 @@ classDiagram ### 理由の語彙 -**P3 の語彙と「P3 で足す文言」は、結果なしの理由を共通層の 1 か所で読む設計 [issue-729-619-584-design.md](issue-729-619-584-design.md) の「理由の語彙」「データ構造」「入出力の契約」へ移した。** 以下は 2026-09-15 時点の記録として残す(`unparsable` の追加と起動し直しの可否は新しい設計だけが持つ)。 +**P3 の語彙と「P3 で足す文言」は、結果なしの理由を共通層の 1 か所で読む設計(#729)へ移した。確定仕様は [起動 1 回の結末](../docs/specifications/cross-review-launch-outcome.md) の「結末の語彙」「データ・設定」にある。** 以下は 2026-09-15 時点の記録として残す(`unparsable` の追加と起動し直しの可否は新しい設計だけが持つ)。 **監視が書く理由**(`monitor_outcome.REASONS`): @@ -372,4 +372,4 @@ conftest.py P1(NDF_METRICS_DIR) | AC68 / AC69 | 文書の `grep` | | AC70〜AC72 | 検証手段の表のコマンド。AC72 はテストの前後で `find` | -**AC63〜AC67 の確かめ方は、[issue-730-583-design.md](issue-730-583-design.md) の「テスト設計」が引き継いだ。** 担当が投稿しなくなるため、投稿済みのレビューを探す鍵(`prior_review_url`)を作らず、AC63〜AC65 の行は対象を失う。上の 2 行は 2026-09-15 時点の記録として残す。 +**AC63〜AC67 の確かめ方は、#730 の設計が引き継いだ。確定仕様は [書き込みを回す側へ集める仕様](../docs/specifications/cross-review-writes-to-conductor.md) の「テスト観点」にある。** 担当が投稿しなくなるため、投稿済みのレビューを探す鍵(`prior_review_url`)を作らず、AC63〜AC65 の行は対象を失う。上の 2 行は 2026-09-15 時点の記録として残す。 diff --git a/issues/issue-662-598-537-619-584-583-requirements.md b/issues/issue-662-598-537-619-584-583-requirements.md index fce9d2166..7d58b27c7 100644 --- a/issues/issue-662-598-537-619-584-583-requirements.md +++ b/issues/issue-662-598-537-619-584-583-requirements.md @@ -157,9 +157,9 @@ ## 受け入れ条件(P3: #619 + #584 + #583 結果が失われる) -**#619 / #584 の受け入れ条件(AC50〜AC62、AC68〜AC69)は、利用上限で止まった担当を起動し直さず理由を報告する要求 [issue-729-619-584-requirements.md](issue-729-619-584-requirements.md) へ移した。** 以下は 2026-09-15 時点の記録として残す。 +**#619 / #584 の受け入れ条件(AC50〜AC62、AC68〜AC69)は、利用上限で止まった担当を起動し直さず理由を報告する要求(#729)へ移した。確定仕様は [起動 1 回の結末](../docs/specifications/cross-review-launch-outcome.md) にある。** 以下は 2026-09-15 時点の記録として残す。 -**#583 の受け入れ条件(AC63〜AC67)は、GitHub と git への書き込みをレビューを回す側だけにする要求 [issue-730-583-requirements.md](issue-730-583-requirements.md) が引き継いだ。** そのうち AC63〜AC65 は取り下げ、AC66 は送信の応答から取る形へ改め、AC67 は引き継いでいる。対応は引き継いだ側の「置き換える既存の受け入れ条件との対応」にある。 +**#583 の受け入れ条件(AC63〜AC67)は、GitHub と git への書き込みをレビューを回す側だけにする要求(#730)が引き継いだ。** そのうち AC63〜AC65 は取り下げ、AC66 は送信の応答から取る形へ改め、AC67 は引き継いでいる。確定仕様は [書き込みを回す側へ集める仕様](../docs/specifications/cross-review-writes-to-conductor.md) にある。 文言と見るファイルの一覧は、契約の文書の「P3 で足す文言」にある。 diff --git a/issues/issue-664-p7-refactor-participants-plan.md b/issues/issue-664-p7-refactor-participants-plan.md deleted file mode 100644 index 02523452f..000000000 --- a/issues/issue-664-p7-refactor-participants-plan.md +++ /dev/null @@ -1,176 +0,0 @@ -# cross-refactoring: 担当を外す引数が無く、使える者が 2 者だとレビュー担当が 1 者になる → 使える者だけで始まり、参加者は codex / kiro とホストを既定に足し引きでき、適用の輪番はその参加者の中で回る(実装計画 P7: cross-refactoring と旧関数の削除 / #664 #736) - -## 関連リンク - -- 親 issue #727、子 issue #664。あわせて #736(リポジトリの根の `CLAUDE.md` の上限の既定の記述) -- 設計: [issue-727-687-478-664-648-design.md](issue-727-687-478-664-648-design.md)(決定 20 件。識別子は冒頭の用語の対応表で引く) -- 要求: [issue-727-687-478-664-648-requirements.md](issue-727-687-478-664-648-requirements.md)(受け入れ条件 AC1〜AC50) -- 契約: [issue-727-687-478-664-648-contracts.md](issue-727-687-478-664-648-contracts.md)(状態ファイル・引数・関数の形) -- 確定仕様: [cross-review-participants-and-seats.md](../docs/specifications/cross-review-participants-and-seats.md)(共通層と cross-review 側) -- 1 本目の実装: [issue-727-p6-participants-plan.md](issue-727-p6-participants-plan.md)(PR #793、マージ済み) - -## モード - -`standard`。収束ループの初期化と担当の決め方を変え、共通層と cross-refactoring と文書にまたがる。 - -## 目的と非目的 - -達成したい状態: - -- 参加する CLI のどれか 1 者が導入・認証されていなくても、cross-refactoring の初期化が使える者で始まる。使えない者と理由が状態ファイルに残る -- 既定の参加者が codex / kiro とホストになり、足す者・外す者の指定で名指しで変えられる。agy は足す者の指定で戻せる -- 適用の輪番が参加者の中で回る。存在しない役(レビュー担当)の記録と出力が消える -- 中断したループを引数を変えて再開すると、上限は反映され、反映しない引数は知らされる -- 使える者の決め方が共通層の 1 か所だけになり、両 Skill に古い形が残らない(親 #727 の完了条件) - -やらないこと: - -- cross-review 側の変更(1 本目で済んだ)。ただし旧関数に依存する cross-review のテスト 3 件は、関数を消すのに合わせて期待値を書き直す -- 起動した後に分かる使えなさで担当を自動的に外す仕組み(設計の決定 18) -- 引数の型の検査(カンマ区切りの名前と予約語)の共通層への移動。cross-review の状態の部品は並行する束(G5)が触っており、この Pull Request では cross-refactoring の側に同じ規則の型を置く -- 1 者指定を cross-refactoring に足すこと(契約の引数の表に無い) -- 指示書の cross-refactoring の節のうち、参加者と輪番と上限以外の古い行(コミットの単位・改修計画の置き場所・取り消しの単位)。#799 として起票した - -## 前提 - -- 前提 1: 設計文書の決定 20 件は変えない。実装で決めたことは「実装で決めたこと」に書き、設計文書の「未確認のまま残ること」に P7 の節を同じ Pull Request で足す -- 前提 2: 並行する束(G5、#730)は cross-review の投稿の部分と共通層の投稿の待ち行列を触る。旧関数を使う箇所が G5 のブランチに無いことを、消す前に `git grep` で確かめる(着手時点で `origin` に G5 の実装ブランチは無く、`develop` の呼び手はこの Pull Request が置き換える箇所だけだった) -- 前提 3: 新しい参加者(ホストが提案に入ること)の所要は測らない。設計の「未確認のまま残ること」のまま運用へ渡す - -## 受け入れ条件 - -要求文書の AC7、AC31〜AC43、AC47、AC49〜AC50 をそのまま使う。検証手段は設計文書の「テスト設計」の表にある。AC47(#664 の再現手順)は手元で実行し、結果を Pull Request 本文に残す。 - -## ドメイン用語 - -識別子は設計文書の用語の対応表と同じ。この計画で新しく使う語は次の 1 つだけである。 - -| 用語 | 意味 | -| --- | --- | -| 反映の表(cross-refactoring) | 再開で渡した引数ごとに「反映する」か「知らせる」かを決める表。cross-review の表と同じ形で、cross-refactoring の部品が持つ | - -## 不変条件 - -- 状態ファイルの参加者の一覧(`runtimes`)は、参加者の記録の使える者と同じ値である -- 新規の状態ファイルに適用専用の母集合の項目とラウンドのレビュー担当の項目が無い -- この変更の前に始めた実行の状態ファイルを、書き換えずに読める(適用の輪番は参加者の一覧から決まる) -- 使える者が 0 者、または全員を要する指定で欠けがあるとき、状態ファイルを作らない・書き換えない - -## 互換性 - -| 対象 | 変更 | 互換性の扱い | -| --- | --- | --- | -| 初期化の引数 | 足す者・外す者・全員を要する指定を足す。上限と重要度の既定を未指定にする | 追加のみ。未指定のときの既定値は変えない | -| 初期化の出力 | 適用専用の母集合の変数を出さない | 読み手は手順書の表だけ(骨組みは読まない)。手順書から消す | -| ラウンドの開始の出力 | レビュー担当の 2 変数を出さない | 読み手はテストの期待値だけ(設計文書の「実測」) | -| 状態ファイル | 参加者の記録と再開で変えた値の記録を足し、適用専用の母集合とレビュー担当を書かない | 古い状態ファイルは項目が無いまま読む。報告は「記録なし」と出す | -| 共通層の関数 | 従来の確認・適用専用の母集合・従来の席と適用の割り当ての 4 つを消す | 呼び手が 0 件になったことを `git grep` のテストで固定する | - -## 修正対象 - -```text -plugins/ndf/scripts/lib/assignment.py 旧関数 3 つと説明を消す -plugins/ndf/scripts/lib/auth.py 従来の確認を消す -plugins/ndf/scripts/tests/test_lib_assignment.py 旧関数のテストを消す -plugins/ndf/scripts/tests/test_auth_probe.py 従来の確認のテストを消す -plugins/ndf/scripts/tests/test_shared_lib_layout.py 旧関数が残らないことの検査を足す(AC7) -plugins/ndf/skills/cross-refactoring/scripts/refactor.py 引数の追加と既定の変更 -plugins/ndf/skills/cross-refactoring/scripts/refactor_lib/commands/setup.py 初期化・再開・ラウンドの開始 -plugins/ndf/skills/cross-refactoring/scripts/refactor_lib/rounds.py 適用の輪番の包み -plugins/ndf/skills/cross-refactoring/scripts/refactor_lib/commands/apply.py 担当の交代の上限 -plugins/ndf/skills/cross-refactoring/scripts/refactor_lib/commands/report.py 母集合の 1 行・参加者の節・列の削除 -plugins/ndf/skills/cross-refactoring/scripts/refactor_lib/plan.py 改修計画の見出しからレビュー担当を消す -plugins/ndf/skills/cross-refactoring/scripts/refactor_lib/gitfacts.py 観測したモデルの記録からレビュー側の枝を消す -plugins/ndf/skills/cross-refactoring/tests/ test_init.py / test_start_round_emits_runtimes.py / test_assignment.py ほか -plugins/ndf/skills/cross-review/tests/ 旧関数に依存する 3 件の期待値 -plugins/ndf/skills/cross-refactoring/SKILL.md / docs/01-state-and-propose.md -CLAUDE.md cross-refactoring の節と cross-review の節 -issues/issue-727-687-478-664-648-design.md 「未確認のまま残ること」に P7 の節 -``` - -## タスク分解 - -各タスクは失敗するテストを先に書き、通す最小の実装を足し、整える。旧関数の削除(Task 6)は呼び手をすべて置き換えた後に行う。 - -### Task 1: 新規の初期化を共通層の使える者の解決へ載せ替える - -- **対象ファイル:** `refactor.py`、`commands/setup.py`、`tests/test_init.py` -- **変更内容:** 母集合の既定(`refactor_pool`)と使える者の解決(`resolve_participants`)を止めない確認(`probe_auth`)で呼ぶ。状態ファイルへ参加者の一覧と参加者の記録と空の再開の記録を書き、適用専用の母集合を書かない・出さない。割り当ての失敗と 0 者を終了コード 4 へ写す。足す者・外す者・全員を要する指定の引数を足す -- **満たす受け入れ条件:** AC31、AC32、AC33、AC35、AC36、AC47 -- **進め方:** 確認を差し替えた初期化のテストを先に書く - -### Task 2: 適用の輪番を参加者の中で回し、レビュー担当を消す - -- **対象ファイル:** `rounds.py`、`commands/setup.py`、`commands/apply.py`、`gitfacts.py`、`tests/test_start_round_emits_runtimes.py`、`tests/test_apply_attempts.py` -- **変更内容:** 輪番の包み(`impl_for_seq`)の中身を適用の輪番(`impl_assign(seq, state["runtimes"])`)へ替え、ラウンドの開始もこれを使う。ラウンドの記録にレビュー担当を書かず、2 変数を出さない。担当の交代を試す回数を参加者の数にする -- **満たす受け入れ条件:** AC34、AC41 -- **進め方:** 出力と記録にレビュー担当が無いテストを先に書く(既存の期待値を反転する) - -### Task 3: 再開で上限を反映し、他の引数を知らせ、担当に関わる引数で参加者を作り直す - -- **対象ファイル:** `refactor.py`、`commands/setup.py`、`tests/test_init.py` -- **変更内容:** 状態に載る引数の既定を未指定にし、新規の経路で現行の既定へ置き換える。反映の表を置き、再開で共通層の再開の反映(`apply_resume_args`)を呼ぶ。足す者・外す者・全員を要する指定のどれかを渡した再開では、渡さなかった値を記録から補って作り直し、1 件として積む。失敗したら書き換えずに終了コード 4 -- **満たす受け入れ条件:** AC38、AC39、AC40 -- **進め方:** 状態ファイルを置いた作業ツリーで再開するテストを先に書く - -### Task 4: 報告と改修計画の表示を 1 つの母集合に揃える - -- **対象ファイル:** `commands/report.py`、`plan.py`、関連するテスト -- **変更内容:** 状態の表示の母集合を 1 行にし、ラウンド表からレビュー担当と初回承認の列を消す。完了報告に参加者の節を足す(参加者の記録が無ければ「記録なし」)。改修計画の見出しからレビュー担当を消す -- **満たす受け入れ条件:** AC37、AC41 -- **進め方:** 報告と改修計画の出力のテストを先に書く - -### Task 5: 手順書とリポジトリの根の指示書を実装に合わせる - -- **対象ファイル:** `SKILL.md`、`docs/01-state-and-propose.md`、`CLAUDE.md` -- **変更内容:** 担当の決め方を 1 つの表にし、引数の表と `argument-hint` に 3 つの引数を足し、前提の「すべてログイン済み」とホストごとの CLI の表を直す。`init` の変数の表から適用専用の母集合を消す。指示書の cross-refactoring の節を新しい母集合と輪番と上限の既定 3 に直し(#736)、cross-review の節を席の規則に直す -- **満たす受け入れ条件:** AC42、AC43 -- **進め方:** 文書の語の検査(既存のテスト)に期待を足してから直す - -### Task 6: 呼び手の無くなった旧関数を消す - -- **対象ファイル:** `assignment.py`、`auth.py`、共通層のテスト、cross-review と cross-refactoring の旧関数のテスト -- **変更内容:** 従来の確認・適用専用の母集合・従来の席と適用の割り当てを消す。消した関数を期待値に使っていたテストは、変更前の値を定数で持つ形へ直す。4 つの名前が共通層と両 Skill の部品に残らないことをテストで固定する -- **満たす受け入れ条件:** AC7 -- **進め方:** 残っていないことの検査を先に足す(失敗する)→ 消す - -### Task 7: 設計文書を更新し、配布物と検査を通す - -- **対象ファイル:** 設計文書、配布物 -- **変更内容:** 「未確認のまま残ること」に P7 で決めたことを足す。配布物を同期し、全体のテストと 6 つの検査を通す -- **満たす受け入れ条件:** AC49、AC50 -- **進め方:** テスト駆動の対象外(検査の実行) - -## 実装で決めたこと - -| 項目 | 決めたこと | -| --- | --- | -| 状態ファイルの確認の結果の項目(`auth`) | 新規の状態に書かない。読み手が無く、確認を通らなかった者と理由は参加者の記録(`participants.unavailable`)が持つ | -| 確認を行う位置 | 新規の経路で、作業ディレクトリの用意と範囲の関門の後、着手前のテストの前。状態ファイルの有無(新規か再開か)を見てから確かめるためで、再開では担当に関わる引数を渡したときだけ確かめる | -| 反映の表の中身 | 「反映する」は上限 4 つとテストの制限時間。「知らせる」はホスト・範囲・モデル・着手前のテスト・継続的統合の検査の名前・重要度の閾値・同期のコマンド・改修計画のファイル・起動のされ方・作業ディレクトリ root の 10 個。着手前のテストはコマンドで、モデルは全ランタイムの辞書で、作業ディレクトリ root は解決したパスで比べる | -| 引数の型の置き場所 | `--exclude` / `--include` の型(カンマ区切りの名前と予約語 `none`)は cross-refactoring の初期化の部品に置く。共通層へ移すと cross-review の状態の部品も触ることになり、並行する束(G5)と重なる | -| 完了報告の参加者の節 | cross-review と同じ行の形にし、席の埋め合わせの行は持たない(cross-refactoring に席は無い)。ラウンド表からはレビュー担当とモデルの列に加え、レビュー担当の判定から作っていた初回承認の列も消す | -| モデルの警告の対象 | 参加者だけ。既定で外れる agy は、足したときだけ警告する | - -## 影響範囲 - -- cross-refactoring の起動する CLI の集合が変わる(既定で agy が外れ、ホストが提案に入る) -- 担当名の読み手(監視・起動)は参加者の一覧をそのまま使うため変わらない -- 指標の集計(共通層の `metrics.py`)は、古い状態ファイルのレビュー担当を読む経路を残す - -## リスクと対処 - -| リスク | 対処 | -| --- | --- | -| 初期化の関数(`cmd_init`)は新規と再開の 2 経路と確認を 1 関数に持ち、再開の反映を足すと長くなる | タスクごとにテストを通す。再開の経路は別の関数に出す | -| 旧関数を消す時点で、並行する束が新しい呼び手を足している | 消す直前に `develop` と並行する束のブランチを `git grep` で確かめる。消した後の検査テストが継続的統合で拾う | -| 期待値に旧関数を使うテストの意味が変わる | 変更前の値を定数として持ち、テストの主張(3 者のときの席は変更前と一致する)を保つ | - -## 切り戻し手順 - -- この Pull Request を revert すれば元へ戻る。データ移行は無い。新しい形で作った状態ファイルは、戻した後の版では適用専用の母集合が無いため、実行の途中で戻すなら状態ファイルを消して最初から始める - -## 完了の定義 - -- [ ] 上の受け入れ条件をすべて満たし、条件ごとに検証手段と結果が対応している -- [ ] 全体のテスト、配布物の同期の検査、6 つの検査が終了コード 0 で終わる diff --git a/issues/issue-678-requirements.md b/issues/issue-678-requirements.md deleted file mode 100644 index b91ae13bf..000000000 --- a/issues/issue-678-requirements.md +++ /dev/null @@ -1,89 +0,0 @@ -# テストの前提: 監視の上限を環境変数で延ばしたシェルでは既定値を前提にするテストが落ち、収束ループの初期化が中断する → テストの実行中は監視の環境変数を共通の前提で外す(#678) - -## 目的 - -無進捗の許容と打ち切りの上限は、環境変数で延ばせる。運用でこれを延ばしたシェルから全体の -テストを起動すると、既定値を前提にするテストが落ちる。落ちた原因はテストの実行環境にあり、 -変更の中身にはない。 - -収束ループ(`cross-refactoring`)の初期化は着手前のテストの通過を条件にするため、上限を -延ばしたシェルでは初期化がそこで止まる。 - -**テストの実行中だけ、監視の環境変数を利用者の環境から切り離す。** 切り離しの置き場所は -リポジトリ直下の共通の前提(`conftest.py`)とし、テストごとに散った除去をそこへ寄せる。 - -## 対象範囲 - -**含む** - -- リポジトリ直下の共通の前提へ、監視の環境変数(接頭辞 `MONITOR_`)を外す仕組みを足す -- テストごとに散った同じ除去を取り除く(1 変数ずつ外す箇所と、接頭辞でまとめて外す箇所) -- 共通の前提が働いていることを確かめるテストを足す - -**含まない** - -- 上限の解決順(担当ごとの指定 → 共通の指定 → 表の既定)の変更 -- 上限の表の値の変更 -- 監視の本体・起動スクリプト・Skill 本文の変更 - -## 前提 - -- 本番の振る舞いも本番コードの構造も変えない。変えるのはテストの前提だけである -- 子プロセスは実行中の環境変数を受け継ぐため、共通の前提で外せば、テストが起動する - 別プロセスにも同じ切り離しが効く -- テストの中で監視の環境変数を設定する箇所(`monkeypatch.setenv`・別プロセスへ渡す - 上書き)は、共通の前提より後に効くため、そのまま働く - -## 着手前の実測(2026-09-21) - -同じコマンドを、監視の環境変数を設定したシェルと、していないシェルで実行した。 - -```console -$ MONITOR_TIMEOUT_AGY=1800 MONITOR_STALL_AGY=1800 uv run --with pytest pytest scripts/tests plugins/ndf -q -FAILED plugins/ndf/skills/cross-review/tests/test_launch_agy.py::test_the_print_timeout_defaults_to_the_longest_phase -FAILED plugins/ndf/skills/cross-review/tests/test_monitor_agy.py::test_the_new_name_has_a_stall_default -FAILED plugins/ndf/skills/cross-review/tests/test_monitor_import_safety.py::test_import_succeeds_with_non_numeric_monitor_stall -3 failed, 4600 passed in 182.10s - -$ uv run --with pytest pytest scripts/tests plugins/ndf -q -4603 passed in 179.67s -``` - -| 観測 | 値 | -| --- | --- | -| 収集した件数 | 4603(どちらのシェルでも同じ) | -| 設定したシェルで落ちる件数 | 3 | -| 設定していないシェルで落ちる件数 | 0 | - -課題の本文が記録した 2026-09-15 の観測では落ちるのが 2 件、その後の追記で 3 件だった。 -件数は着手時点の実測で 3 件のまま変わらない。 - -## 受け入れ条件 - -- [x] 受け入れ条件 1: 監視の環境変数(`MONITOR_TIMEOUT_AGY` と `MONITOR_STALL_AGY`)を設定した - シェルで全体のテストを実行すると、失敗が 0 件になる -- [x] 受け入れ条件 2: 設定したシェルと設定していないシェルで、通過した件数と失敗した件数が - 一致する -- [x] 受け入れ条件 3: 共通の前提が接頭辞 `MONITOR_` の環境変数を外していることを、テストが - 直接確かめる(実行中に該当する環境変数が 1 つも残らない) -- [x] 受け入れ条件 4: テストの中で監視の環境変数を設定する箇所は、共通の前提を足した後も - 同じ値を観測できる(共通の前提が個別の設定を打ち消さない) -- [x] 受け入れ条件 5: 接頭辞でまとめて外していた箇所と、1 変数ずつ外していた箇所が、 - 共通の前提へ寄る(対象のファイルに同じ除去が残らない) -- [x] 受け入れ条件 6: 共通の前提は、テストの実行が終わった後に元の環境変数を戻す - -## 検証手段 - -| 条件 | 確かめ方 | -| --- | --- | -| 1 / 2 | `MONITOR_TIMEOUT_AGY=1800 MONITOR_STALL_AGY=1800 uv run --with pytest pytest scripts/tests plugins/ndf -q` と、設定しない同じコマンドの 2 回を実行し、件数を突き合わせる | -| 3 / 4 / 6 | 共通の前提を確かめるテスト(`scripts/tests/test_root_conftest.py`)を実行する | -| 5 | `grep -rn "MONITOR_" --include="*.py" <テストのディレクトリ>` の結果に、接頭辞での除去と 1 変数ずつの除去が残らないことを確かめる | - -## 境界 - -```text -常に行う … 共通の前提の追加、散った除去の削除、両方のシェルでの全体テスト -確認してから行う … 上限の解決順・表の既定値に触れる変更(この変更では行わない) -行わない … 監視の本体・起動スクリプト・Skill 本文の変更、依頼範囲外の整形 -``` diff --git a/issues/issue-727-687-478-664-648-contracts.md b/issues/issue-727-687-478-664-648-contracts.md deleted file mode 100644 index 7091869e1..000000000 --- a/issues/issue-727-687-478-664-648-contracts.md +++ /dev/null @@ -1,307 +0,0 @@ -# cross-review / cross-refactoring: 参加する CLI が 1 者でも使えないと収束ループを開始できず、再開で渡した引数が黙って無視される → 使える者だけで開始し、cross-review は毎ラウンド 2 席を確保し、再開で渡した引数は反映されるか反映しないと知らされる(契約 / #727 #687 #478 #664 #648) - -## 目的 - -- **壊れていること**: 状態ファイルは使える者の記録を持たず、初期化は認証の確認を 1 件でも通らないと止まる。再開の初期化は渡された引数を状態へ重ねず、黙って捨てる。cross-refactoring は提案と適用で母集合を 2 つ持つ -- **困る人**: 両 Skill の初期化・ラウンドの開始・結果の受け口・起動スクリプトを実装する人と、状態ファイルを読む完了報告と監視の側 -- **直すと成り立つこと**: 使える者の解決の結果を参加者の記録(`participants`)の 1 つのオブジェクトが持ち、席の名前・引数・関数の形が両 Skill で同じになる。状態に載る引数は再開で「反映する」か「知らせる」のどちらかに必ず載る。この変更の前に始めた実行の状態ファイルは書き換えずに読める - -この文書は形だけを書く。決定の理由と用語の対応は設計文書が持つ(末尾の「関連文書」)。形を書く節のため、表と各節の先頭の指し示しには識別子をそのまま置く。 - -## データ構造(状態ファイル) - -両 Skill の状態ファイルに、参加者の記録と再開で変えた値の記録(`resume_changes`)の 2 項目が増える。cross-refactoring からは、適用専用の母集合の項目(`impl_capable`)とラウンドのレビュー担当(`reviewers`)が消える。既存の状態ファイルは書き換えない。 - -### 両 Skill の最上位に増える 2 項目 - -| 項目 | 型 | 空を許すか | 意味 | -| --- | --- | --- | --- | -| `participants` | オブジェクト(下の表) | 許さない(項目が無いことは許す) | 使える者の解決の結果。項目が無いのは「この変更の前に始めた実行」 | -| `resume_changes` | オブジェクトの配列 | 許す(空の配列) | 再開で変えた値の記録。追記だけを行う | - -参加者の記録の中身: - -| 項目 | 型 | 空を許すか | 意味 | -| --- | --- | --- | --- | -| `pool` | 文字列の配列 | 許さない | 母集合の既定(`ALL_RUNTIMES` の順)。cross-review は全ランタイム − ホスト、cross-refactoring は `refactor_pool(host)` | -| `included` | 文字列の配列 | 許す(空) | `--include` で足した者。空は「足していない」 | -| `excluded` | 文字列の配列 | 許す(空) | `--exclude` で外した者。空は「外していない」 | -| `available` | 文字列の配列 | 許す(空。cross-review だけ) | 使える者。`ALL_RUNTIMES` の順。`only` があれば `[only]`。cross-refactoring では 1 者以上 | -| `unavailable` | オブジェクト(名前 → 理由の文字列) | 許す(空) | 確認を通らなかった者と `probe_auth` の `detail`。空は「全員が通った」か「確認を飛ばした」 | -| `probe_skipped` | 真偽値 | 許さない | `NDF_SKIP_AUTH_CHECK` で確認を飛ばしたか。`unavailable` が空である理由を区別する | -| `require_all` | 真偽値 | 許さない | `--require-all` の値。新規の既定は `false` | -| `fallback` | 文字列の配列 | 許す(空) | **cross-review だけ。** 席の埋め合わせに使える者(ホストの確認が通れば `[host]`)。`available` が 2 者以上か `only` があれば空 | - -再開で変えた値の記録の要素は既存の契約と同じ(`at` / `field` / `from` / `to`)。`field` は状態ファイルの鍵である。参加者の記録を作り直したときは、`participants` の 1 件として積む(中の項目ごとには積まない)。 - -### 変わらない項目の意味の変化 - -| Skill | 項目 | 変わること | -| --- | --- | --- | -| 両方 | `host` | 変わらない。再開の `--host` で書き換えない | -| cross-review | `only` | 再開の `--only` で変わる。`--only none` で `null` へ戻る | -| cross-review | `max_rounds` / `rotate_after` / `verify_commands` / `verify_exit_codes` | 再開で明示的に渡したときだけ変わる | -| cross-review | `rounds[].reviewers` | **席の名前**が入る(`claude-2` のような値を取りうる)。過去のラウンドは再開で書き換えない | -| cross-review | `rounds[].<席>` | 鍵が席の名前になる。既存の `rounds[].codex` などはそのまま | -| cross-refactoring | `runtimes` | 使える者(`participants.available`)と同じ値。提案の対象と適用の輪番の両方が読む。既存の読み手(提案の取り込み・`prepare-worktrees.sh`)のために残す | -| cross-refactoring | `max_outer_rounds` / `max_test_rounds` / `max_fix_rounds` / `max_items_per_round` | 再開で明示的に渡したときだけ変わる | -| cross-refactoring | `models` / `target_scope` | 変わらない。再開で違う値が渡されたら知らせる | - -### cross-refactoring の新規の状態から消える項目 - -| 項目 | 理由 | -| --- | --- | -| 最上位の `impl_capable` | 母集合が 1 つになり `runtimes` と同じ値になる(決定 5) | -| `rounds[].reviewers` / `rounds[].reviewer_models` | レビュー工程が #436 で消えており、記録と表示にしか使われない(決定 6) | - -### 実体の関係 - -```mermaid -erDiagram - 状態ファイル ||--|| 参加者 : participants - 状態ファイル ||--o{ 再開で変えた値 : resume_changes - 状態ファイル ||--o{ ラウンド : rounds - 参加者 { - array pool - array included - array excluded - array available - object unavailable - bool probe_skipped - bool require_all - array fallback - } - ラウンド { - int round - array reviewers - string impl - } -``` - -`ラウンド.reviewers` は cross-review、`ラウンド.impl` は cross-refactoring が持つ。 - -### 機能とデータの対応 - -| 機能 | `participants` | `runtimes`(cross-refactoring) | `only` / 上限の項目 | `resume_changes` | `rounds[].reviewers` / `impl` | -| --- | --- | --- | --- | --- | --- | -| F1 使える者の解決(新規の `init`) | C | C | C | C(空) | — | -| F2 席の埋め方(`start-round`) | R | — | R | — | C | -| F3 除外と追加 | C | C | — | — | — | -| F4 適用の輪番(`start-round` / 適用ラウンド) | — | R | — | — | C | -| F5 再開の反映 | U | U | U | U(追記) | R | -| F6 報告 | R | R | R | R | R | - -## 移行と時系列 - -**既存の状態ファイルは書き換えない。** 項目が無いときの読み方を決め、過去の値は再開で変えた値の記録に残す。 - -### 時系列の扱い - -参加者の記録と参加者の一覧(`runtimes`)は上書きし、過去の値は再開で変えた値の記録に事象として積む。ラウンドごとの担当はラウンドの記録(`rounds[]`)が持つ。上書きで失われるのは「どの時点でどの一覧だったか」だけで、それを再開で変えた値の記録が補う。 - -### 項目が無いときの読み方 - -| Skill | 項目が無いとき | 読み方 | -| --- | --- | --- | -| cross-review | `participants` | `host` があれば `review_seats(r, review_pool(host), [])`(変更前の輪番と同じ値)。`host` も無ければ `codex` / `agy` | -| cross-refactoring | `participants` | `runtimes` から `impl_assign` で輪番を決める。`impl_capable` は読まない | -| 両方 | `resume_changes` | 空として読む | - -再開で担当に関わる引数を渡したときだけ、参加者の記録を作り直して書く。渡さない再開では書き足さない。作り直すときの入力は 2 つである。渡した引数と、渡さなかった引数の状態ファイルの値(`participants.included` / `excluded` / `require_all`、最上位の `only`)である。 - -## 初期化の引数 - -両 Skill の初期化が受ける引数と、新規・再開それぞれの経路での扱いを書く。 - -### cross-review の初期化の引数 - -`state.py init` が受ける。 - -| 引数 | 型 | 既定(argparse) | 新規の経路 | 再開の経路 | 変更 | -| --- | --- | --- | --- | --- | --- | -| `pr` | 整数 | 必須 | — | — | 変わらない | -| `--max-rounds N` | 整数 | `None` | 無ければ 12 | 渡せば反映 | 既定を `None` へ | -| `--rotate-after K` | 整数 | `None` | 無ければ 8 | 渡せば反映 | 既定を `None` へ | -| `--only RUNTIME` | 4 つの名前か `none` | `None` | `none` は無しと同じ | 渡せば反映。`none` で `null` | `none` を追加 | -| `--exclude NAMES` | カンマ区切りの名前。繰り返し可。`none` | `None` | 外す | 渡せば置き換え。`none` で空 | **新設** | -| `--include NAMES` | 同上 | `None` | 足す | 渡せば置き換え。`none` で空 | **新設** | -| `--require-all` / `--no-require-all` | 真偽値 | `None` | 無ければ `false` | 渡せば反映 | **新設** | -| `--host RUNTIME` | 4 つの名前 | `None` | 無ければ推定 | 反映しない。違えば 1 行 | 再開での知らせを追加 | -| `--verify-command CMD` | 文字列。繰り返し可 | `None` | 無ければ空 | 渡せば置き換え | 再開で反映 | -| `--verify-exit-code N` | 整数。繰り返し可 | `None` | 無ければ空 | 渡せば置き換え | 再開で反映 | -| `--worktree` / `--focus` / `--extra-instructions-file` | — | — | — | — | 変わらない | - -### cross-refactoring の初期化の引数 - -`refactor.py init` が受ける。 - -| 引数 | 既定(argparse) | 新規の経路 | 再開の経路 | 変更 | -| --- | --- | --- | --- | --- | -| `--exclude NAMES` / `--include NAMES` / `--require-all` | `None` | cross-review と同じ | cross-review と同じ | **新設** | -| `--max-outer-rounds` / `--max-test-rounds` / `--max-fix-rounds` / `--max-items-per-round` / `--test-timeout` | `None` | 無ければ現行の既定(3 / 2 / 3 / 5 / 900) | 渡せば反映(`replace`) | 既定を `None` へ | -| `--model RT=MODEL` / `--host` / `--scope` / `--baseline-test` / `--ci-check` / `--severity-threshold` / `--sync-command` / `--plan-file` / `--workflow-step` / `--worktree-root` | `None`(`--scope` と `--baseline-test` は必須のまま) | 無ければ現行の既定 | 反映しない。状態と違えば 1 行(`notify`) | 再開での知らせを追加 | - -**状態ファイルに載る引数は、「反映する」(`replace`)か「知らせる」(`notify`)のどちらかに必ず載る**(設計文書の決定 13)。載らないのは状態に載らない引数(cross-review の作業ツリー・観点・追加指示のファイルの 3 つ)だけである。 - -### 名前の検査 - -**名前の検査は 2 段に分かれる。** - -| 段 | 何を見るか | 誰が弾くか | 終了コード | -| --- | --- | --- | --- | -| 1 | 綴り(4 つの名前か `none`) | argparse の型 | 2 | -| 2 | 母集合との関係(`--exclude` / `--only` に母集合(既定 ∪ `--include`)に無い名前(cross-review のホストはこれに当たる) / `--include` と `--exclude` の重なり / `--only` と `--exclude` の矛盾 / `none` と名前の混在) | 共通層の `resolve_participants`(`AssignmentError`)。`init` が Skill の終了コードへ写す | cross-review 1 / cross-refactoring 4 | - -## 出力の契約 - -初期化・ラウンドの開始・結果の受け口・完了報告の出力を書く。標準出力の形は変えず、増えるのは標準エラーの行と席の名前である。 - -### 初期化の出力と終了コード - -標準出力の `KEY=VALUE` は、cross-refactoring から適用専用の母集合の変数(`IMPL_POOL`)が消えるほかは変えない。**増えるのは標準エラーの行だけである。** - -| 場面 | 標準エラーに出るもの | cross-review | cross-refactoring | 状態ファイル | -| --- | --- | --- | --- | --- | -| 新規で全員が使える | 母集合と使える者の 1 行 | 0 | 0 | 作る | -| 新規で確認を通らない者がいる | 通らなかった者と理由を 1 者 1 行 | 0 | 0 | 作る | -| 新規で使える者が 2 者に満たない(cross-review) | 埋め合わせの相手を 1 行(`席をホストで埋めます` / `同じランタイムの 2 つ目で埋めます`) | 0 | — | 作る | -| 新規で使える者が 0 者 | 使える者がいない理由 | ホストで埋められれば 0。埋められなければ 1 | 4 | 0 のときだけ作る | -| 確認を飛ばした | 飛ばしたことを 1 行(`auth.py` の既存の文言) | 0 | 0 | 作る(`probe_skipped: true`) | -| `--require-all` で欠けがある | 欠けた者と理由 | 1 | 4 | 作らない | -| 名前の矛盾 | 何が矛盾したか | 1 | 4 | 作らない | -| 再開で引数を反映した | 項目ごとに `<項目>: <旧> → <新>` の 1 行 | 0 | 0 | 書き換える | -| 再開で反映しない引数が状態と違う | 引数ごとに 1 行 | 0 | 0 | 変えない | -| 再開で作り直した使える者が 0 者 / 欠けあり | 新規と同じ | 1 | 4 | 書き換えない | - -### ラウンドの開始の出力 - -| Skill | 変わること | -| --- | --- | -| cross-review | `REVIEWERS` / `REVIEWERS_CSV` に席の名前が並ぶ(`codex claude-2` のような値を取りうる)。形は変わらない | -| cross-refactoring | `REVIEWERS` / `REVIEWERS_CSV` を出さない。`RUNTIMES` / `RUNTIMES_CSV` / `IMPL` / `IMPL_MODEL` は変わらない | - -### 結果の受け口と起動スクリプトの席の受け口(cross-review) - -| 受け口 | 変更前 | 変更後 | -| --- | --- | --- | -| `state.py read-result ` | `choices=ALL_RUNTIMES` | 席の名前(`seat_runtime` で検査)。通らなければ argparse の終了コード 2 | -| `launch-reviewer.sh ` | `case` で 4 つの名前を検査 | 席の名前を受け、CLI は `${SEAT%%-*}`(`seat_runtime` と同じ規則)で選ぶ。stem は席の名前で組む | -| `critique.sh ` | 同上 | 同上 | -| `monitor.py --agents` | 担当名の CSV | 席の名前の CSV。stem を組むだけなので変更は無い | - -### 完了報告の出力(cross-review) - -現行の「PR 履歴」の後に、次の節を足す。 - -```text -## 参加した者 -- 母集合: codex / agy / kiro -- 使える者: codex / kiro -- --exclude で外した者: agy -- --include で足した者: なし -- 確認を通らなかった者: なし -- 席の埋め合わせ: なし -- 再開で変えた値: 2026-09-19T12:00:00 participants … → … -``` - -参加者の記録を持たない状態ファイルでは「使える者: 記録なし」と出す。確認を飛ばした印(`probe_skipped`)が真のときは「確認を通らなかった者: 確認を飛ばした(`NDF_SKIP_AUTH_CHECK`)」と出す。cross-refactoring の完了報告は、現行の「提案・レビュー」と「適用の母集合」の 2 行を「参加者」の 1 行にし、参加者の記録を持てば同じ節を足す。 - -## 関数の形 - -共通層の 3 ファイルと、各 Skill の内部関数の入出力を書く。 - -### 共通層の関数: 割り当て - -置き場所は `lib/assignment.py` である。 - -| 関数 | 入力 | 出力 | 失敗の形 | 変更 | -| --- | --- | --- | --- | --- | -| `review_pool(host)` | ホスト名 | 全ランタイム − ホスト | ホストでない名前で `AssignmentError` | 変わらない | -| `refactor_pool(host)` | ホスト名 | `ALL_RUNTIMES` の順で、`DEFAULT_REFACTOR_RUNTIMES`(`("codex", "kiro")`)とホスト | 同上 | **新設** | -| `resolve_participants(pool, *, host, include=(), exclude=(), only=None, probe, require_all=False)` | 母集合、ホスト名、足す・外す名前、`--only`、確認の関数(`probe_auth` の形)、全員を要するか | `Participants`(`participants` の 8 項目のうち `fallback` を除く 7 項目を持つデータクラス。`to_state()` で辞書にする) | 名前の矛盾(`exclude` / `only` の名前が母集合(既定 ∪ `include`)に無い場合もここ。cross-review のホストはこれに当たる)・`require_all` で欠け → `AssignmentError`(欠けた者と理由を並べる) | **新設** | -| `review_seats(round_no, available, fallback)` | ラウンド番号、使える者、埋め合わせに使える者 | 席の名前 2 つ(`only` は呼び出し側が先に処理する) | `round_no < 1` / `available` と `fallback` が両方空 → `AssignmentError` | **新設**(`review_assign` を置き換える) | -| `impl_assign(round_no, participants)` | ラウンド番号(適用の通し番号)、使える者 | 実装担当 1 者(`participants[round_no % len]`) | `round_no < 1` / 空 → `AssignmentError` | **新設**(`assign` を置き換える) | -| `seat_runtime(seat)` | 席の名前 | ランタイム名 | 形に合わない → `AssignmentError` | **新設** | -| `SEAT_PATTERN` | — | `^(claude\|codex\|agy\|kiro)(-[2-9])?$` | — | **新設** | -| `impl_pool()` / `review_assign()` / `assign()` | — | — | — | **消す**(P7) | - -席の埋め方(`review_seats`)の規則: - -| 使える者の数 | 席 | -| ---: | --- | -| 3 以上 | `pool[round_no % n]` と `pool[(round_no + 1) % n]` を母集合の順に並べた 2 席 | -| 2 | その 2 者 | -| 1 | その 1 者と、`fallback` のうち `available` に含まれない先頭の者。そのような者が無ければ同じランタイムの 2 つ目(`<名前>-2`) | -| 0 | `fallback` の先頭と、その 2 つ目(`<名前>-2`)。`fallback` が空なら `AssignmentError` | - -埋め合わせの候補は使える者に含まれない者だけを使い、含まれる者は飛ばす。同じ席の名前を 2 つ返さないための規則である。例は、足す者の指定でホストが使える者に入った cross-review である。使える者がホストだけで埋め合わせもホストなら、席はホストとその 2 つ目(`["claude", "claude-2"]`)になる。 - -### 共通層の関数: 確認 - -置き場所は `lib/auth.py` である。 - -| 関数 | 入力 | 出力 | 失敗の形 | 変更 | -| --- | --- | --- | --- | --- | -| `probe_auth(runtimes, *, info, env=None)` | 確かめる名前の一覧 | `(結果, 飛ばしたか)`。結果は名前 → `{"command", "ok", "detail"}` | 例外を上げない。コマンドが無い・時間切れ・終了コード非 0・未認証の文言は `ok: false` と `detail` | **新設** | -| `check_auth(...)` | — | — | — | **消す**(P7。P6 では残る) | -| `AUTH_PROBES` / `UNAUTHENTICATED_MARKERS` / `AUTH_PROBE_TIMEOUT` / `SKIP_ENV` | — | — | — | 変わらない | - -### 共通層の関数: 再開の反映 - -置き場所は `lib/statefile.py` である。 - -| 関数 | 入力 | 出力 | 失敗の形 | 変更 | -| --- | --- | --- | --- | --- | -| `apply_resume_args(state, args, spec)` | 状態、argparse の名前空間、反映の表 | 標準エラーへ出す行の一覧。`state` を書き換え、`resume_changes` に積む。書き込みは呼び出し側が 1 回で行う | 上げない | **新設** | -| `ResumeField(arg, key, mode)` | 引数の属性名、状態ファイルの鍵、`replace` / `notify` | — | — | **新設** | - -反映の表(`spec`)は Skill ごとに持つ。「反映する」(`replace`)の項目は未指定でない値を状態へ書く。「知らせる」(`notify`)の項目は、状態と違うときだけ「反映しない」の行を返す。値が同じ項目は行を返さず、再開で変えた値の記録にも積まない。 - -### cross-review の内部関数 - -置き場所は `state.py` である。 - -| 関数 | 契約 | 変更 | -| --- | --- | --- | -| `_resolve_reviewers(host, args)` | `resolve_participants(review_pool(host), host=host, …)` を呼び、`available` が 2 者に満たなければホストを `probe_auth` で確かめて `fallback` を決める。`only` があるときはホストを確かめず `fallback` は空にする(決定 9: `--only` は埋め合わせをしない)。`AssignmentError` は `die(code=1)` へ写す | **新設**(`_auth_targets` / `_validate_only` を置き換える) | -| `_round_reviewers(st, round_no)` | 決定 11 の順で返す | 順を変える | -| `_resume_from_state(pr, repo, worktree, manual_extra_review, args)` | `apply_resume_args` を呼び、担当に関わる引数があれば、渡さなかった引数を状態ファイルの値で補って `_resolve_reviewers` で作り直す。失敗したら状態ファイルを書き換えずに終了コード 1 | 引数を足す | -| `_guard_previous_round(st, prev)` | `prev["reviewers"]`(無ければ `_round_reviewers`)を渡す | 担当を渡す | - -### cross-refactoring の関数 - -置き場所は `refactor_lib` である。 - -| 関数 | 契約 | 変更 | -| --- | --- | --- | -| `commands/setup.py cmd_init` | `resolve_participants(refactor_pool(host), …)` を呼び、`runtimes` と `participants` を書く。`AssignmentError` は `die`(終了コード 4) | 母集合と確認を置き換える | -| `commands/setup.py cmd_init`(再開) | `apply_resume_args` を呼び、担当に関わる引数があれば、渡さなかった引数を状態ファイルの値で補って作り直す | 反映を足す | -| `commands/setup.py cmd_start_round` | `impl_assign(round_no, state["runtimes"])`。`reviewers` を書かない | 担当の決め方を替える | -| `commands/apply.py` / `commands/gate.py` の `assign(seq, host)` | `impl_assign(seq, state["runtimes"])` | 呼び方を替える(各 1 行) | - -## 手順書の骨組み(cross-review) - -対象は `SKILL.md` と `docs/01` である。初期化へは値のある引数だけを渡し、担当はラウンドの開始が返す一覧を使う。 - -```bash -INIT_VARS=$("$SCRIPTS/state.py" init "$STATE_PR" \ - ${MAX_ROUNDS:+--max-rounds "$MAX_ROUNDS"} ${ROTATE_AFTER:+--rotate-after "$ROTATE_AFTER"} \ - ${HOST:+--host "$HOST"} ${ONLY:+--only "$ONLY"} \ - ${EXCLUDE:+--exclude "$EXCLUDE"} ${INCLUDE:+--include "$INCLUDE"} \ - ...) || exit $? - -for r in $REVIEWERS; do "$SCRIPTS/launch-reviewer.sh" "$r" "$STATE_PR" "$ROUND"; done -"$SCRIPTS/monitor.py" "$STATE_PR" --phase review --agents "$REVIEWERS_CSV" || true -for r in $REVIEWERS; do "$SCRIPTS/state.py" read-result "$STATE_PR" "$r" || true; done -"$SCRIPTS/critique-round.sh" "$STATE_PR" "$ROUND" $REVIEWERS -``` - -**ラウンドの上限と交代の間隔(`--max-rounds` / `--rotate-after`)も値があるときだけ渡す。** 現行の骨組みは上限を常に渡すため、再開のたびに利用者が指定していない値で上書きする。 - -## 関連文書 - -| 文書 | 何を持つか | -| --- | --- | -| [issue-727-687-478-664-648-design.md](issue-727-687-478-664-648-design.md) | 決定の理由(「決定の記録」)と用語の対応表。この文書はその続きである | -| [issue-727-687-478-664-648-requirements.md](issue-727-687-478-664-648-requirements.md) | 受け入れ条件 AC1〜AC50 | -| [issue-624-478-648-contracts.md](issue-624-478-648-contracts.md) | 既存の契約(PR #667)。この文書はその P5 の部分を置き換える。既存の 6 項目(`available_reviewers` ほか)は参加者の記録の 1 つのオブジェクトに畳み、cross-refactoring も同じ形を持つ | diff --git a/issues/issue-727-687-478-664-648-design.md b/issues/issue-727-687-478-664-648-design.md deleted file mode 100644 index 80cada95c..000000000 --- a/issues/issue-727-687-478-664-648-design.md +++ /dev/null @@ -1,504 +0,0 @@ -# cross-review / cross-refactoring: 参加する CLI が 1 者でも使えないと収束ループを開始できず、再開で渡した引数が黙って無視される → 使える者だけで開始し、cross-review は毎ラウンド 2 席を確保し、再開で渡した引数は反映されるか反映しないと知らされる(設計 / #727 #687 #478 #664 #648) - -## 目的 - -- **壊れていること**: 参加する CLI のどれか 1 者が導入・認証されていないと、cross-review / cross-refactoring の初期化が止まる。収束ループを開始できない(#478 / #687)。cross-refactoring には担当から agy を外す引数が無く、使える者が 2 者だとレビュー担当が 1 者になる(#664)。中断した収束ループを 1 者指定などの引数を変えて再開しても、引数が黙って無視される(#648) -- **困る人**: 4 つの CLI が揃っていない環境で収束ループを回す利用者と、中断したループを進め方を変えて再開する利用者 -- **直すと成り立つこと**: 使える者の決定・席の埋め方・再開の反映の 3 つの規則を、両 Skill が共有する共通層が 1 か所ずつ持つ。両 Skill の初期化は共通層の結果を状態ファイルと終了コードへ写すだけになり、1 者欠けても止まらない。cross-review は毎ラウンド 2 席を確保し、cross-refactoring の既定の参加者は codex / kiro とホストになる - -この文書は「どう作るか」を扱う。実装は 2 本の Pull Request(P6 / P7)に分ける(決定 19)。要求・契約・既存の設計との関係は末尾の「関連文書」にある。 - -## 用語の対応表 - -本文は左の業務用語で書く。識別子は表・コードブロック・「置き場所」と、業務用語の初出の括弧書きにだけ置く。 - -| 業務用語 | 識別子 | 何を指すか | -| --- | --- | --- | -| 初期化 | `init`(cross-review は `state.py init`、cross-refactoring は `refactor.py init`) | 収束ループを新規に始める・再開する副コマンド | -| ラウンドの開始 | `start-round` | 次のラウンドを開き、担当を決めて返す副コマンド | -| 結果の受け口 | `read-result` | 担当の結果ファイルを状態ファイルへ写す副コマンド | -| 完了報告 | `report` | 収束の結果を出す副コマンド | -| 使える者の解決 | `resolve_participants`(`lib/assignment.py`) | 母集合・足す者・外す者・確認の結果から使える者を決める関数。結果は参加者の記録 | -| 止めない確認 | `probe_auth`(`lib/auth.py`) | 認証の確認コマンドを走らせ、止めずに結果だけを返す関数 | -| 従来の確認 | `check_auth` | 1 件の失敗で止める確認。P7 で消す | -| 母集合の既定 | `review_pool(host)` / `refactor_pool(host)` | Skill ごとの参加者の出発点を返す関数 | -| 既定の参加者の表 | `DEFAULT_REFACTOR_RUNTIMES` | cross-refactoring の既定(codex / kiro) | -| ランタイムの固定の順 | `ALL_RUNTIMES` | claude / codex / agy / kiro の順 | -| 席の埋め方 | `review_seats` | 使える者と埋め合わせから 2 席を返す関数。従来の `review_assign` を置き換える | -| 適用の輪番 | `impl_assign` | 参加者から実装担当 1 者を返す関数。従来の `assign` を置き換える | -| 適用専用の母集合 | `impl_pool()` / `IMPL_POOL` / `impl_capable` | 関数・初期化の出力変数・状態ファイルの項目。3 つとも消す | -| 席の名前の解釈 | `seat_runtime` / `SEAT_PATTERN` | 席の名前からランタイムを引く関数と、形の検査 | -| 再開の反映 | `apply_resume_args` / `ResumeField`(`lib/statefile.py`) | 再開で渡した引数を表に従って状態へ重ねる関数と、表の 1 行 | -| 足す者 / 外す者 | `--include` / `--exclude` | 参加者を名指しで足す・外す引数 | -| 1 者指定 | `--only`(シェル変数 `$ONLY`) | 担当を 1 者に固定する引数 | -| 全員を要する指定 | `--require-all` | 確認の失敗が 1 者でもあれば止める引数 | -| 指定を外す値 | `none` | 再開で 1 者指定・足す者・外す者を空へ戻す予約語 | -| 参加者の記録 | `participants`(`available` / `unavailable` / `fallback` ほか) | 状態ファイルの項目。使える者の解決の結果 | -| 埋め合わせ | `fallback` | 席が足りないときに使う者(ホスト) | -| 参加者の一覧 | `runtimes` | cross-refactoring の状態ファイルの項目。提案の対象と適用の輪番が読む | -| 再開で変えた値の記録 | `resume_changes` | 状態ファイルの項目。追記だけを行う | -| 担当の一覧の出力 | `REVIEWERS` / `REVIEWERS_CSV` | ラウンドの開始が返すシェル変数 | -| ラウンドの担当の記録 | `rounds[].reviewers` / `rounds[].impl` | ラウンドごとの席(cross-review)と実装担当(cross-refactoring) | -| 割り当ての失敗 | `AssignmentError` | 共通層が名前の矛盾と欠けを返す例外 | -| 担当の読み出し / 前ラウンドの検査 | `_round_reviewers` / `_guard_previous_round` | cross-review の内部関数 | -| P6 / P7 | — | 実装の Pull Request の 1 本目(共通層と cross-review)と 2 本目(cross-refactoring と旧関数の削除) | -| G2〜G5 | — | 並行して進む他の設計の束(#732 / #729 / #728 / #730) | - -## 機能一覧 - -6 つの機能を、使う人と出す Pull Request と対で並べる。 - -| # | 機能 | 誰が使うか | Pull Request | -| --- | --- | --- | --- | -| F1 | 確認を通らない者を外し、使える者で収束ループを始める。使えない者と理由を残す | CLI の一部が導入・認証されていない利用者 | P6 / P7 | -| F2 | cross-review の各ラウンドに 2 席を確保する(使える者 → ホスト → 同じランタイムの 2 つ目) | 使える者が 2 者に満たない利用者 | P6 | -| F3 | 外す者・足す者の指定で参加者を名指しで外す・足す | 打ち切り・利用上限が分かっている担当を避けたい利用者、agy を戻したい利用者 | P6 / P7 | -| F4 | cross-refactoring の既定の参加者を codex / kiro / ホストにし、適用の輪番をその中で回す | cross-refactoring の利用者 | P7 | -| F5 | 再開で明示的に渡した引数を反映し、反映しない引数を知らせる | 中断したループを進め方を変えて再開する利用者 | P6 / P7 | -| F6 | 完了報告に参加者・外した者・足した者・確認を通らなかった者・席の埋め合わせ・再開で変えた値を出す | 収束の結果を読む人 | P6 / P7 | - -## 実測 - -決定の根拠になる値である。`develop` のコミット 9eaebe14、Python 3.14 で、割り当ての部品(`assignment.py`)を読み込んで式を突き合わせた。 - -| 場面 | 結果 | -| --- | --- | -| 席の式 `{pool[r % n], pool[(r + 1) % n]}` を母集合の順に並べる(n = 3) | 4 ホスト × ラウンド 1〜12 の全組で、変更前の `review_assign(r, host)` と一致(不一致 0 件) | -| 同じ式で n = 4 | ラウンド 1〜4 の席は `codex agy` / `agy kiro` / `claude kiro` / `claude codex`。各者ちょうど 2 回 | -| n = 2(`codex` / `kiro`) | ラウンド 1〜4 すべて `codex kiro` | -| n = 1 / n = 0 | `codex claude`(ホストあり)/ `codex codex-2`(ホストなし)/ `claude claude-2`(0 者・ホストあり) | -| `impl_assign` を `participants[r % n]` で `["claude", "codex", "kiro"]` に当てる | ラウンド 1〜6 で `codex` / `kiro` / `claude` / `codex` / `kiro` / `claude`。変更前の `assign(r, "claude")` は `codex` / `agy` / `kiro` / `claude` / … | -| 席の形 `^(claude\|codex\|agy\|kiro)(-[2-9])?$` | `kiro` / `kiro-2` / `claude-9` が一致し、`kiro-1` / `kiro-10` / `gemini` / `kiro-2-3` は一致しない | -| stem の逆解析 | `split("-")` / `rsplit` / `.stem` / `re.match(.*agent` を cross-review のスクリプトと `monitor.py` で検索し、担当名へ戻す箇所は 0 件(当たった 1 件は PR のファイル種別の判定) | -| cross-refactoring の `$REVIEWERS` の読み手 | `SKILL.md` / `docs/` / `prompts/` で `reviewer` が 0 件。`setup.py` が出すだけで、読むのはテスト `test_start_round_emits_runtimes.py` の期待値だけ | -| cross-refactoring の再開で反映される引数 | `init` の 15 引数のうち 0 個(`_apply_post_event` の投稿の扱いだけを毎回入れ直す) | -| 既存のテストの数 | `uv run --with pytest pytest scripts/tests plugins/ndf -q --co` で 4413 件 | - -担当名を鍵・分岐・選択肢に使う箇所の一覧(10 か所の受け口を含む)は、調査の控えから「構成要素」の表へ写した。控え(`survey-727-agent-keys.md`)は設計 Pull Request のレビューの間だけ scratchpad に置く。 - -## 決定の記録 - -20 件を 6 つの塊に分ける。 - -| 決定 | 扱うこと | -| --- | --- | -| 1 | 文書の置き方 | -| 2〜4 | 使える者の決定 | -| 5〜8 | cross-refactoring の母集合 | -| 9〜12 | cross-review の席 | -| 13〜18 | 再開と骨組み | -| 19〜20 | 分け方と規則の置き場所 | - -### 決定 1: 通過済みの決定と新しい決定を差分で読み分けるために、設計文書は親 #727 の名前で新設し、既存の本体は書き換えない - -既存の設計は P4 と P5 を 1 つの文書で扱い、P4 は #732 が別に進める。P5 の節をその場で書き換えると、#732 が読む P4 の決定と、この変更で変わる P5 の決定が 1 つの差分に混ざる。**新設して対応表で指せば、変わった決定だけが差分に載る。** 既存の設計文書の本体には案内の 1 行も足さない。設計 Pull Request の本文の「決めたこと」は、変更したファイルの「決定の記録」の見出しをすべて写す。そのため 1 行でも触ると既存の 19 件がこの Pull Request の決定として並ぶ。案内は「決定の記録」を持たない要求と契約の文書にだけ足す(#729 の設計と同じ扱い)。 - -### 決定 2: 使える者を決める規則を 2 か所に持たないために、決定を共通層の 1 つの関数へ移し、両 Skill の初期化は結果を終了コードへ写すだけにする - -いまは cross-review と cross-refactoring の初期化が、それぞれ母集合を作る。前者は `_auth_targets` / `_validate_only`、後者は `cmd_init` が持つ。どちらも従来の確認を呼び、1 件の失敗で止める。使える者を決める規則を Skill ごとに書くと、母集合の作り方・除外の検査・確認の扱いが 2 か所にでき、片方だけが古くなる(親 #727 が採る手「責務の移動」)。**共通層に使える者の解決(`resolve_participants`)を 1 つ置く。** 入力は母集合・ホスト・足す者・外す者・1 者指定・確認・全員を要するかで、参加者の記録を返す。外す者と 1 者指定の名前が母集合に含まれるかの検査も、ホストを受け取るこの関数が持つ。cross-review ではホストが母集合に無いため、ホストを外す指定はここで弾かれる。cross-refactoring ではホストが母集合にあるため外せる。Skill が持つのは 2 つだけである。返った値を状態ファイルへ書くことと、割り当ての失敗を自分の終了コード(cross-review 1 / cross-refactoring 4)へ写すことである。 - -既存の設計の決定 10(cross-refactoring は変えず、従来の確認を残す)は採らない。cross-review だけを直すと、同じ層を使う cross-refactoring に「1 者欠けると初期化ごと失敗する」形が残る(#664)。 - -### 決定 3: 1 者の失敗で全体を止めずに使える者を残すために、使えるかの確認は止めない 1 つの関数に寄せ、確認コマンドは変えない - -従来の確認(`check_auth`)は確認と中断を 1 つの関数が持つため、「使える者で回す」経路から呼べない。止めない確認(`probe_auth`)は結果だけを返し、止めるかどうかは使える者の解決の「全員を要するか」が決める。**両 Skill が止めない確認へ移った時点で従来の確認は呼び手を失うため消す**(P7)。確認コマンド(`AUTH_PROBES`)と未認証の文言は変えない。言語モデルを引く最小の呼び出しへ替える判断は所要の実測が要るため #461 が持つ。この変更が作るのは、確認の結果で使える者を決める入口と、通らなかった理由を参加者の記録に残す形である。 - -### 決定 4: ホストが変わっても一覧を書き直さずに済むように、参加者は「母集合の既定に足す者を加え、外す者を除く」で決め、既定は Skill ごとの関数が持つ - -外したい理由は「この担当が落ちる」であり、名指しするのは外す側である(外す者 `--exclude`)。戻したい理由は「既定から外れている者を入れたい」で、これも名指しである(足す者 `--include`)。使う側を並べる形(`--reviewers codex,kiro`)は、ホストが変わると一覧を書き直すことになるため採らない。**既定は Skill ごとの関数が持ち、使える者の解決はどちらを渡されても同じ規則で解決する。** - -| Skill | 既定を返す関数 | 中身 | -| --- | --- | --- | -| cross-review | `review_pool(host)` | 全ランタイム − ホスト | -| cross-refactoring | `refactor_pool(host)` | ランタイムの固定の順で、既定の参加者の表(codex / kiro)とホスト | - -cross-review の足す者は、ホストを母集合へ入れる用途になる(#687 の「ホストの参加を許す」の明示的な形)。 - -### 決定 5: 提案と適用を同じ者で回すために、cross-refactoring の母集合を 1 つにし、適用専用の母集合を消す - -既定の参加者を codex / kiro / ホスト(既定の参加者の表とホスト)にすると、提案の母集合と適用の母集合が同じ集合になる。適用専用の母集合を関数として残した理由(「両者は一致しない」)が消える。そのため関数・状態ファイルの項目・初期化の出力変数の 3 つ(`impl_pool()` / `impl_capable` / `IMPL_POOL`)を消す。参加者の一覧(`runtimes`)は提案の取り込みと作業ツリーの準備(`prepare-worktrees.sh`)が読むため残し、適用の輪番も同じ値を読む。 - -agy を既定から外す理由は #664 の実測にある。CLI の起動 199 回のうち失敗は agy の 7 回(停止 4 / 結果なし 3)だけで、提案の所要の中央値も agy が最も長い(5 分。codex 3 分、kiro 2 分)。提案は最も遅い者を待つため、所要はほぼ agy で決まっていた。戻す手段は足す者の指定(`--include agy`)である。 - -### 決定 6: 存在しない役の記録を読み手に見せないために、cross-refactoring のレビュー担当を消し、担当の割り当ては実装担当 1 者だけを返す - -cross-refactoring のレビュー工程は #436 で消え、Step 7 の cross-review が担う。従来の割り当て(`assign()`)が返すレビュー担当が流れる先は 3 つだけである。 - -| 流れる先 | 読む側 | -| --- | --- | -| ラウンドの記録(`reviewers` / `reviewer_models`) | 改修計画と報告の表示 | -| ラウンドの開始の出力(`REVIEWERS` / `REVIEWERS_CSV`) | 無い(骨組み・文書・プロンプトで `grep -rn -i reviewer` が 0 件) | -| `record_observed_model` のレビュー側の枝 | 無い(呼び出しは `role="impl"` の 1 か所だけ) | - -**#664 の「使える者が 2 者だとレビュー担当が 1 者になる」は、存在しない役の記録の話である。** 役を消せば、席を埋める規則を cross-refactoring に持ち込む必要が無い。適用の輪番(`impl_assign`)は実装担当 1 者だけを返す。 - -レビュー担当を「記録のためだけに」残す案は採らない。読み手が「このラウンドはこの 2 者がレビューした」と読む。 - -### 決定 7: ホストが最初に適用する形にならないように、適用の輪番は「ラウンド番号を参加者の数で割った余り」の式とランタイムの固定の順を保つ - -いまの割り当ては、4 者の固定の順をラウンド番号で割った余りで引く(`pool[round_no % 4]`)。ラウンド 1 が codex から始まる。式を変えずに除数を参加者の数にすれば、ホスト claude の既定(claude / codex / kiro)でも codex → kiro → claude の順になる。ホストが最初に適用する形にならない(「実測」)。ラウンド 1 から順に並べる式(`(round_no - 1) % n`)は、ホストが先頭に来るため採らない。 - -### 決定 8: 適用ラウンドの群の分け方を変えないために、提案者と適用者が同じランタイムになることを避けない - -ホストが提案に入るため、ある項目の提案者と適用者が同じランタイムになりうる。適用ラウンドは複数の提案者の項目を 1 つの群にまとめるため、群ごとに提案者を避けると群の分け方そのものを変えることになる。**避けない。** 適用の結果は検証(テスト)と Step 7 の cross-review が見る。#687 の「コードを書いたランタイムが担当に入ってよい」と同じ判断である。 - -### 決定 9: 使える者が足りなくても毎ラウンド 2 つの目で見るために、cross-review の席は 2 つとし、使える者 → ホスト → 同じランタイムの 2 つ目の順で埋める - -#687 の 4 つの規則のうち「各ラウンドで 2 者」を最優先に置き、残る 3 つを埋める順序として読む。**違うランタイムを先に使う。** 同じ言語モデルの 2 つの文脈より、違う言語モデルの 2 つの文脈のほうが観点が分かれる。ホストは既定の母集合に無いため、使える者が 1 者のときだけ席に入る。同じランタイムの 2 つ目(`<名前>-2`)は、ホストも使えないときの最後の手段である。 - -**埋め合わせの候補は使える者に含まれない者だけを使う。** 足す者でホストが使える者に入っているとき、ホストを埋め合わせにも使うと同じ席の名前が並ぶ。含まれる者は飛ばす。それでも 2 席に満たなければ同じランタイムの 2 つ目を充てる。使える者がホストだけで埋め合わせもホストなら、席はホストとその 2 つ目(`claude` / `claude-2`)になる。 - -| 使える者の数 | 席 | -| ---: | --- | -| 3 以上 | 輪番で 2 席(3 者のときは変更前の値と一致する) | -| 2 | その 2 者 | -| 1 | その 1 者とホスト。ホストが使えないか、その 1 者と同じなら同じランタイムの 2 つ目 | -| 0 | ホストとその 2 つ目。ホストも使えなければ失敗 | - -1 者指定は利用者が 1 席と決めた指定であり、埋め合わせをしない(既存の決定 11 のまま)。ホストの確認も行わない。使える者が 2 者のとき輪番で 1 者を外す案は、毎ラウンド 1 席になるため採らない。 - -### 決定 10: 同じランタイムの 2 つ目を結果ファイルと状態ファイルで区別するために、担当の単位を「席の名前」にし、形はランタイム名か「ランタイム名に 2〜9 の接尾辞」とする - -同じランタイムの 2 つ目を立てるには、結果ファイルの stem と状態ファイルの鍵を分ける名前が要る。**1 つ目の席の名前はランタイム名そのままにする。** 埋め合わせが要らない実行では、いまと同じ名前しか現れない。接尾辞の区切りはハイフンで、ランタイム名にハイフンを含むものが無いため、シェル側の切り出し(`${SEAT%%-*}`)と席の名前の解釈(`seat_runtime`)が同じ規則になる。stem を逆に解析して担当名へ戻す箇所は無い(「実測」)ため、stem 側の変更は無い。 - -受け口は 10 か所で、いずれも「CLI を選ぶ分岐に席の名前の解釈を通す」か「選択肢の検査を席の形の検査に替える」のどちらかである(「構成要素」の表)。1 者指定とホストの引数はランタイム名のままで、席の名前を取らない。 - -### 決定 11: 再開で 1 者指定を変えても過去のラウンドの担当が変わらないように、担当の決め方をラウンドの記録から先に見る順へ変え、前ラウンドの検査も記録の担当を読む - -```text -ラウンドの reviewers → only → participants の席 → host の輪番(review_pool)→ codex / agy -``` - -既存の決定 12 と同じ理由である。再開で 1 者指定を変えられるようにすると、1 者指定を先に見る現行の順では過去のラウンドの担当まで変わる。前ラウンドの検査にも、そのラウンドの記録の担当を渡す。現行は担当を渡さず codex / agy で数えるため、担当が agy + kiro のラウンドで codex を結果なしと読み、修正の記録が無いまま次のラウンドへ通す。 - -### 決定 12: 全員が揃わないなら始めたくない運用のために、全員を要する指定で従来の関門を選べるようにする - -全員を要する指定(`--require-all`)は、全員が揃わないなら始めたくない運用のために残す(既存の決定 9)。付けると、確認を通らない者が 1 者でもいれば従来の文言で失敗する。外す者で外した者は揃っていなくてよい。両 Skill に同じ引数を置く。 - -### 決定 13: 黙って捨てる引数を残さないために、再開の反映を共通層の 1 つの関数に置き、Skill ごとの表で「反映する」「知らせる」を決める - -#648 の修正レイヤーは状態ファイルの部品(`statefile.py`)である。cross-review の再開の経路(`_resume_from_state`)だけを直すと、cross-refactoring の再開の経路(引数由来の項目を 1 つも反映しない)が残る。**共通層の再開の反映(`apply_resume_args`)が持つのは 1 つの規則だけである。値のある引数を状態へ書き、再開で変えた値の記録に積み、反映しない引数は状態と違うときだけ知らせる。どの引数がどちらかは Skill ごとの表が持つ。** - -**状態ファイルに載る引数は、表のどちらかに必ず載る。** 載らない引数は状態に載らないもの(作業ツリー・観点・追加指示のファイルの 3 つ)だけである。黙って捨てる引数を残さないためで、基準テストの引数のように再開でも渡す必須の引数も「違えば知らせる」に載る。そのため状態に載る引数の既定はすべて未指定(`None`)にし、新規の経路が定数の既定へ置き換える。既定値と同じ値なら渡していないとみなす案は、上限 12 の指定で 20 から 12 へ戻す操作を区別できないため採らない。 - -「知らせる」に置く引数は 2 種類ある。 - -| 種類 | 引数 | -| --- | --- | -| 変えると過去のラウンドと突き合わせられなくなる | `--host` / `--scope` / `--model` | -| 初期化の時点で 1 度だけ効く | `--baseline-test` / `--worktree-root` / `--plan-file` など | - -### 決定 14: 途中で担当が入れ替わって前のラウンドと突き合わせられなくならないように、担当に関わる引数を渡した再開でだけ、確認し直して参加者を作り直す - -1 者指定・外す者・足す者・全員を要する指定のいずれかを渡した再開では、決定 2 と同じ手順で参加者を作り直す。**渡さなかった引数は状態ファイルの値で補う。** ホストを足して始めた実行へ agy を外す指定だけを渡した再開では、足した者の記録はホストのまま残り、外した者だけが agy になる。渡さなかった引数を初期値へ戻すと、「明示した引数だけを反映する」(決定 13)が破れる。いずれも渡さない再開では確かめ直さない。途中で担当が入れ替わると、前のラウンドの記録と突き合わせられなくなるためである。作り直した結果が失敗(0 者、全員を要する指定で欠け)なら、状態ファイルを書き換えずに終了コードで終わる。反映は再開の後に開くラウンドから効く(要求の前提 3)。 - -### 決定 15: 骨組みが空文字列を渡さない形でも指定を外せるように、再開で指定を外す値は「無し」を表す予約語にする - -予約語は `none` である。1 者指定に渡すと `null` へ、外す者・足す者に渡すと一覧を空へ戻す。空文字列は、骨組みが値のあるときだけ引数を渡す形(`${ONLY:+--only "$ONLY"}`)のため、外す手段にならない。新規の経路で予約語を渡すと、渡さないのと同じになる。 - -### 決定 16: 途中から誰を外したかを完了報告で読めるように、再開で変えた値は変更の記録に積み、参加者の作り直しは 1 件として積む - -上書きだけでは、どの時点で何を変えたかが失われ、完了報告で「途中から agy を外した」ことが読めない。参加者の記録の中の項目ごとに積むと 1 回の再開で最大 7 件になり、報告で読みにくい。参加者の記録全体の前後を、再開で変えた値の記録(`resume_changes`)に 1 件として積む。 - -### 決定 17: 状態ファイルとシェル変数がずれても起動と監視が誰かに当たるように、骨組みは 1 者指定のシェル変数で絞らず、ラウンドの開始が返す席を使う - -ラウンドの開始が返す担当の一覧(`REVIEWERS`)は、1 者指定と席の埋め合わせを反映済みである。シェル変数でもう一度絞ると、状態ファイルとシェル変数がずれたときに起動も監視も誰にも当たらない(#648 の 3 段の経過)。手順書(`SKILL.md` / `docs/01`)の Step 2 と Step 2.5 を担当の一覧(`$REVIEWERS` / `$REVIEWERS_CSV`)に揃える。1 者指定のシェル変数(`$ONLY`)は初期化へ渡す 1 行にだけ残す(既存の決定 18)。 - -### 決定 18: 一時的な打ち切りで担当が恒久的に外れないように、起動した後に分かる使えなさで担当を自動的に外す仕組みは作らない - -利用上限や言語モデルの 404 は起動した後に分かり、分類は #729(G3)が持つ。自動で外すと、一時的な打ち切りでも以後のラウンドから恒久的に外れる。この変更は利用者が外す者の指定で外し、再開で反映できる入口までを作る(既存の決定 19)。 - -### 決定 19: P6 で cross-refactoring を壊さないために、P6(共通層と cross-review)→ P7(cross-refactoring と旧関数の削除)の順で出す - -共通層の新しい関数は P6 で入れ、旧関数(従来の確認・適用専用の母集合・従来の席と適用の割り当て)は P7 で消す。P6 の時点で旧関数を消すと cross-refactoring が壊れる。1 本にまとめると、cross-review の状態の部品(`state.py`)と cross-refactoring の部品(`refactor_lib`)と文書 3 種を 1 度にレビューすることになる。G2 / G3 / G5(cross-review 側)と G4(cross-refactoring 側)との競合も 1 度に解くことになる。**「片方にだけ古い形が残らない」(親 #727 の完了条件)は P7 のマージで満たす。** - -### 決定 20: 次に使う人へ規則が届くように、規則の実装は共通層の割り当ての部品、手順は各 Skill の手順書、理由はこの設計文書が持つ - -#687 が問う置き場所である。使える者の数で分岐する実装と席の規則の表は、割り当ての部品(`assignment.py` の `review_seats` の docstring)に 1 つだけ置く。利用者が手順として読む「担当の決まり方と渡す引数」は各 Skill の手順書(`SKILL.md`。cross-review は `docs/05` が本体)に置く。同じランタイムの 2 つ目とホストの参加を許した理由はこの文書が持ち、`plan-to-spec` が `docs/specifications/` へ移す。`CLAUDE.md` には要約の 2 行(cross-refactoring の母集合と輪番、cross-review の席)だけを置く。 - -## 構成要素 - -変える要素を、責務と出す Pull Request と対で並べる。 - -| 要素 | 責務 | Pull Request | -| --- | --- | --- | -| 母集合の既定(`assignment.review_pool` / `refactor_pool`) | Skill ごとの出発点を返す。既定の参加者は表 `DEFAULT_REFACTOR_RUNTIMES` が持つ | P6 | -| 使える者の解決(`assignment.resolve_participants`) | 母集合・ホスト・足す・外す・`only`・確認・`require_all` から `Participants` を返す。名前の矛盾(`--exclude` / `--only` に母集合に無い名前。cross-review のホストはこれに当たる)と欠けを `AssignmentError` で返す | P6 | -| 確認(`auth.probe_auth`) | 止めずに確かめ、担当ごとの結果を返す | P6 | -| 席の埋め方(`assignment.review_seats`) | 使える者と埋め合わせから 2 席を返す(決定 9) | P6 | -| 適用の輪番(`assignment.impl_assign`) | 参加者から実装担当 1 者を返す(決定 7) | P6(呼び手は P7) | -| 席の名前(`assignment.seat_runtime` / `SEAT_PATTERN`) | 席の名前からランタイムを引く。形の検査 | P6 | -| 再開の反映(`statefile.apply_resume_args` / `ResumeField`) | 表に従って状態へ書き、`resume_changes` に積み、知らせる行を返す | P6 | -| cross-review の初期化(`state.py` の `_resolve_reviewers` / `_init_new_state` / `_resume_from_state`) | 共通層を呼び、`participants` を書き、失敗を終了コード 1 へ写す。再開で反映の表を渡す | P6 | -| cross-review の担当の読み出し(`_round_reviewers` / `_guard_previous_round`) | 決定 11 の順で席を返す | P6 | -| cross-review の席の受け口(10 か所) | `launch-reviewer.sh:29,223` / `critique.sh:31` / `critique-round.sh:39` / `wait-review.sh` の CLI を選ぶ分岐、`monitor.py:953` の `target` の `choices`、`monitor.py` の `agent == "codex"` / `"claude"` の比較(629 / 632 / 658 / 808 ほか)、`measure.py:36,128` の `AGENT_NAMES`、`state.py:4389` の `read-result` の `choices`、`_guard_previous_round` の `LEGACY_AGENTS` への落ち方 | P6 | -| cross-review の完了報告(`cmd_report`) | 「参加した者」の節を出す | P6 | -| cross-review の骨組みと文書(`SKILL.md` / `docs/01` / `docs/04` / `docs/05`) | `$ONLY` で絞らない。引数・状態ファイル・席の規則を書く | P6 | -| cross-refactoring の初期化(`refactor_lib/commands/setup.py` の `cmd_init`) | `refactor_pool` と `resolve_participants` を呼び、`runtimes` と `participants` を書く。`impl_capable` / `IMPL_POOL` を出さない。再開で反映の表を渡す。失敗を終了コード 4 へ写す | P7 | -| cross-refactoring の担当(`cmd_start_round` / `apply.py:134` / `gate.py:128`) | `impl_assign(seq, state["runtimes"])`。`reviewers` を書かず `REVIEWERS` を出さない | P7 | -| cross-refactoring の表示(`report.py` / `plan.py`) | 母集合を 1 行にし、レビュー担当の列を消す。`participants` があれば参加者の節を出す | P7 | -| cross-refactoring の引数(`refactor.py`) | `--exclude` / `--include` / `--require-all` を足し、状態に載る引数の既定を `None` にする | P7 | -| cross-refactoring の文書(`SKILL.md` / `docs/01`)と `CLAUDE.md` | 母集合・担当の決め方・前提・引数を実装後に合わせる | P7 | -| 旧関数の削除(`check_auth` / `impl_pool` / `review_assign` / `assign`) | 呼び手が無くなった時点で消す | P7 | - -要素の関係(辺は呼び出し): - -```mermaid -graph TD - subgraph 共通層 - PL[母集合の既定] --> RP[使える者の解決] - RP --> PA[確認] - RS[席の埋め方] - IA[適用の輪番] - SR[席の名前] - RA[再開の反映] - end - subgraph cross-review - RI[初期化] --> RP - RI --> RA - RR[担当の読み出し] --> RS - RC[席の受け口] --> SR - end - subgraph cross-refactoring - FI[初期化] --> RP - FI --> RA - FA[担当] --> IA - end -``` - -完了報告・表示・引数・文書・旧関数の削除は、状態ファイルを読むだけか呼び出しを持たないため、図に含めない。 - -## 文脈と配置 - -**文脈と配置は変わらない。** 動くのは、ホストの CLI から起動される両 Skill の状態の部品(`state.py` / `refactor.py`)の 1 プロセスずつである。外部との出入りは GitHub の CLI(`gh`)と、各 CLI の確認コマンドと起動だけで、確認コマンドの呼び出し先は変えない。変わるのは 2 つである。起動する CLI の集合(cross-refactoring から agy が既定で外れ、ホストが入る)と、cross-review で同じ CLI を 2 プロセス起動しうることである。 - -### 置き場所 - -```text -plugins/ndf/ -├── scripts/lib/ -│ ├── assignment.py # P6: refactor_pool / resolve_participants / review_seats / impl_assign / seat_runtime を新設 -│ │ # P7: impl_pool / review_assign / assign を消す -│ ├── auth.py # P6: probe_auth を新設。P7: check_auth を消す -│ ├── statefile.py # P6: apply_resume_args / ResumeField を新設 -│ ├── monitor.py # P6: target の choices と agent の比較を seat_runtime へ -│ └── tests/ # P6: test_lib_assignment.py を impl_assign へ、test_lib_participants.py / test_lib_resume_args.py を新設 -├── skills/cross-review/ -│ ├── SKILL.md / docs/01 / docs/04 / docs/05 # P6 -│ ├── scripts/state.py / launch-reviewer.sh / critique.sh / critique-round.sh / wait-review.sh / measure.py # P6 -│ └── tests/ # P6: test_state_review_pool.py に追記、test_state_resume_args.py / test_seat_names.py を新設 -├── skills/cross-refactoring/ -│ ├── SKILL.md / docs/01-state-and-propose.md # P7 -│ ├── scripts/refactor.py / refactor_lib/commands/{setup,apply,gate,report}.py / refactor_lib/plan.py # P7 -│ └── tests/ # P6: test_assignment.py の review_assign を review_seats へ -│ # P7: test_assignment.py の assign を impl_assign へ、test_init.py / test_start_round_emits_runtimes.py を直す -└── CLAUDE.md(リポジトリの根) # P7 -``` - -Kiro と agy の配布ディレクトリ(`dev.kiro` / `dev.agy`)は Skill の実体を symlink で参照するため、書き写す配布物は無い。配布物の同期の検査(`bash scripts/build-runtime-plugins.sh --check`)で食い違いが無いことだけを確かめる。 - -## 構造 - -型を足すのは 2 つで、互いに関係を持たず、既存の型とも関係を持たない。参加者の記録(`Participants`、データクラス。契約文書の 7 項目)と、反映の表の 1 行(`ResumeField`、名前付きタプル)である。クラス図は作らない(要求の「対象範囲」)。**変わるのは処理の順序である。** - -## 処理の流れ - -変わるのは新規の初期化・ラウンドの開始・再開の初期化の 3 つの流れと、席の名前が流れる経路である。 - -### 新規の初期化(両 Skill で同じ形) - -```mermaid -graph TD - H[ホストを確定] --> P[母集合の既定を引く] - P --> RP[使える者の解決] - RP -->|名前の矛盾 / require_all で欠け| F[状態を作らず終了コード] - RP -->|通った| CR{Skill} - CR -->|cross-review| N{使える者が 2 者以上} - N -->|はい| W[状態ファイルを書く] - N -->|いいえ| HP[ホストを確認して fallback を決める] - HP -->|席を埋められる| W - HP -->|埋められない| F - CR -->|cross-refactoring| Z{使える者が 1 者以上} - Z -->|はい| W - Z -->|いいえ| F -``` - -使える者の解決の順序: - -| 順 | 何をするか | -| ---: | --- | -| 1 | 足す者・外す者の各名前が `ALL_RUNTIMES` にあり、重ならないことを確かめる。外す者の各名前が「母集合の既定 ∪ 足す者」に含まれなければ弾く(cross-review のホストはこれに当たる) | -| 2 | 参加者 = 母集合の既定 ∪ 足す者 − 外す者(`ALL_RUNTIMES` の順) | -| 3 | 1 者指定があれば参加者に含まれ、外す者に無いことを確かめ、参加者をその 1 者にする | -| 4 | 参加者の確認を止めない確認で行う。`NDF_SKIP_AUTH_CHECK` が立っていれば全員を通ったものとし `probe_skipped` を真にする | -| 5 | 全員を要する指定が真で通らない者がいれば `AssignmentError` | -| 6 | 通った者を `available`、通らなかった者と理由を `unavailable` として返す | - -### ラウンドの開始(cross-review) - -```mermaid -graph TD - S[start-round] --> G[前ラウンドの検査 prev.reviewers を渡す] - G --> R{ラウンドに reviewers がある} - R -->|はい| U[その値] - R -->|いいえ| O{only がある} - O -->|はい| U1["[only]"] - O -->|いいえ| PT{participants がある} - PT -->|はい| RS["review_seats(r, available, fallback)"] - PT -->|いいえ| HO{host がある} - HO -->|はい| LG["review_seats(r, review_pool(host), [])"] - HO -->|いいえ| L2[codex / agy] -``` - -### 再開の初期化(両 Skill で同じ形) - -```mermaid -graph TD - A[状態ファイルを読む] --> AR["apply_resume_args(state, args, 表)"] - AR --> N[知らせる行を出す] - N --> RA{担当に関わる引数を渡した} - RA -->|はい| RP[使える者の解決をやり直す] - RP -->|失敗| F[状態を書き換えず終了コード] - RP -->|通った| W[participants を書き resume_changes に 1 件積む] - RA -->|いいえ| W2[変えた項目だけ書く] - W --> E[start-round へ] - W2 --> E -``` - -### 席の名前が流れる経路(cross-review、埋め合わせがあるときだけ現れる) - -```text -start-round → REVIEWERS="codex claude-2" - → launch-reviewer.sh claude-2 … : CLI は seat_runtime → claude、stem は claude-2-review-pr - → monitor.py --agents codex,claude-2 : stem を組むだけ。CLI 固有のログの検査は seat_runtime で選ぶ - → state.py read-result claude-2 : rounds[-1]["claude-2"] へ書く - → critique-round.sh codex claude-2 → critique.sh claude-2 … -``` - -## 非機能の実現方式 - -要求の非機能 4 項目に、実現方式と確かめ方を対応づける。 - -| 大項目 | 要求の条件 | 実現方式 | 確かめ方 | -| --- | --- | --- | --- | -| 可用性 | 母集合の 1 者が使えないことで、どちらの収束ループも開始できない状態にならない | 確認の失敗を `unavailable` に記録し、使える者で続ける(決定 2・3)。cross-review は席を埋め合わせる(決定 9) | AC14、AC35 のテスト | -| 性能・拡張性 | 確認の回数は「参加者の数 + 埋め合わせが要るときのホスト 1 回」を超えない。再開で確かめ直すのは担当に関わる引数を渡したときだけ | 除外した者は確かめない。ホストは `available` が 2 者に満たないときだけ確かめる。再開は決定 14 の条件でだけ確かめる | AC3、AC26、AC28 のテストで `probe_auth` の呼び出しを数える | -| 運用・保守性 | 担当が欠けたまま収束したこと、席を埋め合わせたこと、反映しなかった引数が出力だけで分かる | `report` が参加者の節を出す。`init` が知らせる行を出す(決定 13・16) | AC24、AC29、AC39 のテスト | -| 移行性 | この変更の前に始めた実行の状態ファイルを書き換えずに読める | 項目が無いときの読み方を契約文書の「移行」が決める | AC22、AC41 のテスト | - -## 実装の分け方 - -P6(共通層と cross-review)→ P7(cross-refactoring と旧関数の削除)の順に出す(決定 19)。受け入れ条件は要求の文書の番号である。 - -| Pull Request | 中身 | 受け入れ条件 | -| --- | --- | --- | -| P6 | 共通層の新設(`probe_auth` / `resolve_participants` / `review_seats` / `impl_assign` / `seat_runtime` / `refactor_pool` / `apply_resume_args`)と cross-review 側 | AC1〜AC6、AC8〜AC30、AC44〜AC46、AC48〜AC50 | -| P7 | cross-refactoring 側、旧関数(`check_auth` / `impl_pool` / `review_assign` / `assign`)の削除、`CLAUDE.md` | AC7、AC31〜AC43、AC47、AC49〜AC50 | - -## テスト設計 - -置き場所の列の読み方は次のとおりである。 - -| 先頭 | 実際の場所 | -| --- | --- | -| `cross-refactoring/` / `cross-review/` | `plugins/ndf/skills/` の下 | -| `lib/` | `plugins/ndf/scripts/tests/` の下 | - -| 受け入れ条件 | 何で確かめるか | 置き場所 | -| --- | --- | --- | -| AC1〜AC5 | `probe` を差し替えて `resolve_participants` を呼び、返り値と例外と呼び出し回数を見る | `lib/test_lib_participants.py`(新設) | -| AC6 | `subprocess.run` を差し替えて `probe_auth` を呼ぶ(既存の `test_auth_probe.py` を書き直す) | `lib/test_auth_probe.py` | -| AC7 | `git grep` の結果を検査するテスト | `lib/test_shared_lib_layout.py`(既存に追記) | -| AC8〜AC12 | `review_seats` を 4 ホスト × 12 ラウンドと 2 / 1 / 0 者で呼ぶ。AC8 は変更前の式を期待値として持つ | `cross-refactoring/tests/test_assignment.py`(`review_assign` のテストを置き換える) | -| AC13 | `seat_runtime` に 7 つの名前を渡す | 同上 | -| AC14〜AC20 | `probe_auth` を差し替えて `_init_new_state` を呼ぶ。状態ファイルの有無と終了コードと標準エラーを見る | `cross-review/tests/test_state_review_pool.py` | -| AC21 | `read-result` を席の名前で呼ぶ。`launch-reviewer.sh` を `launch-cli.sh` を差し替えて呼び、渡った CLI 名と stem を見る | `cross-review/tests/test_seat_names.py`(新設) | -| AC22 | `participants` を持たない状態 / `host` も持たない状態で `_round_reviewers` | `cross-review/tests/test_state_review_pool.py` | -| AC23 | `verdict` の無い前ラウンドで `cmd_start_round` の終了コード 5 | `cross-review/tests/test_state_round_guard.py` | -| AC24 | `participants` と `resume_changes` を持つ状態ファイルで `cmd_report` の出力を見る | `cross-review/tests/test_state_review_pool.py` | -| AC25〜AC29 | 状態ファイルを置いた作業ツリーを渡して `cmd_init` を呼び、状態ファイルと標準エラーを見る。一部の引数だけを渡す組み合わせを含む | `cross-review/tests/test_state_resume_args.py`(新設) | -| AC30 | 2 ファイルの `ONLY` を含む行を数える | `cross-review/tests/test_skill_layout.py` | -| AC31〜AC33、AC35〜AC36 | `probe_auth` を差し替えて `cmd_init` を呼ぶ(既存の `test_init.py` の `check_auth` のテストを置き換える) | `cross-refactoring/tests/test_init.py` | -| AC34 | `impl_assign` を 6 ラウンド呼ぶ。`cmd_start_round` の出力に `REVIEWERS` が無い(`test_start_round_emits_runtimes.py` の期待値を反転する) | `lib/test_lib_assignment.py` / `cross-refactoring/tests/test_start_round_emits_runtimes.py` | -| AC37 | `cmd_report` と改修計画の出力を見る | `cross-refactoring/tests/test_run_metrics_summary.py` / `test_plan_comment.py` | -| AC38〜AC40 | 状態ファイルを置いて `cmd_init` を呼ぶ | `cross-refactoring/tests/test_init.py` | -| AC41 | `impl_capable` を持つ状態で `cmd_start_round` / `cmd_report` | 同上 | -| AC42〜AC44 | 文書の語を `grep` するテスト | `scripts/tests/test_cross_skill_refs.py` / `cross-review/tests/test_skill_layout.py` / `cross-refactoring/tests/test_skill_terms.py` | -| AC45〜AC48 | 各 issue の再現手順を実行し、結果を issue のコメントへ残す | 手元 | -| AC49〜AC50 | コマンドの終了コード | 継続的統合と手元 | - -再開の反映の表の規則(「反映する」「知らせる」、値が同じなら積まない)は、共通層のテスト(`lib/test_lib_resume_args.py`、新設)で Skill に依らず確かめる。 - -## 未確認のまま残ること - -4 件が残る。実装で決める 3 件は P6 で決まった(下の表の後)。残る 4 件は運用と #461 が決める。 - -| 項目 | 内容 | いつ決まるか | -| --- | --- | --- | -| 同じランタイムの 2 席の観点 | `claude` / `claude-2` の 2 席が、別のランタイムの 2 席より指摘を見落とすかは測っていない | この変更の後の運用(`measure.py`) | -| ホストが席に入ったときの `is_own_pr` の扱い | ホストの CLI が自分の Pull Request をレビューするとき、投稿の event が `COMMENT` へ倒れる既存の規則で足りるかは確かめていない。P6 では実機で回していない | P6 の後の運用で 1 度回して見る | -| cross-refactoring でホストが提案に入ることの所要 | 提案は最も遅い者を待つ。claude の提案の所要は測っていない(適用は中央値 2 分) | P7 の後の運用 | -| 確認を「言語モデルを引く最小の呼び出し」へ替えるか | #461。所要の実測が要る | マイルストーン 06 の着手時 | - -### P6 の実装で決めた 3 件 - -| 項目 | 決めたこと | -| --- | --- | -| `monitor.py` の CLI 固有の検査を席の名前に通す形 | 席の名前からランタイムを引く内部関数を 1 つ置き、CLI 固有の比較 3 か所をその関数で包む。**席の形に合わない名前はそのまま返す**(担当名を任意の骨格で受ける cross-refactoring の経路を壊さないため)。位置引数の選択肢は席の形を受ける型の検査へ替えた | -| 出力の文言 | 反映した行は `↻ <項目>: <旧> → <新>`、知らせる行は `ℹ --<引数> は再開では反映しません(状態: <値> / 指定: <値>)`、通らなかった者は `⚠ <名前> を担当から外しました(<理由>)`、埋め合わせは `⚠ 使える者が <数> 者のため、席を<相手>で埋めます(観点が減ります)`。既存の初期化の出力の印(`↻` / `ℹ` / `⚠`)に揃える | -| テストの置き場所 | テスト設計の表のとおり。席の埋め方は cross-refactoring の割り当てのテスト(変更前の席の割り当ての期待値が同じファイルにある)、起動と監視と計測の席の名前は cross-review の `tests/test_seat_names.py`(新設) | - -### P7 の実装で決めた 6 件 - -| 項目 | 決めたこと | -| --- | --- | -| 状態ファイルの確認の結果の項目(`auth`) | 新規の状態に書かない。読み手が無く、確認を通らなかった者と理由は参加者の記録(`participants.unavailable`)が持つ | -| 確認を行う位置 | 新規の経路で、作業ディレクトリの用意と範囲の関門の後、着手前のテストの前。状態ファイルの有無(新規か再開か)を見てから確かめるためで、再開では担当に関わる引数を渡したときだけ確かめる | -| 反映の表の中身 | 「反映する」は上限 4 つとテストの制限時間。「知らせる」はホスト・範囲・モデル・着手前のテスト・継続的統合の検査の名前・重要度の閾値・同期のコマンド・改修計画のファイル・起動のされ方・作業ディレクトリ root の 10 個。着手前のテストはコマンドで、モデルは全ランタイムの辞書で、作業ディレクトリ root は解決したパスで比べる | -| 引数の型の置き場所 | `--exclude` / `--include` の型(カンマ区切りの名前と予約語 `none`)は cross-refactoring の初期化の部品に置く。共通層へ移すと cross-review の状態の部品も触ることになり、並行する束(G5)と重なる | -| 完了報告の参加者の節 | cross-review と同じ行の形にし、席の埋め合わせの行は持たない(cross-refactoring に席は無い)。ラウンド表からはレビュー担当とモデルの列に加え、レビュー担当の判定から作っていた初回承認の列も消す | -| モデルの警告の対象 | 参加者だけ。既定で外れる agy は、足したときだけ警告する | - -## 申し送り(並行する設計との境界) - -並行する 4 つの設計と 2 つの issue との境界を、決めた契約と分担で書く。 - -| 相手 | 決めた契約 | どちらが何をするか | -| --- | --- | --- | -| G2(#732 #624) | `state.py` は同じファイルだが節が違う(G1 は `init` / 再開 / `_round_reviewers` / `read-result` の受け口 / `report` の参加者の節。G2 は `_classify_finding` / `COUNTED_CLASSIFICATIONS`)。**要求と契約の既存文書の先頭へ案内を足す点だけが重なる** | G1 が P5 の案内、G2 が P4 の案内をそれぞれ 1 段落足す。後からマージする側が並べる | -| G3(#729 #619 #584) | `state.py` の `_read_review_result_file` / `_record_no_result` / `_handle_no_result_round` と `report` の結末の節は G3。G1 が `report` に足すのは「参加した者」の節だけ。`monitor.py` は G3 が結末の理由を、G1 が `agent` の比較と `target` の `choices` を触る | 競合は後からマージする側が解く。G1 の `seat_runtime` は `monitor.py` の比較を包むだけで、G3 の語彙に触らない | -| G4(#728 #647 #592 #553) | `apply.py:134` と `gate.py:128` の `impl, _ = assignment.assign(seq, state["host"])` を G1(P7)が `impl = assignment.impl_assign(seq, state["runtimes"])` へ変える(各 1 行)。担当の交代(利用上限で次の輪番へ替える)は G4 が持ち、替える相手は `state["runtimes"]` から選ぶ | G4 は `assign` を新しく呼ばない。G1 の P7 と G4 の実装が同じ行を触ったら、後からマージする側が `impl_assign` に揃える | -| G5(#730 #583) | 起動スクリプト(`launch-reviewer.sh`)は G5 が投稿の経路を、G1 が席の受け口(先頭の `case` と `launch-cli.sh` へ渡す CLI 名)を触る | 競合は後からマージする側が解く | -| #461 | 確認コマンドの差し替え口は `AUTH_PROBES` のまま。`probe_auth` の返り値の形(`ok` / `detail`)は変えずに、コマンドだけを替えられる | #461 が実測して替える | -| #736 | `CLAUDE.md` の「`--max-outer-rounds` の既定が 4」の行は、輪番が参加者の数のラウンドで 1 周する形に合わせて G1 が書き直す | #736 は書き直した行を見て閉じるかを棚卸で決める | - -## 既存の設計との対応 - -既存の設計(PR #667)の各決定を、この文書のどの決定が引き継ぐかを示す。 - -| 既存の決定(PR #667) | この文書 | 変わったこと | -| --- | --- | --- | -| 決定 1(P4 / P5 の分け方) | 決定 19 | P5 を P6 / P7 に分け直した。P4 は #732 | -| 決定 6(確認を把握へ。`available_reviewers` を持つ) | 決定 2・3 | 解決を共通層へ移し、項目を `participants` に畳んだ。`check_auth` を消す | -| 決定 7(`--exclude`) | 決定 4 | `--include` を足した | -| 決定 8(使える者の数で分け、`review_assign` が一覧を受け取る) | 決定 9 | 1 者と 0 者を「席を埋める」に変えた。3 者の値は同じ | -| 決定 9(`--require-all`) | 決定 12 | 両 Skill に置く | -| 決定 10(cross-refactoring を変えない) | 決定 2・5・6 | 採らない。cross-refactoring も同じ層を通り、レビュー担当を消す | -| 決定 11(`--only` は 1 者) | 決定 9 | 同じ(埋め合わせをしない) | -| 決定 12(記録を先に見る) | 決定 11 | 同じ | -| 決定 13(既定を `None` に) | 決定 13 | 共通層へ移し、状態に載る引数は必ず表に載せる | -| 決定 14(`--host` は知らせる) | 決定 13 | 表の `notify` の 1 行になった | -| 決定 15(担当の引数で確かめ直す) | 決定 14 | `--include` を足した | -| 決定 16(`none`) | 決定 15 | `--include none` を足した | -| 決定 17(`resume_changes`) | 決定 16 | `participants` は 1 件として積む | -| 決定 18(`$ONLY` で絞らない) | 決定 17 | 同じ | -| 決定 19(自動で外さない) | 決定 18 | 同じ | -| — | 決定 1・5・7・8・10・20 | 新設 | - -## 関連文書 - -| 文書 | 何を持つか | -| --- | --- | -| [issue-727-687-478-664-648-requirements.md](issue-727-687-478-664-648-requirements.md) | 何を満たすか(目的・対象範囲・用語・受け入れ条件 AC1〜AC50) | -| [issue-727-687-478-664-648-contracts.md](issue-727-687-478-664-648-contracts.md) | 状態ファイル・引数・関数の形 | -| [issue-624-478-648-design.md](issue-624-478-648-design.md) | 既存の設計(PR #667)。この文書はその P5(決定 6〜19)を置き換える。引き継ぐ決定と変える決定は「既存の設計との対応」にある。P4(決定 2〜5)は #732 の設計が持つ | diff --git a/issues/issue-727-687-478-664-648-requirements.md b/issues/issue-727-687-478-664-648-requirements.md deleted file mode 100644 index 78daaa9e9..000000000 --- a/issues/issue-727-687-478-664-648-requirements.md +++ /dev/null @@ -1,363 +0,0 @@ -# cross-review / cross-refactoring: 参加する CLI が 1 者でも使えないと収束ループを開始できず、再開で渡した引数が黙って無視される → 使える者だけで開始し、cross-review は毎ラウンド 2 席を確保し、再開で渡した引数は反映されるか反映しないと知らされる(要求 / #727 #687 #478 #664 #648) - -## 目的 - -- **壊れていること**: 参加する CLI のどれか 1 者が導入・認証されていないと、cross-review / cross-refactoring の初期化が止まる。収束ループを開始できない(#478 / #687)。cross-refactoring には担当から agy を外す引数が無く、使える者が 2 者だとレビュー担当が 1 者になる(#664)。中断した収束ループを 1 者指定などの引数を変えて再開しても、引数が黙って無視される(#648) -- **困る人**: 4 つの CLI が揃っていない環境で収束ループを回す利用者と、中断したループを進め方を変えて再開する利用者 -- **直すと成り立つこと**: 使える者だけで開始でき、使えない者と理由が出力と状態ファイルに残る。cross-review は使える者が 2 者に満たなくても、ホスト、次に同じランタイムの 2 つ目で各ラウンドに 2 席を確保する。cross-refactoring の既定の参加者は codex / kiro とホストになる(ホストが codex / kiro なら 2 者、それ以外なら 3 者)。agy は足す者の指定で戻す。再開で渡した引数は反映されるか、反映しないと知らされる。これらの規則は両 Skill が共有する共通層が 1 か所で持つ - -この文書は「何を満たすか」を扱う。設計・既存の要求との関係は末尾の「関連文書」にある。 - -## 用語 - -受け入れ条件で使う語の意味を先に決める。受け入れ条件の本文は検査の入力と期待値の形をそのまま定めるため、識別子を業務用語へ置き換えずに書く。対応はこの表で引く。 - -| 用語 | 識別子 | 意味 | -| --- | --- | --- | -| ランタイム | `ALL_RUNTIMES` | `claude` / `codex` / `agy` / `kiro` の 4 つ | -| ホスト | `host` / `detect_host` | 収束ループを起動しているランタイム | -| 初期化 | `init` | 収束ループを新規に始める・再開する副コマンド。cross-review は `state.py init`、cross-refactoring は `refactor.py init` | -| ラウンドの開始 | `start-round` | 次のラウンドを開き、担当を決めて返す副コマンド | -| 結果の受け口 | `read-result` | 担当の結果ファイルを状態ファイルへ写す副コマンド | -| 完了報告 | `report` | 収束の結果を出す副コマンド | -| 母集合の既定 | `review_pool(host)` / `refactor_pool(host)` | Skill ごとに決まる参加者の出発点。cross-review は全ランタイム − ホスト、cross-refactoring は codex / kiro / ホスト | -| 足す者 / 外す者 | `--include` / `--exclude` | 参加者を名指しで足す・外す引数 | -| 参加者 | `runtimes`(cross-refactoring) | 母集合の既定に足す者を加え、外す者を除いた一覧。確認の対象 | -| 使える者の解決 | `resolve_participants` | 母集合・足す者・外す者・確認の結果から使える者を決める共通層の関数 | -| 止めない確認 | `probe_auth` | 認証の確認コマンドを走らせ、止めずに結果だけを返す共通層の関数。従来の `check_auth` は 1 件の失敗で止める | -| 使える者 | `participants.available` | 参加者のうち確認を通った者。1 者指定があればその 1 者 | -| 1 者指定 | `--only` | 担当を 1 者に固定する引数。値 `none` で外す | -| 全員を要する指定 | `--require-all` | 確認の失敗が 1 者でもあれば止める引数 | -| 席 | `seat_runtime` / `SEAT_PATTERN` | ラウンドで 1 つの CLI プロセスが占める場所。名前はランタイム名か `<ランタイム>-<2〜9>`(同じランタイムの 2 つ目以降) | -| 席の埋め方 | `review_seats` | 使える者と埋め合わせから 2 席を返す共通層の関数 | -| 適用の輪番 | `impl_assign` | 参加者から実装担当 1 者を返す共通層の関数 | -| 担当 | `rounds[].reviewers` / `rounds[].impl` | そのラウンドの席を占める者。前者は cross-review、後者は cross-refactoring | -| 埋め合わせ | `participants.fallback` | 使える者が 2 席に足りないとき、ホスト、次に同じランタイムの 2 つ目で席を埋めること | -| 再開 | — | 状態ファイルが残り `final` が `null` のときの初期化 | -| 再開で変えた値の記録 | `resume_changes` | 再開で変えた値を積む状態ファイルの項目 | - -## 対象範囲 - -変えるのは共通層の 3 ファイルと、両 Skill の初期化・担当・報告・文書である。指摘の数え方・監視・適用の取り込みは他の設計が持つ。 - -含む: - -| 場所 | 中身 | -| --- | --- | -| 共通層 `lib/assignment.py` | 既定の母集合(Skill ごと)、使える者の解決、席の埋め方、適用の輪番、席の名前 | -| 共通層 `lib/auth.py` | 止めない確認(`probe_auth`)。1 件の失敗で `die` する `check_auth` を消す | -| 共通層 `lib/statefile.py` | 再開で明示的に渡した引数だけを状態へ重ね、反映しない引数を知らせる | -| cross-review | `init`(新規と再開)、`start-round` の担当、`read-result` と起動スクリプトの席の受け口、`report`、`SKILL.md` と `docs/`(01 / 04 / 05) | -| cross-refactoring | `init`(新規と再開)、`start-round` と適用の輪番、`report` / 改修計画の表示、`SKILL.md` と `docs/01` | -| リポジトリ | `CLAUDE.md` の cross-refactoring と cross-review の節 | -| テスト | 共通層と両 Skill | - -含まない: - -| 扱わないもの | 理由 | -| --- | --- | -| 反証する担当がいない指摘の数え方(#624、既存の P4) | #732(G2)の設計が持つ | -| 確認を「言語モデルを引く最小の呼び出し」へ替えること | #461。確認コマンドの所要と形の実測が要る。この変更の共通層は確認の手段を差し替えられる形にする | -| 起動した後に分かる使えなさ(利用上限・言語モデルの 404)で担当を自動で外すこと | #729(G3)が理由の語彙を持つ。この変更は利用者が外す者の指定で外し、再開で反映できる入口までを作る | -| 監視の上限・結末の語彙・投稿の重なり | G3 / G5 の範囲 | -| cross-refactoring の適用ラウンドの取り込みと担当の交代 | #728(G4)の範囲 | -| 提案者と適用者が同じランタイムになることを避ける割り当て | 採らないと決めた(設計文書の決定 8) | -| 再開で検証コマンド(`--verify-command`)を空へ戻す手段 | 置き換えはできる。空へ戻す要求は出ていない | -| クラス図 | 型を追加するのは `Participants` 1 つで、関係を持つ型が無い。形は契約文書のデータ構造が持つ | -| `CHANGELOG.md` と版数 | 配布の工程が書く | - -## 前提 - -この要求は次の 5 つを前提に書いている。 - -| # | 前提 | -| --- | --- | -| 1 | cross-refactoring のレビュー工程は #436 で消えており(Step 7 の cross-review が担う)、従来の割り当て(`assign()`)が返すレビュー担当は状態ファイルへの記録と表示にしか使われない | -| 2 | 使える者の確認は、認証の確認コマンド(`AUTH_PROBES`)のままである。言語モデルを引く最小の呼び出しへ替える判断は #461 が持つ。この変更が作るのは、確認の結果で使える者を決める入口である | -| 3 | 再開の初期化の後、骨組みは必ずラウンドの開始で新しいラウンドを開く。開いたまま中断したラウンドの担当を書き換える必要は無い | -| 4 | G2(#732)が同じ `state.py` の `_classify_finding` を、G3(#729)が `_read_review_result_file` / `_record_no_result` / `report` を、G5(#730)が投稿の経路を触る。この変更が触る節は `init` / 再開 / `_round_reviewers` / `start-round` / `read-result` の担当名の受け口 / `report` の参加者の節である | -| 5 | 同じランタイムの 2 つの CLI プロセスは、別の作業文脈を持てば独立した意見として扱う(#687 の利用者の指示) | - -## 前提とする取り決め - -実装が従う置き場所・書き方・テストの形である。 - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | 担当の決め方は `plugins/ndf/scripts/lib/assignment.py`、確認は `lib/auth.py`、再開の反映は `lib/statefile.py` に置く。判定と状態の鍵は各 Skill の `state.py` / `refactor_lib` が持ち、骨組みは結果を使うだけにする | -| コーディング規約 | 状態ファイルを最小の形で組み、関数を直接呼んで確かめる(`AGENTS.md` の DO)。分岐は表(データ)で持つ(`refactoring` の「分岐をデータ化」) | -| テスト戦略 | 共通層は `plugins/ndf/scripts/tests/` の関数テスト、Skill は既存の形(`conftest.py` の `state_mod` / `crossref_helpers`)で GitHub と CLI の起動を差し替える | - -## 境界 - -承認なしに行うこと・確認してから行うこと・行わないことを分ける。 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 既存テストの実行、配布物の同期の検査、文書の検査 | -| 確認してから行う | 初期化の既定を「確認の失敗で止める」から「使える者で回す」へ変えること。cross-refactoring の既定から agy を外すこと。どちらも設計 Pull Request の承認で確認する | -| 行わない | 監視・起動・投稿の経路の変更、`refactor_lib` の適用の取り込みの変更、確認コマンドの差し替え | - -## 影響 - -変わるのは初期化の振る舞い、既定の参加者、状態ファイルの形、初期化の引数、共通層の関数、担当名の形である。 - -| 対象 | 影響 | -| --- | --- | -| 認証に失敗する CLI がある利用者 | どちらの初期化も止まらず、使える者で回る。従来の関門は全員を要する指定(`--require-all`)で選べる | -| cross-refactoring の既定の参加者 | agy が既定から外れ、ホストが提案と適用に入る。`--include agy` で戻せる。提案者と適用者が同じランタイムになりうる | -| cross-review で使える者が 2 者に満たない利用者 | ホスト、次に同じランタイムの 2 つ目が席を埋める。1 者で回るのは 1 者指定を渡したときだけになる | -| 状態ファイルの形 | 両 Skill の最上位に `participants` と `resume_changes` が増える。cross-refactoring の `impl_capable` とラウンドの `reviewers` / `reviewer_models` が新規の状態から消える。無い項目は従来の読み方で読む | -| 初期化の引数 | 両 Skill に `--exclude` / `--include` / `--require-all` が増える。cross-review の `--only` が `none` を取る。既定値は変わらない | -| 共通層の関数 | `check_auth` / `impl_pool` / `review_assign` / `assign` が消え、`probe_auth` / `resolve_participants` / `review_seats` / `impl_assign` / `seat_runtime` / `refactor_pool` が入る。呼び出し側は両 Skill だけである | -| 担当名の形 | ランタイム名に `-2`〜`-9` の接尾辞を持つ席の名前が、結果ファイルの stem と状態ファイルの鍵に現れうる | -| 再開で修正の記録の無い前ラウンドがある実行 | 担当が `codex` / `agy` 以外のラウンドでも、前ラウンドの検査が止める(AC23) | - -## 受け入れ条件 - -50 件を、共通層・cross-review・cross-refactoring・文書・子 issue の再現・全体の 9 つの塊に分ける。 - -### 共通層: 使える者の解決と確認 - -- [ ] AC1: 母集合 3 者のうち 1 者の確認が失敗する `probe` を `resolve_participants` に渡す。返る値の `available` は残り 2 者(母集合の順)、`unavailable` はその 1 者と理由を持ち、例外は上がらない -- [ ] AC2: AC1 と同じ入力で `require_all=True` を渡すと `AssignmentError` が上がり、メッセージに欠けた者の名前と理由が含まれる -- [ ] AC3: `exclude` に含めた者に対して `probe` が呼ばれない。`include` で足した者は呼ばれる(呼び出しの回数と引数で確かめる) -- [ ] AC4: 次の 4 つはいずれも `AssignmentError` になる。`include` と `exclude` に同じ名前 / `ALL_RUNTIMES` に無い名前 / `only` が `exclude` に含まれる / `only` が参加者に無い -- [ ] AC5: `NDF_SKIP_AUTH_CHECK` が立つと、`available` は参加者の全員、`probe_skipped` は真で、確認コマンドは 1 回も呼ばれない -- [ ] AC6: `probe_auth` は失敗で例外を上げず、`ok: false` と理由(`コマンドが見つかりません` / 時間切れ / 終了コード非 0 / 未認証の文言)を返す。成功は `ok: true` -- [ ] AC7: P7 の後、`check_auth` / `impl_pool` / `review_assign` / `assign` の 4 つを `git grep -n` で探す。`plugins/ndf/scripts/lib/` と両 Skill の `scripts/` で 0 件になる - -### 共通層: 席の埋め方(cross-review の規則) - -- [ ] AC8: 使える者が 3 者のとき、`review_seats(r, available, [])` は変更前の `review_assign(r, host)` と一致する。4 つのホスト × ラウンド 1〜12 の全組で確かめる -- [ ] AC9: 使える者が 4 者(`--include` でホストを足した)のとき、毎ラウンド 2 席で、ラウンド 1〜4 で各者がちょうど 2 回担当になる -- [ ] AC10: 使える者が 2 者のとき、ラウンド 1〜4 の全部でその 2 者が返る -- [ ] AC11: 使える者が 1 者(`codex`)のとき、埋め合わせに `["claude"]` を渡すと `["codex", "claude"]`、空を渡すと `["codex", "codex-2"]` が返る -- [ ] AC12: 使える者が 0 者のとき、埋め合わせに `["claude"]` を渡すと `["claude", "claude-2"]`、空を渡すと `AssignmentError` になる -- [ ] AC13: `seat_runtime("kiro-2")` と `seat_runtime("kiro")` は `kiro` を返す。`gemini` / `kiro-1` / `kiro-10` / `kiro-2-3` は `AssignmentError` になる - -### cross-review: 新規の初期化 - -- [ ] AC14: ホスト `claude` で `kiro` の確認が失敗する。`init` は終了コード 0 で状態ファイルを作る。`participants.available` は `["codex", "agy"]` で、`participants.unavailable.kiro` に理由が入る。標準エラーに `kiro` を外したことが 1 行出る -- [ ] AC15: AC14 と同じ状態で `--require-all` を付けると、`init` は終了コード 1 で終わり、状態ファイルを作らない -- [ ] AC16: `--exclude agy` を渡すと `agy` の確認を行わない。`participants.excluded` が `["agy"]`、`available` が `["codex", "kiro"]` になる。`--exclude agy --exclude kiro` と `--exclude agy,kiro` は同じ状態ファイルを作る -- [ ] AC17: ホスト `claude` で `--include claude` を渡すと、`available` が 4 者になり、`start-round` が 2 席を返す -- [ ] AC18: 使える者が `codex` の 1 者で、ホストの確認が通る。`init` は終了コード 0 で終わり、観点が減ることを 1 行出す。`participants.fallback` は `["claude"]`、`start-round` は `codex claude` を返す。`--only codex` のときはホストを確かめない。`participants.fallback` は空で、`start-round` は `codex` だけを返す(確認コマンドの呼び出しは `codex` の 1 回) -- [ ] AC19: 使える者が 0 者でホストの確認が通ると、`init` は終了コード 0 で終わり、`start-round` は `claude claude-2` を返す。ホストの確認も通らないと `init` は終了コード 1 で終わり、状態ファイルを作らない -- [ ] AC20: 次の 3 つはいずれも終了コード 1 で終わり、状態ファイルを作らない。`--exclude claude`(ホスト)/ `--only codex --exclude codex` / `--include agy --exclude agy` -- [ ] AC21: `read-result claude-2` が受け付けられ、`rounds[-1]["claude-2"]` に結果を書く。`launch-reviewer.sh claude-2 ` は `claude` の CLI を起動する。stem は `claude-2-review-pr` になる(起動は差し替えて確かめる) -- [ ] AC22: `participants` を持たない状態ファイルで、`host` があれば `start-round` は変更前の輪番を返す。`host` も無ければ `codex` / `agy` を返す -- [ ] AC23: 前のラウンドが `verdict` を持たず、担当 `agy` + `kiro` の両者が `REQUEST_CHANGES` で修正の記録が無い。このとき `start-round` は終了コード 5 で止まる -- [ ] AC24: `report` が「参加した者」の節を出す。行は 6 つで、使える者 / `--exclude` で外した者 / `--include` で足した者 / 確認を通らなかった者(理由つき)/ 埋め合わせ / 再開で変えた値である。`participants` を持たない状態ファイルでは「記録なし」と出す - -### cross-review: 再開の初期化 - -- [ ] AC25: `max_rounds: 12` の状態ファイルへ `--max-rounds 20` を渡す。`max_rounds` が 20 になり、`12 → 20` の形で 1 行出る。`resume_changes` に `{field: "max_rounds", from: 12, to: 20}` が 1 件積まれる。`--rotate-after` / `--verify-command` / `--verify-exit-code` も同じく反映され、後の 2 つは置き換える -- [ ] AC26: 引数を渡さない再開では、次の 6 項目が変わらず、確認コマンドは 1 回も呼ばれない。`max_rounds` / `rotate_after` / `verify_commands` / `verify_exit_codes` / `only` / `participants` -- [ ] AC27: `only: null` の状態ファイルへ `--only codex` を渡すと `only` が `codex` になり、次の `start-round` が `codex` だけを返す。記録を持つ過去のラウンドの `reviewers` は変わらない。`--only none` は `only` を `null` へ戻す -- [ ] AC28: `--exclude agy` を渡した再開では、参加者の確認をやり直し、`available` から `agy` が消え、次の `start-round` が `agy` を返さない。`--exclude none` は除外を空へ戻す。`participants.included` が `["claude"]` の状態へ `--exclude agy` だけを渡すと、`included` は `["claude"]` のまま残る。`excluded` が `["agy"]` になる(渡さなかった引数は状態ファイルの値で補う) -- [ ] AC29: `host: "claude"` の状態ファイルへ `--host codex` を渡すと `host` は変わらず、反映しないことが 1 行出る。`--host claude` では何も出ない -- [ ] AC30: `SKILL.md` と `docs/01-state-and-review.md` で `grep -n 'ONLY'` が当たる行は、`init` へ引数を渡す行と引数の説明の行だけになる。起動・監視・取り込み・反証の担当は `$REVIEWERS` / `$REVIEWERS_CSV` を使う - -### cross-refactoring: 母集合と担当 - -- [ ] AC31: ホスト `claude` の新規の `init` で、状態ファイルの `runtimes` は `["claude", "codex", "kiro"]` になり、`agy` の確認は行われない。状態ファイルに `impl_capable` は無く、標準出力に `IMPL_POOL=` の行は無い -- [ ] AC32: ホスト `codex` では `runtimes` が `["codex", "kiro"]`、ホスト `agy` では `["codex", "agy", "kiro"]` になる -- [ ] AC33: `--include agy` で `runtimes` が 4 者に、`--exclude kiro` で 2 者になる。ホスト `claude` で `--exclude claude` を渡すと `runtimes` が `["codex", "kiro"]` になる。`init` は終了コード 0 で終わる(ホストは母集合に含まれるため外せる) -- [ ] AC34: `impl_assign(r, ["claude", "codex", "kiro"])` をラウンド 1〜6 で呼ぶ。返る値は `codex` / `kiro` / `claude` / `codex` / `kiro` / `claude` である。`start-round` は `REVIEWERS` / `REVIEWERS_CSV` を出さない。ラウンドの記録に `reviewers` / `reviewer_models` が無い -- [ ] AC35: `kiro` の確認が失敗しても `init` は終了コード 0 で終わる。`participants.unavailable.kiro` に理由が入り、`runtimes` は 2 者になる。`--require-all` を付けると終了コード 4 で終わり、状態ファイルを作らない -- [ ] AC36: 使える者が 0 者のとき `init` は終了コード 4 で終わり、状態ファイルを作らない -- [ ] AC37: `report` と改修計画の表示にレビュー担当の列が無く、母集合を 1 行で出す(「提案・レビュー」と「適用の母集合」の 2 行に分けない) - -### cross-refactoring: 再開の初期化 - -- [ ] AC38: `max_outer_rounds: 3` の状態ファイルへ `--max-outer-rounds 5` を渡す。5 になり、`3 → 5` の形で 1 行出て、`resume_changes` に 1 件積まれる。`--max-test-rounds` / `--max-fix-rounds` / `--max-items-per-round` も同じ -- [ ] AC39: 再開で `--model codex=x` / `--host codex` / `--scope other` を渡すと、状態は変わらず、反映しないことが引数ごとに 1 行出る。状態に載る他の引数(`--baseline-test` など。契約文書の表)も同じ扱いである。引数を渡さない再開では、上限 4 項目と `models` と `runtimes` が変わらない -- [ ] AC40: 再開で `--exclude kiro` を渡すと参加者の確認をやり直す。`runtimes` から `kiro` が消え、次の `start-round` の `RUNTIMES` に `kiro` が無い。`--include agy` で始めた状態へ `--exclude kiro` だけを渡すと、`included` の `agy` は残る -- [ ] AC41: `impl_capable` を持ち `participants` を持たない状態ファイル(この変更の前に始めた実行)を、`start-round` / `report` が読める。適用の輪番は `runtimes` から決まる - -### 文書 - -- [ ] AC42: `CLAUDE.md` の cross-refactoring の節が「codex / kiro とホスト(ホストが codex / kiro なら 2 者)」を書く。同じ節が「適用担当は参加者の数のラウンドで 1 周する」を書く。「ホストを除く 3 者」「参加する 4 者」を含まない。cross-review の節が「codex / agy の両方」を含まない。次の 3 つがいずれも 0 行を出す - - ```bash - grep -n "ホストを除く 3 者" CLAUDE.md - grep -n "参加する 4 者" CLAUDE.md - grep -n "codex / agy の両方" CLAUDE.md - ``` - -- [ ] AC43: cross-refactoring の `SKILL.md` の「担当の決め方」が母集合を 1 つの表で書く。引数の表と `argument-hint` に `--exclude` / `--include` / `--require-all` がある。「前提」から「すべてログイン済み」が消える。ホストごとに要る CLI の表が `codex` / `kiro-cli`(ホストが codex / kiro ならもう 1 つ)になる。`docs/01-state-and-propose.md` の `init` が返す変数の表に `IMPL_POOL` が無い -- [ ] AC44: cross-review の次の 4 ファイルが、それぞれの内容を書く - - | ファイル | 書く内容 | - | --- | --- | - | `SKILL.md` | 引数の表と `argument-hint` に `--exclude` / `--include` / `--require-all`。`--only` の説明から「デバッグ用」が消える。母集合の行が席の規則を指す | - | `docs/05-pool-and-convergence.md` | 使える者の解決と席の埋め方(3 者以上 / 2 者 / 1 者 / 0 者)、`--exclude` / `--include`、確認が把握になったこと | - | `docs/04-contracts.md` | 状態ファイルの `participants` と `resume_changes`、席の名前の形 | - | `docs/01-state-and-review.md` | 再開で渡した引数の扱いが `docs/04-contracts.md` にあることへの案内 | - -### 子 issue の再現手順 - -- [ ] AC45: #478 の再現(`kiro-cli` が無い環境で `init`)で、`init` が終了コード 0 で終わる。#687 の場面 3(`codex` が使えない)で、`--exclude codex` を付けずに `init` が開始できる -- [ ] AC46: #648 の再現(再開の `init` に `--only codex`)で、次の `start-round` が `codex` だけを返す -- [ ] AC47: #664 の再現(`git grep -n '"--exclude"' -- plugins/ndf/skills/cross-refactoring`)が 1 行以上を出す -- [ ] AC48: #461 の再現(言語モデルを引けない CLI が確認を通る)は、この変更の後も現象が残ることを確かめて記録する(直す判断は #461 が持つ) - -### 全体 - -- [ ] AC49: `uv run --with pytest pytest scripts/tests plugins/ndf -q` が通る -- [ ] AC50: 次の 6 つが終了コード 0 で終わる - - ```bash - bash scripts/build-runtime-plugins.sh --check - claude plugin validate . - python3 scripts/check-skill-frontmatter.py - python3 scripts/check-doc-staleness.py --root . - python3 scripts/check-markdown-links.py --root . - python3 scripts/check-skill-shell-vars.py - ``` - -## 非機能の条件 - -受け入れ条件のうち可用性・性能・運用・移行に当たるものを、大項目で束ねる。 - -| 大項目 | 条件 | -| --- | --- | -| 可用性 | 母集合の 1 者が使えないことで、どちらの収束ループも開始できない状態にならない(AC14、AC35) | -| 性能・拡張性 | 確認の回数は、新規の初期化で「参加者の数 + 埋め合わせが要るときのホスト 1 回」を超えない。除外した者は確かめない。再開で確かめ直すのは担当に関わる引数を渡したときだけである | -| 運用・保守性 | 担当が欠けたまま収束したこと、席を埋め合わせたこと、再開で反映しなかった引数が、完了報告と初期化の出力だけで分かる(AC24、AC29、AC39) | -| 移行性 | この変更の前に始めた実行の状態ファイルを、書き換えずに読める(AC22、AC41) | - -## 検証手段 - -テスト・配布物の同期・文書の検査・手動確認の 4 つで確かめる。 - -| 項目 | 手段 | -| --- | --- | -| テスト | `uv run --with pytest pytest scripts/tests plugins/ndf -q` | -| 配布物の同期 | `bash scripts/build-runtime-plugins.sh --check` | -| 定義と文書の検査 | AC50 の 6 つ | -| 手動確認 | P7 の後、ホスト claude で `--exclude codex` を付けた cross-review と、既定の cross-refactoring を 1 本ずつ回し、完了報告で参加者と席を見る | - -## 未決 - -この変更の外で決まる 2 件を残す。 - -| 項目 | 誰が決めるか | 期限 | -| --- | --- | --- | -| 確認を「言語モデルを引く最小の呼び出し」へ替えるか、替えるならランタイムごとのコマンドと所要 | #461 | マイルストーン 06 の着手時 | -| 同じランタイムの 2 席が、別のランタイムの 2 席より指摘を見落とすか | この変更の後の運用(`measure.py`) | 実測が 3 本たまった時点 | - -## 既存の受け入れ条件との対応 - -既存の設計(PR #667)の P5 の受け入れ条件を、この文書のどこが引き継ぐかを示す。 - -| 既存 | この文書 | 変わったこと | -| --- | --- | --- | -| AC10 | AC14 | 項目が `available_reviewers` から `participants.available` へ。値は同じ | -| AC11 | AC15 | 同じ | -| AC12 | AC5 | 共通層の条件として書き直した | -| AC13 | AC16 | 同じ | -| AC14 | AC20 | `--include` と `--exclude` の矛盾を足した | -| AC15 | AC8 | 関数が `review_assign` から `review_seats` へ。値は同じ | -| AC16 | AC10 | 同じ | -| AC17 | AC18 | 1 者で回すのではなく、ホストで 2 席目を埋める。1 者のまま回るのは `--only` だけ | -| AC18 | AC19 | 0 者で失敗するのではなく、ホストが通れば 2 席を埋める。ホストも通らないときだけ失敗する | -| AC19 | AC22 | 同じ | -| AC20 | AC7、AC34、AC35 | 「cross-refactoring は変わらない」から「cross-refactoring も同じ層を通る」へ。`check_auth` は消える | -| AC21 | AC24 | `--include` で足した者と埋め合わせの行を足した | -| AC22 | AC25 | 同じ | -| AC23 | AC26 | 項目が `participants` に畳まれた | -| AC24 | AC27 | 同じ | -| AC25 | AC27、AC28 | 同じ | -| AC26 | AC28 | 同じ | -| AC27 | AC29 | 同じ | -| AC28 | AC30 | 同じ | -| AC29 | AC23 | 同じ | -| AC30 | AC44 | `--include` と席の規則を足した | -| AC31 | AC49 | 同じ | -| AC32 | AC50 | 文書の検査 3 つを足した | -| — | AC1〜AC4、AC6、AC9、AC11〜AC13、AC17、AC21 | 新設(共通層の解決と席) | -| — | AC31〜AC43 | 新設(cross-refactoring と `CLAUDE.md`) | -| — | AC45〜AC48 | 新設(子 issue の再現手順) | - -## 依頼(原文) - -各 issue の本文から抜粋した原文である。原文は書き換えない。全文は `gh issue view 727` / `687` / `478` / `664` / `648` で読む。 - -### #727(根本原因の親) - -> **参加する CLI が実行できるかを確かめ、使える者から担当を割り当てる共通層。** 場所は `plugins/ndf/scripts/lib/auth.py` の `check_auth`(36 行目)と `assignment.py` の `review_pool` / `review_assign` / `assign`(78 / 95 / 114 行目)である。 -> -> - `check_auth` は認証の成否だけを見て、1 件の失敗で `die` する。モデルを引けるか・更新トークンが生きているかは見ない -> - `assignment.py` は母集合を「全ランタイム − ホスト」の定数から作り、使える者を入力に取らない -> - cross-review(`state.py` の `init`)と cross-refactoring(`refactor_lib/commands/setup.py` の `cmd_init`)の両方が、この層を使う -> -> ## 採る手 -> -> 移動(`move_responsibility`)。使える者の決定を、各 Skill の初期化の関門から共通層の割り当てへ移す。 -> -> **cross-refactoring の既定の母集合は codex / kiro / ホストとし、agy を外す**(#664 / #687 の 2026-09-18 の決定。CLI の起動 199 回のうち失敗は agy の 7 回だけで、提案の所要も agy が最も長かった)。提案も適用もこの 3 者で回す。cross-review と共有する `assignment.py` で規則を 1 つにし、ホストが codex / kiro のとき(母集合が 2 者)の扱いは #687 の規則(各ラウンド 2 者を確保する。同一ランタイム 2 つ・ホストの参加を許す)に従う。 -> -> ## 完了条件 -> -> - 共通層が「最小の呼び出しが通るか」で使える者を決めて返し、割り当てがその一覧から担当を選ぶ -> - cross-review と cross-refactoring の両方がこの層だけを通り、片方にだけ古い形が残らない -> - `CLAUDE.md` の cross-refactoring の節(「ホストを除く 3 者」「参加する 4 者から輪番」)と `cross-refactoring/SKILL.md` の「担当の決め方」を、実装後の母集合に合わせる -> - 各子 issue の再現手順を実行し、現象が出ないことを確かめる(子 issue はその時点の棚卸が「閉じてよい」で閉じる) - -### #687 - -> - **各ラウンドで 2 者がレビューできることを最優先にする。** 誰が担当かより、2 つの目で見ることを優先する -> - **文脈が分かれていれば、同じランタイムを 2 つ立ててよい。** 別プロセス・別文脈なら独立した意見になる -> - **コードを書いたランタイム(ホスト)が担当に入ってよい。** 輪番の形にはこだわらない -> - **他にランタイムが 1 つも無ければ、ホストを 2 つ走らせる形でよい** -> -> **置き場所も決める。** 規則が決まっても、置き場所が決まらなければ次に使う人へ届かない。 -> -> **cross-refactoring では、既定の担当から agy を外し、ホストのランタイムを輪番へ入れる**と決めた(利用者の指示)。提案も適用も codex / kiro / ホストの 3 者になる。 - -### #478 - -> **認証確認を「関門」から「導入状況の把握」へ変える。** 使える者でレビューし、使えない者は最初から数えない。 -> -> 1. `init` で母集合の各ランタイムの認証を確かめ、**通った者の一覧を `state.json` へ記録する**(`available_reviewers`) -> 2. **明示的に外す手段を用意する**(例: `--exclude agy`)。**認証は通るが実行で落ちる担当を外す用途でも使う。** -> 3. 使える者の数で分岐する(3 者: 現行どおり / 2 者: 毎ラウンドその 2 者 / 1 者: 警告して 1 者 / 0 者: 失敗) -> 4. `review_assign()` は「**使える者が 3 者以上のときだけ 1 者を外す**」に変える -> 5. 全員揃っていることを要求したい運用のために `--require-all` を用意する -> 6. 完了報告に「このループに参加したのは誰か」を出す - -### #664 - -> `cross-refactoring` には、担当から特定のランタイムを外す引数が無い。 -> -> **外す手段を足すだけでは足りない。** `assign()` は実装担当を先に決めてから、提案・レビューの母集合から実装担当を除いた者をレビュー担当にする。使える者が 2 者だと、実装担当が codex か kiro のラウンドではレビュー担当が 1 者になる。 -> -> | 母集合 | いま | 変えた後 | -> |---|---|---| -> | 提案(`review_pool(host)` ) | 全ランタイム − ホスト(codex / agy / kiro) | codex / kiro / ホスト | -> | 適用(`impl_pool()` ) | 4 者すべて | codex / kiro / ホスト | -> -> - 外す手段(`--exclude` )を足すだけでなく、**既定の母集合から agy を外す**。agy を戻す手段を残すかは設計で決める -> - 同じ項目の提案者と適用者が重ならないよう、割り当てで避けるかは設計で決める -> - ホストが codex / kiro のときは、母集合が 2 者になる。そのときの扱いは #687 の規則に従う -> - `CLAUDE.md` の cross-refactoring の節(「codex / agy / kiro / claude のうちホストを除く 3 者」「参加する 4 者から輪番」)もあわせて直す - -### #648 - -> `/ndf:cross-review` を中断・再開すると、`state.py init` に渡した `--only` / `--max-rounds` / `--rotate-after` / `--verify-command` / `--verify-exit-code` / `--host` が**黙って無視される**。 -> -> **`cross-refactoring` にも同じ形がある。** `refactor.py` の `init` が受ける `--max-outer-rounds` / `--max-test-rounds` / `--max-fix-rounds` / `--max-items-per-round` / `--model` は再開時に反映されない。 -> -> ## 修正レイヤー -> -> `plugins/ndf/scripts/lib/statefile.py` に置く、再開時の引数の反映の契約。「明示的に渡された引数だけを状態へ重ね、反映しない引数は渡されたら知らせる」を 1 か所で持つ。 - -## 関連文書 - -| 文書 | 何を持つか | -| --- | --- | -| [issue-727-687-478-664-648-design.md](issue-727-687-478-664-648-design.md) | どう作るか(決定の記録・実測・構成要素・処理の流れ・テスト設計) | -| [issue-727-687-478-664-648-contracts.md](issue-727-687-478-664-648-contracts.md) | 状態ファイル・引数・関数の形 | -| [issue-624-478-648-requirements.md](issue-624-478-648-requirements.md) | 既存の要求(PR #667)。この文書はその P5(AC10〜AC30)を置き換える(対応は「既存の受け入れ条件との対応」)。P4(#624)は #732 の設計が持つ。既存の文書の本体は触らない | diff --git a/issues/issue-727-p6-participants-plan.md b/issues/issue-727-p6-participants-plan.md deleted file mode 100644 index fe495e30b..000000000 --- a/issues/issue-727-p6-participants-plan.md +++ /dev/null @@ -1,208 +0,0 @@ -# cross-review / cross-refactoring: 参加する CLI が 1 者でも使えないと収束ループを開始できず、再開で渡した引数が黙って無視される → 使える者だけで開始し、cross-review は毎ラウンド 2 席を確保し、再開で渡した引数は反映されるか反映しないと知らされる(実装計画 P6: 共通層と cross-review / #727 #687 #478 #648) - -## 関連リンク - -- 親 issue #727、子 issue #687 #478 #648(#664 は 2 本目の Pull Request が扱う) -- 設計: [issue-727-687-478-664-648-design.md](issue-727-687-478-664-648-design.md)(決定 20 件。用語の対応表はこの文書の識別子の引き先) -- 要求: [issue-727-687-478-664-648-requirements.md](issue-727-687-478-664-648-requirements.md)(受け入れ条件 AC1〜AC50) -- 契約: [issue-727-687-478-664-648-contracts.md](issue-727-687-478-664-648-contracts.md)(状態ファイル・引数・関数の形) -- 設計 Pull Request: #782(2026-09-19 マージ) - -## モード - -`standard`。収束ループの初期化の振る舞いを変え、複数モジュール(共通層と cross-review)にまたがる。 - -## 目的と非目的 - -達成したい状態: - -- 参加する CLI のどれか 1 者が導入・認証されていなくても、cross-review の初期化が使える者で始まり、使えない者と理由が状態ファイルに残る -- cross-review の各ラウンドに 2 席が確保される(使える者 → ホスト → 同じランタイムの 2 つ目) -- 中断した収束ループを引数を変えて再開したとき、渡した引数が反映されるか、反映しないことが知らされる -- 使える者の決定・席の埋め方・再開の反映の 3 つの規則が共通層に 1 か所ずつ入り、2 本目の Pull Request(cross-refactoring 側)がそのまま呼べる - -やらないこと(2 本目の Pull Request が行う): - -- cross-refactoring の初期化・担当・表示・引数・文書の変更 -- 従来の確認(`check_auth`)・適用専用の母集合(`impl_pool`)・従来の席と適用の割り当て(`review_assign` / `assign`)の削除 -- リポジトリの根の `CLAUDE.md` の書き換え -- 起動した後に分かる使えなさで担当を自動的に外す仕組み(設計の決定 18) - -## 前提 - -- 前提 1: 設計文書の決定 20 件は変えない。実装で決めると設計が残した 3 件(監視の比較の寄せ方・出力の文言・テストの置き場所)は、この計画の「実装で決めたこと」に書き、設計文書の「未確認のまま残ること」の表を同じ Pull Request で更新する -- 前提 2: 並行する束が同じ状態の部品(`state.py`)を触る。G3(PR #791)は副コマンドの登録関数の分割と再開の経路の分割、G2(PR #790)は指摘の分類を触る。この Pull Request は既存の行を書き換える量を最小にし、足す形で書く。競合は後からマージする側が解く -- 前提 3: ホストが席に入ったときの自分の Pull Request への投稿の扱い(`is_own_pr`)は、この Pull Request では実機で回さず、検査の持ち場か運用で確かめる。確かめていないことを Pull Request 本文の残リスクに書く - -## 受け入れ条件 - -要求文書の AC1〜AC6、AC8〜AC30、AC44〜AC46、AC48〜AC50 をそのまま使う。検証手段は要求文書の「検証手段」と設計文書の「テスト設計」の表にある。AC45・AC46・AC48(子 issue の再現手順)は手元で実行して結果を issue のコメントに残す。 - -## ドメイン用語 - -設計文書の「用語の対応表」を使う。この文書で追加する語は無い。 - -## 不変条件 - -- 使える者の並びは、ランタイムの固定の順(`ALL_RUNTIMES`)を保つ -- 使える者が 3 者のときの席は、変更前の輪番と同じ値になる -- この変更の前に始めた実行の状態ファイルは書き換えずに読める -- 再開で渡さなかった引数は、状態ファイルの値のまま残る - -## 互換性 - -| 対象 | 変更 | 互換性の扱い | -| --- | --- | --- | -| `state.py init` の引数 | `--exclude` / `--include` / `--require-all` / `--no-require-all` を足す。`--only` が `none` を取る。`--max-rounds` / `--rotate-after` の既定を未指定へ | 追加のみ。骨組みは値があるときだけ渡す形へ変える | -| `state.py read-result` の担当の引数 | 4 つの名前の選択肢から、席の名前の形の検査へ | 従来の 4 つの名前はそのまま通る | -| 起動スクリプトの第 1 引数 | ランタイム名から席の名前へ | 従来の名前はそのまま通る | -| 監視の位置引数の選択肢 | 4 つの名前と `both` から、席の名前の形と `both` へ | 同上 | -| 状態ファイル | 最上位に `participants` と `resume_changes` が増える。`rounds[].reviewers` と `rounds[].<席>` の鍵に席の名前が入りうる | 項目が無いときの読み方を契約文書の「移行」が決める。既存のファイルは書き換えない | -| 共通層の関数 | 6 つを新設。旧関数は残す | 追加のみ | - -## 修正対象 - -共通層: - -- `plugins/ndf/scripts/lib/auth.py` -- `plugins/ndf/scripts/lib/assignment.py` -- `plugins/ndf/scripts/lib/statefile.py` -- `plugins/ndf/scripts/lib/monitor.py` -- `plugins/ndf/scripts/lib/README.md`(関数の一覧の行) -- `plugins/ndf/scripts/tests/test_auth_probe.py`(書き直し)、`test_lib_participants.py`(新設)、`test_lib_resume_args.py`(新設)、`test_lib_assignment.py`(追記) - -cross-review: - -- `plugins/ndf/skills/cross-review/scripts/state.py` -- `plugins/ndf/skills/cross-review/scripts/launch-reviewer.sh` / `critique.sh` / `critique-round.sh` / `wait-review.sh` / `measure.py` -- `plugins/ndf/skills/cross-review/SKILL.md` / `docs/01-state-and-review.md` / `docs/04-contracts.md` / `docs/05-pool-and-convergence.md` -- `plugins/ndf/skills/cross-review/tests/test_state_review_pool.py`(追記)、`test_state_round_guard.py`(追記)、`test_state_resume_args.py`(新設)、`test_seat_names.py`(新設)、`test_skill_layout.py`(追記) - -cross-refactoring(テストだけ): - -- `plugins/ndf/skills/cross-refactoring/tests/test_assignment.py`(席の埋め方のテストを追記。従来の席の割り当てのテストは 2 本目の Pull Request が消す) - -文書: - -- `issues/issue-727-687-478-664-648-design.md`(「未確認のまま残ること」の 3 行) - -## タスク分解 - -受け入れ条件の番号は要求文書のものである。 - -### Task 1: 止めない確認を共通層に足す - -- **対象ファイル:** `lib/auth.py`、`scripts/tests/test_auth_probe.py` -- **変更内容:** 確認コマンドを走らせて結果だけを返す関数(`probe_auth(runtimes, *, info, env=None)` → `(結果, 飛ばしたか)`)を足す。確認コマンド・未認証の文言・時間切れの秒数・飛ばす環境変数は変えない。従来の確認は残す。既存テストは従来の確認を使っているため、止めない確認のテストへ書き直す(従来の確認のテストは 2 本目の Pull Request が消すまで残してよい) -- **満たす受け入れ条件:** AC5(確認コマンドを呼ばない部分)、AC6 -- **進め方:** 失敗するテスト(コマンドが見つからない / 時間切れ / 終了コード非 0 / 未認証の文言 / 成功 / 飛ばし)→ 最小実装 → 従来の確認と重なる走らせ方を 1 つの内部関数へ寄せる - -### Task 2: 使える者の解決・席の埋め方・適用の輪番・席の名前を共通層に足す - -- **対象ファイル:** `lib/assignment.py`、`scripts/tests/test_lib_participants.py`(新設)、`scripts/tests/test_lib_assignment.py`、`skills/cross-refactoring/tests/test_assignment.py` -- **変更内容:** 既定の参加者の表(`DEFAULT_REFACTOR_RUNTIMES = ("codex", "kiro")`)、母集合の既定(`refactor_pool(host)`)、参加者の記録(`Participants` データクラス。`pool` / `included` / `excluded` / `available` / `unavailable` / `probe_skipped` / `require_all` と `to_state()`)、使える者の解決(`resolve_participants`。順序は設計文書の表の 6 段)、席の形(`SEAT_PATTERN`)と席の名前の解釈(`seat_runtime`)、席の埋め方(`review_seats(round_no, available, fallback)`。規則の表は docstring に置く。設計の決定 20)、適用の輪番(`impl_assign(round_no, participants)`)を足す。モジュールの docstring の「役割ごとに母集合が違う」の表は、2 本目の Pull Request で母集合が 1 つになるまで残す -- **満たす受け入れ条件:** AC1〜AC5、AC8〜AC13、AC34 のうち適用の輪番の値 -- **進め方:** 失敗するテスト → 最小実装 → 整理。AC8 は変更前の席の割り当て(`review_assign`)を期待値に使う - -### Task 3: 再開の反映を共通層に足す - -- **対象ファイル:** `lib/statefile.py`、`scripts/tests/test_lib_resume_args.py`(新設) -- **変更内容:** 反映の表の 1 行(`ResumeField(arg, key, mode)`。`mode` は `replace` / `notify`)と、再開の反映(`apply_resume_args(state, args, spec)` → 標準エラーへ出す行の一覧)を足す。「反映する」は未指定でない値を状態へ書き `resume_changes` に `{at, field, from, to}` を積む。「知らせる」は状態と違うときだけ行を返す。値が同じなら行も記録も出さない。予約語 `none` の扱い(1 者指定は `null`、一覧は空)は呼び出し側が引数を正規化してから渡す形にし、この関数は値をそのまま比べる -- **満たす受け入れ条件:** AC25〜AC29 の共通層の部分 -- **進め方:** 失敗するテスト → 最小実装 → 整理 - -### Task 4: cross-review の新規の初期化を共通層へ載せ替える - -- **対象ファイル:** `cross-review/scripts/state.py`、`cross-review/tests/test_state_review_pool.py` -- **変更内容:** - - 初期化の引数: `--only` の型を 4 つの名前か `none` へ、`--exclude` / `--include`(カンマ区切り・繰り返し可・`none`)、`--require-all` / `--no-require-all`(既定は未指定)を足す。`--max-rounds` / `--rotate-after` の既定を未指定へ変え、新規の経路で 12 / 8 を置く。既存の引数の行はそのまま残し、足す行だけを加える(G3 が副コマンドの登録関数を分けるため) - - 使える者の解決(`_resolve_reviewers(host, args)`): 母集合の既定と使える者の解決を呼び、使える者が 2 者に満たなければホストを止めない確認で確かめて埋め合わせ(`fallback`)を決める。1 者指定があればホストを確かめず埋め合わせは空。割り当ての失敗は終了コード 1 へ写す。従来の確認の相手を決める関数と 1 者指定の検査(`_auth_targets` / `_validate_only`)はこの関数で置き換える - - 初期状態: `participants`(埋め合わせを含む 8 項目)と `resume_changes: []` を書く。`max_rounds` / `rotate_after` は既定を埋めた値 - - 標準エラーの行: 母集合と使える者の 1 行、通らなかった者は 1 者 1 行、埋め合わせは 1 行(文言は「実装で決めたこと」) -- **満たす受け入れ条件:** AC14〜AC20 -- **進め方:** 失敗するテスト(止めない確認を差し替えて新規の初期化を呼び、状態ファイルの有無・終了コード・標準エラーを見る)→ 最小実装 → 整理。既存テスト `test_auth_check_covers_only_the_reviewers_that_run` と `test_init_rejects_an_only_outside_the_pool` は新しい形へ書き直す - -### Task 5: 担当の読み出しを記録から先に見る順へ変え、前ラウンドの検査に記録の担当を渡す - -- **対象ファイル:** `cross-review/scripts/state.py`、`cross-review/tests/test_state_review_pool.py`、`cross-review/tests/test_state_round_guard.py` -- **変更内容:** 担当の読み出し(`_round_reviewers`)を「ラウンドの記録 → 1 者指定 → 参加者の記録から席の埋め方 → ホストの輪番(変更前と同じ値)→ `codex` / `agy`」の順にする。前ラウンドの検査(`_guard_previous_round`)は `prev["reviewers"]`(無ければ担当の読み出し)を結果なしの判定と通過の判定へ渡す -- **満たす受け入れ条件:** AC17(席が 2 つ返る部分)、AC18・AC19(`start-round` の返り値)、AC22、AC23 -- **進め方:** 失敗するテスト → 最小実装 → 整理 - -### Task 6: 席の名前を結果の受け口と起動・監視・計測に通す - -- **対象ファイル:** `cross-review/scripts/state.py`(結果の受け口の引数)、`launch-reviewer.sh` / `critique.sh` / `critique-round.sh` / `wait-review.sh`、`lib/monitor.py`、`cross-review/scripts/measure.py`、`cross-review/tests/test_seat_names.py`(新設)、既存の監視のテスト -- **変更内容:** - - 結果の受け口(`read-result`)の担当の引数を、席の名前の形の検査(`seat_runtime` を型に使う)にする。通らなければ argparse の終了コード 2 - - 起動スクリプト 2 本(`launch-reviewer.sh` / `critique.sh`)の先頭の検査を席の形(`^(claude|codex|agy|kiro)(-[2-9])?$`)にし、CLI は `${SEAT%%-*}` で選んで共通の起動スクリプト(`launch-cli.sh`)へ渡す。結果ファイルの stem は席の名前で組む(変更なし)。`critique-round.sh` は席の名前をそのまま `critique.sh` へ渡すだけで変更は無い(確かめて記録する)。`wait-review.sh` は使い方の説明の担当名を席の名前に直す - - 監視(`lib/monitor.py`): 席の名前からランタイムを引く内部関数を 1 つ置き、CLI 固有の分岐 3 か所(`codex` の sentinel 2 か所、`claude` の標準出力の検査 1 か所)をその関数で包む。位置引数の選択肢を席の形と `both` を受ける型へ替える - - 計測(`measure.py`): 担当の名前の一覧で数える箇所を、記録の鍵のうち席の形に一致するものを数える形にする -- **満たす受け入れ条件:** AC21 -- **進め方:** 失敗するテスト(結果の受け口を `claude-2` で呼ぶ / 起動スクリプトを共通の起動スクリプトを差し替えて呼び、渡った CLI 名と stem を見る / 監視の位置引数に `kiro-2` を渡す)→ 最小実装 → 整理 - -### Task 7: cross-review の再開で引数を反映し、担当に関わる引数で参加者を作り直す - -- **対象ファイル:** `cross-review/scripts/state.py`、`cross-review/tests/test_state_resume_args.py`(新設) -- **変更内容:** 反映の表(`max_rounds` / `rotate_after` / `only` / `verify_commands` / `verify_exit_codes` は反映する。`host` は知らせる)を置き、再開の経路(`_resume_from_state`)に引数を渡して再開の反映を呼ぶ。1 者指定・外す者・足す者・全員を要する指定のどれかを渡した再開では、渡さなかった引数を状態ファイルの値(`participants.included` / `excluded` / `require_all`、`only`)で補って使える者の解決をやり直し、`participants` を書き換えて `resume_changes` に 1 件積む。失敗したら状態ファイルを書き換えずに終了コード 1。既存の関数は引数を 1 つ足し、本体の既存の行は動かさず、反映の呼び出しを 1 ブロック足す形にする(G3 の分割と競合する行を減らす) -- **満たす受け入れ条件:** AC25〜AC29 -- **進め方:** 失敗するテスト(状態ファイルを置いた作業ツリーで初期化を呼び、状態ファイルと標準エラーを見る。一部の引数だけを渡す組み合わせを含む)→ 最小実装 → 整理 - -### Task 8: 完了報告に「参加した者」の節を足す - -- **対象ファイル:** `cross-review/scripts/state.py`、`cross-review/tests/test_state_review_pool.py` -- **変更内容:** 「PR 履歴」の後に「参加した者」の節を出す関数を 1 つ足し、完了報告(`cmd_report`)から 1 行で呼ぶ。行は 7 つ(母集合 / 使える者 / 外した者 / 足した者 / 確認を通らなかった者(理由つき)/ 席の埋め合わせ / 再開で変えた値)。参加者の記録が無ければ「使える者: 記録なし」、確認を飛ばした印が真なら「確認を通らなかった者: 確認を飛ばした(`NDF_SKIP_AUTH_CHECK`)」。既存の行は書き換えない(G3 が完了報告の結末の節を触る) -- **満たす受け入れ条件:** AC24 -- **進め方:** 失敗するテスト → 最小実装 - -### Task 9: 骨組みと文書を席の規則と新しい引数に合わせる - -- **対象ファイル:** `cross-review/SKILL.md`、`docs/01-state-and-review.md`、`docs/04-contracts.md`、`docs/05-pool-and-convergence.md`、`cross-review/tests/test_skill_layout.py` -- **変更内容:** 骨組み(Step 0 / 2 / 2.5)を契約文書の「手順書の骨組み」の形にする。初期化へは値のある引数だけを渡し、起動・監視・取り込み・反証の担当は `$REVIEWERS` / `$REVIEWERS_CSV` を使う。引数の表と `argument-hint` に 3 つの引数を足し、`--only` から「デバッグ用」を消す。`docs/05` に使える者の解決・席の埋め方(3 者以上 / 2 者 / 1 者 / 0 者)・足す者と外す者・確認が把握になったことを書く。`docs/04` に `participants` と `resume_changes` と席の名前の形を書く。`docs/01` に再開で反映する引数と反映しない引数の表を書く。`test_skill_layout.py` に「`ONLY` を含む行は初期化へ渡す行と引数の説明の行だけ」の検査を足す -- **満たす受け入れ条件:** AC30、AC44 -- **進め方:** テスト(`grep` の行数)→ 文書の書き換え。文書は `markdown-writing` の規約で書く - -### Task 10: 設計文書の「未確認のまま残ること」を更新し、配布物と検査を通す - -- **対象ファイル:** `issues/issue-727-687-478-664-648-design.md`、`lib/README.md`、生成物 -- **変更内容:** 実装で決めた 3 件(監視の比較の寄せ方・出力の文言・テストの置き場所)を「未確認のまま残ること」の表から「決めた」へ書き換える。共通層の README の関数の行を足す。`bash scripts/build-runtime-plugins.sh` で生成物を揃え、AC49・AC50 のコマンドを通す。AC45・AC46・AC48 を手元で実行し、結果を issue のコメントに残す -- **満たす受け入れ条件:** AC45、AC46、AC48、AC49、AC50 -- **進め方:** コマンドの実行と結果の記録(テスト駆動の対象ではない) - -## 実装で決めたこと - -設計文書が実装に委ねた 3 件を決める。 - -| 項目 | 決めたこと | 理由 | -| --- | --- | --- | -| 監視の CLI 固有の検査を席の名前に通す形 | 監視(`lib/monitor.py`)に席の名前からランタイムを引く内部関数を 1 つ置き、比較 3 か所をその関数で包む。席の形に合わない名前はそのまま返す | 監視は cross-refactoring も使い、担当名を任意の骨格で受ける経路がある(`test_monitor_generic_stem.py`)。形に合わない名前で失敗させると、その経路が壊れる | -| 出力の文言 | 反映した行は `↻ <項目>: <旧> → <新>`、知らせる行は `ℹ --<引数> は再開では反映しません(状態: <値> / 指定: <値>)`、通らなかった者は `⚠ <名前> を担当から外しました(<理由>)`、埋め合わせは `⚠ 使える者が <数> 者のため、席を<相手>で埋めます(観点が減ります)` | 既存の初期化の出力が `↻` / `ℹ` / `⚠` の印で始まる形に揃える。項目名と値を含めることは設計が決めている | -| テストの置き場所 | 設計文書の「テスト設計」の表のとおり。席の埋め方のテストは cross-refactoring の割り当てのテスト(`test_assignment.py`)に置く | 変更前の席の割り当てのテストが同じファイルにあり、AC8 の期待値をその場で引ける | - -## 影響範囲 - -- cross-review の初期化の既定の振る舞い: 確認を通らない者が 1 者でもあれば止める形から、使える者で回す形へ(従来の形は `--require-all`) -- cross-review の席: 使える者が 2 者に満たないとき、ホスト → 同じランタイムの 2 つ目で埋める。従来は 1 者で回すか失敗していた -- 状態ファイル・結果ファイルの名前に、席の名前(`claude-2` など)が現れうる。読む側(監視・計測・完了報告)はこの Pull Request で追随する -- cross-refactoring: 共通層の関数が増えるだけで振る舞いは変わらない。旧関数を残すため既存のテストは通る - -## リスクと対処 - -| リスク | 対処 | -| --- | --- | -| 状態の部品(`state.py`、4470 行)を G2 / G3 と並行して触る | 既存の行の書き換えを最小にし、足す形で書く。着手の前に G3 の差分を読んだ(副コマンドの登録関数の分割・再開の経路の 3 分割・完了報告の結末の節)。触る関数を初期化・担当の読み出し・前ラウンドの検査・結果の受け口の引数・完了報告の呼び出し 1 行に限る | -| 監視の比較を包む変更が、任意の骨格で担当名を受ける経路を壊す | 席の形に合わない名前はそのまま返す。既存の監視のテスト(`test_monitor_*`)を毎タスクで通す | -| 席の名前が読み手に届かない箇所が残る | 設計文書の「構成要素」の受け口 10 か所を 1 つずつ検査に対応づけ、`grep -n 'claude|codex|agy|kiro' scripts/` で分岐と選択肢を洗い直す | -| 従来の確認のテストが止めない確認の導入で意味を失う | 2 本目の Pull Request で消すまで残す。この Pull Request では止めない確認のテストを足す | - -## 切り戻し手順 - -- Pull Request を revert すれば戻る。状態ファイルの新しい項目(`participants` / `resume_changes`)は、旧版の読み手が読まない鍵のため、途中の実行を旧版で再開しても壊れない -- 席の名前を持つ状態ファイル(`claude-2` の鍵)を旧版で読むと、その席の結果は数えられない。旧版へ戻すときは実行を新しく始める - -## 完了の定義 - -- [ ] AC1〜AC6、AC8〜AC30、AC44〜AC46、AC48〜AC50 を満たし、条件ごとに検証手段と結果が対応している -- [ ] `uv run --with pytest pytest scripts/tests plugins/ndf -q` が通る(監視の環境変数を export していないシェルで実行する) -- [ ] AC50 の 6 つのコマンドが終了コード 0 で終わる -- [ ] 設計文書の「未確認のまま残ること」が更新されている -- [ ] Draft の Pull Request を `develop` 宛に出した diff --git a/issues/issue-728-647-592-553-design.md b/issues/issue-728-647-592-553-design.md deleted file mode 100644 index 709e54111..000000000 --- a/issues/issue-728-647-592-553-design.md +++ /dev/null @@ -1,570 +0,0 @@ -# cross-refactoring: 実装担当が結果を残さないと同じ群が上限なしに開き直され、未検証のコミットが残る → 結果なしを取り込みの 1 か所で受けて取り消し、群が開いた回数と結末で開き直しを決める(設計 / #728 #647 #592 #553) - -## 目的 - -- **壊れていること**: cross-refactoring の実装担当が結果ファイルを残さずに終わることがある。原因は無進捗の打ち切り・利用上限・15 秒で落ちる kiro である。このとき取り込みは、下位の読み取りがプロセスを終わらせるため止まる。同じ群が上限なしに開き直される(PR #757 で 29 回、rf646 で 3729 回)。担当が作ったコミットは検証を受けずに残る。テスト整備の採用 0 件では項目の無い群を起動し続ける(rf587 で 187 回)。claude が担当の群は帰属行のためトレーラーが読めずに落ちる -- **困る人**: cross-refactoring を回す進行側(手で止めるまで CLI の起動と利用料が続く)と、その Pull Request を読む人(未検証の差分が混じる) -- **直すと成り立つこと**: 結果なしは 3 つの取り込みが 1 つの手順で受ける。未検証のコミットを取り消し、結末を記録する。群は開いた回数と前回の結末を持ち、担当を替えて 1 回だけ開き直して終わる。項目の無い群は開かない。帰属行の後ろでもトレーラーが読める - -この文書は「どう作るか」だけを扱う。要求と受け入れ条件、置き換える既存の設計、並行する設計との前後関係は末尾の「関連文書と前後関係」にある。 - -## 用語の対応 - -本文は左の業務用語で書く。右の識別子は、コードブロック・表・「置き場所」「データ構造」「入出力の契約」で使う。 - -| 業務用語 | 識別子 | -| --- | --- | -| 取り込み | 担当の CLI が作ったコミットを進行側が検証して受け入れるコマンド。適用の取り込み `merge-apply`(`commands/apply.py`)/ 修正の取り込み `merge-fix`(`commands/converge.py`)/ 最終ゲートの修正の取り込み `merge-final-fix`(`commands/gate.py`)の 3 つ | -| 群を開く | `next-apply-round`(`commands/apply.py`) | -| 最終ゲート | `final-gate`(`commands/gate.py`) | -| 結末の読み取り | `gitfacts.read_result` | -| 共通層の読み取り | `lib/monitor_outcome.py` の `read_launch_outcome`。G3 が作る | -| 結末 | `LaunchOutcome`。担当 1 回の起動の終わり方。使える結果(`payload`)か、結果なしの理由(`reason`: `missing` / `unparsable` / `stalled` / `usage_limit` など)を持つ | -| 起動し直しの可否 | `LaunchOutcome.relaunch_same_agent`。偽は同じ担当を同じ条件で起動しても解けない(`usage_limit`) | -| 取り込みの共通手順 | 新設の `refactor_lib/intake.py`。範囲の値 `IntakeScope`、閉じた結果 `ClosedAttempt`、範囲の確定 `confirm_range`、取り消し `discard_unverified`、叩き直しの判定 `already_closed`、結果なしの一連 `close_without_result` | -| 群の進行 | `refactor_lib/rounds.py` | -| 開き直しの判定 | `rounds.group_reopening` | -| 輪番から担当を引く関数 | `rounds.impl_for_seq` | -| 群の一覧 | `rounds.apply_groups` | -| 群 | 状態ファイルの `rounds[].apply_rounds[]` の 1 件。書き換えるファイルが重ならない項目の集まり | -| 試行の番号 | 群の `attempt` | -| 結末の記録 | `failed_attempts[]`。群と最終ゲートの記録(`final_gate`)が持つ | -| 取り消しの理由 | 群の `drop_reason`(`no_result` / `empty`) | -| 起点 | 適用は `apply_base_sha` と群の `base_sha`、修正と最終ゲートは `fix_base_sha` | -| 修正ラウンドの数と上限 | `fix_rounds` と `--max-fix-rounds` | -| 輪番の通し番号 | `apply_seq` | -| 試行の上限 | `vocabulary.MAX_APPLY_ATTEMPTS`(2) | -| 無進捗の許容 | `init` が出す `IMPL_STALL_TIMEOUT`。余白は `vocabulary.IMPL_STALL_MARGIN`(900 秒)、テストの制限時間は `--test-timeout` | -| トレーラーの読み取り | `gitfacts.commit_trailers` | -| 結果ファイルの名前の幹 | stem。`paths.stem_for` が組み、監視は `--stem-template` で受ける | -| G1 / G3 / G4 | 実行計画の束の名前。G1 = 参加者の決め方(#727、PR #782)、G3 = 結末の読み取りの共通層(#729、PR #781)、G4 = この設計(#728) | - -## 機能一覧 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| F1 | 担当が結果を残さなくても、3 つの取り込みが範囲を確め、未検証のコミットを取り消し、結末を記録して終わる | cross-refactoring を回す進行側 | -| F2 | 結果を残さない担当の群を、担当を替えて 1 回だけ開き直し、2 回目も残さなければ取り消す | 同上 | -| F3 | 採用 0 件の提案ラウンドと項目の無い群で、適用担当を起動せずに次へ進む | 同上 | -| F4 | 修正の結果を群の担当から読み、結果が無ければ修正ラウンドを 1 つ進める | 同上 | -| F5 | 最終ゲートの修正で結果が無ければ、作られたコミットを取り消して次の判定へ戻す | 同上(`--workflow-step` の実行) | -| F6 | 起動し直しても解けない結末では、同じ担当を同じ工程で起動し直さない | 同上 | -| F7 | 帰属行の段落が後ろに付いたコミットから必須トレーラーを読む | 同上(claude が適用担当の群) | -| F8 | 適用・修正の監視が、テストの実行中の無出力で担当を打ち切らない | 同上 | - -## 実測 - -2026-09-19 に `develop`(9eaebe14)で読み取った現状である。コードを実行して確かめた値は既存の設計の「実測」にあり、変えていない。 - -| 見たもの | 値 | -| --- | --- | -| `read_result` の呼び出し元 | 3 か所(`commands/apply.py:452` / `commands/converge.py:478` / `commands/gate.py:165`)。いずれも `die(code=2)` を内包する | -| 取り消しの本体 | 2 つ(`gitfacts.revert_unverified_range`(`converge.py:383` / `gate.py:203` が呼ぶ)と `apply._revert_unverified_apply_round`)。違いは起点の鍵(`fix_base_sha` / `apply_base_sha` と群の `base_sha`)と `entry["apply"]` の記録 | -| `merge-final-fix` の順序 | `read_result`(:165)→ `commits_in_range`(:167)→ `unassigned_fix_commits` / `verify_final_fix_commit`(:178-189)→ `revert_unverified_range`(:199-206)。結果なしでは 2 つ目以降へ進まない | -| `rounds.apply_groups` | `if groups:`(`rounds.py:109`)だけで分岐し、`None` と `[]` を区別しない | -| `stem_for` / `result_path` | `refactor_lib/paths.py:94` / `:85`。`result_path` は `tmp_dir / f"{stem}-result.json"` で、G3 の `read_launch_outcome` の既定と同じ | -| 既存の設計が名付けた関数 | `MAX_APPLY_ATTEMPTS` / `impl_for_seq` / `load_result` / `_close_failed_attempt` / `_monitor_reason` はいずれも未実装(PR #665 は文書だけ) | - -## 決定の記録 - -### 決定 1: 通過済みの決定と新しい決定を差分で読み分けるために、設計文書は親 #728 の名前で新設し、既存の本体は書き換えない - -既存の設計は 3 課題を 1 つの文書で扱い、設計 Pull Request の関門を通過している。親 #728 は既存の決定 6(取り消しの本体を切り出して共有し、最終ゲートを #674 に残す)を改め、結果の読み取りの向きを変える。その節を書き換えると、通過済みの決定と新しい決定が 1 つの差分に混ざる。**新設して対応表で指せば、変わった決定だけが差分に載る。** 既存の設計文書の本体には案内の 1 行も足さない。設計 Pull Request の本文の「決めたこと」は、変更したファイルの「決定の記録」の見出しをすべて写す。既存の本体に 1 行でも触ると、既存の 13 件がこの Pull Request の決定として並ぶ。案内は「決定の記録」を持たない既存の要求の文書にだけ足す(#729 / #727 の設計と同じ扱い)。 - -### 決定 2: 同じ関数を 2 度変えないために、4 課題と #674 を 1 本の Pull Request で直す - -結末の読み取り(`gitfacts.read_result`)の契約を変えると、呼び出し元 3 か所を同時に書き直すことになる。3 か所は適用・修正・最終ゲートの修正の取り込みである。取り消しの本体を 1 つにすることも、同じ 3 か所を同時に触る。課題ごとに分けると同じ関数を 2 度変える。#553 は同じファイル(`gitfacts.py`)と適用の取り込みの検証に閉じ、分けても触るファイルが重なる。 - -### 決定 3: 監視と同じ結果ファイルの名前を 1 か所で組むために、結末の読み取りは名前を残して引数を状態と工程に変え、結末を値で返す - -G3 の契約(G3 の設計の決定 11)は「共通層の読み取り(`read_launch_outcome`)を呼び、値を返し、プロセスを終わらせない」までである。薄い包みとして残すかは G4 に任せている。**包みとして残す。** 監視に渡した結果ファイルの名前の幹(stem)と食い違う幹を渡すと、監視の結果ファイルを引けない(G3 の未確認 6)。幹を作る場所を結末の読み取り(`read_result(state, runtime, phase, round_no=None)`)の 1 つにすれば、3 つの取り込みが同じ組み立てを通る。突き合わせのテスト(AC3)も 1 か所で書ける。名前を残すのは、親 #728 と子 issue の本文がこの名前で根本原因を指しているためである。引数を変えるため、古い形の呼び出し(`read_result(path, runtime)`)は実行時に失敗し、契約の変更を素通りしない。 - -呼び出し側が共通層の読み取りを直接呼ぶ形は採らない。幹の組み立てが 3 か所に分かれる。 - -### 決定 4: 取り消しの本体を 1 つにするために、「範囲の確定 → 未検証コミットの取り消し → 結末の記録」を新設の取り込みの共通手順に置く - -3 つの取り込みは、結果を読めたときも読めなかったときも同じことを行う。起点から HEAD までの範囲を確め、通らなければ範囲を取り消し、起点を取り消し後の HEAD へ進める。いまは取り消しの本体が 2 つある。修正と最終ゲートが使うもの(`gitfacts.revert_unverified_range`)と、適用が使うもの(`apply._revert_unverified_apply_round`)である。違いは起点の鍵だけである。**取り込みの共通手順(`refactor_lib/intake.py`)を新設する。そこに範囲の確定(`confirm_range`)・取り消し(`discard_unverified`)・結果なしの一連(`close_without_result`)を置く。** 起点の鍵と記録先の違いは、取り込みの範囲の値(`IntakeScope`)で渡す。取り込みが持つのは、その値をどう終了コードと群の状態へ写すかだけになる(親 #728 の統合の手)。 - -置き場所をコマンドの層(`commands/`)にしないのは、適用・修正・最終ゲートの 3 つが読む層だからである。群の進行と同じ理由で、コマンドどうしの取り込みを作らない。git の事実の読み取り(`gitfacts.py`)に足す形も採らない。1158 行あり、取り消しは git の事実の読み取りではなく進行の手順である。 - -### 決定 5: 要約と改修計画が 1 つの読み方で済むように、結果なしの記録は 3 つの取り込みで同じ形の配列にする - -記録の形を 3 通りに分けると、実行の要約と改修計画が 3 通りの読み方を持つ。3 通りとは、群の結末の記録(`failed_attempts`)、修正の取り込み済みの鍵(`fix_merged_keys` の `":missing"`)、最終ゲートの独自の記録である。**記録の辞書は群、または最終ゲートの記録(`final_gate`)である。その結末の記録(`failed_attempts[]`)に `{phase, attempt, impl, reason, detail, at, reverted}` を足す。** 工程(`phase`)が適用・修正・最終ゲートの修正を分ける。試行の番号(`attempt`)は叩き直しの判定に使う。同じ工程と試行の記録があれば、読まずに終了コード 2 を返す。修正の取り込み済みの鍵は、結果を読めたときの二重取り込みの判定に残し、結果なしの判定には使わない。 - -### 決定 6: 壊れた担当に当たり続けないために、同じ群の試行の上限は 2 回の固定値にし、引数を足さない - -2 回目は別の担当が試す(決定 8)。2 回とも結果を残さなければ、担当ではなく群の側を疑える。3 回以上にしても壊れた担当に当たる確率が上がるだけである。値は語彙(`vocabulary.py` の `MAX_APPLY_ATTEMPTS`)に置く。上限の引数(`--max-apply-attempts`)を足す形は採らない。手順書(`SKILL.md`)は、上限を 2 つ置くとどちらで止まったかを読み解く必要が出るとしている。止まった理由は群の記録(取り消しの理由と結末の記録)が持つ。 - -### 決定 7: 中断からの再開と失敗のやり直しを区別するために、開き直しの判定は群の進行の 1 つの関数に置き、群を開く側と適用の取り込みの両方がそれを読む - -いまの群を開く側(`next-apply-round`)は、未着手(`pending`)と適用済み(`applied`)の群を無条件に開き直す。中断からの再開と失敗した試行のやり直しを区別しない。判定に要る値は群が持つ 2 つである。開いた回数(`attempt`)と、結末の記録のうち工程が適用のものの件数である。**開き直しの判定(`group_reopening(group)`)がこの 2 つから次の 4 つのどれかを返す。** - -| 値 | 意味 | -| --- | --- | -| `open` | 開いて試行の番号を進める | -| `resume` | 開いたまま閉じていない試行を再開する。番号を進めない | -| `exhausted` | 上限に達した。開かない | -| `empty` | 項目が無い | - -群を開く側は開くかどうかを、この値で決める。適用の取り込み(`merge-apply`)は結果なしを記録した後に担当を替えるか取り消すかを、同じ関数の値で決める。 - -開いた回数だけを数える形は採らない。進行側が落ちて再開しただけで試行が進む。監視の終了コードで骨組みが分岐する形も採らない。結果ファイルが後から書かれた場合や、監視は正常でも JSON が壊れている場合は、結果ファイルの側で決めるしかない。判定を 1 か所に置けば、骨組みは `|| continue` のまま変わらない。 - -### 決定 8: 壊れた CLI が 1 者でも他の担当で群を進めるために、2 回目の試行は次の輪番の担当が行い、替える先が無いときだけ結末の可否で決める - -結果を残さない原因の多くは担当の CLI の側にある(rf646 の agy の無進捗 4 回、claude の 429 の 3729 回、PR #757 の kiro)。同じ担当で開き直しても直らない。替える先は次の手順で決める。輪番の通し番号(`apply_seq`)を 1 ずつ進め、輪番から担当を引く関数(`rounds.impl_for_seq`)を引く。その群で失敗した担当のどれとも違う担当が出た、最初の番号の担当を替える先とする。1 つ進めるだけにしないのは、輪番が 1 周すると同じ担当へ戻るためである(既存の設計の「実測」: 通し番号 1 と 5 はどちらも codex)。探索は参加者の数だけ進めれば全員を 1 度ずつ見る。打ち切りの回数は固定値ではなく参加者の数から導く。 - -**替える先が無いとき**(参加者が 1 者。G1 の除外の引数 `--exclude` で残りが 1 者になる実行)は、起動し直しの可否(`relaunch_same_agent`)を読む。真(結果なしの理由が `missing` / `stalled` など)なら同じ担当で 2 回目を開く。偽(`usage_limit`)なら 1 回目で群を取り消す。可否を読むのはこの分岐だけで、替える先があるときは常に替える。利用上限で進行全体を止める形は採らない。他の担当で進められる群まで止まる。 - -### 決定 9: 参加者の決め方の変更(G1)を 1 か所で受けるために、作業を任せる担当の決定は輪番から担当を引く 1 つの関数を通す - -輪番から担当を引く関数(`rounds.impl_for_seq(state, seq)`)を新設する。輪番の通し番号から担当と要求モデルを引く呼び出しは、その中だけにする。呼ぶのは 3 か所である。 - -| 呼ぶ場所 | 決めるもの | -| --- | --- | -| 群の割り当て(`apply._assign_apply_rounds_to_state`) | 群の担当 | -| 結果なしの試行を閉じる処理(`apply._close_failed_attempt`) | 交代先 | -| 最終ゲートの修正担当の決定(`gate._final_fix_impl`) | 最終ゲートの修正担当 | - -G1 は担当の割り当て(`assignment.assign`)を参加者からの割り当て(`impl_assign(participants, seq)`)に置き換える。**どちらが先に入っても、変える場所はこの関数の中だけである。** 提案ラウンドの開始(`setup.cmd_start_round`)の担当は CLI を起動しないため通さない。 - -### 決定 10: 修正が上限なしに往復しないように、修正の結果は群の担当から読み、結果なしは修正ラウンドを進める。起動し直せない結末では上限へ進める - -修正の取り込み(`merge-fix`)は提案ラウンドの担当(`entry["impl"]`)の結果を読むが、骨組みが起動するのは群の担当である。一致しない群では結果を一度も取り込めない。修正ラウンドの数(`fix_rounds`)が進まないまま、検証と修正を往復する(既存の設計の「実測」)。担当は群の担当(`current_group(entry)["impl"]`)から読む。結果なしは結果なしの一連(`close_without_result`)を通し、修正ラウンドの数を 1 進めて終了コード 2 で終わる。範囲を確定できないときの既存の扱い(`_resolve_fix_range`)と同じ形である。 - -**起動し直しの可否が偽なら、修正ラウンドの数を上限(`--max-fix-rounds`)の値にする。** 修正の担当は替えない。直しかけの文脈を持つ者が続ける、という最終ゲートの既存の理由と同じである。替えないまま上限まで起動し直すと、利用上限の担当を最大 3 回起動して 3 回とも 15 秒で落ちる。上限へ進めれば見送りの判定(`should-abandon`)が次の呼び出しで見送りへ移し、骨組みの行は変わらない。 - -### 決定 11: 未検証のコミットを Pull Request に残さないために、最終ゲートの修正も同じ手順を通し、結果なしは終了コード 2 で終えて最終ゲートに判定を戻す - -最終ゲートの修正の取り込み(`merge-final-fix`)が結果なしでプロセスを終わらせると、担当が作ったコミットが範囲の検査も取り消しも受けずに残る。次の最終ゲート(`final-gate`)はそのコミットを含む HEAD でテストし、落ちれば起点を HEAD へ置き直す(#674)。**結果なしの一連を通せば、コミットは取り消され、起点は取り消し後の HEAD になる。次の最終ゲートは修正前の地点でテストする。** 終了コードは 2 で、骨組みは見ずに最終ゲートへ戻る(変えない)。最終ゲートが修正ラウンドの数を進めるため、繰り返しは上限で止まる。 - -起動し直しの可否が偽なら、最終ゲートの修正ラウンドの数(`gate.fix_rounds`)を上限の値にする。次の最終ゲートは、テストが落ちれば終了コード 1(取り消さず報告)で終わる。既存の「Step 7 は push 済みの地点。上限に達しても採用した改善項目は取り消さない」の規則を変えない。 - -### 決定 12: 項目の無い群で担当を起動しないために、採用 0 件の提案ラウンドでは群を作らず、項目の無い群は開かずに取り消す - -群の一覧(`rounds.apply_groups`)は `if groups:` で分岐する。鍵が無い(`None`)ときも空の配列(`[]`)のときも 1 つの群を作る。提案の取り込み(`merge-proposals`)は採用 0 件で空の配列(`apply_rounds = []`)を書くため、ここで項目 0 件の群が生まれる。**鍵が無いときだけ古い版として群を作り、空の配列はそのまま返す。** 既に項目の無い群を持つ状態ファイル(rf587)は残る。開き直しの判定が「項目が無い」(`empty`)を返した群は、群を開く側が取り消し済み(`dropped`、理由 `empty`)にして次を探す。適用の取り込みの取り込み済みの判定でも、採用 0 件だった群を取り消し済みに直す。 - -提案の取り込みがテスト整備の採用 0 件で終了コード 2 を返す形は採らない。2 は構造改善の繰り返しを終える合図で、テスト整備では構造改善へ進む前に抜けてしまう。 - -### 決定 13: 帰属行の書式に左右されずに検証を通すために、トレーラーは末尾から続く段落を git の判定で読み、進行側はコミットを書き換えない - -トレーラーの読み取り(`commit_trailers`)はメッセージを空行で段落に分ける。末尾の段落から前へ向かって 1 段落ずつ git の判定(`git interpret-trailers --parse`)に掛け、git がトレーラーの段落と判定しなかった段落で止める。同じ鍵は末尾に近い段落の値を採る。**1 段落目(題名)は掛けない。** 掛けると `Refactor: …` の題名をトレーラーとして読む(既存の設計の「実測」)。 - -#553 の本文は、帰属のトレーラーを付ける責務を進行側の取り込みへ移す(`git commit --amend`)ことを修正レイヤーとしている。**採らない。** 理由は 3 つある。 - -| 理由 | 何が起きるか | -| --- | --- | -| SHA が変わる | 結果ファイルの申告(`commits[].sha`)と実体の対応が切れる | -| 群に複数の項目がある | 先頭のコミットを書き換えた時点で以降のすべてが書き換わる。範囲の検査の前提(申告の SHA が範囲に実在する)が崩れる | -| 実際に使ったモデル名(`Impl-Model`)は担当しか知らない | 進行側が付けるには結果ファイルから受け取ることになる | - -段落ごとの読み取りは、トレーラーの形で書かれた署名なら誰が何行足しても同じに読む。効かないのは、トレーラーの形でない散文を末尾に足すランタイムが現れたときだけである。それは「未確認のまま残ること」に置く。 - -全文から `^: ` を拾う形も採らない。散文の段落にある `Round: …` の形の行を拾う。 - -### 決定 14: 人が git の標準の読み方でも集計できるように、雛形のコミットの規約にも必須トレーラーを最後の段落に置くことを書く - -進行側の検証は決定 13 で通る。一方、人が git の標準の読み方(`git log --format='%(trailers:key=Impl-Model,valueonly)'`)で集計すると、最後の段落しか読まない。帰属行を同じ段落に続けて書けば、git の標準の読み方でも取れる。従わなくても決定 13 で検証は通る。 - -### 決定 15: テストの実行中の無出力で担当を打ち切らないために、無進捗の許容をテストの制限時間 + 900 秒にし、雛形に進捗マーカーを足す - -既存の設計の決定 13 をそのまま引き継ぐ。適用・修正の担当はテストを 1 回実行し、その間は何も出力しない。起動(`init`)が無進捗の許容(`IMPL_STALL_TIMEOUT`)を出す。骨組みの適用・修正・最終ゲートの修正の監視が、それを無進捗の引数(`--stall-timeout`)に渡す。全体の制限時間(`--timeout`)は渡さない。上限は P2 の工程の引数(`--phase`)と `lib/limits.py` が決める。監視(`monitor.py`)は変えない。 - -## 構成要素 - -| 要素 | 新設 / 変更 | 責務 | -| --- | --- | --- | -| 結末の読み取り(`gitfacts.read_result`) | 変更 | 状態と工程から stem を組み、`read_launch_outcome` を呼んで `LaunchOutcome` を返す(決定 3) | -| 取り込みの共通手順(`intake.py`) | 新設 | 範囲の確定・未検証のコミットの取り消し・結末の記録(決定 4・5) | -| 群の進行(`rounds.py`) | 変更 | `apply_groups` の空配列の扱い、`group_reopening`、`impl_for_seq`(決定 7・9・12) | -| 適用の取り込み(`commands/apply.py`) | 変更 | `next-apply-round` が `group_reopening` で開く。`merge-apply` が結果なしを `_close_failed_attempt` へ渡し、担当の交代か取り消しを行う(決定 6〜9) | -| 修正の取り込み(`commands/converge.py`) | 変更 | 群の担当の結果を読む。結果なしは共通の手順を通して修正ラウンドを進める(決定 10) | -| 最終ゲート(`commands/gate.py`) | 変更 | `merge-final-fix` が共通の手順を通す。`_final_fix_impl` が `impl_for_seq` を引く(決定 9・11) | -| 語彙(`vocabulary.py`) | 変更 | `MAX_APPLY_ATTEMPTS = 2`、`IMPL_STALL_MARGIN = 900` | -| 起動(`commands/setup.py`) | 変更 | `_emit_init` が `IMPL_STALL_TIMEOUT` を出す(決定 15) | -| トレーラーの読み取り(`gitfacts.commit_trailers`) | 変更 | 末尾から続くトレーラーの段落を読む(決定 13) | -| 雛形(`prompts/apply.md` / `fix.md` / `final-fix.md`) | 変更 | 必須トレーラーを最後の段落に置く。進捗マーカー(決定 14・15) | -| 手順書(`SKILL.md` / `docs/02` / `docs/04`) | 変更 | 語の表、「別の上限を置かない」の削除、骨組みの監視の引数、結果なしのときの振る舞い | -| 共通層(`lib/monitor_outcome.py`) | 変えない | G3 が実装する `read_launch_outcome` を読む | - -```mermaid -graph TD - subgraph 取り込み - A[merge-apply] - F[merge-fix] - G[merge-final-fix] - end - subgraph 共通の手順 - R[gitfacts.read_result] - I[intake
confirm_range / discard_unverified
close_without_result] - end - subgraph 群の進行 - N[next-apply-round] - RO[rounds.group_reopening] - IS[rounds.impl_for_seq] - end - MO[lib/monitor_outcome
read_launch_outcome] - A --> R - F --> R - G --> R - R --> MO - A --> I - F --> I - G --> I - A --> RO - N --> RO - A --> IS - G --> IS -``` - -**図に含めない要素**は、語彙・起動の出力・トレーラーの読み取り・雛形・手順書である。呼び出しの辺を持たない値と文書か、適用の取り込みの検証が呼ぶ 1 本(トレーラーの読み取り)であり、図の主題(結末の扱いの統合)ではない。 - -### 文脈と配置 - -```mermaid -graph LR - 利用者 --> ホスト[ホストの CLI セッション] - ホスト -->|骨組みの bash| RF[refactor.py] - ホスト -->|launch-cli.sh / monitor.py| 担当[担当の CLI と監視] - RF --> TMP[一時ディレクトリ
結果ファイル・監視の結果ファイル] - 担当 --> TMP - RF --> WORK[作業ツリーの git] -``` - -**配置は変えない。** すべて利用者の機械のホストのセッションから起動するプロセスで、常駐しない。この変更で増える辺は 1 本だけである。進行の本体(`refactor.py`)が共通層を通して監視の結果ファイルを読む辺である。 - -### 置き場所 - -```text -plugins/ndf/skills/cross-refactoring/ -├── SKILL.md # 語の表・段落の削除・骨組み -├── docs/02-apply-and-review.md # Step 4 の結果なし・開き直し・トレーラー -├── docs/04-fix-and-report.md # Step 6 / Step 7 の結果なし・監視の引数 -├── prompts/{apply,fix,final-fix}.md # トレーラーの段落・進捗マーカー -├── scripts/refactor_lib/ -│ ├── intake.py # 新設: IntakeScope / confirm_range / discard_unverified / close_without_result -│ ├── rounds.py # apply_groups / group_reopening / impl_for_seq -│ ├── gitfacts.py # read_result(契約の置き換え)/ commit_trailers / revert_unverified_range の削除 -│ ├── vocabulary.py # MAX_APPLY_ATTEMPTS / IMPL_STALL_MARGIN -│ └── commands/{apply,converge,gate,setup}.py -└── tests/ # 新設 3 本(test_intake / test_apply_attempts / test_commit_trailers_git)と既存 6 本の変更。対応は「テスト設計」 -# dev.kiro / dev.agy の配布物は bash scripts/build-runtime-plugins.sh で同期する -``` - -## 構造 - -変更が触る型だけを載せる。結末(`LaunchOutcome`)は G3 が作る型で、名前と欄だけを置く。取り込みの共通手順の関数は「入出力の契約」にある。 - -```mermaid -classDiagram - class LaunchOutcome { - payload: Optional~dict~ - reason: Optional~str~ - detail: str - relaunch_same_agent: bool - } - class IntakeScope { - holder: dict - base_key: str - records: dict - phase: str - attempt: int - impl: str - label: str - mirror: Optional~dict~ - } - class ClosedAttempt { - reason: str - detail: str - reverted: int - relaunch_same_agent: bool - range_unknown: bool - } - class intake - intake ..> IntakeScope : 受け取る - intake ..> LaunchOutcome : 読む - intake ..> ClosedAttempt : 返す -``` - -| 触る型 | 責務 | -| --- | --- | -| `IntakeScope` | 取り込み 1 つ分の「どこを見て、どこへ書くか」。`holder` は `pending_push` と起点を持つ辞書(`rounds[]` の要素か `final_gate`)、`base_key` は起点の鍵(`apply_base_sha` / `fix_base_sha`)、`records` は `failed_attempts` を持つ辞書(群か `final_gate`)、`mirror` は起点を同じ値に揃える辞書(群の `base_sha`。修正と最終ゲートは `None`) | -| `ClosedAttempt` | 結果なしを閉じた結果。`range_unknown` が真なら取り消しも記録も行っていない(呼び出し側が中断の終了コードを決める) | - -## データ構造 - -状態ファイル(`cross-refactoring-rf<番号>-state.json`)に鍵を足す。**版は上げない。** 鍵が無い状態ファイルは、試行 0・失敗なしとして読む。 - -### 群の記録 - -群(`rounds[].apply_rounds[]` の 1 件)に足す鍵である。 - -| 項目 | 型 | 値 | 空のときの意味 | -| --- | --- | --- | --- | -| `attempt` | 整数 | いま開いている試行の番号。1 から | 鍵なし = 0(まだ開いていない)。`merge-apply` は 0 を 1 回目として記録し、値を 1 に書く(変更前の版で開いた群を再開したとき) | -| `failed_attempts` | 配列 | 結果を残さなかった起動。1 件 = 下の「結末の記録の 1 件」 | 鍵なし = 失敗なし | -| `drop_reason` | 文字列 | `no_result`(試行の上限、または起動し直せない結末で交代先なし)/ `empty`(項目なし) | 鍵なし = 既存の経路で取り消した、または取り消していない | -| `impl` / `impl_model` | 既存 | 担当を替えたときに書き換える。**前の担当は `failed_attempts[].impl` に残る** | — | - -### 最終ゲートの記録 - -最終ゲートの記録(`final_gate`)に足す鍵である。 - -| 項目 | 型 | 値 | 空のときの意味 | -| --- | --- | --- | --- | -| `failed_attempts` | 配列 | 結果を残さなかった最終ゲートの修正の起動 | 鍵なし = 失敗なし | - -### 結末の記録の 1 件 - -結末の記録(`failed_attempts[]`)の 1 要素の形である。 - -| 列 | 型 | 空を許すか | 意味 | -| --- | --- | --- | --- | -| `phase` | 文字列 | 許さない | `apply` / `fix` / `final-fix`。同じ群の適用と修正の記録を分ける | -| `attempt` | 整数 | 許さない | 群の `attempt`(適用)/ `fix_attempts`(修正)/ `fix_rounds`(最終ゲート)。叩き直しの判定の鍵 | -| `impl` | 文字列 | 許さない | 起動した担当 | -| `reason` | 文字列 | 許さない | `LaunchOutcome.reason`(`missing` / `unparsable` / `stalled` / `timeout` / `early_error` / `usage_limit` / `cli_timeout` / `pidfile_bad`) | -| `detail` | 文字列 | 許す(空文字) | 監視の `detail`。監視の結果ファイルが無ければ空文字 | -| `at` | 文字列 | 許さない | 記録した時刻 | -| `reverted` | 整数 | 許さない | その起動の範囲から取り消したコミットの数。0 = コミットなし | - -**記録は追記だけで、上書きしない。** 群の担当は替えると上書きされるが、どの担当がどの試行で失敗したかは記録から読める。見送りの理由(`deferred_items[].defer_reason`)は `実装担当が結果を残しませんでした(agy: stalled → codex: missing)` の形にする。改修計画の「見送った項目」の表にそのまま出る。 - -### 機能とデータの対応 - -| 機能 | 群の `attempt` / `impl` / `status` | 群の `failed_attempts` | `final_gate.failed_attempts` | `deferred_items` | -| --- | --- | --- | --- | --- | -| F2 適用の結果なし | U | C | — | C(上限) | -| F3 項目なし | U(`dropped`) | — | — | — | -| F4 修正の結果なし | — | C | — | — | -| F5 最終ゲートの結果なし | — | — | C | — | - -### 群の状態の遷移 - -```mermaid -stateDiagram-v2 - [*] --> pending: merge-proposals - pending --> dropped: next-apply-round(empty) - pending --> pending: merge-apply(結果なし・open)
担当を替える - pending --> dropped: merge-apply(結果なし・exhausted) - pending --> dropped: merge-apply(未割当・検証の失敗) - pending --> applied: merge-apply(取り込んだ) - applied --> dropped: merge-apply(取り込み済み・採用 0 件) - applied --> verified: verify-round(通った) - applied --> dropped: abandon-items / merge-test-judgements - verified --> [*] - dropped --> [*] -``` - -**未着手のまま同じ担当で無条件に開き直す遷移は無い。** 同じ担当で開き直すのは、交代先が無く起動し直しの可否が真のとき 1 回だけである(決定 8)。 - -## 入出力の契約 - -### 結末の読み取り(変更) - -| 項目 | 内容 | -| --- | --- | -| 名前 | `read_result(state, runtime, phase, round_no=None) -> LaunchOutcome` | -| 入力 | `state`: 状態ファイルの辞書(`tmp_dir` と `id` を読む)。`runtime`: 担当。`phase`: `apply` / `fix` / `final-fix`。`round_no`: 提案ラウンド(`final-fix` は省く) | -| 出力 | `read_launch_outcome(state["tmp_dir"], stem_for(runtime, phase, state["id"], round_no))` の値をそのまま | -| 失敗の形 | **失敗しない。** `die` せず、標準出力・標準エラーに書かない | -| 互換性 | 引数が変わる。呼び出し元 3 か所と `test_git_facts.py` の 3 件を書き直す | - -### 取り込みの共通手順(新設) - -| 名前 | 入力 | 出力 | 失敗の形 | -| --- | --- | --- | --- | -| `confirm_range(state, scope)` | `scope.holder[scope.base_key]` と HEAD | 新しい順のコミットの列。確定できなければ `None` | 失敗しない | -| `discard_unverified(path, state, scope, ordered_range)` | 取り消す範囲 | 取り消した数。`holder["pending_push"]` を立てて保存 → 新しい順に取り消す → `holder[base_key]`(と `mirror["base_sha"]`)を HEAD にして保存 | 取り消しに失敗したら既存の `_revert_range` が着手前へ戻して 4 で中断する | -| `already_closed(scope)` | `scope.records["failed_attempts"]` | 同じ `phase` と `attempt` の記録があれば真 | 失敗しない | -| `close_without_result(path, state, scope, outcome)` | `LaunchOutcome` | `ClosedAttempt`。範囲が `None` なら `range_unknown=True` で即返す。範囲にコミットがあれば `discard_unverified`。`records["failed_attempts"]` に 1 件足して保存。取り消したときだけ `push_with_retry_marker(holder)` | 取り消しの失敗は上と同じ | - -### 群の進行(変更・新設) - -| 名前 | 契約 | -| --- | --- | -| `apply_groups(entry)` | `apply_rounds` の鍵が無いときだけ古い版として群を 1 つ作る。空の配列はそのまま返す | -| `current_group(entry)` | 群が 1 つも無いときは 4 で中断する | -| `group_reopening(group) -> str` | `items` が空なら `empty`。`phase: apply` の失敗の件数を n として、n ≥ `MAX_APPLY_ATTEMPTS` なら `exhausted`、`attempt` > n なら `resume`、それ以外(`attempt` == n)は `open` | -| `impl_for_seq(state, seq) -> tuple[str, Optional[str]]` | 輪番の通し番号から担当と要求モデル。中身は G1 の先後で `assignment.assign(seq, host)` か `impl_assign(participants, seq)` を包む | - -### コマンドの終了コード - -| コマンド | 0 | 1 | 2 | 4 | -| --- | --- | --- | --- | --- | -| `next-apply-round` | 群を開いた | 残りの群が無い(群が無いラウンド・すべて `dropped` を含む) | — | — | -| `merge-apply` | 取り込んだ | — | **この群を取り消した、または担当を替えて開き直す**(意味を広げる) | 着手前テストが `green` でない・範囲を確定できない(2 から変更)・群が無い(新) | -| `merge-fix` | 取り込んだ | — | 範囲を確定できない・**結果なし(新。修正ラウンドは進む)** | 群が無い(新) | -| `merge-final-fix` | 取り込んだ | — | 範囲を確定できない・**結果なし(新。取り消して起点を戻す)** | 修正担当が未記録(変更なし) | - -骨組みは適用の取り込みの 2 で `continue` し、修正と最終ゲートの修正の取り込みの終了コードを見ない。**どちらも変えない。** - -### 起動の出力と骨組み - -起動の出力に無進捗の許容(`IMPL_STALL_TIMEOUT=`)を足す。骨組みの差分は、適用・修正・最終ゲートの修正の 3 つの監視の呼び出しに、無進捗の引数(`--stall-timeout "$IMPL_STALL_TIMEOUT"`)を 1 行ずつ足すことだけである。適用の取り込みの注記は「終了コード 2 = この群を取り消した、または担当を替えて開き直す」に改める。 - -### 雛形に足す文 - -| 雛形 | 足す文 | -| --- | --- | -| `apply.md` / `fix.md` のコミットの規約 | 4 つのトレーラーは**メッセージの最後の段落**に置く。実行環境が帰属行を足すときは、空行を挟まず同じ段落に続ける | -| `apply.md` / `fix.md` / `final-fix.md` | 作業段階が進むたびに `$RF_STEM-progress.log` へ 1 行追記する(`start` / `edit` / `test` / `commit` / `done` と対象だけ) | - -## 処理の流れ - -### 群を開く - -群を開く側(`next-apply-round`)の流れである。 - -```mermaid -graph TD - S[status が pending / applied の群を順に見る] --> K{status} - K -->|無い| E1[終了コード 1] - K -->|applied| O[開き直す
起点・attempt を動かさない] - K -->|pending| R{group_reopening} - R -->|empty| D[dropped / empty] --> S - R -->|exhausted| D - R -->|open| INC[attempt を 1 進め
起点を HEAD にする] - R -->|resume| OUT[APPLY_ROUND / IMPL を出す] - INC --> OUT - O --> OUT -``` - -### 適用の取り込み - -適用の取り込み(`merge-apply`)の流れである。 - -```mermaid -graph TD - B[置き土産を捨てる / 取り消しと push の再開] --> G{取り込み済みか} - G -->|採用あり| R0[終了コード 0] - G -->|採用 0 件| DR[dropped / empty] --> R2[終了コード 2] - G -->|未取り込み| BL{着手前テストが green} - BL -->|いいえ| A4[終了コード 4] - BL -->|はい| AC{already_closed} - AC -->|はい| R2 - AC -->|いいえ| RD[read_result] - RD -->|payload あり| RG{confirm_range} - RG -->|None| A4 - RG -->|範囲| V[既存の検証と取り込み] - RD -->|結果なし| CF[_close_failed_attempt] - CF -->|range_unknown| A4 - CF --> R2 -``` - -結果なしの試行を閉じる処理(`_close_failed_attempt`)は次の順で行う。 - -1. 結果なしの一連(`close_without_result`)を呼ぶ。範囲の確定 → 取り消し → 結末の記録へ 1 件(`{phase: apply, attempt, impl, reason, detail, at, reverted}`)の順である。試行の番号が 0 なら 1 として記録し、群の試行の番号を 1 にする。範囲が確定できない(`range_unknown`)なら項目を `blocked` にして 4 で中断する -2. 開き直しの判定を読む。「開く」なら輪番の通し番号を 1 ずつ進めて輪番から担当を引く。結末の記録にある担当のどれとも違う担当が出たら、その担当と要求モデルで群の担当(`impl` / `impl_model`)を書き換える。参加者の数だけ進めても出なければ(輪番は参加者の数で 1 周する)、起動し直しの可否が真なら担当を替えずに終え、偽なら手順 3 と同じく取り消す -3. 「上限に達した」なら群の項目を `abandoned` にして見送り(`deferred_items`)へ入れる。群は取り消し済み(`dropped`、理由 `no_result`)にし、取り込みの時刻(`apply.merged_at`)を立て、局面を `phase_after_group` にする -4. 保存して 2 で終わる(push は手順 1 が取り消したときに済ませている) - -### 修正の取り込みと最終ゲートの修正の取り込み - -修正の取り込み(`merge-fix`)と最終ゲートの修正の取り込み(`merge-final-fix`)は同じ流れを通る。 - -```mermaid -graph TD - S[置き土産を捨てる / push の再開] --> AC{already_closed} - AC -->|はい| R2[終了コード 2] - AC -->|いいえ| RD[read_result] - RD -->|payload あり| EX[既存の範囲の確定・検証・取り込み] - RD -->|結果なし| CW[close_without_result] - CW -->|range_unknown| RU[既存の扱い
fix: fix_rounds を進めて 2 / final-fix: 2] - CW --> ADV{relaunch_same_agent} - ADV -->|真| P1[fix: fix_rounds を 1 進める
final-fix: 進めない(final-gate が進める)] - ADV -->|偽| PM[fix / final-fix とも
fix_rounds を max_fix_rounds にする] - P1 --> R2 - PM --> R2 -``` - -3 つの取り込みが渡す取り込みの範囲の値(`IntakeScope`)は次のとおりである。結果を読めたときの検証の失敗も取り消し(`discard_unverified`)を呼ぶ。該当は 3 つで、`_revert_unverified_apply_round` / `_revert_invalid_fix_round` / 最終ゲートの `unassigned or problems` である。 - -| 取り込み | `holder` | `base_key` | `records` | `phase` | `attempt` | `impl` | `label` | `mirror` | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `merge-apply` | `entry` | `apply_base_sha` | 群 | `apply` | 群の `attempt` | 群の `impl` | `R<提案ラウンド>-A<群の番号>` | 群 | -| `merge-fix` | `entry` | `fix_base_sha` | 群 | `fix` | `entry.fix_attempts` | 群の `impl` | `R<提案ラウンド>-fix` | なし | -| `merge-final-fix` | `final_gate` | `fix_base_sha` | `final_gate` | `final-fix` | `final_gate.fix_rounds` | `final_gate.impl` | `final-gate-fix` | なし | - -## 非機能の実現方式 - -| 大項目 | 要求の条件 | 実現方式 | 確かめ方 | -| --- | --- | --- | --- | -| 性能・拡張性 | 適用担当の起動が群の数 × 2 回、修正担当と最終ゲートの修正担当の起動が `--max-fix-rounds` 回を超えない | `group_reopening` の上限(決定 6・7)、結果なしでも `fix_rounds` を進めること(決定 10・11)、起動し直せない結末で上限へ進めること | AC11(開く回数)、AC24・AC26(`should-abandon` が 0)、AC30 | -| 運用・保守性 | 取り消した理由が改修計画から読め、3 つの取り込みの記録が同じ形 | `defer_reason` に担当と理由を並べる。記録は `failed_attempts[]` の 1 形式(決定 5) | AC5、AC10 | - -## テスト設計 - -実行は `uv run --with pytest pytest plugins/ndf/skills/cross-refactoring/tests -q` である。状態の遷移は関数を直接呼ぶ既存の形(`no_git` / `patch_lib` / `git_facts`)で確かめる。トレーラーは一時リポジトリで実際に git を実行する。共通層の読み取りは差し替えず、一時ディレクトリに結果ファイルと監視の結果ファイルを置いて本物を通す。 - -| 受け入れ条件 | 何で確かめるか | 置き場所 | -| --- | --- | --- | -| AC1、AC2 | 一時ディレクトリに結果ファイルなし / 監視の結果ファイル(`stalled` / `usage_limit`)を置き、`read_result` の 5 つの欄。`capsys` で出力が空 | `test_intake.py` | -| AC3 | `stem_for` の 3 工程の値と、`SKILL.md` の `--stem-template` を担当名で埋めた値の一致 | `test_skill_terms.py` | -| AC4〜AC7 | `IntakeScope` を 3 つの取り込みの形で作り、`commits_in_range` が 1 件 / 0 件を返す状態で `close_without_result`。`no_git` の記録に `git revert` / `git push` が出るか、記録の件数、2 度目の呼び出しで件数が変わらないこと | `test_intake.py` | -| AC8 | `commits_in_range` が `None` を返す状態で 3 つの取り込みを呼び、`SystemExit` の値 | `test_intake.py` | -| AC9〜AC13、AC15、AC17 | 群 2 つ / 4 つの状態で結果ファイルを置かずに(AC12 は壊れた JSON と配列で)`next-apply-round` → `merge-apply`。`status` / `impl` / `attempt` / `failed_attempts` / `apply_seq` / `deferred_items` | `test_apply_attempts.py` | -| AC14 | `impl_for_seq` を常に同じ担当を返す関数に差し替え、監視の結果ファイルを `usage_limit` / `missing` で置く | `test_apply_attempts.py` | -| AC16 | 着手前テスト `red` で `SystemExit(4)`、結果ファイルを読まない(読み取りを差し替えて呼ばれないこと) | `test_apply_attempts.py` | -| AC18 | `group_reopening` を差し替え、`next-apply-round` と `merge-apply` の分岐が変わる | `test_apply_attempts.py` | -| AC19〜AC22 | 採用 0 件から `merge-proposals` → `next-apply-round`。鍵なし・rf587 の形・`items: []` の `pending` | `test_apply_rounds.py` | -| AC23〜AC27 | 群の担当と提案ラウンドの担当を分けた状態で `merge-fix`。AC26 は監視の結果ファイルを `usage_limit` で置き `should-abandon` まで | `test_abandon_items.py` | -| AC28〜AC31 | `final_gate` に `fix_base_sha` と `impl` を置き、結果ファイルなしで `merge-final-fix` → `final-gate`。`fix_commits` / `fix_base_sha` / `fix_rounds` / 終了コード | `test_final_fix.py` | -| AC32〜AC38 | 一時リポジトリで各形のコミットを作り、`commit_trailers` と `verify_apply_round` | `test_commit_trailers_git.py` | -| AC39、AC42 | 雛形の文言を `grep` で探す | `test_skill_terms.py` | -| AC40 | `_emit_init` の出力に `IMPL_STALL_TIMEOUT=1800` | `test_init.py` | -| AC41、AC43、AC44 | `SKILL.md` と `docs/02` / `docs/04` の骨組みの `monitor.py` の引数、語の表の行、段落の有無、結果なしの記述 | `test_skill_terms.py` | -| AC45 | トレーラーの節の記載をレビューで見る | 手動 | -| AC46 | 既存の `test_a_verified_apply_round_marks_every_item_applied` に `failed_attempts` が無いことを足す | `test_merge_apply.py` | -| AC47、AC48 | 全体のテスト、`bash scripts/build-runtime-plugins.sh --check`、`claude plugin validate .`、`python3 scripts/check-skill-frontmatter.py` | 手動 | -| AC49 | `impl_for_seq` を差し替え、群の割り当て・交代先・最終ゲートの修正担当の 3 つ | `test_final_fix.py` / `test_apply_attempts.py` | -| AC50 | `git grep -n revert_unverified_range -- plugins/ndf/skills/cross-refactoring/scripts` が 0 件 | 手動(レビューの手順) | - -## 未確認のまま残ること - -| # | 項目 | 内容 | 決める時点 | -| --- | --- | --- | --- | -| 1 | G3 の実装の形 | `LaunchOutcome` の欄の名前は PR #781 の設計のとおりとしている。実装で変われば `read_result` の包みが吸収し、取り込みは変わらない | G3 の実装 Pull Request のマージ | -| 2 | G1 の先後 | **決まった。** 実装の着手時点の開発版の起点に参加者の決め方の変更は入っていなかった(`git grep impl_assign` は設計文書だけに当たる)。輪番から担当を引く関数は現行の割り当て(`assignment.assign(seq, host)`)を包む形で書いた。後から入る側がその中だけを差し替える | 決定済み | -| 3 | 担当を替えた後の担当も結果を残さない割合 | 2 回目で救える群の数は測っていない。実行の要約の `apply_attempts` で数える | 配布後 | -| 4 | トレーラーの形でない署名を末尾に足すランタイム | codex / agy / kiro のコミットで帰属行の段落を見ていない。散文の段落を足す者が現れれば決定 13 は効かない | 次の実行の `failed` の理由を読む | -| 5 | 帰属行を同じ段落に続ける指示に claude が従うか | 決定 14 は補助で、従わなくても決定 13 で検証は通る | 実装後の最初の実行 | -| 6 | 担当が雛形の進捗マーカーに従うか | 従わなくても決定 15 の許容で打ち切られないのはテスト 1 回分まで | 実装後の最初の実行 | -| 8 | 適用の説明の行数 | 結果なしの節を足したことで行数の上限(500 行)に達したため、改修計画の節を報告の説明へ移した。次に節を足すときは分割が要る | 次に適用の説明を書き足すとき | -| 7 | 修正の担当が利用上限のとき、群の他の項目を救う手段 | 決定 10 は修正を見送りへ進める。担当を替えて修正を続ける形は、直しかけの文脈が要るため採らなかった。見送りが増えれば見直す | 配布後 | - -## 申し送り(並行する設計との境界) - -| 相手 | 決めた契約 | どちらが何をするか | -| --- | --- | --- | -| G3(#729) | `read_launch_outcome` / `LaunchOutcome` / `NO_RELAUNCH_REASONS`(PR #781 の「入出力の契約」)。監視の終了コードと標準出力は変えない | G3 が共通層を実装し先にマージする。G4 は `read_result` の包みで呼び、`failed_attempts[].reason` に `reason` を、交代の分岐に `relaunch_same_agent` を写す。G3 の既存の設計にあった `apply._monitor_reason` / `gitfacts.load_result` は作らない | -| G1(#727) | 輪番の母集合と `impl_assign(participants, seq)` | G1 が `assignment.py` を変える。G4 の呼び出しは `rounds.impl_for_seq` の 1 か所で、後からマージする側がその中身を合わせる。`gate._final_fix_impl` の `assignment.assign`(`gate.py:128`)は G4 が `impl_for_seq` へ寄せる | -| D-A(#662 の P1) | 実行の要約の `apply_attempts` は、鍵 `"r<ラウンド>-g<群>"` → `{"attempts", "failed", "dropped_reason"}` を状態ファイルの群から作る | 既存の設計の契約を引き継ぐ。`failed` は `phase: apply` の件数 | -| G6(#678) | `conftest.py` の監視の環境変数 | 触るファイルが重ならない | - -## 既存の設計との対応 - -| 既存の決定(PR #665) | この文書 | 変わったこと | -| --- | --- | --- | -| 決定 1(1 本の Pull Request) | 決定 2 | #674 を範囲に足した | -| 決定 2(上限 2 回・引数なし) | 決定 6 | 同じ | -| 決定 3(次の輪番へ替える) | 決定 8 | 替える先が無いときの分岐に `relaunch_same_agent` を使う | -| 決定 4(判定は `merge-apply`、骨組みは分岐しない) | 決定 7 | 判定を `rounds.group_reopening` へ移し、`next-apply-round` も同じ関数を読む | -| 決定 5(試行番号は `next-apply-round` が進める) | 決定 7 | `resume` / `open` の判定を同じ関数に含めた | -| 決定 6(取り消しの本体を切り出して共有し、最終ゲートは #674) | 決定 4・11 | `intake.py` に 3 つの取り込みの手順を置き、最終ゲートも通す | -| 決定 7(着手前テストと範囲の未確定は 4) | 「入出力の契約」 | 同じ。範囲の確定は `confirm_range` を通る | -| 決定 8(`rounds.impl_for_seq`) | 決定 9 | G1 との先後を明記 | -| 決定 9・10(採用 0 件と項目の無い群) | 決定 12 | `group_reopening` の `empty` へ寄せた | -| 決定 11・12(トレーラー) | 決定 13・14 | #553 の本文が改めた修正レイヤー(進行側の `--amend`)を採らない理由を足した | -| 決定 13(無進捗の許容) | 決定 15 | 同じ | -| 構成要素の `apply._monitor_reason` / `gitfacts.load_result` | — | G3 の `read_launch_outcome` に置き換わり、作らない | -| — | 決定 1・3・5・10 | 新設(文書の置き方、`read_result` の包み、記録の形、修正の結果なしと起動し直せない結末) | - -## 関連文書と前後関係 - -| 項目 | 内容 | -| --- | --- | -| 要求と受け入れ条件 | [issue-728-647-592-553-requirements.md](issue-728-647-592-553-requirements.md)。この文書は「どう作るか」だけを扱う | -| 置き換える既存の設計 | [issue-647-592-553-design.md](issue-647-592-553-design.md)(PR #665)。**この文書が置き換える。** 引き継ぐ決定と変える決定は「既存の設計との対応」にある | -| 従う契約 | G3(#729、PR #781)が決めた結末の読み取りの契約。その先(3 つの取り込みが値をどう扱うか)をこの文書が決める | -| 実装の単位と順序 | 1 本の Pull Request にまとめる(決定 2)。G3 の実装 Pull Request が `develop` に入った後に始める | diff --git a/issues/issue-728-647-592-553-plan.md b/issues/issue-728-647-592-553-plan.md deleted file mode 100644 index eb4ffa65e..000000000 --- a/issues/issue-728-647-592-553-plan.md +++ /dev/null @@ -1,205 +0,0 @@ -# cross-refactoring: 実装担当が結果を残さないと同じ群が上限なしに開き直され、未検証のコミットが残る → 結果なしを取り込みの 1 か所で受けて取り消し、群が開いた回数と結末で開き直しを決める(実装計画 / #728 #647 #592 #553) - -## 関連リンク - -| 文書 | 何を持つか | -| --- | --- | -| [issue-728-647-592-553-requirements.md](issue-728-647-592-553-requirements.md) | 受け入れ条件 50 件(AC1〜AC50) | -| [issue-728-647-592-553-design.md](issue-728-647-592-553-design.md) | 決定 15 件、入出力の契約、テスト設計 | -| [issue-647-592-553-design.md](issue-647-592-553-design.md) | 置き換えられる既存の設計 | -| 課題 | #728(親)/ #647 / #592 / #553 / #674(最終ゲートの修正。閉じるのは棚卸に任せる) | - -**用語は設計文書の「用語の対応」の表で引く。** この計画は業務用語で書き、識別子はコードブロックと表にだけ置く。 - -## モード - -`standard`。公開しているコマンドの終了コードの意味と、状態ファイルの構造が変わる。複数のモジュールにまたがる。 - -## 目的と非目的 - -達成したい状態: - -- 担当の作業結果が残っていなくても、3 つの取り込み(適用・修正・最終ゲートの修正)が同じ手順で受け、未検証のコミットを取り消し、結末を記録して終わる -- 同じ改善項目の集まりを開き直す回数が 2 回で止まり、2 回目は別の担当が試す -- 採用が 0 件だった提案ラウンドと、項目が 1 件も無い集まりでは担当を起動しない -- 実行環境が帰属行を足したコミットでも、必須の記名(トレーラー)が読める - -やらないこと: - -- 監視そのもの(`plugins/ndf/scripts/lib/monitor.py`)の変更。結末の読み取りの共通層は別の課題(#729)が入れ終えている -- 参加者の決め方の変更(#727)。輪番から担当を引く 1 つの関数に寄せるところまでを行う -- 記録の集計(実行の要約の新しい欄)。契約だけを合わせ、集計は別の課題が行う - -## 前提 - -- 前提 1: 結末の読み取りの共通層(`plugins/ndf/scripts/lib/monitor_outcome.py`)は開発版の起点に入っている。`read_launch_outcome(tmp_dir, stem, result_path=None)` が 5 つの欄を持つ値を返し、同じ担当を起動し直せない理由は利用上限(`usage_limit`)の 1 語だけである -- 前提 2: 参加者の決め方の変更(#727)は開発版の起点に入っていない。輪番から担当を引く関数の中身は、現行の割り当て(`assignment.assign(seq, host)`)を包む形で書く。後から入る側がその中だけを差し替える -- 前提 3: 同じ提案ラウンドの別の変更(#727 の後続)が、担当の割り当ての 2 行(`commands/apply.py:134` と `commands/gate.py:128`)を触る。後からマージする側が輪番から担当を引く関数の中身を揃える - -## 実測 - -この計画のために実行して確かめた値である。 - -| 見たもの | 値 | -| --- | --- | -| 変更前のテスト | `uv run --with pytest pytest plugins/ndf/skills/cross-refactoring/tests -q` が 704 件成功・終了コード 0(31.8 秒) | -| 記名の解析に題名が要るか | **要る。** 記名だけの段落を単独で `git interpret-trailers --parse` へ渡すと出力が空になる。題名の行と空行を前に付けると 4 つとも返る(git 2.53.0) | -| 散文と記名が混ざる段落 | 出力は空。題名を付けても同じ | -| 題名の位置に記名の形の行を置いた場合 | その行が記名として返る。**先頭の段落を解析に掛けないことで避ける** | -| 解析にリポジトリが要るか | 要らない。リポジトリの外の現在地でも終了コード 0 で動く | - -## 受け入れ条件 - -要求の文書の AC1〜AC50 をそのまま用いる。この計画では各タスクが満たす番号だけを示す。検証手段は要求の文書の「検証手段」と設計文書の「テスト設計」が持つ。 - -## 代替案と採否 - -| 案 | 内容 | 採否 | 理由 | -| --- | --- | --- | --- | -| 結末の読み取りを包みとして残す | 名前を残し、引数を状態と工程に変える | 採用 | 結果ファイルの名前の幹を組む場所が 1 つになる(設計の決定 3) | -| 取り込みが共通層を直に呼ぶ | 包みを置かない | 不採用 | 名前の幹の組み立てが 3 か所に分かれる | -| 記名を進行側が付け直す | 取り込みでコミットを書き換える | 不採用 | コミットの識別子が変わり、申告と実体の対応が切れる(設計の決定 13) | - -## ドメイン用語 - -設計文書の「用語の対応」の表を正本とする。この計画で追加する語は無い。 - -## 不変条件 - -- 検証を受けていないコミットを、公開したまま次の工程へ渡さない -- 取り消した後の作業ツリーの先端が、次の範囲の起点になる -- 同じ工程・同じ試行番号の結末は、記録に 1 件しか入らない - -## 互換性 - -| 対象 | 変更 | 互換性の扱い | -| --- | --- | --- | -| 適用の取り込みの終了コード | 着手前テストが成功と確認できていない場合が 2 から 4 へ。2 の意味に「担当を替えて開き直す」が加わる | 骨組みの分岐は変えない(2 で次の群へ進む、4 で止まる、のどちらも既存の扱い) | -| 修正・最終ゲートの修正の取り込みの終了コード | 結果なしで 2 を返す場合が増える | 骨組みは両者の終了コードを見ない | -| 結末の読み取りの引数 | ファイルのパスと担当 → 状態・担当・工程・提案ラウンド | 内部の関数。古い形の呼び出しは実行時に失敗する | -| 状態ファイル | 集まりの側に試行番号・結末の記録・取り消しの理由、最終ゲートの記録に結末の記録を足す | 版を上げない。鍵が無い状態ファイルは試行 0・失敗なしとして読む | -| 取り消しの本体 | 事実の読み取りの層にあったものを取り込みの共通手順へ移す | 内部の関数。呼び出し元 2 か所を同じ変更で書き換える | - -## 修正対象 - -```text -plugins/ndf/skills/cross-refactoring/ -├── SKILL.md -├── docs/02-apply-and-review.md -├── docs/04-fix-and-report.md -├── prompts/{apply,fix,final-fix}.md -├── scripts/refactor_lib/ -│ ├── intake.py # 新設 -│ ├── gitfacts.py -│ ├── rounds.py -│ ├── vocabulary.py -│ └── commands/{apply,converge,gate,setup}.py -└── tests/ - ├── test_intake.py # 新設 - ├── test_apply_attempts.py # 新設 - ├── test_commit_trailers_git.py # 新設 - └── test_{git_facts,merge_apply,apply_rounds,abandon_items,final_fix,skill_terms,init}.py -``` - -配布物(`plugins/ndf/dev.kiro/` と `plugins/ndf/dev.agy/`)は `bash scripts/build-runtime-plugins.sh` で揃える。 - -## タスク分解 - -### Task 1: 起動の結末を値で受け取る - -- **対象ファイル:** `scripts/refactor_lib/gitfacts.py`、`tests/test_intake.py`(新設)、`tests/test_git_facts.py`、`tests/test_skill_terms.py` -- **変更内容:** 結末の読み取り(`read_result`)を、状態・担当・工程・提案ラウンドから結果ファイルの名前の幹を組み、共通層の読み取りを呼んで値を返す包みにする。中断も画面への出力も行わない。名前の幹を組む関数(`paths.stem_for`)の値と、手順書が監視へ渡す名前の雛形(`--stem-template`)を担当名で埋めた値の一致を確かめる -- **満たす受け入れ条件:** AC1、AC2、AC3 -- **進め方:** 一時ディレクトリに結果ファイルを置かない状態・監視の結果ファイルだけを置いた状態でそれぞれ失敗するテストを書き、包みを差し替えて通す。既存の 3 件(中断を現状固定していたもの)は、値を返す形へ書き直す - -### Task 2: 取り込みの共通手順を新設し、取り消しを 1 つにする - -- **対象ファイル:** `scripts/refactor_lib/intake.py`(新設)、`gitfacts.py`、`commands/{apply,converge,gate}.py`、`tests/test_intake.py` -- **変更内容:** 範囲の確定・未検証のコミットの取り消し・叩き直しの判定・結果なしの一連を新しいモジュールへ置く。取り込み 1 つ分の「どこを見て、どこへ書くか」は範囲の値(`IntakeScope`)で渡す。事実の読み取りの層にあった取り消し(`gitfacts.revert_unverified_range`)を削除し、修正・最終ゲート・適用の 3 か所を新しい取り消しへ寄せる。試し打ちの対応(`--dry-run`)は取り消しの引数で受ける -- **満たす受け入れ条件:** AC4、AC5、AC6、AC7、AC8、AC50 -- **進め方:** 3 つの取り込みの形の範囲の値を作り、範囲が 1 件・0 件・確定できない場合で失敗するテストを書いてから実装する。取り消しと公開の順序は、既存のテストが見ている記録(外部コマンドの呼び出し列)で確かめる - -### Task 3: 同じ集まりの試行を 2 回で止め、2 回目は別の担当が試す - -- **対象ファイル:** `scripts/refactor_lib/rounds.py`、`vocabulary.py`、`commands/apply.py`、`tests/test_apply_attempts.py`(新設)、`tests/test_merge_apply.py` -- **変更内容:** 開き直しの判定(`rounds.group_reopening`)と、輪番から担当を引く関数(`rounds.impl_for_seq`)を新設する。集まりを開く側と適用の取り込みの両方が同じ判定を読む。結果なしのときは共通手順で取り消しと記録を行い、次の輪番の担当のうち、その集まりで失敗した担当のどれとも違う最初の担当へ替える。替える先が無いときだけ、起動し直しの可否で続けるか取り消すかを決める。着手前テストの確認を結果の読み取りより前へ移し、終了コードを 4 にする -- **満たす受け入れ条件:** AC9、AC10、AC11、AC12、AC13、AC14、AC15、AC16、AC17、AC18、AC49(3 つのうち 2 つ) -- **進め方:** 集まりが 2 つ・4 つの状態で、結果ファイルを置かずに開く → 取り込む、を繰り返す失敗するテストを先に書く。担当の交代は、輪番から担当を引く関数を差し替えて確かめる - -### Task 4: 採用が 0 件の提案ラウンドと、項目の無い集まりで担当を起動しない - -- **対象ファイル:** `scripts/refactor_lib/rounds.py`、`commands/apply.py`、`tests/test_apply_rounds.py` -- **変更内容:** 集まりの一覧を返す関数で、鍵が無いときだけ古い版として 1 つ作り、空の配列はそのまま返す。集まりが 1 つも無い状態で進行中の集まりを引くと中断する。開き直しの判定が「項目が無い」を返した集まりは、開かずに取り消し済みにして次を探す。取り込み済みで採用が 0 件だった集まりも取り消し済みに直す -- **満たす受け入れ条件:** AC19、AC20、AC21、AC22 -- **進め方:** 採用 0 件からの取り込み → 開く、古い形の状態ファイル、項目の無い集まりの 3 通りで失敗するテストを書いてから実装する - -### Task 5: 修正の結果が無いときに修正ラウンドを進める - -- **対象ファイル:** `scripts/refactor_lib/commands/converge.py`、`tests/test_abandon_items.py` -- **変更内容:** 修正の結果を、提案ラウンドの担当ではなく集まりの担当から読む。結果が無ければ共通手順で取り消しと記録を行い、修正ラウンドの数を 1 進めて終了コード 2 で終える。起動し直せない結末では、修正ラウンドの数を上限の値にして見送りの判定へ渡す -- **満たす受け入れ条件:** AC23、AC24、AC25、AC26、AC27 -- **進め方:** 集まりの担当と提案ラウンドの担当を分けた状態で失敗するテストを書く。上限まで続けた後に見送りの判定が 0 を返すところまで通す - -### Task 6: 最終ゲートの修正の結果が無いときにコミットを取り消す - -- **対象ファイル:** `scripts/refactor_lib/commands/gate.py`、`tests/test_final_fix.py` -- **変更内容:** 最終ゲートの修正の取り込みを共通手順へ通す。結果が無ければ取り消して起点を取り消し後の先端へ進め、終了コード 2 で最終ゲートへ判定を戻す。起動し直せない結末では最終ゲートの修正ラウンドの数を上限の値にする。最終ゲートの修正担当の決定を、輪番から担当を引く関数へ寄せる -- **満たす受け入れ条件:** AC28、AC29、AC30、AC31、AC49(残り 1 つ) -- **進め方:** 最終ゲートの記録に起点と担当を置き、結果ファイルを置かずに取り込む → 判定する、の順で失敗するテストを書いてから実装する - -### Task 7: 帰属行の後ろでも記名を読む - -- **対象ファイル:** `scripts/refactor_lib/gitfacts.py`、`prompts/{apply,fix}.md`、`tests/test_commit_trailers_git.py`(新設)、`tests/test_merge_apply.py` -- **変更内容:** コミットのメッセージを空行で段落に分け、末尾の段落から前へ 1 段落ずつ git の解析へ掛ける。解析が記名の段落と判定しなかったところで止める。**先頭の段落(題名)は掛けない。** 解析には題名の行を補って渡す(実測のとおり、補わないと何も返らない)。同じ鍵は末尾に近い段落の値を採る。雛形のコミットの規約に、必須の記名を最後の段落へ置くことと、実行環境が帰属行を足すときは空行を挟まず同じ段落に続けることを書く -- **満たす受け入れ条件:** AC32、AC33、AC34、AC35、AC36、AC37、AC38、AC39 -- **進め方:** 一時リポジトリに各形のコミットを作り、実際に git を実行して確かめる失敗するテストを先に書く - -### Task 8: テストの実行中の無出力で担当を打ち切らない - -- **対象ファイル:** `scripts/refactor_lib/vocabulary.py`、`commands/setup.py`、`SKILL.md`、`prompts/{apply,fix,final-fix}.md`、`tests/test_init.py`、`tests/test_skill_terms.py` -- **変更内容:** 起動の出力に無進捗の許容を足す。値はテストの制限時間に 900 秒を加えたものとする。手順書の骨組みで、適用・修正・最終ゲートの修正の 3 つの監視の呼び出しにこの値を渡す。3 つの雛形に、作業段階が進むたびに進捗の記録へ 1 行足す指示を書く -- **満たす受け入れ条件:** AC40、AC41、AC42 -- **進め方:** 起動の出力と手順書の骨組みを読む失敗するテストを先に書く - -### Task 9: 手順書と説明文書を実装に合わせる - -- **対象ファイル:** `SKILL.md`、`docs/02-apply-and-review.md`、`docs/04-fix-and-report.md`、`tests/test_skill_terms.py` -- **変更内容:** 使う語の表の適用ラウンドの行に、同じ集まりの試行の上限(2 回)を書く。上限を 1 つに保つとしていた段落を外す。適用の工程と修正・最終ゲートの工程の説明に、結果が無いときの取り消し・記録・終了コードを書く。記名の節に、git の標準の読み方が最後の段落しか読まないことと、進行側の読み方の 2 つを書く -- **満たす受け入れ条件:** AC43、AC44、AC45 -- **進め方:** 文言を読むテストを先に書く。記名の節の記載(AC45)は Pull Request のレビューで見る - -### Task 10: 退行の確認と配布物の同期 - -- **対象ファイル:** `tests/test_merge_apply.py`、配布物一式 -- **変更内容:** 結果があり検証を通る適用が、変更前と同じく 1 回目の試行で取り込まれ、結末の記録を持たないことを既存のテストへ足す。配布物を同期し、定義と frontmatter の検査を通す -- **満たす受け入れ条件:** AC46、AC47、AC48 -- **進め方:** 全体のテスト → 同期 → 3 つの検査の順で実行し、結果を Pull Request 本文へ載せる - -## 影響範囲 - -| 影響を受けるもの | 何が変わるか | -| --- | --- | -| 収束リファクタリングの骨組み | 監視の呼び出しに引数が 1 つ増える。分岐は変えない | -| 進行中の実行の状態ファイル | 新しい鍵は無くても読める。項目の無い集まりを持つ既存の状態は、開こうとした時点で取り消し済みになる | -| 実行の要約と改修計画 | 見送りの理由に、どの担当がどの理由で結果を残さなかったかが並ぶ | -| 収束レビューの共通層 | 読むだけで変えない | - -## リスクと対処 - -| リスク | 対処 | -| --- | --- | -| 適用の取り込み(893 行)が 1 つの関数で結果の読み取り・検証・取り消し・記録を抱えており、分岐を足すと読めなくなる | **先に構造を整える。** Task 2 で取り消しを共通手順へ出し、Task 3 の分岐はそこへ委ねる。新しい分岐を既存の関数へ直接足さない | -| 適用の検証のテスト(1735 行)が終了コードと状態を細かく固定しており、変更が広範囲の失敗として現れる | タスクごとにテストを通す。失敗したテストは、期待値が変わったものと退行とを 1 件ずつ切り分けて記録する | -| 記名の解析が git の版で変わる | 実測した版(2.53.0)を計画へ残し、一時リポジトリで実際に git を実行するテストで固定する | -| 担当の割り当ての 2 行を別の変更が触る | 輪番から担当を引く関数の中だけに割り当ての呼び出しを置く。後からマージする側がその中身を揃える | - -## 切り戻し手順 - -Pull Request 単位で戻せる。状態ファイルの版を上げないため、途中まで進んだ実行の状態は変更前のコードでもそのまま読める。足した鍵は読まれずに残るだけである。 - -## 完了の定義 - -- [ ] 受け入れ条件 AC1〜AC50 について、条件ごとに検証手段と結果が対応している -- [ ] `uv run --with pytest pytest scripts/tests plugins/ndf -q` が通る -- [ ] `bash scripts/build-runtime-plugins.sh --check` / `claude plugin validate .` / `python3 scripts/check-skill-frontmatter.py` が終了コード 0 で終わる -- [ ] 手順書と説明文書が、実装した振る舞いと同じことを書いている diff --git a/issues/issue-728-647-592-553-requirements.md b/issues/issue-728-647-592-553-requirements.md deleted file mode 100644 index b2fac640d..000000000 --- a/issues/issue-728-647-592-553-requirements.md +++ /dev/null @@ -1,295 +0,0 @@ -# cross-refactoring: 実装担当が結果を残さないと同じ群が上限なしに開き直され、未検証のコミットが残る → 結果なしを取り込みの 1 か所で受けて取り消し、群が開いた回数と結末で開き直しを決める(要求と受け入れ条件 / #728 #647 #592 #553) - -## 目的 - -- **壊れていること**: cross-refactoring の実装担当が結果ファイルを残さずに終わることがある。このとき取り込みは、下位の読み取りがプロセスを終わらせるため止まる。同じ群が上限なしに開き直される。担当が作ったコミットは検証を受けずに残る。テスト整備の採用 0 件では項目の無い群を起動し続ける。claude が担当の群は帰属行のためトレーラーが読めずに落ちる -- **困る人**: cross-refactoring を回す進行側(手で止めるまで CLI の起動と利用料が続く)と、その Pull Request を読む人(未検証の差分が混じる) -- **直すと成り立つこと**: 3 つの取り込み(適用・修正・最終ゲートの修正)が結果なしを同じ手順で受ける。未検証のコミットを取り消し、結末を記録し、終了コードを返す。適用ラウンドの繰り返しは有限回で終わる。群は開いた回数と前回の結末で、開き直すか・担当を替えるか・取り消すかが決まる。採用 0 件では群を作らない。起動し直しても解けない結末(利用上限)では、同じ担当を同じ工程で起動し直さない。帰属行の後ろでもトレーラーが読める - -この文書は「何を満たすか」だけを扱う。設計、置き換える既存の要求、範囲に入れる子 issue は末尾の「関連文書と前後関係」にある。 - -## 用語 - -本文は左の用語で書く。右の識別子は、引用・表・受け入れ条件の判定値で使う。 - -| 用語 | 意味 | -| --- | --- | -| 取り込み | 担当の CLI が作ったコミットを、進行側が検証して受け入れるコマンド。適用の取り込み `merge-apply` / 修正の取り込み `merge-fix` / 最終ゲートの修正の取り込み `merge-final-fix` の 3 つ | -| 群を開く | `next-apply-round`。次に適用する群を選んで担当を出す | -| 最終ゲート | `final-gate`。全体のテストを実行して判定する | -| 結末の読み取り | `gitfacts.read_result`。担当の結果ファイルを読む | -| 共通層の読み取り | `lib/monitor_outcome.py` の `read_launch_outcome`。G3 が作る | -| 群 | 適用ラウンド。書き換えるファイルが重ならない項目の集まりで、状態の `rounds[].apply_rounds[]` の 1 件 | -| 試行 | 1 つの群に対して適用担当を起動し、適用の取り込みで取り込もうとした 1 回。番号は `attempt` | -| 結末 | 担当 1 回の起動の終わり方。G3 の `LaunchOutcome`(使える結果か、結果なしの理由か) | -| 結果なし | `LaunchOutcome.payload` が `None`。理由は `reason`(`missing` / `unparsable` / `stalled` など) | -| 起動し直しの可否 | `LaunchOutcome.relaunch_same_agent`。偽は同じ担当を同じ条件で起動しても解けない(`usage_limit`) | -| 結末の記録 | `failed_attempts[]`。結果を残さなかった起動の記録で、群と最終ゲートの記録(`final_gate`)が持つ | -| 取り消しの理由 | 群の `drop_reason`(`no_result` / `empty`) | -| 開き直しの判定 | `rounds.group_reopening` | -| 輪番から担当を引く関数 | `rounds.impl_for_seq` | -| 取り込みの共通手順 | 新設の `refactor_lib/intake.py`。取り消しの本体は `intake.discard_unverified` | -| 範囲 | 取り込みが検査するコミットの列。起点(`apply_base_sha` / `fix_base_sha`)から HEAD まで | -| 未検証のコミット | 範囲にあるが、結果なしで検証を受けられなかったコミット | -| 修正ラウンドの数と上限 | `fix_rounds` と `--max-fix-rounds` | -| 無進捗の許容 | `init` が出す `IMPL_STALL_TIMEOUT`。テストの制限時間(`--test-timeout`)+ 900 秒 | -| 帰属行 | Claude Code がコミットメッセージへ足す `Co-Authored-By:` / `Claude-Session:` の行 | -| トレーラーの段落 | `git interpret-trailers --parse` がトレーラーとして読む段落 | -| トレーラーの読み取り | `gitfacts.commit_trailers` | -| G1 / G3 | 実行計画の束の名前。G1 = 参加者の決め方(#727、PR #782)、G3 = 結末の読み取りの共通層(#729、PR #781) | - -## 依頼(原文) - -### #728(根本原因の親) - -> `plugins/ndf/skills/cross-refactoring/scripts/refactor_lib/gitfacts.py` の `read_result` は、結果ファイルが無い・読めないと `die(code=2)` で進行の終了コードを決める(1089 / 1093 / 1096 行目) -> -> それを呼ぶ取り込みが 3 つある(`commands/apply.py` の `merge-apply`、`commands/converge.py` の `merge-fix`、`commands/gate.py` の `merge-final-fix`)。下位の読み取りがプロセスを終わらせるため、群の状態を書く機会と、未検証のコミットを取り消す機会が無い -> -> 群を開き直す `apply.py` の `next-apply-round` は、中断からの再開と失敗した試行のやり直しを同じ `pending` / `applied` で区別しない。`rounds.apply_groups` は項目 0 件の群も作る -> -> ## 採る手 -> -> - 向きの修正(`fix_dependency_direction`): 下位の `read_result` は結果なしを値として返し、進行の扱いは取り込みが決める -> - 統合(`consolidate_duplication`): 3 つの取り込みの「範囲の確定 → 未検証コミットの取り消し → 群の状態の記録」を 1 つの手順にする -> -> ## 完了条件 -> -> - `read_result` は結果なしを値で返し、3 つの取り込みが同じ手順で取り消しと群の状態の記録を行う -> - 群が開いた回数と前回の結末を持ち、開き直しの判定が 1 か所にある。空の群は作らない -> - 各子 issue の再現手順を実行し、現象が出ないことを確かめる - -### #647 - -> `/ndf:cross-refactoring` で、同じ適用ラウンド(書き換えるファイルが重ならない改善項目の群)の適用が**上限なしに再試行される**。後ろの群へ順番が回らず、収束の判定と最終ゲートへ届かない。 -> -> **PR #757:** kiro が適用フェーズで 15 秒で終わり `kiro-apply-r1-result.json` を残さない → `merge-apply` が 2 を返し、駆動側の `continue` が `next-apply-round` へ戻る → 同じ群を再び開く。**29 回繰り返した時点で手で止めた。** - -### #592 - -> **テスト整備ラウンドの採用が 0 件のとき、項目の無い適用ラウンドが開き、上限なしに同じ群を繰り返す。** - -### #553 - -> `cross-refactoring` の適用ラウンドで、**claude が実装担当のときだけ**必須トレーラー(`Item-Id` / `Round` / `Impl-Runtime` / `Impl-Model`)が読めず、群が丸ごと取り消される。 -> -> **担当に書かせて、git の最終段落の定義で読み返す構造**が、ランタイムが後ろへ段落を足すたびに壊れる。読み取りを段落単位にする直しは現れている場所の直しで、次に別の書式の署名を足すランタイムが現れれば同じ形が起きうる。進行側が知っている値を進行側が取り込みで書けば、担当のランタイムの帰属行の書式に左右されない。 - -### #674 - -> `merge-final-fix` は `read_result` が結果ファイルの欠落で `die(code=2)` し、範囲の検査(`unassigned_fix_commits` / `verify_final_fix_commit`)と取り消しへ進まない。`final-gate` は担当が作ったコミットを含む HEAD でテストし、落ちれば `fix_base_sha` を HEAD へ置き直す。そのコミットは以後どの範囲にも入らない。**検証を受けていないコミットが Pull Request に残りうる。** - -## 前提 - -| # | 前提 | -| --- | --- | -| 1 | G3(#729、PR #781)の契約に従う。`lib/monitor_outcome.py` に `read_launch_outcome(tmp_dir, stem, result_path=None) -> LaunchOutcome`(`payload` / `reason` / `detail` / `monitor` / `relaunch_same_agent`)と `NO_RELAUNCH_REASONS = {"usage_limit"}` が入り、理由の語彙は 9 語(`ok` / `timeout` / `stalled` / `early_error` / `usage_limit` / `cli_timeout` / `missing` / `pidfile_bad` / `unparsable`)である。**G3 の実装 Pull Request が `develop` に入った後に、この変更の実装を始める** | -| 2 | 監視の終了コード 0〜6 と標準出力は変わらない(G3 の申し送り)。骨組みは終了コードで分岐しない | -| 3 | P1(監視の結果ファイル `-monitor.json`)と P2(`--phase` と `lib/limits.py`)は v10.13.0 で `develop` に入っている | -| 4 | 輪番の母集合は G1(#727、PR #782)が変える(既定を codex / kiro / ホストにし、`impl_assign(participants, seq)` を新設)。この変更が担当を引く呼び出しは `rounds.impl_for_seq` の 1 つで、G1 と先後どちらでも変える場所はその中だけである | -| 5 | Claude Code が足す帰属行は、メッセージの末尾に独立した段落として付く(#553 の実測 `26a0fff`)。他のランタイムが足す署名もトレーラーの形(`Key: value` の行だけの段落)である | -| 6 | 除外されない参加者が 2 者以上いる。1 者しかいない実行では、担当の交代先が無いため、結末の可否だけで 2 回目を開くか取り消すかを決める(設計文書の決定 8) | - -## 対象範囲 - -含む: - -| 変えるもの | 内容 | -| --- | --- | -| 結末の読み取りの契約 | 結果なしを値で返す。プロセスを終わらせない(`die` しない) | -| 取り込みの共通手順の新設 | 3 つの取り込みの「範囲の確定 → 未検証コミットの取り消し → 結末の記録」を共通化する(`refactor_lib/intake.py`) | -| 取り消しの本体の一本化 | `gitfacts.revert_unverified_range` と `apply._revert_unverified_apply_round` の 2 つを 1 つにする | -| 群の開き直しの判定の一本化 | 開き直しの判定(`rounds.group_reopening`)と、群が持つ試行の記録(`attempt` / `failed_attempts` / `drop_reason`) | -| 試行の上限と担当の交代 | 同じ群の試行の上限(2 回)と、2 回目の担当の交代 | -| 採用 0 件の扱い | 採用 0 件の提案ラウンドで群を作らない。項目の無い群を開かない(#592) | -| 修正の取り込みが読む担当 | 修正の取り込みが読む結果の担当と、結果が無いときの修正ラウンドの数え方 | -| 最終ゲートの修正の結果なし | 最終ゲートの修正の取り込みが、結果なしで未検証のコミットを取り消す(#674) | -| 起動し直せない結末 | 起動し直しの可否が偽のときの 3 つの取り込みの振る舞い | -| トレーラーの読み方 | トレーラーの読み取りの読み方と、適用・修正の雛形のコミットの規約(#553) | -| 無進捗の許容 | 適用・修正・最終ゲートの修正の監視に渡す無進捗の許容と、雛形の進捗マーカー(#647 の無進捗の対策。既存の設計から引き継ぐ) | -| 手順書 | `SKILL.md` の語の表・「別の上限を置かない」の段落・骨組みの監視の引数、`docs/02-apply-and-review.md` / `docs/04-fix-and-report.md` の対応箇所 | - -含まない: - -| 扱わないもの | 理由 | -| --- | --- | -| `lib/monitor_outcome.py` / `lib/monitor.py` / `lib/launch-cli.sh` の変更 | G3(#729)が所有する。この変更は `read_launch_outcome` を呼ぶ側 | -| `lib/assignment.py` と参加者の決め方 | G1(#727)が所有する。この変更は `rounds.impl_for_seq` の中で呼ぶだけ | -| 利用上限で進行全体を止めること | 結末の可否を見て担当を替えるか、修正の上限へ進める(設計文書の決定 8・10) | -| 骨組みの bash を `scripts/` へ出すこと | #560 | -| 進行側が取り込みで `git commit --amend` によりトレーラーを足し直すこと | 設計文書の決定 13。SHA が変わり申告との対応が切れる。群に複数のコミットがあると先頭の書き換えが以降をすべて書き換える | -| `deferred_items` の 2 通りの形(`rounds.deferred_record` の固定鍵と、`apply.py:178` の提案の複製)を揃えること | この変更が足す見送りは `deferred_record` の形を使う。読む側(`plan.py`)は鍵が無くても落ちない | -| `cross-review` 側の取り込み | G3 が `state.py` の `read-result` を変える | -| `CHANGELOG.md` と版数 | 配布の工程が書く | -| クラス図 | 設計文書の「構造」に触る型(`LaunchOutcome` / `IntakeScope` / `ClosedAttempt`)だけを載せる | - -## 受け入れ条件(結末の読み取り) - -- [x] AC1: 結果ファイルが無い状態で結末の読み取り(`gitfacts.read_result`)を呼ぶと、例外(`SystemExit`)を出さず、標準出力・標準エラーに書かない。結果なしの値(`payload` が `None`、`reason` が `missing`)を返す -- [x] AC2: 監視の結果ファイル(`-apply-r-monitor.json`)に無進捗の理由(`reason: stalled`)があるとき、結末の読み取りの理由は `stalled`、起動し直しの可否は真である。利用上限の理由(`reason: usage_limit`)のとき、可否は偽である -- [x] AC3: 結末の読み取りに渡す結果ファイルの名前の幹(stem)は、監視の名前の雛形(`--stem-template`: `{agent}-apply-r$ROUND` / `{agent}-fix-r$ROUND` / `{agent}-final-fix`)を担当名で埋めた値と一致する。幹を組む関数(`paths.stem_for`)の 3 つの工程の値を、骨組みの雛形から作った値と突き合わせる - -## 受け入れ条件(共通の手順) - -- [x] AC4: 前提: 結果なしで、起点から HEAD までにコミットが 1 件以上ある - 操作: 3 つの取り込みのいずれかを呼ぶ - 結果: そのコミットは取り消され、起点(`apply_base_sha` と群の `base_sha` / `fix_base_sha` / `final_gate.fix_base_sha`)は取り消し後の HEAD になる -- [x] AC5: 結果なしのとき、3 つの取り込みのいずれでも、記録の辞書(群 / `final_gate`)の結末の記録(`failed_attempts`)に 1 件(`{phase, attempt, impl, reason, detail, at, reverted}`)が足される。理由(`reason`)は結末の読み取りの値、取り消した数(`reverted`)は取り消したコミットの数である -- [x] AC6: 結果なしで範囲にコミットが無いとき、`git revert` も `git push` も実行されない -- [x] AC7: 結果なしの取り込みを、同じ試行番号でもう一度呼ぶと、結果ファイルを読まずに前回と同じ終了コード 2 を返す。結末の記録の件数は増えない。その間に結果ファイルが現れても読まない -- [x] AC8: 3 つの取り込みで範囲を確定できないとき(起点が無い、または git が範囲を返さない)の終了コードは次のとおりである。適用の取り込みは 4、修正の取り込みは修正ラウンドを 1 進めて 2、最終ゲートの修正の取り込みは 2 - -## 受け入れ条件(#647: 適用ラウンド) - -- [x] AC9: 群が 2 つ(1 つ目の担当 agy、2 つ目の担当 codex)の状態で、1 つ目の結果ファイルを置かずに群を開く → 適用の取り込みを呼ぶ。終了コードは 2。1 つ目の群は未着手(`status: pending`)のまま担当が agy 以外に替わる。試行の番号(`attempt`)は 1、結末の記録は 1 件(`phase: apply`、`attempt: 1`、`impl: agy`)である -- [x] AC10: AC9 の後、替わった担当の結果ファイルも置かずにもう一度、群を開く → 適用の取り込みを呼ぶ。1 つ目の群は取り消し済み(`status: dropped`・`drop_reason: no_result`)、項目は `abandoned` になる。見送り(`deferred_items`)に `実装担当が結果を残しませんでした(agy: missing → codex: missing)` の形の理由で入る -- [x] AC11: 結果ファイルを 1 つも置かずに、群を開く操作が 1 を返すまで繰り返す。群を開く操作の呼び出しは 5 回(開く 4 回 + 尽きた 1 回)で終わり、両方の群が取り消し済み(`dropped`)になる -- [x] AC12: 結果ファイルが JSON として読めない場合と JSON の配列の場合も AC9 と同じ状態になり、結末の記録の理由(`failed_attempts[].reason`)は `unparsable` である -- [x] AC13: 群が 4 つ(輪番の通し番号 `apply_seq` が 4)あり、先頭の群(担当 codex)が結果を残さない。替えた後の担当は codex 以外である。輪番の通し番号は進めた分だけ進み、他の群の担当は変わらない -- [x] AC14: 監視の結果ファイルの理由が `usage_limit` で、輪番から担当を引く関数(`rounds.impl_for_seq`)の差し替えにより交代先が無い状態では、1 回目の失敗で群が取り消し済み(`dropped`、`drop_reason: no_result`)になる。理由が `missing` で交代先が無い状態では、同じ担当で 2 回目を開く -- [x] AC15: 群を開く操作を、適用の取り込みを挟まず 2 回呼ぶ(取り込みの前に進行が止まった再開)。群の試行の番号は 1 のまま進まない -- [x] AC16: 着手前のテストの状態が `green` でない状態で適用の取り込みを呼ぶと、結果ファイルを読まずに終了コード 4 で終わる -- [x] AC17: 適用の取り込みが終了コード 2 で終わった後の群は、取り消し済み(`dropped`)か、結末の記録を持つ未着手(`pending`)のどちらかである。確かめる経路は 4 つ(結果なし / 未割当のコミット / 適用の検証の失敗 / 取り込み済みで採用 0 件) -- [x] AC18: 開き直しの判定(`rounds.group_reopening`)を差し替えると、群を開く側の開き方(開く・再開・開かない)と、適用の取り込みの結果なしの後の扱い(担当の交代・取り消し)の両方が、差し替えた関数の返す値に従う - -## 受け入れ条件(#592: 採用 0 件と項目の無い群) - -- [x] AC19: テスト整備ラウンドで提案が 0 件の状態で提案の取り込み(`merge-proposals`)を呼んだ後、群を開く操作を呼ぶ。1 回目で終了コード 1 を返し、そのラウンドの群の配列(`apply_rounds`)は空のままである -- [x] AC20: 群の配列の鍵(`apply_rounds`)を持たない状態ファイル(群を導入する前の版)では、群を開く操作が従来どおりラウンド全体を 1 つの群として開く -- [x] AC21: 前提: rf587 で残った形の群(`status: applied`・`items: []`・`apply.merged_at` あり・`applied: []`) - 操作: 適用の取り込みを呼ぶ - 結果: 終了コード 2 で終わり、群が取り消し済み(`dropped`、`drop_reason: empty`)になる。続く群を開く操作は 1 を返す -- [x] AC22: 未着手で項目が無い群(`status: pending`・`items: []`)を持つ状態で、群を開く操作を呼ぶ。その群は開かれずに取り消し済み(`dropped`、`drop_reason: empty`)になる。次の群があればそれを開き、無ければ終了コード 1 を返す - -## 受け入れ条件(修正ラウンド) - -- [x] AC23: 群の担当が agy、提案ラウンドの担当が codex の状態で、agy の結果ファイル(`agy-fix-r1-result.json`)を置いて修正の取り込みを呼ぶ。agy の結果が取り込まれ、修正ラウンドの数(`fix_rounds`)が 1 になる -- [x] AC24: 修正の結果ファイルが無い状態で修正の取り込みを呼ぶと、終了コード 2 で終わり、修正ラウンドの数が 1 進む。群の結末の記録に `phase: fix` の 1 件が足される。上限(`--max-fix-rounds`)の回数だけ続けた後の見送りの判定(`should-abandon`)は終了コード 0 を返す -- [x] AC25: AC24 の直後に検証(`verify-round`)を挟まず修正の取り込みをもう一度呼んでも、修正ラウンドの数は進まない(AC7 の修正ラウンドの形) -- [x] AC26: 修正の結果なしで監視の理由が `usage_limit` のとき、修正の取り込みは修正ラウンドの数を上限の値にする。続く見送りの判定は終了コード 0 を返す -- [x] AC27: 修正の結果なしで起点から HEAD にコミットがあるとき、取り消され、起点(`fix_base_sha`)が取り消し後の HEAD になる(AC4 の修正ラウンドの形) - -## 受け入れ条件(#674: 最終ゲートの修正) - -- [x] AC28: 前提: 最終ゲートの修正の結果ファイルが無く、最終ゲートの起点(`final_gate.fix_base_sha`)から HEAD にコミットが 1 件ある - 操作: 最終ゲートの修正の取り込みを呼ぶ - 結果: 終了コード 2。そのコミットは取り消され、最終ゲートの起点は取り消し後の HEAD になる。最終ゲートの結末の記録(`final_gate.failed_attempts`)は 1 件(`phase: final-fix`) -- [x] AC29: AC28 の後に最終ゲートを呼ぶと、テストは取り消し後の HEAD で実行され、修正のコミットの一覧(`fix_commits`)に取り消したコミットは入らない -- [x] AC30: 最終ゲートの修正の結果なしで監視の理由が `usage_limit` のとき、最終ゲートの修正ラウンドの数(`final_gate.fix_rounds`)は上限の値になる。続く最終ゲートは、テストが落ちれば終了コード 1(取り消さず報告)で終わる -- [x] AC31: 結果ファイルがあり検証を通る最終ゲートの修正は、変更前と同じく取り込まれ、最終ゲートの結末の記録を持たない - -## 受け入れ条件(#553: 帰属行の後ろのトレーラー) - -一時リポジトリで実際にコミットを作って確かめる: - -- [x] AC32: 必須トレーラー 4 つの段落の後に、空行を挟んで `Co-Authored-By:` の段落が付いたコミットで、トレーラーの読み取りが 4 つとも値を返す -- [x] AC33: AC32 の段落の後に `Co-Authored-By:` と `Claude-Session:` の 2 行の段落が付いても、4 つとも返す -- [x] AC34: 必須トレーラーの段落と末尾の段落の間に散文の段落があるコミットで、散文より前にある `Round: …` の形の行を読まない -- [x] AC35: 末尾の段落に散文とトレーラーの形の行が混ざる(git がトレーラーの段落と判定しない)コミットで、その行を読まない -- [x] AC36: 同じ鍵が 2 つの段落にあるとき、末尾に近い段落の値を返す -- [x] AC37: AC32 の形のコミットを申告した適用ラウンドが、トレーラーの欠落で取り消されない -- [x] AC38: 本文がトレーラーの段落 1 つだけで、題名が `Round: 本文の題名` の形のコミットで、題名を読まない -- [x] AC39: 適用と修正の雛形(`prompts/apply.md` / `prompts/fix.md`)のコミットの規約が、必須トレーラーをメッセージの最後の段落に置くことを書く - -## 受け入れ条件(無進捗の打ち切り) - -- [x] AC40: 起動(`init`)の出力に無進捗の許容(`IMPL_STALL_TIMEOUT`)が入り、値がテストの制限時間(`--test-timeout`)の値 + 900 である(既定で 1800) -- [x] AC41: `SKILL.md` の骨組みで、適用・修正・最終ゲートの修正(`--phase apply` / `fix` / `final-fix`)の 3 つの監視(`monitor.py`)の呼び出しが `--stall-timeout "$IMPL_STALL_TIMEOUT"` を持ち、`--timeout` を持たない -- [x] AC42: 適用・修正・最終ゲートの修正の雛形(`prompts/apply.md` / `fix.md` / `final-fix.md`)が、作業段階ごとに進捗の記録(`$RF_STEM-progress.log`)へ 1 行追記する指示を持つ - -## 受け入れ条件(文書) - -- [x] AC43: `SKILL.md` の「この Skill で使う語」の適用ラウンドの行が、同じ群の試行の上限(2 回)を書く。`grep -n "別の上限を置かない\|別に置かない" SKILL.md` が何も出力しない -- [x] AC44: `docs/02-apply-and-review.md` の Step 4 と `docs/04-fix-and-report.md` の Step 6・Step 7 が 2 つを書く。結果なしのときの取り込みの振る舞い(取り消し・記録・終了コード)と、`SKILL.md` と同じ監視の引数である -- [x] AC45: `docs/02-apply-and-review.md` のトレーラーの節が、git の標準の読み方(`git log --format='%(trailers:…)'`)が最後の段落しか読まないことと、進行側の読み方の 2 つを書く - -## 受け入れ条件(退行しない) - -- [x] AC46: 結果ファイルがあり検証を通る適用ラウンドは、変更前と同じく 1 回目の試行で取り込まれ、結末の記録を持たない -- [x] AC47: `uv run --with pytest pytest scripts/tests plugins/ndf -q` が通る -- [x] AC48: 配布物の同期・定義・frontmatter の 3 つの検査が終了コード 0 で終わる(コマンドは「検証手段」の表) - -## 受け入れ条件(他の設計との契約) - -- [x] AC49: 輪番から担当を引く関数(`rounds.impl_for_seq`)を差し替えると、群を割り当てたときの担当・結果を残さなかった群の交代先・最終ゲートの修正担当の 3 つが、差し替えた関数の返す担当になる -- [x] AC50: 取り消しの本体は取り込みの共通手順の 1 つ(`intake.discard_unverified`)になる。`gitfacts.revert_unverified_range` は無くなり、`apply._revert_unverified_apply_round` は `discard_unverified` を呼ぶ - -## 非機能の条件 - -| 大項目 | 条件 | -| --- | --- | -| 性能・拡張性 | 中断と再開を挟まない実行で、1 つの提案ラウンドで適用担当を起動する回数が群の数 × 2 回を超えない。修正担当を起動する回数も群の数 × `--max-fix-rounds` 回を超えない。最終ゲートの修正担当の起動は `--max-fix-rounds` 回を超えない | -| 運用・保守性 | 群を取り消した理由(担当と結末の理由)が改修計画の「見送った項目」の表から読める。3 つの取り込みの結果なしの記録が同じ形(`failed_attempts[]`)で、実行の要約が同じ読み方で数えられる | - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| `gitfacts.read_result` | 引数と戻り値が変わる(結果なしを値で返す)。呼び出し元 3 か所とテスト(`test_git_facts.py` の 3 件)を書き直す | -| `gitfacts.revert_unverified_range` | 無くなる。呼び出し元 2 か所(`converge.py:383` / `gate.py:203`)は `intake.discard_unverified` へ | -| `merge-apply` の終了コード | 着手前テストの未確認と範囲の未確定が 2 から 4(中断)へ変わる。`SKILL.md` の終了コードの表は既に 4 と書いている | -| `merge-fix` / `merge-final-fix` の終了コード | 結果なしで 2(新)。骨組みは終了コードを見ないため変わらない | -| 状態ファイル | 群に `attempt` / `failed_attempts` / `drop_reason`、`final_gate` に `failed_attempts` が増える。既存の状態ファイルは鍵が無いまま読める(試行 0 回・失敗なし) | -| 群の担当 | 結果を残さなかった群だけ、2 回目の試行で次の輪番の担当へ替わる | -| 無進捗の打ち切り | 適用・修正・最終ゲートの修正で、どの担当も既定の許容より長くなる(既定 1800 秒) | -| トレーラーの読み取り | 最後の段落に加え、その直前に続くトレーラーの段落も読む | - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| テスト | `uv run --with pytest pytest scripts/tests plugins/ndf -q`(cross-refactoring だけなら `plugins/ndf/skills/cross-refactoring/tests`) | -| 配布物の同期 | `bash scripts/build-runtime-plugins.sh --check` | -| 定義の検査 | `claude plugin validate .` と `python3 scripts/check-skill-frontmatter.py` | -| 手動確認 | 次に cross-refactoring を回した実行で、進捗の記録(`-apply-r*-progress.log`)に作業段階が残るか。担当が結果を残さなかった群の結末の記録の理由が、監視の結果ファイルの理由と一致するか | - -## 前提とする取り決め - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | 状態の判定は `refactor_lib/` に置き、骨組みの bash は判定を持たない(`SKILL.md` の「実行」)。`commands/` どうしの取り込みを作らず、複数のコマンドが読む処理は `refactor_lib/` の直下(`rounds.py` / `intake.py`)に置く | -| コーディング規約 | 外部コマンド(`git interpret-trailers` / `git revert`)の挙動は書く前に実行して確かめる(`AGENTS.md` の DO) | -| テスト戦略 | 状態の遷移は既存の形(`tests/conftest.py` の `no_git` / `patch_lib` と `test_merge_apply.py` の `git_facts`)で関数を直接呼ぶ。トレーラーの読み取りは一時リポジトリで実際に git を実行する。共通層 `read_launch_outcome` はテストで差し替えず、一時ディレクトリに監視の結果ファイルと結果ファイルを置いて本物を通す | - -## 境界 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 既存テストの実行、配布物の同期の検査 | -| 確認してから行う | 同じ群の試行の上限を引数にすること(この変更では固定の 2 回) | -| 行わない | `monitor.py` / `monitor_outcome.py` / `assignment.py` の変更、骨組みを `scripts/` へ出すこと、進行側によるコミットの書き換え(`--amend`) | - -## 未決 - -| 項目 | 誰が決めるか | 期限 | -| --- | --- | --- | -| G1(#727)の `impl_assign` / `participants` と、この変更の `rounds.impl_for_seq` のどちらが先に `develop` へ入るか | 進行側(実装の持ち場の着手時点) | 実装の計画 | - -## 既存の受け入れ条件との対応 - -| 既存(PR #665) | この文書 | 変わったこと | -| --- | --- | --- | -| AC1〜AC3、AC5、AC7 | AC9〜AC11、AC13、AC15 | 記録の名前を `failed_attempts[]`(`phase` 付き)に揃えた | -| AC4 | AC12 | 理由 `unparsable` を明記 | -| AC6 | AC7 | 3 つの取り込みに広げた | -| AC8 | AC4、AC6 | 3 つの取り込みに広げた。コミットが無いときは push しない | -| AC9、AC10 | AC16、AC8 | 同じ | -| AC11 | AC17 | 同じ | -| AC12 | AC2、AC5 | `_monitor_reason` を `read_launch_outcome` の値に置き換えた | -| AC13〜AC16 | AC19〜AC22 | `drop_reason: empty` を明記 | -| AC17〜AC19 | AC23〜AC25 | 記録を `failed_attempts[]` に揃えた | -| AC20〜AC27 | AC32〜AC39 | 同じ | -| AC28〜AC30 | AC40〜AC42 | 同じ | -| AC31〜AC33 | AC43〜AC45 | AC44 に Step 7 と結果なしの記述を足した | -| AC34〜AC36 | AC46〜AC48 | 同じ | -| AC37 | AC49 | 同じ | -| — | AC1、AC3、AC14、AC18、AC26〜AC31、AC50 | 新設(結末の読み取り、起動し直しの可否、最終ゲート、開き直しの判定の一本化、取り消しの本体の一本化) | - -## 関連文書と前後関係 - -| 項目 | 内容 | -| --- | --- | -| 設計 | [issue-728-647-592-553-design.md](issue-728-647-592-553-design.md)。この文書は「何を満たすか」だけを扱う | -| 置き換える既存の要求 | [issue-647-592-553-requirements.md](issue-647-592-553-requirements.md)(PR #665)。**この文書が置き換える。** 対応は「既存の受け入れ条件との対応」にある | -| 親の名前で置く理由 | 親 #728 が根本原因の場所(結果の読み取りの向きと、3 つの取り込みの重複)を定め直したため、受け入れ条件を親の名前で改めて置く | -| 範囲に入れる子 issue | #674(最終ゲートの修正)は #728 の子として範囲に入れる。閉じるのは棚卸に任せる | diff --git a/issues/issue-729-619-584-design.md b/issues/issue-729-619-584-design.md deleted file mode 100644 index 137efdb84..000000000 --- a/issues/issue-729-619-584-design.md +++ /dev/null @@ -1,579 +0,0 @@ -# cross-review: 利用上限で止まった担当が「結果ファイル無し」と報告されて空振りの起動し直しで待たされ、止めた担当が後から結果を書く → 上限を理由に報告して同じラウンドで起動し直さず、止めた後は書かせない(設計 / #729 #619 #584) - -## 目的 - -**起きていること。** 担当の CLI が利用上限で落ちると、監視はその文言を読めない。結果なしの理由は「結果ファイル無し」に畳まれる。進行側は同じ担当を起動し直し、監視の上限 1 回分を待ってから全体を誤りで終える(#619)。監視が止めた担当の子プロセスが、止めた後に結果ファイルを書く(#584)。 - -**根本原因。** 結果なしの判断を cross-review と cross-refactoring がそれぞれ結果ファイルの有無だけで行い、監視が書いた理由を誰も読まない(#729)。 - -**この設計で成り立つこと。** 結末の語彙と起動し直しの可否を結末の共通層の 1 か所に置き、両 Skill がその値を読む。利用上限は理由「利用上限」として報告され、同じラウンドでは起動し直さない。止めた担当の子プロセスは結果を書かない。 - -## 用語の対応表 - -本文は左の業務用語で書く。識別子はこの表と、コードブロック・表・契約の節にだけ置く。 - -| 業務用語 | 識別子 | -| --- | --- | -| 結末の共通層 | `plugins/ndf/scripts/lib/monitor_outcome.py`(読み込み名 `monitor_outcome`) | -| 監視 | `plugins/ndf/scripts/lib/monitor.py` | -| 起動の手順 | `plugins/ndf/scripts/lib/launch-cli.sh` | -| 結末を読む関数 | `monitor_outcome.read_launch_outcome(tmp_dir, stem, result_path)` | -| 起動 1 回の結末(値) | `LaunchOutcome`。欄は使える結果 `payload`・理由 `reason`・起動し直しの可否 `relaunch_same_agent`・監視の詳細 `detail`・監視の結果 `monitor` | -| 可否を返す関数 | `monitor_outcome.relaunch_same_agent(reason)` | -| 起動し直せない理由の集合 | `monitor_outcome.NO_RELAUNCH_REASONS` | -| 理由の語彙 | `monitor_outcome.REASONS` | -| 状態からの既定の理由 | `monitor_outcome.reason_for(status)` | -| 監視の結末(型) | `MonitorOutcome`。新しい欄は `reason` | -| 監視の状態 | 正常 `OK` / 監視の上限 `TIMEOUT` / 無進捗 `STALLED` / 結果なし `NO_RESULT`(終了コード 3)/ 早期の致命 `EARLY_ERROR`(終了コード 4)/ pid ファイル不正 `PIDFILE_BAD` | -| 理由(監視が書く) | 正常 `ok` / 監視の上限 `timeout` / 無進捗 `stalled` / 致命の文言 `early_error` / 利用上限 `usage_limit` / CLI の上限 `cli_timeout` / 結果ファイル無し `missing` / pid ファイル不正 `pidfile_bad` | -| 理由(読む側が書く) | 読めない結果 `unparsable`。cross-review 固有は、判定の値が無い `no_verdict` / 未投稿 `not_posted` | -| 結果ファイル | `-result.json` | -| 監視の結果ファイル | `-monitor.json` | -| 監視の記録 | `monitor-outcomes.jsonl` | -| 結果の取り込み(cross-review) | `state.py read-result`。関数は `_read_review_result_file`、結果なしの記録は `_record_no_result` | -| 判定(cross-review) | `state.py judge`。結果なしのラウンドの扱いは `_handle_no_result_round` | -| 報告の表(cross-review) | `state.py report` | -| 結果なしの理由の鍵 | 状態ファイルの `rounds[-1].<担当>.no_result_reason` | -| 監視の詳細の鍵 | 状態ファイルの `rounds[-1].<担当>.monitor_detail` | -| 判定が出す理由の行 | 標準出力の `NO_RESULT_REASONS='<担当>=<理由> ...'` | -| 起動し直しの指示 | 判定の終了コード 7 と `RELAUNCH_AGENTS` | -| 誤りの終わり | 状態ファイルの `final=error`。判定の終了コード 1 | -| cross-refactoring の結果の読み取り | `refactor_lib/gitfacts.py` の `read_result`。進行を止める終了は `die` | -| 文言の照合(err.log) | 既存の `_scan_patterns`。表は `USAGE_LIMIT_FATAL` / `EARLY_ERROR_FATAL` / `CLI_TIMEOUT_AFTER_EXIT` | -| 文言の照合(claude の stdout.log) | 既存の `_scan_claude_stdout_fatal`。表は `CLAUDE_STDOUT_USAGE_LIMIT` | -| 結末を書く処理 | `monitor._record_outcome`。結末を作る場所は `_early_error_outcome` / `_process_exit_outcome` | -| プロセスの停止 | `monitor._kill_pid`。グループへは `os.killpg` | -| ジョブ制御 | bash の `set -m`。背景で起動した CLI の pid がプロセスグループの番号になる | -| 検知する文言 | 利用上限は kiro: `Monthly request limit reached`、claude: `"api_error_status":429`。CLI の上限は agy: `print timeout after <時間> with turn in progress` | -| 責務の移動 | issue の分類 `move_responsibility` | -| 実行の要約 | `run_metrics.py` が作る。起動の一覧は `launches[]` | - -## 機能一覧 - -5 つの機能のうち F1〜F3 は共通層、F4 は cross-review、F5 は起動と監視が担う。 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| F1 | 利用上限の文言を検知し、理由 `usage_limit` として残す | 監視。進行側と利用者が理由を読む | -| F2 | CLI 自身の上限で結果を書かずに終わった担当を、理由 `cli_timeout` として残す | 同上 | -| F3 | 結果ファイルの有無・読めるかと監視の結末を 1 つの値として読み、起動し直しの可否を添える | cross-review の取り込み(`read-result`)と cross-refactoring の取り込み(G4) | -| F4 | 起動し直しても解けない結末の担当を、同じラウンドで起動し直さずに止めて理由を報告する | cross-review の判定(`judge`) | -| F5 | 監視が止めた担当の子プロセスに、止めた後に結果を書かせない | 収束ループ | - -## 決定の記録 - -12 件。決定 1 は文書の置き方、2〜3 は共通層の契約、4〜9 は語彙と検知、10 はプロセスグループ、11〜12 は両 Skill の読み方である。 - -### 決定 1: 変わった決定だけを差分に載せるため、設計文書は親 #729 の名前で新設し、既存の設計文書の本体は書き換えない - -既存の設計は 6 課題・3 本の Pull Request を 1 つの文書で扱い、P1 と P2 は配布済みである。P3 の節をその場で書き換えると、配布済みの決定と新しい決定が 1 つの差分に混ざる。承認する人が「何が変わるのか」を読み分けられない。親 #729 は根本原因の場所(共通層)を定め直し、既存の決定 14(判定が理由「利用上限」を見る)の前提を変える。**新設して対応表で指せば、変わった決定だけが差分に載る。** - -既存の設計文書の本体(`-design.md`)には案内の 1 行も足さない。設計 Pull Request の本文の「決めたこと」は、変更したファイルの「決定の記録」の見出しをすべて写す。1 行でも触ると、既存の 20 件の決定がこの Pull Request の決定として並ぶ。案内は「決定の記録」を持たない要求の文書と契約の文書にだけ足す。 - -### 決定 2: 同じ判断を両 Skill に書かないため、結果なしの判断と起動し直しの可否を結末の共通層の 1 つの関数へ移す - -いまは cross-review の結果の取り込みと cross-refactoring の結果の読み取りが、それぞれ結果なしを決めている。判断の材料は結果ファイルの有無だけである。監視が書いた理由はどちらも読まない。理由を読む処理を各 Skill に書くと、同じ表(監視の理由 → 結果なしの理由)が 2 か所にできる。語彙を足すたびに片方が古くなる(親 #729 の責務の移動)。**結末の共通層に結末を読む関数を 1 つ置き、両 Skill はその値(使える結果・理由・起動し直しの可否)を受け取るだけにする。** 語彙・対応表・可否の表は結末の共通層だけが持つ。 - -各 Skill が監視の結果ファイルを読む既存の関数(`read_outcome`)を呼び、自分で表を引く形は採らない。既存の G4 の設計の `apply._monitor_reason` がこの形である。表が Skill の数だけ増える。 - -### 決定 3: 進行側の問いに合わせ、起動し直しの可否は「同じ担当を同じ条件で起動し直せば解けるか」の 1 つの真偽値にし、利用上限だけを偽にする - -進行側が結末を見て決めることは「同じ担当をもう 1 度起動してよいか」に尽きる。利用上限は起動し直しても解けない。起動のたびに待ちと相手の CLI の枠を使う(#619 の 2 回目の空振り、#647 の 3729 回)。それ以外の理由(監視の上限・無進捗・致命の文言・CLI の上限・結果なし・読めない)は、対象や負荷で変わりうる。1 度は起動し直してよい。 - -**偽のときに何をするかは Skill が決める。** cross-review は止めて理由を報告する(担当を外して回す判断は #478)。cross-refactoring は次の輪番の担当へ替える(G4 の設計の決定 3)。共通層が持つのは可否だけで、進行の分岐は持たない。 - -理由ごとに「止める / 替える / 起動し直す」の 3 値を返す形は採らない。「替える」は担当の集合を知る Skill にしか決められない。共通層に置くと担当の割り当てを読み込むことになる。 - -### 決定 4: 終了コードで分岐する骨組みを変えないため、利用上限は「早期の致命」の状態のまま、理由だけを「利用上限」にする - -監視の終了コードは骨組みと G4 の設計が分岐に使う(既存の設計の決定 16)。利用上限は「プロセスが続いても結果を生成できないと分かった」致命の一種である。状態としては早期の致命(`EARLY_ERROR`、終了コード 4)と同じである。区別が要るのは理由の側だけである。監視の結果ファイルと記録に理由「利用上限」(`usage_limit`)が入れば、読む側は状態を変えずに区別できる。 - -新しい状態(`USAGE_LIMIT`、終了コード 7)を足す形は採らない。終了コードの意味が変わる。終了コードで分岐する骨組みと文書(cross-refactoring だけで監視の呼び出しが 8 か所)を見直すことになる。 - -### 決定 5: 「終わったが結果が無い」の状態を保つため、CLI の上限は「結果なし」の状態のまま、理由だけを「CLI の上限」にする - -agy は自分の上限に当たると終了コード 0 で終わり、結果ファイルを書かない。監視から見れば「終わったが結果が無い」(`NO_RESULT`、終了コード 3)で正しい。理由だけを「CLI の上限」(`cli_timeout`)にする。文言は終了の後にだけ見る。生きている間に見ると、途中で出た警告を致命と読む。結果ファイルがあれば理由は「正常」にする(上限に当たっても結果を書き終えていれば使える)。 - -### 決定 6: 実物の文言に一致し引用を誤検知しないため、利用上限の文言は err.log を全担当で見、stdout.log は claude だけ JSON 向けの照合で見る - -kiro の利用上限の文言は err.log に出る(#619 の実物)。claude の文言はどちらに出るか未確認のため、両方を見る(前提 1)。err.log は既存の文言の照合で見る。この照合は表・引用・バッククォート・grep 形式を除外する。実測では、claude の文言を含む JSON の 1 行も err.log の側で一致し、引用の判定に飲み込まれなかった(「実測」)。stdout.log は既存の claude 向けの照合と同じ JSON 向けの形(除外を掛けない)で見る。JSON は 1 行に引用符を多く含む。行単位の引用の判定が「引用の内側」を真と判定するためである。 - -### 決定 7: 両 Skill が同じ形で見る理由だけを共通にするため、「読めない結果」を共通の語彙に入れ、「判定の値が無い」「未投稿」は cross-review 固有に残す - -結果ファイルが JSON として読めない・オブジェクトでないことは、両 Skill の読み取りが同じ形で見ている。共通の関数が結果ファイルを読む以上、この理由(`unparsable`)も共通の語彙に要る。「判定の値が無い」(`no_verdict`)と「未投稿」(`not_posted`)は結果ファイルの中身とレビューの投稿の話である。監視も cross-refactoring も知りえない。**cross-review が共通の関数の後で自分の理由を上書きする形にする。** - -理由の語彙は 9 語になる。状態からの既定の理由が返すのは、監視の状態から決まる 6 語のままである。「利用上限」と「CLI の上限」は監視が結末に理由を添えたときだけ現れる。「読めない結果」は読む側だけが使う。 - -### 決定 8: 状態からは決まらない理由を残すため、監視の結末に理由の欄を持たせ、無ければ状態からの既定を使う - -いま結末を書く処理は、状態からの既定の理由で理由を決めている。利用上限と CLI の上限は状態からは決まらない。結末を作る場所が理由を添える。監視の結末の型に理由の欄(`reason: Optional[str] = None`)を足す。結末を書く処理は、結末に理由があればそれを、無ければ状態からの既定を書く(`outcome.reason or reason_for(status)`)。結末を作る既存の呼び出し 8 か所(`create(status, detail)`)は変えない。 - -### 決定 9: 起動し直しで上書きされる 1 回目の理由を失わないため、その理由は追記だけの監視の記録が持つ - -状態ファイルの担当ごとの結果は、そのラウンドの最後の結果を持つ構造である。起動し直すと、1 回目の結果なしの理由は 2 回目で上書きされる。これは既存の構造のままにする。**過去を失わないのは追記だけの監視の記録の側で**(既存の設計の決定 2)、1 回目の理由も記録の理由と終了時刻(`reason` と `ended_at`)から読める。状態ファイルに履歴の配列を足す形は採らない。判定が読むのは最後の結果だけである。履歴は実行の要約の起動の一覧が既に持つ。 - -### 決定 10: 止めた後に子プロセスが結果を書かないよう、CLI を独立したプロセスグループで起動し、グループの先頭のときだけグループへシグナルを送る - -既存の設計の決定 17 をそのまま引き継ぐ。起動の手順がジョブ制御を有効にしてから背景で起動すると、CLI の pid がそのままプロセスグループの番号になる。監視は pid がグループの先頭であり、かつ監視自身のグループと違うときだけグループへ送る。それ以外は従来どおり pid だけへ送る。`setsid` は macOS に標準で入っていないため採らない。結果の取り込みの前に結果ファイルの出現を待つ形(#584 の候補 2)も採らない。書き出しそのものを止めれば待つ理由が無い。 - -### 決定 11: cross-refactoring も同じ値を読むよう、結果の読み取りは結果なしで進行を止めず、共通の関数の値で返す契約に置き換える。実装は G4 - -#728 は、cross-refactoring の結果の読み取りが進行を止める終了(`die(code=2)`)で終了コードを決める向きを直す。その向きを直した先が、結末を読む関数の値である。G3 が決めるのは次の範囲までである。結果の読み取りに相当する処理は結末を読む関数を呼び、使える結果・理由・起動し直しの可否を返す。終了コードを決めず、標準エラーに書かない。3 つの取り込みがその値をどう扱うか(群の状態・担当の交代・終了コード)は G4 が決める。薄い包みとして残すか呼び出し側が直接呼ぶかも G4 に任せる(要求の「未決」)。 - -### 決定 12: cross-review の骨組みの行を変えないため、結果の取り込みの終了コードと判定の 0 / 2 / 7 / 8 は変えず、利用上限は既存の 1 の枝で終える - -骨組み(`SKILL.md`)は結果の取り込みの終了コードを `|| true` で受け、判定の 7 / 8 / 0 / 2 で分岐し、それ以外を `exit` する。利用上限で起動し直さずに止める結末は、既存の「2 度目も結果なし → 誤りの終わり → 1」と同じ出口へ載せる。骨組みの行は 1 つも変わらない(AC24)。 - -新しい終了コードで「利用上限で止めた」を骨組みへ伝える形は採らない。骨組みの分岐が増える。cross-review の `SKILL.md` と `docs/` の 2 か所へ同じ値を書くことになる。理由は判定が出す理由の行と報告の表で読める。 - -## 実測 - -2026-09-19 に `develop`(9eaebe14、bash 5.3.9、Python 3.14.4)で、提案する文言を既存の文言の照合に通した。 - -```text -kiro 実物 usage=HIT cli_timeout=- 現行fatal=- -claude 429 JSON 1 行 usage=HIT cli_timeout=- 現行fatal=- -claude 429 空白あり usage=HIT cli_timeout=- 現行fatal=- -HTTP 429 行 usage=HIT cli_timeout=- 現行fatal=HIT -HTTP 401 行 usage=- cli_timeout=- 現行fatal=HIT -表の中 usage=- cli_timeout=- 現行fatal=- -バッククォート usage=- cli_timeout=- 現行fatal=- -引用行 usage=- cli_timeout=- 現行fatal=- -agy print timeout usage=- cli_timeout=HIT 現行fatal=- -grep 形式 usage=- cli_timeout=- 現行fatal=- -stdout JSON 向け照合: True -``` - -- 利用上限の 4 つの文言は、実物の 3 形式(kiro の 1 行・claude の JSON 1 行・HTTP 429 行)に一致する。表・バッククォート・引用・grep 形式では一致しない -- HTTP の 401 の行は利用上限に入らず、現行の致命の文言(`early_error`)に残る(AC4) -- claude の JSON の 1 行は、err.log 向けの引用の判定を通しても一致した(決定 6 の前提) -- 現行の致命の照合(`_scan_early_fatal`)は kiro の実物と claude の JSON に一致しない(#619 の再現) - -プロセスグループの実測は既存の契約の文書の「実測」(2026-09-15)にあり、変更していない。ジョブ制御を有効にして起動した CLI をグループへのシグナルで止めると、3 秒後に子が書く結果ファイルは書かれなかった。 - -## 構成要素 - -変えるのは共通層の 3 ファイルと cross-review の 4 ファイルで、cross-refactoring は契約だけを受け取る。 - -| 要素 | 新設 / 変更 | 責務 | -| --- | --- | --- | -| 結末の語彙と読み取り(`lib/monitor_outcome.py`) | 変更 | 理由 9 語、起動し直しの可否の表、結末を 1 つの値として読む `read_launch_outcome`(決定 2・3・7) | -| 監視(`lib/monitor.py`) | 変更 | 利用上限と CLI の上限の文言の検知、理由を持つ結末(決定 4・5・6・8)、グループへの停止(決定 10) | -| 起動(`lib/launch-cli.sh`) | 変更 | `set -m` で CLI を独立したプロセスグループにする(決定 10) | -| cross-review の状態(`cross-review/scripts/state.py`) | 変更 | `read-result` が共通の値を読んで理由と `monitor_detail` を記録する。`judge` が理由を出し、可否が偽なら止める。`report` が理由を表に出す(決定 12) | -| cross-review の文書(`docs/01-state-and-review.md` / `docs/03-review-output.md` / `docs/04-contracts.md`) | 変更 | 理由の表、上限の見分け方、状態ファイルの鍵 | -| 共通層の一覧(`lib/README.md`) | 変更 | `monitor_outcome.py` の行に読み取りの責務を足す | -| cross-refactoring の取り込み(`refactor_lib/gitfacts.py` ほか) | **契約のみ** | `read_result` に相当する読み取りが `read_launch_outcome` を呼ぶ(決定 11)。実装は G4 | -| テスト(`scripts/tests/` / `cross-review/tests/`) | 変更 | 「テスト設計」 | - -```mermaid -graph TD - subgraph 起動と監視 - L[起動 launch-cli.sh
独立したプロセスグループ] - M[監視 monitor.py
文言の検知・理由を持つ結末・グループへの停止] - end - subgraph 共通層の結末 - MO[結末の語彙と読み取り monitor_outcome.py
理由 9 語・可否の表・read_launch_outcome] - end - subgraph 一時ディレクトリ - F1[結果ファイル stem-result.json] - F2[監視の結果ファイル stem-monitor.json] - F3[監視の記録 monitor-outcomes.jsonl] - S[状態ファイル] - end - subgraph cross-review - RR[read-result] - J[judge] - RP[report] - end - subgraph CR["cross-refactoring(G4 が実装)"] - GF[取り込み merge-apply / merge-fix / merge-final-fix] - end - L -->|pgid = pid| M - M -->|reason| MO - MO --> F2 - MO --> F3 - RR -->|payload / reason / 可否| MO - GF -.->|payload / reason / 可否| MO - MO -->|読む| F1 - MO -->|読む| F2 - RR -->|no_result_reason / monitor_detail| S - J -->|理由を読む・可否で止める| S - RP -->|理由を表に出す| S -``` - -**図に含めない要素**は次の 2 つである。 - -| 要素 | 図との関係 | -| --- | --- | -| cross-review の文書と共通層の一覧 | 手順と表の記述で、呼び出しの辺を持たない | -| テスト | 実行時の依存ではない | - -## 文脈と配置 - -利用上限を返すのは各 CLI の API の提供元で、こちらは変えられない。**この変更が変えるのは、その返答が err.log / stdout.log に現れたときの読み方だけである。** - -```mermaid -graph LR - H[進行側のホスト CLI] --> SK[cross-review / cross-refactoring の骨組み] - SK --> CLI[担当の CLI: codex / agy / kiro / claude] - CLI --> API[各 CLI の API の提供元
利用上限はここが返す] - SK --> GH[GitHub] - SK --> TMP[作業ツリーの中の一時ディレクトリ] -``` - -| 実行の単位 | どこで動くか | 境界 | -| --- | --- | --- | -| 担当の CLI | 背景のプロセス。**独立したプロセスグループ**(pgid = pid) | 一時ディレクトリへ結果ファイルとログを書く | -| 監視 | 進行側のシェルから起動する Python(cross-review は `bg-wait.sh` の背景) | 一時ディレクトリを読み、監視の結果ファイルと記録を書き、CLI のグループへシグナルを送る | -| 状態の操作 | 進行側が 1 コマンドずつ呼ぶ Python | 一時ディレクトリの結果ファイル・監視の結果ファイル・状態ファイルを読み書きする | - -配置で変わるのは担当の CLI のプロセスグループだけである。 - -## 置き場所 - -変えるファイルと、それぞれで変わるものを示す。 - -```text -plugins/ndf/scripts/lib/ - monitor_outcome.py 変更(語彙 9 語・可否の表・read_launch_outcome・LaunchOutcome) - monitor.py 変更(USAGE_LIMIT_FATAL / CLI_TIMEOUT_AFTER_EXIT / MonitorOutcome.reason / _kill_pid のグループ) - launch-cli.sh 変更(set -m) - README.md 変更(monitor_outcome.py の行) -plugins/ndf/scripts/tests/ - test_monitor_outcome_unit.py 変更(語彙と read_launch_outcome の単体) -plugins/ndf/skills/cross-review/ - scripts/state.py 変更(_read_review_result_file / _record_no_result / _handle_no_result_round / report) - docs/01-state-and-review.md 変更(理由の表) - docs/03-review-output.md 変更(上限の見分け方) - docs/04-contracts.md 変更(monitor_detail) - tests/ 変更(文言・グループ・judge・report) -plugins/ndf/skills/cross-refactoring/scripts/refactor_lib/gitfacts.py 契約のみ(G4 が変える) -# dev.kiro / dev.agy の配布物は bash scripts/build-runtime-plugins.sh で同期する -``` - -## 構造 - -変更が触る型だけを載せる。 - -```mermaid -classDiagram - class MonitorOutcome { - status: str - exit_code: int - icon: str - detail: str - +reason: Optional~str~ - create(status, detail, reason=None) MonitorOutcome - } - class LaunchOutcome { - payload: Optional~dict~ - reason: Optional~str~ - detail: str - monitor: Optional~dict~ - relaunch_same_agent: bool - } - class monitor_outcome { - REASONS: tuple - NO_RELAUNCH_REASONS: frozenset - reason_for(status) str - relaunch_same_agent(reason) bool - read_launch_outcome(tmp_dir, stem, result_path) LaunchOutcome - } - class AgentStatus - class state_py - class gitfacts_read_result - AgentStatus --> MonitorOutcome : outcome - monitor_outcome ..> LaunchOutcome : 作る - state_py ..> monitor_outcome : read_launch_outcome を呼ぶ - gitfacts_read_result ..> monitor_outcome : 契約(G4 が実装) -``` - -`+` の付いた欄が増える。担当の状態の型(`AgentStatus`)と cross-review の状態の操作の既存の欄・関数は変えない。 - -| 触る型 | 責務 | -| --- | --- | -| `MonitorOutcome.reason` | 状態からは決まらない理由(`usage_limit` / `cli_timeout`)を結末に添える。`None` なら `reason_for(status)` | -| `LaunchOutcome` | 起動 1 回の結末。`payload` があれば使える結果、無ければ `reason` が理由。`relaunch_same_agent` は `reason` から導く(`payload` があれば `True`) | -| `monitor_outcome.NO_RELAUNCH_REASONS` | 起動し直しても解けない理由の集合。値は `{"usage_limit"}` | - -## 理由の語彙 - -共通層の理由の語彙は 9 語である。監視が書く語と読む側だけが書く語を、起動し直しの可否と合わせて 1 つの表で持つ。 - -| 理由 | 監視の状態 | 誰が書くか | 起動し直しの可否 | 何が起きたか | -| --- | --- | --- | --- | --- | -| `ok` | `OK` | 監視 | — | 結果ファイルがあって終わった | -| `timeout` | `TIMEOUT` | 監視 | 可 | 監視の上限 | -| `stalled` | `STALLED` | 監視 | 可 | 無進捗の許容 | -| `early_error` | `EARLY_ERROR` | 監視 | 可 | 利用上限以外の致命の文言 | -| `usage_limit` | `EARLY_ERROR` | 監視(決定 8) | **否** | 利用上限の文言 | -| `cli_timeout` | `NO_RESULT` | 監視(決定 8) | 可 | 結果なしで終わり、err.log に CLI の上限の文言 | -| `missing` | `NO_RESULT` | 監視・読む側 | 可 | 結果なしで終わり、理由の文言が無い | -| `pidfile_bad` | `PIDFILE_BAD` | 監視 | 可 | pid ファイルが無い・別のプロセス | -| `unparsable` | — | 読む側だけ | 可 | 結果ファイルがあるが JSON オブジェクトとして読めない | - -## データ構造 - -永続データは JSON のファイルで、データベースは無い。ER 図は作らず、表で持つ。 - -### 監視の結果ファイルと監視の記録(P1 から。理由の値だけが増える) - -| 列 | 型 | 空を許すか | 意味 | -| --- | --- | --- | --- | -| `reason` | 文字列 | 許さない | 監視が書く理由。`ok` / `timeout` / `stalled` / `early_error` / `usage_limit` / `cli_timeout` / `missing` / `pidfile_bad` の 8 語(`unparsable` は監視が書かない) | - -他の 14 個のキーは変えない(既存の契約の文書の「監視の結果ファイル」)。 - -### 状態ファイルの担当ごとの最後の結果(cross-review) - -| 列 | 型 | 空を許すか | 意味 | -| --- | --- | --- | --- | -| `intent` | 文字列 | 許さない | `NO_RESULT` のとき下の 2 列が意味を持つ(既存) | -| `no_result_reason` | 文字列 | 許さない(`NO_RESULT` のとき) | `read_launch_outcome` の `reason`(`ok` を除く 8 語)、または cross-review が上書きする `no_verdict` / `not_posted` | -| `monitor_detail` | 文字列 | 許す | 監視の `detail`(最大 200 文字の err.log の抜粋)。**鍵が無い** = 監視の結果ファイルが無かった。空文字は書かない | - -結果なしの理由の値の集合は 10 語になる。既存の 3 語(結果ファイル無し・読めない結果・判定の値が無い)と未投稿の意味は変えない。 - -### 機能とデータの対応 - -| 機能 | 監視の結果ファイル | 監視の記録 | 結果ファイル | 状態ファイル | -| --- | --- | --- | --- | --- | -| F1 / F2 検知して理由を残す | C | C | — | — | -| F3 結末を 1 つの値として読む | R | — | R | — | -| F4 止めて理由を報告する(cross-review) | — | — | — | R / U | -| F5 止めた後に書かせない | — | — | (書かせない) | — | - -時系列の扱いは決定 9 のとおり。状態ファイルは上書き(最後の結果だけ)、監視の記録は追記だけで過去を持つ。移行は無い(鍵の追加と値の追加だけで、既存の状態ファイルはそのまま読める)。 - -## 入出力の契約 - -共通層の 2 つの関数と、監視・起動の約束を表で持つ。この節の識別子は「用語の対応表」の右の列である。 - -### 結末を読む関数(新設。両 Skill の取り込みが呼ぶ) - -| 項目 | 内容 | -| --- | --- | -| 名前 | `read_launch_outcome(tmp_dir, stem, result_path=None) -> LaunchOutcome` | -| 入力 | `tmp_dir`: 一時ディレクトリ。`stem`: 監視と同じ stem(`-review-pr` / `-apply-r` など)。`result_path`: 結果ファイルのパス。省くと `/-result.json` | -| 出力(使える結果) | `payload` に JSON オブジェクト、`reason` は `None`、`relaunch_same_agent` は `True`、`monitor` に監視の結果ファイルの辞書(無ければ `None`)、`detail` に監視の `detail`(無ければ空文字) | -| 出力(結果なし) | `payload` は `None`、`reason` は下の表、`relaunch_same_agent` は `reason not in NO_RELAUNCH_REASONS`、`detail` は監視の `detail`(無ければ読めなかった理由の 1 文) | -| 失敗の形 | **失敗しない。** 例外を投げず、`SystemExit` も出さず、標準出力・標準エラーに書かない。監視の結果ファイルが壊れていれば無いものとして扱う | -| 互換性 | 新設。既存の `read_outcome` / `reason_for` / `REASONS` の呼び出し側は変わらない(`REASONS` は 9 語になるが、一覧を持つ読み手は無い) | - -結果なしの理由の決め方(要求の AC9): - -| 監視の結果ファイルの `reason` | 結果ファイル | `reason` | -| --- | --- | --- | -| `timeout` / `stalled` / `early_error` / `usage_limit` / `cli_timeout` / `pidfile_bad` | 問わない | その値(**監視が止めたか、結果を書けない終わり方をしたことが分かっている**) | -| `ok` / `missing` / ファイルが無い・読めない | 無い、または空 | `missing` | -| `ok` / `missing` / ファイルが無い・読めない | あるが JSON オブジェクトでない | `unparsable` | - -**結果ファイルが読めれば使える結果が勝つ。** 監視が利用上限で止めた後にも結果ファイルが残っていれば、それは止める前に書き終えていた結果で、使ってよい。 - -### 可否を返す関数(新設) - -`relaunch_same_agent(reason) -> bool` は `reason not in NO_RELAUNCH_REASONS` を返す。`NO_RELAUNCH_REASONS = frozenset({"usage_limit"})`。理由を足すときはこの集合だけを見直す。 - -### 監視(変更。終了コードと標準出力は変えない) - -| 引数・出力 | 約束 | -| --- | --- | -| 終了コード 0〜6 | 変えない。`usage_limit` は 4、`cli_timeout` は 3 | -| 標準出力の 13 個のキー | 変えない | -| 監視の結果ファイルと記録の `reason` | `usage_limit` / `cli_timeout` が増える | -| `--no-early-error` | 変えない。利用上限の検知も致命の検知と一緒に無効になる(`MONITOR_NO_EARLY_ERROR=1` の逃げ道はそのまま) | - -検知の文言(err.log は既存の照合の除外を掛ける。claude の stdout.log は JSON 向けの照合で除外を掛けない): - -| 表 | 理由 | 見るファイル | いつ見るか | 文言(正規表現) | -| --- | --- | --- | --- | --- | -| `USAGE_LIMIT_FATAL` | `usage_limit` | err.log(全担当) | 生きている間の巡回ごと | `Monthly request limit reached` | -| `USAGE_LIMIT_FATAL` | `usage_limit` | err.log(全担当) | 同上 | `"api_error_status"\s*:\s*429` | -| `USAGE_LIMIT_FATAL` | `usage_limit` | err.log(既存の一致を付け替え) | 同上 | `\b(?:quota exceeded\|rate limit exceeded)\b`(大文字小文字を問わない)/ `^HTTP/\d\S* 429 ` | -| `CLAUDE_STDOUT_USAGE_LIMIT` | `usage_limit` | claude の stdout.log | 同上 | `"api_error_status"\s*:\s*429` | -| `EARLY_ERROR_FATAL` | `early_error` | err.log(既存の一致を分ける) | 同上 | `^HTTP/\d\S* (?:401\|403) ` と、残りの既存の致命 | -| `CLI_TIMEOUT_AFTER_EXIT` | `cli_timeout` | err.log | **終了した後、結果ファイルが無いときだけ** | `print timeout after \S+ with turn in progress` | - -照合の順序は利用上限 → 致命 → 警告の見た目の致命。同じ err.log に利用上限と他の致命が両方あれば、理由は「利用上限」になる。上限で落ちた後に別の文言が続く形が普通で、上限のほうが原因である。 - -### 起動の手順と監視の停止(変更。既存の設計の決定 17) - -| 項目 | 約束 | -| --- | --- | -| 起動 | `set -m` を有効にして背景起動する。CLI の pid = pgid | -| 停止 | `os.getpgid(pid) == pid` かつ `!= os.getpgrp()` なら `os.killpg`(SIGTERM → 3 秒 → SIGKILL)。それ以外は従来どおり `os.kill` | -| 互換性 | 呼び出し側の引数は変わらない。pid ファイルの中身も変わらない | - -## 両 Skill が従う契約 - -cross-review は状態の操作を変更し、cross-refactoring は結果の読み取りの契約だけを受け取る。 - -### cross-review の状態の操作(変更) - -| コマンド | 変わる出力 | 変わらないもの | -| --- | --- | --- | -| `read-result` | `rounds[-1].<担当>.no_result_reason` に `read_launch_outcome` の `reason`(`no_verdict` / `not_posted` は従来どおり後で上書き)。監視の結果ファイルがあれば `monitor_detail` | 終了コード(`unparsable` は 3、それ以外は 1、使える結果は 0) | -| `judge` | 結果なしがあるとき標準出力に `NO_RESULT_REASONS='<担当>=<理由> ...'`。可否が偽の理由を含めば `final=error` と終了コード 1、標準エラーに担当・理由・`monitor_detail` | 終了コード 0 / 2 / 7 / 8 の意味と `RELAUNCH_AGENTS` の形、8 の `flush` の枝 | -| `report` | ラウンド表で `<担当>=NO_RESULT(<理由>)` | 他の行 | - -### cross-refactoring の結果の読み取り(契約のみ。G4 が実装する) - -| 項目 | 契約 | -| --- | --- | -| 読み取り | `read_launch_outcome(state["tmp_dir"], stem_for(...), result_path(...))` を呼ぶ。自前で結果ファイルを開かない | -| 返すもの | `LaunchOutcome`(`payload` / `reason` / `relaunch_same_agent` / `detail` / `monitor`) | -| 失敗の形 | **`die` しない。** 終了コードを決めるのは 3 つの取り込み(`merge-apply` / `merge-fix` / `merge-final-fix`) | -| 群の記録 | `failed_attempts[].reason` には `LaunchOutcome.reason` をそのまま写す(G4 の設計の `_monitor_reason` はこの値で置き換わる) | -| 担当の交代 | `relaunch_same_agent` が偽なら同じ担当で試行を重ねない。替えるか止めるかは G4 の決定 3 | - -## 処理の流れ - -監視の 1 担当、結末の読み取り、cross-review の 1 ラウンドの 3 つを図にする。 - -### 監視の 1 担当(利用上限と CLI の上限の枝が入る) - -```mermaid -graph TD - A[pid ファイルを待つ] -->|無い| PB[PIDFILE_BAD / pidfile_bad] - A --> P[巡回] - P -->|経過 ≥ 監視の上限| T[TIMEOUT / timeout] - P -->|利用上限の文言
err.log 全担当・stdout claude| U[EARLY_ERROR / usage_limit] - P -->|その他の致命の文言| E[EARLY_ERROR / early_error] - P -->|終了して結果あり| OK[OK / ok] - P -->|終了して結果なし
err.log に CLI の上限の文言| CT[NO_RESULT / cli_timeout] - P -->|終了して結果なし| NR[NO_RESULT / missing] - P -->|無進捗 ≥ 許容| SL[STALLED / stalled] - T --> K[止める: pgid = pid ならグループへ] - U --> K - E --> K - SL --> K - K --> W[結果ファイルと記録を書く
reason = outcome.reason or reason_for] - OK --> W - CT --> W - NR --> W - PB --> W - W --> O[標準出力の JSON と終了コード(変えない)] -``` - -### 結末を読む関数の流れ - -```mermaid -graph TD - S[結果ファイルを読む] -->|JSON オブジェクト| P[payload / reason None / 可 True] - S -->|無い・空・読めない| M[監視の結果ファイルを読む] - M -->|reason が timeout / stalled / early_error
usage_limit / cli_timeout / pidfile_bad| R1[reason = その値] - M -->|ok / missing / 無い・壊れている| R2{結果ファイルは} - R2 -->|無い・空| R3[reason = missing] - R2 -->|あるが読めない| R4[reason = unparsable] - R1 --> V[可否 = reason not in NO_RELAUNCH_REASONS] - R3 --> V - R4 --> V -``` - -### cross-review の 1 ラウンド(起動し直しの判断) - -```mermaid -sequenceDiagram - participant H as 進行側(骨組み、変えない) - participant M as monitor.py - participant MO as monitor_outcome - participant S as state.py - H->>M: 監視(担当ごと) - M->>MO: 結末(reason 付き)を結果ファイルと記録へ - H->>S: read-result(担当ごと) - S->>MO: read_launch_outcome(tmp_dir, stem) - MO-->>S: payload / reason / 可否 / detail - alt payload あり - S->>S: 取り込み(no_verdict / not_posted は従来どおりここで判定) - else 結果なし - S->>S: rounds[-1].<担当> に NO_RESULT / no_result_reason / monitor_detail - S-->>H: 終了コード 1(unparsable は 3) - end - H->>S: judge - S->>S: 結果なしの担当の理由を集める - S-->>H: NO_RESULT_REASONS='kiro=usage_limit' - alt 可否が偽の理由がある - S->>S: final=error - S-->>H: 終了コード 1(担当・理由・monitor_detail を標準エラーへ) - else すべて可(1 度目) - S-->>H: 終了コード 7 と RELAUNCH_AGENTS(変えない) - else すべて可(2 度目) - S-->>H: 終了コード 1(変えない) - end -``` - -## 非機能の実現方式 - -要求の非機能の条件 3 件に、それぞれ実現方式を対応させる。 - -| 条件 | 実現方式 | -| --- | --- | -| 理由が 4 か所で同じ語彙で読める | 監視が結末に理由を添え、`_record_outcome` が結果ファイルと記録へ同じ値を書く。`read-result` は `read_launch_outcome` の `reason` をそのまま `no_result_reason` に写す。要約の `launches[]` は記録の行から作る(P1 のまま)。語彙を足すときは `REASONS` と `NO_RELAUNCH_REASONS` と検知の表だけを変える | -| `monitor_detail` が要約に入らない | `run_metrics.py` は状態ファイルの `rounds[-1].<担当>` の鍵を要約へ写さず、記録の行から `detail` を除いて `launches[]` を作る(P1 の決定 7)。この変更は要約の側を触らない | -| macOS でもグループで止める | 外部コマンドに頼らない `set -m` を使う。bash 3.2 は未確認(「未確認のまま残ること」)。成り立たなければ pid だけの停止に落ちる(`os.getpgid(pid) != pid` の枝) | - -## テスト設計 - -受け入れ条件 24 件を、既存のテストの形(偽の CLI を実プロセスで動かす・一時ディレクトリに結果を置く・状態ファイルを作って呼ぶ)で確かめる。 - -| 受け入れ条件 | 何で確かめるか | -| --- | --- | -| AC1 | `REASONS` の 9 語と、6 つの状態の `reason_for` の返り値を固定する(`plugins/ndf/scripts/tests/test_monitor_outcome_unit.py`) | -| AC2〜AC5 | 各文言を 1 行書いた err.log / stdout.log と、終わったプロセスの pid ファイルで `monitor_agent` を呼び、状態・終了コード・結果ファイルの `reason`(`plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py` 新設)。AC5 は結果ファイルあり・なしの 2 通り | -| AC6 | 「実測」の 10 行をそのまま入力にし、表・引用・バッククォート・grep 形式で一致しないこと。claude の stdout.log の JSON で一致すること(`plugins/ndf/skills/cross-review/tests/test_monitor_early_error.py` の形に倣う) | -| AC7 | 既存の `plugins/ndf/skills/cross-review/tests/test_monitor_outcome_file.py` の標準出力のキーの検査を変更せずに通す。記録の行の `reason` を読む | -| AC8〜AC11 | 一時ディレクトリに監視の結果ファイル(各理由・壊れた JSON・無し)と結果ファイル(オブジェクト・配列・壊れた JSON・空・無し)を組み合わせて置き、`read_launch_outcome` の 5 つの欄。`capsys` で標準出力・標準エラーが空(`plugins/ndf/scripts/tests/test_monitor_outcome_unit.py`) | -| AC12 | `git grep -n 'usage_limit' -- plugins/ndf/skills` の一致行が、文書とテストとテンプレートの文言だけである(テスト化せず、レビューの手順) | -| AC13 | 結果ファイル無し + 監視の結果ファイル(各理由)/ 無しで `read-result` を呼び、状態ファイルの `no_result_reason` と `monitor_detail` と終了コード(`plugins/ndf/skills/cross-review/tests/test_read_result_reason.py` 新設) | -| AC14〜AC17 | 状態ファイルを作って `judge` と `report` を呼ぶ。`usage_limit` を含む / 含まない / 2 度目の 3 通り(`plugins/ndf/skills/cross-review/tests/test_judge_no_result_reason.py` 新設) | -| AC18 | 文書の `grep`(10 語と「見分け方」の節) | -| AC19〜AC21 | 3 秒後に子が書く偽の CLI を `launch-cli.sh` で起動し、`os.getpgid` と、監視の上限 2 秒で止めた後の結果ファイルの有無。先頭でない pid では `os.killpg` が呼ばれないことを `mock` で見る(`plugins/ndf/skills/cross-review/tests/test_launch_cli_process_group.py` 新設) | -| AC22〜AC23 | 検証手段の表のコマンド | -| AC24 | `SKILL.md` の骨組みの該当行(`bg-wait.sh run` から `case $JUDGE_RC` まで)を変更前と `diff` して差が無い | - -## 未確認のまま残ること - -6 件。実装で決めたものが 4 件(1・3・5・6。決めた結果を「決める時点」の列に残す)、確かめないまま進めるものが 2 件(2・4。どちらでも設計が塞ぐ)である。 - -| # | 項目 | 内容 | 決める時点 | -| --- | --- | --- | --- | -| 1 | claude の 429 の出る先 | `"api_error_status":429` が err.log と stdout.log のどちらに出るか。実物のログが手元に無い。両方を見るためどちらでも拾える | 実装で偽の claude を err.log と stdout.log の両方の形で試し、どちらでも `EARLY_ERROR` / `usage_limit` になることをテストで固定した(`test_monitor_usage_limit.py` の `test_usage_limit_stops_the_agent_as_early_error_with_reason_usage_limit`)。実物の出る先は次に上限に当たったときの記録で確かめる | -| 2 | kiro の利用上限のときの終わり方 | プロセスが直ちに終わるのか、待ち続けるのか。#619 の実物では `err.log` に 1 行出て `NO_RESULT` になった(直ちに終わったと読める) | どちらでも監視は巡回で文言を拾い `usage_limit` にする。実装で確かめない | -| 3 | macOS の bash 3.2 の `set -m` | Linux の bash 5.3 だけで確かめた | 一次資料で確かめた。GNU の配布物 bash-3.2 の `doc/bash.1`(2006-09-28)の `set` の `-m` の項に「Background processes run in a separate process group」とあり、`CHANGES` の bash-2.01 の節に `set -m` の修正の記載がある(2.01 の時点で存在する)。手元の bash 5.3.9 では非対話・tty 無しで pid = pgid になり、標準エラーへジョブ制御の通知は出ない。macOS の実機では未確認のまま。成り立たなければ `_leads_own_group` が偽になり pid だけの停止に落ちる(構造は同じ) | -| 4 | agy が子プロセスで結果を書くか | #584 の事例が止めた後の書き出しだったかは確かめられていない | 確かめないまま進める(グループで止めればどちらでも塞がる) | -| 5 | 利用上限と他の致命が同じ err.log に並ぶ順序 | 上限の後に別の致命が続く形を想定して利用上限を先に見る。逆の順で並ぶ実物は未確認 | 照合の順序を利用上限 → 致命 → 警告の見た目の致命に固定し、err.log の並びがどちらの順でも理由が `usage_limit` になることをテストで固定した(`test_usage_limit_wins_over_other_fatal_lines_in_either_order`)。逆の順で並ぶ実物が出ても結果は変わらない | -| 6 | `read_launch_outcome` に渡す stem の組み立て | cross-review は `-review-pr`、cross-refactoring は `stem_for` の値。監視の `--stem-template` と食い違うと監視の結果ファイルを引けない | cross-review の 3 か所(`launch-reviewer.sh` の `STEM=` の行・監視の `DEFAULT_STEM_TEMPLATE`・取り込みが渡す stem)が同じ形であることを 1 つのテストで固定した(`test_read_result_reason.py` の `test_the_three_stems_have_the_same_shape`)。cross-refactoring の `stem_for` との突き合わせは G4 が `read_result` を置き換えるときに同じ形のテストを置く | - -## 申し送り(並行する設計との境界) - -同時に進む G4 / G5 / D-B / G1 と、どこまでをこの設計が持つかを決めた。 - -| 相手 | 決めた契約 | どちらが何をするか | -| --- | --- | --- | -| G4(#728 #647 #592 #553) | `gitfacts.read_result` の契約(「両 Skill が従う契約」)。理由の語彙 9 語と `relaunch_same_agent` | G3 が共通層と cross-review を実装する。G4 は `read_result` に相当する読み取りを `read_launch_outcome` に置き換え、3 つの取り込みで `payload` 無しのときの群の状態・担当の交代・終了コードを決める。G4 の既存の設計の `apply._monitor_reason` と `gitfacts.load_result` は `read_launch_outcome` で置き換わる | -| G4 | 監視の終了コード 0〜6 と標準出力は変えない | G3 が守る。G4 は終了コードで分岐しない設計(G4 の決定 4)を続ける | -| G5(#730 #583) | 既存の設計の決定 18(`prior_review_url` と記録だけの起動)・決定 19(起動し直しを初回と同じ経路へ)・AC63〜AC67 | G5 が持つ。G3 は `read-result` の結果なしの記録に `monitor_detail` を足すだけで、`prior_review_url` の鍵と `launch-reviewer.sh` は触らない。G5 が `_record_no_result` に `prior_review_url` を足すときは、G3 の `monitor_detail` の書き方(鍵が無い = 無かった)に揃える | -| D-B(#478) | 判定が出す `NO_RESULT_REASONS` の行と、可否が偽のときに止めること | G3 が入れる。利用上限の担当を外して残りで回す判断は #478 | -| G1(#727 #478 ほか) | `state.py` の `init` / 再開の引数 | 触るファイルが同じ(`state.py`)だが節が違う(G3 は `_read_review_result_file` / `_record_no_result` / `_handle_no_result_round` / `report`)。競合は後からマージする側が解く | - -## 既存の設計との対応 - -既存の設計(PR #666)の P3 の決定を、この文書のどの決定が引き継ぐかを示す。 - -| 既存の決定(PR #666) | この文書 | 変わったこと | -| --- | --- | --- | -| 決定 14(利用上限は致命として止め、同じラウンドで起動し直さない) | 決定 3・4 | 「判定が `usage_limit` を見る」から「共通層の可否を見る」へ。値は同じ | -| 決定 15(CLI の上限は結果なしのまま理由だけ) | 決定 5 | 結果ファイルがあれば `ok` を明記 | -| 決定 16(監視の終了コードと標準出力を変えない) | 決定 4・12 | 同じ | -| 決定 17(プロセスグループで止める) | 決定 10 | 同じ | -| 決定 18(投稿済みの担当は記録だけの起動) | — | #730(G5)へ | -| 決定 19(起動し直しを初回と同じ経路へ) | — | #730(G5)へ | -| 決定 20(利用上限の検知は err.log + claude の stdout.log) | 決定 6 | 実測を足した。値は同じ | -| — | 決定 1・2・7・8・9・11 | 新設 | - -## この文書の位置づけ - -この文書は「どう作るか」だけを扱う。要求と受け入れ条件は [issue-729-619-584-requirements.md](issue-729-619-584-requirements.md) にある。 - -**既存の設計 [issue-662-598-537-619-584-583-design.md](issue-662-598-537-619-584-583-design.md) の P3 を置き換える。** 既存の決定のうち引き継ぐものと変えるものは「既存の設計との対応」にある。#583(決定 18・19)は #730 の設計へ移る。 diff --git a/issues/issue-729-619-584-implementation-plan.md b/issues/issue-729-619-584-implementation-plan.md deleted file mode 100644 index c1fbb7e30..000000000 --- a/issues/issue-729-619-584-implementation-plan.md +++ /dev/null @@ -1,173 +0,0 @@ -# cross-review: 利用上限で止まった担当が「結果ファイル無し」と報告されて空振りの起動し直しで待たされ、止めた担当が後から結果を書く → 上限を理由に報告して同じラウンドで起動し直さず、止めた後は書かせない(実装計画 / #729 #619 #584) - -## 関連リンク - -- 親 issue #729、子 issue #619 #584 -- 要求と受け入れ条件: [issue-729-619-584-requirements.md](issue-729-619-584-requirements.md)(AC1〜AC24) -- 設計: [issue-729-619-584-design.md](issue-729-619-584-design.md)(決定 12 件。識別子は冒頭の「用語の対応表」で引く) -- 設計 Pull Request: #781(`develop` へマージ済み) - -## モード - -`standard`。本番の振る舞い(結果なしの理由と起動し直しの判断)を変え、共通層と cross-review の複数モジュールにまたがる。 - -## 目的と非目的 - -達成したい状態: - -- 担当の CLI が利用上限で落ちたとき、進行側と利用者に理由「利用上限」が届き、同じラウンドで同じ担当を起動し直さない -- 結果なしの判断と起動し直しの可否を、結末の共通層の 1 つの関数が持つ。cross-review はその値を読むだけになる -- 監視が止めた担当の子プロセスが、止めた後に結果ファイルを書かない - -やらないこと: - -- cross-refactoring の結果の読み取りと 3 つの取り込みの実装(G4 #728 が、この計画が作る結末を読む関数を使って行う) -- 投稿の後に打ち切られた担当の記録と重ねての投稿(G5 #730) -- 利用上限の担当を外して残りの担当で回す判断(#478) -- 監視の終了コードと標準出力の変更、cross-review の骨組み(`SKILL.md`)の変更 -- テストが共通層のモジュールを読み込む流儀を揃えること(実装中に見つけ、#789 として起票した) - -## 前提 - -- 前提 1: 設計文書の決定 12 件を変えない。実装で決めるのは「未確認のまま残ること」の 4 件(claude の 429 の出る先・bash 3.2 の `set -m`・文言の並ぶ順序・stem の突き合わせ)で、決めた結果は設計文書の同じ節へ書く -- 前提 2: 監視の結果ファイルと監視の記録(P1)、上限の表と工程(P2)は `develop` にある。この変更はその上に載せる -- 前提 3: 手元の bash は 5.3.9 である。macOS の bash 3.2 は実機が無いため、`set -m` の互換は bash の変更履歴の確認までとし、成り立たない場合の逃げ道(グループの先頭でなければ pid だけを止める)を実装で保証する - -## 受け入れ条件 - -要求文書の AC1〜AC24 をそのまま使う。検証手段は設計文書の「テスト設計」の表にある。この計画では、各タスクが満たす番号を「タスク分解」で示す。 - -## ドメイン用語 - -設計文書の「用語の対応表」を使う。この計画で新たに使う語は次の 2 つである。 - -| 用語 | 意味 | -| --- | --- | -| 共通層のタスク | 結末の語彙・監視・起動の手順を変えるタスク(Task 1〜4)。cross-review を触らない | -| 読む側のタスク | cross-review の状態の操作を変えるタスク(Task 5〜6)。共通層のタスクの後に行う | - -## 不変条件 - -- 監視の終了コード 0〜6 と標準出力の 13 個のキーは変わらない -- 結果の取り込みの終了コード(使える結果 0 / 読めない結果 3 / それ以外 1)と、判定の 0 / 2 / 7 / 8 の意味は変わらない -- 結果ファイルが JSON オブジェクトとして読めるなら、監視の結末が何であっても使える結果として扱う -- 理由の語彙と起動し直しの可否の表を持つのは結末の共通層だけである - -## 互換性 - -| 対象 | 変更 | 互換性の扱い | -| --- | --- | --- | -| 監視の標準出力・終了コード | 変えない | 変えない | -| 監視の結果ファイル・監視の記録の `reason` | `usage_limit` / `cli_timeout` の 2 値が増える | 追加のみ。読む側は値を集計するだけで一覧を持たない | -| 状態ファイルの `rounds[-1].<担当>` | `monitor_detail` の鍵が増える(結果なしのときだけ)。`no_result_reason` の値が 10 種類になる | 追加のみ。既存の状態ファイルはそのまま読める | -| 結末の共通層の関数 | `read_launch_outcome` / `relaunch_same_agent` / `LaunchOutcome` / `NO_RELAUNCH_REASONS` を新設 | 追加のみ。既存の `read_outcome` / `reason_for` / `REASONS` の呼び出し側は変わらない | -| 起動の手順の引数・pid ファイル | 変えない | 変えない | - -## 修正対象 - -```text -plugins/ndf/scripts/lib/monitor_outcome.py -plugins/ndf/scripts/lib/monitor.py -plugins/ndf/scripts/lib/launch-cli.sh -plugins/ndf/scripts/lib/README.md -plugins/ndf/scripts/tests/test_monitor_outcome_unit.py -plugins/ndf/skills/cross-review/scripts/state.py -plugins/ndf/skills/cross-review/docs/01-state-and-review.md -plugins/ndf/skills/cross-review/docs/03-review-output.md -plugins/ndf/skills/cross-review/docs/04-contracts.md -plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py 新設 -plugins/ndf/skills/cross-review/tests/test_launch_cli_process_group.py 新設 -plugins/ndf/skills/cross-review/tests/test_read_result_reason.py 新設 -plugins/ndf/skills/cross-review/tests/test_judge_no_result_reason.py 新設 -issues/issue-729-619-584-design.md 「未確認のまま残ること」の更新 -plugins/ndf/dev.kiro/ / plugins/ndf/dev.agy/ 配布物の同期(生成物) -``` - -## タスク分解 - -共通層のタスク(1〜4)→ 読む側のタスク(5〜6)→ 文書と退行の確認(7〜8)の順に進める。各タスクは失敗するテスト → 通す最小実装 → 整理の順で行う。 - -### Task 1: 結末を 1 つの値として読む関数を共通層に置く - -- **対象ファイル:** `plugins/ndf/scripts/lib/monitor_outcome.py`、`plugins/ndf/scripts/tests/test_monitor_outcome_unit.py` -- **変更内容:** 理由の語彙を 9 語にする(`REASONS` に `usage_limit` / `cli_timeout` / `unparsable`)。起動し直せない理由の集合 `NO_RELAUNCH_REASONS = frozenset({"usage_limit"})` と `relaunch_same_agent(reason)` を置く。`LaunchOutcome`(`payload` / `reason` / `detail` / `monitor` / `relaunch_same_agent`)と `read_launch_outcome(tmp_dir, stem, result_path=None)` を新設する。理由の決め方は設計文書の「結末を読む関数」の表のとおり。例外・`SystemExit`・標準出力/標準エラーへの出力を出さない。モジュールの冒頭の説明に読み取りの責務を足す -- **満たす受け入れ条件:** AC1、AC8〜AC11 -- **進め方:** 監視の結果ファイル(各理由・壊れた JSON・無し)× 結果ファイル(オブジェクト・配列・壊れた JSON・空・無し)の組み合わせを一時ディレクトリに置く単体テストを先に書く。`capsys` で出力が空であることも見る - -### Task 2: 監視が利用上限の文言を検知し、理由「利用上限」を結末に添える - -- **対象ファイル:** `plugins/ndf/scripts/lib/monitor.py`、`plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py`(新設) -- **変更内容:** `MonitorOutcome` に `reason: Optional[str] = None` を足し、`create(status, detail, reason=None)` にする。`USAGE_LIMIT_FATAL`(kiro の文言・claude の 429 の JSON・既存の `quota exceeded` / `rate limit exceeded`・`HTTP/x 429`)と `CLAUDE_STDOUT_USAGE_LIMIT` を新設し、`EARLY_ERROR_FATAL` から 429 と quota / rate limit の一致を外す(401 / 403 は残す)。致命の照合の前に利用上限の照合を置く(err.log は全担当、stdout.log は claude だけ JSON 向けの照合)。`_early_error_outcome` は利用上限に一致したとき `reason="usage_limit"` を添える。`_record_outcome` は `outcome.reason or reason_for(status)` で理由を書く。結末を作る既存の呼び出し 8 か所は変えない -- **満たす受け入れ条件:** AC2〜AC4、AC6、AC7 -- **進め方:** 設計文書の「実測」の 10 行を入力にした照合の単体テストと、文言を 1 行書いた err.log / stdout.log と終わったプロセスの pid ファイルで `monitor_agent` を呼ぶテストを先に書く。既存の `test_monitor_outcome_file.py` と `test_monitor_early_error.py` は変えずに通す。**照合の順序(利用上限 → 致命 → 警告の見た目の致命)をテストで固定する**(設計文書の未確認 5) - -### Task 3: CLI 自身の上限で結果を書かずに終わった担当を、理由「CLI の上限」にする - -- **対象ファイル:** `plugins/ndf/scripts/lib/monitor.py`、`plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py` -- **変更内容:** `CLI_TIMEOUT_AFTER_EXIT`(`print timeout after \S+ with turn in progress`)を新設する。`_process_exit_outcome` で、終了して結果ファイルが無いときだけ err.log を照合し、一致すれば `reason="cli_timeout"` を添える。結果ファイルがあれば従来どおり `OK` -- **満たす受け入れ条件:** AC5 -- **進め方:** 結果ファイルあり・なしの 2 通りのテストを先に書く - -### Task 4: CLI を独立したプロセスグループで起動し、止めるときはグループへ送る - -- **対象ファイル:** `plugins/ndf/scripts/lib/launch-cli.sh`、`plugins/ndf/scripts/lib/monitor.py`、`plugins/ndf/skills/cross-review/tests/test_launch_cli_process_group.py`(新設) -- **変更内容:** 起動の手順の `launch_runtime` を呼ぶ前に `set -m` を有効にし、背景起動した CLI の pid がプロセスグループの番号になるようにする(起動の直後に `set +m` で戻す)。`_kill_pid` は `os.getpgid(pid) == pid` かつ `!= os.getpgrp()` のとき `os.killpg` で SIGTERM → 3 秒 → SIGKILL を送り、それ以外は従来どおり pid だけへ送る。生存の確認はグループの先頭の pid で見る -- **満たす受け入れ条件:** AC19〜AC21 -- **進め方:** 3 秒後に子プロセスが結果ファイルを書く偽の CLI(`bash -c`)を PATH に置いて起動の手順から起動し、`os.getpgid(pid) == pid` を確かめるテスト、監視の上限 2 秒で止めた後 4 秒待っても結果ファイルが無いテスト、先頭でない pid で `os.killpg` が呼ばれないことを `mock` で見るテストを先に書く。**bash 3.2 の互換は、`set -m` が bash 2 系から存在する組み込みであることを `man bash` / 変更履歴で確かめ、設計文書の未確認 3 へ書く**(実機での確認は未確認のまま残す) - -### Task 5: cross-review の結果の取り込みが共通の値を読み、理由と監視の詳細を残す - -- **対象ファイル:** `plugins/ndf/skills/cross-review/scripts/state.py`、`plugins/ndf/skills/cross-review/tests/test_read_result_reason.py`(新設) -- **変更内容:** 共通層の `monitor_outcome` を読み込む。`_read_review_result_file` は結果ファイルを自前で開かず `read_launch_outcome(tmp_dir, f"{agent}-review-pr{pr}", rfile)` を呼び、`payload` が無ければ `reason` を `no_result_reason` として記録して従来の終了コード(`unparsable` は 3、それ以外は 1)で止める。`_record_no_result` に `monitor_detail`(監視の結果ファイルがあるときだけ鍵を書く。空文字は書かない)を足す。`no_verdict` / `not_posted` の上書きは従来どおり -- **満たす受け入れ条件:** AC13、AC12(`plugins/ndf/skills/` に `usage_limit` の条件分岐を書かない) -- **進め方:** 結果ファイル無し + 監視の結果ファイル(各理由)/ 無しで `read-result` を呼び、状態ファイルの `no_result_reason` と `monitor_detail` と終了コードを見るテストを先に書く。既存の `test_state_read_result.py` / `test_state_no_result.py` は変えずに通す。**stem の突き合わせ**(設計文書の未確認 6)は、`launch-reviewer.sh` の `STEM` と監視の `DEFAULT_STEM_TEMPLATE` と取り込みの stem が同じ形であることをテストで固定する - -### Task 6: 判定が理由を出し、起動し直せない理由があれば止める。報告の表に理由を出す - -- **対象ファイル:** `plugins/ndf/skills/cross-review/scripts/state.py`、`plugins/ndf/skills/cross-review/tests/test_judge_no_result_reason.py`(新設) -- **変更内容:** `_handle_no_result_round` は結果なしの担当の理由を集めて標準出力に `NO_RESULT_REASONS='<担当>=<理由> ...'` を出す。理由に `relaunch_same_agent` が偽のものがあれば、起動し直さずに `final=error` として終了コード 1 で止め、標準エラーに担当・理由・`monitor_detail` を出す。すべて起動し直してよい理由なら従来どおり 7 / 2 度目は 1。`_print_round_summary` は結果なしの担当を `<担当>=NO_RESULT(<理由>)` の形で出す -- **満たす受け入れ条件:** AC14〜AC17、AC24 -- **進め方:** 状態ファイルを作って `judge` と `report` を呼ぶ。`usage_limit` を含む / 含まない / 2 度目の 3 通りを先に書く。`SKILL.md` の骨組みの行(`bg-wait.sh run` から `case $JUDGE_RC` まで)は `git diff` で差が無いことを確かめる - -### Task 7: 文書を更新する - -- **対象ファイル:** `plugins/ndf/skills/cross-review/docs/01-state-and-review.md`、`docs/03-review-output.md`、`docs/04-contracts.md`、`plugins/ndf/scripts/lib/README.md`、`issues/issue-729-619-584-design.md` -- **変更内容:** 理由の表を 10 語にする(`missing` / `unparsable` / `no_verdict` / `not_posted` / `timeout` / `stalled` / `early_error` / `usage_limit` / `cli_timeout` / `pidfile_bad`)。「monitor.py が誤って kill する場合の手順」に、上限に当たった場合の見分け方(`reason` と `monitor-outcomes.jsonl` の読み方)を足す。契約の文書に `monitor_detail` の鍵と `reason` の 2 値を足す。共通層の一覧の `monitor_outcome.py` の行に読み取りの責務を足す。設計文書の「未確認のまま残ること」の 1・3・5・6 を、実装で決めた結果へ更新する -- **満たす受け入れ条件:** AC18 -- **進め方:** テスト駆動を適用しない(文書)。`grep` で 10 語と「見分け方」の節を確かめる。`python3 scripts/check-doc-line-limit.py` を通す - -### Task 8: 退行の確認と配布物の同期 - -- **対象ファイル:** `plugins/ndf/dev.kiro/` `plugins/ndf/dev.agy/` の生成物 -- **変更内容:** `bash scripts/build-runtime-plugins.sh` で生成物を揃える。全テスト・定義の検査を通す。#619 と #584 の再現を手動で確かめる(要求文書の「検証手段」) -- **満たす受け入れ条件:** AC22、AC23 -- **進め方:** テスト駆動を適用しない(検証)。コマンドの終了コードを記録し、Pull Request 本文へ載せる - -## 影響範囲 - -- 監視を使う 2 つの Skill(cross-review / cross-refactoring)。cross-refactoring は監視の結果ファイルの `reason` に 2 値が増えるだけで、終了コードの分岐は変わらない -- 実行の要約(`run_metrics.py`)は `reason` の値を集計するだけで、語彙の一覧を持たないため変更なし -- 担当の CLI のプロセスが独立したプロセスグループで動く。起動の手順を呼ぶ側(`launch-reviewer.sh`、cross-refactoring の `launch-*.sh`)の引数は変わらない - -## リスクと対処 - -| リスク | 対処 | -| --- | --- | -| `state.py` は 4470 行の 1 ファイルで、G1(#727)も同じファイルの別の節を触る | タスクごとにテストを通す。触る関数を `_read_review_result_file` / `_record_no_result` / `_handle_no_result_round` / `_print_round_summary` の 4 つに限る。競合は後からマージする側が解く | -| `docs/01-state-and-review.md` は 497 行で、行数の上限(500)に近い。理由の表に 7 行足すと超える | 同じ文書の既存の記述を詰め、501 行以上にしない。詰められなければ理由の表を契約の文書(`04-contracts.md`)へ移し、元の場所からリンクする | -| 利用上限の文言の照合を致命の照合の前に置くため、既存の 429 / quota の一致の `reason` が変わる | 既存テストは `_scan_early_fatal` の返り値だけを見ており、利用上限の照合を先に置いても `_scan_early_fatal` 単体の挙動は変えない。理由の変化は新しいテストで固定する | -| `set -m` を有効にすると、bash がジョブの終了を標準エラーへ出す・起動元がジョブ制御の影響を受ける | `launch_runtime` の直前で有効にし、直後に `set +m` で戻す。既存の `test_lib_launch_cli_runtimes.py` / `test_launch_cli_guards.py` で標準エラーの形が変わらないことを見る | -| 監視の環境変数(`MONITOR_*`)を export したシェルでは既存テストが落ちる(#678、G6) | export していないシェルでテストを実行する | - -## 切り戻し手順 - -- すべてコードと文書の変更で、データ移行は無い。Pull Request の revert で戻せる -- 状態ファイルの `monitor_detail` は追加の鍵で、戻した後の読み手は無視する。監視の結果ファイルの `usage_limit` / `cli_timeout` は戻した後の `reason_for` の一覧に無いが、読む側は値を集計するだけで一覧を照合しない - -## 完了の定義 - -- [ ] AC1〜AC24 をすべて満たし、条件ごとに検証手段と結果が対応している(Pull Request 本文の表) -- [ ] `uv run --with pytest pytest scripts/tests plugins/ndf -q` が通る(export していないシェル) -- [ ] `bash scripts/build-runtime-plugins.sh --check`、`python3 scripts/check-skill-frontmatter.py`、`claude plugin validate .` が終了コード 0 -- [ ] `git grep -n 'usage_limit' -- plugins/ndf/skills` の一致行が文書・テスト・テンプレートの文言だけである(AC12) -- [ ] 設計文書の「未確認のまま残ること」の 1・3・5・6 が、実装で決めた結果へ更新されている diff --git a/issues/issue-729-619-584-requirements.md b/issues/issue-729-619-584-requirements.md deleted file mode 100644 index 04b3a08fc..000000000 --- a/issues/issue-729-619-584-requirements.md +++ /dev/null @@ -1,261 +0,0 @@ -# cross-review: 利用上限で止まった担当が「結果ファイル無し」と報告されて空振りの起動し直しで待たされ、止めた担当が後から結果を書く → 上限を理由に報告して同じラウンドで起動し直さず、止めた後は書かせない(要求と受け入れ条件 / #729 #619 #584) - -## 目的 - -**起きていること。** 担当の CLI(kiro / claude)が月間の利用上限に当たると、監視はその文言を読めない。結果なしの理由は「結果ファイル無し」に畳まれる。進行側は同じ担当を同じラウンドで起動し直し、監視の上限 1 回分(レビューで 1200 秒)を待ってから全体を誤りで終える(#619。#647 では 3729 回の空振り)。また、監視が止めた担当の子プロセスが、止めた後に結果ファイルを書く(#584)。 - -**困る人。** 収束ループを回す進行側と、結果を待つ利用者。届く理由が「結果ファイル無し」のため、上限に当たったのか、監視の上限で打ち切られたのかを判別できない。 - -**直すと成り立つこと。** 理由が「利用上限」として進行側と利用者に届き、同じラウンドで同じ担当を起動し直さない。結果ファイルが無いときの「監視が打ち切った」「CLI が自分の上限で終わった」「終わったが結果を書かなかった」を、結末の語彙 1 つで区別できる。結果なしの判断と起動し直しの可否は共通層の 1 か所が持つ。cross-review と cross-refactoring がその値を読む(両 Skill が同じ判断を別々に書かない)。監視が止めた担当の子プロセスは、止めた後に結果ファイルを書かない。 - -## 用語 - -本文はこの表の用語で書く。受け入れ条件は検査で突き合わせる値を持つため、識別子の列の語で書く。 - -| 用語 | 意味 | 識別子 | -| --- | --- | --- | -| 担当 | レビュー・反証・適用・修正を行う CLI(codex / agy / kiro / claude) | — | -| 起動 1 回 | 起動の手順が担当を 1 度起動し、監視がそれを見終わるまで | `launch-cli.sh` / `monitor.py` | -| 結末 | 起動 1 回の終わり方。監視の状態と理由、結果ファイルの有無と読めるかを合わせたもの | 監視の `status` と `reason` | -| 理由 | 結末の語彙の 1 語 | `monitor_outcome.REASONS` | -| 起動し直しの可否 | 同じ担当を同じ条件で起動し直せば解ける結末か(偽なら、起動し直しても同じ結末になる) | `relaunch_same_agent` | -| 使える結果 | 結果ファイルがあり、JSON オブジェクトとして読める | `payload` | -| 結果ファイル | 担当が一時ディレクトリへ書く JSON | `-result.json` | -| 監視の結果ファイル | 担当 1 者の最後の監視の結果(P1) | `-monitor.json` | -| 監視の記録 | 監視の結果を追記だけで積む(P1) | `monitor-outcomes.jsonl` | -| 利用上限 | 担当の CLI の月間・週間の利用枠に達し、起動し直しても解けない状態 | 理由 `usage_limit`。監視の状態は早期の致命 `EARLY_ERROR`(終了コード 4) | -| CLI の上限 | 担当の CLI 自身の実行時間の上限(agy の `--print-timeout`) | 理由 `cli_timeout`。監視の状態は結果なし `NO_RESULT`(終了コード 3) | -| 結果ファイル無し | 結果ファイルが無く、理由の文言も無い | 理由 `missing` | -| 読めない結果 | 結果ファイルがあるが JSON オブジェクトとして読めない | 理由 `unparsable` | -| 誤りの終わり | 収束ループ全体を誤りとして終える | 状態ファイルの `final=error`。判定の終了コード 1 | -| 結果の取り込み / 判定 / 報告の表 | cross-review の状態の操作の 3 コマンド | `state.py read-result` / `judge` / `report` | -| 結末の共通層 | 2 つ以上の Skill が使う部品の置き場所にある、結末の語彙と読み取り | `plugins/ndf/scripts/lib/monitor_outcome.py` | - -## 対象範囲 - -変えるのは共通層の 3 ファイルと cross-review で、cross-refactoring は契約だけを決める。 - -含む: - -| 場所 | 扱うもの | -| --- | --- | -| 共通層 `plugins/ndf/scripts/lib/monitor_outcome.py` | 理由の語彙 2 つの追加、結末を 1 つの値として読む関数、起動し直しの可否 | -| 共通層 `plugins/ndf/scripts/lib/monitor.py` | 利用上限と CLI の上限の文言の検知、理由を持つ結末、プロセスグループへの停止 | -| 共通層 `plugins/ndf/scripts/lib/launch-cli.sh` | CLI を独立したプロセスグループで起動する | -| cross-review `scripts/state.py` | `read-result` の結果なしの理由と `monitor_detail`、`judge` の理由の出力と利用上限での停止、`report` の表 | -| cross-review `docs/` | 理由の表、上限に当たった場合の見分け方 | -| テスト | 共通層と cross-review のテスト | - -含まない: - -| 扱わないもの | 理由 | -| --- | --- | -| cross-refactoring の `gitfacts.read_result` と 3 つの取り込み(`merge-apply` / `merge-fix` / `merge-final-fix`)の実装 | #728(G4)。この文書は契約だけを決める(設計文書の「両 Skill が従う契約」) | -| 投稿の後に打ち切られた担当の記録と重ねての投稿(`prior_review_url`、記録だけの起動) | #583 → #730(G5)。既存の設計の決定 18・AC63〜AC67 はそちらへ移る | -| 起動し直した担当を初回と同じ経路(証拠集約を含む)に通す骨組みの変更 | 既存の設計の決定 19・AC67。投稿の重なりと同じ骨組みの行を触るため #730(G5)へ渡す | -| 利用上限の担当を外して残りの担当で回す | #478(D-B) | -| 反証の取り直し(`critique-round.sh`)で理由を見ること | 既存の設計の「未確認のまま残ること」10。直さない | -| codex のモデルの 404 を理由として区別すること | #461(マイルストーン 06)。この語彙では `early_error` か `missing` に落ちる | -| 監視の終了コードと標準出力の変更 | 前提 6 | -| 型・クラスの新設のうち、画面・永続データベース・OpenAPI に当たるもの | この変更に画面と API は無い。永続データは状態ファイルと監視の結果ファイル(JSON)で、設計文書の「データ構造」が表で持つ | -| `CHANGELOG.md` と版数 | 配布の工程が書く | - -## 前提 - -利用上限の文言の実物は 2 つで、配布済みの P1 / P2 の上に載せる。 - -| # | 前提 | -| --- | --- | -| 1 | claude の利用上限の文言の実物は `"api_error_status":429` を含む行である(#647 の本文)。err.log と stdout.log のどちらに出るかは未確認のため、両方を見る | -| 2 | kiro の利用上限の文言の実物は err.log の `Monthly request limit reached` である(#619 の本文、PR #601 の round 2) | -| 3 | P1(監視の結果ファイル `-monitor.json` と `monitor_outcome.read_outcome`)と P2(`limits.py`、`--phase`)は v10.13.0 で `develop` に入っている。この変更はその上に載せる | -| 4 | cross-refactoring の側の実装(`gitfacts.read_result` と 3 つの取り込み)は #728(G4)が行う。この変更が決めるのは `read_result` が従う契約だけである | -| 5 | 利用上限で止まった担当を外して残りの担当で回す判断は #478(D-B)が持つ。この変更は「同じ担当を起動し直さずに止めて理由を報告する」までである | -| 6 | 監視の終了コード 0〜6 と標準出力の 13 個のキーは変えない(既存の設計の決定 16。G4 の骨組みがこれを前提にする) | - -## 影響 - -監視の終了コードと標準出力は変わらず、増えるのは理由の値と状態ファイルの鍵である。 - -| 対象 | 影響 | -| --- | --- | -| `monitor.py` の終了コードと標準出力 | 変わらない。監視の結果ファイルと記録の `reason` に 2 つの値が増える | -| `monitor_outcome.REASONS` | 6 語から 9 語になる。読む側(`run_metrics.py` の `--by reason`)は値を集計するだけで、語彙の一覧を持たないため変更なし | -| 状態ファイル | `rounds[-1].<担当>` に `monitor_detail` が増える(結果なしのときだけ)。`no_result_reason` の値が 3 種類から 10 種類になる | -| `state.py judge` | `NO_RESULT_REASONS` の行が増える。利用上限では起動し直さず 1 で終わる。0 / 2 / 7 / 8 の意味は変わらない | -| `state.py read-result` | 終了コードは変わらない。`no_result_reason` の値が増える | -| `gitfacts.read_result` | この変更では触らない。契約(結果なしを値で返す)を G4 が実装する | -| 担当の CLI のプロセス | 独立したプロセスグループで動く。監視の停止がグループへ届く | -| 待ち時間の最悪値 | 利用上限では起動し直さないため、1 ラウンドあたり監視の上限 1 回分(レビュー 1200 秒)短くなる | - -## 非機能の条件 - -運用・保守性、セキュリティ、システム環境の 3 つを条件にする。 - -| 大項目 | 条件 | -| --- | --- | -| 運用・保守性 | 結果なしの理由が、状態ファイル(`no_result_reason`)・監視の結果ファイル・監視の記録・実行の要約の `launches[]` の 4 か所で同じ語彙で読める。理由の語彙を足すときに変える場所は `monitor_outcome.py` の 1 か所である | -| セキュリティ | `monitor_detail` は err.log の抜粋(最大 200 文字)で、作業ツリーの中の状態ファイルにだけ残る。実行の要約には入れない(既存の設計の決定 7 のまま) | -| システム環境 | macOS の bash 3.2 でも AC19 が成り立つこと(未確認。設計文書の「未確認のまま残ること」) | - -## 前提とする取り決め - -プロジェクトの規約のうち、この変更が従うものを 3 つ挙げる。 - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | 2 つ以上の Skill が使う部品は `plugins/ndf/scripts/lib/` に置く(`plugins/ndf/scripts/lib/README.md` の「置いてよいもの・いけないもの」)。結末を読む関数は両 Skill が使うため共通層に置く | -| コーディング規約 | 外部コマンドとシグナルの挙動、正規表現の一致範囲は書く前に実行して確かめる(`AGENTS.md` の DO)。状態ファイルの鍵は追加だけで、既存の鍵の意味を変えない | -| テスト戦略 | 監視と起動は、PATH へ置いた偽の CLI を実プロセスとして動かす既存の形(`cross-review/tests/test_monitor_*.py`)。結末の読み取りは一時ディレクトリに監視の結果ファイルと結果ファイルを置いて単体に試す。`judge` / `report` は状態ファイルを作って呼ぶ | - -## 境界 - -常に行うこと、確認してから行うこと、行わないことを分ける。 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 既存テストの実行、配布物の同期の検査、新しい文言を実物のログの形で試すこと | -| 確認してから行う | 利用上限で起動し直さない判断(AC15)。理由の語彙の追加(G4 が読む) | -| 行わない | cross-refactoring の取り込みの実装(G4)、投稿の重なりと骨組みの経路の変更(G5)、担当を外して回す判断(D-B)、監視の終了コードの変更 | - -## 検証手段 - -テスト・配布物の同期・定義の検査と、2 つの issue の再現の手動確認で確かめる。 - -| 項目 | 手段 | -| --- | --- | -| テスト | `uv run --with pytest pytest scripts/tests plugins/ndf -q` | -| 配布物の同期 | `bash scripts/build-runtime-plugins.sh --check` | -| 定義の検査 | `claude plugin validate .` と `python3 scripts/check-skill-frontmatter.py` | -| 手動確認(#619 の再現) | err.log に `Monthly request limit reached` を書く偽の kiro を PATH に置いて cross-review を 1 ラウンド回し、`judge` が 7 を返さず `NO_RESULT_REASONS='kiro=usage_limit'` を出して 1 で終わる | -| 手動確認(#584 の再現) | #584 の本文の再現スクリプト(3 秒後に子が書く `bash -c`)を `launch-cli.sh` 経由で起動し、`_kill_pid` の後に `late-result.json` が無い | - -## 受け入れ条件 - -24 件を 5 つの群に分ける。文言の一覧は設計文書の「入出力の契約」の「検知の文言」にある。 - -語彙と検知(監視、#619): - -- [ ] AC1: `monitor_outcome.REASONS` は次の 9 語である。`reason_for(status)` の 6 つの状態に対する返り値は変更前と同じである - - | 既存の 6 語 | 足す 3 語 | - | --- | --- | - | `ok` / `timeout` / `stalled` / `early_error` / `missing` / `pidfile_bad` | `usage_limit` / `cli_timeout` / `unparsable` | - -- [ ] AC2: err.log に `Monthly request limit reached` の行が出ると、監視は担当を止めて `EARLY_ERROR`(終了コード 4)を返す。監視の結果ファイルの `reason` は `usage_limit` である -- [ ] AC3: err.log(全担当)か stdout.log(claude だけ)に、`"api_error_status"` と `429` を `:` で結んだ行が出たときも AC2 と同じである。`reason` は `usage_limit` になる。`:` の前後の空白の有無は問わない -- [ ] AC4: 既存の一致のうち `quota exceeded` / `rate limit exceeded` / `HTTP/<版> 429` の `reason` は `usage_limit` になる。`HTTP/<版> 401` / `HTTP/<版> 403` とそれ以外の致命の一致は `early_error` のままである -- [ ] AC5: 担当が結果ファイル無しで終わり、err.log に `print timeout after <時間> with turn in progress` がある場合を扱う。監視は `NO_RESULT`(終了コード 3)を返し、`reason` は `cli_timeout` である。同じ文言があっても結果ファイルがあれば `OK` / `ok` である -- [ ] AC6: AC2〜AC5 の文言が err.log で markdown の表の行・引用・バッククォート・grep 形式の引用の中にあるときは一致しない。claude の stdout.log は JSON 向けの照合で見るため、この除外を掛けない -- [ ] AC7: `usage_limit` / `cli_timeout` は監視の結果ファイルと監視の記録の `reason` に入る。監視の標準出力の 13 個のキーと値の型、終了コードは変更前と同じである - -結末を 1 つの値として読む(共通層、#729): - -- [ ] AC8: `monitor_outcome.read_launch_outcome(tmp_dir, stem, result_path)` を呼ぶ。結果ファイルが JSON オブジェクトとして読めるとき、返り値はその辞書を `payload` に持つ。`reason` は `None`、`relaunch_same_agent` は `True` である。監視の結果ファイルの `reason` が何であっても同じである -- [ ] AC9: 使える結果が無いとき、`reason` は次の表で決まる - - | 監視の結果ファイルの `reason` | 結果ファイル | `reason` | - | --- | --- | --- | - | `timeout` / `stalled` / `early_error` / `usage_limit` / `cli_timeout` / `pidfile_bad` | 問わない | その値 | - | `ok` / `missing` / ファイルが無い・読めない | 無い、または空 | `missing` | - | `ok` / `missing` / ファイルが無い・読めない | あるが JSON として読めない、または JSON オブジェクトでない | `unparsable` | - -- [ ] AC10: `relaunch_same_agent` は `reason` が `usage_limit` のときだけ `False` で、それ以外の理由では `True` である。`monitor_outcome.relaunch_same_agent(reason)` も同じ値を返す -- [ ] AC11: `read_launch_outcome` は `SystemExit` を投げず、標準出力・標準エラーへ何も書かない。監視の結果ファイルがあれば `monitor` にその辞書、無ければ `None` を持ち、`detail` に監視の `detail`(無ければ読めなかった理由の 1 文)を持つ -- [ ] AC12: 理由の一覧と起動し直しの可否の表を持つのは `plugins/ndf/scripts/lib/monitor_outcome.py` だけである。`plugins/ndf/skills/` の下に `usage_limit` を含む条件分岐(`== "usage_limit"` / `in (...)` の形)は無い - -cross-review が値を読む(#619): - -- [ ] AC13: `read-result` は使える結果が無いとき、`no_result_reason` に AC9 の `reason` を記録する。監視の結果ファイルがあれば `rounds[-1].<担当>.monitor_detail` に監視の `detail` を残す。終了コードは変更前と同じ(`unparsable` は 3、それ以外は 1)である -- [ ] AC14: `judge` は結果なしの担当があると、標準出力に `NO_RESULT_REASONS='<担当>=<理由> ...'` の 1 行を出す -- [ ] AC15: 結果なしの担当の理由に `relaunch_same_agent` が偽のもの(`usage_limit`)があるとき、`judge` は起動し直さない。`final=error` として終了コード 1 で終わり、標準エラーに担当・理由・`monitor_detail` が出る -- [ ] AC16: 理由がすべて起動し直してよいものなら、`judge` は変更前と同じく終了コード 7 で `RELAUNCH_AGENTS` を返し、2 度目の結果なしで `final=error` になる。終了コード 0 / 2 / 8 の枝は変更前と同じである -- [ ] AC17: `state.py report` のラウンド表で、結果なしの担当は `kiro=NO_RESULT(usage_limit)` の形で出る -- [ ] AC18: `docs/01-state-and-review.md` の理由の表に 10 個の理由が載る。10 個は AC1 の 9 語から `ok` を除いた 8 語に、`no_verdict` / `not_posted` を足したものである。`docs/03-review-output.md` の「monitor.py が誤って kill する場合の手順」に、上限に当たった場合の見分け方が載る。見分け方は `reason` と `monitor-outcomes.jsonl` の読み方である - -止めた後に書かせない(#584): - -- [ ] AC19: `launch-cli.sh` で起動した CLI のプロセスは、自分の pid をプロセスグループの番号に持つ -- [ ] AC20: 3 秒後に子プロセスが結果ファイルを書く CLI を監視の上限で止めると、4 秒待っても結果ファイルが無い -- [ ] AC21: 対象の pid がプロセスグループの先頭でないとき、監視はその pid だけを止め、監視自身のプロセスグループへシグナルを送らない - -退行しないこと: - -- [ ] AC22: `uv run --with pytest pytest scripts/tests plugins/ndf -q` が通る。既存の `test_monitor_*.py` と `test_monitor_outcome_unit.py` は変更せずに通る -- [ ] AC23: 「検証手段」の配布物の同期と定義の検査の 3 つのコマンドが、終了コード 0 で終わる -- [ ] AC24: cross-review の `SKILL.md` の骨組みの行は変えない。骨組みとは、レビューの起動 → 待ち → `read-result` → `judge` → 7 で起動し直し → 8 で `flush` の並びである。判定の終了コードの分岐が増えない - -## 未決 - -1 件。決めるのは G4 の設計である。 - -| 項目 | 誰が決めるか | 期限 | -| --- | --- | --- | -| `gitfacts.read_result` を共通の関数の薄い包みとして残すか、呼び出し側が共通の関数を直接呼ぶか | G4(#728)の設計 | G4 の設計 Pull Request | - -## 既存の受け入れ条件との対応 - -既存の設計(PR #666)の P3 の受け入れ条件を、この文書のどこが引き継ぐかを示す。 - -| 既存 | この文書 | 変わったこと | -| --- | --- | --- | -| AC50 | AC2 | 同じ | -| AC51 | AC3 | 同じ | -| AC52 | AC4 | 同じ | -| AC53 | AC5 | 結果ファイルがあれば `ok` になることを明記 | -| AC54 | AC6 | 同じ | -| AC55 | AC9 + AC13 | 理由の表を `state.py` の規則から共通層の関数の規則へ移した。`unparsable` を共通の語彙に入れた | -| AC56 | AC14 | 同じ | -| AC57 | AC15 | 判定の条件を「`usage_limit` を含む」から「起動し直しの可否が偽」へ変えた。値は同じ | -| AC58 | AC16 | 同じ | -| AC59 | AC17 | 同じ | -| AC60〜AC62 | AC19〜AC21 | 同じ | -| AC63〜AC67 | — | #730(G5)へ | -| AC68 | AC18 | 理由が 10 個になった(`unparsable` を足し、`no_verdict` / `not_posted` を残す) | -| AC69 | AC18 | 同じ | -| — | AC1、AC8、AC10〜AC12 | 新設(結末を 1 つの値として読む契約) | -| — | AC24 | 新設(骨組みを変えない) | - -## 依頼(原文) - -3 つの issue の本文を、書かれたままの形で引く。 - -### #729(根本原因の親) - -> **担当 1 回の起動の結末(結果ファイルの有無と、監視が打ち切った理由)を読み、起動し直してよいかを返す契約。** -> -> - 結末の語彙は `plugins/ndf/scripts/lib/monitor_outcome.py`、早期の致命の検知は `monitor.py` の `EARLY_ERROR_FATAL` にある -> - cross-review の `state.py`(`_read_review_result_file`)も cross-refactoring の `refactor_lib/gitfacts.py`(`read_result`)も語彙を読まず、結果ファイルの有無だけで判断する(Skill 側で `monitor_outcome` を読むコードは 0 件) -> - `EARLY_ERROR_FATAL` は `Monthly request limit reached` や `"api_error_status":429` の行に一致しない -> -> `read_result` が `die` で進行を決める向きは #728 が持つ。 -> -> ## 採る手 -> -> - 移動(`move_responsibility`): 結果なしの判断を、各 Skill の結果ファイルの読み取りから共通層の結末へ移す -> - 新設: 利用上限(`usage_limit`)の語彙と検知の文言 -> -> ## 完了条件 -> -> - 両 Skill が共通層の結末を読み、利用上限を理由として出し、同じラウンドでの起動し直しを止める -> - 利用上限の実際の出力(上の 2 形式)を検知することを検査が確かめる -> - 各子 issue の再現手順を実行し、現象が出ないことを確かめる - -### #619 - -> **監視の結果(状態と `detail`)を担当ごとにファイルへ残し、`read-result` が `NO_RESULT` の理由に使う。** -> 理由は、監視の上限(timeout)・CLI 自身の上限(cli_timeout)・利用上限(usage_limit)・早期エラー(early_error)・未投稿(not_posted)・結果ファイル無し(missing)を区別する。 -> -> - 利用上限は起動し直しても解けないため、理由が `usage_limit` のときは起動し直さずに止めて報告する判断もここに置ける(cross-refactoring の #647 と共通) - -### #584 - -> 移動(`move_responsibility`)。停止の単位を pid からプロセスグループへ移す。`launch-cli.sh` は CLI を新しいプロセスグループとして起動し、`_kill_pid` はそのグループへ SIGTERM / SIGKILL を送る。 -> -> 止めた理由を読む側(結果なしの理由の語彙)は、担当 1 回の起動の結末を共通の語彙で読む #729 が持つ。 - -## この文書の位置づけ - -この文書は「何を満たすか」だけを扱う。設計は [issue-729-619-584-design.md](issue-729-619-584-design.md) にある。 - -**既存の要求 [issue-662-598-537-619-584-583-requirements.md](issue-662-598-537-619-584-583-requirements.md) の P3 を置き換える。** 置き換える受け入れ条件は AC50〜AC62 と AC68〜AC69 で、対応は「既存の受け入れ条件との対応」にある。P1(#662)と P2(#598 #537)は v10.13.0 で配布済みで、この文書は触らない。#583 は #730 の設計が持つ。 diff --git a/issues/issue-730-583-design.md b/issues/issue-730-583-design.md deleted file mode 100644 index aa3b8d8dc..000000000 --- a/issues/issue-730-583-design.md +++ /dev/null @@ -1,446 +0,0 @@ -# cross-review: PR に出ている指摘が記録に残らず、同じ論点が 2 つのスレッドに分かれる → GitHub と git へ書くのをレビューを回す側だけにし、途中で止まっても二度書かない(#730 #583 の設計) - -## 目的 - -レビューを任された担当が、PR へ指摘を書き込んでから結果を残す前に止まると、PR には指摘が出ているのに記録には残らない。レビューを回す側は記録だけを読んで「結果なし」と判定し、同じ担当をもう一度起動する。利用者は 1 つの論点に 2 つのスレッドを見て、両方へ返信する。 - -原因は、書き込みと記録を別々の相手が行っていることである。GitHub と git へ書くのをレビューを回す側 1 か所に集め、担当は結果を残すだけにする。書き込みと記録が同じ手順の中で続けて起きるため、途中で止まっても同じものを二度書き込まない。 - -## 用語の対応表 - -| 業務用語 | 識別子・実体 | -| --- | --- | -| レビューを回す側 | `plugins/ndf/skills/cross-review/scripts/state.py` と、それを呼ぶ `SKILL.md` の骨組み | -| 担当 | 1 ラウンドで 1 者ぶんのレビューを行う CLI。`launch-reviewer.sh` が起動する | -| 席 | そのラウンドで担当が入る枠。`claude` / `codex` / `agy` / `kiro` と、同じランタイムの 2 つ目の `-2`〜`-9` | -| 修正の担当 | 指摘を直すサブエージェント、または単独で動く `fix` の実行 | -| 結果ファイル | `<席>-review-pr<番号>-result.json` | -| 指摘の控え | `<席>-review-pr<番号>-round-payload.json` | -| 修正の結果ファイル | `fix-pr<番号>-result.json` | -| 投稿の待ち行列 | `plugins/ndf/scripts/lib/post_queue.py` | -| 二度書かないための照合 | `post_queue.already_posted()` / `posted_match()` | -| 結果ファイルを投稿へ変える層 | 新設する `plugins/ndf/scripts/lib/result_posts.py` | -| 指摘の取り込み | `state.py read-result` | -| 修正の取り込み | `state.py merge-fix` | -| 最終スイープの照合 | `state.py verify-sweep` | -| 投稿の種別 | `pr-comment` / `review-post` / `review-reply` / `thread-resolve` | - -## 機能一覧 - -| # | 機能 | 受け入れ条件 | -| --- | --- | --- | -| 1 | 担当のプロンプトから投稿の手順を外し、結果だけを書かせる | AC1〜AC4 | -| 2 | 指摘の取り込みが、控えからレビューを組み立てて送る | AC5・AC6・AC16・AC17 | -| 3 | 修正の取り込みが、返信・決着・まとめを送り、修正を送信して照合する | AC7〜AC9 | -| 4 | 同じものを二度書かない照合を、4 種別すべてに掛ける | AC10〜AC14 | -| 5 | 申告と実数の突き合わせをやめ、送信の応答を記録にする | AC15・AC32 | -| 6 | 起動し直しを初回と同じ経路へ通す | AC18〜AC20 | -| 7 | `fix` の書き込みを 1 つの実装にまとめる | AC21・AC22 | -| 8 | 文書の決定を書き直す | AC25〜AC29 | - -## なぜ変えるか - -**書き込みと記録が別の相手にあるため、片方だけが残る状態を作れる。** 担当は GitHub へ投稿し、その後に結果ファイルを書く。この 2 つの間で担当が止まると、投稿は残り記録は残らない。レビューを回す側は記録しか見ないので、投稿があることを知らないまま同じ担当を起動する。 - -**待ち行列は用意されているのに、担当の投稿がそこを通らない。** 上限で拒まれた投稿を残して後から流す仕組みはレビューを回す側にあるが、担当が自分で送るため、上限に当たった投稿はその場で失われる。 - -**送信の報告を確かめる手段がない。** 修正の担当はブランチ名を指定して送信する。作業ツリーが切り離された頭で作られている場合、この指定では現在の頭が送られないまま終了コード 0 で終わる。取り込む側は報告をそのまま記録する。 - -書き込みを 1 か所へ集めると、この 3 つが同じ場所で解ける。投稿は待ち行列を通り、送信は現在の頭を指定して行い、送った結果がそのまま記録になる。 - -## 実測 - -2026-09-21、`origin/develop`(`c853691a`)で確かめた。 - -| 何を測ったか | 値 | 確かめ方 | -| --- | --- | --- | -| 投稿の待ち行列の種別 | 4 つ(`pr-comment` / `review-post` / `review-reply` / `thread-resolve`) | `post_queue.py` の `KINDS` | -| そのうち、積む側が実装されている種別 | 1 つ(`pr-comment`。`rotate-pr.sh:53` の 1 か所だけ) | `grep -rn -- "--kind" plugins/ndf` の結果から、手順・テスト・別スクリプトの行を除いた | -| レビューを回す側の `gh` の呼び出し | 12 か所。書き込みの引数(`--method` / `-X POST` / `mutation`)を持つ行は 0 | `grep -c '"gh"'` と `grep -n -- '--method\|-X POST\|mutation'` | -| 担当のプロンプトに書かれた投稿の指示 | 4 か所(`launch-reviewer.sh:95,158,204,206`) | `grep -n "gh api"` | -| 修正の担当が行う書き込み | 4 種類(返信・決着・まとめ・送信)。`fix/SKILL.md:294,311,328,363` ほか | `grep -n "gh api\|gh pr comment\|git push"` | -| 重なりが出た実行 | PR #578 の round 1。1 回目にインライン 4 件、起動し直した 2 回目に 1 件。記録に入ったのは 2 回目の 1 件だけ | #583 の本文 | - -**待ち行列の受け皿だけが先にあり、積む側が無い状態である。** レビューの投稿を積む呼び出しは、配布物のどこにも無い。送信の確認(`_confirm_flushed`)はレビューの投稿の種別を読む形で用意されている。 - -## 決定の記録 - -見出しは「何のために何を決めたか」を書く。判断の材料は各節の本文にある。 - -### 決定 1: 並行する設計と 1 つのファイルで競合しないよう、設計文書は親課題の名前で新設し、既存の設計文書の本体は書き換えない - -同じ時期に 4 つの設計が同じ既存文書を指している。本体を書き換えると、先にマージした側の記述が後から入る側の差分で戻る。**足すのは、置き換え先を指す段落だけにする。** 足す先は、置き換わる受け入れ条件を持つ要求の文書と、その確かめ方を持つ契約の文書の 2 本である。 前例は #727 / #728 / #729 / #732 の 4 つである。 - -### 決定 2: 片方だけが残る状態を作らないため、GitHub と git への書き込みをレビューを回す側だけが行う - -担当は指摘の控えと結果ファイルを書き、レビューを回す側がそこから投稿を組み立てて送る。修正の担当はコミットまでを行い、取り込む側が送信する。 - -採らない案と理由: - -- **担当の投稿の後に、レビューを回す側が実物を照合して記録を補う。** 照合は GitHub への問い合わせを増やし、問い合わせが上限で失敗すると同じ食い違いが残る。書き込みを増やさずに読み取りを増やしても、原因の場所は動かない -- **担当に待ち行列へ積ませ、レビューを回す側が流す。** 担当は自分の作業領域しか持たず、待ち行列はレビューを回す側の作業ツリーにある。置き場所を担当へ開くと、巻き直しで捨てる範囲が変わる - -### 決定 3: 投稿の本文をレビューを回す側の応答へ載せないため、投稿は結果ファイルを読んだプロセスの中で組み立てる - -取り込みの部分命令が、控えのファイルを読み、投稿の項目を組み立て、待ち行列へ積み、流すところまでを 1 つのプロセスで行う。**部分命令の引数に本文を置かず、標準出力にも本文を出さない。** 応答に出るのは件数・URL・状態だけである。 - -採らない案と理由: - -- **控えの本文を引数や標準入力で渡す。** 収束ループを駆動している側の応答に本文が載る。文脈の予算の決めに反する -- **待ち行列のコマンドに種別ごとの引数を足して、手順から呼ぶ。** 引数の数が種別ごとに増え、手順の行が長くなる。組み立てはプロセスの中に閉じるほうが、手順の行が 1 本で済む - -### 決定 4: 投稿と記録の間に新しい食い違いを作らないため、「読んで記録する」と「投稿する」を 1 つの部分命令に閉じる - -取り込みの部分命令の名前と、骨組みの行の並びは変えない。中の順序を「控えを読む → 投稿を積む → 流す → 送信の応答を記録へ書き戻す → 指摘を取り込む」にする。 - -**別の部分命令に分けない。** 分けると「投稿したが記録していない」に加えて「記録したが投稿していない」がもう 1 つ増える。1 つに閉じれば、途中で止まった状態は「送れていない」か「送れたが記録が無い」の 2 つになる。 - -**止まり方は 2 つで、立て直し方が違う。** 送信に成功した項目は記録より先に待ち行列から取り除かれるため、**待ち行列に項目が残ることに頼らない。** - -| 止まった場所 | 待ち行列の項目 | 立て直し | -| --- | --- | --- | -| 送る前(上限などで送れていない) | 残る | 判定の終了コード 8 の枝が流し直す | -| 送った後・記録の前 | 残らない | 取り込みをもう一度呼ぶ。結果ファイルが残るため投稿を組み立て直せ、照合が先客を見つけるので増えない | - -採らない案と理由: - -- **送信の応答を控えてから項目を消す形へ、待ち行列の流し方を変える。** すでに動いている投稿の経路(巻き直しのコメント)の契約まで変わる。取り込みをもう一度呼べば同じ状態になるため、変えずに済む - -### 決定 5: 途中で止まった実行を流し直しても増やさないため、二度書かない照合を 4 種別すべてに掛ける - -投稿の待ち行列は、送る前に同じものが先にあるかを照合する仕組みを持つ。レビューの投稿・返信・決着・まとめの 4 種別すべてでこれを通す。**照合の鍵は、本文の先頭行のうちラウンドと席までの前方一致とする。** 判定の語(`APPROVE` / `REQUEST_CHANGES` / `COMMENT`)と、レビューの状態を鍵に含めない。含めると、起動し直して判定が変わったときに別の投稿と読まれ、二重に投稿する。 - -| 種別 | 照合の鍵 | -| --- | --- | -| `review-post` | 投稿者と、本文の先頭行の `## 🤖 cross-review \| round \| <席> \|` までの前方一致(判定の語を含めない)。ラウンドの開始時刻を持つときは、それ以降に出たレビューに限る | -| `review-reply` | 返信先の指摘の識別子と、本文の先頭 80 文字 | -| `thread-resolve` | スレッドの識別子と、すでに決着しているかどうか | -| `pr-comment` | 投稿者と、本文の先頭 80 文字(ラウンドを含む) | - -投稿者はどの席でも同じになる。ラウンドと席を先頭行に持たせることで、同じ投稿者の別の投稿と区別できる。 - -**レビューの照合は、そのラウンドが始まった時刻(状態ファイルの `rounds[-1].started_at`)以降に出たレビューに限る。** ラウンドの番号は実行ごとに 1 から数え直すため、収束した PR へ回し直すと、ラウンドと席だけの鍵が前の実行のレビューに一致し、新しい指摘を送らずに前の実行の投稿を送れた先として記録する。開始時刻で絞っても同じ実行の中の送り直しは従来どおり見つかり、開始時刻かレビューの時刻を読めないときはラウンドと席だけの照合へ落とす。 - -### 決定 6: 投稿済みで記録なしの状態が起きなくなるため、投稿済みのレビューを探して記録だけの起動をする決めを取り下げる - -担当が投稿しなくなるため、探す対象が無い。**GitHub からレビューを探す照会も、記録だけを行うプロンプトも作らない。** 止まった担当を起動し直すときは、初回と同じプロンプトをそのまま使う。 - -この決めは、担当が投稿する前提のうえで、投稿済みの担当をもう一度投稿させないために置かれていた。前提のほうを変えるため、対処のほうは要らなくなる。 - -### 決定 7: 起動し直した担当の指摘が根拠の検証と反証を通るよう、起動し直しを初回と同じ経路へ通す - -骨組みの起動し直しの枝を、繰り返しの先頭へ戻す形にする。繰り返しは「起動 → 待ち → 取り込み → 根拠の検証 → 反証 → 判定」である。**経路が 1 本なら、証拠の集約を飛ばす枝が構造として無くなる。** 待ち行列に残りがあるときの枝(判定の終了コード 8)は、2 度目を含む各判定の直後に置き、7 の判定より先に見る順序を保つ。 - -### 決定 8: 申告と実物の食い違いを無くすため、申告件数と実数の突き合わせをやめ、送信の応答を記録にする - -投稿するのがレビューを回す側になるため、申告を受け取る相手がいない。記録に入る投稿の URL は送信の応答から取り、件数は送信に成功したインラインの数から取る。**レビューの投稿の応答は件数の項目を持たない。** **申告件数を GitHub の実数と比べて中断する処理は取り除く。** - -この突き合わせは、担当が投稿したことを確かめるために置かれていた。投稿する側と記録する側が同じになるため、確かめる対象が無くなる。 - -### 決定 9: 差分の外を指す指摘を落とさないため、拒まれた指摘は投稿する側が本文へ退避する - -インラインの投稿が差分の外を理由に拒まれたとき(HTTP 422)、投稿する側がその指摘を本文の末尾へ移し、レビューをもう一度送る。退避した指摘の `posted_to` は `body` として記録する。**退避した件数を取り込みの出力に出す。** - -**HTTP 422 のすべてを退避の契機にしない。** 応答の本文が行を解決できないことを示すときだけ退避する。それ以外の 422(判定の値の誤り、基準のコミットの誤りなど)は失敗として残す。区別しないと、別の不具合が退避として飲み込まれる。 - -採らない案と理由: - -- **投稿の前に、差分に含まれる行かどうかを判定して振り分ける。** 差分の範囲を別に取り寄せる必要があり、取り寄せが失敗したときの分岐が増える。拒まれてから退避すれば、判定の根拠は応答そのものになる - -### 決定 10: 送ったという報告と実物が食い違わないよう、送信は現在の頭を指定して行い、送った後に照合する - -修正の送信は取り込む側が `git push origin HEAD:<ブランチ名>` で行う。**ブランチ名だけを指定しない。** 作業ツリーが切り離された頭で作られている場合、ブランチ名だけの指定では現在の頭が送られないまま終了コード 0 で終わる。 - -送信の後、修正の結果ファイルが報告するコミットが、送り先のブランチの履歴に含まれることを確かめる。含まれなければ取り込みは失敗として止まる。 - -### 決定 11: 起動の経路で書き込みの担い手が変わらないよう、修正の書き込みを 1 つの共通層にまとめる - -返信・決着・まとめの投稿・送信の実装を、共通層の 1 つのまとまりに置く。置き場所は `plugins/ndf/scripts/lib/result_posts.py` である。cross-review から呼ぶときは修正の取り込みがこれを呼び、単独で使うときは同じまとまりを部分命令として直接呼ぶ。 - -```bash -python3 "$SCRIPTS/lib/result_posts.py" fix --repo <所有者>/<リポジトリ> --pr <番号> \ - --result <結果ファイル> --head <ブランチ名> --worktree <作業ツリー> -``` - -**待ち行列の置き場所は渡さない。** 作業ツリーの下の決まった名前から導く。リポジトリと作業ツリーを省いたときは、いまいるディレクトリから引く。 - -**新しい入口のスクリプトを足さない。** 待ち行列のまとまりが取り込み用の口と部分命令の口の両方を持つ形に前例がある。手順に書く行は 1 本で済み、実装は 1 つになる。 - -### 決定 12: 読み取り側が途中の内容を読まないよう、担当は結果を一時の名前で書いてから改名する - -担当は結果ファイルと指摘の控えを一時の名前で書き、書き終えてから正式の名前へ改名する。**改名の順序は、控えが先、結果ファイルが後である。** 結果ファイルが正式の名前で現れたことが、2 つとも揃った印になる。控えだけが正式の名前で結果ファイルが無い状態は、結果なしとして扱う。**読めた状態は書き終えた状態である**ことが成り立つため、取り込み側は内容の途中を読むことがない。担当が途中で止まれば正式の名前のファイルは現れず、結果なしとして扱われる。 - -### 決定 13: すでに 1 か所にある操作を動かさないため、PR の巻き直しの開閉は移さない - -PR の締めと作り直しと再開はすでにレビューを回す側が行っている。待ち行列に積まず、上限のときは待って同じ操作をやり直す形である。**この性質を変えない。** 巻き直しは順序が意味を持ち、待ち行列へ積むと後続の投稿と並び替わる。 - -### 決定 14: 自分の Pull Request への投稿が拒まれないよう、判定の格下げを投稿する側が送信の時点で行う - -自分の Pull Request には変更を求めるレビューを送れない(HTTP 422)。担当が投稿していたときはその場で格下げしていたため、投稿する側が引き取る。**格下げるのは送った形だけで、本来の判定は落とさない。** 収束の判定は本来の判定を読むため、格下げがループの続き方を変えない。 - -## データ構造 - -### 結果ファイル(レビュー) - -| 項目 | 変更前 | 変更後 | -| --- | --- | --- | -| `event` | 担当が書く | 変わらない | -| `posted_as` | 担当が書く(`APPROVE` / `REQUEST_CHANGES` / `COMMENT`) | 投稿する側が、送信の時点で自分の Pull Request かどうかを見て決める | -| `comments_count` | 担当が申告する | 投稿する側が、送れたインラインの件数で埋める | -| `review_url` | 担当が投稿の応答から書く | **担当は書かない。** 投稿する側が送信の応答から書く | -| `by_severity` | 担当が書く | 変わらない | -| `post_error` | 担当が書く | **無くなる。** 投稿の失敗は待ち行列の側に残る | - -### 指摘の控え - -形は変えない。`posted_to` の決め方だけが変わる。 - -| 項目 | 変更前 | 変更後 | -| --- | --- | --- | -| `path` / `line` / `body` / `severity` | 担当が書く | 変わらない | -| `evidence` / `falsification` / `suggested_check` | 担当が書く | 変わらない | -| `posted_to` | 担当が、自分が投稿した先を書く | **投稿する側が、送れた先を書く**(`inline` / `body`) | - -### 記録(ラウンドごとの担当の欄) - -**この表は足す鍵と書き方が変わる鍵だけを並べる。** 収束の判定が読む既存の鍵(`intent` / `posted_as` / `by_severity`)はそのまま残る。 - -| 鍵 | 何が入るか | -| --- | --- | -| `review_url` | 送信の応答が返した URL。流し直しで先客が見つかったときは、その先客の URL | -| `queued` | 上限などで送れず待ち行列に残っているとき、真になる | -| `posted_inline` | インラインとして送れた件数 | -| `posted_body` | 差分の外を理由に本文へ退避した件数 | - -**`prior_review_url` の鍵は作らない**(決定 6)。 - -### 修正の結果ファイル - -形は変えない。取り込む側が読む項目と、それを何に使うかだけを決める。 - -| 項目 | 取り込む側が何に使うか | -| --- | --- | -| `fix_commit` | 送信の後に、送り先の履歴に含まれることを確かめる(決定 10) | -| `resolved_threads` | 決着の投稿を積む | -| `deferred` / `rejected` | 返信の投稿を積む | -| `summary_comment_url` | **担当は書かない。** 投稿する側が、まとめの投稿の応答から書く | - -## 入出力の契約 - -### 結果ファイルを投稿へ変える層 - -| 関数 | 入力 | 出力 | -| --- | --- | --- | -| `review_posts(payload_path, result_path, repo, pr, round_no, seat, head_sha, is_own_pr)` | 控えと結果ファイルのパス、および自分の Pull Request かどうか | 待ち行列へ積む項目の列 | -| `fix_posts(result_path, repo, pr)` | 修正の結果ファイルのパス | 待ち行列へ積む項目の列 | -| `push_fix(worktree, head_branch, fix_commit)` | 作業ツリーと送り先とコミット | 送信の結果と照合の可否 | - -**本文は引数として渡さない。** どの関数もファイルのパスを受け取り、本文はまとまりの中だけで扱う。 - -**判定の格下げは、自分の Pull Request かどうかを受け取ったこの層が決める**(決定 14)。呼ぶ側は状態ファイルが持つ値をそのまま渡す。 - -### 取り込みの標準出力 - -```text -POSTED review_url=https://github.com/.../pull/793#pullrequestreview-... -INLINE=7 BODY=1 QUEUED=0 -FINDINGS=8 -``` - -本文は出さない。上限で送れなかったときは `QUEUED` が 1 以上になり、判定の終了コード 8 の枝が流し直す。 - -### 変える出力と変えない出力 - -**終了コードは変えない。** 取り込み・判定・報告のどれも、変更前と同じ値を返す。 - -**収束の判定と報告が読む変数も変えない**(`REVIEWER_INTENTS` / `NEW_FINDINGS` / `CARRIED_OVER_THREADS` / `PENDING_POSTS`)。 - -**変わるのは取り込みの標準出力だけである。** 投稿の結果を表す行(`POSTED` / `INLINE` / `BODY` / `QUEUED`)と、取り込んだ指摘の件数(`FINDINGS`)を足す。判定が出す `NEW_FINDINGS` はこれとは別の変数で、変えない。 - -## 構成要素 - -**動くもの**(「処理の流れ」の図と手順に現れる 5 つ): - -| 構成要素 | 変える内容 | -| --- | --- | -| `plugins/ndf/scripts/lib/result_posts.py` | **新設。** 結果ファイルから投稿の項目を組み立て、送信と照合を行う。部分命令の口も持つ | -| `plugins/ndf/scripts/lib/post_queue.py` | レビューの投稿と返信と決着の照合の鍵を、決定 5 の表に合わせる。拒まれたときの区別(HTTP 422)を返す | -| `plugins/ndf/skills/cross-review/scripts/launch-reviewer.sh` | プロンプトから投稿の手順を外す。控えと結果ファイルを一時の名前で書いてから改名させる | -| `plugins/ndf/skills/cross-review/scripts/state.py` | 指摘の取り込みが投稿を行う。申告と実数の突き合わせを取り除く。修正の取り込みが返信・決着・まとめと送信を行う | -| `plugins/ndf/skills/cross-review/SKILL.md` | 設計方針の表の投稿の行。起動し直しの枝を繰り返しの先頭へ戻す | - -**書き直す文書**(振る舞いを持たないため、流れの図には現れない): - -| 文書 | 変える内容 | -| --- | --- | -| `plugins/ndf/skills/cross-review/docs/02-fix-and-rotation.md` | 修正の手順から担当の送信の行を外す | -| `plugins/ndf/skills/cross-review/docs/03-review-output.md` | 直接投稿の決定を待ち行列を通す形へ書き直す | -| `plugins/ndf/skills/cross-review/docs/04-contracts.md` | 投稿の種別ごとの契約を載せる | -| `plugins/ndf/skills/cross-review/references/context-budget.md` | 工夫の一覧の 4 番目を書き直す | -| `plugins/ndf/skills/fix/SKILL.md` | 返信・決着・まとめ・送信の手順を、共通層を呼ぶ 1 行へ置き換える | - -## 置き場所 - -**結果ファイルを投稿へ変える層は共通層に置く。** cross-review と `fix` の両方から呼ぶためである。cross-review の配下に置くと、単独で `fix` を使う経路が cross-review に依存する。 - -**待ち行列の保存先は変えない。** レビューを回す側の作業ツリーの中にあり、巻き直しのときは作業ツリーごと捨てられる。 - -## 処理の流れ - -### 1 ラウンドのレビュー - -```mermaid -sequenceDiagram - participant M as レビューを回す側 - participant A as 担当 - participant G as GitHub - M->>A: 起動(投稿の手順を持たないプロンプト) - A->>A: 指摘の控えと結果を書く - A-->>M: 終了 - M->>M: 控えを読み、投稿を待ち行列へ積む - M->>G: レビューを送る - G-->>M: レビューの URL - M->>M: 送れたインラインを数え、URL と件数を記録し、指摘を取り込む -``` - -担当が控えを書く前に止まれば、レビューを回す側は積むものを持たないため、GitHub には何も増えない。起動し直しても重ならない。 - -### 途中で止まった実行の立て直し - -```mermaid -graph TD - A[取り込みが投稿を送る] --> B{送れたか} - B -->|送れていない| D[待ち行列に項目が残る] - D --> E[判定の 8 の枝が流し直す] - B -->|送れた| C{記録まで済んだか} - C -->|済んだ| J[次の段へ進む] - C -->|止まった| K[取り込みをもう一度呼ぶ] - E --> F{先客がいるか} - K --> F - F -->|いる| G[送らずに
先客の URL を記録する] - F -->|いない| H[送って記録する] -``` - -### 修正の取り込み - -1. 修正の担当がコミットまでを行い、結果ファイルを書く -2. 取り込む側が現在の頭を指定して送信する -3. 報告されたコミットが送り先の履歴に含まれることを確かめる。含まれなければ止まる -4. 返信・決着・まとめの投稿を待ち行列へ積んで流す -5. 投稿の応答(まとめのコメントの URL など)を記録へ書き戻す - -## 非機能の実現方式 - -| 条件 | どう満たすか | -| --- | --- | -| 応答の量 | 取り込みの標準出力を件数・URL・状態だけにする。本文はまとまりの中だけを通る | -| 上限への耐性 | 送れなかった投稿は待ち行列に残り、判定の終了コード 8 の枝が流し直す。担当をもう一度起動しない | -| 所要 | 担当のプロンプトから投稿の手順が消えるぶん、担当の実行が短くなる。監視の上限は変えない | -| 権限 | 担当の CLI に GitHub への書き込みの権限が要らなくなる | - -## テスト設計 - -| 受け入れ条件 | 何で確かめるか | -| --- | --- | -| AC1・AC2 | 担当のプロンプトを取り出し、投稿の手順の語が 0 件であることを見る | -| AC3 | 控えだけを正式の名前で置き、結果ファイルが無い状態で取り込みを呼び、結果なしとして扱われ、投稿が 0 件であることを見る | -| AC4 | 控えを書かずに終わる偽の担当で 1 ラウンドを回し、偽の `gh` が受けたレビューの投稿が 0 件であることを見る | -| AC5・AC6 | 取り込みの標準出力に、控えの本文の文字列が含まれないことを見る | -| AC7 | 修正の取り込みを呼び、返信・決着・まとめの 3 種別が待ち行列へ積まれることを見る | -| AC8・AC9 | 送信を偽装し、報告されたコミットが送り先に無いときに失敗することを見る | -| AC10・AC11 | 偽の `gh` が先客を返す状態で投稿を積んで流し、新しい投稿が 0 件で項目が取り除かれることを見る | -| AC12 | 投稿の後・記録の前で止めた状態から取り込みをもう一度呼び、レビューが増えず記録に先客の URL が入ることを見る | -| AC13・AC14 | 返信と決着とまとめについて、同じ項目を 2 度積んでも増えないことを見る | -| AC15 | 記録に入る URL が偽の `gh` の応答の値と一致し、件数が実際に送ったインラインの数と一致することを見る | -| AC16 | 偽の `gh` が HTTP 422 を返す状態で、応答の本文が行を解決できないことを示すときだけ本文へ退避して送り直し、示さないときは失敗として止まることを見る | -| AC17 | 控えに 1 件あり、インラインが 422 で全件退避された実行で、結果なしにならないことを見る | -| AC18・AC19 | 骨組みの行の並びを読む既存のレイアウトの検査へ条件を足す | -| AC20 | 送信の後・記録の前で取り込みを止め、同じ取り込みをもう一度呼ぶ結合の経路で、レビューが 1 件しか増えないことを見る | -| AC21・AC22 | 共通層の部分命令を直接呼び、cross-review から呼んだときと同じ項目が積まれることを見る | -| AC23・AC24 | 終了コードと、収束の判定・報告が読む変数を見る既存のテストを変えずに通す | -| AC25〜AC29 | 変更した文書の行を読む検査 | -| AC32 | 自分の Pull Request の状態で投稿を組み立て、送った形が `COMMENT` で本来の判定が変わらないことを見る | -| AC30・AC31 | 検証手段の表のコマンド | - -**偽の `gh` を使う形は既存のテストにある**(待ち行列の照合と巻き直しの投稿)。同じ仕掛けを使う。 - -## 実測で決めた 4 件 - -設計の時点で未確認だった 4 件を、2026-09-22 に実物の `gh` で 1 度ずつ動かして決めた。対象は -この束の設計を載せた Pull Request #794(マージ済み、head `233f28ba`)である。拒まれた要求は -何も作らないため、確かめた跡は残っていない(レビューとレビューのコメントを引いて 0 件)。 - -### 差分の外を指す指摘が拒まれるときの応答の形 - -レビューの作成は**要求ごとに全件が拒まれる**。正しいインラインを混ぜても、一部だけが作られる -ことはない。終了コードは 1 である。 - -| 何を送ったか | 応答の `errors` | -| --- | --- | -| 差分に無いファイルのインライン 1 件 | `["Path could not be resolved"]` | -| 差分にあるファイルの、塊の外の行のインライン 1 件(正しいインライン 1 件と同時) | `["Line could not be resolved"]` | -| 塊の外・差分に無いファイル・塊の外の 3 件 | `["Line could not be resolved, Path could not be resolved, and Line could not be resolved"]` | -| 判定の値に知らない語 | `["Variable $event of type PullRequestReviewEvent was provided invalid value"]` | -| 基準のコミットに存在しない値 | `["The commitOID is not part of the pull request"]` | - -**退避の契機に使う語は `could not be resolved` である。** 判定の値の誤りと基準のコミットの誤りは -この語を含まないため、決定 9 のとおり失敗として残せる。 - -### 拒まれた応答から、どのインラインが原因かを 1 件ずつ特定できるか - -**特定できない。** 応答は語を `, ` と `and` でつないだ 1 つの文字列で、位置も識別子も持たない。 -拒まれた件数は語の数から読めるが、正しいインラインを混ぜると位置が対応しない。 - -**決定 9 の落とし先を採る。** 行やファイルを解決できないことを理由に拒まれた要求は、その要求の -インラインをすべて本文へ退避して送り直す。1 ラウンドの指摘は多くて 10 件前後、1 件の本文は -数百文字のため、すべてを退避しても数 KB に収まり、投稿の本文の上限(65536 文字)に対して余裕がある。 - -### すでに決着したスレッドをもう一度決着させたときの応答 - -**冪等である。** 決着の操作は成功し(終了コード 0)、決着済みであることを返す。失敗にならない。 -決定 5 の照合の鍵(スレッドの識別子と、すでに決着しているかどうか)が送信を止め損ねても、 -二重の決着が失敗にはならない。 - -### 投稿者のアカウントが席ごとに違う環境があるか - -**無い。** 作業環境の `gh` は 1 アカウントだけを持ち、配布物の中に席ごとの認証を切り替える口は -無い(`plugins/ndf/` を認証の環境変数で引くと 2 行あり、どちらも別の目的の注釈である)。 -要求の前提 1(投稿者アカウントは実質 1 つ)はそのまま成り立ち、照合の鍵に投稿者を含める形を変えない。 - -## 申し送り(並行する設計との境界) - -この束は並行する設計の中で **G5** と呼ぶ(#727 が G1、#732 が G2、#729 が G3、#728 が G4)。 - -| 相手 | 決めた契約 | -| --- | --- | -| #727(G1、PR #793) | 席の名前と、結果ファイル・控えの名前が席で組まれること、レビューの本文の先頭行が席を持つことを**この設計が前提にする**。決め方は G1 が持つ。触るファイル(`state.py` / `launch-reviewer.sh` / `SKILL.md`)が重なるため、後からマージする側が競合を解く | -| #729(G3、マージ済み) | 結果なしの理由の語彙を変えない。**投稿済みのレビューを探す鍵(`prior_review_url`)は作らない**ため、G3 が残した「鍵が無い = 無かった」の書き方に足すものは無い | -| #732(G2、PR #790) | 指摘の中身と数え方に触らない。#583 の収束の部分は G2 が塞いだ | -| #728(G4) | 触るファイルが重ならない | -| 子課題 #548 #350 #585 #676 | この設計で根本が動くため、現象が出なくなる見込みがある。**受け入れ条件は持たず、閉じるのは棚卸に任せる**(要求の「影響」) | - -## 置き換える既存の設計との対応 - -**この文書は[置き換える前の設計](issue-662-598-537-619-584-583-design.md)の決定 18・19 を置き換える。** 既存の設計文書そのものは書き換えない。置き換え先を指す段落を足す先は、次の 2 本である(決定 1)。 - -| 足す先 | 何を持つ文書か | -| --- | --- | -| [置き換える前の要求](issue-662-598-537-619-584-583-requirements.md) | 置き換わる受け入れ条件(AC63〜AC67) | -| [置き換える前の契約](issue-662-598-537-619-584-583-design-contracts.md) | その確かめ方 | - -| 既存の決定 | この文書 | 扱い | -| --- | --- | --- | -| 決定 18(投稿済みのレビューを持つ担当は、記録だけを行う形で起動し直す) | 決定 2・6 | **取り下げる。** 担当が投稿しなくなるため、対処の前提が無くなる | -| 決定 19(起動し直した担当を、初回と同じ経路に通す) | 決定 7 | 引き継ぐ | - -## 関連する文書 - -この文書は「どう作るか」だけを扱う。 - -| 文書 | 何を持つか | -| --- | --- | -| [issue-730-583-requirements.md](issue-730-583-requirements.md) | 何を満たすか(目的・対象範囲・用語・受け入れ条件 AC1〜AC32) | -| [issue-662-598-537-619-584-583-design.md](issue-662-598-537-619-584-583-design.md) | 置き換える前の設計(決定 18・19) | -| [issue-729-619-584-design.md](issue-729-619-584-design.md) | 結末の語彙と、止めた担当に書かせないこと | -| [issue-732-624-706-design.md](issue-732-624-706-design.md) | 数えない指摘の区分と収束の判定 | -| [issue-727-687-478-664-648-design.md](issue-727-687-478-664-648-design.md) | 席の決め方と再開の引数 | diff --git a/issues/issue-730-583-plan.md b/issues/issue-730-583-plan.md deleted file mode 100644 index d9ab65e5d..000000000 --- a/issues/issue-730-583-plan.md +++ /dev/null @@ -1,185 +0,0 @@ -# cross-review: PR に出ている指摘が記録に残らず、同じ論点が 2 つのスレッドに分かれる → GitHub と git へ書くのをレビューを回す側だけにし、途中で止まっても二度書かない(#730 #583) - -## 関連リンク - -| 文書 | 何を持つか | -| --- | --- | -| [issue-730-583-requirements.md](issue-730-583-requirements.md) | 何を満たすか(受け入れ条件 AC1〜AC32) | -| [issue-730-583-design.md](issue-730-583-design.md) | どう作るか(決定 1〜14・データ構造・入出力の契約) | -| #730 | 根本原因の親課題 | -| #583 | 投稿の重なりと起動し直しの経路 | - -## モード - -`standard`。レビューを回す仕組みの振る舞いを変え、複数の実行単位にまたがる。 - -## 目的と非目的 - -達成したい状態: - -- レビューを任された担当が途中で止まっても、PR に出ている指摘と記録が食い違わない -- 起動し直しても同じ論点のスレッドが 2 つに分かれない -- 修正を送ったという報告と、送り先のブランチの実物が一致する - -やらないこと: - -- 収束の判定・指摘の数え方・区分を変える(別の束が持つ) -- 席の決め方と再開の引数を変える(別の束が持つ) -- PR の巻き直しの締め・作り直し・再開の手順を変える(設計の決定 13) -- 子課題 #548 #350 #585 #676 の個別の受け入れ条件を立てる(根本の修正で現象が出なくなる見込みを要求へ書き、閉じるのは棚卸に任せる) - -## 用語の対応 - -**説明は業務用語で通す。** 識別子は設計文書の「用語の対応表」で引く。この計画で使う語だけを再掲する。 - -| 業務用語 | 実体 | -| --- | --- | -| レビューを回す側 | `plugins/ndf/skills/cross-review/scripts/state.py` と、それを呼ぶ `SKILL.md` の骨組み | -| 担当 | 1 ラウンドで 1 者ぶんのレビューを行う CLI | -| 席 | そのラウンドで担当が入る枠 | -| 指摘の控え | 担当が書く、指摘 1 件ごとのファイル | -| 結果ファイル | 担当が書く、判定の要約のファイル | -| 投稿の待ち行列 | `plugins/ndf/scripts/lib/post_queue.py` | -| 結果ファイルを投稿へ変える層 | 新設する `plugins/ndf/scripts/lib/result_posts.py` | -| 指摘の取り込み | `state.py read-result` | -| 修正の取り込み | `state.py merge-fix` | - -## 前提 - -- 前提 1: レビューの投稿者アカウントは 1 つである。作業環境の `gh` の認証は 1 アカウントで、席ごとに切り替える口は配布物に無い(実測で確かめた。「実測で決めたこと」の 4 番目) -- 前提 2: 担当が結果を書けずに止まったとき、その担当は何も投稿していない。投稿の手順をプロンプトから外すため、変更の後に成り立つ -- 前提 3: 修正の担当が働く作業ツリーは、取り込む側から同じパスで見える -- 前提 4: 待ち行列は作業ツリーの中にあり、巻き直しのときは作業ツリーごと捨てられる - -## 実測で決めたこと - -設計文書が「未確認のまま残ること」として残した 4 件を、実物の `gh` で 1 度ずつ動かして決めた。 -実測の記録は設計文書の同じ節へ書き戻す(同じ Pull Request に含める)。 - -| 何を | 決めたこと | -| --- | --- | -| 差分の外を指す指摘が拒まれるときの応答の形 | 要求ごとに全件が拒まれ、一部だけが作られることはない。判定に使う語は `could not be resolved` である。判定の値の誤りと基準のコミットの誤りはこの語を含まないため、失敗として残せる | -| 拒まれた応答から原因を 1 件ずつ特定できるか | できない。応答は語をつないだ 1 つの文字列で、位置も識別子も持たない。設計の落とし先(拒まれた要求のインラインをすべて本文へ退避する)を採る | -| すでに決着したスレッドをもう一度決着させたときの応答 | 冪等である。成功し、決着済みを返す。失敗にならない | -| 投稿者のアカウントが席ごとに違う環境があるか | 無い。前提 1 のとおり | - -## 受け入れ条件 - -**一覧は要求の文書が持つ**(AC1〜AC32)。この計画では、タスクごとに満たす番号を示す。 - -## 互換性 - -| 対象 | 変更 | 互換性の扱い | -| --- | --- | --- | -| 取り込み・判定・報告の終了コード | 変えない | AC23 | -| 収束の判定と報告が読む変数 | 変えない | AC23 | -| 取り込みの標準出力 | 投稿の結果を表す行と、取り込んだ指摘の件数を足す | 追加のみ | -| 結果ファイルの項目 | 担当が書く項目を減らし、投稿する側が埋める項目を増やす | 担当の側の契約が変わる。プロンプトと同じ変更に含める | -| 待ち行列の保存先 | 変えない | 前提 4 | -| 席の名前・結末の語彙・数えない指摘の区分 | 変えない | AC24 | - -## 修正対象 - -| ファイル | 扱い | -| --- | --- | -| `plugins/ndf/scripts/lib/result_posts.py` | 新設 | -| `plugins/ndf/scripts/lib/post_queue.py` | 変更 | -| `plugins/ndf/skills/cross-review/scripts/state.py` | 変更 | -| `plugins/ndf/skills/cross-review/scripts/launch-reviewer.sh` | 変更 | -| `plugins/ndf/skills/cross-review/SKILL.md` | 変更 | -| `plugins/ndf/skills/cross-review/docs/02-fix-and-rotation.md` | 変更 | -| `plugins/ndf/skills/cross-review/docs/03-review-output.md` | 変更 | -| `plugins/ndf/skills/cross-review/docs/04-contracts.md` | 変更 | -| `plugins/ndf/skills/cross-review/references/context-budget.md` | 変更 | -| `plugins/ndf/skills/fix/SKILL.md` | 変更 | -| `issues/issue-730-583-design.md` | 「未確認のまま残ること」を実測の結果へ書き直す | -| 各ランタイムの配布物 | 生成の実行で揃える | - -## タスク分解 - -機能単位で分ける。各タスクは、失敗するテストを先に書いてから通す。 - -### Task 1: 二度書かない照合を 4 種別すべてへ広げ、拒まれ方を区別して返す - -- **対象ファイル:** `plugins/ndf/scripts/lib/post_queue.py` -- **変更内容:** 先客がいるかを見る照合の鍵を、レビューの投稿・返信・決着・まとめの 4 種別それぞれに与える。レビューの投稿の鍵は本文の先頭行のうちラウンドと席までの前方一致とし、判定の語を含めない。送信が拒まれたとき、行やファイルを解決できないことを示す応答と、それ以外の拒まれ方を呼ぶ側が見分けられる形で返す -- **満たす受け入れ条件:** AC10・AC11・AC13・AC14(照合)、AC16(拒まれ方の区別) -- **進め方:** 偽の `gh` で先客を返す状態を作り、同じ項目を 2 度積んでも増えないことを見るテストを先に書く - -### Task 2: 指摘の控えと結果ファイルからレビューの投稿を組み立てて送る層を新設する - -- **対象ファイル:** `plugins/ndf/scripts/lib/result_posts.py`(新設) -- **変更内容:** 控えと結果ファイルのパスを受け取り、待ち行列へ積む項目の列を返す。本文は引数にも標準出力にも出さない。自分の Pull Request かどうかを受け取り、送った形だけを格下げする。インラインが行を解決できないことを理由に拒まれたら、その要求のインラインを本文の末尾へ移して送り直し、移した指摘の宛先を本文として記録する -- **満たす受け入れ条件:** AC5・AC15・AC16・AC17・AC32 -- **進め方:** 偽の `gh` が拒む応答を返す状態で、本文へ移した件数と宛先の値を見るテストを先に書く - -### Task 3: 指摘の取り込みが投稿を行い、申告と実数の突き合わせをやめる - -- **対象ファイル:** `plugins/ndf/skills/cross-review/scripts/state.py` -- **変更内容:** 控えを読む → 投稿を積む → 流す → 送信の応答を記録へ書き戻す → 指摘を取り込む、の順で 1 回の呼び出しの中を進める。担当の申告件数を GitHub の実数と比べて中断する処理を取り除く。記録に入る投稿の URL は送信の応答から取り、件数は送れたインラインの数から取る。標準出力へ足すのは件数・URL・状態の行だけにする -- **満たす受け入れ条件:** AC5・AC6・AC12・AC15・AC17・AC23 -- **進め方:** 標準出力に控えの本文の文字列が含まれないことと、投稿の後・記録の前で止めた状態から呼び直してもレビューが増えないことを見るテストを先に書く - -### Task 4: 担当のプロンプトから投稿の手順を外し、書き終えてから改名で公開する - -- **対象ファイル:** `plugins/ndf/skills/cross-review/scripts/launch-reviewer.sh` -- **変更内容:** プロンプトから、レビューを投稿する手順(投稿の呼び出し・判定の値の指定・インラインの組み立て)を外す。担当が書くのは指摘の控えと結果ファイルの 2 つだけにする。どちらも一時の名前で書かせ、控えを先、結果ファイルを後の順で正式の名前へ改名させる -- **満たす受け入れ条件:** AC1・AC2・AC3・AC4 -- **進め方:** 組み立てたプロンプトを取り出し、投稿の手順の語が 0 件であることを見るテストを先に書く - -### Task 5: 修正の返信・決着・まとめと送信を 1 つの層にまとめ、単独の口を与える - -- **対象ファイル:** `plugins/ndf/scripts/lib/result_posts.py`・`plugins/ndf/skills/cross-review/scripts/state.py`・`plugins/ndf/skills/fix/SKILL.md` -- **変更内容:** 修正の結果ファイルから返信・決着・まとめの投稿を組み立てる口と、現在の頭を指定して送信する口を同じ層へ置く。送信の後、報告されたコミットが送り先のブランチの履歴に含まれることを確かめ、含まれなければ失敗として止まる。修正の取り込みはこの層を呼び、単独で使うときは同じ層を部分命令として直接呼ぶ。単独で使う手順は 1 行のコマンドへ置き換える -- **満たす受け入れ条件:** AC7・AC8・AC9・AC21・AC22 -- **進め方:** 送信を偽装し、報告されたコミットが送り先に無いときに失敗することを見るテストを先に書く - -### Task 6: 起動し直しを初回と同じ経路へ通す - -- **対象ファイル:** `plugins/ndf/skills/cross-review/SKILL.md` -- **変更内容:** 骨組みの起動し直しの枝を、繰り返しの先頭へ戻す形にする。待ち行列に残りがあるときの枝は、2 度目を含む各判定の直後に残す。設計方針の表から、担当が直接投稿する記述を外し、投稿の担い手とその理由を入れる -- **満たす受け入れ条件:** AC18・AC19・AC25 -- **進め方:** 骨組みの行の並びを読む既存の検査へ条件を足す - -### Task 7: 文書の決定を書き直す - -- **対象ファイル:** `docs/02-fix-and-rotation.md`・`docs/03-review-output.md`・`docs/04-contracts.md`・`references/context-budget.md`(いずれも `plugins/ndf/skills/cross-review/` の配下)・`issues/issue-730-583-design.md` -- **変更内容:** 修正の手順から担当が送信する行を外し、取り込む側が送信することを書く。担当が直接投稿する決定を、待ち行列を通す形へ書き直す。投稿の種別ごとの契約を載せる。文脈の予算の工夫の一覧を、本文がレビューを回す側のプロセスの中だけを通る形へ書き直す。設計文書の「未確認のまま残ること」を実測の結果へ書き直す -- **満たす受け入れ条件:** AC26・AC27・AC28・AC29 -- **進め方:** 文言の検査。振る舞いを持たないためテストは書かない - -### Task 8: 生成物を揃え、全体の検査を通す - -- **対象ファイル:** 各ランタイムの配布物 -- **変更内容:** 生成の実行で配布物を揃え、全体のテストと検査を通す -- **満たす受け入れ条件:** AC23・AC24・AC30・AC31 -- **進め方:** テストと検査のコマンドを実行し、終了コードを証跡として残す - -## 影響範囲 - -| 何が | どう変わるか | -| --- | --- | -| 担当の CLI | GitHub への書き込みの権限が要らなくなる。実行時間が短くなる | -| 収束の判定 | 変わらない。読む変数と終了コードを保つ | -| 修正を単独で行う経路 | 書き込みの実装が、レビューを回す経路と 1 つになる | -| 並行する束 | 席の決め方(PR #793、マージ済み)と同じファイルを触る。マージ済みのため競合は着手の時点で解けている | - -## リスクと対処 - -| リスク | 対処 | -| --- | --- | -| 変更が 1 ファイル 4983 行の取り込みの実装に集中する | タスクごとにテストを通す。構造の整理はこの変更の後の構造改善へ回す | -| 投稿の担い手を移す間に、送る経路が 2 つ並ぶ | 担当のプロンプトから手順を外す変更(Task 4)と、投稿を行う変更(Task 3)を同じ Pull Request に入れる | -| 拒まれ方の区別が実物の応答に合わない | 実測で確かめた語を契約へ書き、偽の応答をその形で作る | -| 本文へ退避した指摘で本文が長くなる | 1 ラウンドの指摘は多くて 10 件前後で、投稿の本文の上限に対して十分小さい(実測の記録に根拠を置く) | - -## 切り戻し手順 - -Pull Request の単位で戻せる。外部の系の状態を変える移行は無い。待ち行列の保存先を変えないため、 -途中まで積まれた項目は作業ツリーごと捨てられる。 - -## 完了の定義 - -- [ ] 受け入れ条件 AC1〜AC32 をすべて満たし、条件ごとに検証手段と結果が対応している -- [ ] `uv run --with pytest pytest scripts/tests plugins/ndf -q` が通る -- [ ] `bash scripts/build-runtime-plugins.sh --check`・`claude plugin validate .`・`python3 scripts/check-skill-frontmatter.py` が終了コード 0 で終わる diff --git a/issues/issue-730-583-requirements.md b/issues/issue-730-583-requirements.md deleted file mode 100644 index 4a60879c7..000000000 --- a/issues/issue-730-583-requirements.md +++ /dev/null @@ -1,246 +0,0 @@ -# cross-review: PR に出ている指摘が記録に残らず、同じ論点が 2 つのスレッドに分かれる → GitHub と git へ書くのをレビューを回す側だけにし、途中で止まっても二度書かない(#730 #583 の要求) - -## 目的 - -レビューを 1 者ぶん任された担当が、PR へ指摘を書き込んでから、回す側が読む結果を書く前に止まることがある。PR には指摘が出ているのに記録には無いため、回す側は「結果なし」と読んで同じ担当をもう一度起動し、同じ論点がもう一度投稿される。利用者は 1 つの論点に 2 つのスレッドを見て、両方へ返信する。修正の側でも、送ったという報告どおりにブランチが進んでいないことがある。 - -GitHub と git へ書くのをレビューを回す側だけにし、担当は結果を残すだけにする。途中で止まっても同じものを二度書き込まない。 - -## 用語と識別子の対応 - -| 業務用語 | 識別子・実体 | -| --- | --- | -| レビューを回す側 | `plugins/ndf/skills/cross-review/scripts/state.py` と、それを呼ぶ `SKILL.md` の骨組み。3 層では supervisor | -| 担当 | 1 ラウンドで 1 者ぶんのレビューを行う CLI。`launch-reviewer.sh` が起動する | -| 席 | そのラウンドで担当が入る枠。`claude` / `codex` / `agy` / `kiro` と、同じランタイムの 2 つ目の `-2`〜`-9`(#727) | -| 修正の担当 | 指摘を直すサブエージェント、または単独で動く `fix` の実行 | -| 結果ファイル | `<席>-review-pr<番号>-result.json`。判定の要約(`event` / `posted_as` / `comments_count` / `by_severity`) | -| 指摘の控え | `<席>-review-pr<番号>-round-payload.json`。指摘 1 件ごとの `path` / `line` / `body` / `severity` ほか | -| 修正の結果ファイル | `fix-pr<番号>-result.json`。`fix_commit` / `resolved_threads` / `deferred` / `rejected` ほか | -| 投稿の待ち行列 | `plugins/ndf/scripts/lib/post_queue.py`。種別は `pr-comment` / `review-post` / `review-reply` / `thread-resolve` | -| 二度書かないための照合 | `post_queue.already_posted()` / `posted_match()` | -| 結果ファイルを投稿へ変える層 | 新設する `plugins/ndf/scripts/lib/result_posts.py` | -| 指摘の取り込み | `state.py read-result` | -| 修正の取り込み | `state.py merge-fix` | -| 最終スイープの照合 | `state.py verify-sweep` | - -## 対象範囲 - -**含む。** - -| 何を | どう変わるか | -| --- | --- | -| レビューの投稿(本文とインライン) | 担当が `gh api` で直接送る → 担当は指摘の控えを書くだけ。レビューを回す側が待ち行列を通して送る | -| 指摘への返信とスレッドの決着 | 修正の担当が送る → 修正の結果ファイルを取り込む側が待ち行列を通して送る | -| 修正のまとめの投稿 | 同上 | -| 修正の送信 | 修正の担当が `git push origin <ブランチ名>` → 取り込む側が `git push origin HEAD:<ブランチ名>` | -| 申告と実数の突き合わせ | 担当の申告件数を GitHub の実数と比べる → 送った結果をそのまま記録にする | -| 投稿済みのレビューを探して記録だけの起動をする決め | 取り下げる(担当が投稿しないため、投稿済みで記録なしの状態が起きない) | -| 起動し直しの経路 | 初回と同じ経路(根拠の検証と反証を含む)へ通す | -| `fix` を単独で使う経路 | 書き込みの実装を cross-review から呼ぶ経路と 1 つにする | - -**含まない。** - -| 何を | なぜ | -| --- | --- | -| PR の巻き直しの close / create / reopen | すでにレビューを回す側が行う(`rotate-pr.sh`)。待ち行列に積まない性質も変えない | -| 反証の投稿 | 反証はもともと投稿しない(ファイルへ書くだけ) | -| 収束の判定・区分・数え方 | #732(G2)が持つ | -| 利用上限で止まった担当の報告と起動し直しの可否 | #729(G3)が持つ | -| 席の決め方・参加する者の選び方・再開の引数 | #727(G1)が持つ | -| cross-refactoring の書き込み | この束の外。`cross-refactoring` はすでに「公開するのは進行側だけ」である | -| 子課題 #548 #350 #585 #676 の個別の受け入れ条件 | マイルストーン 06。根本の修正で直る見込みは「影響」に書き、閉じるのは棚卸に任せる | - -## 前提 - -- **前提 1: レビューの投稿者アカウントは、いまも実質 1 つである。** 担当の CLI は同じ作業環境の `gh` の認証を使うため、投稿の担い手を変えても PR 上の投稿者は変わらない。どの席の指摘かは本文の先頭行が表す -- **前提 2: 担当が結果を書けずに止まったとき、その担当は何も投稿していない。** 投稿の手順をプロンプトから外すため、この前提は変更の後に成り立つ -- **前提 3: 修正の担当が働く作業ツリーは、取り込む側から同じパスで見える。** cross-review は状態ファイルが持つ作業ツリーを使い、単独の `fix` は現在の作業ツリーを使う -- **前提 4: 待ち行列は作業ツリーの中に置かれ、巻き直しのときは作業ツリーごと捨てられる。** 現行のとおりで変えない - -## 受け入れ条件 - -### 担当は結果だけを残す - -- [ ] AC1: 担当へ渡すプロンプトに、レビューを投稿する手順が 1 つも無い。 - 対象は `gh api` の呼び出し・`event` の指定・インラインの組み立ての 3 つである -- [ ] AC2: 担当へ渡すプロンプトは、指摘の控えと結果ファイルの 2 つだけを書かせる。 - 結果ファイルのうち担当が書くのは `event` と `by_severity` だけである。 - `posted_as` と `comments_count` と `review_url` は投稿する側が埋め、`post_error` は無くなる -- [ ] AC3: 担当は結果ファイルと指摘の控えを、一時の名前で書いてから改名する。 - 改名の順序は控えが先、結果ファイルが後である。 - 控えだけが正式の名前で結果ファイルが無い状態では、取り込みは結果なしとして扱い、投稿を 0 件にする -- [ ] AC4: 担当が指摘の控えを書かずに終わった実行では、その PR に新しいレビューが 1 件も増えない - -### 書き込みは 1 か所から行う - -- [ ] AC5: 指摘の取り込みは、指摘の控えからレビューの投稿を組み立て、待ち行列へ積み、流すところまでを 1 回の呼び出しで行う。投稿の本文は呼び出しの引数に現れない -- [ ] AC6: 取り込みの標準出力に、レビューの本文とインラインの本文が出ない。出るのは件数・URL・状態だけである -- [ ] AC7: 修正の取り込みは、返信・スレッドの決着・まとめの投稿を待ち行列へ積んで流す。修正の担当はこの 3 つを行わない -- [ ] AC8: 修正の送信は取り込む側が `git push origin HEAD:<ブランチ名>` で行う。修正の担当はコミットまでを行い、送らない -- [ ] AC9: 送信の後、報告されたコミットが送り先のブランチ(Pull Request の head)に載っていることを確かめる。載っていなければ取り込みは失敗として止まる - -### 途中で止まっても二度書かない - -- [ ] AC10: レビューの本文の先頭行は `## 🤖 cross-review | round | <席> | ` である。先頭 80 文字にラウンドと席が入る - 照合に使うのは席までの前方一致で、判定の語を含めない -- [ ] AC11: 同じラウンド・同じ席のレビューが PR にすでにあるとき、判定が変わっていても新しいレビューは増えない。待ち行列の項目は送信済みとして取り除かれる -- [ ] AC12: 投稿の後・記録の前に取り込みが止まった状態から取り込みをもう一度呼ぶと、レビューは増えず、記録には既存のレビューの URL が入る -- [ ] AC13: 同じ指摘への返信が 2 度積まれても、返信は 1 件しか増えない。すでに決着したスレッドをもう一度決着させても、結果は変わらず失敗にもならない -- [ ] AC14: 同じラウンドのまとめの投稿を 2 度積んでも、コメントは 1 件しか増えない - -### 申告をやめ、送った結果を記録にする - -- [ ] AC15: 記録に入る投稿の URL は、送信の応答から取る。 - 件数は、送信に成功したインラインの数から取る(レビューの投稿の応答は件数の項目を持たない)。 - 担当の申告件数と GitHub の実数を突き合わせる処理は無くなる -- [ ] AC16: 差分の外を指す指摘は、投稿する側が本文へ退避する。 - 対象はインラインが HTTP 422 で拒まれたものである。 - 退避した指摘の `posted_to` は `body` になり、退避した件数が取り込みの出力に出る - 行を解決できないことを示さない HTTP 422 は退避せず、取り込みが失敗として止まる -- [ ] AC17: 指摘の控えに指摘が 1 件以上あり、インラインとして送れたものが 0 件でも、その担当は結果なしにならない -- [ ] AC32: 自分の Pull Request では、投稿する側が送信の時点で `posted_as` を `COMMENT` へ落とす。本来の判定(`intent`)は落とさない - -### 起動し直しの経路(#583) - -- [ ] AC18: 骨組みで、起動し直した担当は初回と同じ経路を通る。取り込みの後に根拠の検証と反証を通ってから 2 度目の判定へ進む -- [ ] AC19: 待ち行列に残りがあるときの枝(判定の終了コード 8)は、2 度目を含む各判定の直後に残る -- [ ] AC20: 取り込みが送信の後・記録の前に止まった実行を再現し、取り込みをもう一度呼ぶ。 - PR のレビューは 1 件しか増えず、同じ論点のスレッドが 2 つに分かれない - -### `fix` を単独で使う経路 - -- [ ] AC21: 返信・スレッドの決着・まとめの投稿・修正の送信の実装は 1 つである。 - cross-review から呼ぶ経路と単独で呼ぶ経路が、同じものを使う -- [ ] AC22: 単独で `fix` を使うときの手順に、修正の結果ファイルを投稿へ変えるコマンドが 1 行で書かれている。利用者が `gh api` を手で組み立てる手順は残らない - -### 退行しない - -- [ ] AC23: 取り込み・判定・報告の終了コードは変更前と同じである。 - 収束の判定と報告が読む変数(`REVIEWER_INTENTS` / `NEW_FINDINGS` / `CARRIED_OVER_THREADS` / `PENDING_POSTS`)も変わらない。 - 取り込みの標準出力には、投稿の結果を表す行が足される(AC6) -- [ ] AC24: 席の名前(#727)・結末の語彙(#729)・数えない指摘の区分(#732)の扱いを変えない - -### 文書 - -- [ ] AC25: `SKILL.md` の設計方針の表から「AI 自身が `gh api` で PR に直接投稿」が消える。 - 代わりに、投稿の担い手とその理由が入る -- [ ] AC26: `docs/03-review-output.md` の「AI 直接投稿」の決定が、待ち行列を通す形へ書き直される -- [ ] AC27: `docs/02-fix-and-rotation.md` の修正の手順から、担当が送信する行が消える。 - 代わりに、取り込む側が送信することが書かれる -- [ ] AC28: `references/context-budget.md` の工夫の一覧から「中間ペイロードがメインを通らない」が消える。 - 代わりに、本文がレビューを回す側のプロセスの中だけを通り、応答には載らないことが書かれる -- [ ] AC29: 投稿の待ち行列の種別のうち、この変更で積む側ができたものが `docs/04-contracts.md` の契約に載る - -### 全体 - -- [ ] AC30: `uv run --with pytest pytest scripts/tests plugins/ndf -q` が通る -- [ ] AC31: 次の 3 つが終了コード 0 で終わる。`bash scripts/build-runtime-plugins.sh --check`。`claude plugin validate .`。`python3 scripts/check-skill-frontmatter.py` - -## 非機能の条件 - -| 項目 | 条件 | -| --- | --- | -| 応答の量 | 取り込み 1 回の標準出力は、投稿の件数と URL と状態だけで 20 行以内。レビューの本文を載せない | -| 上限への耐性 | 投稿が上限で拒まれたら待ち行列へ残り、判定の枝(終了コード 8)で流し直される。担当をもう一度起動しない | -| 所要 | 担当の実行時間は、投稿の手順が無くなるぶん短くなる。監視の上限(#729)は変えない | -| 権限 | 担当の CLI に GitHub への書き込みの権限が要らなくなる | - -## 影響 - -| 課題 | この変更で何が変わるか | 誰が閉じるか | -| --- | --- | --- | -| #583 | 担当が投稿しないため、投稿済みで記録なしの状態が起きない。起動し直しても論点が重ならない | この束 | -| #548 | 本文だけに書く担当がいなくなり、申告と実数の突き合わせも無くなるため、インライン 0 件で結果なしにならない | 棚卸(再現手順の確認だけ行う) | -| #350 | 担当と `fix` の投稿が待ち行列を通るため、上限の間に失われない | 棚卸 | -| #585 | 送信を取り込む側が `HEAD:<ブランチ名>` で行い、コミットが載ったことを確かめるため、報告と実物が食い違わない | 棚卸 | -| #676 | まとめの投稿が待ち行列を通るため、残らない回が無くなる | 棚卸 | - -## 検証手段 - -| 何を確かめるか | どう確かめるか | -| --- | --- | -| プロンプトから投稿の手順が消えたこと | 担当の起動スクリプトが作るプロンプトを取り出す単体テスト | -| 二度書かないこと | 偽の `gh` で「先客がいる」応答を返し、投稿が増えないことを見る単体テスト | -| 投稿の本文が応答に出ないこと | 取り込みの標準出力に本文の文字列が含まれないことを見る単体テスト | -| 送信と照合 | 送信を偽装し、報告されたコミットが送り先のブランチに無いときに失敗することを見る単体テスト | -| 骨組みの経路 | 骨組みの行の並びを読む既存のレイアウトの検査へ条件を足す | -| 文書 | 変更した行を読む検査、または該当ファイルの文言の検査 | - -## 前提とする取り決め - -この束は並行する設計の中で **G5** と呼ぶ(#727 が G1、#732 が G2、#729 が G3、#728 が G4)。 - -| 相手 | 取り決め | -| --- | --- | -| #727(G1、PR #793) | 席の名前(`claude-2` の形)と、結果ファイル・指摘の控えの名前が席で組まれること。レビューの本文の先頭行が席を持つこと。**この束は席の名前をそのまま使い、決め方を変えない** | -| #729(G3、PR #791、マージ済み) | 結末の語彙と、結果ファイルが無いときの理由の記録。**投稿の担い手が変わっても、結果なしの理由の語彙を変えない** | -| #732(G2、PR #790) | 数えない指摘の区分と収束の判定。**この束は指摘の中身に触らない** | -| #728(G4) | 触るファイルが重ならない | - -## 境界 - -```text -常に行う … 既存テストの実行、変更した文書の検査、待ち行列を通す形への統一 -確認してから行う … 部分命令の名前の変更、終了コードの変更、待ち行列の保存先の変更 -行わない … 収束の判定の書き換え、席の決め方の変更、PR の巻き直しの手順の変更 -``` - -## 未決 - -- 単独で `fix` を使うときの入口を、専用のスクリプトにするか既存の共通層の部分命令にするか(設計の決定 11 で決める) -- 差分の外を指す指摘の退避を、投稿の前に判定するか 422 を受けてから行うか(設計の決定 9 で決める) - -## 置き換える既存の受け入れ条件との対応 - -| 既存の受け入れ条件 | この文書 | 扱い | -| --- | --- | --- | -| AC63(結果が使えない担当の投稿済みレビューを探す) | — | **取り下げる。** 担当が投稿しないため、探す対象が無い | -| AC64(照会が失敗したら残さない) | — | 同上 | -| AC65(記録だけを行うプロンプトへ差し替える) | — | 同上。プロンプトからは投稿の手順そのものが消える(AC1) | -| AC66(申告された URL を投稿済みとして取り込む) | AC15 | **改める。** 申告ではなく、送信の応答から取る | -| AC67(起動し直しを初回と同じ経路へ) | AC18・AC19 | 引き継ぐ | - -既存の一覧の置き場所は、[置き換える前の要求](issue-662-598-537-619-584-583-requirements.md)の「受け入れ条件(P3)」である。**その文書は 2026-09-15 時点の記録として残し、本体は書き換えない。** - -## 依頼(原文) - -### #730(根本原因の親) - -> **cross-review が GitHub と git へ行う書き込み(レビューの投稿・修正の push・サマリの投稿)の置き場所。** -> -> - いまは担当が行う。レビューは担当の CLI が `gh api` で直接投稿し、修正は担当のサブエージェントが `git push origin {HEAD_BRANCH}` で送る -> - 進行側の `scripts/state.py` は結果ファイルを読むだけで、実物と照合しない -> - 投稿の待ち行列 `plugins/ndf/scripts/lib/post_queue.py` は進行側にあるが、担当の投稿は通らない -> - 書き込みと、進行側が読む記録を担当が別々に行うため、打ち切り・detach の作業ツリー・書き忘れのどれでも、記録と実物が食い違う -> -> 移動(`move_responsibility`)。cross-refactoring の「公開するのは進行側だけ」と同じ形にする。担当は結果ファイルだけを書く。進行側は結果ファイルから `post_queue.py` を通して投稿し、修正は `HEAD:` で push し、サマリも投稿する。 -> -> **進行側の投稿は、結果ファイルのパスを `post_queue.py` へ渡す形にする。** 進行側が投稿を持つとき、payload の本文を supervisor の応答へ載せず、スクリプトがファイルから読んで送る。 -> -> 完了条件: -> -> - 担当は結果ファイルだけを書き、レビューの投稿・修正の push・サマリの投稿は `state.py` が行う -> - 「投稿済みで記録なし」「push したと報告して実物なし」の状態が起きないことを検査が確かめる -> - `fix` を単独で使う経路(返信と Resolve)の扱いを決める -> - 各子 issue の再現手順を実行し、現象が出ないことを確かめる - -### #583 - -> **担当が投稿を終えた後、結果ファイルを書く前に監視の上限に達すると、投稿済みの指摘が状態ファイルに記録されない。** 判定は「結果なし」として同じラウンドで担当を起動し直し、起動し直した担当が同じ論点を重ねて投稿する。 -> -> 1 回目の 3 件(minor)は判定の入力に一度も入らない。**同じ論点が 2 つのスレッドに分かれ、修正の担当が両方へ返信する。** -> -> #730(親) — 修正レイヤーはこちらが持つ。担当は結果ファイルだけを書き、レビューの投稿は進行側(`state.py`)が行う形にすれば「投稿済みで記録なし」が起きない - -**#583 の収束の部分は #732(G2)の設計で塞がった。** この束に残るのは投稿の重なりと、起動し直しの経路である。 - -## 関連する文書 - -| 文書 | 何を持つか | -| --- | --- | -| [issue-730-583-design.md](issue-730-583-design.md) | どう作るか(決定・データ構造・契約・処理の流れ) | -| [issue-662-598-537-619-584-583-requirements.md](issue-662-598-537-619-584-583-requirements.md) | 置き換える前の受け入れ条件(AC63〜AC67) | -| [issue-729-619-584-requirements.md](issue-729-619-584-requirements.md) | 結末の語彙と、止めた担当に書かせないこと | -| [issue-732-624-706-requirements.md](issue-732-624-706-requirements.md) | 数えない指摘の区分と収束の判定 | -| [issue-727-687-478-664-648-requirements.md](issue-727-687-478-664-648-requirements.md) | 席の決め方と再開の引数 | diff --git a/issues/issue-732-624-706-design.md b/issues/issue-732-624-706-design.md deleted file mode 100644 index f07180897..000000000 --- a/issues/issue-732-624-706-design.md +++ /dev/null @@ -1,373 +0,0 @@ -# cross-review: 誤りを示されていない重大な指摘が数えられずに承認で終わる → 数えない指摘を棄却と軽微な指摘に限る(#732 #624 #706 の設計) - -## 目的 - -**この設計の後、cross-review の収束の判定が数えないのは、棄却された指摘と軽微な指摘だけになる。** 誰にも誤りを示されていない重大な指摘は、新しい区分「未反証」として数えられ、修正の工程へ渡る。「なぜ独立に確かめられていないか」は、未反証の理由として状態ファイルに残る。 - -いまは、誰にも誤りを示されていない重大な指摘が立証不足の区分へ落ち、数えられない。その結果、新しい指摘 0 件として承認で終わり、修正の要る指摘が修正の工程へ渡らない。 - -## 用語の対応表 - -本文は左の業務用語で書く。右は状態ファイル・スクリプトでの識別子で、コードブロック・表・「データ構造」「入出力の契約」「置き場所」の節ではそのまま使う。 - -| 業務用語 | 識別子 | 何を指すか | -| --- | --- | --- | -| 重大な指摘 / 軽微な指摘 | `severity` の `major` / `minor`(本文の「重大な指摘」は `major` 以上、「軽微な指摘」は `minor` 以下) | 指摘の重大度。軽微な指摘は承認を妨げない | -| 区分 | `classification` | 印のあるラウンドで指摘ごとに付く値。6 つになる | -| 再現した重大な指摘 / 再現した軽微な指摘 | `verified_blocking` / `verified_non_blocking` | 実行検証で再現した指摘の区分(順 1・2) | -| 棄却 | `rejected`(理由は `rejection_reason`) | 誤りだと示された指摘の区分(順 3) | -| 人の判断待ち | `needs_human_judgment` | 独立に確かめた担当がいる重大な指摘の区分(順 4) | -| 未反証 | `unrefuted`(理由は `unrefuted_reason`) | **新設。** 誰も誤りを示しておらず、独立に確かめた担当もいない重大な指摘の区分(順 5) | -| 未反証の理由: 反証なし / 支持なし | `no_critique` / `not_supported` | 反証を返した担当が 0 者 / 反証はあるが支持も否定も無い | -| 立証不足 | `insufficient_evidence`(区分の値) | 上のいずれにも当たらない軽微な指摘の区分(順 6) | -| 反証 | `critiques`(要素の `verdict`) | 提案者以外の担当が指摘へ返した賛否 | -| 反証の値: 支持 / 否定 / 立証できない / 範囲外 | `support` / `refute` / `insufficient_evidence` / `out_of_scope` | 反証の担当が返す 5 つの値のうち、この文書が扱う 4 つ | -| 実行検証の結果: 再現した / 再現しなかった / 実行していない | `verification.result` の `reproduced` / `not_reproduced` / `not_run` | 指摘の手順を実行した結果 | -| 根拠の 2 項目 / 根拠の有無 | `evidence` と `falsification` / `has_evidence` | 別の担当が確かめるための入力と、両方が揃っているかの真偽値 | -| 出した担当 | `origin_runtimes` | 同じ指摘を独立に出した担当の一覧。2 者以上で「独立に確かめた」とみなす | -| 印 | `evidence_rounds` | 統合・実行検証・反証を通ったラウンドの番号の一覧 | -| 却下の記録 | `rejected_findings` | 修正の工程が却下した指摘と理由。次のラウンドのプロンプトへ渡る | -| 数える区分の集合 | `COUNTED_CLASSIFICATIONS`(状態の管理スクリプトと測定スクリプトの 2 か所) | 新規性の層が新しい指摘として数える区分 | -| 状態の管理スクリプト / 測定スクリプト / 反証のプロンプト | `state.py` / `measure.py` / `critique.sh` | いずれも `plugins/ndf/skills/cross-review/scripts/` の下 | -| 区分の判定 / 区分の書き込み / 数える指摘の抽出 / 新しい指摘の数え上げ / 反証の不足の扱い / 印を付ける処理 | `_classify_finding` / `_apply_classification` / `_counted_finding_keys` / `_new_finding_count` / `_handle_incomplete_critiques` / `_mark_evidence_round` | 状態の管理スクリプトの内部関数 | -| 収束の判定 / 反証の取り込み | `judge`(本体は `cmd_judge`)/ `collect-critiques` | 状態の管理スクリプトの副コマンド | -| 全員が通したラウンド | `round_passes` | 全担当が承認か、重大な指摘の無いコメントを返したこと | -| 測定の方式「この変更の方式」 | `proposed` | 測定スクリプトが持つ方式の 1 つ | -| 承認 / 修正要求 / コメント | `APPROVE` / `REQUEST_CHANGES` / `COMMENT` | 担当が返すレビューの意図 | -| 承認で終わる | `approved` | 収束ループの結末 | -| GitHub CLI | `gh` | 状態の管理スクリプトが外部と出入りする唯一の経路 | - -## 機能一覧 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| F1 | 誤りを示されていない重大な指摘を、反証の有無・担当の数・根拠の項目の有無によらず新しい指摘として数える | 1 者で回す利用者(#624)、担当が起動し直されたループ(#583 の収束の部分)、実行検証も支持も無い指摘が出た Pull Request(#706) | -| F2 | 数えた指摘に「なぜ独立に確かめられていないか」の理由を残す | 修正の担当(何が確かめられていないかを読む)、収束の後に記録を読む人 | -| F3 | 反証が揃わなかったラウンドを、全件を数える扱いへ戻す | 収束ループ全般 | -| F4 | 効果の測定の方式「この変更の方式」が、数える 3 区分を採る | 収束の記録を測る人 | - -## なぜ変えるか - -**原因は、1 つの区分が 2 つの意味を兼ねていることにある。** 立証不足の区分に「反証の機会があって支持されなかった」と「立証の機会が無かった」の両方が落ち、どちらも数えられない。誤りを示されていない重大な指摘が数えられない形は、3 つの場面で出る。 - -| 場面 | 課題 | -| --- | --- | -| 反証する担当がいない 1 者のループ | #624 | -| 実行検証も支持も無い指摘 | #706 | -| 起動し直した担当の指摘 | #583 の収束の部分 | - -## 実測 - -11 通りの最小の指摘を区分の判定に渡し、いまの区分と変更後の区分を並べた。数え方が変わるのは A・B・D・E・F・H の 6 件で、いずれも重大な指摘である。軽微な指摘(C)、棄却(I・J)、再現(K)、支持つき根拠あり(G)の 5 件は変わらない。 - -測った条件は `develop` 9eaebe14、Python 3.14.4 である。実行検証は「実行していない」、担当 1 者の指摘は出した担当が 1 者である。 - -| 記号 | 入力 | いまの区分 | 数える | 変更後の区分 | 数える | -| --- | --- | --- | --- | --- | --- | -| A | `major`、根拠あり、反証なし(1 者) | `insufficient_evidence` | いいえ | `unrefuted`(`no_critique`) | **はい** | -| B | `major`、根拠なし、反証なし | `insufficient_evidence` | いいえ | `unrefuted`(`no_critique`) | **はい** | -| C | `minor`、根拠あり、反証なし | `insufficient_evidence` | いいえ | `insufficient_evidence` | いいえ | -| D | `major`、根拠あり、相手が `insufficient_evidence` | `insufficient_evidence` | いいえ | `unrefuted`(`not_supported`) | **はい** | -| E | `major`、根拠あり、相手が `out_of_scope` | `insufficient_evidence` | いいえ | `unrefuted`(`not_supported`) | **はい** | -| F | `major`、根拠なし、相手が `support` | `insufficient_evidence` | いいえ | `needs_human_judgment` | **はい** | -| G | `major`、根拠あり、相手が `support` | `needs_human_judgment` | はい | `needs_human_judgment` | はい | -| H | `major`、根拠なし、2 者が独立に出した | `insufficient_evidence` | いいえ | `needs_human_judgment` | **はい** | -| I | `major`、相手が `refute` | `rejected` | いいえ | `rejected` | いいえ | -| J | `major`、`not_reproduced` | `rejected` | いいえ | `rejected` | いいえ | -| K | `major`、`reproduced` | `verified_blocking` | はい | `verified_blocking` | はい | - -## 決定の記録 - -決定 1 は文書の形を扱う。決定 2〜6 は区分、決定 7 は測定、決定 8 は印、決定 9〜11 はプロンプト・判定の出口・確定仕様を扱う。区分の中核は決定 2 で、決定 3〜6 はその境界を定める。 - -### 決定 1: 並行する設計と 1 つのファイルで競合しないよう、設計文書は親 #732 の名前で新設し、既存の設計文書の本体は書き換えない - -既存の設計は 3 課題・2 本の Pull Request を 1 つの文書で扱う。その P5(決定 6〜19)は #727 の設計が並行して置き換える。P4 の節をその場で書き換えると、P5 の変更と 1 つのファイルで競合する。承認する人が「どの束が何を変えたか」を読み分けられない。親 #732 は根本原因を「1 つの区分が 2 つの意味を兼ねる」と定め直した。これは既存の決定 2〜4(区分の名前を増やさず、順 4 に条件を足す)の前提を変える。**新設して対応表で指せば、変わった決定だけが差分に載る。** - -既存の設計文書の本体(`-design.md`)には案内の 1 行も足さない。設計 Pull Request の本文の「決めたこと」は、変更したファイルの決定の見出しをすべて写す。1 行でも触ると、既存の 19 件の決定がこの Pull Request の決定として並ぶ。案内は、決定の記録を持たない要求の文書と契約の文書にだけ足す(#729 の設計と同じ形)。 - -### 決定 2: 未解決の重大な指摘を残して収束しないよう、数えない判断を棄却と軽微な指摘に限り、誤りを示されていない重大な指摘を新しい区分「未反証」として数える - -区分の順 5(立証不足)には、いま 7 つの形が落ちる(「実測」の A〜F・H)。そのうち軽微な指摘(C)を除く 6 つは、いずれも「誰も誤りを示していない重大な指摘」である。 - -| 落ちる形 | 実測の記号 | -| --- | --- | -| 反証する担当がいない | A・B・H | -| 相手が「立証できない」か「範囲外」を返した | D・E | -| 根拠の 2 項目を欠く | B・F・H | - -どれも、指摘が誤りだという主張ではない。**数えないのは、誤りだと示された棄却と、承認を妨げない軽微な指摘だけにする。** 誤りを示されていない重大な指摘は未反証として数え、修正の工程へ渡す。修正の担当の扱い(直す・却下の理由を返す・範囲外として起票する)は、人の判断待ちと同じである。 - -数えることで増えるラウンドは、指摘 1 件につき最大 1 回である。修正の工程が却下の理由を却下の記録へ残し、次のラウンドのレビュープロンプトへ渡す。そのため同じ論点は戻らない。戻っても、新規性の一致(位置・近傍・本文)が前のラウンドの指摘と結び、新規に数えない。#156 が避けた「同じ論点で 5 ラウンド」(#69)は、却下の記録が無かった頃の形である。 - -| 採らない案 | 採らない理由 | -| --- | --- | -| 順 4 の条件に「反証が空」を足す(既存の設計の決定 3) | A と H は数えられる。D・E(反証の機会があって支持されなかった)と B・F(根拠を欠く)は落ちたままで、親 #732 の完了条件「数えない判断が棄却された指摘に限られ」に当たらない | -| 反証が 0 件のラウンドは絞り込まない(#624 の候補 1) | ラウンド単位の判断では、2 者のうち 1 件だけが後から入った #583 の形と、2 者で相手が「立証できない」を返した #706 の形を塞げない | -| 誤りを示されていない重大な指摘を全件、人の判断待ちに入れる | 数え方は同じになる。しかし「独立に確かめた担当がいる」と「誰も確かめていない」が同じ名前になり、修正の担当と測定がその差を読めない。親 #732 は別の区分にすることを採る手としている | - -### 決定 3: 別の担当が支持した指摘が根拠の項目の欠けで落ちないよう、人の判断待ちの区分の条件から根拠の 2 項目を外す - -根拠の 2 項目は、別の担当がその指摘を確かめるための入力である。別の担当が支持を返した、または 2 者以上が同じ指摘へ独立に到達した時点で、確かめる目的は果たされている。その後で 2 項目の欠けを理由に落とすと、「2 人が見て同じことを言っている」情報が判定に効かない。#706 の PR #45 では、支持の付いた 2 件がこの形で落ちた。**順 4 は「重大な指摘で、支持が 1 件以上または出した担当が 2 者以上」**とする。 - -根拠の有無の値そのものは変えずに残す。反証のプロンプトが読み、測定と記録がそのことを持つ。**区分が読まなくなるだけである。** 4 項目を持たない指摘を捨てないという既存の決定(`docs/06` の「4 項目を持たない指摘」)は、この変更で「捨てず、数える」まで進む。 - -採らない案: **根拠を欠く指摘は未反証に入れ、人の判断待ちには入れない。** 順 4 と順 5 の差が「独立に確かめたか」でなく「2 項目を書いたか」で決まり、支持の意味が薄れる。 - -### 決定 4: 立証できないことを棄却と扱わないよう、反証の担当が「立証できない」「範囲外」だけを返した重大な指摘も未反証として数える - -反証の値「立証できない」(`insufficient_evidence`)の意味は「可能性はあるが立証できない」である。「範囲外」(`out_of_scope`)の意味は「この Pull Request の範囲から外れる」である。どちらも指摘が誤りだという主張ではない。規約も「否定の代わりに使わせない」と定めている。誤りを示せない指摘を数えずに収束させると、未解決の重大な指摘が残ったまま承認で終わる(#706 の観測そのもの)。**担当 2 者で相手が支持しなかった重大な指摘を数えないという #156 の判断を改める。** #624 は「#156 の設計どおりで対象外」と書いた。しかし親 #732 の採る手と完了条件は、この形も棄却ではない側に置く。 - -この変更で、反証の担当が指摘を数から落とす手段は否定だけになる。プロンプトにそのことを書く(決定 9)。 - -採らない案: **「立証できない」を返された重大な指摘は数えないまま残す(#156 の判断を保つ)。** 立証できない指摘と反証する担当がいない指摘は、反証の有無で状態ファイルから区別できる。しかし区別して前者だけを落とすと、実行検証を持たない Pull Request(文書だけの変更)では、担当が確かめられなかった重大な指摘がすべて落ちる。#706 の PR #45 は文書の Pull Request である。 - -### 決定 5: 修正の担当と測定が「なぜ確かめられていないか」を読めるよう、未反証の理由を「反証なし」「支持なし」の 2 値で状態ファイルに残す - -棄却が棄却の理由を持つのと同じ形で、未反証が「なぜ独立に確かめられていないか」の理由(`unrefuted_reason`)を持つ。値は 2 つである。 - -| 値 | 意味 | 修正の担当の読み方 | -| --- | --- | --- | -| 反証なし(`no_critique`) | 反証を返した担当が 0 者 | 誰も見ていない指摘 | -| 支持なし(`not_supported`) | 反証はあるが、支持も否定も無い | 相手が確かめられなかった指摘 | - -測定は、1 者のループと 2 者のループでどちらの理由が多いかを、同じ記録から読める。 - -根拠の 2 項目の欠けは理由に入れない。根拠の有無が既に持つ値で、理由と直交する(反証なし、かつ根拠なし、のように組になる)。 - -採らない案: **理由を持たず、反証の有無から読む。** 読めはする。しかし棄却の理由と対になる形が崩れ、状態ファイルを読む人が区分ごとに違う導き方を覚えることになる。 - -### 決定 6: 旧い記録と語彙の付け替えを避けるため、立証不足の区分の名前は残し、軽微な指摘の残余だけに当てる - -重大な指摘の 4 つの形が未反証へ移った後、順 6(旧 5)に残るのは「再現も棄却もされていない軽微な指摘」だけである。名前(`insufficient_evidence`)を別の語へ変えると、付け替える先が 3 つ同時に出る。旧い状態ファイルの区分の値、測定スクリプトの読み方、規約と確定仕様の語彙である。**意味は「立証されておらず、修正必須でもない」に狭まるが、名前は変えない。** 区分の表の条件の列で意味を定める。 - -採らない案: **立証不足の区分を `not_blocking` へ改名する。** 語彙は正確になる。しかしこの変更の目的(数えない判断を棄却に限る)に要らず、改名は測定の比較(変更の前後の記録)を難しくする。 - -### 決定 7: 収束の判定と測定が同じ指摘を数えるよう、測定スクリプトの「この変更の方式」を数える 3 区分に揃え、2 か所の集合の一致をテストで固定する - -測定の方式「この変更の方式」(`proposed`)は、「この変更の方式が採る指摘」を数える。その定義は、状態の管理スクリプトの数える区分の集合と同じ集合である(測定スクリプトのコメントが明記する)。状態の管理スクリプトだけに未反証を足すと、収束の判定が数えた指摘を測定が採らない。この方式の再現率が実際より低く出る。**両方の定数を `("verified_blocking", "needs_human_judgment", "unrefuted")` にする。** 測定のテストに、2 つの値が等しいことを見るテストを足す。 - -測定スクリプトが状態の管理スクリプトを読み込む形は採らない。測定スクリプトは状態ファイルを読むだけの独立したスクリプトである(確定仕様の決定「測定は独立したスクリプトにする」)。状態の管理スクリプトを読み込むと、GitHub CLI を呼ぶ側の前提を持ち込む。 - -### 決定 8: 反証が届いていないラウンドを全件を数える側へ戻すため、反証が揃わない取り込みでは先に付いていた印を外す - -既存の設計の決定 5 をそのまま引き継ぐ。反証の取り込みは、揃わないとき印を付けない。しかし既に付いている印は外さない。取り直しの後もそのラウンドに印が残ると、「印を付けないため、このラウンドは全件を数えます」の出力と実際の数え方が食い違う。反証の不足の扱いの先頭で、そのラウンドの番号を印の一覧から除く。 - -決定 2 の後もこの決定は要る。印の無いラウンドは棄却と軽微な指摘も数える。反証が揃っていない(否定が届いていないかもしれない)ラウンドでは、全件を数える側が安全である。 - -### 決定 9: 誤りを示せる指摘が「立証できない」へ流れないよう、反証のプロンプトに「立証できないと返しても指摘は数から落ちない」を書く - -決定 4 の後、反証の担当が指摘を数から落とす手段は否定だけになる。プロンプトがそのことを言わないと、担当は従来どおり「判断できないものは立証できない」と返す。誤りを示せる指摘まで「立証できない」に流れ、修正の工程へ渡る。反証のプロンプトの「返す値」の表の下に 1 段落を足す。書くのは 2 つである。「立証できない」を返しても指摘は数から落ちず、修正の工程へ渡ること。誤りを示せるなら、理由を添えて否定を返すこと。 - -反証の値の語彙(5 つ)は変えない。変えるのは説明だけである。 - -### 決定 10: #729 が固定した境界を守るため、収束の判定の本体・終了コード・出力の変数は変えず、区分の内訳を新しい出力として足さない - -#729(G3)の設計が、収束の判定の終了コード 0 / 2 / 7 / 8 と出力の変数を境界として固定している。この変更が触るのは、新しい指摘の数え上げから下と、反証の不足の扱いである。「下」は数える指摘の抽出・区分の書き込み・区分の判定を指す。収束の判定の本体(`cmd_judge`)の行は書き換えない。区分ごとの件数を収束の判定の標準出力へ足す案は採らない。骨組み(`SKILL.md`)が読まない値を足しても読む側が無く、出力の契約(`docs/04`)を増やすだけである。件数は状態ファイルの区分から読める。 - -### 決定 11: 確定仕様が古いまま配布される期間を作らないため、確定仕様の区分の表は実装 Pull Request の同じ差分で更新する - -確定仕様 `docs/specifications/cross-review-evidence-based.md` は、区分の表・区分ごとの行き先・収束の判定の表を持つ。「数えるのは 2 区分だけ」を決定としても書いている。仕様化の工程(`plan-to-spec`)まで待つと、実装が配布されてから確定仕様が古いまま残る期間ができる。文書の鮮度の検査(`check-doc-staleness.py`)はそれを拾わない(確定仕様は検査の対象外である)。**区分の表・行き先の表・決定の表・テスト観点の行を、実装の差分と同じ Pull Request で直す。** 経緯の節(背景・関連リンク)は仕様化の工程が足す。 - -## データ構造 - -状態ファイル `cross-review-pr<番号>-state.json` の `review_findings[]` の要素で変わるのは 2 項目である。**新しい最上位の項目は無く、`version` の類は持たないため上げない。** - -| 項目 | 型 | 空を許すか | 変更 | -| --- | --- | --- | --- | -| `classification` | 文字列 | 許さない(区分の後) | 値の集合が 6 つになる。`verified_blocking` / `verified_non_blocking` / `rejected` / `needs_human_judgment` / **`unrefuted`** / `insufficient_evidence` | -| `unrefuted_reason` | 文字列 | 項目が無いことを許す | **新設。** `classification` が `unrefuted` のときだけ持つ。値は `no_critique` / `not_supported`。区分が変わると消える(`rejection_reason` と同じ扱い) | -| `rejection_reason` | 文字列 | 項目が無いことを許す | 変わらない。`rejected` のときだけ持つ | -| `has_evidence` | 真偽値 | 許さない | 変わらない。**区分の条件から外れるが、値は残る** | - -### 区分の条件 - -区分の判定(`_classify_finding`)が、上から順に当てる。 - -| 順 | 区分 | 条件 | 数える | 理由の項目 | -| --- | --- | --- | --- | --- | -| 1 | `verified_blocking` | 再現した、かつ `major` 以上 | はい | — | -| 2 | `verified_non_blocking` | 再現した、かつ `minor` 以下 | いいえ | — | -| 3 | `rejected` | 再現しなかった、または `refute` が 1 件以上 | いいえ | `rejection_reason` | -| 4 | `needs_human_judgment` | `major` 以上で、`support` が 1 件以上または `origin_runtimes` が 2 者以上 | はい | — | -| 5 | `unrefuted` | `major` 以上(上のいずれにも当たらない) | はい | `unrefuted_reason` | -| 6 | `insufficient_evidence` | 上のいずれにも当たらない(`minor` 以下) | いいえ | — | - -未反証の理由は、反証の記録が空なら反証なし、1 件以上あれば支持なしである。反証の記録は提案者以外の値だけを持つため、空は「反証を返した担当が 0 者」を表す。 - -### 機能とデータの対応 - -| 機能 | 読む | 書く | -| --- | --- | --- | -| F1 数える | `severity` / `verification.result` / `critiques[].verdict` / `origin_runtimes` | `classification` | -| F2 理由を残す | `critiques` の有無 | `unrefuted_reason`(他の区分では消す) | -| F3 印を戻す | `evidence_rounds` / `rounds[].critique_relaunched` | `evidence_rounds`(番号を除く) | -| F4 測る | `classification` / `evidence_rounds` / `merged_into` | (測定の出力。状態ファイルは書かない) | - -### 移行 - -この変更より前の状態ファイルは、次に収束の判定か反証の取り込みを呼んだ時点で区分が付け直される。区分は保存された値を読まず、毎回、区分の書き込みが計算する。未反証の理由はそのときに付く。測定スクリプトは保存された区分を読むため、収束の終わった旧い記録は旧い値のまま測られ、未反証は出ない。 - -## 入出力の契約 - -**状態の管理スクリプトの引数・終了コード・標準出力の変数は変わらない。** 変わるのは内部関数の契約だけである。 - -| 関数 | 変更前 | 変更後 | -| --- | --- | --- | -| `_classify_finding(finding) -> str` | 5 つの値を返す。順 4 に `has_evidence` を求める | 6 つの値を返す。順 4 から `has_evidence` を外し、順 5 に `unrefuted` を置く。**純粋な関数で、出力も終了コードも持たない**(変わらない) | -| `_apply_classification(finding) -> str` | `rejected` に `rejection_reason` を書き、他では消す | 加えて `unrefuted` に `unrefuted_reason` を書き、他では消す | -| `COUNTED_CLASSIFICATIONS`(`state.py` / `measure.py`) | `("verified_blocking", "needs_human_judgment")` | `("verified_blocking", "needs_human_judgment", "unrefuted")`。2 か所の値は等しい | -| `_handle_incomplete_critiques(pr, st, round_no, missing)` | 印を付けない。既にある印は残す。終了コード 7 と `CRITIQUE_RETRY_AGENTS` を返す(変わらない) | 先頭で `evidence_rounds` からそのラウンドの番号を除く。それ以外は変わらない | -| `_new_finding_count(st, pr) -> (int, bool)` | 印のあるラウンドを 2 区分へ絞る | 印のあるラウンドを 3 区分へ絞る。返る値の意味は変わらない | -| `cmd_judge` | 0 / 2 / 7 / 8 / 1 | 変わらない。本体の行を書き換えない | -| `critique.sh` のプロンプト | 「判断できないものは `insufficient_evidence` にする」 | 加えて「`insufficient_evidence` を返しても指摘は数から落ちず、修正の工程へ渡る。誤りを示せるなら理由を添えて `refute`」 | - -## 構成要素 - -変えるのは、状態の管理スクリプトの 4 つの関数・定数、測定スクリプトの定数、反証のプロンプト、規約 3 文書、確定仕様、テスト 4 ファイルである。新設する要素は無い。 - -| 要素 | 責務 | 変える・新設 | -| --- | --- | --- | -| 区分の判定(`_classify_finding`) | 6 つの区分を上から順に当てる。順 4 から根拠の条件を外し、順 5 に `unrefuted`(`major` 以上の残余)を置く(決定 2・3・4) | 変える | -| 区分の書き込み(`_apply_classification`) | 区分を要素へ書く。`rejected` に `rejection_reason`、`unrefuted` に `unrefuted_reason` を残し、他の区分では両方を消す(決定 5) | 変える | -| 数える集合(`COUNTED_CLASSIFICATIONS`。`state.py` と `measure.py`) | `unrefuted` を足した 3 つにする(決定 2・7) | 変える | -| 反証の不足の扱い(`_handle_incomplete_critiques`) | 先頭でそのラウンドの印を外してから、取り直す担当を返す(決定 8) | 変える | -| 反証のプロンプト(`critique.sh`) | `insufficient_evidence` が指摘を数から落とさないことを書く(決定 9) | 変える | -| 規約と契約(`docs/04` / `docs/05` / `docs/06`) | 6 区分・数える 3 つ・1 者と起動し直しの数え方・印を外すことを書く | 変える | -| 確定仕様(`docs/specifications/cross-review-evidence-based.md`) | 区分の表・行き先の表・決定の表・テスト観点を揃える(決定 11) | 変える | -| テスト(`test_classify_findings.py` / `test_critiques.py` / `test_measure.py` / `test_skill_layout.py`) | 実測の A〜K と AC10〜AC13、集合の一致、文書の語を固定する | 変える | - -```mermaid -graph TD - J[judge] --> NF[_new_finding_count] - NF --> CK[_counted_finding_keys] - CK --> AP[_apply_classification] - AP --> CL[_classify_finding] - CK --> CC[COUNTED_CLASSIFICATIONS] - COL[collect-critiques] --> IC[_handle_incomplete_critiques] - M[measure.py _proposed] --> CC2[COUNTED_CLASSIFICATIONS measure.py] -``` - -図の辺は呼び出しと参照を表す。収束の判定・反証の取り込み・測定スクリプトは、変える要素の呼び出し元として置いた既存の要素である。プロンプト・規約・確定仕様・テストは呼び出しを持たないため、図に含めない。 - -**文脈と配置は変わらない。** 動くのは 2 つのプロセスである。ホストの CLI から起動される状態の管理スクリプトと、状態ファイルを読む測定スクリプトである。外部との出入り(GitHub CLI)は変わらない。 - -## 置き場所 - -```text -plugins/ndf/skills/cross-review/ -├── SKILL.md # 変えない(`--only` の説明は #727 が持つ) -├── docs/04-contracts.md # classification の項を 6 区分・数える 3 つへ -├── docs/05-pool-and-convergence.md # 終了基準に 1 者と起動し直しの数え方を足す -├── docs/06-evidence.md # 区分の表を 6 行へ、印を外すことを書く -├── scripts/ -│ ├── critique.sh # 返す値の表の下に 1 段落を足す -│ ├── measure.py # COUNTED_CLASSIFICATIONS に unrefuted を足す -│ └── state.py # _classify_finding / _apply_classification / COUNTED_CLASSIFICATIONS / _handle_incomplete_critiques -└── tests/ - ├── test_classify_findings.py # A〜K、AC10、期待値を変える 4 件 - ├── test_critiques.py # AC13(印を外す) - ├── test_measure.py # AC11(一致)・AC12(proposed) - └── test_skill_layout.py # AC18〜AC20 の grep -docs/specifications/cross-review-evidence-based.md # 区分・行き先・決定・テスト観点 -``` - -`dev.kiro` / `dev.agy` は `skills/` を symlink で参照するため、書き写す配布物は無い。 - -## 処理の流れ - -収束の判定は、印のあるラウンドだけ指摘ごとに区分を決め、数える 3 区分へ絞る。印の無いラウンドは、レビュー結果の本体(payload)の全件を数える。 - -```mermaid -graph TD - J[judge] --> K{payload を読めたか} - K -->|いいえ| U["(0, False) 全員 pass に従う"] - K -->|はい| M{ラウンドに印があるか} - M -->|いいえ| ALL[payload の全件を数える] - M -->|はい| C[指摘ごとに区分を決める] - C --> R{再現した} - R -->|はい| V[順 1・2 verified_*] - R -->|いいえ| X{not_reproduced / refute あり} - X -->|はい| RJ[順 3 rejected 数えない] - X -->|いいえ| S{major 以上} - S -->|いいえ| IE[順 6 insufficient_evidence 数えない] - S -->|はい| T{support あり / 2 者が独立に} - T -->|はい| NH[順 4 needs_human_judgment 数える] - T -->|いいえ| UR[順 5 unrefuted 数える] -``` - -順 5 に入った指摘は、反証の有無で未反証の理由が決まる。反証の取り込みで揃わないとき、反証の不足の扱いがそのラウンドの印を外す。次に収束の判定が数えるとき「印があるか」が「いいえ」へ進む。取り直して揃えば、印を付ける処理が印を戻す。 - -**1 者で回したラウンドの収束は、未反証の新規性と、全員が通したかの 2 つで決まる。** 担当が承認か、重大な指摘の無いコメントを返せば、全員が通したとして収束する。修正要求なら、未反証の重大な指摘が新規に数えられ、修正の工程へ進む。修正の後のラウンドで同じ指摘が戻れば、前のラウンドと一致して新規 0 件になる。戻らなければ承認で収束する。 - -## 非機能の実現方式 - -| 大項目 | 実現方式 | -| --- | --- | -| 運用・保守性 | 数える集合の 2 か所は `test_measure.py` の一致のテストが固定する。区分の理由は `rejection_reason` / `unrefuted_reason` として状態ファイルに残る | -| 移行性 | 区分は毎回計算し直すため、旧い状態ファイルの移行の処理は書かない。`version` は上げない | - -## テスト設計 - -置き場所は `plugins/ndf/skills/cross-review/tests/` の下である。区分は区分の判定の単体で確かめる。数え方は、状態ファイルを組んだ新しい指摘の数え上げと、収束の判定の終了コードで確かめる。 - -| 受け入れ条件 | 何で確かめるか | 置き場所 | -| --- | --- | --- | -| AC1・AC3 | 担当 1 者・印付きの状態ファイルを組み、`_new_finding_count` と `cmd_judge` の終了コードを見る(実測の A・C) | `test_classify_findings.py` | -| AC2 | A の指摘の `has_evidence` を偽にして同じ組を見る(実測の B) | 同上 | -| AC4 | `agy` + `kiro` で、`kiro` の指摘に反証が付き `agy` の指摘に付かない状態ファイル | 同上 | -| AC5・AC6 | `_classify_finding` に反証 `insufficient_evidence` / `out_of_scope` 付きの `major` を渡す(実測の D・E) | 同上 | -| AC7・AC8 | `has_evidence` 偽で `support` 付き、`has_evidence` 偽で `origin_runtimes` 2 者(実測の F・H) | 同上 | -| AC9 | 実測の I・J・K を既存のテストで確かめる(期待値を変えない) | 同上(既存) | -| AC10 | `_apply_classification` の後の `unrefuted_reason` の値と、区分が変わったときに消えること | 同上 | -| AC11 | `state_mod.COUNTED_CLASSIFICATIONS == measure_mod.COUNTED_CLASSIFICATIONS` と、3 つの値 | `test_measure.py` | -| AC12 | 印のあるラウンドの `unrefuted` を `proposed` の `found` が数える | 同上(既存の `test_proposed_takes_only_the_two_counted_classifications` を 3 区分へ改める) | -| AC13 | 印の付いた状態で `cmd_collect_critiques` を 2 回呼び、`evidence_rounds` と数え方を見る | `test_critiques.py` | -| AC14・AC15 | 既存のテスト(`minor` の区分、印なしの数え方)を期待値を変えずに通す | 既存のまま | -| AC16 | 4 件のテストの期待値を新しい区分へ改め、他は変えない | `test_classify_findings.py` | -| AC17 | `cmd_judge` の既存のテストを期待値を変えずに通す。差分に `cmd_judge` の行が無いことをレビューで見る | 既存のまま・設計 Pull Request のレビュー | -| AC18〜AC20 | 文書の語を `grep` するテスト | `test_skill_layout.py` | -| AC21・AC22 | コマンドの終了コード | 継続的統合と手元 | - -## 未確認のまま残ること - -6 件である。実装で決めたもの 2 件(テストの置き場所、プロンプトの文言)と、配布後の運用か別の課題で決まるもの 4 件に分かれる。 - -| 項目 | 内容 | いつ決まるか | -| --- | --- | --- | -| 収束までのラウンド数の増え方 | 実測の D・E(相手が「立証できない」を返した重大な指摘)を数えることで、2 者のループのラウンド数がどれだけ増えるかは測っていない | 配布後の運用で測定スクリプトの出力を見る | -| 未反証が多いときの修正の担当の負荷 | 誰も確かめていない重大な指摘が修正の工程へ渡る件数が増える。却下の理由を書く回数が増える | 同上 | -| 担当を 3 者以上へ広げたときの順 3 と順 4 | 確定仕様が「広げるときに決め直す」としている。この変更は 2 者のまま | #478 の後 | -| 立証不足の区分の改名 | 決定 6 で残す。意味が狭まった名前をいつ付け替えるかは未決 | 要求が出たとき | -| テストの置き場所 | テスト設計の表のとおりに置いた。区分の単体と AC1〜AC4・AC10 は `test_classify_findings.py`、AC11・AC12 は `test_measure.py`、AC13 は `test_critiques.py`、AC18〜AC20 は `test_skill_layout.py` | 実装で決めた(実装 Pull Request) | -| プロンプトの文言 | 「返す値」の表の下に 3 文を置いた。「立証できない」を返しても指摘は数から落ちず未反証として修正の工程へ渡ること、誤りを示せるなら何がそう言えるかを理由へ書いて否定を返すこと、指摘を数から落とす手段は否定だけであること | 実装で決めた(実装 Pull Request) | - -## 申し送り(並行する設計との境界) - -| 相手 | 決めた契約 | -| --- | --- | -| #730(G5、#583) | **#583 の収束の部分はこの設計で塞がる。** 起動し直した担当の指摘は、取り込みの経路が証拠集約(`verify-findings` / `critique-round.sh`)を通っても通らなくても、反証を持たない重大な指摘として未反証に入り数えられる。G5 が投稿を進行側へ移す設計を採っても、この判定は変わらない。G5 に残るのは投稿の重なりと、`JUDGE_RC -eq 7` の分岐が証拠集約を通らない点だけである | -| #729(G3) | 収束の判定の終了コード 0 / 2 / 7 / 8 と出力の変数を変えない。本体(`cmd_judge`)の行を書き換えない(決定 10)。この設計が触る関数は `_new_finding_count` から下と `_handle_incomplete_critiques` で、G3 の `_read_review_result_file` と重ならない | -| #727(G1) | `--only` の意味づけ(使える者を 1 者へ絞る)と担当の決め方は G1 が持つ。1 者のときに何を数えるかはこの設計が持つ(処理の流れの最後の段落)。G1 の実装が 1 者で回す分岐を入れても、この設計が先に入っていれば #624 の誤った収束を踏まない。**既存の要求の文書と契約の文書に足す案内の段落は G1 も同じファイルへ足す可能性がある。** 節が違うため衝突は起きにくいが、起きたら後からマージする側が解く | -| #728(G4) | 触るファイルが重ならない(`refactor_lib` は区分を持たない) | - -## 置き換える既存の設計との対応 - -**この文書は既存の設計 [issue-624-478-648-design.md](issue-624-478-648-design.md) の P4(決定 2〜5。PR #667、2026-09-15)を置き換える。** P5(決定 6〜19)は #727 の設計が持つ。 - -| 既存の決定 | この文書 | 扱い | -| --- | --- | --- | -| 決定 1(P4 / P5 に分け、P4 を先に出す) | — | 束の分け方は実行計画(G1 / G2)が引き継いだ。P4 が先という順序は保つ(G1 の 1 者で回す分岐が #624 を踏まないため) | -| 決定 2(数えない判断は「反証を受けた単独の指摘」だけに掛け、記録から導く) | 決定 2・4・5 | **改める。** 数えないのは棄却と軽微な指摘だけにし、反証を受けて支持されなかった重大な指摘も数える。記録から導く(項目を足さない)方針は、未反証の理由を足すことで改める | -| 決定 3(順 4 の支持の条件だけを外し、根拠と重大度の条件は残す) | 決定 2・3 | **改める。** 根拠の条件を外し、支持も反証も無い重大な指摘は別の区分へ入れる。重大度の条件(軽微な指摘を数えない)は引き継ぐ | -| 決定 4(区分の名前を増やさない) | 決定 2・6・7 | **改める。** 未反証を足す。増やさない理由だった 2 か所の集合と語彙の同期は、テストと同じ差分で受ける | -| 決定 5(反証が揃わない取り込みでは先に付いていた印を外す) | 決定 8 | 引き継ぐ | - -## 関連する文書 - -この文書は「どう作るか」だけを扱う。 - -| 文書 | 何を持つか | -| --- | --- | -| [issue-732-624-706-requirements.md](issue-732-624-706-requirements.md) | 何を満たすか(目的・対象範囲・用語・受け入れ条件 AC1〜AC22) | -| [issue-624-478-648-design.md](issue-624-478-648-design.md) | 置き換える前の設計(P4)。P5 は #727 の設計が持つ | diff --git a/issues/issue-732-624-706-plan.md b/issues/issue-732-624-706-plan.md deleted file mode 100644 index 393859fe6..000000000 --- a/issues/issue-732-624-706-plan.md +++ /dev/null @@ -1,162 +0,0 @@ -# cross-review: 誤りを示されていない重大な指摘が数えられずに承認で終わる → 数えない指摘を棄却と軽微な指摘に限る(#732 #624 #706 の実装計画) - -## 関連リンク - -| 文書 | 何を持つか | -| --- | --- | -| [issue-732-624-706-requirements.md](issue-732-624-706-requirements.md) | 何を満たすか(受け入れ条件 AC1〜AC22) | -| [issue-732-624-706-design.md](issue-732-624-706-design.md) | どう作るか(決定 1〜11、区分の条件、入出力の契約、テスト設計)。**業務用語と識別子の対応表はこの文書の冒頭にある** | -| 親 #732、子 #624 #706 | 課題。設計 Pull Request は #783 | - -## モード - -`standard`。収束の判定が数える指摘の範囲(本番の振る舞い)を変えるため。 - -## 目的と非目的 - -達成したい状態: - -- 収束の判定が数えないのは、誤りだと示された指摘(棄却)と、承認を妨げない軽微な指摘だけになる -- 誰にも誤りを示されていない重大な指摘は、新しい区分「未反証」として数えられ、修正の工程へ渡る。「なぜ独立に確かめられていないか」の理由が状態ファイルに残る -- 効果の測定の方式「この変更の方式」が、収束の判定と同じ 3 区分を数える -- 反証が揃わなかったラウンドは、先に付いていた印が外れ、全件を数える側へ戻る -- 規約 3 文書と確定仕様が、実装と同じ差分で新しい区分を書く - -やらないこと: - -- 収束の判定の本体・終了コード・標準出力の変数の変更(#729 の束が持つ境界) -- 担当を 1 者へ絞る指定の意味づけ(#727 の束) -- 投稿の重なりの扱い(#730 の束) -- 軽微な指摘を数えること -- 立証不足の区分の改名(設計文書の決定 6) -- 既存の設計文書(`issue-624-478-648-design.md`)の本体の書き換え(設計文書の決定 1) - -## 前提 - -- 前提 1: 反証の記録は提案者以外の担当の値だけを持つ。そのため、記録が空であることは「反証を返した担当が 0 者」と同じである(状態の管理スクリプトの取り込みが提案者自身の値を落とす。既存のテスト `test_a_critique_on_own_finding_is_dropped` が固定する) -- 前提 2: 区分は保存された値を読まず、判定のたびに計算し直す。そのため、この変更より前の状態ファイルに移行の処理は要らない -- 前提 3: 収束の判定の本体を触らずに数え方を変えられる。数える指摘の抽出が数える集合を参照しており、集合の値を変えれば判定の本体は変わらない(`develop` 2b140606 の `_new_finding_count` → `_counted_finding_keys` → `COUNTED_CLASSIFICATIONS` の呼び出しで確認) - -## 受け入れ条件 - -要求文書の AC1〜AC22 をそのまま使う。条件ごとの検証手段は設計文書の「テスト設計」の表にある。この計画では、タスクごとに満たす条件の番号を書く。 - -## ドメイン用語 - -設計文書の「用語の対応表」を正本とし、ここには写さない。本文は業務用語で書き、識別子はコードブロック・表・ファイルの指し示しにだけ使う。 - -## 不変条件 - -- 実行で再現した指摘は、担当の反証によらず「再現した」の区分に入る(順 1・2 が最初に当たる) -- 誤りだと示された指摘(再現しなかった、または否定が 1 件以上)は、重大度によらず棄却になる(順 3 が順 4・5 より先に当たる) -- 軽微な指摘は、反証の有無・支持の有無によらず数えない -- 未反証の理由は、区分が未反証のときだけ存在する。区分が変われば消える(棄却の理由と同じ扱い) -- 数える区分の集合は、状態の管理スクリプトと測定スクリプトで同じ値を持つ - -## 互換性 - -| 対象 | 変更 | 互換性の扱い | -| --- | --- | --- | -| 状態の管理スクリプトの引数・終了コード・標準出力の変数 | 変えない | 変えない | -| 状態ファイルの `review_findings[].classification` | 値の集合が 5 つから 6 つになる(`unrefuted` が増える) | 追加のみ。読む側は測定スクリプトだけで、知らない値は「採らない」に落ちる | -| 状態ファイルの `review_findings[].unrefuted_reason` | 新設 | 追加のみ。`version` は上げない。旧い状態ファイルは次の判定で区分が付け直される | -| 反証のプロンプトの返す値の語彙 | 変えない | 説明の段落を足すだけ | - -## 修正対象 - -```text -plugins/ndf/skills/cross-review/ -├── docs/04-contracts.md -├── docs/05-pool-and-convergence.md -├── docs/06-evidence.md -├── scripts/critique.sh -├── scripts/measure.py -├── scripts/state.py -└── tests/ - ├── test_classify_findings.py - ├── test_critiques.py - ├── test_measure.py - └── test_skill_layout.py -docs/specifications/cross-review-evidence-based.md -issues/issue-732-624-706-design.md(「未確認のまま残ること」の 2 行だけ) -``` - -配布物の生成(`bash scripts/build-runtime-plugins.sh`)で変わるファイルがあれば同じ Pull Request に含める。 - -## タスク分解 - -機能単位で分ける。各タスクは、失敗するテスト → 通す最小実装 → 整理の順で進める。 - -### Task 1: 誤りを示されていない重大な指摘を「未反証」として数え、理由を残す - -- **対象ファイル:** `scripts/state.py`(`_classify_finding` / `_apply_classification` / `COUNTED_CLASSIFICATIONS` とその注記)、`tests/test_classify_findings.py` -- **変更内容:** 区分の判定を 6 区分にする。順 4(人の判断待ち)の条件から根拠の有無を外し、順 5 に未反証(重大な指摘の残余)を置き、順 6 を立証不足(軽微な指摘の残余)にする。区分の書き込みは、未反証に理由(反証の記録が空なら `no_critique`、あれば `not_supported`)を書き、他の区分では理由を消す。数える集合に `unrefuted` を足す。関数の docstring と注記の「5 つ」「2 つだけ」を新しい数に合わせる -- **テスト:** 設計文書の実測 A〜K を単体で固定する(A・B・D・E・F・H が変わる 6 件、C・G・I・J・K が変わらない 5 件)。AC1〜AC4 は状態ファイルを組み、新しい指摘の数え上げと収束の判定の終了コードで確かめる(既存の `test_the_new_count_uses_the_classification` の形)。AC10 は理由の値と、区分が変わったときに消えることを見る。期待値を変える既存のテストは AC16 の 4 件だけで、名前を新しい振る舞いに合わせて変える -- **満たす受け入れ条件:** AC1〜AC10、AC14、AC16 -- **進め方:** 失敗するテスト → 最小実装 → 整理 - -### Task 2: 効果の測定の方式「この変更の方式」を、数える 3 区分に揃える - -- **対象ファイル:** `scripts/measure.py`(`COUNTED_CLASSIFICATIONS` とその注記)、`tests/test_measure.py` -- **変更内容:** 測定スクリプトの数える集合に `unrefuted` を足す。注記の「2 つ」「残る 3 つ」を「3 つ」「残る 3 つ」に直す -- **テスト:** 2 つのスクリプトの集合が等しく、3 つの値を持つこと(AC11)。印のあるラウンドの未反証を「この変更の方式」が数えること(AC12。既存の `test_proposed_takes_only_the_two_counted_classifications` を 3 区分へ改める) -- **満たす受け入れ条件:** AC11、AC12 -- **進め方:** 失敗するテスト → 最小実装 → 整理 - -### Task 3: 反証が揃わない取り込みでは、先に付いていた印を外す - -- **対象ファイル:** `scripts/state.py`(`_handle_incomplete_critiques`)、`tests/test_critiques.py` -- **変更内容:** 反証の不足の扱いの先頭で、そのラウンドの番号を印の一覧から除く。取り直す担当を返す動きと終了コード 7 は変えない -- **テスト:** 印の付いた状態で反証の取り込みを 2 回呼び(1 回目は取り直し、2 回目も揃わない)、印が消えていることと、新しい指摘の数え上げがレビュー結果の本体の全件を数えることを見る(AC13) -- **満たす受け入れ条件:** AC13、AC15 -- **進め方:** 失敗するテスト → 最小実装 → 整理 - -### Task 4: 反証のプロンプトに「立証できないと返しても指摘は数から落ちない」を書く - -- **対象ファイル:** `scripts/critique.sh`、`tests/test_skill_layout.py` -- **変更内容:** 「返す値」の表の下に 1 段落を足す。書くのは 2 つ。「立証できない」を返しても指摘は数から落ちず修正の工程へ渡ること、誤りを示せるなら理由を添えて否定を返すこと。返す値の語彙は変えない -- **テスト:** プロンプトの文字列に「数から落ち」が含まれること(AC19) -- **満たす受け入れ条件:** AC19 -- **進め方:** 失敗するテスト → 最小実装 - -### Task 5: 規約 3 文書と確定仕様を、6 区分・数える 3 つへ揃える - -- **対象ファイル:** `docs/04-contracts.md`、`docs/05-pool-and-convergence.md`、`docs/06-evidence.md`、`docs/specifications/cross-review-evidence-based.md`、`tests/test_skill_layout.py` -- **変更内容:** 規約 06 の区分の表を 6 行にし、数えるのは 3 つと書き、反証が揃わないときに印を外すことを書く。規約 04 の区分の項を 6 区分・数える 3 つにし、`unrefuted_reason` の項を足す。規約 05 の終了基準に、担当 1 者と起動し直した担当の指摘の数え方を足す。確定仕様の概要・決定の表・区分の表・行き先の表・収束の判定の表・測定の表・テスト観点の行を揃える(経緯の節は仕様化の工程が足す)。測定の方式の表(規約 06・確定仕様)も 3 区分にする -- **テスト:** AC18 の 4 つの grep と AC20 の grep を配置テストで固定する -- **満たす受け入れ条件:** AC18、AC20 -- **進め方:** 失敗するテスト → 文書の更新。文書は `markdown-writing` の規約で書く(説明文の主語・目的語に識別子を置かない) - -### Task 6: 検証と配布物の同期 - -- **対象ファイル:** 生成物(`bash scripts/build-runtime-plugins.sh` が変えるもの)、`issues/issue-732-624-706-design.md`(「未確認のまま残ること」の「テストの置き場所」「プロンプトの文言」の 2 行を決めた結果で更新) -- **変更内容:** 全体テスト、フロントマターの検査、文書の鮮度・リンク・行数の検査を通す。配布物を同期する -- **満たす受け入れ条件:** AC17(収束の判定の本体の行が差分に無いことを `git diff` で見る)、AC21、AC22 -- **進め方:** コマンドの実行と結果の記録(テスト駆動は当たらない。検証の工程である) - -## 影響範囲 - -- 印のあるラウンドで、反証を受けていない・支持されていない・根拠の項目を欠く重大な指摘が数えられる。2 者で相手が「立証できない」「範囲外」を返した重大な指摘も数える。収束までのラウンド数が増えることがある(指摘 1 件につき最大 1 回) -- 修正の担当が読む指摘に未反証が加わる。扱いは人の判断待ちと同じ(直す・却下の理由を返す・範囲外として起票する) -- 効果の測定の「この変更の方式」の再現率が、収束の判定と同じ母集合で出る - -## リスクと対処 - -| リスク | 対処 | -| --- | --- | -| 状態の管理スクリプトは 3700 行を超える 1 ファイルで、G3(#729)が同じファイルの別の関数を同時に触る | タスクごとにテストを通す。触る関数を区分の判定・書き込み・数える集合・反証の不足の扱いの 4 つに限り、収束の判定の本体の行を書き換えない。競合は後からマージする側が解く | -| 数える集合が 2 か所にあり、片方だけ変わる | Task 2 の一致のテストが固定する | -| 文書の「2 つだけ」「5 つの区分」の記述が残る | Task 5 で `grep -rn "2 つだけ\|2 区分\|5 つの区分" plugins/ndf/skills/cross-review docs/specifications/cross-review-evidence-based.md` を実行し、0 件を確かめる | -| 実装の後の構造改善で足りるか | 触る範囲が狭く(関数 4 つ・定数 2 つ)、区分のテストが 30 件以上ある。構造改善は後の工程(`cross-refactoring`)で足りる | - -## 切り戻し手順 - -- Pull Request の revert で戻せる。状態ファイルの `version` を上げないため、データの移行は無い。戻した後の状態ファイルに残る `unrefuted` / `unrefuted_reason` は、次の判定で区分が付け直されるときに上書き・削除される(区分は毎回計算し直す) - -## 完了の定義 - -- [ ] AC1〜AC22 をすべて満たし、条件ごとに検証手段と結果が対応している -- [ ] `uv run --with pytest pytest scripts/tests plugins/ndf -q` が通る -- [ ] AC22 の 4 つの検査が終了コード 0 で終わる -- [ ] 収束の判定の本体(`cmd_judge`)の行が差分に含まれない -- [ ] 配布物が同期され、pre-commit の検査が通る diff --git a/issues/issue-732-624-706-requirements.md b/issues/issue-732-624-706-requirements.md deleted file mode 100644 index 9cbeeb63f..000000000 --- a/issues/issue-732-624-706-requirements.md +++ /dev/null @@ -1,265 +0,0 @@ -# cross-review: 誤りを示されていない重大な指摘が数えられずに承認で終わる → 数えない指摘を棄却と軽微な指摘に限る(#732 #624 #706 の要求) - -## 目的 - -**この変更の後、cross-review の収束の判定が数えないのは、棄却された指摘(実行して再現しなかった・否定を受けた)と軽微な指摘だけになる。** 誰にも誤りを示されていない重大な指摘は、担当の数・反証の有無・根拠の項目の有無によらず新しい指摘として残り、修正の工程へ渡る。 - -いまは、誰にも誤りを示されていない重大な指摘が「数えない」区分へ落ち、新しい指摘 0 件として承認で終わる。修正の要る指摘が修正の工程へ渡らない。この形は 3 つの場面で出る。3 つの場面は同じ 1 つの直しで数えられる。立証不足の区分が 2 つの意味を兼ねる状態が解け、区分を読めば未解決の理由が分かる。 - -| 場面 | 課題 | -| --- | --- | -| 反証する担当がいない 1 者のループ | #624 | -| 実行検証も支持も無い指摘 | #706 | -| 起動し直した担当の指摘 | #583 の収束の部分 | - -## 用語と識別子の対応 - -本文は左の業務用語で書く。識別子は、コードブロック・表・受け入れ条件(検査の入力と期待値をそのまま定める)でそのまま使う。 - -| 用語 | 識別子 | 意味 | -| --- | --- | --- | -| 数える | `COUNTED_CLASSIFICATIONS`(数える区分の集合) | 収束の判定(新規性の層)が、そのラウンドの新しい指摘として件数に入れること | -| 重大な指摘 / 軽微な指摘 | `severity` の `major` / `minor`(`major` 以上 / `minor` 以下) | 指摘の重大度。軽微な指摘は承認(`APPROVE`)を妨げない | -| 区分 | `classification` | 印のあるラウンドで指摘ごとに付く値 | -| 棄却 | `rejected`(理由は `rejection_reason`) | 指摘が誤りだと示されたこと。実行して再現しなかった(`not_reproduced`)か、別の担当が否定(`refute`)を返した | -| 反証の機会 | `critiques` が 1 件以上 | 提案者以外の担当が、その指摘へ賛否を返したこと | -| 反証の値: 支持 / 否定 / 立証できない / 範囲外 | `support` / `refute` / `insufficient_evidence` / `out_of_scope` | 反証の担当が返す値のうち、この文書が扱う 4 つ | -| 立証の機会が無かった | — | 反証の機会が無い、または反証はあるが支持も否定も無い。誤りは示されていないが、独立に確かめた担当もいない | -| 未反証 | `unrefuted`(理由は `unrefuted_reason`。値は反証なし `no_critique` / 支持なし `not_supported`) | 新しい区分。重大な指摘で、再現も棄却もされておらず、独立に確かめた担当もいない | -| 人の判断待ち | `needs_human_judgment` | 独立に確かめた担当がいる重大な指摘の区分 | -| 立証不足 | `insufficient_evidence`(区分の値) | 変更後は、再現も棄却もされていない軽微な指摘だけの区分 | -| 独立に確かめた | `support` が 1 件以上、または `origin_runtimes` が 2 者以上 | 提案者以外の担当が支持を返した、または 2 者以上が同じ指摘を独立に出した | -| 根拠の 2 項目 | `evidence` と `falsification`(両方が空でないとき `has_evidence` が真) | 別の担当が確かめるための入力 | -| 印 | `evidence_rounds` | そのラウンドが統合・実行検証・反証を通ったこと。印のあるラウンドだけが区分で絞られ、無いラウンドは全件を数える | -| 代表 | `merged_into` を持たない側 | 統合した組で判定が読む 1 件。束ねられた側は `merged_into` を持つ | -| 却下の記録 | `rejected_findings` | 修正の工程が却下した指摘と理由。次のラウンドのレビュープロンプトへ渡る | -| 状態の管理スクリプト / 測定スクリプト / 反証のプロンプト | `state.py` / `measure.py` / `critique.sh` | いずれも `plugins/ndf/skills/cross-review/scripts/` の下 | -| 区分の判定 / 区分の書き込み / 新しい指摘の数え上げ / 反証の不足の扱い | `_classify_finding` / `_apply_classification` / `_new_finding_count` / `_handle_incomplete_critiques` | 状態の管理スクリプトの内部関数 | -| 収束の判定 / 反証の取り込み | `judge`(本体は `cmd_judge`)/ `collect-critiques` | 状態の管理スクリプトの副コマンド | -| 1 者に絞る引数 | `--only` | 使える担当を 1 者へ絞る起動の引数。意味づけは #727 が持つ | -| 承認で終わる | `approved` | 収束ループの結末 | - -## 対象範囲 - -変えるのは、区分の判定と数える集合、印の外し方、それらの説明と確定仕様、テストである。1 者に絞る引数の扱い・投稿の経路・終了コードは並行する設計が持つ。 - -含む: - -- `state.py` の区分の判定(`_classify_finding` / `_apply_classification`)と、数える区分の集合(`COUNTED_CLASSIFICATIONS`。`measure.py` の同名の定数も) -- `state.py` の反証が揃わないときの印の外し方(`_handle_incomplete_critiques`) -- 反証のプロンプト(`critique.sh`)の `insufficient_evidence` の説明 -- `cross-review` の `docs/04` / `docs/05` / `docs/06` の区分の表 -- 確定仕様 `docs/specifications/cross-review-evidence-based.md` の区分の表 -- テスト 4 ファイル(`tests/test_classify_findings.py` / `tests/test_critiques.py` / `tests/test_measure.py` / `tests/test_skill_layout.py`) - -含まない: - -| 扱わないもの | 理由 | -| --- | --- | -| `--only` の意味づけ・使える担当の決め方・再開の引数 | #727(G1)の設計が持つ。1 者のときに何を数えるかだけをこの文書が決める | -| 起動し直した担当の投稿の重なり、投稿を進行側へ移すこと | #730(G5)の設計が持つ。#583 のうちこの文書が扱うのは、起動し直した担当の指摘が数えられずに収束する部分だけである | -| `read-result` / `judge` の終了コードと出力の変数、結末の語彙 | #729(G3)の設計が持つ。`judge` の 0 / 2 / 7 / 8 とその出力の変数は変えない | -| `cross-refactoring` の収束 | 区分を持たない(`refactor_lib` は `classification` を読まない) | -| 新規性の一致の判定(位置・近傍・本文)と振動の閾値 | 変えるのは母集合だけである(#156 の設計のまま) | -| `minor` 以下の指摘を数えること | `minor` は `APPROVE` を妨げない(`docs/03` の判定の基準)。数えると、`minor` だけの `REQUEST_CHANGES` でラウンドが続く | -| 反証の値(5 つ)の追加・削除 | 反証の担当が返す語彙は変えない。変えるのは、返った値を区分がどう読むかである | -| クラス図・型の追加 | 型を追加・変更しない(関数と辞書で組んだ状態ファイルを扱う)。状態ファイルの形は設計文書の「データ構造」が持つ | -| システムの文脈・配置の図 | 動くのは `state.py` の 1 プロセスで、外部との出入り(`gh`)は変わらない | -| `CHANGELOG.md` と版数 | 配布の工程が書く | - -## 前提 - -数える区分を増やしても成り立つことを 3 つ前提にする。修正の工程が全件を読むこと、却下の記録が同じ論点を止めること、新規性の一致が前のラウンドの指摘を新規に数えないことである。 - -| # | 前提 | 崩れたときに起きること | -| --- | --- | --- | -| 1 | 修正の工程(`fix`)は Pull Request の未解決のスレッドを区分によらず全件読む。区分は収束の判定と測定だけが読む(`plugins/ndf/skills/fix/` と `docs/02-fix-and-rotation.md` に区分を読む箇所が無い。`grep -rn classification` が 0 件) | 数えるだけで修正へ渡らない指摘が生まれ、同じ指摘が毎ラウンド新規に見える | -| 2 | 却下した指摘は `rejected_findings` に位置と理由つきで残り、次のラウンドのレビュープロンプトへ渡る(#156 の 1 本目) | 数える区分が増えたとき、同じ論点が戻る。この記録がそれを止める | -| 3 | 新規性の一致(位置・近傍・本文)は変えない。前のラウンドと一致する指摘は、区分によらず新規に数えない | — | - -## 受け入れ条件 - -22 件である。区分 12 件(AC1〜AC12)、印 1 件(AC13)、退行しない 4 件(AC14〜AC17)、文書 3 件(AC18〜AC20)、全体 2 件(AC21〜AC22)。#624 の再現は AC1、#706 の再現は AC5 と AC7、#583 の収束の部分は AC4 が確かめる。各条件は検査の入力と期待値をそのまま定めるため識別子で書く。業務用語との対応は「用語と識別子の対応」にある。 - -### 区分(#624 / #706 / #583 の収束の部分) - -- [ ] AC1: 前提: 担当が 1 者(`only: "codex"`)で、印の付いたラウンドが 1 つある。そのラウンドに根拠の 2 項目を持つ `major` の指摘が 1 件、反証は 0 件、前のラウンドは無い - 操作: `_new_finding_count` と `judge` を呼ぶ - 結果: `_new_finding_count` が `(1, True)` を返し、`judge` が終了コード 2 で終わる。指摘の `classification` は `unrefuted`(変更前は `(0, True)` と終了コード 0。#624 の再現) -- [ ] AC2: 前提: AC1 の指摘が根拠の 2 項目のどちらかを欠く(`has_evidence` が偽) - 結果: `_new_finding_count` は `(1, True)` を返す。`classification` は `unrefuted` で、`has_evidence` は偽のまま残る -- [ ] AC3: 前提: AC1 の指摘が `minor` である - 結果: `_new_finding_count` は `(0, True)` を返し、`classification` は `insufficient_evidence` である -- [ ] AC4: 前提: 担当が `agy` + `kiro` で、印の付いたラウンドがある。そのラウンドで `kiro` の指摘は反証を持ち、`agy` の根拠を持つ `major` は反証 0 件のまま入っている(起動し直した担当の指摘が、反証を取り込んだ後に取り込まれた形) - 結果: `_new_finding_count` が `agy` の 1 件を数える。`classification` は `unrefuted`(#583 の収束の部分) -- [ ] AC5: 前提: 担当 2 者で、相手が根拠を持つ `major` へ `insufficient_evidence` を返し、`support` も `refute` も無い - 結果: `classification` は `unrefuted` で、数える(変更前は `insufficient_evidence` で数えない。#706 の「反証の担当が `support` を返さなかった」) -- [ ] AC6: AC5 で相手が `out_of_scope` を返したときも、`classification` は `unrefuted` で、数える -- [ ] AC7: 前提: `major` の指摘に `support` が 1 件以上ある - 結果: `has_evidence` の真偽によらず `classification` は `needs_human_judgment` である。変更前は `has_evidence` が偽なら `insufficient_evidence` だった(#706 の「`support` が付いていても根拠の 2 項目の欠落で落ちる」) -- [ ] AC8: `origin_runtimes` が 2 者以上の `major` は、`has_evidence` の真偽によらず `needs_human_judgment` である -- [ ] AC9: `refute` が 1 件以上、または実行検証が `not_reproduced` の指摘は `rejected` になり、`unrefuted` より先に当たる。`reproduced` の指摘は `verified_blocking` / `verified_non_blocking` になる(変更前と同じ) -- [ ] AC10: `unrefuted` の指摘は `unrefuted_reason` を持つ。値は `no_critique`(反証を返した担当が 0 者)か `not_supported`(反証はあるが `support` も `refute` も無い)のどちらかである。他の区分の指摘は `unrefuted_reason` を持たない(区分が変わったときは消える) -- [ ] AC11: `COUNTED_CLASSIFICATIONS` は `verified_blocking` / `needs_human_judgment` / `unrefuted` の 3 つである。`state.py` と `measure.py` の値が一致する -- [ ] AC12: `measure.py` の方式 `proposed` は、印のあるラウンドの `unrefuted` の指摘を `found` に数える - -### 反証の取り直しで揃わないときは印を外す - -- [ ] AC13: 前提: 印が付いたラウンドがある - 操作: `collect-critiques` を実行し、反証が揃わない(終了コード 7) - 結果: `evidence_rounds` からそのラウンドの番号が消え、`_new_finding_count` は payload の全件を数える。取り直した後も揃わないとき(2 度目)も印は付かない - -### 退行しない - -- [ ] AC14: `minor` 以下の指摘の区分は変わらない。`support` が付いた `minor` は `insufficient_evidence`、再現した `minor` は `verified_non_blocking` で、どちらも数えない -- [ ] AC15: 印を持たないラウンドの数え方(payload の全件)は変わらない -- [ ] AC16: `tests/test_classify_findings.py` の既存のテストのうち、期待値を変えるのは旧い区分を固定した 4 件だけである。それ以外は期待値を変えずに通る。4 件は次のとおり - `test_support_without_evidence_is_insufficient` / `test_nothing_matched_is_insufficient` / - `test_a_finding_without_verification_is_readable` / `test_only_two_classifications_are_counted` -- [ ] AC17: `judge` の終了コード(0 / 2 / 7 / 8 / 1)は変わらない。標準出力の変数(`REVIEWER_INTENTS` / `NEW_FINDINGS` / `CARRIED_OVER_THREADS` / `PENDING_POSTS` / `RELAUNCH_AGENTS`)も変わらない。`cmd_judge` の本体の行を書き換えない - -### 文書 - -- [ ] AC18: 規約の 3 文書が新しい区分を書く。`docs/06-evidence.md` の区分の表が 6 行になり、`unrefuted` の行と「数えるのは 3 つ」を持つ。`docs/04-contracts.md` の `classification` の項が 6 区分と数える 3 つを書く。`docs/05-pool-and-convergence.md` の終了基準が、担当 1 者と起動し直した担当の指摘の数え方を書く。次の 4 つがそれぞれ 1 行以上を出す - - ```bash - grep -n "unrefuted" plugins/ndf/skills/cross-review/docs/06-evidence.md - grep -n "unrefuted" plugins/ndf/skills/cross-review/docs/04-contracts.md - grep -n "unrefuted" plugins/ndf/skills/cross-review/docs/05-pool-and-convergence.md - grep -n "印を外す" plugins/ndf/skills/cross-review/docs/06-evidence.md - ``` - -- [ ] AC19: `critique.sh` のプロンプトが「`insufficient_evidence` を返しても指摘は数から落ちない。誤りを示せるなら `refute` を返す」ことを書く。`grep -n "数から落ち" plugins/ndf/skills/cross-review/scripts/critique.sh` が 1 行以上を出す -- [ ] AC20: 確定仕様 `docs/specifications/cross-review-evidence-based.md` が `docs/06-evidence.md` と同じ 6 区分を持つ。持つのは区分の表と区分ごとの行き先の表である。`grep -c "unrefuted" docs/specifications/cross-review-evidence-based.md` が 2 以上を出す - -### 全体 - -- [ ] AC21: `uv run --with pytest pytest scripts/tests plugins/ndf -q` が通る -- [ ] AC22: 次の 4 つが終了コード 0 で終わる - - ```bash - python3 scripts/check-skill-frontmatter.py - python3 scripts/check-doc-staleness.py - python3 scripts/check-markdown-links.py --root . - python3 scripts/check-doc-line-limit.py - ``` - -## 非機能の条件 - -| 大項目 | 条件 | -| --- | --- | -| 運用・保守性 | 数える区分の集合は `state.py` と `measure.py` の 2 か所にあり、一致をテストが固定する(AC11)。区分の理由(`rejection_reason` / `unrefuted_reason`)は状態ファイルに残り、収束の後に「なぜ数えたか・数えなかったか」を読める | -| 移行性 | 状態ファイルの `version` は上げない。この変更より前の状態ファイルは、次に `judge` を呼んだ時点で区分が付け直される(区分は毎回計算し直す)。旧い記録の `classification` を `measure.py` が読むときは、旧い値のまま読む | - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| 公開インタフェース | `state.py` の引数・終了コード・出力の変数は変わらない。状態ファイルの `classification` に値 `unrefuted` が増え、`unrefuted_reason` が増える(読む側は `measure.py` だけ) | -| データ | 状態ファイルの形は変わらない(項目が 1 つ増える。`version` は上げない) | -| 既存の振る舞い | 印のあるラウンドで、反証を受けていない・支持されていない・根拠の項目を欠く重大な指摘が数えられるようになる。2 者で相手が「立証できない」を返した重大な指摘も数える(#156 の判断を改める。理由は設計文書の決定 4)。収束までのラウンド数が増えることがある | - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| テスト | `uv run --with pytest pytest scripts/tests plugins/ndf -q` | -| 静的解析・文書の検査 | AC22 の 4 コマンド | -| 再現手順 | #624: 状態ファイルを最小の形で組み `_new_finding_count` と `cmd_judge` を呼ぶ(AC1)。#706: `_classify_finding` に反証 `insufficient_evidence` 付きの `major` と、`support` 付きで根拠を欠く `major` を渡す(AC5 / AC7) | -| 手動確認 | 無し(すべてテストで確かめる) | - -## 前提とする取り決め - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | Skill の実体は `plugins/ndf/skills/cross-review/`。テストは同じ Skill の `tests/`。`dev.kiro` / `dev.agy` は symlink で参照するため書き写す配布物は無い | -| コーディング規約 | `state.py` は stdlib だけの uv 自己完結スクリプト(`AGENTS.md` / `cross-review/SKILL.md`)。区分の判定は純粋な関数で、出力も終了コードも持たない | -| テスト戦略 | 区分は `_classify_finding` の単体で、数え方は状態ファイルを組んだ `_new_finding_count` と `cmd_judge` の終了コードで確かめる(既存の `test_classify_findings.py` の形) | - -## 境界 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 区分の判定と数える集合の変更、印の外し方、文書と確定仕様の更新、テスト | -| 確認してから行う | 無し | -| 行わない | `--only` の扱い(#727)、投稿の移動(#730)、終了コードと結末の語彙(#729)、`minor` を数えること | - -## 未決 - -| 項目 | 誰が決めるか | 期限 | -| --- | --- | --- | -| 未反証を数えることで収束までのラウンド数がどれだけ増えるか | 実装の後の運用で測定スクリプトの出力を見る | 配布後 | - -## 置き換える既存の受け入れ条件との対応 - -**この文書は、既存の設計 [issue-624-478-648-requirements.md](issue-624-478-648-requirements.md) の P4(AC1〜AC9。PR #667、2026-09-15)を置き換える。** P5(#478 #648)は #727 の設計が持ち、この文書は触らない。#583 の投稿の重なりは #730 の設計が持つ。 - -| 既存 | この文書 | 扱い | -| --- | --- | --- | -| AC1(1 者、根拠付き `major`、反証 0 件 → 数える) | AC1 | 引き継ぐ。区分の名前が `needs_human_judgment` から `unrefuted` へ変わる | -| AC2(1 者、`minor` → 数えない) | AC3 | 引き継ぐ | -| AC3(1 者、根拠なし → 数えない) | AC2 | **改める。** 根拠の項目の欠けは立証の機会が無かった側に入る(親 #732) | -| AC4(起動し直した担当の `major` → 数える) | AC4 | 引き継ぐ。区分は `unrefuted` | -| AC5(揃わないときに印を外す) | AC13 | 引き継ぐ | -| AC6(2 者で相手が `insufficient_evidence` / `out_of_scope` → 数えない) | AC5 / AC6 | **改める。** 数える(設計文書の決定 4) | -| AC7(`origin_runtimes` 2 者の区分は変わらない) | AC8 | 引き継ぐ。根拠の条件は外す | -| AC8(印なしのラウンドと実行検証の区分は変わらない) | AC9 / AC15 / AC16 | 引き継ぐ。期待値を変える既存のテスト 4 件を名指しする | -| AC9(文書の 3 つの grep) | AC18 | 引き継ぐ。語を `unrefuted` に変える | - -## 依頼(原文) - -3 件の依頼はいずれも、指摘が数えない区分へ落ちたまま収束する現象を報告している。#732 が根本原因と採る手を定め、#624 と #706 がそれぞれの再現を持つ。引用のため、識別子と文の長さは元のまま残す。 - -### #732(根本原因の親) - -> **cross-review の収束で「数えない」とする区分。** 場所は `plugins/ndf/skills/cross-review/scripts/state.py` の `_classify_finding`(3443 行目)と `COUNTED_CLASSIFICATIONS`(3431 行目)、規約 `docs/06-evidence.md` の区分の表である。 -> -> - `insufficient_evidence` が 2 つの場合を兼ねている。反証の機会があって支持されなかった場合と、立証の手段が無かった場合(反証する担当がいない・実行検証が無い・根拠の項目が欠ける)である -> - どちらも新規性の数から落ちるため、立証の手段が無かっただけの未解決の `major` が残ったまま収束する -> -> #583 の収束の部分も、同じ区分を通る。 -> -> ## 採る手 -> -> 分離(`extract_strategy`)。「棄却された(`refute` / `not_reproduced`)」と「立証の機会が無かった」を別の区分にし、数えない判断を前者だけに掛ける。 -> -> ## 完了条件 -> -> - 数えない判断が棄却された指摘に限られ、反証の機会が無かった `major` が新規性に残ることを検査が確かめる -> - 各子 issue の再現手順を実行し、現象が出ないことを確かめる(子 issue はその時点の棚卸が「閉じてよい」で閉じる) - -### #624 - -> `cross-review` を `--only codex` で 1 者だけにして回すと、codex が `REQUEST_CHANGES` で新しい指摘を投稿したラウンドでも、`judge` が収束と判定する(ndf 10.10.1、2026-09-13)。 -> -> `--only` で担当が 1 者だと、反証する他の担当がいないため数える区分に入る指摘が 0 件になり、`_evaluate_convergence`(`:2848`)の `findings_measurable and new_findings == 0` が真になる。 -> -> 状態ファイルを最小の形で組み、関数を直接呼んだ再現(`--only codex`、証拠付きの major 1 件、印の付いたラウンド 1): -> -> ```text -> new_finding_count (0, True) classification insufficient_evidence -> round_passes False converged True -> ``` - -### #706 - -> `cross-review` で、**未解決の `major` が残っているのに、指摘が数えない区分 `insufficient_evidence` へ落ち、新規性の母集合から外れて「新しい指摘 0 件」と判定される。** 結果は `approved` になる。 -> -> ideabase の PR #45 のラウンド 2 で、3 件の指摘がいずれも `suggested_check` を実行できず `insufficient_evidence` になり、judge が `NEW_FINDINGS=0` を返した。指摘のうち 2 件は別の担当が `support` を付けており、こちらでもコードを読んで再現を確かめられた(文書が実装と食い違っていた)。 -> -> 数えない区分へ落ちるのは、次のいずれかに当たる指摘である。 -> -> - 反証の担当が `support` を返さなかった(`insufficient_evidence` を返した、または反証が無い) -> - 根拠の 2 項目(`evidence` / `falsification`)のどちらかが欠けている -> - 重要度が `minor` 以下である -> -> 対処の候補: 実行検証ができない指摘は `insufficient_evidence` ではなく別の区分(未検証)として新規性に数える、または他の担当の `support` が付いた指摘は区分によらず数える。 - -## 関連する文書 - -この文書は「何を満たすか」だけを扱う。 - -| 文書 | 何を持つか | -| --- | --- | -| [issue-732-624-706-design.md](issue-732-624-706-design.md) | どう作るか(決定の記録・実測・データ構造・契約・処理の流れ・テスト設計) | -| [issue-624-478-648-requirements.md](issue-624-478-648-requirements.md) | 置き換える前の受け入れ条件(P4)。P5 は #727 の設計が持つ | diff --git a/issues/refactoring-plan-rf790.md b/issues/refactoring-plan-rf790.md deleted file mode 100644 index 3ebd316d7..000000000 --- a/issues/refactoring-plan-rf790.md +++ /dev/null @@ -1,286 +0,0 @@ -# 改修計画 — devbasex/ai-plugins #790 - -`/ndf:cross-refactoring` が提案し、適用した改善項目の記録である。 -理由と手順は提案の時点でしか残らないため、公開の直前に書き出している。 - -- 対象範囲: plugins/ndf/skills/cross-review/scripts, plugins/ndf/skills/cross-review/tests -- 着手前のテスト: uv run --with pytest pytest scripts/tests plugins/ndf -q - -## ラウンド 1(実装 codex / レビュー agy / kiro) - -### R1-001 — `plugins/ndf/skills/cross-review/scripts/measure.py#measure` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| boundary | unit | — | codex / agy | 採用 | 1 | - -**なぜ**: measure 関数に None や辞書以外の型が渡されたとき、空辞書にフォールバックして例外なく指標辞書(pr, prs, rounds, methods, cost, convergence)を返す境界値の振る舞いが固定されていない - -**手順**: 1. evidence_rounds に文字列の有効ラウンド番号、重複する整数、不正値を含み、印付き・印なし両方の findings を持つ state を作る -2. measure を実行する -3. 有効番号が一つの印として扱われ、不正値が無視され、proposed の found・oracle_scope・oracle_base が現在の値になることを比較する - -### R1-002 — `plugins/ndf/skills/cross-review/scripts/critique-round.sh#critique-round` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| branch | integration | — | kiro | 採用 | 1 | - -**なぜ**: run_round を通す既存テストは 2 本とも指摘 0 件で、collect-critiques が 1 回目に 0 を返して 1 回で抜ける経路しか固定していない。exit 7 と CRITIQUE_RETRY_AGENTS を受けて 2 回目の起動を回すループ本体(for _attempt in 1 2)はどのテストも通っていない。 - -**手順**: 1. critique-round.sh を temp ディレクトリへ複製し、隣に stub の critique.sh・monitor.py・state.py・_tmpdir.sh を置く(test_wait_review.py と同じ、兄弟スクリプトを差し替える方式) -2. stub の state.py collect-critiques を、呼び出し回数を記録したうえで 1 回目は stdout に CRITIQUE_RETRY_AGENTS='kiro' を出して終了コード 7、2 回目は終了コード 0 を返すようにする -3. stub の critique.sh は渡された担当名を追記で記録し、対応する pid ファイルを作る -4. critique-round.sh に PR・ROUND・agy kiro を渡して実行し、終了コード 0 を確かめる -5. critique.sh の記録が 2 回目は kiro だけへ絞られている(agy は再起動されない)ことと、collect-critiques が 2 回呼ばれたことを比較する - -### R1-003 — `plugins/ndf/skills/cross-review/scripts/critique-round.sh#critique-round` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| error | integration | — | kiro | 採用 | 1 | - -**なぜ**: collect-critiques が 7 以外を返したときに exit "$COLLECT_RC" でその終了コードを素通しする分岐が固定されていない。既存テストは 0 で抜ける経路だけを見ており、失敗の終了コードがラウンドの外へ伝わるかを誰も確かめていない。 - -**手順**: 1. critique-round.sh を temp ディレクトリへ複製し、隣に stub の critique.sh・monitor.py・state.py・_tmpdir.sh を置く -2. stub の state.py collect-critiques を、呼び出し回数を記録して終了コード 5 で終わるようにする -3. critique-round.sh に PR・ROUND・担当を渡して実行する -4. 終了コードが 5(collect-critiques が返した値)と一致することを確かめる -5. collect-critiques が 1 回だけ呼ばれ、2 回目の起動へ進んでいないことを記録から確かめる - -### R1-004 — `plugins/ndf/skills/cross-review/scripts/rotate-pr.sh#cmd_prepare` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| normal | integration | — | codex | 採用 | 1 | - -**なぜ**: 公開入口 prepare は state、GitHub の PR メタデータ、git log/diff をつないで prepare.json と eval 用の出力を作るが、既存テストは生成済み prepare.json を与えるだけで、この経路を実行していない - -**手順**: 1. 一時 worktree と state を作り、gh pr view・git fetch/log/diff を現状の出力を返す代替コマンドへ差し替える -2. rotate-pr.sh prepare を公開 CLI から実行する -3. 終了コード 0、stdout の shell 代入を評価して得る値、prepare.json の PR・branch・draft・round・git 要約を比較する - -### R1-005 — `plugins/ndf/skills/cross-review/scripts/rotate-pr.sh#execute_squash` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| error | integration | — | codex | 採用 | 1 | - -**なぜ**: 新 PR 作成失敗時に旧 PR を reopen する経路は light モードだけ固定され、同じ ERR trap を使う squash モードでは未固定である - -**手順**: 1. squash の close までは成功し、gh pr create だけが失敗する代替 git・gh と一時 state を用意する -2. rotate-pr.sh execute --mode squash を公開 CLI から実行する -3. 非ゼロ終了、新 PR が存在しないこと、旧 PR の最終状態が open に戻ること、成功用の NEW_PR が出ないことを比較する - -## ラウンド 2(実装 agy / レビュー codex / kiro) - -### R2-001 — `plugins/ndf/skills/cross-review/scripts/measure.py#measure` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| branch | unit | — | codex / agy | 採用 | 1 | - -**なぜ**: measure の oracle 算出において、resolved_thread_positions の要素が辞書形式である経路は固定されているが、リスト内に非辞書要素(文字列や null など)が混在した場合にそれを unmatched として数えて測定を継続する分岐が未固定である - -**手順**: 1. resolved_thread_positions に文字列や null などの非辞書要素を含む状態ファイルを用意する -2. measure を実行する -3. oracle の found が 0、unmatched が 1、ambiguous が 0 と計算され、例外を出さずに全体の測定結果が返ることを確かめる - -### R2-002 — `plugins/ndf/skills/cross-review/scripts/rotate-pr.sh#cmd_execute` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| boundary | integration | — | agy / kiro | 採用 | 1 | - -**なぜ**: cmd_execute の引数解析は不正な --mode 値と未知フラグ(exit 2)は固定済みだが、--mode の直後に値が無いとき ${2:?--mode requires light|squash} で落ちる境界と、そもそも引数が 0 個のときの entrypoint の usage(exit 2)は固定されていない。 - -**手順**: 1. rotate-pr.sh execute --mode を値なしで実行し、終了コードが 0 以外で stderr に --mode requires light|squash が出ることを確かめる -2. rotate-pr.sh を引数なしで実行し、終了コードが 2 で usage が stderr に出ることを確かめる -3. いずれも state.json を用意せず、gh/git を呼ぶ前の引数解析だけで止まることを確かめる - -### R2-003 — `plugins/ndf/skills/cross-review/scripts/rotate-pr.sh#execute_light` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| branch | integration | — | agy / kiro | 取り消し | 1 | - -**なぜ**: execute_light は prepare.json / newtext.json の有無と title/body の null を 4 本の分岐で弾くが、既存テストは prepare.json と newtext.json が両方揃った成功・失敗経路(_Rotation)しか通していない。前提ファイルが欠ける分岐と、newtext.json の title が空・body が null になる分岐はどのテストも到達していない。 - -**手順**: 1. test_rotate_pr_queue.py の _Rotation と同じ組み立て(state.json・bin の gh/git 代替)を使い、gh/git は呼ばれる前に止まることを見込む -2. prepare.json を書かずに execute --mode light を実行し、終了コードが 1 で stderr に prepare.json not found が出ることを確かめる -3. prepare.json は置き newtext.json を書かずに実行し、終了コード 1 と stderr の newtext.json not found を確かめる -4. newtext.json に {"title": "", "body": "x"} を書いて実行し、終了コード 1 を確かめる -5. newtext.json に {"title": "x", "body": null} を書いて実行し、終了コード 1 を確かめる -6. いずれの分岐でも gh の呼び出し記録(GH_CALLS)が空で、旧 PR を close していないことを確かめる - -### R2-004 — `plugins/ndf/skills/cross-review/scripts/rotate-pr.sh#load_state` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| error | integration | — | agy / kiro | 採用 | 1 | - -**なぜ**: load_state は state.json が不在または空のときに終了コード 1 と state.json not found を返して中断するが、rotate-pr.sh の公開入口を経由してこのエラー経路を通すテストが無い。launch-reviewer.sh 等では固定されているが rotate-pr.sh では未固定である - -**手順**: 1. CROSS_REVIEW_TMP_DIR を空の temp ディレクトリに向け、state.json を置かない -2. rotate-pr.sh execute を実行する -3. 終了コードが 1 であることを確かめる -4. stderr に state.json not found が含まれることを確かめる -5. gh/git の代替を PATH に置き、呼び出し記録が空(load_state の手前で止まる)であることを確かめる - -### R2-005 — `plugins/ndf/skills/cross-review/scripts/state.py#cmd_set_current_pr` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| branch | unit | — | codex / agy | 採用 | 1 | - -**なぜ**: cmd_set_current_pr において、pr_history に既に複数の履歴(過去に閉じた PR と現在開いている PR)が存在する場合に、過去 PR のエントリを変更せず直前の現在 PR のみ closed_at と rounds を更新して新 PR エントリを追加する分岐が未固定である - -**手順**: 1. 閉じた過去 PR(closed_at 設定済み)と現在の PR(closed_at が None)を順に含む pr_history を持つ状態ファイルを用意する -2. cmd_set_current_pr(pr, new_pr, head_branch) を実行する -3. 過去 PR の closed_at や rounds が変更されず保持されることを確かめる -4. 直前の現在 PR に closed_at が記録され、rounds がその PR のラウンド数と一致することを確かめる -5. 新 PR エントリが closed_at: None、rounds: 0 で末尾に追加されることを確かめる - -## ラウンド 3(実装 kiro / レビュー codex / agy) - -### R3-001 — `plugins/ndf/skills/cross-review/scripts/state.py#_verify_findings` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | major | codex | 未着手 | 0 | - -**なぜ**: 検証コマンドの正規化・重複実行の抑止・実行結果の分類・各 finding への記録・統合グループ代表の最良結果選択という独立した段階が 1 関数に連続し、実行キャッシュと統合関係の走査を同時に追う必要がある。 - -**手順**: 1. 1 finding の verification record を生成し、コマンド実行キャッシュを利用して結果を分類する helper を抽出する -2. merged_into の関係をたどって代表へ最良の verification を選ぶ処理を別 helper へ抽出する -3. _verify_findings は対象抽出、各 finding の検証、代表結果の集約という 3 段階だけを並べる -4. test_verify_findings.py と findings pipeline の既存テストで、同一コマンドの実行回数、結果優先順位、finding_id、ran_at が不変であることを確認する - -### R3-002 — `plugins/ndf/skills/cross-review/scripts/state.py#_resume_from_state` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | major | codex | 未着手 | 0 | - -**なぜ**: 再開 state の探索・互換フィールドの補完・未解決指摘の引き継ぎ・待ち行列の flush・worktree 同期・結果出力という複数段階が 1 関数に同居し、書き戻しと flush の順序制約まで同じ本体で管理している。 - -**手順**: 1. state の互換フィールド補完と review_instructions 再構成を、state と変更有無を返す helper へ抽出する -2. 書き戻し後の auto-flush と worktree 同期を、順序を保持した再開準備 helper へ抽出する -3. _resume_from_state は state の有無・完了判定、各 helper の呼び出し、既存の _print_init_result だけを順に行う構成へ縮める -4. 既存の再開・carried-over・worktree 同期・run metrics のテストで出力と副作用順が不変であることを確認する - -### R3-003 — `plugins/ndf/skills/cross-review/scripts/state.py#_thread_ids` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| duplication | consolidate_duplication | minor | agy | 未着手 | 0 | - -**なぜ**: _thread_ids における入力データ(リスト、単一辞書、数値等)の辞書要素抽出・正規化ロジックが、同モジュール内の共通関数 _normalize_dict_items と同じ関心をインラインで再実装しており重複している。_thread_positions と同様に _normalize_dict_items を呼び出す形に統一することで、入力値の正規化処理を一元化し一貫性と保守性を高められる。 - -**手順**: 1. _thread_ids 内の辞書要素抽出処理を _normalize_dict_items(value) の呼び出しに置き換える -2. 既存の test_state_thread_ids.py を実行し、各種入力に対する戻り値が変わらないことを確認する - -### R3-004 — `plugins/ndf/skills/cross-review/scripts/state.py#_is_generated_path` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| magic_value | introduce_named_constant | minor | agy | 未着手 | 0 | - -**なぜ**: パス分類判定関数群(_is_dependency_path, _is_config_ci_path, _is_infra_path 等)がモジュール定数(DEPENDENCY_FILENAMES, CONFIG_CI_FILENAMES, INFRA_FILENAMES 等)を参照しているのに対し、_is_generated_path 内にのみロックファイル名の一覧 set リテラルがハードコードされている。名前付きモジュール定数 GENERATED_LOCK_FILENAMES を定義して参照させることで、定数管理の一貫性と保守性を向上できる。 - -**手順**: 1. モジュール定数 GENERATED_LOCK_FILENAMES を定義する -2. _is_generated_path 内の set リテラルを GENERATED_LOCK_FILENAMES の参照に置き換える -3. 既存テストでパス分類の判定動作が不変であることを確認する - -### R3-005 — `plugins/ndf/skills/cross-review/scripts/state.py#_absorb` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| duplication | consolidate_duplication | minor | kiro | 未着手 | 0 | - -**なぜ**: 「2 つの指摘のうち検証結果 (reproduced > not_reproduced > not_run) が高い方を採る」という同じ業務ルールが _absorb (3645-3646 行) と _verify_findings の代表選び直しループ (3137-3138 行) の 2 箇所に _VERIFY_RANK.get(...) > _VERIFY_RANK.get(...) の比較として書かれている。_VERIFY_RANK の順位定義を変えるときや、片方だけ result の取り出し方 (_verify_result vs best.get('result')) を直したときに、もう片方だけ取り残される。両者の docstring がどちらも同じ順位を根拠に挙げており、同じ理由で一緒に変わる重複である。 - -**手順**: 1. _VERIFY_RANK 定義の直後に、2 つの verification dict を受け取り順位の高い方を返すヘルパー _higher_ranked_verification(current, candidate) を追加する(rank は _verify_result で正規化して比較する) -2. _verify_findings の代表選び直しループ (3135-3140) を、best と member['verification'] をヘルパーへ渡して best を更新する形へ置き換える -3. _absorb の verification 継承部 (3644-3648) を、同じヘルパーで rep['verification'] を更新する形へ置き換える -4. test_verify_findings.py / test_merge_duplicates.py / test_state_merge_fix.py を実行し、reproduced/not_reproduced/not_run の組で代表が採る値が変わらないことを確認する - -## ラウンド 4(実装 claude / レビュー codex / kiro) - -### R4-001 — `plugins/ndf/skills/cross-review/scripts/state.py#COUNTED_CLASSIFICATIONS` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| scattered_config | centralize_configuration | major | codex | 採用 | 1 | - -**なぜ**: 収束判定が数える区分の組が state.py と measure.py に重複し、両者の一致をテストで監視している。区分追加時に片方だけ変わると、実行時の収束判定と事後測定が異なる集合を数える。 - -**手順**: 1. scripts 配下の小さな共有モジュールへ COUNTED_CLASSIFICATIONS を移す -2. state.py と measure.py は共有定義を import して各判定に使う -3. 値そのものと両経路の既存出力を既存テストで固定する - -### R4-002 — `plugins/ndf/skills/cross-review/scripts/state.py#_finding_keys` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | split_into_pipeline | major | codex | 採用 | 1 | - -**なぜ**: レビュワーごとのファイル解決、JSON 読み込み、payload と comments の境界検証、path・line・本文の正規化が1つの二重ループに入り、入力境界の失敗とキー変換の責務が分離されていない。 - -**手順**: 1. payload ファイルの読み込みと dict 検証を第1段へ抽出する -2. comments 要素の検証と3要素キーへの変換を第2段へ抽出する -3. _finding_keys はレビュワー列挙から各段をつなぐ処理だけにする -4. 不正 payload・不正 comment・欠損位置・正常な振動照合の既存テストを各段階で実行する - -### R4-003 — `plugins/ndf/skills/cross-review/scripts/state.py#_init_new_state` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | major | codex | 取り消し | 1 | - -**なぜ**: 新規初期化の1関数内に PR 所有権解決、レビュー条件作成、worktree と既存コメントの準備、担当認証、初期 state 構築、保存と表示がネスト関数として同居し、各段階を単独で参照・テストできない。 - -**手順**: 1. ネストされた各段階を同じ入出力のモジュールレベル関数へ順に移す -2. _init_new_state はコンテキストを段階間で受け渡すオーケストレーションだけにする -3. init の再開・新規作成・既存 worktree・コメント取得失敗の既存テストを各抽出後に実行する - -### R4-004 — `plugins/ndf/skills/cross-review/tests/conftest.py#_no_github` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| test_bypasses_module_boundary | move_responsibility | minor | codex | 採用 | 1 | - -**なぜ**: autouse fixture が state_mod を引数に取るため、monitor.py や measure.py だけを検査するテストまで state.py を共通入口から読み込み、GitHub 照会の内部関数を一律に差し替えている。 - -**手順**: 1. subprocess の gh 実行ガードと state.py の既定差し替えを別 fixture に分ける -2. state.py の差し替えは state_mod を利用するテスト経路だけが要求する形へ移す -3. monitor・measure のテストが state.py を読み込まず、state 系テストでは従来どおり実 GitHub 呼び出しを防ぐことを確認する - -### R4-005 — `plugins/ndf/skills/cross-review/scripts/measure.py#_proposed` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | minor | codex | 採用 | 1 | - -**なぜ**: 証拠ラウンドの検査、採用 finding 集合の作成、oracle のラウンド別分母への絞り込み、出力メタデータ付与を1関数が連続して担い、分母規則だけを独立に検証しにくい。 - -**手順**: 1. finding_id から round を引き分母を絞る処理を _scoped_oracle_ids として抽出する -2. oracle が未計算の場合と evidence_rounds が一部だけの場合の戻り値を明示する -3. _proposed は採用集合の作成と出力組み立てだけに残し、既存の measure テストを実行する - -## 見送った項目 - -| ラウンド | 対象 | 兆候・経路 | 理由 | -| --- | --- | --- | --- | -| 1 | `plugins/ndf/skills/cross-review/scripts/rotate-pr.sh#execute_squash` | normal | 1 ラウンドの採用上限 5 件を超えた | -| 1 | `plugins/ndf/skills/cross-review/scripts/state.py#cmd_check_oscillation` | branch | 1 ラウンドの採用上限 5 件を超えた | -| 1 | `plugins/ndf/skills/cross-review/scripts/state.py#cmd_check_oscillation` | normal | 1 ラウンドの採用上限 5 件を超えた | -| 1 | `plugins/ndf/skills/cross-review/scripts/state.py#cmd_collect_critiques` | boundary | 1 ラウンドの採用上限 5 件を超えた | -| 1 | `plugins/ndf/skills/cross-review/scripts/state.py#cmd_collect_critiques` | error | 1 ラウンドの採用上限 5 件を超えた | -| 1 | `plugins/ndf/skills/cross-review/scripts/state.py#cmd_verify_findings` | error | 1 ラウンドの採用上限 5 件を超えた | -| 2 | `plugins/ndf/skills/cross-review/scripts/launch-reviewer.sh#launch_reviewer` | branch | 1 ラウンドの採用上限 5 件を超えた | -| 2 | `plugins/ndf/skills/cross-review/scripts/rotate-pr.sh#execute_light` | branch | コミット 4cd469bc388e45e7c6e77f0793dc45cbad66c08c にトレーラーが欠けています: Item-Id, Round, Impl-Runtime, Impl-Model | -| 3 | `plugins/ndf/skills/cross-review/scripts/state.py#_apply_classification` | long_method | 1 ラウンドの採用上限 5 件を超えた | -| 4 | `plugins/ndf/skills/cross-review/scripts/state.py#_finding_keys` | duplication | 1 ラウンドの採用上限 5 件を超えた | -| 4 | `plugins/ndf/skills/cross-review/scripts/state.py#_print_init_result` | long_parameter_list | 1 ラウンドの採用上限 5 件を超えた | -| 4 | `plugins/ndf/skills/cross-review/scripts/state.py#_init_new_state` | long_method | テストの期待する振る舞いが変わっています(plugins/ndf/skills/cross-review/tests/test_init_body_not_duplicated.py)。構造改善では期待出力を変えません。振る舞いの変更は別の変更に分けてください | diff --git a/issues/refactoring-plan-rf791.md b/issues/refactoring-plan-rf791.md deleted file mode 100644 index a5a60c350..000000000 --- a/issues/refactoring-plan-rf791.md +++ /dev/null @@ -1,370 +0,0 @@ -# 改修計画 — devbasex/ai-plugins #791 - -`/ndf:cross-refactoring` が提案し、適用した改善項目の記録である。 -理由と手順は提案の時点でしか残らないため、公開の直前に書き出している。 - -- 対象範囲: plugins/ndf/scripts/lib, plugins/ndf/skills/cross-review/scripts, plugins/ndf/scripts/tests, plugins/ndf/skills/cross-review/tests -- 着手前のテスト: uv run --with pytest pytest scripts/tests plugins/ndf -q - -## ラウンド 1(実装 codex / レビュー agy / kiro) - -### R1-001 — `plugins/ndf/scripts/lib/auth.py#check_auth` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| branch | unit | — | kiro | 採用 | 1 | - -**なぜ**: 終了コード 0 でも UNAUTHENTICATED_MARKERS を含む出力を未認証と判定する分岐が固定されていない。これはこのモジュールの存在理由そのものだが未固定である - -**手順**: 1. subprocess.run を差し替え、returncode 0 かつ stdout に 'not logged in' を含む結果を返す -2. check_auth(['codex'], ...) を呼ぶ -3. results['codex']['ok'] が False であることを確認する -4. failed があるため die が一度呼ばれることを確認する - -### R1-002 — `plugins/ndf/scripts/lib/auth.py#check_auth` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| error | unit | — | kiro | 未着手 | 0 | - -**なぜ**: 確認コマンドが見つからないとき(FileNotFoundError)に ok=False とし detail を 'コマンドが見つかりません' にする分岐が固定されていない - -**手順**: 1. subprocess.run を差し替え、FileNotFoundError を送出させる -2. check_auth(['codex'], ...) を呼ぶ -3. results['codex']['ok'] が False であることを確認する -4. results['codex']['detail'] に見つからない旨が入り、die が一度呼ばれることを確認する - -### R1-003 — `plugins/ndf/scripts/lib/monitor_outcome.py#append_journal` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| normal | unit | — | agy | 採用 | 1 | - -**なぜ**: monitor_outcome.py には read_journal の単体テストはあるが、ペアとなる append_journal の単体テストが存在しない。親ディレクトリの自動生成や非ASCII文字(UTF-8)の保持を含め、複数回の追記によって順序通りに記録・復元できる正常系が単体レベルで未固定である。 - -**手順**: 1. 一時ディレクトリと複数の outcome 辞書を準備する -2. append_journal を複数回呼び出して追記する -3. read_journal で読み出し、追記された順序と内容が期待通り一致することを検証する - -### R1-004 — `plugins/ndf/scripts/lib/monitor_outcome.py#default_result_path` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| normal | unit | — | agy | 未着手 | 0 | - -**なぜ**: read_launch_outcome の既定フォールバック先として使われる公開関数 default_result_path について、パス命名規則(/-result.json)を直接検証する単体テストが存在しない。 - -**手順**: 1. tmp_dir と stem を準備する -2. default_result_path(tmp_dir, stem) を呼び出す -3. 返された Path が期待される /-result.json と等しいことを検証する - -### R1-005 — `plugins/ndf/scripts/lib/monitor_outcome.py#relaunch_same_agent` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| boundary | unit | — | agy | 未着手 | 0 | - -**なぜ**: relaunch_same_agent は再試行可否の判定に使われる。既存テストは定義済み語彙(REASONSの9語)のみをテストしており、reason=None や空文字列 ""、未知の理由文字列が渡された場合に True を返す境界値の挙動が固定されていない。 - -**手順**: 1. None、空文字列 ""、未知の理由文字列を引数として準備する -2. relaunch_same_agent を呼び出す -3. いずれも戻り値が True であることを検証する - -## ラウンド 2(実装 agy / レビュー codex / kiro) - -### R2-001 — `plugins/ndf/scripts/lib/refresh.py#compare` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| branch | unit | — | kiro | 採用 | 1 | - -**なぜ**: compare は取得失敗・前回記録なし・一致・不一致の 4 分岐を返す純関数だが、対象範囲のどこでも固定されていない。instructions-check の既存テストは refresh.fetch をスタブへ差し替えるため、compare 本体は一度も通らない - -**手順**: 1. ok=False の FetchResult を作り compare(result, 'sha256:aa') を呼ぶ -2. ok=True で fingerprint を持つ FetchResult を作り、previous を None・空文字・同じ指紋・異なる指紋の 4 通りで呼ぶ -3. 実行して得た戻り値(取得失敗・前回記録なし・一致・不一致を表す文字列)を期待値として固定する - -### R2-002 — `plugins/ndf/scripts/lib/refresh.py#fetch` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| error | unit | — | kiro | 取り消し | 1 | - -**なぜ**: fetch は取得の失敗を例外にせず FetchResult(ok=False, error=...) へ畳む分岐を持ち、error の文言は _reason が例外の種類ごとに書き分ける。この経路は未固定で、instructions-check のテストは fetch 自体を差し替えるため通らない。opener は差し替え用に設計された引数である - -**手順**: 1. opener に OSError を送出する呼び出し可能を渡し fetch(url, timeout, opener) を呼ぶ -2. urllib.error.HTTPError と urllib.error.URLError を送出する opener でも同様に呼ぶ -3. 3 通りとも ok が False で、実行して得た error の文言(種類ごとに異なる)を期待値として固定する - -### R2-003 — `plugins/ndf/scripts/lib/refresh.py#refresh` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| normal | integration | — | kiro | 採用 | 1 | - -**なぜ**: refresh は fetch・row・compare をつないで全件の 1 行と失敗件数を返す公開入口だが、この配線を通す固定が対象範囲に無い。取れなかった URL を黙って落とさず件数へ数える振る舞いが未固定である - -**手順**: 1. 成功と失敗を混ぜて返す opener を差し替えで用意する -2. name/checked_at/claim を持つ複数の source を渡し refresh(sources, timeout, opener) を呼ぶ -3. 返る行数が source 数と一致し、失敗件数が失敗した source 数と一致することを固定する -4. 各行に source の name と取得の成否が含まれることを、実行して得た値で固定する - -## ラウンド 3(実装 kiro / レビュー codex / agy) - -### R3-001 — `plugins/ndf/scripts/lib/monitor_outcome.py#OUTCOME_KEYS` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| scattered_config | centralize_configuration | major | kiro | 採用 | 1 | - -**なぜ**: 監視の結果ファイルのキーの一覧が 3 か所に散っている。monitor_outcome.OUTCOME_KEYS(14 個・runtime では未使用)と、実際に辞書を組み立てる monitor.py の _record_outcome(`phase` を含む 15 個)と、test_monitor_outcome_file.py の独自コピー(15 個)である。正本のはずの OUTCOME_KEYS が `phase` を欠いており既に食い違っている。キーを足すたびにどこかが古くなる。 - -**手順**: 1. OUTCOME_KEYS へ `phase` を加え、契約文書の並びと揃える -2. monitor.py の _record_outcome が組み立てた辞書のキー集合を OUTCOME_KEYS と突き合わせる assert を置き、食い違いをその場で落とす(値の生成は現状のまま) -3. read_journal / read_outcome の既存テストと test_monitor_outcome_file.py を実行し、キー集合の期待が変わっていないことを確かめる - -### R3-002 — `plugins/ndf/skills/cross-review/scripts/state.py#_handle_no_result_round` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | major | codex | 採用 | 1 | - -**なぜ**: 1 関数に理由の集約と出力、再起動不能時の終了処理、再起動済み時の終了処理、再起動対象の記録とシェル向け出力という別々の段階が同居し、状態保存と終了条件が複数箇所に散っている。 - -**手順**: 1. 担当別理由の収集と NO_RESULT_REASONS 出力を小さな関数へ抽出する -2. final・ended_at・保存・die を行う共通の異常終了処理を抽出する -3. 再起動対象の記録と互換出力を行う処理を抽出する -4. usage_limit、再起動済み、再起動要求の既存テストで終了コード・state・出力順を固定する - -### R3-003 — `plugins/ndf/skills/cross-review/scripts/state.py#_print_init_result` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_parameter_list | introduce_parameter_object | major | codex | 採用 | 1 | - -**なぜ**: 初期化結果という同じ概念を表す 11 引数を位置で受け取り、特に連続する 3 個の bool と末尾の件数・再開フラグは呼び出し側で順序を取り違えても検出しにくい。新規初期化と再開の 2 経路が同じ組を渡している。 - -**手順**: 1. 出力対象を表す _InitResult の値オブジェクトを定義する -2. _print_init_result の引数を _InitResult 1 個へ置き換える -3. 新規初期化経路と再開経路で名前付きフィールドから _InitResult を構築する -4. 両経路の既存テストで標準出力が不変であることを確認する - -### R3-004 — `plugins/ndf/scripts/lib/monitor.py#monitor_agent` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| duplication | consolidate_duplication | minor | codex | 採用 | 1 | - -**なぜ**: 監視ループの 5 つの終了分岐が、同じ _finish_monitor(status, outcome, (config.log_prefix, agent)) 呼び出しを繰り返している。終了時に渡すログ文脈を変更すると各分岐を同時に直す必要がある。 - -**手順**: 1. status とログ文脈を閉じ込めて outcome を受け取る局所的な finish 処理を定義する -2. PID 不正・完了・timeout・early error・process exit・stall の各終了分岐を同じ入口へ寄せる -3. monitor_agent を通る既存テストで status、ログ、終了結果が不変であることを確認する - -### R3-005 — `plugins/ndf/skills/cross-review/scripts/state.py#_verify_findings` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | minor | kiro | 採用 | 1 | - -**なぜ**: 1 つの関数が 2 つの独立した段を通しで行う。前段は各指摘の suggested_check を(重複を除いて)実行し verification を記録する反復、後段は束ねた組の代表へ最良の結果を選び直す反復である。段ごとに名前が付き、共有するのは targets と by_id だけである。 - -**手順**: 1. 前段を _run_finding_checks(targets, allowed, work, codes, run) として抽出し、実行済みコマンドの対応表を関数内へ閉じる -2. 後段を _propagate_best_verification(targets, by_id) として抽出する -3. _verify_findings は codes / run / targets / by_id を用意し、2 つを順に呼ぶだけにする -4. test_verify_findings.py を実行して verification の記録が変わらないことを確かめる - -## ラウンド 4(実装 claude / レビュー codex / kiro) - -### R4-001 — `plugins/ndf/skills/cross-review/scripts/state.py#_resume_from_state` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | major | codex | 採用 | 1 | - -**なぜ**: 既存stateの探索、旧形式の補完、追加レビュー観点の再計算、引き継ぎ記録、保存、待ち行列flush、worktree同期、機械可読出力までが1関数に直列で置かれ、副作用の順序を長い本体とコメントから追う必要がある。各段階には独立した終了条件と入出力があり、名前を付けて分離できる。 - -**手順**: 1. cmd_initを通る既存の再開テストで、stateなし、final済み、旧形式補完、追加観点更新、引き継ぎ、flush後の同期と出力を現状固定する -2. stateファイルの探索と再開可否判定を、stateとpathを返す関数へ抽出する -3. 旧形式の補完、manual指示の反映、review_instructions再計算、carried_over記録を、変更有無も返す関数へ抽出する -4. 保存後のauto_flush、tmp_dir解決、登録済みworktree同期を副作用順序が見える関数へ抽出する -5. _resume_from_stateを各段階の呼び出しと_print_init_resultだけにし、再開関連テストと全体テストで出力と保存順序が不変であることを確認する - -### R4-002 — `plugins/ndf/skills/cross-review/scripts/state.py#_init_new_state` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | major | codex | 取り消し | 1 | - -**なぜ**: 新規初期化の1関数に、PR所有権の解決、レビュー観点の構築、worktreeと既存コメントの準備、認証確認、state構築、永続化と出力が同居し、さらに5個のローカル関数が本体を約200行へ広げている。各段階は既に名前と入出力を持つため、モジュールレベルへ抽出すれば段階単位で読めて個別にテストできる。 - -**手順**: 1. 既存の入口テストで、新規worktree、既存worktree、変更ファイル取得fallback、認証失敗、初期state出力の経路を現状固定する -2. _resolve_pr_and_ownership と _prepare_review_instructions をモジュールレベル関数へ抽出し、既存のcontext型を入出力に使う -3. _prepare_worktree_and_comments をモジュールレベル関数へ抽出し、worktree作成より後に_tmp_dirを呼ぶ順序とコメント取得失敗時の停止を保つ -4. _prepare_initial_assignment、_build_initial_review_state、_finalize_initial_state をモジュールレベルへ抽出し、_init_new_stateを段階を順に呼ぶオーケストレーションだけにする -5. 初期化関連テストと全体テストを実行し、標準出力、stateの内容、副作用の順序が不変であることを確認する - -### R4-003 — `plugins/ndf/skills/cross-review/scripts/state.py#cmd_check_oscillation` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| conditional_chain | replace_with_lookup_table | minor | kiro | 取り消し | 1 | - -**なぜ**: 現ラウンドの各指摘について _finding_match_kind の戻り値 ("exact"/"near"/"body") を if/elif で数えている。種別ごとの集計は種別を増やすたびに分岐を足すことになる。あわせて、collect_keys クロージャは _finding_keys(st, pr, round_no) をそのまま呼ぶだけの指標なしの間接参照で、読み手が本体を追う負荷を増やしている。test_state_check_oscillation.py と test_state_oscillation_matching.py が cmd_check_oscillation / _finding_match_kind を通す。 - -**手順**: 1. exact/near/same_body の 3 変数と if/elif/elif の加算を、collections.Counter に対する `Counter(_finding_match_kind(k, prev) for k in curr)` へ置き換える -2. overlap_count は None 以外の合計として counts の値の総和から出す -3. info の表示は counts.get("exact", 0) 等から読む -4. collect_keys クロージャを消し、呼び出し 2 箇所を _finding_keys(st, pr, prev_round_no) / (curr_round_no) の直接呼び出しに戻す -5. テストを実行して振る舞い不変を確認する - -### R4-004 — `plugins/ndf/skills/cross-review/scripts/state.py#_normalize_fix_result` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | minor | kiro | 採用 | 1 | - -**なぜ**: 1 関数に (a) 別名 fallback(fix_commit/commit_sha、fixed_count/fixed)、(b) deferred の list/dict/int による件数の場合分けと dict 要素への正規化、(c) rejected の同じ正規化、(d) 記録用辞書の組み立て、が同居する。deferred と rejected はどちらも _normalize_dict_items + 件数決定という同型の処理で、片方だけ直すと食い違いうる。test_state_merge_fix.py と test_state_ci_classification.py が cmd_merge_fix 経由で通す。 - -**手順**: 1. 「_normalize_dict_items した項目」と「保存する件数」を組で返す小関数 _normalize_deferred_like(raw) を抽出する(list/dict は展開件数、劣化表現の int/str は _count の値、という現在の規則をそのまま移す) -2. deferred と rejected の両方をこの関数で得る(現状 rejected の件数は常に _count なので、raw の型で分岐する現在の deferred 規則へ揃える形にはせず、抽出関数は deferred の規則を表し、rejected は従来どおり _count を使うなら別に保つ。振る舞いを変えないため、まず deferred 経路だけを抽出する) -3. 別名 fallback(fix_commit/fixed_count)を _resolve_fix_aliases として抽出する -4. 末尾の辞書組み立てを、抽出した値を差し込む形へ整える -5. テストを実行して出力の辞書が不変であることを確認する - -## ラウンド 5(実装 codex / レビュー agy / kiro) - -### R5-001 — `plugins/ndf/scripts/lib/post_queue.py#Queue.flush` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | major | codex | 取り消し | 1 | - -**なぜ**: 1 件の処理の中に、壊れた JSON の停止判定、既投稿の照合と削除、送信成功時の応答保存と削除、送信失敗時の再試行情報保存と rate limit 判定が直列に並び、flush 自体が順序制御と各項目の状態遷移の両方を担っている。 - -**手順**: 1. 読み取り済み項目について既投稿・送信成功・送信失敗を処理する部分を Queue の補助メソッドへ抽出する -2. 補助メソッドの戻り値で継続または停止と rate_limited を表し、flush は連番走査と集計だけを担うようにする -3. 壊れた項目で停止する既存経路は flush 側に残し、項目順序と停止位置を変えない -4. test_post_queue.py と cross-review/tests/test_queue_idempotency.py で skipped・sent・failed・remaining とファイル削除順を確認する - -### R5-002 — `plugins/ndf/skills/cross-review/scripts/state.py#_sync_worktree` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | major | codex | 取り消し | 1 | - -**なぜ**: PR head の取得方法の決定、strict 時の同期済み判定と失敗処理、worktree の reset・未追跡ファイル掃除、同期結果の表示という独立した段階が 1 関数に同居している。HeadRef と旧来の文字列 head の分岐も取得段階に閉じず、後続の制御へ have_base・target・label の組で持ち越されている。 - -**手順**: 1. HeadRef と文字列 head から have_base・target・label を解決する取得段階を補助関数へ抽出する -2. strict 時の同期済み早期終了と基準取得失敗の判定を補助関数へ抽出する -3. _sync_worktree は取得、判定、reset、clean、結果表示の順序だけを示す構成にする -4. test_state_sync_worktree.py と test_state_offline_fetch.py で既存の strict/fallback/失敗時終了コードを確認する - -### R5-003 — `plugins/ndf/skills/cross-review/scripts/state.py#_load_payload` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| duplication | consolidate_duplication | minor | kiro | 取り消し | 0 | - -**なぜ**: payload が dict でない場合と comments が list でない場合の 2 経路が、`info(f"⚠ {agent}: ...形式不正で、判定は中断します")` を出して `return None` する同じ形で並ぶ。返す条件(dict でない / list でない)と型名の埋め込みが繰り返され、警告文の末尾の定型句も重複する。片方の文言だけ直すと 2 経路のメッセージが食い違う。test_review_findings.py が不正 payload での 0 件記録を固定している。 - -**手順**: 1. `_reject_payload(agent: str, path: pathlib.Path, detail: str) -> None` を追加し、`info(f"⚠ {agent}: {detail}({path}...)。指摘の記録は 0 件です。review launcher の出力形式不正で、判定は中断します")` を出して None を返す -2. dict でない場合と comments が list でない場合の 2 経路を、type 名を含む detail 文字列を渡す呼び出しへ置き換える -3. 部分不正(items != raw)は継続する経路のため対象にせず、そのまま残す -4. `uv run --with pytest pytest plugins/ndf/skills/cross-review/tests/test_review_findings.py -q` で 0 件記録と警告が不変なことを確認する - -### R5-004 — `plugins/ndf/scripts/lib/metrics.py#format_report` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| duplication | consolidate_duplication | minor | kiro | 取り消し | 1 | - -**なぜ**: unmeasured と assumed の 2 節が同じ形(見出し + 空行を lines へ足し、dict.fromkeys で重複を除いた項目を `- {w}` で並べる)で並んでいる。片方だけ書式を変えると 2 節の見た目が食い違う。同じ業務ルール(分離・代用の一覧の出し方)に由来し、変わるときは一緒に変わる。既存テスト test_models_and_metrics.py が format_report の出力を固定している。 - -**手順**: 1. `_emit_bullet_section(lines: list[str], title: str, items: list[str]) -> None` を追加し、items が空でなければ `['', f'## {title}', '']` と `[f'- {w}' for w in dict.fromkeys(items)]` を lines へ足す -2. unmeasured の if ブロックを `_emit_bullet_section(lines, "集計から分離したラウンド", metrics["unmeasured"])` へ置き換える -3. assumed の if ブロックを `_emit_bullet_section(lines, "指定値で代用したラウンド", metrics.get("assumed") or [])` へ置き換える -4. 比較の限界(COMPARISON_CAVEATS)節は常に出るため対象外のまま残す -5. `uv run --with pytest pytest scripts/tests plugins/ndf -q` で出力が不変なことを確認する - -### R5-005 — `plugins/ndf/skills/cross-review/scripts/state.py#build_parser` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | minor | codex | 採用 | 1 | - -**なぜ**: init の多数のオプション定義と、ラウンド進行・結果取込・検証・報告に属する 11 個の副コマンド登録が 1 関数に連続しており、個別コマンドの引数変更でも 125 行の構築処理全体を読む必要がある。副コマンドごとに独立した名前を付けられる段階になっている。 - -**手順**: 1. init のパーサ設定を専用の補助関数へ抽出する -2. 各副コマンドの parser 作成・引数追加・set_defaults を用途別の小さな登録関数へ抽出する -3. build_parser はトップレベル parser と subparsers を作り、登録関数を順に呼んで返すだけにする -4. test_state_subcommand_help.py、test_state_review_pool.py、test_findings_pipeline_wiring.py で選択肢・help・func の対応が不変であることを確認する - -## ラウンド 6(実装 agy / レビュー codex / kiro) - -### R6-001 — `plugins/ndf/scripts/lib/auth.py#check_auth` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | major | codex | 採用 | 1 | - -**なぜ**: スキップ判定、CLI ごとの subprocess 実行、例外の認証結果への変換、結果の集約、全体の失敗通知が 1 関数に同居しており、個別 CLI のプローブ規則と複数 CLI の制御を別々に読めない。既存の test_auth_probe.py が未知 runtime、成功、未認証マーカー、コマンド不在を公開入口から固定している。 - -**手順**: 1. 1 runtime の probe 実行と FileNotFoundError・TimeoutExpired の認証結果への変換を、runtime と probe を受け取る補助関数へ抽出する -2. ok・detail・command の結果を check_auth が受け取り、既存どおり info 出力、failed 集約、die 判定を行う形へ置き換える -3. test_auth_probe.py を実行し、戻り値、通知文、die 呼び出しが不変であることを確認する - -### R6-002 — `plugins/ndf/skills/cross-review/scripts/state.py#_confirm_flushed` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | major | codex | 採用 | 1 | - -**なぜ**: 待ち行列項目の適用可否判定、response からの URL 復元、対象ラウンド探索、GitHub 到達確認、結果なしまたは成功状態への更新、永続化が 1 関数に直列で同居している。投稿確認は収束可否に関わるため、対象特定と状態遷移を独立した名前で読める構造にする価値が高く、test_state_queue_judge.py が送信済み・冪等スキップ・未到達の経路を固定している。 - -**手順**: 1. item の kind・extra・response を解釈して agent、round、review URL を返す処理を補助関数へ抽出する -2. state の rounds から書き戻し対象を探す処理を補助関数へ抽出する -3. _confirm_flushed は早期 return、到達確認、既存と同じ target 更新、_save の順序だけを担うよう置き換える -4. test_state_queue_judge.py を実行し、queued、review_url、not_posted、保存回数を含む観測結果が不変であることを確認する - -### R6-003 — `plugins/ndf/skills/cross-review/scripts/state.py#_guard_previous_round` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | major | codex | 未着手 | 0 | - -**なぜ**: 旧形式 state の verdict 復元、修正記録の必須判定、申告済み Resolve の GitHub 照会、未解決 ID の検出という独立した 2 つのガードが 1 関数に同居している。各ガードは異なる理由で変更され、test_state_round_guard.py が修正記録なし、照会不能、未解決残存、正常通過を固定している。 - -**手順**: 1. 保存済み verdict が無い場合の no_result・pass からの復元を補助関数へ抽出する -2. fix の resolved_thread_ids と対象 PR を受け、照会不能時の通知または未解決 ID を判定する補助関数へ抽出する -3. _guard_previous_round は修正記録ガードと Resolve 状態ガードを順に呼ぶ構成へ置き換える -4. test_state_round_guard.py を実行し、終了コード、GitHub 照会先、警告、正常通過が不変であることを確認する - -### R6-004 — `plugins/ndf/skills/cross-review/scripts/state.py#_is_generated_path` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| scattered_config | centralize_configuration | minor | kiro | 未着手 | 0 | - -**なぜ**: 兄弟の判定述語(_is_dependency_path は DEPENDENCY_FILENAMES、_is_infra_path は INFRA_FILENAMES、_is_generated_path 自身も GENERATED_MARKERS)はいずれもモジュール定数を引くのに、_is_generated_path だけ lockfile 名の集合(package-lock.json / go.sum / cargo.lock など 9 件)を関数本体へじか書きしている。うち package-lock.json 等は DEPENDENCY_FILENAMES にも重複して載っており、lockfile を足すとき 2 か所を直す必要があるうえ、どこに定義があるか読み手が探す。定義を 1 か所へ寄せて兄弟と同じ形にする。 - -**手順**: 1. モジュール定数群(DEPENDENCY_FILENAMES / GENERATED_MARKERS / INFRA_FILENAMES の並び)へ GENERATED_LOCKFILES を新設し、現在インラインにある 9 件の集合をそのまま移す -2. _is_generated_path の本体を `name in GENERATED_LOCKFILES` へ置き換え、インラインの集合リテラルを消す -3. test_gap を埋めるため、先に go.sum / cargo.lock(DEPENDENCY_FILENAMES に無く、インライン集合にだけある名前)で generated カテゴリが立つ現状固定テストを test_state_auto_review_templates.py に追加し、移動の前後で同じ結果になることを確認する - -## 見送った項目 - -| ラウンド | 対象 | 兆候・経路 | 理由 | -| --- | --- | --- | --- | -| 1 | `plugins/ndf/scripts/lib/statefile.py#save` | error | 1 ラウンドの採用上限 5 件を超えた | -| 1 | `plugins/ndf/scripts/lib/statefile.py#save` | normal | 1 ラウンドの採用上限 5 件を超えた | -| 1 | `plugins/ndf/skills/cross-review/scripts/state.py#cmd_read_result` | boundary | 1 ラウンドの採用上限 5 件を超えた | -| 1 | `plugins/ndf/skills/cross-review/scripts/state.py#cmd_read_result` | error | 1 ラウンドの採用上限 5 件を超えた | -| 2 | `plugins/ndf/scripts/lib/refresh.py#fetch` | error | テストの期待する振る舞いが変わっています(plugins/ndf/scripts/tests/test_refresh.py)。構造改善では期待出力を変えません。振る舞いの変更は別の変更に分けてください | -| 3 | `plugins/ndf/scripts/lib/monitor.py#_record_outcome` | long_method | 1 ラウンドの採用上限 5 件を超えた | -| 4 | `plugins/ndf/skills/cross-review/scripts/state.py#_init_new_state` | long_method | テストの期待する振る舞いが変わっています(plugins/ndf/skills/cross-review/tests/test_init_body_not_duplicated.py)。構造改善では期待出力を変えません。振る舞いの変更は別の変更に分けてください | -| 4 | `plugins/ndf/skills/cross-review/scripts/state.py#cmd_check_oscillation` | conditional_chain | コミット 95387271f1b307d6c0a88f1cde8f6401fe3a6111 にトレーラーが欠けています: Item-Id, Round, Impl-Runtime, Impl-Model | -| 5 | `plugins/ndf/scripts/lib/post_queue.py#Queue.flush` | long_method | どの改善項目にも割り当てられていないコミットが 1 件(311bf7f)。検証を回避した変更や、状態と実差分の食い違いを Pull Request に残さないため、この適用ラウンドを取り消します | -| 5 | `plugins/ndf/skills/cross-review/scripts/state.py#_sync_worktree` | long_method | どの改善項目にも割り当てられていないコミットが 1 件(311bf7f)。検証を回避した変更や、状態と実差分の食い違いを Pull Request に残さないため、この適用ラウンドを取り消します | -| 5 | `plugins/ndf/scripts/lib/metrics.py#format_report` | duplication | どの改善項目にも割り当てられていないコミットが 1 件(311bf7f)。検証を回避した変更や、状態と実差分の食い違いを Pull Request に残さないため、この適用ラウンドを取り消します | -| 5 | `plugins/ndf/skills/cross-review/scripts/state.py#_load_payload` | duplication | どの改善項目にも割り当てられていないコミットが 1 件(1e8216f)。検証を回避した変更や、状態と実差分の食い違いを Pull Request に残さないため、この適用ラウンドを取り消します | diff --git a/issues/refactoring-plan-rf793.md b/issues/refactoring-plan-rf793.md deleted file mode 100644 index 6e5dda055..000000000 --- a/issues/refactoring-plan-rf793.md +++ /dev/null @@ -1,399 +0,0 @@ -# 改修計画 — devbasex/ai-plugins #793 - -`/ndf:cross-refactoring` が提案し、適用した改善項目の記録である。 -理由と手順は提案の時点でしか残らないため、公開の直前に書き出している。 - -- 対象範囲: plugins/ndf/scripts/lib, plugins/ndf/skills/cross-review/scripts, plugins/ndf/scripts/tests, plugins/ndf/skills/cross-review/tests -- 着手前のテスト: uv run --with pytest pytest scripts/tests plugins/ndf -q - -## ラウンド 1(実装 codex / レビュー agy / kiro) - -### R1-001 — `plugins/ndf/scripts/lib/assignment.py#detect_host` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| branch | unit | — | agy | 採用 | 1 | - -**なぜ**: detect_host は収束ループ共通層においてホストを確定する重要関数であり、誤判定すると母集合が狂う致命的な影響を持つ。しかし共通層テスト(plugins/ndf/scripts/tests/)には単体テストが全く存在しない。明示指定(explicit)、環境変数ヒント(HOST_ENV_HINTS)の順序による推定、および手掛かりがない場合の例外送出の各分岐を共通層単体テストとして固定する必要がある。 - -**手順**: 1. plugins/ndf/scripts/tests/test_lib_assignment.py に test_detect_host_* を追加する。 -2. 明示指定分岐: HOST_RUNTIMES に含まれる名前を指定したときに (host, 'explicit') が返り、無効な名前を指定したときに AssignmentError が送出されることを検証する。 -3. 環境変数推定分岐: CLAUDE_PLUGIN_ROOT, CODEX_HOME, KIRO_AGENT 等の環境変数ヒントを含む辞書を渡し、正しいホスト名と 'env' が返ることを検証する。 -4. 推定不能分岐: 環境変数が空辞書(またはヒントなし)の場合に、既定値を勝手に置かず AssignmentError が送出されることを検証する。 - -### R1-002 — `plugins/ndf/scripts/lib/assignment.py#review_seats` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| branch | unit | — | agy | 採用 | 1 | - -**なぜ**: assignment.py で新設された review_seats は、cross-review において各ラウンドのレビュワー2席を割り当てるコア関数である。しかし共通層テスト(plugins/ndf/scripts/tests/)には単体テストが一切存在しない(別スキル cross-refactoring のテスト側に暫定配置されているのみ)。len(available) の人数(3者以上の輪番、2者の固定、1者時の fallback または副席 -2 補填、0者時の fallback 2席割当および fallback 空時の例外送出)の全分岐の振る舞いを共通層の単体テストとして固定する必要がある。 - -**手順**: 1. plugins/ndf/scripts/tests/test_lib_assignment.py に test_review_seats_* を追加する。 -2. 3者以上: available=['codex', 'agy', 'kiro'] でラウンド1〜3を実行し、available の順序を保った2席が輪番で選ばれることを検証する。 -3. 2者: available=['codex', 'kiro'] で複数ラウンドを実行し、ラウンド番号によらず常にその2者が返ることを検証する。 -4. 1者: available=['codex'], fallback=['claude'] で ['codex', 'claude'] が返り、fallback が空または available と重複する場合は ['codex', 'codex-2'] が返ることを検証する。 -5. 0者: available=[], fallback=['claude'] で ['claude', 'claude-2'] が返り、fallback も空の場合は AssignmentError となることを検証する。 -6. round_no < 1 の場合に AssignmentError が送出されることを検証する。 - -### R1-003 — `plugins/ndf/scripts/lib/assignment.py#review_seats` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| error | unit | — | codex | 採用 | 1 | - -**なぜ**: 0 人でも fallback がある経路は状態初期化から固定されているが、available と fallback がともに空の公開入口が AssignmentError になる経路は未固定である。 - -**手順**: 1. round_no=1、available=[]、fallback=[] で公開入口を呼ぶ -2. AssignmentError が送出されることを観測する -3. 例外の利用者向け理由から、使える者と埋め合わせ候補がともに無いことを示す要点だけを確認する - -### R1-004 — `plugins/ndf/scripts/lib/assignment.py#seat_runtime` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| boundary | unit | — | agy | 取り消し | 1 | - -**なぜ**: seat_runtime は正規表現 SEAT_PATTERN(^(claude|codex|agy|kiro)(-[2-9])?$)に従って席名を検証・抽出するが、共通層テストに境界値・異常値のテストが存在しない。接尾辞の数値境界(-1 は不可、-2〜-9 は可、-10 は不可)、区切り文字違い(_2)、未知のランタイム、空文字列等で AssignmentError が送出される境界値の振る舞いを単体レベルで固定する必要がある。 - -**手順**: 1. plugins/ndf/scripts/tests/test_lib_assignment.py に test_seat_runtime_rejects_malformed_seat を追加する。 -2. 接尾辞の数値境界: 'kiro-1'(下限未満)、'kiro-10'(上限超過)で AssignmentError が発生することを検証する。 -3. 区切り形式・重複の境界: 'claude_2'(アンダースコア)、'kiro-2-3'(ハイフン重複)、空文字列 '' で AssignmentError が発生することを検証する。 -4. 未知のランタイム: 'gemini', 'gpt' 等の ALL_RUNTIMES 外の名称で AssignmentError が発生することを検証する。 - -### R1-005 — `plugins/ndf/scripts/lib/assignment.py#seat_runtime` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| normal | unit | — | agy | 採用 | 1 | - -**なぜ**: assignment.py で新設された seat_runtime(seat: str) は、席名から基底ランタイム名を取り出す共通層関数であり、結果受け口・起動スクリプト・監視処理で広く使われる。しかし共通層テスト(plugins/ndf/scripts/tests/)には単体テストが存在しない。接尾辞なしのランタイム名(claude, codex, agy, kiro)および同一ランタイムの副席名(-2〜-9 接尾辞)から正確にランタイム名が抽出される正常系の振る舞いを共通層単体テストとして固定する必要がある。 - -**手順**: 1. plugins/ndf/scripts/tests/test_lib_assignment.py に test_seat_runtime_extracts_runtime_name を追加する。 -2. ALL_RUNTIMES の全ランタイム名('claude', 'codex', 'agy', 'kiro')をそのまま渡した場合に、同一のランタイム名が返ることを検証する。 -3. ハイフン付き席名('kiro-2', 'claude-9', 'agy-3' 等)を渡した場合に、接尾辞を除去した基底ランタイム名が正しく返ることを検証する。 - -## ラウンド 2(実装 agy / レビュー codex / kiro) - -### R2-001 — `plugins/ndf/scripts/lib/assignment.py#assign` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| error | unit | — | codex / agy | 採用 | 1 | - -**なぜ**: assign は 8 ラウンド周期の割り当てを行う公開関数であり正常系は固定されているが、round_no < 1(0 や負数)が渡された場合に AssignmentError を送出するエラー経路が scripts/tests 内で固定されていない。 - -**手順**: 1. 有効な各ホスト(claude, codex, agy, kiro)について assignment.assign(0, host) および assignment.assign(-1, host) を呼び出す -2. どちらも assignment.AssignmentError が送出されることを検証する -3. 送出された例外メッセージに「ラウンド番号は 1 以上です」が含まれることを検証する - -### R2-002 — `plugins/ndf/scripts/lib/assignment.py#resolve_participants` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| boundary | unit | — | codex / agy | 採用 | 1 | - -**なぜ**: resolve_participants で母集合の全メンバーを exclude に指定し、参加可能なメンバーが 0 件になる下限境界の振る舞い(空一覧で認証確認が呼ばれ、available が空リスト、excluded が固定順で記録されること)が固定されていない。 - -**手順**: 1. 母集合の全員(例: ['codex', 'agy', 'kiro'])を exclude に指定し、記録用プローブを渡して resolve_participants を呼び出す -2. プローブが空の一覧 [] で 1 回だけ呼ばれることを検証する -3. 戻り値の Participants において available が []、unavailable が {}、excluded が固定順(['codex', 'agy', 'kiro'])で保持されることを検証する - -### R2-003 — `plugins/ndf/scripts/lib/assignment.py#review_assign` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| boundary | unit | — | agy / kiro | 採用 | 1 | - -**なぜ**: review_assign の round_no < 1 の下限境界条件で AssignmentError を送出する振る舞いが scripts/tests 内で固定されていない。同モジュールの impl_assign や review_seats には round_no < 1 の境界テストがあるが、review_assign だけ抜けている。 - -**手順**: 1. test_lib_assignment.py で assignment.review_assign(0, "claude") および assignment.review_assign(-1, "claude") を呼び出す -2. どちらの呼び出しでも assignment.AssignmentError が送出されることを検証する -3. 例外メッセージに「ラウンド番号は 1 以上です」が含まれることを検証する - -### R2-004 — `plugins/ndf/scripts/lib/assignment.py#review_assign` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| branch | unit | — | agy / kiro | 取り消し | 1 | - -**なぜ**: review_assign は適用の役を持たない工程が使う公開入口だが、scripts/tests には直接の固定が無い。in-scope の test_lib_assignment.py は assign / impl_assign / review_seats を固定するだけで、この関数の輪番(母集合3者から dropped=(round_no-1)%3 を外す各分岐)は通っていない。out-of-scope の cross-review テストは _round_reviewers の照合オラクルとして呼ぶだけで、この関数自身の戻り値を固定していない。 - -**手順**: 1. test_lib_assignment.py の assignment フィクスチャで各ホスト(claude, codex, agy, kiro)について review_assign(round_no, host) を round 1..6 で呼び出す -2. 各ホストで返る担当ペアの一覧が 3 ラウンド周期で循環し、現状の決定結果(例: claude は [['agy', 'kiro'], ['codex', 'kiro'], ['codex', 'agy']] が 2 周する)と完全一致することを検証する -3. 返されるレビュー担当が常に 2 者であり、指定したホスト自身を含まないことを併せて検証する - -### R2-005 — `plugins/ndf/scripts/lib/assignment.py#review_assign` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| error | unit | — | codex / agy | 採用 | 1 | - -**なぜ**: review_assign に HOST_RUNTIMES に含まれない無効なホスト名が渡された場合、内部の review_pool から AssignmentError(「ホストになれないランタイムです」)が送出されるエラー経路が固定されていない。 - -**手順**: 1. test_lib_assignment.py で assignment.review_assign(1, "gemini") や assignment.review_assign(1, "unknown") を呼び出す -2. assignment.AssignmentError が送出されることを検証する -3. 例外メッセージに「ホストになれないランタイムです」が含まれることを検証する - -## ラウンド 3(実装 kiro / レビュー codex / agy) - -### R3-001 — `plugins/ndf/skills/cross-review/scripts/measure.py#_matches` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_parameter_list | introduce_parameter_object | major | kiro | 取り消し | 0 | - -**なぜ**: 解決位置の突き合わせ鍵 (pr, round_no, path, line) の 4 引数が _matches・_find_best_match・_add_oracle_match の 3 関数を順に渡り回っている。呼び出し側で順序を取り違えても型で防げず、鍵の項目を増やすたびに 3 関数すべての引数を直すことになる。 - -**手順**: 1. NamedTuple `MatchKey(pr, round_no, path, line)` を定義する -2. _matches の引数を (finding, key: MatchKey) にし、本体を key.* へ書き換える -3. _find_best_match・_add_oracle_match も MatchKey を受け取る形に変え、呼び出し側(_oracle のループ)で MatchKey を 1 度組み立てて渡す -4. test_measure.py の oracle 系テストで退行を確認する - -### R3-002 — `plugins/ndf/scripts/lib/assignment.py#resolve_participants` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | split_into_pipeline | major | codex | 取り消し | 0 | - -**なぜ**: 入力の正規化、名前と集合制約の検証、only 適用、認証 probe、利用可否の集計、require_all 判定、結果生成が直列に並び、検証規則と外部 probe の境界を個別に読みにくい。 - -**手順**: 1. test_lib_participants.py の既存ケースを現状固定として実行する -2. pool/include/exclude の正規化と制約検証を独立した段へ抽出する -3. only を適用して probe 対象を返す段を抽出する -4. probe 結果を available と unavailable へ変換し require_all を判定する段を抽出する -5. resolve_participants は各段の出力を次段へ渡して Participants を返す処理だけにする -6. 対象テストと全体テストで例外文言、順序、probe 呼び出し、戻り値が不変であることを確認する - -### R3-003 — `plugins/ndf/skills/cross-review/scripts/state.py#_init_new_state` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | major | codex | 取り消し | 0 | - -**なぜ**: 200 行の関数内に PR 所有者判定、レビュー指示生成、worktree と既存コメントの準備、担当決定、初期 state 構築、保存と表示が同居し、補助関数もすべてローカル定義のため各段階を単独で検証できない。 - -**手順**: 1. 既存の init 経路テストを現状固定として実行する -2. _resolve_pr_and_ownership と _prepare_review_instructions をモジュールレベルへ抽出する -3. _prepare_worktree_and_comments と _prepare_initial_assignment をモジュールレベルへ抽出する -4. _build_initial_review_state と _finalize_initial_state をモジュールレベルへ抽出し、_init_new_state は各段階を順に呼ぶ構成へ縮める -5. init 関連テストと全体テストで公開入口の出力と副作用が不変であることを確認する - -### R3-004 — `plugins/ndf/skills/cross-review/scripts/measure.py#_state_file_pr` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| duplication | consolidate_duplication | minor | kiro | 採用 | 1 | - -**なぜ**: _state_file_pr と _prs が同じ pr_history 走査(dict 判定→_as_int(entry.get("pr"))→current_pr へのフォールバック)を別々に持つ。_state_file_pr は実質「_prs の先頭」で、片方だけ直すと状態ファイルの鍵の選び方が食い違う。同じ業務ルール(状態ファイルの鍵の決め方)に由来し、必ず一緒に変わる重複である。 - -**手順**: 1. _prs を先に評価し、走査ロジックの唯一の持ち主にする -2. _state_file_pr を `prs = _prs(st); return prs[0] if prs else None` へ置き換える -3. test_measure.py の pr/prs を検査するテスト(test_identity_keys_report_state_file_key_and_all_prs 他)で退行を確認する - -### R3-005 — `plugins/ndf/scripts/lib/refresh.py#fetch` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | minor | kiro | 取り消し | 0 | - -**なぜ**: fetch が opener 呼び出し・期限付き読み取りループ・socket への期限伝播・close の後始末を通しで行う。読み取りループ(deadline 判定・_set_socket_timeout の bounded 蓄積・chunk 蓄積)だけを名前付きの段へ分けると、読み取り部分と取得の骨格を別々に読める。 - -**手順**: 1. 読み取りループを `_read_until_deadline(response, deadline, timeout) -> bytes` として抽出し、bounded 判定と FetchTimeout の送出をその中へ移す -2. fetch は opener 呼び出しと finally の close を残し、本文取得を抽出関数の呼び出しに置き換える -3. test_refresh.py の refresh/fetch 経路のテストで退行を確認する - -## ラウンド 4(実装 claude / レビュー codex / kiro) - -### R4-001 — `plugins/ndf/scripts/lib/metrics.py#_aggregate_reviewer_round` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | major | kiro | 採用 | 1 | - -**なぜ**: 1 つの関数が 2 つの独立した集計を通しで行う。前半は review ごとの指摘件数と解決件数の集計、後半は entry.get('reviewers') を回して判定一致(verdict_pairs / verdict_agreements)を数える二重ループである。指摘の集計と判定一致の集計は変更理由が別で、後半のネストしたループが読む負荷を上げている。 - -**手順**: 1. 後半の others ループ(verdict_pairs / verdict_agreements の加算)を _tally_verdict_agreement(rb, entry, review, name) として抽出する -2. 抽出した関数は entry.get('reviewers') から name 以外を取り出し、_verdict を使って一致数を rb へ加算する -3. _aggregate_reviewer_round のループ本体を、指摘集計+抽出した関数の呼び出しに置き換える -4. metrics.aggregate を通す既存テスト(cross-refactoring 側 test_models_and_metrics.py の resolution_rate / agreement_rate)で退行が無いことを確かめる - -### R4-002 — `plugins/ndf/skills/cross-review/scripts/rotate-pr.sh#execute_light / execute_squash` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| duplication | consolidate_duplication | major | codex | 採用 | 1 | - -**なぜ**: 両モードが旧 PR へのコメント、close、ERR trap の設定、新 PR 作成、trap 解除、URL からの番号抽出、NEW_PR・NEW_PR_URL・NEW_BRANCH の出力を同じ順序で持つ。同じ障害対策のコメントが両方へ反映されており、変更理由も共通している。 - -**手順**: 1. 既存の rotate-pr テストで light と squash の close、作成失敗時の reopen、成功時の出力を固定する -2. モード固有処理から新 PR の head、base、title、body、draft を組み立てる部分だけを残す -3. close から create、trap 管理、番号抽出、結果出力までを共通関数へ抽出する -4. execute_light と execute_squash を共通関数呼び出しへ置き換える -5. 両モードの既存テストを実行してコマンド順と標準出力が不変であることを確認する - -### R4-003 — `plugins/ndf/scripts/lib/worktree-common.sh#wt_extract_write_target` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | split_into_pipeline | major | codex | 採用 | 1 | - -**なぜ**: 書き込み先抽出の入口に、ヒアドキュメント除去、字句化、作業ディレクトリと複合構文の状態追跡、sed・tee・cp・mv・リダイレクトの対象抽出が連続して同居している。多数の局所状態と入れ子の補助関数を一度に追う必要があり、各段を独立して固定できない。 - -**手順**: 1. 対象テスト配下に wt_extract_write_target の公開入出力を通す現状固定テストを追加し、cd、パイプ、部分シェル、case、関数定義、各書き込み形式を固定する -2. 前処理と字句化を、改行区切りの語列を返す段として独立させる -3. 現在地と複合構文の追跡を、語列から走査状態を更新する段へ分ける -4. 書き込み先候補の抽出と相対パス解決を最終段へ分け、入口は各段を順に接続するだけにする -5. 各段の後と最後に現状固定テストおよび全体テストを実行する - -### R4-004 — `plugins/ndf/scripts/lib/run_metrics.py#_by_round_count` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| conditional_chain | extract_method | minor | kiro | 採用 | 1 | - -**なぜ**: バケット鍵の決定が入れ子の三項式 key = "3 以上" if count >= 3 else str(count) if count in (1, 2) else None に埋まっている。ラウンド数から表示区分を導く判断がループ本体の 1 行に押し込まれ、境界(1 / 2 / 3 以上 / 対象外)が読み取りづらい。 - -**手順**: 1. count から区分文字列(または None)を返す _round_count_bucket(count) を抽出する -2. 分岐を if count >= 3 / elif count in (1, 2) / else None として平坦に書く -3. _by_round_count のループ本体で key = _round_count_bucket(count) を呼ぶ形へ置き換える -4. test_run_metrics.py::test_aggregate_by_round_count(1 / 2 / 3 以上 の 3 行)で退行が無いことを確かめる - -### R4-005 — `plugins/ndf/scripts/lib/run_metrics.py#_select` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| conditional_chain | extract_method | minor | kiro | 採用 | 1 | - -**なぜ**: 行ごとの絞り込みが 5 本の連続した if ... continue と、until 判定に埋め込まれた入れ子の三項(started >= until if until_exclusive else started > until)で構成される。時刻の下限・上限・repo・kind・version という別々の観点が 1 つのループ本体に同居し、until_exclusive の分岐が特に読みづらい。 - -**手順**: 1. 時刻の下限・上限の判定を _within_time_bound(started, since, until, until_exclusive) として抽出し、入れ子の三項をその中に閉じ込める -2. _select は since/until を計算した後、_within_time_bound と残りの属性一致(repo / kind / version)で 1 行を通すか決める -3. 属性一致も見通しが悪ければ _matches_filters(row, args) へまとめる -4. test_run_metrics.py::test_aggregate_filters(since / until / repo / kind / version の 5 例)で退行が無いことを確かめる - -## ラウンド 5(実装 codex / レビュー agy / kiro) - -### R5-001 — `plugins/ndf/scripts/lib/auth.py#_probe_all` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| one_by_one_iteration | replace_with_bulk_operation | major | codex / agy | 取り消し | 0 | - -**なぜ**: AUTH_PROBES に定義された各 CLI(最大4者)の認証確認コマンド(タイムアウト各120秒)を for ループ内で直列に実行しており、参加者数に比例して全体の待ち時間が累積する。入力順と出力順を維持したまま有界な並行実行(ThreadPoolExecutor 等)へ置き換えることで待ち時間を短縮できる。 - -**手順**: 1. test_auth_probe.py で複数 CLI の確認順序・出力順序・戻り値構造を検証する既存テストを確認する -2. _probe_all 内で AUTH_PROBES に存在する対象を抽出し、有界な並行ワーカー(concurrent.futures 等)で並行実行する -3. 各ランタイムの結果を入力順に results へ格納し、info 出力も入力順に発出する -4. pytest plugins/ndf/scripts/tests/test_auth_probe.py および全体テストで互換性と表示順を検証する - -### R5-002 — `plugins/ndf/skills/cross-review/scripts/state.py#_apply_resume_args_block` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | major | codex / agy | 取り消し | 0 | - -**なぜ**: 1 つの関数内で、引数の正規化、--only none による特殊な状態解除と履歴追記、一般フィールドの反映、参加者再構築用の引数名前空間生成、_resolve_reviewers による再解決、成功時の状態・履歴更新という複数の段階が連続して書かれており、状態更新の原子性と各段階の責務が混在している。 - -**手順**: 1. test_state_resume_args.py で --only none、通常引数反映、参加者再構築失敗時の原子性(ロールバック/非更新)がテストされていることを確認する -2. --only none の状態解除と履歴追記を補助関数へ抽出する -3. 既存の参加者情報と再開引数をマージして再解決用 Namespace を組み立てる処理を補助関数へ抽出する -4. 参加者の解決成功後に状態と resume_changes を更新する処理を補助関数へ抽出する -5. _apply_resume_args_block を各ステップの明瞭なオーケストレーションに再構成し、対象テストと全体テストを実行する - -### R5-003 — `plugins/ndf/skills/cross-review/scripts/rotate-pr.sh#execute_squash` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | major | codex / agy | 取り消し | 0 | - -**なぜ**: 86行の関数内で、prepare.json や gh pr view からの PR メタ情報(base, title)解決、detached HEAD からの head ブランチ復元、タイトル末尾の (rotated) 接尾辞の正規化ループ、squash コミットの作成と push、新 PR 本文の組み立て、rotate_close_and_create 呼び出しが密結合しており、情報解決と Git/GitHub 副作用の分離が不明瞭になっている。 - -**手順**: 1. test_rotate_pr_queue.py などの現状固定テストで squash モードの振る舞い(接尾辞正規化、ブランチ名解決など)を確認する -2. PR メタ情報(base / title)のフォールバック取得処理を補助関数へ抽出する -3. ブランチ復元(git branch --show-current / prepare.json / gh pr view)と (rotated) 接尾辞の正規化処理を補助関数へ抽出する -4. squash コミット作成とリモート push の Git 操作を補助関数へ抽出する -5. execute_squash を各抽出関数のパイプライン呼び出しに整理し、テストを実行する - -## ラウンド 6(実装 agy / レビュー codex / kiro) - -### R6-001 — `plugins/ndf/scripts/lib/transcript_agents.py#_aggregate_token_metrics` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | major | codex | 取り消し | 1 | - -**なぜ**: 1回の走査でトークンの固定費・最大値、応答IDの重複排除、モデル別件数を集め、その後に派生値と代表モデルまで確定している。異なる集計規則が同じ局所状態へ混在し、各規則を単独で追いにくい。 - -**手順**: 1. 合成でない assistant 行を選ぶ処理を名前付きの反復単位へ抽出する -2. トークン指標の更新を _update_token_metrics として抽出する -3. 応答IDとモデル件数の更新を _collect_response_model として抽出する -4. 呼び出し側は集計結果から responses・work・modelを従来どおり確定し、既存フィクスチャの契約値で退行確認する - -### R6-002 — `plugins/ndf/scripts/lib/metrics.py#format_report` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_method | extract_method | major | codex | 取り消し | 1 | - -**なぜ**: 実装担当表の行生成、レビュー担当表の行生成、計測不能・指定値代用の注記、比較上の注意の4段階を1関数が通しで組み立てており、表の列変更と注記構成の変更が同じ関数へ集中している。 - -**手順**: 1. 実装担当の行生成を _format_impl_rows として抽出する -2. レビュー担当の行生成を _format_reviewer_rows として抽出する -3. 計測注記の追加を _append_measurement_notes として抽出する -4. format_report は各段を順に呼び、既存の文字列出力が一致することを既存テストで確認する - -### R6-003 — `plugins/ndf/skills/cross-review/tests/conftest.py#_no_github_state` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| mock_targets_implementation_detail | fix_dependency_direction | major | codex | 取り消し | 1 | - -**なぜ**: autouse fixture が state.py の非公開関数 _fetch_check_runs と _fetch_pr_metadata を名前で直接差し替えるため、GitHub取得処理の抽出や改名だけで広範なテストが壊れる。実際の外部境界は _gh_rest と subprocess.run なのに、その内側の実装手順を全テストへ固定している。 - -**手順**: 1. state.py が使うGitHub取得境界を明示した依存としてまとめる -2. cmd系の入口からその境界を注入できる最小の既定値を置く -3. _no_github_state は非公開取得関数ではなく境界の偽実装を注入する -4. 実取得の契約テストは既存の fake gh と _gh_rest 差し替えを維持し、全テストで外部通信が発生しないことを確認する - -### R6-004 — `plugins/ndf/scripts/lib/metrics.py#_append_model_measurement_warnings` - -| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット | -| --- | --- | --- | --- | --- | ---: | -| long_parameter_list | introduce_parameter_object | minor | kiro | 取り消し | 1 | - -**なぜ**: 引数が 7 個。うち unmeasured / assumed は出力の蓄積先、round_no / runtime / requested / observed / role_label は 1 ラウンド 1 担当の計測文脈で、常に組で渡り回る。2 つの呼び出し側(_aggregate(impl)と _aggregate_round_reviewers)で同じ 5 値をその順で並べており、順序を取り違えると requested と observed が入れ替わっても型が同じ str のため気付けない。 - -**手順**: 1. runtime / requested / observed / role_label(と round_no)をまとめる NamedTuple もしくは dataclass(例 MeasurementContext)を metrics.py に定義する -2. _append_model_measurement_warnings の署名を (unmeasured, assumed, ctx) へ変更し、本体の runtime 等の参照を ctx.runtime 等へ置き換える -3. aggregate 内の impl 経路(round_no・impl_runtime・requested・observed・"実装担当")で ctx を組み立てて渡す -4. _aggregate_round_reviewers 内のレビュー担当経路(round_no・name・requested・observed・"レビュー担当")でも ctx を組み立てて渡す -5. cross-refactoring/tests/test_models_and_metrics.py(既存)で aggregate の出力(unmeasured / assumed の文言)が不変であることを確認する - -## 見送った項目 - -| ラウンド | 対象 | 兆候・経路 | 理由 | -| --- | --- | --- | --- | -| 1 | `plugins/ndf/scripts/lib/models.py#mismatch_warning` | branch | 1 ラウンドの採用上限 5 件を超えた | -| 1 | `plugins/ndf/scripts/lib/models.py#observed_model` | branch | 1 ラウンドの採用上限 5 件を超えた | -| 1 | `plugins/ndf/scripts/lib/models.py#separation_reason` | branch | 1 ラウンドの採用上限 5 件を超えた | -| 1 | `plugins/ndf/scripts/lib/monitor.py#monitor_agent` | branch | 1 ラウンドの採用上限 5 件を超えた | -| 1 | `plugins/ndf/scripts/lib/statefile.py#save` | error | 1 ラウンドの採用上限 5 件を超えた | -| 1 | `plugins/ndf/skills/cross-review/scripts/critique.sh#select_targets` | branch | 1 ラウンドの採用上限 5 件を超えた | -| 1 | `plugins/ndf/scripts/lib/assignment.py#seat_runtime` | boundary | コミット 36dd097d4dff427b0de545bcd0cdc0de0e7b74fb にトレーラーが欠けています: Item-Id, Round, Impl-Runtime, Impl-Model | -| 2 | `plugins/ndf/scripts/lib/assignment.py#review_seats` | boundary | 1 ラウンドの採用上限 5 件を超えた | -| 2 | `plugins/ndf/skills/cross-review/scripts/launch-reviewer.sh#main` | normal | 1 ラウンドの採用上限 5 件を超えた | -| 2 | `plugins/ndf/scripts/lib/assignment.py#review_assign` | branch | コミット 0c3b60c7101ac9b439e0b13b677f8061b81eb851 にトレーラーが欠けています: Item-Id, Round, Impl-Runtime, Impl-Model | -| 3 | `plugins/ndf/scripts/lib/post_queue.py#Queue.flush` | long_method | 1 ラウンドの採用上限 5 件を超えた | -| 3 | `plugins/ndf/skills/cross-review/scripts/measure.py#_matches` | long_parameter_list | どの改善項目にも割り当てられていないコミットが 1 件(1a81a1a)。検証を回避した変更や、状態と実差分の食い違いを Pull Request に残さないため、この適用ラウンドを取り消します | -| 3 | `plugins/ndf/scripts/lib/assignment.py#resolve_participants` | long_method | どの改善項目にも割り当てられていないコミットが 1 件(1a81a1a)。検証を回避した変更や、状態と実差分の食い違いを Pull Request に残さないため、この適用ラウンドを取り消します | -| 3 | `plugins/ndf/skills/cross-review/scripts/state.py#_init_new_state` | long_method | どの改善項目にも割り当てられていないコミットが 1 件(1a81a1a)。検証を回避した変更や、状態と実差分の食い違いを Pull Request に残さないため、この適用ラウンドを取り消します | -| 3 | `plugins/ndf/scripts/lib/refresh.py#fetch` | long_method | どの改善項目にも割り当てられていないコミットが 1 件(1a81a1a)。検証を回避した変更や、状態と実差分の食い違いを Pull Request に残さないため、この適用ラウンドを取り消します | -| 4 | `plugins/ndf/skills/cross-review/scripts/state.py#_sync_worktree` | long_method | 1 ラウンドの採用上限 5 件を超えた | -| 5 | `plugins/ndf/scripts/lib/auth.py#_probe_all` | one_by_one_iteration | どの改善項目にも割り当てられていないコミットが 1 件(89bb86f)。検証を回避した変更や、状態と実差分の食い違いを Pull Request に残さないため、この適用ラウンドを取り消します | -| 5 | `plugins/ndf/skills/cross-review/scripts/state.py#_apply_resume_args_block` | long_method | どの改善項目にも割り当てられていないコミットが 1 件(89bb86f)。検証を回避した変更や、状態と実差分の食い違いを Pull Request に残さないため、この適用ラウンドを取り消します | -| 5 | `plugins/ndf/skills/cross-review/scripts/rotate-pr.sh#execute_squash` | long_method | どの改善項目にも割り当てられていないコミットが 1 件(89bb86f)。検証を回避した変更や、状態と実差分の食い違いを Pull Request に残さないため、この適用ラウンドを取り消します | -| 6 | `plugins/ndf/scripts/lib/transcript_agents.py#_aggregate_token_metrics` | long_method | 適用結果に項目がありません: R6-003(群の全項目を 1 つのコミットへまとめ、各項目へ同じ SHA を申告します) | -| 6 | `plugins/ndf/scripts/lib/metrics.py#format_report` | long_method | 適用結果に項目がありません: R6-003(群の全項目を 1 つのコミットへまとめ、各項目へ同じ SHA を申告します) | -| 6 | `plugins/ndf/skills/cross-review/tests/conftest.py#_no_github_state` | mock_targets_implementation_detail | 適用結果に項目がありません: R6-003(群の全項目を 1 つのコミットへまとめ、各項目へ同じ SHA を申告します) | -| 6 | `plugins/ndf/scripts/lib/metrics.py#_append_model_measurement_warnings` | long_parameter_list | コミット 5f63640dff6dd7739738a8794edc5db43792f342 にトレーラーが欠けています: Item-Id, Round, Impl-Runtime, Impl-Model |