何を見つけたか
/ndf:cross-review を中断・再開すると、state.py init に渡した --only /
--max-rounds / --rotate-after / --verify-command / --verify-exit-code /
--host が黙って無視される。状態ファイルが残っている限り再開経路へ入り、
これらの値は最初に init したときのものが使われ続ける。
実測(PR #646 の実装レビューで踏んだ):
state.py init 646 --host claude で開始(only は null)
- 特定のランタイムだけでレビューを回したくなり、
--only codex を付けて
init をやり直した
- 状態ファイルが残っているため再開経路へ入り、
only は null のまま。
start-round はこれまでどおり輪番の 2 者を返した
出力にも「指定した値を採らなかった」旨は出ない。↻ 追加レビュー観点を state に 反映して再開 は追加レビュー観点だけを指しており、他の引数を無視したことは
どこにも現れない。
どこで見つけたか
plugins/ndf/skills/cross-review/scripts/state.py の _resume_from_state()(:1540。呼び出しは :1644 の 1 か所)。
_resume_from_state(pr, repo, worktree, manual_extra_review) は引数の名前空間(args)を受け取らないため、
init に渡した引数を反映する手段が無い。再開経路が状態ファイルへ書き戻すのは次の項目だけである。
changed_files / auto_review_categories / auto_review_instructions
(古い状態ファイルのとき)
manual_extra_review_instructions / extra_review_instructions
(--focus / --extra-instructions-file を渡したとき)
review_instructions
carried_over
only / max_rounds / rotate_after / host / verify_commands /
verify_exit_codes はいずれも書き戻されない。_init_new_state() が状態を
組み立てるときにしか読まれない。build_parser の既定値は --max-rounds 12 /
--rotate-after 8(state.py:4352-4353)である。
cross-refactoring にも同じ形がある。 plugins/ndf/skills/cross-refactoring/scripts/refactor_lib/commands/setup.py:305-312
は、状態ファイルがあり final が決まっていなければ、投稿の扱い(_apply_post_event)だけを
反映して return する。refactor.py の init が受ける --max-outer-rounds(:108、既定 3)/
--max-test-rounds(:112)/ --max-fix-rounds(:117)/ --max-items-per-round(:119)/
--model(:127)は再開時に反映されない。無視したことは出力に出ない(出るのは
↻ 前回中断した状態から再開します だけ)。
共通層 plugins/ndf/scripts/lib/statefile.py は load / save / emit / die / info と保存の後の
処理の登録を持ち、cross-refactoring が状態ファイルの読み書きに使う(cross-review の state.py は
使っていない。grep -c 'statefile\.' state.py は 0)。再開時に引数を状態へ重ねる手順は、どちらの
Skill にも共通層にも無い。
なぜこの変更の範囲外なのか
PR #646(issue #495)の受け入れ条件は AC11〜AC31 で、対象は
.ndf/worktree.local.json を共有の宣言へ重ねる仕組みである。cross-review の
Skill そのものの引数の扱いは、受け入れ条件にも計画の「やらないこと」にも
含まれない。
直さないと何が起きるか
- 引数で進め方を変えられない。 長丁場の収束ループで担当を絞る、上限を
伸ばす、実行検証を後から足す、といった調整が効かない。効かなかったことも
出力に出ないため、進行側は指定が通ったものとして読む
- 回避するには状態ファイルを手で書き換えるか、退避して作り直すしかない。
作り直すとラウンド数・引き継いだ指摘・待ち行列・却下の記録が失われる
--only の指定が通っていないことに気づかないまま進むと、外したはずの
ランタイムが起動する。打ち切りが分かっているランタイムを避けたい場面
(agy が 4 回連続 STALLED になった rf646 の実績)で、避けたつもりで
避けられない
- シェル側の
$ONLY と状態ファイルの only がずれ、起動し直しで外したつもりのランタイムが起動する。
SKILL.md の骨組みは起動・監視・取り込みをシェル変数の $ONLY で絞る(SKILL.md:229(起動)、
:236(監視)、:239(取り込み)、:249(根拠の検証))。
一方、判定は状態ファイルの only で担当を決める(_round_reviewers、state.py:1893-1916)。
再開時に ONLY=codex を渡し、状態ファイルの only が null のままだと次のように進む。
- 輪番の担当が agy + kiro のラウンドでは、
$ONLY の絞り込みで誰も起動しない
judge は agy と kiro を結果なしとして扱い、RELAUNCH_AGENTS='agy kiro'(終了コード 7)を返す
- 起動し直しの分岐(
SKILL.md:255-261)は $ONLY で絞らないため、避けたかった agy を起動する。
この分岐は launch-reviewer.sh → bg-wait.sh run(monitor.py --phase review --agents "$RELAUNCH_AGENTS_CSV")→ bg-wait.sh wait → read-result の順で回り、どの段でも $ONLY を
見ない(監視へ渡す担当は judge が返した RELAUNCH_AGENTS_CSV である)
修正レイヤー
plugins/ndf/scripts/lib/statefile.py に置く、再開時の引数の反映の契約。「明示的に渡された引数
だけを状態へ重ね、反映しない引数は渡されたら知らせる」を 1 か所で持つ。現れている cross-review の
_resume_from_state だけを直すと、同じ形を持つ cross-refactoring の再開経路(setup.py:305-312)が
残り、cross-review の --only と cross-refactoring の上限・モデルの指定で反映の規則が食い違う。
採る手
- 新設: 渡されたかどうかの区別と、反映しない引数の知らせ方を持つ再開時の反映を
statefile.py に作る
- 統合(
consolidate_duplication): cross-review の _resume_from_state と cross-refactoring の init の再開経路が、その反映を使う
回避策
状態ファイルの only を書き換える。 _round_reviewers は only を最初に見るため、後のラウンドにも効く。
rounds[-1].reviewers の書き換えは開いているラウンドにしか効かない。次の start-round で輪番へ戻る。
提案
両 Skill の再開経路で、明示的に渡された引数だけを状態ファイルへ反映する(上の「修正レイヤー」の契約)。
既定値のまま渡された引数で上書きすると、再開のたびに --max-rounds が
12 へ戻る。argparse の既定を None にして「渡されたかどうか」を
区別できる形にするのが最小の変更である。
反映できないと決めた引数(例えば --host。母集合が変わると輪番の再現性が
崩れる)は、渡されたら 1 行知らせる。黙って捨てない。
由来
PR #646
関連
進行
モード: standard / 作業ツリー: .worktrees/feat/issue-727-participants / 計画: issues/issue-727-p6-participants-plan.md
閉じた理由
PR #793 #800 で直り、ndf 10.16.0(2026-09-22、main / タグ ndf--v10.16.0、PR #810)で配布した。リリース後テスト(#810 (comment) )でこの課題の受け入れ条件はすべて合格した。
振り返り: #810 (comment)
何を見つけたか
/ndf:cross-reviewを中断・再開すると、state.py initに渡した--only/--max-rounds/--rotate-after/--verify-command/--verify-exit-code/--hostが黙って無視される。状態ファイルが残っている限り再開経路へ入り、これらの値は最初に
initしたときのものが使われ続ける。実測(PR #646 の実装レビューで踏んだ):
state.py init 646 --host claudeで開始(onlyはnull)--only codexを付けてinitをやり直したonlyはnullのまま。start-roundはこれまでどおり輪番の 2 者を返した出力にも「指定した値を採らなかった」旨は出ない。
↻ 追加レビュー観点を state に 反映して再開は追加レビュー観点だけを指しており、他の引数を無視したことはどこにも現れない。
どこで見つけたか
plugins/ndf/skills/cross-review/scripts/state.pyの_resume_from_state()(:1540。呼び出しは:1644の 1 か所)。_resume_from_state(pr, repo, worktree, manual_extra_review)は引数の名前空間(args)を受け取らないため、initに渡した引数を反映する手段が無い。再開経路が状態ファイルへ書き戻すのは次の項目だけである。changed_files/auto_review_categories/auto_review_instructions(古い状態ファイルのとき)
manual_extra_review_instructions/extra_review_instructions(
--focus/--extra-instructions-fileを渡したとき)review_instructionscarried_overonly/max_rounds/rotate_after/host/verify_commands/verify_exit_codesはいずれも書き戻されない。_init_new_state()が状態を組み立てるときにしか読まれない。
build_parserの既定値は--max-rounds 12/--rotate-after 8(state.py:4352-4353)である。cross-refactoringにも同じ形がある。plugins/ndf/skills/cross-refactoring/scripts/refactor_lib/commands/setup.py:305-312は、状態ファイルがあり
finalが決まっていなければ、投稿の扱い(_apply_post_event)だけを反映して
returnする。refactor.pyのinitが受ける--max-outer-rounds(:108、既定 3)/--max-test-rounds(:112)/--max-fix-rounds(:117)/--max-items-per-round(:119)/--model(:127)は再開時に反映されない。無視したことは出力に出ない(出るのは↻ 前回中断した状態から再開しますだけ)。共通層
plugins/ndf/scripts/lib/statefile.pyはload/save/emit/die/infoと保存の後の処理の登録を持ち、
cross-refactoringが状態ファイルの読み書きに使う(cross-reviewのstate.pyは使っていない。
grep -c 'statefile\.' state.pyは 0)。再開時に引数を状態へ重ねる手順は、どちらのSkill にも共通層にも無い。
なぜこの変更の範囲外なのか
PR #646(issue #495)の受け入れ条件は AC11〜AC31 で、対象は
.ndf/worktree.local.jsonを共有の宣言へ重ねる仕組みである。cross-review のSkill そのものの引数の扱いは、受け入れ条件にも計画の「やらないこと」にも
含まれない。
直さないと何が起きるか
伸ばす、実行検証を後から足す、といった調整が効かない。効かなかったことも
出力に出ないため、進行側は指定が通ったものとして読む
作り直すとラウンド数・引き継いだ指摘・待ち行列・却下の記録が失われる
--onlyの指定が通っていないことに気づかないまま進むと、外したはずのランタイムが起動する。打ち切りが分かっているランタイムを避けたい場面
(agy が 4 回連続 STALLED になった rf646 の実績)で、避けたつもりで
避けられない
$ONLYと状態ファイルのonlyがずれ、起動し直しで外したつもりのランタイムが起動する。SKILL.mdの骨組みは起動・監視・取り込みをシェル変数の$ONLYで絞る(SKILL.md:229(起動)、:236(監視)、:239(取り込み)、:249(根拠の検証))。一方、判定は状態ファイルの
onlyで担当を決める(_round_reviewers、state.py:1893-1916)。再開時に
ONLY=codexを渡し、状態ファイルのonlyがnullのままだと次のように進む。$ONLYの絞り込みで誰も起動しないjudgeは agy と kiro を結果なしとして扱い、RELAUNCH_AGENTS='agy kiro'(終了コード 7)を返すSKILL.md:255-261)は$ONLYで絞らないため、避けたかった agy を起動する。この分岐は
launch-reviewer.sh→bg-wait.sh run(monitor.py --phase review --agents "$RELAUNCH_AGENTS_CSV")→bg-wait.sh wait→read-resultの順で回り、どの段でも$ONLYを見ない(監視へ渡す担当は
judgeが返したRELAUNCH_AGENTS_CSVである)修正レイヤー
plugins/ndf/scripts/lib/statefile.pyに置く、再開時の引数の反映の契約。「明示的に渡された引数だけを状態へ重ね、反映しない引数は渡されたら知らせる」を 1 か所で持つ。現れている
cross-reviewの_resume_from_stateだけを直すと、同じ形を持つcross-refactoringの再開経路(setup.py:305-312)が残り、
cross-reviewの--onlyとcross-refactoringの上限・モデルの指定で反映の規則が食い違う。採る手
statefile.pyに作るconsolidate_duplication):cross-reviewの_resume_from_stateとcross-refactoringのinitの再開経路が、その反映を使う回避策
状態ファイルの
onlyを書き換える。_round_reviewersはonlyを最初に見るため、後のラウンドにも効く。rounds[-1].reviewersの書き換えは開いているラウンドにしか効かない。次のstart-roundで輪番へ戻る。提案
両 Skill の再開経路で、明示的に渡された引数だけを状態ファイルへ反映する(上の「修正レイヤー」の契約)。
既定値のまま渡された引数で上書きすると、再開のたびに
--max-roundsが12 へ戻る。
argparseの既定をNoneにして「渡されたかどうか」を区別できる形にするのが最小の変更である。
反映できないと決めた引数(例えば
--host。母集合が変わると輪番の再現性が崩れる)は、渡されたら 1 行知らせる。黙って捨てない。
由来
PR #646
関連
setup.py:305-312)。除外の引数を足すと、その引数も再開時に黙って無視されるCLAUDE.mdの--max-outer-roundsの既定の記述)進行
モード: standard / 作業ツリー:
.worktrees/feat/issue-727-participants/ 計画:issues/issue-727-p6-participants-plan.md閉じた理由
PR #793 #800 で直り、ndf 10.16.0(2026-09-22、
main/ タグndf--v10.16.0、PR #810)で配布した。リリース後テスト(#810 (comment) )でこの課題の受け入れ条件はすべて合格した。振り返り: #810 (comment)