事象
/ndf:cross-refactoring で、同じ適用ラウンド(書き換えるファイルが重ならない改善項目の群)の適用が上限なしに再試行される 。後ろの群へ順番が回らず、収束の判定と最終ゲートへ届かない。
実測は 4 回ある。
rf646(PR #646 ):
実装担当の停止 : 構造改善ラウンド 2 の項目 R4-004 で agy が 4 回続けて STALLED(480 秒無進捗で打ち切り)になり、--- 適用ラウンド 4 / 5 (実装 agy / 項目 R4-004)--- が繰り返された。進行側は停止したまま先へ進まない
週次上限の 429 : 同じ日の早い時点では、claude が api_error_status: 429 で 15 秒で落ちる状態が続き、同じ項目(R3-001)の再試行が 3729 回 記録された(grep -c 'api_error_status.:429')
PR #535 (提案ラウンド 3): 適用ラウンド 1 / 4(実装 agy / 項目 R5-001) が 4 回続けて返り、後ろの群にあった R5-002 / R5-003 / R5-004 の 3 件は順番が来ないまま実行が終わった。状態ファイルの群は status: pending のままだった。
--- 適用ラウンド 1 / 4 (実装 agy / 項目 R5-001)---
--- 適用ラウンド 1 / 4 (実装 agy / 項目 R5-001)---
(以下 2 回同じ)
{"apply_round" : 1 , "impl" : " agy" , "items" : [" R5-001" ], "status" : " pending" ,
"base_sha" : " 08f7ba0c..." , "head_sha" : null , "fix_rounds" : 0 }
devbasex/devbase PR #191 (issue #189 ): 範囲外の項目(R2-001)の適用を止めるため実装担当(kiro)のプロセスを停止したところ、monitor は NO_RESULT を返したが merge-apply の終了コードが 2 ではなく、SKILL.md の rf merge-apply ... || continue により next-apply-round が同じ群を再び返して kiro が再起動された。進行ごと停止して回避した。
PR #757 (#657 の中断と再開、v10.15.0): kiro が適用フェーズで 15 秒で終わり kiro-apply-r1-result.json を残さない → merge-apply が 2 を返し、駆動側の continue が next-apply-round へ戻る → 同じ群(適用ラウンド 3 / 3、項目 R1-004)を再び開く。29 回繰り返した時点で手で止めた。 繰り返しの回数に上限が無いため、止めるまで CLI の起動が続く。この回は残る担当(codex 1 者)で回し直した。
原因
群の状態を進めずに抜ける経路が 3 か所ある
merge-apply(scripts/refactor_lib/commands/apply.py の cmd_merge_apply)が取り込みの前に 終了コード 2 で抜けると、群の status は pending のまま残る。next-apply-round(同 cmd_next_apply_round、apply.py:296-298)は pending と applied の群を無条件に返すため、次の繰り返しで同じ群が開き直される。
経路
場所
起きる場面
結果ファイルが無い・JSON として読めない・JSON オブジェクトでない
scripts/refactor_lib/gitfacts.py の read_result(:1088-1100)
実装担当が STALLED / TIMEOUT / 429 などで結果を残さずに終わった
着手前のテストが成功と確認できていない
apply.py の _load_apply_context(:460-466)
着手前テストの状態が green でない
適用の範囲が確定しない
apply.py の _load_apply_context(:475-483)
起点から HEAD までのコミットを列挙できない
後の 2 つは項目を blocked にするが、群の status は書き換えない。
向きの問題として、下位の読み取り関数が進行の終了コードを決めている。 gitfacts.read_result(:1082)は結果ファイルが無い・読めないときに die(..., code=2)(:1089 / :1093 / :1096)でプロセスを終わらせるため、呼び出し元の cmd_merge_apply が群の状態を書く機会が無い。
取り込んだ後に取り消す経路(例: 必須トレーラーの欠落)は群を status: dropped にし、次の群へ進む。止まるのは取り込みの前に抜ける 3 か所だけ である。PR #535 では、同じ実行の別のラウンド(取り消しの理由は「申告したコミットが実体として無い」「トレーラーの欠落」)は取り消しの後に次の群へ進んでおり、この違いと合う。止まった回の担当は agy で、結果ファイルが無い経路と読める。
再現(本物の tests/conftest.py と test_merge_apply.py の補助を使い、cmd_next_apply_round → cmd_merge_apply を直接呼んだ。群は 2 つ、1 つ目の担当は agy、結果ファイルは置かない):
❌ agy の結果ファイルがありません: …/.cross_refactoring/agy-apply-r1-result.json
(4 回同じ。終了コードは毎回 2)
opened: [1, 1, 1, 1] statuses: ['pending', 'pending']
着手前テストが red の場合も code 2 status pending になった。トレーラー欠落の場合は status after trailer drop: dropped だった。
輪番が進まない
群の担当(impl)は群を割り当てた時点で決まり、開き直しても変わらない。上の再現でも 2 つ目の群(担当 codex)は一度も開かれず、結果を残せない同じ担当が起動し続ける 。
手順書の骨組みが終了コードを捨て、上限も置いていない
SKILL.md の「実行」の骨組み(:315-319)は次の形である。
" $SCRIPTS /launch-cli.sh" " $IMPL " apply " $ID " " $ROUND "
" $LIB /monitor.py" " $ID " --agents " $IMPL " --tmp-dir " $TMP_DIR " \
--stem-template " {agent}-apply-r$ROUND " --phase apply
# 終了コード 2 = 適用が通らずこの群を取り消した。修正ラウンドは回さない
rf merge-apply " $ID " " $ROUND " || continue
上限の値は骨組みが持たない。工程名(--phase apply)から上限の表を引く。 表は
plugins/ndf/scripts/lib/limits.py だけが持ち、PHASE_TIMEOUT['apply'] は 3600 秒である
(limits.py:32-40)。
monitor.py(plugins/ndf/scripts/lib/monitor.py)の終了コード(2 = TIMEOUT / 3 = NO_RESULT / 4 = EARLY_ERROR / 5 = STALLED)を見ていない。どの失敗も merge-apply に「結果ファイルなし」として届く
merge-apply の終了コード 2 を「群を取り消した」とみなして continue するが、取り込みの前に抜けた場合は群が残っているため、同じ群へ戻る
SKILL.md:49(ラウンドの表の「適用ラウンド」の行)と SKILL.md:56 は「適用ラウンドに別の上限を置かない 」と定めている。採用件数の上限が群の数を切るという理由だが、同じ群を開き直す場合には効かない。直すときはこの 2 か所を改める必要がある
agy が STALLED になる背景(推定)
無進捗の許容は上限の表が持つ。limits.AGENT_STALL(limits.py:43-48)で agy は 480 秒である
(monitor.py:101 の DEFAULT_STALL_AGENT_BUILTIN はこの表への別名)
cross-refactoring の骨組みは --stall-timeout を渡していない。 limits.py:15 は
「cross-refactoring の apply / fix / final-fix には、骨組みが --stall-timeout を引数で
渡す(テスト 1 回を含む無出力の最長が工程で決まるため)。その許容はこの表に持たない」と書くが、
SKILL.md:315-319 は --phase apply だけを渡す。文書とコードが食い違っている
無進捗の判定は err.log / stdout.log / progress.log の合計サイズで行う(monitor.py:792-796)。cross-review は scripts/launch-reviewer.sh:137-142 で agy に $STEM-progress.log へ作業段階を追記させている。cross-refactoring の prompts/*.md には進捗マーカーの指示が無い(grep -n progress prompts/*.md の出力なし、終了コード 1)
適用やテストの実行は出力の無い時間が長いため、agy が動いていても 480 秒で打ち切られている可能性がある 。agy を実行して確かめてはいないため、rf646 の agy のログで確認が要る
429 を早期の致命として検知できていない
monitor.py の EARLY_ERROR_FATAL(:114-126)は ^HTTP/… 429 と rate limit exceeded だけを見る。claude が出す "api_error_status":429 を含む行を _scan_early_fatal に渡すと None が返る(一致しない)。そのため 429 は 15 秒で落ちても NO_RESULT として扱われ、上の開き直しに入る。
影響
進行が止まっていることに気づけない(ハートビートは出続ける)
上限のある環境では、再試行がそのまま利用量を消費する(rf646 で 3729 回)
後ろの群が 1 つも適用されない(PR Add: 却下した指摘を per-item で残す(#156 の 1 本目) #535 で 3 件)
収束の判定(新しい提案が出なくなる)と最終ゲートへ到達できず、人が止めるしかない。打ち切ると最終ゲートが未実施で残る
担当の CLI が結果を残さなければ毎回起きる(agy の STALLED・利用上限・認証切れ・容量不足)。既定の参加者に agy が含まれる
回避は手で止め、状態ファイルの群を dropped へ書き換えることだが、手順書には書かれていない。agy の STALLED は、MONITOR_STALL_AGY を大きくすると打ち切りまでの時間を延ばせる。
所要時間への影響(2026-09-01〜09-15 の実測)
集計の方法と全体の値は #662 にある。
提案
関連
出どころ
rf646 の進行ログ(STALLED 4 回 / 429 の 3729 行)。#495 の担当(PR #646 )の実施中に発生。PR #535 の構造改善の実測。devbasex/devbase#191 (2026-09-17)と PR #757 (2026-09-18、v10.15.0 の振り返り issue #550 )で同じ事象を確認した。
進行
モード: standard / 作業ツリー: .worktrees/feat/issue-728-apply-intake / 計画: issues/issue-728-647-592-553-plan.md
閉じた理由
PR #796 で直り、ndf 10.16.0(2026-09-22、main / タグ ndf--v10.16.0、PR #810 )で配布した。リリース後テスト(#810 (comment) )でこの課題の受け入れ条件はすべて合格した。
振り返り: #810 (comment)
事象
/ndf:cross-refactoringで、同じ適用ラウンド(書き換えるファイルが重ならない改善項目の群)の適用が上限なしに再試行される。後ろの群へ順番が回らず、収束の判定と最終ゲートへ届かない。実測は 4 回ある。
rf646(PR #646):
R4-004で agy が 4 回続けてSTALLED(480 秒無進捗で打ち切り)になり、--- 適用ラウンド 4 / 5 (実装 agy / 項目 R4-004)---が繰り返された。進行側は停止したまま先へ進まないapi_error_status: 429で 15 秒で落ちる状態が続き、同じ項目(R3-001)の再試行が 3729 回記録された(grep -c 'api_error_status.:429')PR #535(提案ラウンド 3):
適用ラウンド 1 / 4(実装 agy / 項目 R5-001)が 4 回続けて返り、後ろの群にあった R5-002 / R5-003 / R5-004 の 3 件は順番が来ないまま実行が終わった。状態ファイルの群はstatus: pendingのままだった。{"apply_round": 1, "impl": "agy", "items": ["R5-001"], "status": "pending", "base_sha": "08f7ba0c...", "head_sha": null, "fix_rounds": 0}devbasex/devbase PR #191(issue #189): 範囲外の項目(R2-001)の適用を止めるため実装担当(kiro)のプロセスを停止したところ、monitor は NO_RESULT を返したが
merge-applyの終了コードが 2 ではなく、SKILL.mdのrf merge-apply ... || continueによりnext-apply-roundが同じ群を再び返して kiro が再起動された。進行ごと停止して回避した。PR #757(#657 の中断と再開、v10.15.0): kiro が適用フェーズで 15 秒で終わり
kiro-apply-r1-result.jsonを残さない →merge-applyが 2 を返し、駆動側のcontinueがnext-apply-roundへ戻る → 同じ群(適用ラウンド 3 / 3、項目 R1-004)を再び開く。29 回繰り返した時点で手で止めた。 繰り返しの回数に上限が無いため、止めるまで CLI の起動が続く。この回は残る担当(codex 1 者)で回し直した。原因
群の状態を進めずに抜ける経路が 3 か所ある
merge-apply(scripts/refactor_lib/commands/apply.pyのcmd_merge_apply)が取り込みの前に終了コード 2 で抜けると、群のstatusはpendingのまま残る。next-apply-round(同cmd_next_apply_round、apply.py:296-298)はpendingとappliedの群を無条件に返すため、次の繰り返しで同じ群が開き直される。scripts/refactor_lib/gitfacts.pyのread_result(:1088-1100)STALLED/TIMEOUT/ 429 などで結果を残さずに終わったapply.pyの_load_apply_context(:460-466)greenでないapply.pyの_load_apply_context(:475-483)後の 2 つは項目を
blockedにするが、群のstatusは書き換えない。向きの問題として、下位の読み取り関数が進行の終了コードを決めている。
gitfacts.read_result(:1082)は結果ファイルが無い・読めないときにdie(..., code=2)(:1089/:1093/:1096)でプロセスを終わらせるため、呼び出し元のcmd_merge_applyが群の状態を書く機会が無い。取り込んだ後に取り消す経路(例: 必須トレーラーの欠落)は群を
status: droppedにし、次の群へ進む。止まるのは取り込みの前に抜ける 3 か所だけである。PR #535 では、同じ実行の別のラウンド(取り消しの理由は「申告したコミットが実体として無い」「トレーラーの欠落」)は取り消しの後に次の群へ進んでおり、この違いと合う。止まった回の担当は agy で、結果ファイルが無い経路と読める。再現(本物の
tests/conftest.pyとtest_merge_apply.pyの補助を使い、cmd_next_apply_round→cmd_merge_applyを直接呼んだ。群は 2 つ、1 つ目の担当は agy、結果ファイルは置かない):着手前テストが
redの場合もcode 2 status pendingになった。トレーラー欠落の場合はstatus after trailer drop: droppedだった。輪番が進まない
群の担当(
impl)は群を割り当てた時点で決まり、開き直しても変わらない。上の再現でも 2 つ目の群(担当 codex)は一度も開かれず、結果を残せない同じ担当が起動し続ける。手順書の骨組みが終了コードを捨て、上限も置いていない
SKILL.mdの「実行」の骨組み(:315-319)は次の形である。上限の値は骨組みが持たない。工程名(
--phase apply)から上限の表を引く。 表はplugins/ndf/scripts/lib/limits.pyだけが持ち、PHASE_TIMEOUT['apply']は 3600 秒である(
limits.py:32-40)。monitor.py(plugins/ndf/scripts/lib/monitor.py)の終了コード(2 = TIMEOUT / 3 = NO_RESULT / 4 = EARLY_ERROR / 5 = STALLED)を見ていない。どの失敗もmerge-applyに「結果ファイルなし」として届くmerge-applyの終了コード 2 を「群を取り消した」とみなしてcontinueするが、取り込みの前に抜けた場合は群が残っているため、同じ群へ戻るSKILL.md:49(ラウンドの表の「適用ラウンド」の行)とSKILL.md:56は「適用ラウンドに別の上限を置かない」と定めている。採用件数の上限が群の数を切るという理由だが、同じ群を開き直す場合には効かない。直すときはこの 2 か所を改める必要があるagy が STALLED になる背景(推定)
limits.AGENT_STALL(limits.py:43-48)で agy は 480 秒である(
monitor.py:101のDEFAULT_STALL_AGENT_BUILTINはこの表への別名)--stall-timeoutを渡していない。limits.py:15は「cross-refactoring の
apply/fix/final-fixには、骨組みが--stall-timeoutを引数で渡す(テスト 1 回を含む無出力の最長が工程で決まるため)。その許容はこの表に持たない」と書くが、
SKILL.md:315-319は--phase applyだけを渡す。文書とコードが食い違っているerr.log/stdout.log/progress.logの合計サイズで行う(monitor.py:792-796)。cross-review はscripts/launch-reviewer.sh:137-142で agy に$STEM-progress.logへ作業段階を追記させている。cross-refactoring のprompts/*.mdには進捗マーカーの指示が無い(grep -n progress prompts/*.mdの出力なし、終了コード 1)429 を早期の致命として検知できていない
monitor.pyのEARLY_ERROR_FATAL(:114-126)は^HTTP/… 429とrate limit exceededだけを見る。claude が出す"api_error_status":429を含む行を_scan_early_fatalに渡すとNoneが返る(一致しない)。そのため 429 は 15 秒で落ちても NO_RESULT として扱われ、上の開き直しに入る。影響
回避は手で止め、状態ファイルの群を
droppedへ書き換えることだが、手順書には書かれていない。agy の STALLED は、MONITOR_STALL_AGYを大きくすると打ち切りまでの時間を延ばせる。所要時間への影響(2026-09-01〜09-15 の実測)
集計の方法と全体の値は #662 にある。
提案
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の形を足す--stall-timeoutを渡す。 これは新しい決めごとではなく、limits.py:15が既にそう宣言している。文書とコードを揃える作業である
$STEM-progress.log)の指示を足す。先に rf646 の agy のログで、打ち切りの時点で agy が動いていたかを確かめるSKILL.mdの手順にする関連
next-apply-roundの開き直しで止まらない。同じ変更で直すmonitor.pyで重なるため、一緒に直すscripts/へ出す。上限と終了コードの扱いは、駆動をスクリプトへ出すときの置き場所になる--stall-timeoutを渡していない点である出どころ
rf646 の進行ログ(
STALLED4 回 / 429 の 3729 行)。#495 の担当(PR #646)の実施中に発生。PR #535 の構造改善の実測。devbasex/devbase#191(2026-09-17)と PR #757(2026-09-18、v10.15.0 の振り返り issue #550)で同じ事象を確認した。進行
モード: standard / 作業ツリー:
.worktrees/feat/issue-728-apply-intake/ 計画:issues/issue-728-647-592-553-plan.md閉じた理由
PR #796 で直り、ndf 10.16.0(2026-09-22、
main/ タグndf--v10.16.0、PR #810)で配布した。リリース後テスト(#810 (comment) )でこの課題の受け入れ条件はすべて合格した。振り返り: #810 (comment)