Skip to content
Merged
415 changes: 415 additions & 0 deletions issues/PLAN21_cross-review-gemini-stall-and-fix-merge.md

Large diffs are not rendered by default.

158 changes: 158 additions & 0 deletions issues/i18-issue-gemini.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# ndf plugin (4.7.2) cross-review skill 修正提案

> 報告者: takemi-ohama
> 発見日: 2026-05-23
> 対象 plugin: `ai-plugins/ndf/4.7.2` — `skills/cross-review/`
> 検証 PR: devbasex/devbase #19 (Round 1 で収束。両 AI とも COMMENT/0 critical/0 major)

PR #19 で `/ndf:cross-review` を実走させた際に観測した、運用上の不整合 3 点をまとめる。
いずれも cross-review ループ自体は完走したが、メインセッション側で手動補正が必要だった。

## 1. gemini の stall-timeout デフォルト (180s) が短すぎる

### 現状

- `skills/cross-review/scripts/monitor.py:63`
```python
DEFAULT_STALL = int(os.environ.get("MONITOR_STALL", "180")) # 3 min no progress
```
- 既定 stall = 3 分。err.log + stdout.log の合計サイズに変化が無いと STALLED 扱いで kill される。

### 観測した事象 (PR #19 Round 1)

- gemini プロセス pid=26149 を起動 → 180 秒の間 err.log は 340B から増えず STALLED 判定 → kill
- 1 度目の launch で gemini node プロセスが「子側」だったため、`pkill` 後も実プロセス (pid 26155) が
生き残っていた (skill `アンチパターン` でも触れている既知の挙動)
- `--stall-timeout 480 --timeout 900` で再起動したところ、**195 秒**で正常終了 (`result.json` 生成済)
- sentinel は最後まで出ず (`sentinel_seen: false`)、`result_exists=True` のみで判定された
- 観測ログ (抜粋):
```
[gemini] ⏳ gemini elapsed=180s pid=26968 err_log=340B sentinel=-
[gemini] ✅ gemini OK (195s) — process exited; sentinel=False; result_exists=True
```

### 原因の推定

- codex は推論ステップを逐次 err.log に書き出すため `err_log_size` が増え続け stall 検知に引っかからない
- gemini はリクエスト送信〜レスポンス受領まで **err.log にほぼ何も書かない**
(起動直後の `YOLO mode is enabled.` 等の 340B のみ)。「サイズ非変化 = stall」前提と相性が悪い

### 提案

1. **agent 別に DEFAULT_STALL を分岐**:
```python
DEFAULT_STALL_CODEX = int(os.environ.get("MONITOR_STALL_CODEX", "180"))
DEFAULT_STALL_GEMINI = int(os.environ.get("MONITOR_STALL_GEMINI", "480"))
```
`monitor.py` の per-agent ループで切り替える。
2. 上記が大きすぎる変更なら、**gemini については stall 判定を緩める fallback** を入れる:
- `result.json` 不在 + プロセス生存 + sentinel 無し のときは hard timeout のみで判定
3. docs (`docs/01-state-and-review.md` 周辺) に「**gemini は err.log が静かなため stall-timeout を
≥ 480 秒推奨**」と明記

## 2. launch-gemini.sh プロンプトに result.json スキーマが明示されていない

### 現状

- `skills/cross-review/scripts/launch-gemini.sh:90`
```
- 投稿後、サマリを **$TMP_DIR/gemini-review-pr$STATE_PR-result.json** に書く
(フォーマットは launch-codex.sh と同じ)
```
- codex 側 (`launch-codex.sh:81-90`) は JSON フィールドを inline で例示している:
```json
{
"event": "REQUEST_CHANGES",
"posted_as": "COMMENT",
"comments_count": 5,
"review_url": "...",
"by_severity": {"critical": 0, "major": 3, "minor": 2, "nit": 0}
}
```
- `state.py:248` の `cmd_read_result` は `r.get("event")` 等を **codex の field 名前提で** 読む

### 観測した事象 (PR #19 Round 1)

