何を見つけたか
cross-review で、レビュワーが結果ファイルを書き終える前に judge が NO_RESULT と読み、ラウンドを中断する ことがある。
monitor.py が戻った直後に read-result → judge を流すと、<agent>-review-pr<N>-result.json が無いと読まれる。judge は「結果を残さなかった」として同じラウンドで 1 度だけ起動し直し、2 度目も同じ形で空振りすると final = error で中断する。
実際にはレビュワーはその後に結果ファイルを書き終えており、state の該当ラウンドには両者の APPROVE が後から入っていた。
どこで見つけたか
volareinc/carmo-cdk の PR #329 (cross-review 1 ラウンド)。
monitor.py 329 --agents agy,kiro が戻る
read-result が agy・kiro とも「result 未生成」
judge が RELAUNCH_AGENTS='agy kiro'(rc=7)→ 起動し直す
2 度目も 1〜3 と同じ → judge が rc=1「実行環境の側の問題として中断」
しばらく後に state を読むと final: approved、agy・kiro とも intent=APPROVE
同じ変更の PR #330 では、monitor.py の後に「全担当の結果ファイルが揃うまで最大 4 分待つ」ループを足したところ、1 回目で正しく agy=REQUEST_CHANGES / kiro=APPROVE と判定できた。
#598 / #537 は v10.13.0(2026-09-16)で解決した。 監視の上限は工程ごとの表
(plugins/ndf/scripts/lib/limits.py)が持ち、レビューの工程は 1200 秒になった。担当の CLI の
上限は、環境変数で解決した監視の上限 + 120 秒(既定 1320 秒)を導く。無進捗の許容は担当ごと
(codex 180 / agy 480 / kiro 480 / claude 900 秒)で、監視の上限とは別に働く。
そのため、agy と kiro に先に当たるのは無進捗の許容(480 秒)である。 レビューの工程の上限
1200 秒より短いため、打ち切りは STALLED として起きる。この事例はその打ち切りを経て起きた可能性が
最も高い。
なぜこの変更の範囲外なのか
cross-review の手順(scripts/monitor.py と SKILL.md の骨組み)の課題で、開発対象のリポジトリでは直せない。
直さないと何が起きるか
収束しているラウンドが「環境の問題」として中断する。利用者は外部 AI が落ちたと読み、レビュー無しでマージするか、何度も回し直すかの判断を迫られる。
原因を切り分ける問い
monitor.py はプロセスの終了だけでは OK を返さない。 プロセスが終わった時点で結果ファイルの
存在(サイズが 0 より大きいこと)を見て、無ければ NO_RESULT(終了コード 3)を返す
(plugins/ndf/scripts/lib/monitor.py:738-752 の _process_exit_outcome。--no-require-result を渡さない限り有効)。
そのため「結果ファイルの存在を完了判定に含める」形は既にある。 それでも後から結果が入った
経路として、次のどれが起きたかを分ける。
問い
確かめる材料
monitor.py は OK / NO_RESULT / TIMEOUT のどれで戻ったか
carmo-cdk PR #329 の monitor.py の標準出力(status と detail)
戻った後に誰が結果ファイルを書いたか
監視の上限で止めるとき、_kill_pid(monitor.py:444-469)は pidfile の 1 つの pid だけへ SIGTERM / SIGKILL を送り、プロセスグループは止めない。CLI が子プロセスに書かせていれば、止めた後に書き出しが起きうる。一般的な形では再現できた (下の実行結果。agy の実物では未確認)
起動し直した 2 度目も同じ経路か
2 度目の monitor.py の出力と、結果ファイルの更新時刻
止めた後の書き出しの再現。_kill_pid で、3 秒後に子プロセスが結果ファイルを書くシェルの親を止めた。
$ python3 - << 'E '
import sys, subprocess, time, os
sys.path.insert(0, 'plugins/ndf/scripts/lib')
import monitor
p = subprocess.Popen(['bash', '-c', '(sleep 3; echo {} > late-result.json) & wait'])
time.sleep(0.5)
monitor._kill_pid(p.pid)
time.sleep(0.3)
print('parent alive after kill:', p.poll() is None)
time.sleep(4)
print('result written after kill:', os.path.exists('late-result.json'))
E
parent alive after kill: False
result written after kill: True
切り分けの材料は残る。 #662 は v10.13.0(2026-09-16)で解決した。 監視の結果は担当ごとの <stem>-monitor.json と
追記だけの monitor-outcomes.jsonl に残り(書き出すのは monitor.py の main、:944)、理由の語彙は
plugins/ndf/scripts/lib/monitor_outcome.py が持つ(ok / timeout / stalled /
early_error / missing / pidfile_bad の 6 語。usage_limit と cli_timeout は未実装)。
結果ファイルには status / reason / detail / result_exists / elapsed が入るため、監視が
打ち切ったのか、プロセスの終了後に結果が無かったのか は後から分かる。
残るのは語彙 2 つである。 利用上限(usage_limit)と CLI 自身の上限(cli_timeout)は
未実装で、今は early_error と missing へ落ちる(#619 )。
監視の上限(TIMEOUT)で戻った後に書き出された場合は、#583 と同じ順序の問題になる。
その場合は #583 と一緒に直す。
修正レイヤー
plugins/ndf/scripts/lib/launch-cli.sh の起動(担当の CLI を nohup だけで起動し、プロセスグループを分けない。
:95 / :105 / :112 / :120)と、plugins/ndf/scripts/lib/monitor.py の _kill_pid(:444。1 つの pid だけへ
シグナルを送る)。「監視が止めた担当は、止めた後に何も書かない」を起動と停止の組で保証する場所である。
現れている read-result → judge の側で結果ファイルを待つと、止めた後に現れる結果ファイルや投稿の経路が
残り、#583 の投稿の後の打ち切りでも同じ食い違いが起きる。止めた担当の子プロセスまで止めれば、後から
結果ファイルが現れる経路そのものが消える。
止めた理由を読む側(結果なしの理由の語彙)は、担当 1 回の起動の結末を共通の語彙で読む #729 が持つ。
採る手
移動(move_responsibility)。停止の単位を pid からプロセスグループへ移す。launch-cli.sh は CLI を新しい
プロセスグループとして起動し、_kill_pid はそのグループへ SIGTERM / SIGKILL を送る。
直し方の候補
どちらも未実装である。 修正レイヤーに当たるのは 1 つ目で、2 つ目は現れている場所での受け皿になる。
監視の上限で止めるときに、プロセスグループごと止める(止めた後に書き出させない)。
plugins/ndf/scripts/lib/launch-cli.sh は CLI を nohup だけで起動しており、setsid も
os.killpg もリポジトリに無い(git grep -n 'setsid\|killpg\|getpgid' は monitor.py と
launch-cli.sh のどちらにも一致しない)。setsid で新しいプロセスグループとして起動し、
_kill_pid は os.killpg でグループへ SIGTERM / SIGKILL を送る
SKILL.md の骨組みで、read-result の前に結果ファイルの出現を上限付きで待つ
(PR cross-review の手順書 01-state-and-review.md が 508 行で、分割の基準を超えている #330 で足した「全担当の結果ファイルが揃うまで最大 4 分待つ」ループの形)。現行の骨組みに
この待ちは無い(bg-wait.sh は Bash の 1 回 600 秒の制限を越えるためのもので、結果ファイルは
待たない)
由来
volareinc/carmo-cdk issue #324 / PR #329
関連
#598 と #537 を直すと、この課題の頻度が下がる。 agy / kiro が 420 秒の監視の上限や 600 秒の CLI の上限に届くことが、止めた後の書き出しの入口になるためである。
次の課題は、同じ出口(結果なし → 同じラウンドで起動し直す → final=error)を持つ。
原因と直す箇所はそれぞれ別で、1 件を直しても他は残る。
進行
モード: standard / 作業ツリー: .worktrees/feat/issue-729-outcome-vocabulary / 計画: issues/issue-729-619-584-implementation-plan.md
閉じた理由
PR #791 で直り、ndf 10.16.0(2026-09-22、main / タグ ndf--v10.16.0、PR #810 )で配布した。リリース後テスト(#810 (comment) )でこの課題の受け入れ条件はすべて合格した。
振り返り: #810 (comment)
何を見つけたか
cross-review で、レビュワーが結果ファイルを書き終える前に
judgeが NO_RESULT と読み、ラウンドを中断することがある。monitor.pyが戻った直後にread-result→judgeを流すと、<agent>-review-pr<N>-result.jsonが無いと読まれる。judgeは「結果を残さなかった」として同じラウンドで 1 度だけ起動し直し、2 度目も同じ形で空振りするとfinal = errorで中断する。実際にはレビュワーはその後に結果ファイルを書き終えており、state の該当ラウンドには両者の
APPROVEが後から入っていた。どこで見つけたか
volareinc/carmo-cdkの PR #329(cross-review 1 ラウンド)。monitor.py 329 --agents agy,kiroが戻るread-resultが agy・kiro とも「result 未生成」judgeがRELAUNCH_AGENTS='agy kiro'(rc=7)→ 起動し直すjudgeが rc=1「実行環境の側の問題として中断」final: approved、agy・kiro ともintent=APPROVE同じ変更の PR #330 では、
monitor.pyの後に「全担当の結果ファイルが揃うまで最大 4 分待つ」ループを足したところ、1 回目で正しくagy=REQUEST_CHANGES / kiro=APPROVEと判定できた。#598 / #537 は v10.13.0(2026-09-16)で解決した。 監視の上限は工程ごとの表
(
plugins/ndf/scripts/lib/limits.py)が持ち、レビューの工程は 1200 秒になった。担当の CLI の上限は、環境変数で解決した監視の上限 + 120 秒(既定 1320 秒)を導く。無進捗の許容は担当ごと
(codex 180 / agy 480 / kiro 480 / claude 900 秒)で、監視の上限とは別に働く。
そのため、agy と kiro に先に当たるのは無進捗の許容(480 秒)である。 レビューの工程の上限
1200 秒より短いため、打ち切りは STALLED として起きる。この事例はその打ち切りを経て起きた可能性が
最も高い。
なぜこの変更の範囲外なのか
cross-review の手順(
scripts/monitor.pyとSKILL.mdの骨組み)の課題で、開発対象のリポジトリでは直せない。直さないと何が起きるか
収束しているラウンドが「環境の問題」として中断する。利用者は外部 AI が落ちたと読み、レビュー無しでマージするか、何度も回し直すかの判断を迫られる。
原因を切り分ける問い
monitor.pyはプロセスの終了だけでは OK を返さない。 プロセスが終わった時点で結果ファイルの存在(サイズが 0 より大きいこと)を見て、無ければ
NO_RESULT(終了コード 3)を返す(
plugins/ndf/scripts/lib/monitor.py:738-752の_process_exit_outcome。--no-require-resultを渡さない限り有効)。そのため「結果ファイルの存在を完了判定に含める」形は既にある。 それでも後から結果が入った
経路として、次のどれが起きたかを分ける。
monitor.pyは OK / NO_RESULT / TIMEOUT のどれで戻ったかmonitor.pyの標準出力(statusとdetail)_kill_pid(monitor.py:444-469)は pidfile の 1 つの pid だけへ SIGTERM / SIGKILL を送り、プロセスグループは止めない。CLI が子プロセスに書かせていれば、止めた後に書き出しが起きうる。一般的な形では再現できた(下の実行結果。agy の実物では未確認)monitor.pyの出力と、結果ファイルの更新時刻止めた後の書き出しの再現。
_kill_pidで、3 秒後に子プロセスが結果ファイルを書くシェルの親を止めた。切り分けの材料は残る。 #662 は v10.13.0(2026-09-16)で解決した。 監視の結果は担当ごとの
<stem>-monitor.jsonと追記だけの
monitor-outcomes.jsonlに残り(書き出すのはmonitor.pyのmain、:944)、理由の語彙はplugins/ndf/scripts/lib/monitor_outcome.pyが持つ(ok/timeout/stalled/early_error/missing/pidfile_badの 6 語。usage_limitとcli_timeoutは未実装)。結果ファイルには
status/reason/detail/result_exists/elapsedが入るため、監視が打ち切ったのか、プロセスの終了後に結果が無かったのかは後から分かる。
残るのは語彙 2 つである。 利用上限(
usage_limit)と CLI 自身の上限(cli_timeout)は未実装で、今は
early_errorとmissingへ落ちる(#619)。監視の上限(TIMEOUT)で戻った後に書き出された場合は、#583 と同じ順序の問題になる。
その場合は #583 と一緒に直す。
修正レイヤー
plugins/ndf/scripts/lib/launch-cli.shの起動(担当の CLI をnohupだけで起動し、プロセスグループを分けない。:95/:105/:112/:120)と、plugins/ndf/scripts/lib/monitor.pyの_kill_pid(:444。1 つの pid だけへシグナルを送る)。「監視が止めた担当は、止めた後に何も書かない」を起動と停止の組で保証する場所である。
現れている
read-result→judgeの側で結果ファイルを待つと、止めた後に現れる結果ファイルや投稿の経路が残り、#583 の投稿の後の打ち切りでも同じ食い違いが起きる。止めた担当の子プロセスまで止めれば、後から
結果ファイルが現れる経路そのものが消える。
止めた理由を読む側(結果なしの理由の語彙)は、担当 1 回の起動の結末を共通の語彙で読む #729 が持つ。
採る手
移動(
move_responsibility)。停止の単位を pid からプロセスグループへ移す。launch-cli.shは CLI を新しいプロセスグループとして起動し、
_kill_pidはそのグループへ SIGTERM / SIGKILL を送る。直し方の候補
どちらも未実装である。 修正レイヤーに当たるのは 1 つ目で、2 つ目は現れている場所での受け皿になる。
plugins/ndf/scripts/lib/launch-cli.shは CLI をnohupだけで起動しており、setsidもos.killpgもリポジトリに無い(git grep -n 'setsid\|killpg\|getpgid'はmonitor.pyとlaunch-cli.shのどちらにも一致しない)。setsidで新しいプロセスグループとして起動し、_kill_pidはos.killpgでグループへ SIGTERM / SIGKILL を送るSKILL.mdの骨組みで、read-resultの前に結果ファイルの出現を上限付きで待つ(PR cross-review の手順書 01-state-and-review.md が 508 行で、分割の基準を超えている #330 で足した「全担当の結果ファイルが揃うまで最大 4 分待つ」ループの形)。現行の骨組みに
この待ちは無い(
bg-wait.shは Bash の 1 回 600 秒の制限を越えるためのもので、結果ファイルは待たない)
由来
volareinc/carmo-cdk issue #324 / PR #329
関連
#598 と #537 を直すと、この課題の頻度が下がる。 agy / kiro が 420 秒の監視の上限や 600 秒の CLI の上限に届くことが、止めた後の書き出しの入口になるためである。
次の課題は、同じ出口(結果なし → 同じラウンドで起動し直す →
final=error)を持つ。原因と直す箇所はそれぞれ別で、1 件を直しても他は残る。
--print-timeoutが 600 秒固定で、大きな差分では CLI が先に打ち切る(CLI の上限)NO_RESULT(missing)へ畳まれ、上限であることが出ない(CLI の利用上限)進行
モード: standard / 作業ツリー:
.worktrees/feat/issue-729-outcome-vocabulary/ 計画:issues/issue-729-619-584-implementation-plan.md閉じた理由
PR #791 で直り、ndf 10.16.0(2026-09-22、
main/ タグndf--v10.16.0、PR #810)で配布した。リリース後テスト(#810 (comment) )でこの課題の受け入れ条件はすべて合格した。振り返り: #810 (comment)