- gemini が以下の **独自スキーマ**で result.json を生成:
```json
{
"status": "success",
"intent": "COMMENT",
"summary": "S3 URIのパース仕様と依存関係の案内メッセージについて、2点修正を提案します。"
}
```
- `state.py read-result 19 gemini` を実行すると `intent=None posted_as=None comments=None` で
state に登録され、judge 段階で `GEMINI_INTENT=None` (= SKIP 扱い) になりかけた
- 実際には gemini は GitHub にレビュー (id 4350631675) と inline コメント 2 件を正しく投稿していた
ため、メインセッション側で gh api から拾い直して result.json を手書きする羽目になった

### 提案

1. **launch-gemini.sh のプロンプトに codex と同じ JSON 例を inline 展開**する
(「launch-codex.sh と同じ」は LLM 側からは読めない参照のため不十分)
2. もしくは `state.py read-result` に **後方互換 field マップ** を入れる:
- `event` が無く `intent` がある → `intent` を採用
- `comments_count` が無ければ `review_url` から `gh api` で実数を拾う
3. 最小修正としては (1) で十分。長期的には (1) + (2) 両方が望ましい

## 3. fix サブエージェント戻り値ファイルのパス指定が散逸している

### 現状

- `state.py:368` (merge-fix)
```python
ffile = pathlib.Path(args.file or _tmp_dir() / f"fix-pr{pr}-result.json")
```
- `_tmp_dir()` は環境変数 `CROSS_REVIEW_TMP_DIR` または `~/.gemini/tmp/<workspace>/` を返す
(本検証では `/Users/takemi_ohama/.gemini/tmp/pr19/`)
- 一方、skill 本文 (`docs/02-fix-and-rotation.md` 周辺) の例や、メインセッションが Agent tool に
渡すプロンプトでは **`/tmp/fix-pr<#>-result.json`** が登場する箇所がある (要監査)

### 観測した事象 (PR #19 Round 1)

- general-purpose サブエージェントが `/tmp/fix-pr19-result.json` に書き込み完了
- メイン側で `state.py merge-fix 19` を実行 → `❌ fix サブエージェントが戻り値ファイルを生成しなかった`
で exit=3 (= ci-code-fail 扱い) になった
- 手動で `cp /tmp/fix-pr19-result.json $CROSS_REVIEW_TMP_DIR/` してから再実行で復旧

### 提案

1. skill docs (`docs/02-fix-and-rotation.md` および `SKILL.md` の Step 5 例) に
**「サブエージェントに渡すパスは必ず `$CROSS_REVIEW_TMP_DIR/fix-pr<PR>-result.json`」**
と明記し、`/tmp/...` の例を排除する
2. `state.py merge-fix` に **fallback** を 1 段追加:
```python
for candidate in [args.file, _tmp_dir() / f"fix-pr{pr}-result.json",
pathlib.Path(f"/tmp/fix-pr{pr}-result.json")]:
if candidate and candidate.exists() and candidate.stat().st_size > 0:
ffile = candidate; break
```
メインセッションのプロンプト誤りを救済できる
3. さらに、サブエージェントのキー名も `commit_sha`/`fixed` で来てしまうと
`state.py merge-fix` が `fix_commit`/`fixed_count` を期待して fixed=0 と記録される
(本検証で発生)。同じく後方互換 map を入れるのが望ましい

## 影響まとめ

| # | 影響 | 回避策 (現状) | 提案後の効果 |
|---|---|---|---|
| 1 | gemini が毎回 1 度目に STALLED で kill される | `--stall-timeout 480` を明示 | 既定で 1 発成功、孤児プロセスも減る |
| 2 | gemini 結果が state に正しく載らず judge 誤動作の恐れ | 手動で result.json を書き直す | スキーマ統一で手動補正不要 |
| 3 | merge-fix が失敗し final=error 扱いになるリスク | 手動 cp で復旧 | fallback または docs 統一で防止 |

## 関連ファイル

- `skills/cross-review/scripts/monitor.py` (#1)
- `skills/cross-review/scripts/launch-gemini.sh` (#2)
- `skills/cross-review/scripts/launch-codex.sh` (#2 参照元)
- `skills/cross-review/scripts/state.py` (#2, #3)
- `skills/cross-review/docs/01-state-and-review.md` (#1 ドキュメント追記)
- `skills/cross-review/docs/02-fix-and-rotation.md` (#3 ドキュメント追記)
- `skills/cross-review/SKILL.md` (#3 メインプロンプト例の修正)

## 補足: 本検証の環境

- macOS Darwin 25.5.0 / bash / claude-opus-4-7[1m]
- ndf plugin: `~/.claude/plugins/cache/ai-plugins/ndf/4.7.2/`
- worktree path: `/work/` が read-only のため `--worktree $HOME/cross-review-worktrees/pr19` を明示指定
(これは別途 docs 側に「`/work/` 不在時の代替パス例」を追記すると親切)
2 changes: 1 addition & 1 deletion plugins/ndf/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ndf",
"version": "4.7.3",
"version": "4.7.4",
"description": "Integrated plugin with 8 specialized agents (model-tiered: opus/sonnet/haiku), 39 skills including official mcp-builder, on-demand loader for Anthropic official skills, generic workflow/principle skills, skill usage statistics, pytest-playwright based scenario E2E testing (v0.5.0: BREAKING — package renamed scenario_test → playwright_kit, fixtures/CLI ndf_*/--ndf-* → pwk_*/--pwk-*, all-in-one runtime layout enabling Skill-independent operation via init_project.sh + run.sh, accessibility/web vitals autouse, overlay (formerly HUD), report.md, Drive integration, body_check autouse enabled by default to detect server-rendered PHP/SSR errors leaked into HTML), Google Drive/Chat integration, and Codex CLI integration via /ndf:codex skill. Transcript retention is automatically kept at >= 90 days. BREAKING (v4.0.0): Codex MCP server is removed (use /ndf:codex skill); legacy CLAUDE.ndf.md detection hook and /ndf:cleanup skill are removed (obsolete since v3.0.0). Serena MCP is a separate plugin (mcp-serena).",
"author": {
"name": "takemi-ohama",
Expand Down
51 changes: 51 additions & 0 deletions plugins/ndf/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,56 @@
# NDF Plugin CHANGELOG

### v4.7.4 (cross-review: gemini stall 既定の per-agent 化 + fix 戻り値マージの堅牢化)

`/ndf:cross-review` を実走させた際に、cross-review ループ自体は完走するが
メインセッション側で手動補正が必要だった運用不整合の残り 2 件を恒久対応する
PATCH リリース。

- **monitor.py: per-agent stall timeout 既定の導入** (`skills/cross-review/scripts/monitor.py`):
- 従来は `MONITOR_STALL=180s` の単一既定値だったため、err.log にほぼ進捗を出さない
gemini を 1 度目に毎回 STALLED 扱いで kill していた (孤児プロセスも残存)。
- 既定を agent 別ビルトインに変更: **codex=180s (変更なし) / gemini=480s**。
- 解決順: CLI `--stall-timeout` (明示優先) > env `MONITOR_STALL_<AGENT>` (per-agent) >
env `MONITOR_STALL` (両 agent 共通、後方互換) > agent 別ビルトイン。
- `--stall-timeout` の argparse default を `int` → `None` に変更し、未指定時のみ
per-agent 解決を行う。
- **state.py merge-fix: fix 戻り値ファイルの探索 fallback と key 別名対応**
(`skills/cross-review/scripts/state.py`):
- サブエージェントが `/tmp/fix-pr<PR>-result.json` に書き、`merge-fix` は
`$TMP_DIR` (= `~/.gemini/tmp/<workspace>/`) のみ参照する不整合で
exit 3 (ci-code-fail 扱い) になる事象を解消。探索順を **(1) `--file` 明示
→ (2) `$TMP_DIR/` → (3) `/tmp/`** の 3 段に変更。
- 全候補不在時の die メッセージに探索 path 一覧を含め、原因切り分け可能にした。
- サブエージェントが `commit_sha` / `fixed` 別名キーで書き出した場合も
`fix_commit` / `fixed_count` と同等に受理する key alias を追加 (silent な
`fixed=0` 記録の救済)。
- **SKILL.md / docs のパス記述統一**:
- `SKILL.md` 内の `/tmp/fix-pr<#>-result.json` ハードコード 3 箇所を
`$TMP_DIR/fix-pr<#>-result.json` に統一。
- 「すべて `/tmp/` に置き」の汎用記述も `$TMP_DIR/` (= `_tmp_dir()` 解決先) に修正。
- `docs/02-fix-and-rotation.md` に `$TMP_DIR` の解決順
(env `CROSS_REVIEW_TMP_DIR` > `~/.gemini/tmp/<workspace>/` > `/tmp/`) を 1 行明示。
- `docs/01-state-and-review.md` の monitor 説明表に per-agent stall 既定の
解決順を追記。
- **pytest 追加** (`skills/cross-review/tests/`):
- `test_monitor_stall_default.py` — codex/gemini ビルトイン + env 上書き 6 ケース。
- `test_state_merge_fix.py` — 正規 / `/tmp/` fallback / key 別名 / 全候補不在 /
`--file` 明示の 5 ケース。
- **関連 issue / plan**:
- `issues/i18-issue-gemini.md` (再現報告) /
`issues/PLAN21_cross-review-gemini-stall-and-fix-merge.md` (実装プラン)。

#### 既存ユーザへの影響

- `--stall-timeout` を CLI で明示しているユーザ: **挙動不変**。
- `MONITOR_STALL` env を指定しているユーザ: **挙動不変** (両 agent に同じ値が適用)。
- 何も指定していないユーザ: gemini 側の既定 stall timeout が 180s → 480s に
**緩和**される (kill されにくくなる方向のため非破壊)。
- 正規パス (`$TMP_DIR/fix-pr<PR>-result.json`) + 正規 key (`fix_commit` /
`fixed_count`) で書き出していたユーザ: **挙動不変**。
- 旧プロンプトで `/tmp/` ハードコードや別名 key を使っていたユーザ: **silent fail
していたケースが拾われるようになる** (修復方向の変更)。

### v4.7.3 (cross-review: macOS 対応 worktree base + result.json スキーマ堅牢化)

`/ndf:cross-review` を非 Linux コンテナ環境 (macOS / WSL 等) でも素直に動かせるよう、
Expand Down
9 changes: 5 additions & 4 deletions plugins/ndf/skills/cross-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ flowchart TD
Gemini --> Decide

Decide -->|両方 APPROVE / SKIP| Approved([final = approved]):::ok
Decide -->|一方でも REQUEST_CHANGES| Fix["Agent (general-purpose)<br/>/ndf:fix &lt;PR&gt; --defer-nit を worktree 内で実行<br/>・critical/major/minor 修正 + push<br/>・reply + resolveReviewThread<br/>・deferred/rejected は reply のみ<br/>→ /tmp/fix-pr&lt;#&gt;-result.json"]
Decide -->|一方でも REQUEST_CHANGES| Fix["Agent (general-purpose)<br/>/ndf:fix &lt;PR&gt; --defer-nit を worktree 内で実行<br/>・critical/major/minor 修正 + push<br/>・reply + resolveReviewThread<br/>・deferred/rejected は reply のみ<br/>→ $TMP_DIR/fix-pr&lt;#&gt;-result.json"]

Fix --> Check{収束チェック}
Check -->|max-rounds 到達| MaxR([final = max_rounds]):::stop
Expand Down Expand Up @@ -190,7 +190,7 @@ while :; do
# Step 4: 振動検知 (4=oscillation)
"$SCRIPTS/state.py" check-oscillation "$STATE_PR" || [ $? -eq 2 ] || exit 4

# Step 5: 修正サブエージェント起動 (Agent tool) → /tmp/fix-pr<STATE_PR>-result.json
# Step 5: 修正サブエージェント起動 (Agent tool) → $TMP_DIR/fix-pr<STATE_PR>-result.json
# - メインで Agent(subagent_type=general-purpose, ...) を呼ぶ。docs/02 参照
# - tmp パスは launcher / monitor.py と同じく **STATE_PR ベース** で統一
# Step 5 後段: fix 戻り値マージ + CI 分類 (3=code-fail で中断)
Expand Down Expand Up @@ -319,7 +319,7 @@ body に書くのは **設計レベル・PR 横断の修正提案** のみ。
## CI failure の分類(誤中断防止)

「CI 失敗 → 即 `final=error`」は乱暴。`scripts/state.py merge-fix` が
fix 戻り値ファイル (`/tmp/fix-pr<PR>-result.json`) を受け取った際に
fix 戻り値ファイル (`$TMP_DIR/fix-pr<PR>-result.json`) を受け取った際に
`ci_failed_checks` を以下で分類する:

| 分類 | パターン | 振る舞い |
Expand Down Expand Up @@ -371,7 +371,8 @@ pint / larastan / test / build などは **中断** を原則とする。
## メイン context 節約の工夫

1. **大きいファイルはメイン context に載せない**: payload / err.log / diff は
すべて `/tmp/` に置き、メインは state.json と result.json だけ読む
すべて `$TMP_DIR/` (= `state.py _tmp_dir()` の解決先) に置き、メインは
state.json と result.json だけ読む
2. **サブエージェント分離**: 修正は別 context window で実行
3. **PR ローテーション**: 1 PR あたりの会話履歴を抑える
4. **AI 直接投稿**: 中間ペイロードがメインを通らない
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ fi
| pidfile + `kill -0` | プロセス生存確認。alive 確認後に `/proc/<pid>/cmdline` で agent 名一致も検証 (PID 再利用対策)。**プロセスが既に死んでいる場合は result.json の有無のみで OK 判定**する (死亡直後 cmdline 不一致で誤検知しないため) |
| codex sentinel | err.log に `^tokens used$` 出現で正常完了マーク |
| early-error | **行頭限定** で `^Error:` / `^FATAL:` / `^panic:` / `^Traceback ` / `^HTTP/1.1 401\|403\|429` / `^Approval mode overridden to "default"` / `^Authentication failed` / 「quota exceeded」「rate limit exceeded」「API key not found/missing/invalid」「sandbox error」を含む行を検出 (diff/doc 引用文中の同語句は誤検知しないよう anchor + benign フィルタ併用) |
| stall timeout | err.log + stdout.log の合計サイズが既定 **3 分** 変化しなければ STALLED で中断 (`--stall-timeout` or `MONITOR_STALL` env) |
| stall timeout | err.log + stdout.log の合計サイズが一定時間変化しなければ STALLED で中断。既定は **agent 別** (codex=**180s** / gemini=**480s**)。gemini は err.log がほぼ無音のため大きめに取る。codex 側既定は不変。上書き方法: CLI `--stall-timeout` (明示優先) > env `MONITOR_STALL_<AGENT>` (per-agent) > env `MONITOR_STALL` (両 agent 共通) > agent 別ビルトイン |
| hard timeout | 既定 **7 分**。`--timeout` or `MONITOR_TIMEOUT` env で上書き |
| result.json 存在 | プロセス終了後、result.json が無ければ NO_RESULT (exit 3) |
| **失敗時 kill** | TIMEOUT / STALLED / EARLY_ERROR / PIDFILE_BAD で返るときは対象プロセスに SIGTERM → 3 秒後 SIGKILL。残存プロセスが後から `gh api` 投稿や result.json 書き込みを行うのを防ぐ |
Expand Down
2 changes: 2 additions & 0 deletions plugins/ndf/skills/cross-review/docs/02-fix-and-rotation.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
4. nit / 判断が割れる minor は **修正せず deferred 記録**(reply は `[deferred / nit]` ラベル付き、Resolve しない)
5. **PR レベルの Summary コメントを `gh pr comment` で投稿**(対応件数 / 重要度別 / deferred 件数 / rejected 件数 / commit SHA を含む)
6. 戻り値ファイル `$TMP_DIR/fix-pr<PR>-result.json` を必ず書き出す
(`$TMP_DIR` は env `CROSS_REVIEW_TMP_DIR` > `~/.gemini/tmp/<workspace>/` > `/tmp/` の順で解決。
詳細は `scripts/state.py _tmp_dir()` 参照。`/tmp/` 直書きでも `state.py merge-fix` は fallback で拾う)

> ⚠ inline thread への reply + Resolve **だけでは不十分**。PR ページの
> conversation タブに表示される **PR レベルコメント** がレビュアーへの
Expand Down
Loading