From 4f7fe89349c0786a8d460ae1b77600130aa6aa0c Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 16:13:04 +0000 Subject: [PATCH 01/22] =?UTF-8?q?Docs:=20=E5=88=A9=E7=94=A8=E4=B8=8A?= =?UTF-8?q?=E9=99=90=E3=81=AE=E6=96=87=E8=A8=80=E3=81=A8=E8=B5=B7=E5=8B=95?= =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=81=AA=E3=81=84=E7=A2=BA=E8=AA=8D=E3=82=B3?= =?UTF-8?q?=E3=83=9E=E3=83=B3=E3=83=89=E3=81=AE=E8=A6=81=E6=B1=82=E3=81=A8?= =?UTF-8?q?=E8=A8=AD=E8=A8=88=E3=82=92=E6=9B=B8=E3=81=8F=EF=BC=88#811=20#8?= =?UTF-8?q?13=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit codex と claude の実物の文言を記録と実行ファイルから集め、照合に足す 2 行と 誤検知の実測を設計に残す。認証の確認が起動できない例外を「通らない」として 返す設計と、10.16.1 のリリース後テストで確かめ直す条件(#478 #619 #729)を書く。 Co-Authored-By: Claude Opus 5 (1M context) --- issues/issue-811-813-design.md | 249 +++++++++++++++++++++++++++ issues/issue-811-813-requirements.md | 132 ++++++++++++++ 2 files changed, 381 insertions(+) create mode 100644 issues/issue-811-813-design.md create mode 100644 issues/issue-811-813-requirements.md diff --git a/issues/issue-811-813-design.md b/issues/issue-811-813-design.md new file mode 100644 index 000000000..713f3b996 --- /dev/null +++ b/issues/issue-811-813-design.md @@ -0,0 +1,249 @@ +# cross-review / cross-refactoring: codex と claude が利用上限で止まっても同じラウンドで起動し直され、PATH に読めないディレクトリがあると始まらない → 上限を理由として報告して起動し直さず、CLI が欠けても使える者だけで始まる(#811 #813) + +## 目的 + +- **壊れていること:** 監視の利用上限の照合の表が codex と claude の実物の文言に 1 つも一致しない(#811)。認証の確認が権限の例外を捕まえず、開始の手順ごと落ちる(#813) +- **誰が困るか:** 収束ループを回す利用者。上限の担当を打ち切りまで待たされ、読めない PATH の環境ではループが始まらない +- **直すと成り立つこと:** 実物の文言に一致する 2 行を照合の表へ足し、確認コマンドを起動できないときは「通らない」として返す。どちらも関数 1 つの範囲で直す + +要求と受け入れ条件は [issue-811-813-requirements.md](issue-811-813-requirements.md) にある。この文書は「どう作るか」だけを扱う。 + +## 用語の対応表 + +本文は左の業務用語で書く。識別子は表とコードブロックにだけ置く。 + +| 業務用語 | 識別子 | +| --- | --- | +| 監視 | `plugins/ndf/scripts/lib/monitor.py` | +| 利用上限の照合の表 | `USAGE_LIMIT_FATAL` | +| 行単位の照合 | `_scan_patterns`。表・引用・grep 形式の行と、引用符の内側の一致を除く | +| 標準エラーの記録 | 担当ごとの `-err.log` | +| 行頭の印 | codex が標準エラーの誤りの行に付ける `ERROR: ` | +| 認証の確認 | `plugins/ndf/scripts/lib/auth.py` の `probe_auth` | +| 確認コマンドを 1 つ走らせる関数 | `auth._run_probe` | +| 権限の例外 | `PermissionError`(`errno` 13)。`OSError` の下位 | +| 起動できない例外 | `OSError` とその下位のすべて | +| 実物 | CLI が実際に出した文言。手元の記録か、導入済みの CLI の実行ファイルに埋め込まれた文字列 | + +## なぜ要るか + +**利用上限の照合の表は 4 行で、どれも codex と claude の実物に一致しない。** #729 の要求の +時点で集めた文言は kiro と claude の JSON 出力だけだった。codex(既定の参加者)が上限に +当たると、理由が「結果ファイル無し」になり、同じラウンドで起動し直してまた上限で落ちる。 + +**認証の確認は、確認コマンドが見つからないことを「見つからない例外」だけで判定している。** +Python の子プロセスの起動は PATH を順に探し、見つからないまま読めないディレクトリを通ると、 +見つからない例外ではなく権限の例外を上げる。認証の確認は「例外は上げない」を約束しているが、 +この例外が開始の手順まで抜ける。 + +## 実測 + +測った環境: `develop`(90c0f06f)、Python 3.14.4、bash 5.3.9、codex-cli 0.154.0、 +claude 2.1.278(2026-09-22)。 + +### 文言を集めた経路と件数 + +| CLI | 集めた経路 | 件数 | 見つかった形 | +| --- | --- | --- | --- | +| codex | `~/.codex/sessions` の記録の誤りの本文(`error.message`)。誤りの種別が `usage_limit_exceeded` のもの | 62(1 形、2026-09-15) | `You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at <日時>.` | +| codex | 導入済みの実行ファイルに埋め込まれた文字列 | 6 形 | 上の形のほか、`You've hit your usage limit.` で始まる 3 形、`You've hit your usage limit for <モデル>.`、`exceeded retry limit, last status: <状態>` | +| claude | `~/.claude/projects` の記録のうち、API の誤りの印(`isApiErrorMessage`)が真の本文 | 5,103(4 形、2026-09-01〜22) | 週 3,751 / セッション 758 / 個人の支出 593 / 月の支出 1(下の表) | +| claude | 導入済みの実行ファイル | 期間を書かない形 `You've hit your limit` ほか | `You've hit your ` の後に期間や種類を差し込む作りである | +| claude | 同上。旧い形 `Claude AI usage limit reached` | 0 | 記録にも 0 件 | +| kiro | `~/.kiro` の記録 | 0 | 既存の照合の `Monthly request limit reached` は #619 の実物から採った | +| agy | `~/.gemini` の記録と `antigravity-cli/cli.log` | 0 | 一致したのは監視の照合の定義を写した行と、別のリポジトリのコード差分だけ | +| 全 CLI | 手元に残る標準エラーと標準出力の記録 296 件 | 上限の実物 0 | 一致したのは設計文書・テストを担当が読み上げた行だけ | + +**codex が誤りを標準エラーへ書く形は、行頭の印の後に誤りの本文を続けた 1 行である。** 存在しない +モデルを指定して `codex exec` を実行し、標準エラーの記録に次の行が出ることを確かめた。記録の +誤りの本文と同じ文字列である。利用上限の文言でこの形を見たわけではない(「未確認のまま残ること」)。 + +```text +ERROR: {"type":"error","status":400,"error":{"type":"invalid_request_error","message":"The 'ndf-no-such-model-xyz' model is not supported when using Codex with a ChatGPT account."}} +``` + +**claude の JSON 出力の上限は、足さなくても読める。** 導入済みの実行ファイルは、API の誤りで +終わった結果行に `api_error_status` を載せる。10.16.0 のリリース後テストでも、この形は理由 +「利用上限」になった。 + +### 足す 2 行の照合の結果 + +次の 2 行を照合の表の末尾に足し、行単位の照合で入力 1 行ずつを読んだ結果である。 + +```python +re.compile(r"^(?:ERROR:\s*)?You['’]ve hit your (?:[\w'’ ]+ )?(?:limit|budget)\b", re.MULTILINE) +re.compile(r"^(?:ERROR:\s*)?exceeded retry limit, last status: 429\b", re.MULTILINE) +``` + +| 区分 | 入力 | 今の利用上限 | 足した後 | +| --- | --- | --- | --- | +| 実物 | codex の利用上限(行頭の印あり / なし) | — | 一致 | +| 実物 | codex の再試行の上限 `… last status: 429 Too Many Requests` | — | 一致 | +| 実物 | codex の `Quota exceeded. Check your plan and billing details.` | 一致 | 一致 | +| 実物 | claude の `You've hit your weekly limit · resets Sep 22, 6am (UTC)` | — | 一致 | +| 実物 | claude の `You've hit your session limit · resets 6:30pm (UTC)` | — | 一致 | +| 実物 | claude の `You've hit your individual spend limit · run /usage-credits …` | — | 一致 | +| 実物 | claude の `You've hit your monthly spend limit. Run /usage-credits …` | — | 一致 | +| 実物 | claude の `You've hit your limit · resets 3am (UTC)` | — | 一致 | +| 実物 | kiro の `Monthly request limit reached` / HTTP 429 の状態行 | 一致 | 一致 | +| 一致しない | codex の再試行の上限 `… last status: 503 Service Unavailable` | — | — | +| 一致しない | codex の 400 の行(上のコードブロック) | — | — | +| 誤検知 | 表(バッククォートあり / なし)、本文のバッククォート、本文の「」 | — | — | +| 誤検知 | 文の途中、リスト、引用、grep 形式、Python の文字列 | — | — | +| 誤検知 | 差分の追加行 `+ERROR: You've hit …`、差分の文脈行 | — | — | + +### 起動できないときの例外 + +| 条件 | 上がる例外 | `errno` | +| --- | --- | --- | +| PATH に読めないディレクトリがあり、コマンドがどこにも無い | 権限の例外 | 13 `Permission denied` | +| 読めないディレクトリの無い PATH で、コマンドがどこにも無い | 見つからない例外 | 2 | +| 読めないディレクトリの後ろにコマンドがある | 例外なし(終了コード 0) | — | +| 実行権の無いファイル | 権限の例外 | 13 | +| 実行形式でないファイル | 起動できない例外 | 8 `Exec format error` | + +**cross-refactoring の開始の手順も落ちることを確かめた。** 参加者の解決の関数を、ホストの +claude が見つからず読めないディレクトリを含む PATH で呼ぶと、権限の例外で終了コード 1 になる。 +読めないディレクトリを外すと、codex だけが使える者として返る。 + +## 決定の記録 + +### 決定 1: 誤検知を防ぐために、足す文言は行頭で始まる形だけを照合する + +監視は担当が生きている間に照合し、一致すれば担当を止めて起動し直さない。文書の引用に一致すると、 +結果を書ける担当を止める。見逃すと 1 度起動し直すだけである。誤検知の費用のほうが大きい。 +**codex は作業中のコマンドの出力(差分やファイルの中身)も標準エラーへ書く**ため、文言を +含む文書やテストを読み上げた行が記録に入る。行頭に固定すると、文の途中・差分の行・字下げ +した文字列は一致しない。行頭の印は codex の形に合わせて省略可能にする。 + +行頭に固定しない照合は採らない。除外の規則(表・引用・grep 形式)は文の途中に裸で出た文言を +除けない。 + +### 決定 2: 利用上限の文言は、期間や種類を問わず 1 行の照合で読む + +claude は `You've hit your ` の後に期間や種類(週・セッション・個人の支出など)を差し込んで +文言を作る。codex も同じ書き出しで 5 形を持つ。書き出しと、末尾の `limit` / `budget` で読めば、 +記録と実行ファイルで見つけた書き出しの形をすべて 1 行で覆える。形ごとに行を足すと、CLI が種類を増やすたびに照合が遅れる。 + +### 決定 3: codex の再試行の上限は、最後の状態が 429 のときだけ利用上限と読む + +同じ文言は状態コードを差し込んで作られ、503 などの一時的な誤りでも出る。一時的な誤りは +起動し直せば解けうるため、利用上限にしない。 + +### 決定 4: 実物が見つからない文言は照合に足さない + +照合の表の行は、記録か導入済みの実行ファイルに出所を持つものだけにする。出所の無い行は、 +合っているかをテストで確かめられない。書いた側の思い込みがテストの期待値にも入り、通って +しまう。claude の旧い形(`Claude AI usage limit reached`)は記録にも実行ファイルにも無いため +足さない。記録に現れたら、同じ手順で出所を書いて足す。 + +### 決定 5: 照合を足す先は標準エラーの記録だけにし、claude の標準出力の照合は変えない + +claude は JSON を出す形で起動しており、上限の結果行は状態コードを持つ。JSON は 1 行に文章を +含むため、行頭の照合が効かず、担当のレビュー本文の引用に一致しうる。 + +### 決定 6: 確認コマンドを起動できないときは、例外の種類を問わず「通らない」として返す + +認証の確認は「例外は上げない」を約束しており、起動できない理由(権限・実行形式など)が +何であっても、その CLI は使えない。見つからない例外だけは従来の理由のまま残し、その他の +起動できない例外は `コマンドを実行できません(<理由>)` にまとめる。理由には例外の説明 +(`strerror`)を入れる。 + +権限の例外だけを足す形は採らない。実行形式でないファイルで同じ落ち方をする。 + +### 決定 7: 読めない PATH で見つからないときも、理由は起動できないとして出す + +Python の起動は、読めないディレクトリがあると「見つからない」と「実行権が無い」を同じ +例外で返す。見分けるには PATH を自分で探し直す必要があり、関数 1 つの範囲を超える。理由の +文言に `Permission denied` が出れば、利用者は PATH かファイルの権限を見ればよい。 + +## 構成要素 + +| 要素 | 変えること | +| --- | --- | +| 利用上限の照合の表(監視) | 決定 1〜3 の 2 行を末尾に足す | +| 確認コマンドを 1 つ走らせる関数(認証の確認) | 起動できない例外を捕まえ、「通らない」と理由を返す | +| 監視のテスト | 実測の表の実物と誤検知の形を入力にする | +| 認証の確認のテスト | 起動できない 2 形(権限・実行形式)を入力にする | +| 2 つの開始の手順のテスト | 読めないディレクトリを含む PATH で、欠けた CLI が外れることを確かめる | + +```text +plugins/ndf/ +├── scripts/lib/ +│ ├── monitor.py # 利用上限の照合の表に 2 行 +│ └── auth.py # 確認コマンドを 1 つ走らせる関数 +├── scripts/tests/ +│ └── test_auth_probe.py +└── skills/ + ├── cross-review/tests/ + │ ├── test_monitor_usage_limit.py + │ └── test_state_review_pool.py + └── cross-refactoring/tests/ + └── test_init.py +``` + +```mermaid +graph TD + CR["cross-review の開始の手順"] --> AU["認証の確認"] + RF["cross-refactoring の開始の手順"] --> AU + AU --> RP["確認コマンドを
1 つ走らせる関数"] + MON["監視"] --> UL["利用上限の照合の表"] + UL --> OUT["結末の理由
利用上限"] +``` + +## 処理の流れ + +### 監視が標準エラーの記録を読む + +```mermaid +graph TD + A["標準エラーの記録の
末尾 200KB"] --> B{"利用上限の
照合の表に一致"} + B -->|"一致"| C{"除外の規則
表・引用・grep 形式"} + C -->|"除外しない"| D["担当を止める
理由は利用上限"] + C -->|"除外する"| B + B -->|"一致なし"| E["既存の致命の照合へ"] +``` + +足した 2 行は照合の表の末尾に入るため、既存の 4 行より後に照合される。どの行に一致しても +理由は同じであり、順序は結果を変えない。 + +### 認証の確認が 1 つの CLI を確かめる + +```mermaid +graph TD + A["確認コマンドを起動"] --> B{"例外"} + B -->|"見つからない"| C["通らない
コマンドが見つかりません"] + B -->|"時間切れ"| D["通らない
応答しませんでした"] + B -->|"その他の起動できない例外"| E["通らない
コマンドを実行できません"] + B -->|"なし"| F["終了コードと文言で判定"] +``` + +見つからない例外は起動できない例外の下位にあるため、先に捕まえる。時間切れの例外は起動 +できない例外の下位ではないため、順序に依らない。 + +## テスト設計 + +| 受け入れ条件 | 何で確かめるか | +| --- | --- | +| AC1〜AC4 | 監視のテストに、実測の「実物」の行を 1 つずつ標準エラーの記録へ書き、結末を読む。理由が利用上限・起動し直しの可否が偽・終了コード 4 | +| AC2 の後半 | 同じテストで 503 の行を書き、利用上限にならないこと | +| AC5 | 同じテストで実測の「誤検知」の形を書き、担当が止まらないこと | +| AC6 | テストの入力の文字列が、実測の表の文言と一致すること(レビューで照らす) | +| AC7 / AC8 | 認証の確認のテストで、読めないディレクトリを含む PATH と、実行形式でないファイルを確認コマンドにする | +| AC9 | 同じテストの既存の 2 件(見つからない・時間切れ)が通ること | +| AC10 | cross-review の参加者の解決のテストに、読めないディレクトリを含む PATH の場合を足す | +| AC11 | cross-refactoring の開始の手順のテストに、同じ場合を足す | +| AC12〜AC14 | 10.16.1 のリリース後テスト(利用者の環境で手動) | + +読めないディレクトリはテストの中で作る(権限を外した一時ディレクトリ)。root で実行すると +権限が効かないため、そのときはテストを飛ばす。 + +## 未確認のまま残ること + +| 項目 | 内容 | +| --- | --- | +| codex の利用上限の文言の行頭の形 | 400 の誤りで測った形(`ERROR: ` + 本文)からの類推。照合は行頭の印の有無を問わないため、印が別の形(時刻を前に置くなど)だと一致しない。10.16.1 の後に上限に当たった記録で確かめる | +| codex の再試行の上限の状態の書き方 | 実行ファイルの文字列は `last status: ` までで、状態の後ろの書き方は記録に無い。照合は `429` の直後の語の区切りまでしか見ない | +| kiro と agy の利用上限の文言 | 手元の記録に 0 件。kiro は既存の #619 の実物、agy は照合なし | +| claude の旧い形 | 記録にも実行ファイルにも 0 件のため足さない(決定 4) | +| 行頭に固定しても残る誤検知 | 文言で始まる行をそのまま読み上げた場合(文書の本文の行頭に裸で置いた文言を `cat` したときなど)は一致する。この束の文書とテストでは、文言を表・バッククォート・文字列の中にだけ置く | diff --git a/issues/issue-811-813-requirements.md b/issues/issue-811-813-requirements.md new file mode 100644 index 000000000..e3eec807a --- /dev/null +++ b/issues/issue-811-813-requirements.md @@ -0,0 +1,132 @@ +# cross-review / cross-refactoring: codex と claude が利用上限で止まっても同じラウンドで起動し直され、PATH に読めないディレクトリがあると始まらない → 上限を理由として報告して起動し直さず、CLI が欠けても使える者だけで始まる(#811 #813) + +## 目的 + +- **壊れていること:** codex と claude の利用上限の文言を監視が読まず、上限で止まった担当を同じラウンドで起動し直す(#811)。PATH に読めないディレクトリがあると、CLI が 1 つ欠けただけで開始の手順が落ちる(#813) +- **誰が困るか:** 収束ループ(cross-review / cross-refactoring)を回す利用者。上限は解けないため打ち切りまで待たされ、理由も出ない。読めない PATH を持つ環境では、ループが始まらない +- **直すと成り立つこと:** 実物の文言で止まった担当は理由「利用上限」で報告され、起動し直されない。確認コマンドを実行できない CLI は外され、使える者だけで始まる + +設計は [issue-811-813-design.md](issue-811-813-design.md) にある。この文書は「何を満たすか」だけを扱う。 + +## 用語の対応表 + +本文は左の業務用語で書く。識別子は表とコードブロックにだけ置く。 + +| 業務用語 | 識別子 | +| --- | --- | +| 監視 | `plugins/ndf/scripts/lib/monitor.py` | +| 利用上限の照合の表 | `USAGE_LIMIT_FATAL` | +| 標準エラーの記録 | 担当ごとの `-err.log` | +| 結末 | 起動 1 回の終わり方(`read_launch_outcome` が返す `LaunchOutcome`) | +| 理由「利用上限」 | `reason = "usage_limit"` | +| 起動し直しの可否 | `relaunch_same_agent` | +| 早期の致命 | 監視の状態 `EARLY_ERROR`(終了コード 4) | +| 認証の確認 | `plugins/ndf/scripts/lib/auth.py` の `probe_auth` | +| 確認コマンドを 1 つ走らせる関数 | `auth._run_probe` | +| 開始の手順 | cross-review の `state.py init`、cross-refactoring の `refactor.py init` | +| 実物 | CLI が実際に出した文言。手元の記録か、導入済みの CLI の実行ファイルに埋め込まれた文字列 | + +## なぜ要るか + +ndf 10.16.0 のリリース後テスト(PR #810 のコメント、2026-09-22)で 3 つの受け入れ条件が不合格になった。 + +| 課題 | 条件 | 不合格の中身 | 起票 | +| --- | --- | --- | --- | +| #729 / #619 | 利用上限で止まった担当が理由「利用上限」で報告され、起動し直されない | kiro と claude の JSON 出力は満たした。codex の文言 2 形式と claude のテキスト出力の文言 2 形式は理由が「結果ファイル無し」になり、同じラウンドで起動し直す | #811 | +| #478 | AC45 前半: CLI が 1 者欠けても開始の手順が終了コード 0 で終わり、使える者だけで 2 席を埋める | PATH の 13 番目にある読めないディレクトリ(`/root/.local/bin`)のため、確認コマンドが見つからないときに権限の例外が上がり、終了コード 1 で落ちた | #813 | + +**#729 の要求の時点で文言を集めたのは kiro と claude の JSON 出力だけだった**(10.16.0 の振り返り)。そのため、この束では文言を実物から集め、集めた経路と件数を設計の「実測」に残す。 + +## 対象範囲 + +含む: + +- 監視が読む利用上限の文言に、codex と claude の実物の形を足す(#811) +- 確認コマンドを実行できないときに、例外を上げず「通らない」として返す(#813) +- 上の 2 つを cross-review と cross-refactoring の両方の経路で確かめる + +含まない: + +- **実物が見つからない文言は足さない。** claude の旧い形(`Claude AI usage limit reached|<時刻>`)は、手元の記録に 0 件だった。導入済みの claude 2.1.278 の実行ファイルにも 0 件だった(設計の「実測」)。agy の利用上限の文言も記録に 0 件で、足さない +- claude の標準出力(JSON)の照合は変えない。JSON の結果行が上限の状態コードを持つことは実行ファイルで確かめてあり、10.16.0 のリリース後テストでも合格している +- 利用上限の後に担当を替えるかどうかの判断(Skill 側の既存の振る舞い)は変えない +- 理由の語彙・監視の終了コード・標準出力のキーは変えない +- 必須の外部コマンド(`git` / `gh`)の呼び出しの例外処理は変えない。無ければ動けないため、落ちる振る舞いのままでよい +- **設計の成果物のうち、対象が無いため省くもの:** システム構成図(外部の系・配置が変わらない)、クラス図(型を足さず変えない)、非機能設計表(非機能の条件が無い) + +## 影響 + +| 対象 | 影響 | +| --- | --- | +| 公開インタフェース | 変わらない。監視の終了コード・標準出力のキー・結末の語彙はそのまま | +| データ | 変わらない。状態ファイル・監視の結果ファイルの形は同じ | +| 既存の振る舞い | 利用上限の照合の表に 2 行が増える。認証の確認が、起動できない CLI を例外ではなく「通らない」として返す | + +## 前提とする取り決め + +| 項目 | 参照先 / 決めたこと | +| --- | --- | +| プロジェクト構造 | `AGENTS.md`。監視と認証の確認は共通層(`plugins/ndf/scripts/lib/`)に置き、Skill 側へ写さない | +| コーディング規約 | `AGENTS.md` の「確かめる対象は外部コマンドに限らない」。照合の入力はこの束で集めた実物から作る | +| テスト戦略 | 照合の表は単体テスト(`plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py`)、認証の確認は単体テスト(`plugins/ndf/scripts/tests/test_auth_probe.py`)、2 つの開始の手順は既存の参加者の解決のテストに PATH の条件を足す | + +## 境界 + +| 区分 | 内容 | +| --- | --- | +| 常に行う | 全テストの実行。照合の文言を足すときは、出所(記録か実行ファイル)を設計の「実測」に書く | +| 確認してから行う | 監視の終了コード・理由の語彙を変えること | +| 行わない | 実物の無い文言を照合へ足すこと。必須の外部コマンドの例外処理を変えること | + +## 検証手段 + +| 項目 | 手段 | +| --- | --- | +| テスト | `uv run --with pytest pytest scripts/tests plugins/ndf -q` | +| 文書の検査 | `python3 scripts/check-doc-line-limit.py`、`python3 scripts/check-markdown-links.py` | +| 手動確認 | AC10 / AC11 は、読めないディレクトリを先頭に置いた PATH で開始の手順を実際に実行する。AC12〜AC14 は 10.16.1 のリリース後テスト | + +## 受け入れ条件 + +### 利用上限の文言(#811) + +各条件の「利用上限として止まる」は、次の 3 つがそろうことを指す: 監視が担当を止めて早期の致命(終了コード 4)で終わる / 結末の理由が「利用上限」 / 起動し直しの可否が偽。 + +- [ ] AC1: 標準エラーの記録に codex の利用上限の文言の 1 行が出ると、利用上限として止まる。文言は `You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage …` である。行頭の `ERROR: ` の有無を問わない +- [ ] AC2: 標準エラーの記録に codex の再試行の上限の行が出て、最後の状態が 429 なら、利用上限として止まる。行は `exceeded retry limit, last status: 429 Too Many Requests` である。最後の状態が 429 以外(例 `503 Service Unavailable`)なら利用上限にならない +- [ ] AC3: 標準エラーの記録に claude の利用上限の文言が行頭から出ると、利用上限として止まる。確かめる形は設計の「実測」の claude の 5 形(週・セッション・個人の支出・月の支出・期間を書かない形) +- [ ] AC4: 10.16.0 で合格した形は、引き続き利用上限として止まる。形は 4 つで、kiro の `Monthly request limit reached`、claude の JSON の上限の状態コード、HTTP 429 の状態行、`Quota exceeded` である +- [ ] AC5: AC1〜AC3 の文言が、表・バッククォート・「」・リスト・引用・grep 形式・Python の文字列・差分の追加行と文脈行・文の途中に出たときは、利用上限として止まらない +- [ ] AC6: AC1〜AC5 のテストの入力は、設計の「実測」の表にある実物の文言をそのまま写したものである(推測で作った文言を入力にしない) + +### 確認コマンドを実行できないとき(#813) + +- [ ] AC7: PATH に読めないディレクトリがあり、確認コマンドがどこにも無いとき、認証の確認は例外を上げず「通らない」を返す。理由は `コマンドを実行できません(Permission denied)` の形である +- [ ] AC8: 確認コマンドが実行形式でない(`Exec format error`)など、権限以外の理由で起動できないときも、AC7 と同じく「通らない」を返し、理由に起動できなかった理由を入れる +- [ ] AC9: 確認コマンドが見つからないときの理由(`コマンドが見つかりません`)と、時間切れのときの理由は変わらない +- [ ] AC10: AC7 の PATH で CLI を 1 つ欠いたとき、cross-review の開始の手順が終了コード 0 で終わり、欠けた CLI を外して使える者だけで席を埋める +- [ ] AC11: AC7 の PATH で CLI を 1 つ欠いたとき、cross-refactoring の開始の手順が終了コード 0 で終わり、欠けた CLI を外して使える者だけで始まる + +### 10.16.1 のリリース後テストで確かめ直す条件 + +**次の 3 行は、10.16.1 を配布した後に利用者の環境で確かめる。** 3 行とも合格したら #478 #619 #729 を閉じる。 + +- [ ] AC12(#478 の AC45 前半): 利用者の PATH から `kiro-cli` だけを隠し、検証用の Pull Request で cross-review の開始の手順を実行する。終了コード 0 で終わり、使える者だけで 2 席を埋める。PATH は読めない `/root/.local/bin` を含んだままにする +- [ ] AC13(#619 / #729): 導入先の監視に、設計の「実測」の codex の 2 形と claude の 5 形を 1 つずつ与えて結末を読む。すべて理由「利用上限」・起動し直しの可否が偽になる +- [ ] AC14(#619 / #729 の退行): 同じ手順で、10.16.0 で合格した kiro と claude の JSON の形も理由「利用上限」・起動し直しの可否が偽のままである + +## 依頼(原文) + +### #811 の本文(抜粋) + +> ndf 10.16.0(タグ `ndf--v10.16.0`)の監視は、**codex と claude(テキスト出力)の利用上限の文言を利用上限として読まない。** 担当が上限で止まると理由が `usage_limit` ではなく `missing` になり、`read_launch_outcome` は `relaunch_same_agent=True` を返す。つまり同じラウンドで起動し直す。 + +### #811 の追記 + +> 直すときは、各 CLI の実物の記録(`~/.codex/sessions` の `error.message`、`claude -p` のテキスト出力、kiro の err.log)から利用上限の文言を集め、テストの入力をその写しから作る。 + +### #813 の本文(抜粋) + +> PATH に**読めないディレクトリ**があると、参加者の CLI が入っていないときに `cross-review` の `init` が終了コード 0 ではなく、トレースバックを出して終了コード 1 で落ちる。#478 が直した「CLI が 1 者欠けても使える者だけで始まる」が、この PATH では成り立たない。 +> +> 直し方の候補は `except OSError` で捕まえて「コマンドを実行できません(<理由>)」として外すこと。 From 33699a92fe86d63e479a9ff2e1ac2e5738a1f86a Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 16:33:54 +0000 Subject: [PATCH 02/22] =?UTF-8?q?Docs:=20claude=20=E3=81=AE=E5=88=A9?= =?UTF-8?q?=E7=94=A8=E4=B8=8A=E9=99=90=E3=81=AE=E6=96=87=E8=A8=80=E3=81=8C?= =?UTF-8?q?=E6=A8=99=E6=BA=96=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=B8=E5=87=BA?= =?UTF-8?q?=E3=82=8B=E3=81=8B=E3=82=92=E6=9C=AA=E7=A2=BA=E8=AA=8D=E3=81=A8?= =?UTF-8?q?=E3=81=97=E3=81=A6=E6=98=8E=E8=A8=98=E3=81=99=E3=82=8B=EF=BC=88?= =?UTF-8?q?#811=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- issues/issue-811-813-design.md | 3 +++ issues/issue-811-813-requirements.md | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/issues/issue-811-813-design.md b/issues/issue-811-813-design.md index 713f3b996..39787fd73 100644 --- a/issues/issue-811-813-design.md +++ b/issues/issue-811-813-design.md @@ -142,6 +142,8 @@ claude は `You've hit your ` の後に期間や種類(週・セッション claude は JSON を出す形で起動しており、上限の結果行は状態コードを持つ。JSON は 1 行に文章を 含むため、行頭の照合が効かず、担当のレビュー本文の引用に一致しうる。 +claude の 5 形は、codex と共通の 1 行(決定 2)で読める。そのため標準エラーの記録に出れば利用上限になる。ただし、JSON 出力で起動した claude が上限のときに標準エラーへ書いた記録は無い。起動した claude の上限を読む経路は、標準出力の状態コードのままである。 + ### 決定 6: 確認コマンドを起動できないときは、例外の種類を問わず「通らない」として返す 認証の確認は「例外は上げない」を約束しており、起動できない理由(権限・実行形式など)が @@ -243,6 +245,7 @@ graph TD | 項目 | 内容 | | --- | --- | | codex の利用上限の文言の行頭の形 | 400 の誤りで測った形(`ERROR: ` + 本文)からの類推。照合は行頭の印の有無を問わないため、印が別の形(時刻を前に置くなど)だと一致しない。10.16.1 の後に上限に当たった記録で確かめる | +| claude の文言が標準エラーの記録へ出るか | 実測の claude の 5 形の出所は、会話の記録と実行ファイルである。JSON 出力で起動した claude が上限のときに標準エラーへ書いた記録は無い。起動した claude の上限は、標準出力の状態コード(10.16.0 で合格)で読む。10.16.1 の後に上限に当たった標準エラーの記録で確かめる | | codex の再試行の上限の状態の書き方 | 実行ファイルの文字列は `last status: ` までで、状態の後ろの書き方は記録に無い。照合は `429` の直後の語の区切りまでしか見ない | | kiro と agy の利用上限の文言 | 手元の記録に 0 件。kiro は既存の #619 の実物、agy は照合なし | | claude の旧い形 | 記録にも実行ファイルにも 0 件のため足さない(決定 4) | diff --git a/issues/issue-811-813-requirements.md b/issues/issue-811-813-requirements.md index e3eec807a..774c5ac0d 100644 --- a/issues/issue-811-813-requirements.md +++ b/issues/issue-811-813-requirements.md @@ -94,7 +94,7 @@ ndf 10.16.0 のリリース後テスト(PR #810 のコメント、2026-09-22 - [ ] AC1: 標準エラーの記録に codex の利用上限の文言の 1 行が出ると、利用上限として止まる。文言は `You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage …` である。行頭の `ERROR: ` の有無を問わない - [ ] AC2: 標準エラーの記録に codex の再試行の上限の行が出て、最後の状態が 429 なら、利用上限として止まる。行は `exceeded retry limit, last status: 429 Too Many Requests` である。最後の状態が 429 以外(例 `503 Service Unavailable`)なら利用上限にならない -- [ ] AC3: 標準エラーの記録に claude の利用上限の文言が行頭から出ると、利用上限として止まる。確かめる形は設計の「実測」の claude の 5 形(週・セッション・個人の支出・月の支出・期間を書かない形) +- [ ] AC3: 標準エラーの記録に claude の利用上限の文言が行頭から出ると、利用上限として止まる。確かめる形は設計の「実測」の claude の 5 形(週・セッション・個人の支出・月の支出・期間を書かない形)である。JSON 出力で起動した claude がこの文言を標準エラーへ書くかは確かめていない。起動した claude の上限は AC4 の JSON の状態コードで読む - [ ] AC4: 10.16.0 で合格した形は、引き続き利用上限として止まる。形は 4 つで、kiro の `Monthly request limit reached`、claude の JSON の上限の状態コード、HTTP 429 の状態行、`Quota exceeded` である - [ ] AC5: AC1〜AC3 の文言が、表・バッククォート・「」・リスト・引用・grep 形式・Python の文字列・差分の追加行と文脈行・文の途中に出たときは、利用上限として止まらない - [ ] AC6: AC1〜AC5 のテストの入力は、設計の「実測」の表にある実物の文言をそのまま写したものである(推測で作った文言を入力にしない) From cb1a7bfe367bfcdfc64d90f1c0aba3d186c8e629 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 16:40:44 +0000 Subject: [PATCH 03/22] =?UTF-8?q?Docs:=20=E5=AE=9F=E7=89=A9=E3=81=AB?= =?UTF-8?q?=E7=84=A1=E3=81=84=E6=9C=AB=E5=B0=BE=E3=82=92=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=81=AE=E5=85=A5=E5=8A=9B=E3=81=8B=E3=82=89=E5=A4=96?= =?UTF-8?q?=E3=81=97=E3=80=81codex=20=E3=81=AE=E4=B8=8A=E9=99=90=E3=81=AE?= =?UTF-8?q?=E6=A8=99=E6=BA=96=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=AE=E8=A8=98?= =?UTF-8?q?=E9=8C=B2=E3=82=92=E5=AE=9F=E6=B8=AC=E3=81=B8=E5=85=A5=E3=82=8C?= =?UTF-8?q?=E3=82=8B=EF=BC=88#811=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- issues/issue-811-813-design.md | 12 +++++++----- issues/issue-811-813-requirements.md | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/issues/issue-811-813-design.md b/issues/issue-811-813-design.md index 39787fd73..66e361c35 100644 --- a/issues/issue-811-813-design.md +++ b/issues/issue-811-813-design.md @@ -46,6 +46,7 @@ claude 2.1.278(2026-09-22)。 | CLI | 集めた経路 | 件数 | 見つかった形 | | --- | --- | --- | --- | | codex | `~/.codex/sessions` の記録の誤りの本文(`error.message`)。誤りの種別が `usage_limit_exceeded` のもの | 62(1 形、2026-09-15) | `You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at <日時>.` | +| codex | この設計 Pull Request の cross-review のラウンド 2 で、codex が利用上限で止まった標準エラーの記録(2026-09-22 16:35 UTC、132 行) | 2 行(1 形) | `ERROR: You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at 5:44 PM.` | | codex | 導入済みの実行ファイルに埋め込まれた文字列 | 6 形 | 上の形のほか、`You've hit your usage limit.` で始まる 3 形、`You've hit your usage limit for <モデル>.`、`exceeded retry limit, last status: <状態>` | | claude | `~/.claude/projects` の記録のうち、API の誤りの印(`isApiErrorMessage`)が真の本文 | 5,103(4 形、2026-09-01〜22) | 週 3,751 / セッション 758 / 個人の支出 593 / 月の支出 1(下の表) | | claude | 導入済みの実行ファイル | 期間を書かない形 `You've hit your limit` ほか | `You've hit your ` の後に期間や種類を差し込む作りである | @@ -56,7 +57,7 @@ claude 2.1.278(2026-09-22)。 **codex が誤りを標準エラーへ書く形は、行頭の印の後に誤りの本文を続けた 1 行である。** 存在しない モデルを指定して `codex exec` を実行し、標準エラーの記録に次の行が出ることを確かめた。記録の -誤りの本文と同じ文字列である。利用上限の文言でこの形を見たわけではない(「未確認のまま残ること」)。 +誤りの本文と同じ文字列である。利用上限の文言でも同じ形になることを、上の表のラウンド 2 の記録で確かめた。この記録を今の照合の表で読むと一致せず、監視の結末は理由「結果ファイル無し」になった。足す 2 行を加えると、行頭の印の付いた行に一致する。 ```text ERROR: {"type":"error","status":400,"error":{"type":"invalid_request_error","message":"The 'ndf-no-such-model-xyz' model is not supported when using Codex with a ChatGPT account."}} @@ -78,13 +79,13 @@ re.compile(r"^(?:ERROR:\s*)?exceeded retry limit, last status: 429\b", re.MULTIL | 区分 | 入力 | 今の利用上限 | 足した後 | | --- | --- | --- | --- | | 実物 | codex の利用上限(行頭の印あり / なし) | — | 一致 | -| 実物 | codex の再試行の上限 `… last status: 429 Too Many Requests` | — | 一致 | +| 実物(書き出し) | codex の再試行の上限 `ERROR: exceeded retry limit, last status: 429`。書き出しは実行ファイルの文字列、`429` は差し込まれる状態 | — | 一致 | | 実物 | codex の `Quota exceeded. Check your plan and billing details.` | 一致 | 一致 | | 実物 | claude の `You've hit your weekly limit · resets Sep 22, 6am (UTC)` | — | 一致 | | 実物 | claude の `You've hit your session limit · resets 6:30pm (UTC)` | — | 一致 | | 実物 | claude の `You've hit your individual spend limit · run /usage-credits …` | — | 一致 | | 実物 | claude の `You've hit your monthly spend limit. Run /usage-credits …` | — | 一致 | -| 実物 | claude の `You've hit your limit · resets 3am (UTC)` | — | 一致 | +| 実物 | claude の `You've hit your limit`(実行ファイルの文字列) | — | 一致 | | 実物 | kiro の `Monthly request limit reached` / HTTP 429 の状態行 | 一致 | 一致 | | 一致しない | codex の再試行の上限 `… last status: 503 Service Unavailable` | — | — | | 一致しない | codex の 400 の行(上のコードブロック) | — | — | @@ -92,6 +93,8 @@ re.compile(r"^(?:ERROR:\s*)?exceeded retry limit, last status: 429\b", re.MULTIL | 誤検知 | 文の途中、リスト、引用、grep 形式、Python の文字列 | — | — | | 誤検知 | 差分の追加行 `+ERROR: You've hit …`、差分の文脈行 | — | — | +**実物に無い末尾は入力に足さない。** 再試行の上限の状態の後ろ(`Too Many Requests` など)と、claude の期間を書かない形の後ろ(`· resets 3am (UTC)`)は、#811 の表にある形で、記録にも実行ファイルにも無い。照合はどちらも末尾を読まないため、入力は実物の範囲で止める。 + ### 起動できないときの例外 | 条件 | 上がる例外 | `errno` | @@ -244,9 +247,8 @@ graph TD | 項目 | 内容 | | --- | --- | -| codex の利用上限の文言の行頭の形 | 400 の誤りで測った形(`ERROR: ` + 本文)からの類推。照合は行頭の印の有無を問わないため、印が別の形(時刻を前に置くなど)だと一致しない。10.16.1 の後に上限に当たった記録で確かめる | | claude の文言が標準エラーの記録へ出るか | 実測の claude の 5 形の出所は、会話の記録と実行ファイルである。JSON 出力で起動した claude が上限のときに標準エラーへ書いた記録は無い。起動した claude の上限は、標準出力の状態コード(10.16.0 で合格)で読む。10.16.1 の後に上限に当たった標準エラーの記録で確かめる | -| codex の再試行の上限の状態の書き方 | 実行ファイルの文字列は `last status: ` までで、状態の後ろの書き方は記録に無い。照合は `429` の直後の語の区切りまでしか見ない | +| codex の再試行の上限の状態の書き方 | 実行ファイルの文字列は `last status: ` までで、状態の後ろの書き方は記録に無い。テストの入力は `429` までにし、照合も `429` の直後の語の区切りまでしか見ない | | kiro と agy の利用上限の文言 | 手元の記録に 0 件。kiro は既存の #619 の実物、agy は照合なし | | claude の旧い形 | 記録にも実行ファイルにも 0 件のため足さない(決定 4) | | 行頭に固定しても残る誤検知 | 文言で始まる行をそのまま読み上げた場合(文書の本文の行頭に裸で置いた文言を `cat` したときなど)は一致する。この束の文書とテストでは、文言を表・バッククォート・文字列の中にだけ置く | diff --git a/issues/issue-811-813-requirements.md b/issues/issue-811-813-requirements.md index 774c5ac0d..3c036e0c6 100644 --- a/issues/issue-811-813-requirements.md +++ b/issues/issue-811-813-requirements.md @@ -93,7 +93,7 @@ ndf 10.16.0 のリリース後テスト(PR #810 のコメント、2026-09-22 各条件の「利用上限として止まる」は、次の 3 つがそろうことを指す: 監視が担当を止めて早期の致命(終了コード 4)で終わる / 結末の理由が「利用上限」 / 起動し直しの可否が偽。 - [ ] AC1: 標準エラーの記録に codex の利用上限の文言の 1 行が出ると、利用上限として止まる。文言は `You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage …` である。行頭の `ERROR: ` の有無を問わない -- [ ] AC2: 標準エラーの記録に codex の再試行の上限の行が出て、最後の状態が 429 なら、利用上限として止まる。行は `exceeded retry limit, last status: 429 Too Many Requests` である。最後の状態が 429 以外(例 `503 Service Unavailable`)なら利用上限にならない +- [ ] AC2: 標準エラーの記録に codex の再試行の上限の行が出て、最後の状態が 429 なら、利用上限として止まる。行は `exceeded retry limit, last status: 429` で始まる。最後の状態が 429 以外(例 `503 Service Unavailable`)なら利用上限にならない - [ ] AC3: 標準エラーの記録に claude の利用上限の文言が行頭から出ると、利用上限として止まる。確かめる形は設計の「実測」の claude の 5 形(週・セッション・個人の支出・月の支出・期間を書かない形)である。JSON 出力で起動した claude がこの文言を標準エラーへ書くかは確かめていない。起動した claude の上限は AC4 の JSON の状態コードで読む - [ ] AC4: 10.16.0 で合格した形は、引き続き利用上限として止まる。形は 4 つで、kiro の `Monthly request limit reached`、claude の JSON の上限の状態コード、HTTP 429 の状態行、`Quota exceeded` である - [ ] AC5: AC1〜AC3 の文言が、表・バッククォート・「」・リスト・引用・grep 形式・Python の文字列・差分の追加行と文脈行・文の途中に出たときは、利用上限として止まらない From 27b0203d1ea9931a914ff9e53846f999d4423c47 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 17:26:10 +0000 Subject: [PATCH 04/22] =?UTF-8?q?Docs:=20#811=20#813=20=E3=81=AE=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=E8=A8=88=E7=94=BB=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 設計文書の決定 1〜7 とテスト設計から、3 つのタスクへ分解した。 Co-Authored-By: Claude Opus 5 (1M context) --- issues/issue-811-813-implementation-plan.md | 132 ++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 issues/issue-811-813-implementation-plan.md diff --git a/issues/issue-811-813-implementation-plan.md b/issues/issue-811-813-implementation-plan.md new file mode 100644 index 000000000..5a901ade0 --- /dev/null +++ b/issues/issue-811-813-implementation-plan.md @@ -0,0 +1,132 @@ +# cross-review / cross-refactoring: codex と claude が利用上限で止まっても同じラウンドで起動し直され、PATH に読めないディレクトリがあると始まらない → 上限を理由として報告して起動し直さず、CLI が欠けても使える者だけで始まる(#811 #813) + +## 関連リンク + +- 要求と受け入れ条件: [issue-811-813-requirements.md](issue-811-813-requirements.md)(AC1〜AC14) +- 設計: [issue-811-813-design.md](issue-811-813-design.md)(決定 1〜7、実測の表) +- 課題: #811 / #813。直った後に閉じる課題: #478 #619 #729(10.16.1 のリリース後テストで AC12〜AC14 が合格したら) + +## モード + +`standard`。本番の振る舞いのバグ修正であり、対象にテストがある。 + +## 用語の対応表 + +本文は左の業務用語で書く。識別子は表とコードブロックにだけ置く(設計文書の表と同じ)。 + +| 業務用語 | 識別子 | +| --- | --- | +| 監視 | `plugins/ndf/scripts/lib/monitor.py` | +| 利用上限の照合の表 | `USAGE_LIMIT_FATAL` | +| 行単位の照合 | `_scan_patterns` | +| 標準エラーの記録 | 担当ごとの `-err.log` | +| 認証の確認 | `plugins/ndf/scripts/lib/auth.py` の `probe_auth` | +| 確認コマンドを 1 つ走らせる関数 | `auth._run_probe` | +| 起動できない例外 | `OSError` とその下位のすべて | +| 使える者の解決 | `plugins/ndf/scripts/lib/assignment.py` の `resolve_participants` | +| 開始の手順 | cross-review の `state.py init`、cross-refactoring の `refactor.py init` | + +## 目的と非目的 + +達成したい状態: + +- 利用上限で止まった担当が、実物の文言から理由「利用上限」として報告され、同じラウンドで起動し直されない +- 確認コマンドを起動できない CLI が、例外ではなく「通らない」として外れ、使える者だけで収束ループが始まる + +やらないこと: + +- 実物の出所が無い文言を照合へ足すこと(決定 4) +- 起動した claude の標準出力の照合を変えること(決定 5) +- 理由の語彙・監視の終了コード・標準出力のキーを変えること +- 必須の外部コマンドの呼び出しの例外処理を変えること + +## 前提 + +- 前提 1: 足す 2 行の照合は、設計の「実測」の実物 11 入力に一致し、誤検知の 12 入力に一致しない。テストの入力はその表から写す +- 前提 2: 読めないディレクトリを使うテストは、権限が効かない実行者(root)では条件が成り立たない。その場合は例外を差し込む形のテストで同じ理由の文言を確かめる + +## 受け入れ条件 + +要求の AC1〜AC11 をこの変更で満たす。AC12〜AC14 は 10.16.1 のリリース後テストで確かめるため、この Pull Request の範囲外である。 + +- [ ] AC1〜AC4: 実物の文言が利用上限として止まり、10.16.0 で合格した 4 形も止まり続ける(監視のテスト) +- [ ] AC5 / AC6: 誤検知の 12 形で止まらず、入力は実測の表の写しである(監視のテスト) +- [ ] AC7〜AC9: 起動できない CLI が「通らない」として返り、見つからない・時間切れの理由は変わらない(認証の確認のテスト) +- [ ] AC10 / AC11: 読めないディレクトリを含む PATH で CLI が 1 つ欠けても、2 つの開始の手順が終了コード 0 で終わる(開始の手順のテスト) +- [ ] 退行しないこと: 既存のテストがすべて通る(`uv run --with pytest pytest scripts/tests plugins/ndf -q`) + +## 代替案と採否 + +| 案 | 内容 | 採否 | 理由 | +| --- | --- | --- | --- | +| 行頭に固定した 2 行の照合 | 書き出しと種類の差し込みを 1 行で読む | 採用 | 設計の決定 1〜3。文書の引用・差分の行に一致しない | +| 形ごとに 1 行ずつ足す | 週・セッション・支出などを個別に書く | 不採用 | CLI が種類を増やすたびに照合が遅れる | +| 権限の例外だけを捕まえる | `PermissionError` に限って「通らない」とする | 不採用 | 実行形式でないファイルで同じ落ち方をする(決定 6) | + +## 不変条件 + +- 監視の結末の理由の語彙は `usage_limit` / `early_error` / `cli_timeout` / `missing` / `ok` のままである +- 認証の確認は例外を上げない。どの CLI の確認が失敗しても、残りの確認が続く + +## 互換性 + +| 対象 | 変更 | 互換性の扱い | +| --- | --- | --- | +| 公開インタフェース(監視の終了コード・標準出力のキー・開始の手順の引数) | 無し | 変えない | +| データ(状態ファイル・監視の結果ファイル) | 無し | 変えない | + +## 修正対象 + +- `plugins/ndf/scripts/lib/monitor.py` +- `plugins/ndf/scripts/lib/auth.py` +- `plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py` +- `plugins/ndf/scripts/tests/test_auth_probe.py` +- `plugins/ndf/skills/cross-review/tests/test_state_review_pool.py` +- `plugins/ndf/skills/cross-refactoring/tests/test_init.py` +- 配布物の同期(`bash scripts/build-runtime-plugins.sh` が揃える生成物) + +## タスク分解 + +### Task 1: 実物の文言を利用上限として読む + +- **対象ファイル:** `plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py`、`plugins/ndf/scripts/lib/monitor.py` +- **変更内容:** 設計の「実測」の実物 11 入力と誤検知 12 入力をテストの定数にし、利用上限の照合の表へ 2 行を足す。足す行は行頭で始まる形だけを読み、再試行の上限は最後の状態が 429 のときだけ一致する +- **満たす受け入れ条件:** AC1〜AC6 +- **進め方:** 失敗するテスト → 通す最小実装 → 整理 + +### Task 2: 起動できない確認コマンドを「通らない」として返す + +- **対象ファイル:** `plugins/ndf/scripts/tests/test_auth_probe.py`、`plugins/ndf/scripts/lib/auth.py` +- **変更内容:** 確認コマンドを 1 つ走らせる関数が起動できない例外を捕まえ、理由に起動できなかった理由を入れて返す。見つからない例外と時間切れの理由は変えない。テストは実際に権限を外した一時ディレクトリを PATH に置く形と、例外を差し込む形の両方を置く +- **満たす受け入れ条件:** AC7〜AC9 +- **進め方:** 失敗するテスト → 通す最小実装 → 整理 + +### Task 3: 読めない PATH でも 2 つの開始の手順が始まる + +- **対象ファイル:** `plugins/ndf/skills/cross-review/tests/test_state_review_pool.py`、`plugins/ndf/skills/cross-refactoring/tests/test_init.py` +- **変更内容:** 認証の確認を差し替えずに、読めないディレクトリを含む PATH と、CLI を 1 つ欠いた状態で開始の手順を通すテストを足す +- **満たす受け入れ条件:** AC10 / AC11 +- **進め方:** 失敗するテスト → 通す(Task 2 の実装で通る)→ 整理 + +## 影響範囲 + +- 収束ループ 2 つ(cross-review / cross-refactoring)の開始の手順と、担当の監視 +- 配布物(4 ランタイム分の生成物)は同期のコマンドで揃える + +## リスクと対処 + +| リスク | 対処 | +| --- | --- | +| 照合の追加で、文書やテストの本文を読み上げた行に一致する | 行頭に固定し、誤検知 12 形をテストの入力にする。触る範囲が狭くテストが厚いため、実装の後の構造改善で足りる | +| 権限を外したディレクトリが root では効かない | 例外を差し込むテストを併置し、どちらの実行者でも理由の文言を確かめる | +| 照合の表を写した配布物が古いまま残る | 同期のコマンドを実行し、差分をコミットに含める | + +## 切り戻し手順 + +- この Pull Request を revert すれば元へ戻る。データ移行も設定の変更も伴わない + +## 完了の定義 + +- [ ] AC1〜AC11 を満たし、条件ごとにテストが対応している +- [ ] 全体テストが通る(`uv run --with pytest pytest scripts/tests plugins/ndf -q`) +- [ ] 配布物の同期と Skill の frontmatter の検査が通る From 58cc63c2ddc33ab8808b16b9dac7b376d5459b26 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 17:26:10 +0000 Subject: [PATCH 05/22] =?UTF-8?q?Fix:=20=E5=88=A9=E7=94=A8=E4=B8=8A?= =?UTF-8?q?=E9=99=90=E3=81=AE=E7=85=A7=E5=90=88=E3=81=B8=20codex=20?= =?UTF-8?q?=E3=81=A8=20claude=20=E3=81=AE=E5=AE=9F=E7=89=A9=E3=81=AE=202?= =?UTF-8?q?=20=E8=A1=8C=E3=82=92=E8=B6=B3=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 行頭で始まる形だけを照合し、再試行の上限は最後の状態が 429 のときだけ利用上限と 読む。テストの入力は設計文書の実測の表から写した。 Co-Authored-By: Claude Opus 5 (1M context) --- plugins/ndf/scripts/lib/monitor.py | 9 ++ .../tests/test_monitor_usage_limit.py | 107 ++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/plugins/ndf/scripts/lib/monitor.py b/plugins/ndf/scripts/lib/monitor.py index cac43b06f..f21a05422 100755 --- a/plugins/ndf/scripts/lib/monitor.py +++ b/plugins/ndf/scripts/lib/monitor.py @@ -161,6 +161,15 @@ def _seat_or_both(value: str) -> str: re.compile(r"\b(?:quota exceeded|rate limit exceeded)\b", re.IGNORECASE), # HTTP 429 の状態行 re.compile(r"^HTTP/\d\S* 429 ", re.MULTILINE), + # codex と claude の実物(#811)。**行頭で始まる形だけを読む。** 担当は作業中の + # コマンドの出力(差分・ファイルの中身)も err.log へ書くため、文書やテストを + # 読み上げた行に一致させない。codex は誤りの行に `ERROR: ` を付ける。 + # claude は `You've hit your ` の後に期間や種類(週・セッション・支出)を差し込む。 + re.compile(r"^(?:ERROR:\s*)?You['’]ve hit your (?:[\w'’ ]+ )?(?:limit|budget)\b", + re.MULTILINE), + # codex の再試行の上限。**最後の状態が 429 のときだけ利用上限と読む**(503 などの + # 一時的な誤りは起動し直せば解けうる)。 + re.compile(r"^(?:ERROR:\s*)?exceeded retry limit, last status: 429\b", re.MULTILINE), ] # err.log の行頭に近い形で出る **明確な致命** パターン (kill 対象。理由は `early_error`)。 diff --git a/plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py b/plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py index 4a04ff71e..942693887 100644 --- a/plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py +++ b/plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py @@ -62,6 +62,57 @@ ] +# 設計文書(issues/issue-811-813-design.md)の「実測」から写した実物の行(#811)。 +# 推測で作った文言は入れない。出所は記録(`~/.codex/sessions` / `~/.claude/projects`)と +# 導入済みの実行ファイルの文字列である。 +CODEX_USAGE_LIMIT = ("You've hit your usage limit. Visit " + "https://chatgpt.com/codex/settings/usage to purchase more credits " + "or try again at 5:44 PM.") +CODEX_USAGE_LIMIT_PREFIXED = f"ERROR: {CODEX_USAGE_LIMIT}" +CODEX_RETRY_429 = "ERROR: exceeded retry limit, last status: 429" +CODEX_RETRY_503 = "ERROR: exceeded retry limit, last status: 503 Service Unavailable" +CODEX_BAD_REQUEST = ('ERROR: {"type":"error","status":400,"error":{"type":' + '"invalid_request_error","message":"The \'ndf-no-such-model-xyz\' model ' + 'is not supported when using Codex with a ChatGPT account."}}') +CLAUDE_WEEKLY = "You've hit your weekly limit · resets Sep 22, 6am (UTC)" +CLAUDE_SESSION = "You've hit your session limit · resets 6:30pm (UTC)" +CLAUDE_INDIVIDUAL_SPEND = ("You've hit your individual spend limit · run /usage-credits " + "to raise it, or visit claude.ai/admin-settings/usage") +CLAUDE_MONTHLY_SPEND = ("You've hit your monthly spend limit. Run /usage-credits to manage " + "your limit and keep using the model or switch models to continue " + "this chat.") +CLAUDE_BARE_LIMIT = "You've hit your limit" + +# 実物として一致すべき 7 行(codex 2 形 + claude 5 形)。行頭の印の有無は codex が決める。 +MEASURED_USAGE_LIMIT_LINES = [ + ("codex 利用上限", CODEX_USAGE_LIMIT), + ("codex 利用上限 行頭の印あり", CODEX_USAGE_LIMIT_PREFIXED), + ("codex 再試行の上限 429", CODEX_RETRY_429), + ("claude 週", CLAUDE_WEEKLY), + ("claude セッション", CLAUDE_SESSION), + ("claude 個人の支出", CLAUDE_INDIVIDUAL_SPEND), + ("claude 月の支出", CLAUDE_MONTHLY_SPEND), + ("claude 期間なし", CLAUDE_BARE_LIMIT), +] + +# 一致してはいけない行。状態が 429 でない再試行の上限・400 の行・引用と差分の形。 +NOT_USAGE_LIMIT_LINES = [ + ("codex 再試行の上限 503", CODEX_RETRY_503), + ("codex 400 の行", CODEX_BAD_REQUEST), + ("表(バッククォートあり)", f"| usage_limit | `{CODEX_USAGE_LIMIT}` | 利用上限 |"), + ("表(バッククォートなし)", f"| usage_limit | {CLAUDE_WEEKLY} | 利用上限 |"), + ("本文のバッククォート", f"上限の文言は `{CLAUDE_SESSION}` である"), + ("本文の「」", f"上限の文言は「{CLAUDE_WEEKLY}」である"), + ("文の途中", f"担当は {CODEX_USAGE_LIMIT} を出して止まった"), + ("リスト", f"- {CLAUDE_MONTHLY_SPEND}"), + ("引用", f"> {CLAUDE_WEEKLY}"), + ("grep 形式", f"plugins/ndf/scripts/lib/monitor.py:160: # {CLAUDE_BARE_LIMIT}"), + ("Python の文字列", f' CLAUDE_WEEKLY = "{CLAUDE_WEEKLY}"'), + ("差分の追加行", f"+{CODEX_USAGE_LIMIT_PREFIXED}"), + ("差分の文脈行", f" {CLAUDE_SESSION}"), +] + + def _write(path: pathlib.Path, text: str) -> pathlib.Path: path.write_text(text + "\n", encoding="utf-8") return path @@ -81,6 +132,22 @@ def test_ten_measured_lines_match_as_the_design_records(tmp_path, monitor_mod, l assert (monitor_mod._scan_early_fatal(log) is not None) is (usage_hit or fatal_hit) +@pytest.mark.parametrize(("label", "line"), MEASURED_USAGE_LIMIT_LINES, + ids=[t[0] for t in MEASURED_USAGE_LIMIT_LINES]) +def test_measured_codex_and_claude_lines_are_usage_limits(tmp_path, monitor_mod, label, line): + """AC1〜AC3 / AC6: 実測の実物の行は利用上限の表に一致する。""" + log = _write(tmp_path / "err.log", line) + assert monitor_mod._scan_patterns(log, monitor_mod.USAGE_LIMIT_FATAL) is not None + + +@pytest.mark.parametrize(("label", "line"), NOT_USAGE_LIMIT_LINES, + ids=[t[0] for t in NOT_USAGE_LIMIT_LINES]) +def test_quoted_and_non_limit_lines_are_not_usage_limits(tmp_path, monitor_mod, label, line): + """AC2 の後半 / AC5: 状態が 429 でない行と、引用・差分・文の途中は一致しない。""" + log = _write(tmp_path / "err.log", line) + assert monitor_mod._scan_patterns(log, monitor_mod.USAGE_LIMIT_FATAL) is None + + def test_claude_stdout_json_is_matched_without_the_quote_exclusion(tmp_path, monitor_mod): """stdout.log の JSON は 1 行に引用符が多く、行単位の除外を掛けると取りこぼす。""" out = _write(tmp_path / "stdout.log", CLAUDE_429) @@ -217,6 +284,46 @@ def test_usage_limit_stops_the_agent_as_early_error_with_reason_usage_limit(tmp_ assert len(out) == 1 and set(out[0]) == STDOUT_KEYS and out[0]["exit_code"] == 4 +@pytest.mark.parametrize(("agent", "err"), [ + ("codex", CODEX_USAGE_LIMIT_PREFIXED), # AC1 + ("codex", CODEX_USAGE_LIMIT), # AC1 + ("codex", CODEX_RETRY_429), # AC2 + ("claude", CLAUDE_WEEKLY), # AC3 + ("claude", CLAUDE_SESSION), # AC3 +]) +def test_measured_lines_stop_the_agent_with_reason_usage_limit(tmp_path, agent, err): + """AC1〜AC3: 実測の行で担当が止まり、理由は利用上限、終了コードは 4。""" + stem = _finished(tmp_path, agent, err=err) + + proc = _run_monitor(tmp_path, agent) + + assert proc.returncode == 4, proc.stderr + outcome = _outcome(tmp_path, stem) + assert (outcome["status"], outcome["reason"]) == ("EARLY_ERROR", "usage_limit") + + +def test_the_retry_limit_with_another_status_is_not_a_usage_limit(tmp_path): + """AC2 の後半: 最後の状態が 429 以外なら利用上限にしない。""" + stem = _finished(tmp_path, "codex", err=CODEX_RETRY_503) + + proc = _run_monitor(tmp_path, "codex") + + assert proc.returncode == 3, proc.stderr + assert _outcome(tmp_path, stem)["reason"] == "missing" + + +def test_quoted_measured_lines_do_not_stop_the_agent(tmp_path): + """AC5: 引用・差分・文の途中に出た実物の文言では止まらない。""" + stem = _finished(tmp_path, "codex", + err="\n".join(line for _label, line in NOT_USAGE_LIMIT_LINES), + result=True) + + proc = _run_monitor(tmp_path, "codex") + + assert proc.returncode == 0, proc.stderr + assert _outcome(tmp_path, stem)["reason"] == "ok" + + @pytest.mark.parametrize("err", [HTTP_401, "HTTP/1.1 403 Forbidden", "Permission denied"]) def test_other_fatal_lines_keep_reason_early_error(tmp_path, err): stem = _finished(tmp_path, "kiro", err=err) From 8932777e0b2bb728f296ad1b0df9cdb4e53d4d3c Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 17:26:10 +0000 Subject: [PATCH 06/22] =?UTF-8?q?Fix:=20=E7=A2=BA=E8=AA=8D=E3=82=B3?= =?UTF-8?q?=E3=83=9E=E3=83=B3=E3=83=89=E3=82=92=E8=B5=B7=E5=8B=95=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=81=AA=E3=81=84=E3=81=A8=E3=81=8D=E3=81=AF=E9=80=9A?= =?UTF-8?q?=E3=82=89=E3=81=AA=E3=81=84=E3=81=A8=E3=81=97=E3=81=A6=E8=BF=94?= =?UTF-8?q?=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PATH に読めないディレクトリがあると、コマンドがどこにも無いときに権限の例外が 上がる。起動できない例外をすべて捕まえ、理由に起動できなかった理由を入れる。 Co-Authored-By: Claude Opus 5 (1M context) --- plugins/ndf/scripts/lib/auth.py | 6 ++ plugins/ndf/scripts/tests/test_auth_probe.py | 55 ++++++++++++++ .../cross-refactoring/tests/test_init.py | 65 ++++++++++++++++- .../tests/test_state_review_pool.py | 73 ++++++++++++++++++- 4 files changed, 194 insertions(+), 5 deletions(-) diff --git a/plugins/ndf/scripts/lib/auth.py b/plugins/ndf/scripts/lib/auth.py index c981f2824..e7fe4b4f2 100644 --- a/plugins/ndf/scripts/lib/auth.py +++ b/plugins/ndf/scripts/lib/auth.py @@ -49,6 +49,10 @@ def _run_probe(probe: tuple[str, ...]) -> tuple[bool, str]: 理由は stderr か stdout の先頭 200 文字。終了コード 0 でも未認証の文言を含めば 通らなかったものとする(kiro は成否を終了コードで表さない)。 + + **起動できない理由が何であっても「通らない」として返す**(#813)。PATH に読めない + ディレクトリがあると、コマンドがどこにも無いときに権限の例外が上がる。実行形式で + ないファイルも同じ形で落ちる。見つからない例外は下位にあるため先に捕まえる。 """ try: r = subprocess.run(list(probe), capture_output=True, text=True, @@ -57,6 +61,8 @@ def _run_probe(probe: tuple[str, ...]) -> tuple[bool, str]: return False, "コマンドが見つかりません" except subprocess.TimeoutExpired: return False, f"{AUTH_PROBE_TIMEOUT} 秒で応答しませんでした" + except OSError as exc: + return False, f"コマンドを実行できません({exc.strerror or exc})" merged = f"{r.stdout}\n{r.stderr}".lower() ok = r.returncode == 0 and not any(m in merged for m in UNAUTHENTICATED_MARKERS) return ok, (r.stderr.strip() or r.stdout.strip())[:200] diff --git a/plugins/ndf/scripts/tests/test_auth_probe.py b/plugins/ndf/scripts/tests/test_auth_probe.py index f8bca89a5..f6de12c26 100644 --- a/plugins/ndf/scripts/tests/test_auth_probe.py +++ b/plugins/ndf/scripts/tests/test_auth_probe.py @@ -53,6 +53,61 @@ def missing(*args, **kwargs): assert messages == ["❌ codex: codex login status"] +def test_probe_reports_a_command_it_cannot_start_with_a_real_unreadable_path( + auth, monkeypatch, tmp_path): + """AC7: 読めないディレクトリだけの PATH で、確認コマンドが見つからないとき。 + + 実際に権限を外したディレクトリを PATH に置いて再現する。権限が効かない実行者 + (root)では条件が成り立たないため、その場合は飛ばす。同じ理由の文言は、例外を + 差し込む次のテストがどちらの実行者でも確かめる。 + """ + unreadable = tmp_path / "unreadable" + unreadable.mkdir() + unreadable.chmod(0o000) + try: + try: + list(unreadable.iterdir()) + pytest.skip("権限が効かない実行者のため、読めない PATH を再現できない") + except PermissionError: + pass + monkeypatch.setenv("PATH", str(unreadable)) + + results, _ = auth.probe_auth(["codex"], info=lambda _m: None, env={}) + finally: + unreadable.chmod(0o700) + + assert results["codex"]["ok"] is False + assert results["codex"]["detail"] == "コマンドを実行できません(Permission denied)" + + +def test_probe_reports_a_command_it_cannot_start(auth, monkeypatch): + """AC7: 起動が権限の例外で終わるときも、例外を上げずに理由を返す。""" + def denied(*args, **kwargs): + raise PermissionError(13, "Permission denied") + + monkeypatch.setattr(auth.subprocess, "run", denied) + + results, _ = auth.probe_auth(["codex"], info=lambda _m: None, env={}) + + assert results["codex"]["ok"] is False + assert results["codex"]["detail"] == "コマンドを実行できません(Permission denied)" + + +def test_probe_reports_a_command_that_is_not_an_executable_format(auth, monkeypatch, tmp_path): + """AC8: 実行形式でないファイルを確認コマンドにしたときも「通らない」を返す。""" + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + fake = bin_dir / "codex" + fake.write_text("\x7fnot an executable\n", encoding="utf-8") + fake.chmod(0o755) + monkeypatch.setenv("PATH", str(bin_dir)) + + results, _ = auth.probe_auth(["codex"], info=lambda _m: None, env={}) + + assert results["codex"]["ok"] is False + assert results["codex"]["detail"] == "コマンドを実行できません(Exec format error)" + + def test_probe_reports_a_timeout(auth, monkeypatch): def time_out(*args, **kwargs): raise subprocess.TimeoutExpired(args[0], kwargs["timeout"]) diff --git a/plugins/ndf/skills/cross-refactoring/tests/test_init.py b/plugins/ndf/skills/cross-refactoring/tests/test_init.py index e5d8cdaa7..aac848e07 100644 --- a/plugins/ndf/skills/cross-refactoring/tests/test_init.py +++ b/plugins/ndf/skills/cross-refactoring/tests/test_init.py @@ -10,6 +10,7 @@ import os import pathlib import subprocess +import types import pytest @@ -79,8 +80,11 @@ def run_init(refactor_lib, paths, patch_lib, refactor, origin_repo, monkeypatch) refactor_lib = sys.modules["refactor_lib"] probed: list[list[str]] = [] - def _run(args, viewer="someone-else", probe=None): - """`probe` を渡すと確認を差し替える。`{ランタイム: 理由}` の者だけが通らない。""" + def _run(args, viewer="someone-else", probe=None, real_probe=False): + """`probe` を渡すと確認を差し替える。`{ランタイム: 理由}` の者だけが通らない。 + + `real_probe` を立てると差し替えず、止めない確認をそのまま走らせる(#813)。 + """ real_sh = paths.sh def fake_sh(cmd, cwd=None, check=True): @@ -112,7 +116,9 @@ def fake_sh(cmd, cwd=None, check=True): monkeypatch.delenv("CROSS_REFACTORING_TMP_DIR", raising=False) # 認証確認は実際の CLI を起動する。既定では飛ばし、`probe` を渡したときだけ # 止めない確認(`probe_auth`)を差し替えて結果を決める。 - if probe is None: + if real_probe: + monkeypatch.delenv("NDF_SKIP_AUTH_CHECK", raising=False) + elif probe is None: monkeypatch.setenv("NDF_SKIP_AUTH_CHECK", "1") else: monkeypatch.delenv("NDF_SKIP_AUTH_CHECK", raising=False) @@ -205,6 +211,59 @@ def test_a_failed_probe_drops_the_runtime_and_keeps_going(run_init, tmp_path, ca assert "kiro を担当から外しました(Not logged in)" in capsys.readouterr().err +def test_init_starts_when_an_unreadable_path_hides_a_missing_cli( + run_init, tmp_path, monkeypatch, capsys): + """AC11 — 読めないディレクトリを含む PATH で CLI が 1 つ欠けても始まる(#813)。 + + 確認コマンドは、終わりが分かる短い実行ファイルへ差し替える。欠ける 1 者だけは + どこにも無い名前にし、読めないディレクトリを PATH の末尾へ足す。 + """ + cmd_setup = sys.modules["refactor_lib.commands.setup"] + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + stub = bin_dir / "ndf-stub-ok" + stub.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + stub.chmod(0o755) + unreadable = tmp_path / "unreadable" + unreadable.mkdir() + unreadable.chmod(0o000) + monkeypatch.setattr(cmd_setup.auth, "AUTH_PROBES", { + "claude": ("ndf-stub-ok",), "codex": ("ndf-stub-ok",), + "agy": ("ndf-stub-ok",), "kiro": ("ndf-stub-missing",), + }) + monkeypatch.setenv("PATH", f"{os.environ['PATH']}:{bin_dir}:{unreadable}") + try: + run_init(_args(tmp_path), real_probe=True) + finally: + unreadable.chmod(0o700) + + _, state = _state_of(tmp_path) + assert state["runtimes"] == ["claude", "codex"] + assert list(state["participants"]["unavailable"]) == ["kiro"] + assert "kiro を担当から外しました" in capsys.readouterr().err + + +def test_init_starts_when_a_probe_cannot_be_launched(run_init, tmp_path, monkeypatch): + """AC11 — 起動できない例外は、権限が効かない実行者でも「外して続ける」になる。""" + cmd_setup = sys.modules["refactor_lib.commands.setup"] + + def run(cmd, **kwargs): + if cmd[0] == "kiro-cli": + raise PermissionError(13, "Permission denied") + return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="") + + # 差し替える先は認証の確認が見る名前だけにする。標準ライブラリの属性を差し替えると、 + # 同じモジュールを使う git の呼び出しまで偽物になる。 + monkeypatch.setattr(cmd_setup.auth, "subprocess", types.SimpleNamespace( + run=run, TimeoutExpired=subprocess.TimeoutExpired)) + run_init(_args(tmp_path), real_probe=True) + + _, state = _state_of(tmp_path) + assert state["runtimes"] == ["claude", "codex"] + assert state["participants"]["unavailable"] == { + "kiro": "コマンドを実行できません(Permission denied)"} + + def test_require_all_stops_without_writing_the_state(run_init, tmp_path): """AC35 — 全員を要する指定では従来の関門で止め、状態ファイルを作らない。""" with pytest.raises(SystemExit) as e: diff --git a/plugins/ndf/skills/cross-review/tests/test_state_review_pool.py b/plugins/ndf/skills/cross-review/tests/test_state_review_pool.py index 4fe3d06bc..43242c8e3 100644 --- a/plugins/ndf/skills/cross-review/tests/test_state_review_pool.py +++ b/plugins/ndf/skills/cross-review/tests/test_state_review_pool.py @@ -9,9 +9,15 @@ from __future__ import annotations import json +import subprocess +import types import pytest +# 子プロセスの起動の本物。テストのフィクスチャが差し替える前に控える(#813 の AC10 で +# 認証の確認だけを本物のまま走らせるため)。 +_REAL_RUN = subprocess.run + def _state(tmp_path, **over): """最小の状態ファイルを組み立ててパスを返す。""" @@ -370,8 +376,9 @@ def new_init(state_mod, monkeypatch, tmp_path): monkeypatch.setattr(state_mod, "_sync_before_round", lambda st, pr: None) calls: list[list[str]] = [] - def run(*argv: str, failing=None, only=None): - monkeypatch.setattr(state_mod.auth, "probe_auth", _fake_probe(failing or {}, calls)) + def run(*argv: str, failing=None, only=None, real_probe=False): + if not real_probe: + monkeypatch.setattr(state_mod.auth, "probe_auth", _fake_probe(failing or {}, calls)) state_mod.cmd_init(_init_args(tmp_path, *argv, only=only)) return json.loads((tmp_path / f"cross-review-pr{PR_INIT}-state.json").read_text()) @@ -506,6 +513,68 @@ def test_the_second_seat_falls_back_to_a_second_copy_when_the_host_is_unavailabl assert _start_round(state_mod, tmp_path) == ["codex", "codex-2"] +# ---------- 読めないディレクトリを含む PATH(#813: AC10) ---------- + +def _stub_cli(bin_dir, *names: str) -> None: + """確認コマンドが終了コード 0 で終わる短い実行ファイルを置く。""" + bin_dir.mkdir(exist_ok=True) + for name in names: + path = bin_dir / name + path.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + path.chmod(0o755) + + +def _real_subprocess(state_mod, monkeypatch): + """認証の確認だけを本物の起動に戻す。 + + `new_init` は子プロセスの起動を差し替える。差し替え先は標準ライブラリの同じ + モジュールのため、属性を戻すと開始の手順の側まで本物になる。認証の確認が見る + 名前だけを別の入れ物へ向けて、2 つを分ける。 + """ + monkeypatch.setattr(state_mod.auth, "subprocess", types.SimpleNamespace( + run=_REAL_RUN, TimeoutExpired=subprocess.TimeoutExpired)) + + +def test_init_starts_when_an_unreadable_path_hides_a_missing_cli( + new_init, state_mod, monkeypatch, tmp_path, capsys): + """AC10: 読めないディレクトリを含む PATH で CLI が 1 つ欠けても、開始の手順は終わる。 + + 権限が効かない実行者(root)では、欠けた CLI の理由が「コマンドが見つかりません」に + なる。外れて続くことは同じであり、理由の文言は認証の確認のテストが持つ。 + """ + _stub_cli(tmp_path / "bin", "codex", "agy") + unreadable = tmp_path / "unreadable" + unreadable.mkdir() + unreadable.chmod(0o000) + monkeypatch.setenv("PATH", f"{tmp_path / 'bin'}:{unreadable}") + _real_subprocess(state_mod, monkeypatch) + try: + st = new_init(real_probe=True) + finally: + unreadable.chmod(0o700) + + assert st["participants"]["available"] == ["codex", "agy"] + assert list(st["participants"]["unavailable"]) == ["kiro"] + assert _start_round(state_mod, tmp_path) == ["codex", "agy"] + + +def test_init_starts_when_a_probe_cannot_be_launched(new_init, state_mod, monkeypatch, tmp_path): + """AC10: 起動できない例外がどの実行者でも「外して続ける」になることを確かめる。""" + def run(cmd, **kwargs): + if cmd[0] == "kiro-cli": + raise PermissionError(13, "Permission denied") + return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="") + + monkeypatch.setattr(state_mod.auth, "subprocess", types.SimpleNamespace( + run=run, TimeoutExpired=subprocess.TimeoutExpired)) + + st = new_init(real_probe=True) + + assert st["participants"]["available"] == ["codex", "agy"] + assert st["participants"]["unavailable"] == { + "kiro": "コマンドを実行できません(Permission denied)"} + + @pytest.mark.parametrize("argv", [ ("--exclude", "claude"), ("--only", "codex", "--exclude", "codex"), From e8ac50f885bac637b94eb241a61c955eff960309 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 18:03:14 +0000 Subject: [PATCH 07/22] =?UTF-8?q?Test:=20=E3=83=A2=E3=83=87=E3=83=AB?= =?UTF-8?q?=E8=A6=B3=E6=B8=AC=E3=81=A8=E6=8C=87=E6=A8=99=E9=9B=86=E8=A8=88?= =?UTF-8?q?=E3=81=AE=E7=8F=BE=E7=8A=B6=E5=9B=BA=E5=AE=9A=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit observed_model の分岐と aggregate の代表的な集計結果を公開入口から固定する。 Item-Id: R1-001 Round: 1 Impl-Runtime: codex Impl-Model: default --- plugins/ndf/scripts/tests/test_metrics.py | 106 ++++++++++++++++++++++ plugins/ndf/scripts/tests/test_models.py | 30 +++++- 2 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 plugins/ndf/scripts/tests/test_metrics.py diff --git a/plugins/ndf/scripts/tests/test_metrics.py b/plugins/ndf/scripts/tests/test_metrics.py new file mode 100644 index 000000000..adf092075 --- /dev/null +++ b/plugins/ndf/scripts/tests/test_metrics.py @@ -0,0 +1,106 @@ +"""担当ごとの指標集計に対する現状固定テスト。""" +from __future__ import annotations + +import importlib.util +import pathlib +import sys + +LIB = pathlib.Path(__file__).resolve().parents[1] / "lib" +if str(LIB) not in sys.path: + sys.path.insert(0, str(LIB)) + +SPEC = importlib.util.spec_from_file_location("metrics", LIB / "metrics.py") +assert SPEC is not None and SPEC.loader is not None +metrics = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(metrics) + + +def test_aggregate_current_metrics_for_representative_state() -> None: + """現状固定。代表的な状態辞書から得られる全バケットと比率を記録する。""" + state = { + "items": [ + {"item_id": "R1-001", "status": "done", "budget_exceeded": True}, + {"item_id": "R1-002", "status": "abandoned", "test_failed": True}, + ], + "rounds": [ + { + "round": 1, + "impl": "codex", + "impl_model": {"requested": "gpt-5", "observed": None}, + "reviewers": ["claude", "agy"], + "reviewer_models": { + "claude": {"requested": "sonnet", "observed": "sonnet"}, + "agy": {"requested": "gpt-5", "observed": None}, + }, + "items": ["R1-001", "R1-002"], + "durations": {"apply": 100, "fix": 20}, + "fix_rounds": 2, + "reviewer_seconds": {"claude": 30, "agy": 40}, + "reviews": [ + { + "claude": "REQUEST_CHANGES", + "agy": "REQUEST_CHANGES", + "findings": [ + {"reviewer": "claude", "resolved": True}, + {"reviewer": "claude", "resolved": False}, + {"reviewer": "agy", "resolved": True}, + ], + }, + { + "claude": "APPROVE", + "agy": "REQUEST_CHANGES", + "findings": [], + }, + ], + } + ], + } + + assert metrics.aggregate(state) == { + "impl": { + "codex / gpt-5": { + "rounds": 1, + "applied": 1, + "abandoned": 1, + "fix_rounds": 2, + "budget_exceeded": 1, + "test_failed": 1, + "first_review_total": 1, + "first_review_approved": 0, + "seconds": 120.0, + "first_review_approval_rate": 0.0, + "avg_fix_rounds": 2.0, + "budget_exceeded_rate": 0.5, + "test_failure_rate": 0.5, + } + }, + "reviewer": { + "agy / gpt-5": { + "reviews": 2, + "findings": 1, + "findings_resolved": 1, + "verdict_pairs": 2, + "verdict_agreements": 1, + "seconds": 40.0, + "resolution_rate": 1.0, + "agreement_rate": 0.5, + }, + "claude / sonnet": { + "reviews": 2, + "findings": 2, + "findings_resolved": 1, + "verdict_pairs": 2, + "verdict_agreements": 1, + "seconds": 30.0, + "resolution_rate": 0.5, + "agreement_rate": 0.5, + }, + }, + "unmeasured": [], + "assumed": [ + "round 1: codex は指定した gpt-5 で動いた前提で数える" + "(実測不可)(実装担当)", + "round 1: agy は指定した gpt-5 で動いた前提で数える" + "(実測不可)(レビュー担当)", + ], + } diff --git a/plugins/ndf/scripts/tests/test_models.py b/plugins/ndf/scripts/tests/test_models.py index 20f9bd915..44a4e9d71 100644 --- a/plugins/ndf/scripts/tests/test_models.py +++ b/plugins/ndf/scripts/tests/test_models.py @@ -10,7 +10,7 @@ if str(LIB) not in sys.path: sys.path.insert(0, str(LIB)) -from models import ModelSpecError, parse_model_args +from models import ModelSpecError, observed_model, parse_model_args @pytest.mark.parametrize( @@ -30,3 +30,31 @@ def test_parse_model_args_rejects_invalid_specs( parse_model_args(values) assert message_part in str(exc_info.value) + + +@pytest.mark.parametrize( + ("runtime", "stdout_text"), + [ + ("codex", ""), + ("claude", '{"result": "ok"}'), + ("claude", '{"modelUsage": {broken json'), + ], +) +def test_observed_model_returns_none_when_model_cannot_be_observed( + runtime: str, stdout_text: str +) -> None: + """現状固定。公開出力からモデルを特定できない経路は None を返す。""" + assert observed_model(runtime, stdout_text) is None + + +def test_observed_model_selects_model_with_most_input_tokens() -> None: + """現状固定。複数モデルでは入力トークンが最大のモデル名を返す。""" + stdout_text = """{ + "modelUsage": { + "claude-sonnet": {"inputTokens": 120}, + "claude-opus": {"inputTokens": 450}, + "claude-haiku": {"inputTokens": 30} + } + }""" + + assert observed_model("claude", stdout_text) == "claude-opus" From 74966c7e4a6ea085b4bc57f32a274f86455d2ae6 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 18:13:13 +0000 Subject: [PATCH 08/22] =?UTF-8?q?Test:=20=E7=8F=BE=E7=8A=B6=E5=9B=BA?= =?UTF-8?q?=E5=AE=9A=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD=E5=8A=A0=20?= =?UTF-8?q?=E2=80=94=20models.py#separation=5Freason=20/=20metrics.py#form?= =?UTF-8?q?at=5Freport?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit models.py の separation_reason による3分岐(kiro auto / 未指定実測不可 / 分離しない)と、 metrics.py の format_report によるMarkdown出力構造(見出し・表の行・注意書き)を 現状固定テストとして追加。 Item-Id: R1-002 Round: 1 Impl-Runtime: agy Impl-Model: default --- plugins/ndf/scripts/tests/test_metrics.py | 52 +++++++++++++++++++++++ plugins/ndf/scripts/tests/test_models.py | 37 +++++++++++++++- 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/plugins/ndf/scripts/tests/test_metrics.py b/plugins/ndf/scripts/tests/test_metrics.py index adf092075..44a31d546 100644 --- a/plugins/ndf/scripts/tests/test_metrics.py +++ b/plugins/ndf/scripts/tests/test_metrics.py @@ -104,3 +104,55 @@ def test_aggregate_current_metrics_for_representative_state() -> None: "(実測不可)(レビュー担当)", ], } + + +def test_format_report_current_structure() -> None: + """現状固定。format_report の見出し・表の行・注意書きを含む出力構造を記録する。""" + report_metrics = { + "impl": { + "codex / gpt-5": { + "rounds": 1, + "applied": 1, + "abandoned": 0, + "first_review_approval_rate": 1.0, + "avg_fix_rounds": 0.0, + "budget_exceeded_rate": 0.0, + "test_failure_rate": 0.0, + "seconds": 120.0, + } + }, + "reviewer": { + "claude / sonnet": { + "reviews": 1, + "findings": 2, + "resolution_rate": 0.5, + "agreement_rate": 1.0, + "seconds": 30.0, + } + }, + "unmeasured": ["round 2: kiro の auto は分離"], + "assumed": ["round 1: codex は前提で数える"], + } + + report = metrics.format_report(report_metrics) + + # 見出しが含まれること + assert "## 実装担当" in report + assert "## レビュー担当" in report + assert "## 集計から分離したラウンド" in report + assert "## 指定値で代用したラウンド" in report + assert "## 比較として読むときの限界" in report + + # 各表の要点(行データ)が含まれること + assert "| codex / gpt-5 |" in report + assert "| claude / sonnet |" in report + + # unmeasured / assumed / caveats のリスト項目が含まれること + assert "- round 2: kiro の auto は分離" in report + assert "- round 1: codex は前提で数える" in report + for caveat in metrics.COMPARISON_CAVEATS: + assert f"- {caveat}" in report + + # 全体の行数を現状の値で固定する + assert len(report.splitlines()) == 29 + diff --git a/plugins/ndf/scripts/tests/test_models.py b/plugins/ndf/scripts/tests/test_models.py index 44a4e9d71..3883dd6ba 100644 --- a/plugins/ndf/scripts/tests/test_models.py +++ b/plugins/ndf/scripts/tests/test_models.py @@ -10,7 +10,12 @@ if str(LIB) not in sys.path: sys.path.insert(0, str(LIB)) -from models import ModelSpecError, observed_model, parse_model_args +from models import ( + ModelSpecError, + observed_model, + parse_model_args, + separation_reason, +) @pytest.mark.parametrize( @@ -58,3 +63,33 @@ def test_observed_model_selects_model_with_most_input_tokens() -> None: }""" assert observed_model("claude", stdout_text) == "claude-opus" + + +@pytest.mark.parametrize( + ("runtime", "model", "expected"), + [ + ( + "kiro", + None, + "kiro の auto はラウンドごとに違うモデルが動きうる", + ), + ( + "kiro", + "auto", + "kiro の auto はラウンドごとに違うモデルが動きうる", + ), + ( + "codex", + None, + "codex はモデルを指定しておらず、実際に動いたモデルも取得できない", + ), + ("claude", None, None), + ("kiro", "claude-sonnet", None), + ("codex", "gpt-5", None), + ], +) +def test_separation_reason_current_behavior( + runtime: str, model: str | None, expected: str | None +) -> None: + """現状固定。kiro の auto / 未指定かつ実測不可 / 分離しないの 3 分岐を記録する。""" + assert separation_reason(runtime, model) == expected From 0217c3ad1170e4aff17390213607dd47e2d5ea8a Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 18:21:49 +0000 Subject: [PATCH 09/22] =?UTF-8?q?Test:=20mismatch=5Fwarning=20=E3=81=AE?= =?UTF-8?q?=E7=8F=BE=E7=8A=B6=E5=9B=BA=E5=AE=9A=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=20=E2=80=94=20plugins/ndf/scripts/l?= =?UTF-8?q?ib/models.py#mismatch=5Fwarning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit observed なし / requested なし / 両方なし / 一致 / 不一致の各分岐を実行して 現状の出力を期待値として固定する。対象コードは変更していない。 Item-Id: R1-005 Round: 1 Impl-Runtime: kiro Impl-Model: default --- plugins/ndf/scripts/tests/test_models.py | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/plugins/ndf/scripts/tests/test_models.py b/plugins/ndf/scripts/tests/test_models.py index 3883dd6ba..d457b4128 100644 --- a/plugins/ndf/scripts/tests/test_models.py +++ b/plugins/ndf/scripts/tests/test_models.py @@ -12,6 +12,7 @@ from models import ( ModelSpecError, + mismatch_warning, observed_model, parse_model_args, separation_reason, @@ -93,3 +94,27 @@ def test_separation_reason_current_behavior( ) -> None: """現状固定。kiro の auto / 未指定かつ実測不可 / 分離しないの 3 分岐を記録する。""" assert separation_reason(runtime, model) == expected + + +@pytest.mark.parametrize( + ("requested", "observed"), + [ + ("gpt-5", None), + (None, "gpt-5"), + (None, None), + ("gpt-5", "gpt-5"), + ], +) +def test_mismatch_warning_returns_none_when_no_conflict( + requested: str | None, observed: str | None +) -> None: + """現状固定。実測なし / 指定なし / 両方なし / 一致では None を返す。""" + assert mismatch_warning("claude", requested, observed) is None + + +def test_mismatch_warning_reports_conflict() -> None: + """現状固定。指定値と実測値が食い違うと警告文字列を返す。""" + assert mismatch_warning("claude", "claude-opus", "claude-sonnet") == ( + "⚠ claude: 指定したモデル claude-opus と実際に動いたモデル claude-sonnet が" + "食い違っています。比較には使えません" + ) From 546d9cd66676acf5e82666fbb00222fbff7da4ab Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 18:36:15 +0000 Subject: [PATCH 10/22] =?UTF-8?q?Test:=20=E7=8F=BE=E7=8A=B6=E5=9B=BA?= =?UTF-8?q?=E5=AE=9A=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD=E5=8A=A0=20?= =?UTF-8?q?=E2=80=94=20metrics.py#aggregate=20/=20models.py#assumption=5Fn?= =?UTF-8?q?ote?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - aggregate: rounds も items も空の入力({} と空リストの辞書)で、空のバケットと空リストを返す経路を固定する - assumption_note: 未指定 / 実測可能ランタイム / 分離対象(kiro の auto)/ 代用注記の 4 分岐を固定する Item-Id: R2-001 Round: 2 Impl-Runtime: claude Impl-Model: default Co-Authored-By: Claude Opus 5.5 (1M context) --- plugins/ndf/scripts/tests/test_metrics.py | 13 +++++++++++++ plugins/ndf/scripts/tests/test_models.py | 17 +++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/plugins/ndf/scripts/tests/test_metrics.py b/plugins/ndf/scripts/tests/test_metrics.py index 44a31d546..d201ffc67 100644 --- a/plugins/ndf/scripts/tests/test_metrics.py +++ b/plugins/ndf/scripts/tests/test_metrics.py @@ -5,6 +5,8 @@ import pathlib import sys +import pytest + LIB = pathlib.Path(__file__).resolve().parents[1] / "lib" if str(LIB) not in sys.path: sys.path.insert(0, str(LIB)) @@ -106,6 +108,17 @@ def test_aggregate_current_metrics_for_representative_state() -> None: } +@pytest.mark.parametrize("state", [{}, {"rounds": [], "items": []}]) +def test_aggregate_returns_empty_buckets_for_empty_state(state: dict) -> None: + """現状固定。rounds も items も空なら、空のバケットと空リストを返す。""" + assert metrics.aggregate(state) == { + "impl": {}, + "reviewer": {}, + "unmeasured": [], + "assumed": [], + } + + def test_format_report_current_structure() -> None: """現状固定。format_report の見出し・表の行・注意書きを含む出力構造を記録する。""" report_metrics = { diff --git a/plugins/ndf/scripts/tests/test_models.py b/plugins/ndf/scripts/tests/test_models.py index d457b4128..fda188ec1 100644 --- a/plugins/ndf/scripts/tests/test_models.py +++ b/plugins/ndf/scripts/tests/test_models.py @@ -12,6 +12,7 @@ from models import ( ModelSpecError, + assumption_note, mismatch_warning, observed_model, parse_model_args, @@ -118,3 +119,19 @@ def test_mismatch_warning_reports_conflict() -> None: "⚠ claude: 指定したモデル claude-opus と実際に動いたモデル claude-sonnet が" "食い違っています。比較には使えません" ) + + +@pytest.mark.parametrize( + ("runtime", "model", "expected"), + [ + ("codex", None, None), + ("claude", "claude-opus", None), + ("kiro", "auto", None), + ("codex", "gpt-5", "codex は指定した gpt-5 で動いた前提で数える(実測不可)"), + ], +) +def test_assumption_note_current_behavior( + runtime: str, model: str | None, expected: str | None +) -> None: + """現状固定。未指定 / 実測可能 / 分離対象 / 代用注記の 4 分岐を記録する。""" + assert assumption_note(runtime, model) == expected From 19fa674250375bfe022a6787acdacd3b784f16e3 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 18:36:55 +0000 Subject: [PATCH 11/22] =?UTF-8?q?Revert=20"Test:=20=E7=8F=BE=E7=8A=B6?= =?UTF-8?q?=E5=9B=BA=E5=AE=9A=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=20=E2=80=94=20metrics.py#aggregate=20/=20models.py#assumption?= =?UTF-8?q?=5Fnote"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 546d9cd66676acf5e82666fbb00222fbff7da4ab. --- plugins/ndf/scripts/tests/test_metrics.py | 13 ------------- plugins/ndf/scripts/tests/test_models.py | 17 ----------------- 2 files changed, 30 deletions(-) diff --git a/plugins/ndf/scripts/tests/test_metrics.py b/plugins/ndf/scripts/tests/test_metrics.py index d201ffc67..44a31d546 100644 --- a/plugins/ndf/scripts/tests/test_metrics.py +++ b/plugins/ndf/scripts/tests/test_metrics.py @@ -5,8 +5,6 @@ import pathlib import sys -import pytest - LIB = pathlib.Path(__file__).resolve().parents[1] / "lib" if str(LIB) not in sys.path: sys.path.insert(0, str(LIB)) @@ -108,17 +106,6 @@ def test_aggregate_current_metrics_for_representative_state() -> None: } -@pytest.mark.parametrize("state", [{}, {"rounds": [], "items": []}]) -def test_aggregate_returns_empty_buckets_for_empty_state(state: dict) -> None: - """現状固定。rounds も items も空なら、空のバケットと空リストを返す。""" - assert metrics.aggregate(state) == { - "impl": {}, - "reviewer": {}, - "unmeasured": [], - "assumed": [], - } - - def test_format_report_current_structure() -> None: """現状固定。format_report の見出し・表の行・注意書きを含む出力構造を記録する。""" report_metrics = { diff --git a/plugins/ndf/scripts/tests/test_models.py b/plugins/ndf/scripts/tests/test_models.py index fda188ec1..d457b4128 100644 --- a/plugins/ndf/scripts/tests/test_models.py +++ b/plugins/ndf/scripts/tests/test_models.py @@ -12,7 +12,6 @@ from models import ( ModelSpecError, - assumption_note, mismatch_warning, observed_model, parse_model_args, @@ -119,19 +118,3 @@ def test_mismatch_warning_reports_conflict() -> None: "⚠ claude: 指定したモデル claude-opus と実際に動いたモデル claude-sonnet が" "食い違っています。比較には使えません" ) - - -@pytest.mark.parametrize( - ("runtime", "model", "expected"), - [ - ("codex", None, None), - ("claude", "claude-opus", None), - ("kiro", "auto", None), - ("codex", "gpt-5", "codex は指定した gpt-5 で動いた前提で数える(実測不可)"), - ], -) -def test_assumption_note_current_behavior( - runtime: str, model: str | None, expected: str | None -) -> None: - """現状固定。未指定 / 実測可能 / 分離対象 / 代用注記の 4 分岐を記録する。""" - assert assumption_note(runtime, model) == expected From b69a2ac4d14be86245d744fadde91a4483086de6 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 18:42:21 +0000 Subject: [PATCH 12/22] =?UTF-8?q?Test:=20=E7=8F=BE=E7=8A=B6=E5=9B=BA?= =?UTF-8?q?=E5=AE=9A=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD=E5=8A=A0=20?= =?UTF-8?q?=E2=80=94=20metrics.py#aggregate=20/=20models.py#is=5Fmeasurabl?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit モデル分離と判定なしレビューの集計経路、および計測可否の真偽分岐を固定する。 Item-Id: R2-002 Round: 2 Impl-Runtime: codex Impl-Model: default --- plugins/ndf/scripts/tests/test_metrics.py | 48 ++++++++++++++++++++++- plugins/ndf/scripts/tests/test_models.py | 18 +++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/plugins/ndf/scripts/tests/test_metrics.py b/plugins/ndf/scripts/tests/test_metrics.py index 44a31d546..5c76e2808 100644 --- a/plugins/ndf/scripts/tests/test_metrics.py +++ b/plugins/ndf/scripts/tests/test_metrics.py @@ -106,6 +106,53 @@ def test_aggregate_current_metrics_for_representative_state() -> None: } +def test_aggregate_current_metrics_for_unmeasured_models_and_missing_verdicts() -> None: + """現状固定。分離理由と判定なしレビューを担当ごとに記録する。""" + state = { + "items": [{"item_id": "R2-001", "status": "done"}], + "rounds": [ + { + "round": 2, + "impl": "claude", + "impl_model": { + "requested": "claude-opus", + "observed": "claude-sonnet", + }, + "reviewers": ["claude", "agy", "kiro", "codex"], + "reviewer_models": { + "claude": {"requested": "claude-sonnet", "observed": "claude-sonnet"}, + "agy": {"requested": "gpt-5", "observed": None}, + "kiro": {"requested": "auto", "observed": None}, + "codex": {"requested": None, "observed": None}, + }, + "items": ["R2-001"], + "reviews": [ + {"claude": "APPROVE", "findings": []}, + {"claude": "APPROVE", "agy": "APPROVE", "findings": []}, + ], + } + ], + } + + result = metrics.aggregate(state) + + # モデル不一致は警告されるが、現状では指定モデルの実装集計に残る。 + assert list(result["impl"]) == ["claude / claude-opus"] + assert result["unmeasured"] == [ + "round 2: ⚠ claude: 指定したモデル claude-opus と実際に動いたモデル " + "claude-sonnet が食い違っています。比較には使えません", + "round 2: kiro の auto はラウンドごとに違うモデルが動きうるため、" + "レビュー担当の集計から分離する", + "round 2: codex はモデルを指定しておらず、実際に動いたモデルも取得できないため、" + "レビュー担当の集計から分離する", + ] + assert set(result["reviewer"]) == {"agy / gpt-5", "claude / claude-sonnet"} + assert result["reviewer"]["claude / claude-sonnet"]["reviews"] == 2 + assert result["reviewer"]["agy / gpt-5"]["reviews"] == 1 + assert result["reviewer"]["claude / claude-sonnet"]["agreement_rate"] == 1.0 + assert result["reviewer"]["agy / gpt-5"]["agreement_rate"] == 1.0 + + def test_format_report_current_structure() -> None: """現状固定。format_report の見出し・表の行・注意書きを含む出力構造を記録する。""" report_metrics = { @@ -155,4 +202,3 @@ def test_format_report_current_structure() -> None: # 全体の行数を現状の値で固定する assert len(report.splitlines()) == 29 - diff --git a/plugins/ndf/scripts/tests/test_models.py b/plugins/ndf/scripts/tests/test_models.py index d457b4128..5298df2d9 100644 --- a/plugins/ndf/scripts/tests/test_models.py +++ b/plugins/ndf/scripts/tests/test_models.py @@ -12,6 +12,7 @@ from models import ( ModelSpecError, + is_measurable, mismatch_warning, observed_model, parse_model_args, @@ -96,6 +97,23 @@ def test_separation_reason_current_behavior( assert separation_reason(runtime, model) == expected +@pytest.mark.parametrize( + ("runtime", "model", "expected"), + [ + ("claude", None, True), + ("codex", "gpt-5", True), + ("kiro", "auto", False), + ("kiro", None, False), + ("codex", None, False), + ], +) +def test_is_measurable_current_behavior( + runtime: str, model: str | None, expected: bool +) -> None: + """現状固定。分離理由の有無に対応する計測可否を記録する。""" + assert is_measurable(runtime, model) is expected + + @pytest.mark.parametrize( ("requested", "observed"), [ From 6a69ffbc33200b94412eaccdbc3b4d68b10d95e3 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 18:48:01 +0000 Subject: [PATCH 13/22] =?UTF-8?q?Test:=20=E7=8F=BE=E7=8A=B6=E5=9B=BA?= =?UTF-8?q?=E5=AE=9A=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD=E5=8A=A0=20?= =?UTF-8?q?=E2=80=94=20metrics.py#format=5Freport?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 両表が空のときに「(記録なし)」が出力される境界経路を固定する。 Item-Id: R2-003 Round: 2 Impl-Runtime: agy Impl-Model: default --- plugins/ndf/scripts/tests/test_metrics.py | 41 +++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/plugins/ndf/scripts/tests/test_metrics.py b/plugins/ndf/scripts/tests/test_metrics.py index 5c76e2808..f8c210e1d 100644 --- a/plugins/ndf/scripts/tests/test_metrics.py +++ b/plugins/ndf/scripts/tests/test_metrics.py @@ -202,3 +202,44 @@ def test_format_report_current_structure() -> None: # 全体の行数を現状の値で固定する assert len(report.splitlines()) == 29 + + +def test_format_report_empty_tables_emits_no_records() -> None: + """現状固定。impl と reviewer が空のときに「(記録なし)」が出力されることを記録する。""" + report_metrics = { + "impl": {}, + "reviewer": {}, + "unmeasured": [], + "assumed": [], + } + + report = metrics.format_report(report_metrics) + + # 実装担当とレビュー担当の見出し直後に「(記録なし)」が出ること + assert "## 実装担当\n\n(記録なし)" in report + assert "## レビュー担当\n\n(記録なし)" in report + + # unmeasured / assumed が空のときは見出しが出ないこと + assert "## 集計から分離したラウンド" not in report + assert "## 指定値で代用したラウンド" not in report + + # 比較の限界の見出しと注意書きが残ること + assert "## 比較として読むときの限界" in report + for caveat in metrics.COMPARISON_CAVEATS: + assert f"- {caveat}" in report + + # 完全な出力行の一致を固定する + expected_lines = [ + "## 実装担当", + "", + "(記録なし)", + "", + "## レビュー担当", + "", + "(記録なし)", + "", + "## 比較として読むときの限界", + "", + *[f"- {caveat}" for caveat in metrics.COMPARISON_CAVEATS], + ] + assert report.splitlines() == expected_lines From 33e59ae0c4651d0194498a81ccecc8672de6881d Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 19:04:01 +0000 Subject: [PATCH 14/22] =?UTF-8?q?Refactor:=20=E6=A7=8B=E9=80=A0=E6=94=B9?= =?UTF-8?q?=E5=96=84=EF=BC=88=E9=81=A9=E7=94=A8=E3=83=A9=E3=82=A6=E3=83=B3?= =?UTF-8?q?=E3=83=89=201=20/=20R3-001,002,004,005=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit result_posts.py: 「積む → 読み直す → 連番」の 3 行を _enqueue_seq へ集約 (consolidate_duplication)。 assignment.py: resolve_participants を検証・絞り込み・分類の 3 段へ抽出 (extract_method)。 metrics.py: format_report の表ヘッダ / 区切りを IMPL_HEADER 等の定数へ (introduce_named_constant)。 monitor.py: _scan_early_fatal の or 連鎖を (patterns, benign) の表引きへ (replace_with_lookup_table)。現状固定テストを追加。 R3-003 は enqueue / post の呼び出し元が対象範囲外 (plugins/ndf/skills/cross-review/tests)にあり、投稿指定オブジェクトを 新しい引数の形にすると壊れるため skipped。 Item-Id: R3-001 Round: 3 Impl-Runtime: kiro Impl-Model: default --- plugins/ndf/scripts/lib/assignment.py | 106 ++++++++++++------ plugins/ndf/scripts/lib/metrics.py | 22 ++-- plugins/ndf/scripts/lib/monitor.py | 26 +++-- plugins/ndf/scripts/lib/result_posts.py | 25 +++-- .../scripts/tests/test_monitor_early_fatal.py | 61 ++++++++++ 5 files changed, 178 insertions(+), 62 deletions(-) create mode 100644 plugins/ndf/scripts/tests/test_monitor_early_fatal.py diff --git a/plugins/ndf/scripts/lib/assignment.py b/plugins/ndf/scripts/lib/assignment.py index 42c7b29c1..a1d0b3a55 100644 --- a/plugins/ndf/scripts/lib/assignment.py +++ b/plugins/ndf/scripts/lib/assignment.py @@ -140,36 +140,14 @@ def to_state(self) -> dict[str, Any]: Probe = Callable[[list[str]], tuple[dict[str, dict[str, Any]], bool]] -def resolve_participants( - pool: Iterable[str], - *, - host: str, - include: Iterable[str] = (), - exclude: Iterable[str] = (), - only: Optional[str] = None, - probe: Probe, - require_all: bool = False, -) -> Participants: - """母集合の既定・足す者・外す者・1 者指定から使える者を決める(設計の決定 2〜4)。 - - 順序: - - 1. `include` / `exclude` の各名前が `ALL_RUNTIMES` にあり、重ならないことを確かめる。 - `exclude` の名前が「`pool` ∪ `include`」に無ければ弾く(cross-review でホストを - 外す指定はここに当たる) - 2. 参加者 = `pool` ∪ `include` − `exclude`(`ALL_RUNTIMES` の順) - 3. `only` があれば、参加者に含まれ `exclude` に無いことを確かめ、参加者をその 1 者にする - 4. `probe(参加者)` で確かめる。飛ばされたら全員を通ったものとし `probe_skipped` を真にする - 5. `require_all` が真で通らない者がいれば `AssignmentError`(欠けた者と理由を並べる) - 6. 通った者を `available`、通らなかった者と理由を `unavailable` として返す +def _validate_and_base( + pool: list[str], include: list[str], exclude: list[str] +) -> set[str]: + """入力名と競合を検証し、検証済みの母集合(`pool` ∪ `include`)を返す。 - 名前の綴りの検査(argparse の型)はこの前段で済んでいる前提だが、ここでも - `ALL_RUNTIMES` に無い名前は弾く。 + `include` / `exclude` の名前が `ALL_RUNTIMES` にあること、両者が重ならないこと、 + `exclude` が母集合の中の名前だけであることを確かめる(順序 1)。 """ - pool = list(pool) - include = list(include) - exclude = list(exclude) - for name in (*include, *exclude): if name not in ALL_RUNTIMES: raise AssignmentError( @@ -187,9 +165,18 @@ def resolve_participants( f"母集合に無い者は外せません: {', '.join(_in_fixed_order(outside))}" f"(母集合: {', '.join(_in_fixed_order(base))})" ) + return base - participants = _in_fixed_order(base - set(exclude)) +def _select_participants( + base: set[str], exclude: list[str], only: Optional[str] +) -> list[str]: + """参加者を絞り込む(順序 2〜3)。 + + 参加者 = 母集合 − `exclude`(`ALL_RUNTIMES` の順)。`only` があれば `exclude` と + 矛盾せず参加者に含まれることを確かめ、その 1 者にする。 + """ + participants = _in_fixed_order(base - set(exclude)) if only is not None: if only in exclude: raise AssignmentError(f"--only と --exclude が矛盾しています: {only}") @@ -199,17 +186,62 @@ def resolve_participants( f"(参加者: {', '.join(participants)})" ) participants = [only] + return participants + + +def _classify_availability( + participants: list[str], probe: Probe +) -> tuple[list[str], dict[str, str], bool]: + """`probe` の結果を使える者・使えない者へ分類する(順序 4)。 + 飛ばされたら全員を通ったものとして扱う。戻り値は + `(available, unavailable, probe_skipped)`。 + """ results, skipped = probe(list(participants)) if skipped: - available, unavailable = list(participants), {} - else: - unavailable = { - n: str(results.get(n, {}).get("detail", "")) - for n in participants - if not results.get(n, {}).get("ok", False) - } - available = [n for n in participants if n not in unavailable] + return list(participants), {}, True + unavailable = { + n: str(results.get(n, {}).get("detail", "")) + for n in participants + if not results.get(n, {}).get("ok", False) + } + available = [n for n in participants if n not in unavailable] + return available, unavailable, False + + +def resolve_participants( + pool: Iterable[str], + *, + host: str, + include: Iterable[str] = (), + exclude: Iterable[str] = (), + only: Optional[str] = None, + probe: Probe, + require_all: bool = False, +) -> Participants: + """母集合の既定・足す者・外す者・1 者指定から使える者を決める(設計の決定 2〜4)。 + + 順序: + + 1. `include` / `exclude` の各名前が `ALL_RUNTIMES` にあり、重ならないことを確かめる。 + `exclude` の名前が「`pool` ∪ `include`」に無ければ弾く(cross-review でホストを + 外す指定はここに当たる) + 2. 参加者 = `pool` ∪ `include` − `exclude`(`ALL_RUNTIMES` の順) + 3. `only` があれば、参加者に含まれ `exclude` に無いことを確かめ、参加者をその 1 者にする + 4. `probe(参加者)` で確かめる。飛ばされたら全員を通ったものとし `probe_skipped` を真にする + 5. `require_all` が真で通らない者がいれば `AssignmentError`(欠けた者と理由を並べる) + 6. 通った者を `available`、通らなかった者と理由を `unavailable` として返す + + 名前の綴りの検査(argparse の型)はこの前段で済んでいる前提だが、ここでも + `ALL_RUNTIMES` に無い名前は弾く。 + """ + pool = list(pool) + include = list(include) + exclude = list(exclude) + + base = _validate_and_base(pool, include, exclude) + participants = _select_participants(base, exclude, only) + available, unavailable, skipped = _classify_availability(participants, probe) if require_all and unavailable: failed = " / ".join(f"{n}({d})" for n, d in unavailable.items()) diff --git a/plugins/ndf/scripts/lib/metrics.py b/plugins/ndf/scripts/lib/metrics.py index af2d625c9..bf92c888e 100644 --- a/plugins/ndf/scripts/lib/metrics.py +++ b/plugins/ndf/scripts/lib/metrics.py @@ -301,16 +301,25 @@ def _reviewer_rows(metrics: dict[str, Any]) -> list[str]: ] +# 表のヘッダ行と区切り行。同じ収束ループ共通層の `transcript_agents.py` が +# LIST_HEADER / LIST_RULE などを定数へ寄せているのに合わせ、書式を 1 か所へ集める。 +IMPL_HEADER = ( + "| ランタイム / モデル | 担当R | 適用 | 見送り | 初回承認率 | 平均修正R | 予算超過率 | テスト失敗率 | 所要秒 |" +) +IMPL_RULE = "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |" +REVIEWER_HEADER = ( + "| ランタイム / モデル | レビュー回数 | 指摘 | 修正に至った率 | 判定一致率 | 所要秒 |" +) +REVIEWER_RULE = "| --- | ---: | ---: | ---: | ---: | ---: |" + + def format_report(metrics: dict[str, Any]) -> str: """人が読む形へ整形する。比較の限界を必ず添える。""" lines: list[str] = [] _emit_table( lines, "実装担当", - ( - "| ランタイム / モデル | 担当R | 適用 | 見送り | 初回承認率 | 平均修正R | 予算超過率 | テスト失敗率 | 所要秒 |", - "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |", - ), + (IMPL_HEADER, IMPL_RULE), _impl_rows(metrics), ) @@ -318,10 +327,7 @@ def format_report(metrics: dict[str, Any]) -> str: _emit_table( lines, "レビュー担当", - ( - "| ランタイム / モデル | レビュー回数 | 指摘 | 修正に至った率 | 判定一致率 | 所要秒 |", - "| --- | ---: | ---: | ---: | ---: | ---: |", - ), + (REVIEWER_HEADER, REVIEWER_RULE), _reviewer_rows(metrics), ) diff --git a/plugins/ndf/scripts/lib/monitor.py b/plugins/ndf/scripts/lib/monitor.py index f21a05422..f49531f0e 100755 --- a/plugins/ndf/scripts/lib/monitor.py +++ b/plugins/ndf/scripts/lib/monitor.py @@ -646,20 +646,30 @@ def _scan_patterns( return None +# 致命の照合の並び。`(patterns, benign)` を優先順位の順に並べる。**利用上限を先に置く。** +# 上限で落ちた後に別の致命が続く形が普通で、上限のほうが原因(#729 の決定 4)。種類を +# 足すときはこの表へ 1 行足す。`benign` が `None` のときは `_scan_patterns` の既定 +# (`EARLY_ERROR_BENIGN`)を使う。 +_EARLY_FATAL_SCANS: tuple[tuple[list[re.Pattern[str]], Optional[list[re.Pattern[str]]]], ...] = ( + (USAGE_LIMIT_FATAL, None), + (EARLY_ERROR_FATAL, None), + (EARLY_ERROR_FATAL_WARNING_SHAPED, EARLY_ERROR_BENIGN_KEEP_WARNINGS), +) + + def _scan_early_fatal(path: pathlib.Path) -> Optional[str]: """err.log の致命の一致(kill 対象)。**利用上限も含む。** 理由(`usage_limit` か `early_error` か)の区別はここでは行わず、`_early_error` が `USAGE_LIMIT_FATAL` を先に照合して決める。この関数は「止めるべき文言があるか」だけを返す。 + + 照合は `_EARLY_FATAL_SCANS` の順に行い、最初のヒットを返す。 """ - hit = _scan_patterns(path, USAGE_LIMIT_FATAL) or _scan_patterns(path, EARLY_ERROR_FATAL) - if hit: - return hit - return _scan_patterns( - path, - EARLY_ERROR_FATAL_WARNING_SHAPED, - benign=EARLY_ERROR_BENIGN_KEEP_WARNINGS, - ) + for patterns, benign in _EARLY_FATAL_SCANS: + hit = _scan_patterns(path, patterns, benign=benign) + if hit: + return hit + return None def _scan_early_warn(path: pathlib.Path) -> Optional[str]: diff --git a/plugins/ndf/scripts/lib/result_posts.py b/plugins/ndf/scripts/lib/result_posts.py index 010e778e5..8b249e5c6 100644 --- a/plugins/ndf/scripts/lib/result_posts.py +++ b/plugins/ndf/scripts/lib/result_posts.py @@ -181,6 +181,19 @@ class ReviewOutcome(NamedTuple): detail: str +def _enqueue_seq(queue: post_queue.Queue, item: dict[str, Any], repo: str, pr: int, + actor: str | None) -> tuple[pathlib.Path, Any]: + """項目を 1 件積み、積んだファイルと連番を返す。 + + 「積む → 読み直す → 連番を取り出す」の 3 行はレビューの初回・退避の送り直し・ + 修正のループで同じ形で繰り返す。連番の取り出し方を 1 か所へ寄せる。 + """ + path = post_queue.enqueue(queue, item["kind"], repo, pr, item["fields"], + actor=actor, extra=item["extra"]) + seq = (post_queue.read_item(path) or {}).get("seq") + return path, seq + + def _find(items: list[dict[str, Any]], seq: Any) -> dict[str, Any] | None: return next((i for i in items if i.get("seq") == seq), None) @@ -237,9 +250,7 @@ def post_review(queue: post_queue.Queue, payload_path: pathlib.Path | str, findings = len(_findings(_read_json(payload_path))) item = review_posts(payload_path, result_path, repo, pr, round_no, seat, head_sha, is_own_pr, since=since)[0] - path = post_queue.enqueue(queue, item["kind"], repo, pr, item["fields"], - actor=actor, extra=item["extra"]) - seq = (post_queue.read_item(path) or {}).get("seq") + _, seq = _enqueue_seq(queue, item, repo, pr, actor) flushed = queue.flush() ours_failed = flushed.failed is not None and flushed.failed.get("seq") == seq @@ -247,9 +258,7 @@ def post_review(queue: post_queue.Queue, payload_path: pathlib.Path | str, queue.drop(flushed.failed.get("seq")) item = review_posts(payload_path, result_path, repo, pr, round_no, seat, head_sha, is_own_pr, evacuate_all=True, since=since)[0] - path = post_queue.enqueue(queue, item["kind"], repo, pr, item["fields"], - actor=actor, extra=item["extra"]) - seq = (post_queue.read_item(path) or {}).get("seq") + _, seq = _enqueue_seq(queue, item, repo, pr, actor) flushed = queue.flush() done = _find(flushed.sent, seq) or _find(flushed.skipped, seq) @@ -366,9 +375,7 @@ def post_fix(queue: post_queue.Queue, result_path: pathlib.Path | str, repo: str """返信・決着・まとめを待ち行列へ積んで流す。""" seqs: dict[int, str] = {} for item in fix_posts(result_path, repo, pr, round_no): - path = post_queue.enqueue(queue, item["kind"], repo, pr, item["fields"], - actor=actor, extra=item["extra"]) - seq = (post_queue.read_item(path) or {}).get("seq") + _, seq = _enqueue_seq(queue, item, repo, pr, actor) if seq is not None: seqs[int(seq)] = item["kind"] flushed = queue.flush() diff --git a/plugins/ndf/scripts/tests/test_monitor_early_fatal.py b/plugins/ndf/scripts/tests/test_monitor_early_fatal.py new file mode 100644 index 000000000..32d538647 --- /dev/null +++ b/plugins/ndf/scripts/tests/test_monitor_early_fatal.py @@ -0,0 +1,61 @@ +"""`monitor._scan_early_fatal` の現状固定テスト(構造改善 R3-005 の前置き)。 + +致命の照合を条件の連鎖から表引きへ置き換える前に、現状の戻り値を記録する。 +利用上限・致命・警告の見た目の致命・いずれも無しの 4 経路を通す。 +""" +from __future__ import annotations + +import importlib.util +import pathlib +import sys + +LIB = pathlib.Path(__file__).resolve().parents[1] / "lib" +if str(LIB) not in sys.path: + sys.path.insert(0, str(LIB)) + +SPEC = importlib.util.spec_from_file_location("monitor", LIB / "monitor.py") +assert SPEC is not None and SPEC.loader is not None +monitor = importlib.util.module_from_spec(SPEC) +# `monitor` は `@dataclass` を使うため、exec 前に sys.modules へ登録する +# (dataclasses がクラスのモジュールを名前で引く)。 +sys.modules[SPEC.name] = monitor +SPEC.loader.exec_module(monitor) + + +def _err_log(tmp_path: pathlib.Path, text: str) -> pathlib.Path: + path = tmp_path / "agent-err.log" + path.write_text(text, encoding="utf-8") + return path + + +def test_usage_limit_only_hits(tmp_path) -> None: + """利用上限の文言だけがあるとき、その行を返す。""" + path = _err_log(tmp_path, "Monthly request limit reached\n") + + assert monitor._scan_early_fatal(path) == "Monthly request limit reached" + + +def test_early_error_only_hits(tmp_path) -> None: + """致命(認証失敗)だけがあるとき、その行を返す。""" + path = _err_log(tmp_path, "Authentication failed\n") + + assert monitor._scan_early_fatal(path) == "Authentication failed" + + +def test_warning_shaped_only_hits(tmp_path) -> None: + """警告の見た目の致命だけがあるとき、その行を返す。""" + path = _err_log( + tmp_path, + "is rejected because it matches one or more rules on the denied list\n", + ) + + assert monitor._scan_early_fatal(path) == ( + "is rejected because it matches one or more rules on the denied list" + ) + + +def test_no_fatal_returns_none(tmp_path) -> None: + """致命の文言が無いときは `None` を返す。""" + path = _err_log(tmp_path, "just some ordinary progress output\n") + + assert monitor._scan_early_fatal(path) is None From 565cf4fc0cfb57d925b677c709d935e0ac8f099c Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 19:04:42 +0000 Subject: [PATCH 15/22] =?UTF-8?q?Revert=20"Refactor:=20=E6=A7=8B=E9=80=A0?= =?UTF-8?q?=E6=94=B9=E5=96=84=EF=BC=88=E9=81=A9=E7=94=A8=E3=83=A9=E3=82=A6?= =?UTF-8?q?=E3=83=B3=E3=83=89=201=20/=20R3-001,002,004,005=EF=BC=89"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 33e59ae0c4651d0194498a81ccecc8672de6881d. --- plugins/ndf/scripts/lib/assignment.py | 106 ++++++------------ plugins/ndf/scripts/lib/metrics.py | 22 ++-- plugins/ndf/scripts/lib/monitor.py | 26 ++--- plugins/ndf/scripts/lib/result_posts.py | 25 ++--- .../scripts/tests/test_monitor_early_fatal.py | 61 ---------- 5 files changed, 62 insertions(+), 178 deletions(-) delete mode 100644 plugins/ndf/scripts/tests/test_monitor_early_fatal.py diff --git a/plugins/ndf/scripts/lib/assignment.py b/plugins/ndf/scripts/lib/assignment.py index a1d0b3a55..42c7b29c1 100644 --- a/plugins/ndf/scripts/lib/assignment.py +++ b/plugins/ndf/scripts/lib/assignment.py @@ -140,14 +140,36 @@ def to_state(self) -> dict[str, Any]: Probe = Callable[[list[str]], tuple[dict[str, dict[str, Any]], bool]] -def _validate_and_base( - pool: list[str], include: list[str], exclude: list[str] -) -> set[str]: - """入力名と競合を検証し、検証済みの母集合(`pool` ∪ `include`)を返す。 +def resolve_participants( + pool: Iterable[str], + *, + host: str, + include: Iterable[str] = (), + exclude: Iterable[str] = (), + only: Optional[str] = None, + probe: Probe, + require_all: bool = False, +) -> Participants: + """母集合の既定・足す者・外す者・1 者指定から使える者を決める(設計の決定 2〜4)。 + + 順序: + + 1. `include` / `exclude` の各名前が `ALL_RUNTIMES` にあり、重ならないことを確かめる。 + `exclude` の名前が「`pool` ∪ `include`」に無ければ弾く(cross-review でホストを + 外す指定はここに当たる) + 2. 参加者 = `pool` ∪ `include` − `exclude`(`ALL_RUNTIMES` の順) + 3. `only` があれば、参加者に含まれ `exclude` に無いことを確かめ、参加者をその 1 者にする + 4. `probe(参加者)` で確かめる。飛ばされたら全員を通ったものとし `probe_skipped` を真にする + 5. `require_all` が真で通らない者がいれば `AssignmentError`(欠けた者と理由を並べる) + 6. 通った者を `available`、通らなかった者と理由を `unavailable` として返す - `include` / `exclude` の名前が `ALL_RUNTIMES` にあること、両者が重ならないこと、 - `exclude` が母集合の中の名前だけであることを確かめる(順序 1)。 + 名前の綴りの検査(argparse の型)はこの前段で済んでいる前提だが、ここでも + `ALL_RUNTIMES` に無い名前は弾く。 """ + pool = list(pool) + include = list(include) + exclude = list(exclude) + for name in (*include, *exclude): if name not in ALL_RUNTIMES: raise AssignmentError( @@ -165,18 +187,9 @@ def _validate_and_base( f"母集合に無い者は外せません: {', '.join(_in_fixed_order(outside))}" f"(母集合: {', '.join(_in_fixed_order(base))})" ) - return base - - -def _select_participants( - base: set[str], exclude: list[str], only: Optional[str] -) -> list[str]: - """参加者を絞り込む(順序 2〜3)。 - 参加者 = 母集合 − `exclude`(`ALL_RUNTIMES` の順)。`only` があれば `exclude` と - 矛盾せず参加者に含まれることを確かめ、その 1 者にする。 - """ participants = _in_fixed_order(base - set(exclude)) + if only is not None: if only in exclude: raise AssignmentError(f"--only と --exclude が矛盾しています: {only}") @@ -186,62 +199,17 @@ def _select_participants( f"(参加者: {', '.join(participants)})" ) participants = [only] - return participants - - -def _classify_availability( - participants: list[str], probe: Probe -) -> tuple[list[str], dict[str, str], bool]: - """`probe` の結果を使える者・使えない者へ分類する(順序 4)。 - 飛ばされたら全員を通ったものとして扱う。戻り値は - `(available, unavailable, probe_skipped)`。 - """ results, skipped = probe(list(participants)) if skipped: - return list(participants), {}, True - unavailable = { - n: str(results.get(n, {}).get("detail", "")) - for n in participants - if not results.get(n, {}).get("ok", False) - } - available = [n for n in participants if n not in unavailable] - return available, unavailable, False - - -def resolve_participants( - pool: Iterable[str], - *, - host: str, - include: Iterable[str] = (), - exclude: Iterable[str] = (), - only: Optional[str] = None, - probe: Probe, - require_all: bool = False, -) -> Participants: - """母集合の既定・足す者・外す者・1 者指定から使える者を決める(設計の決定 2〜4)。 - - 順序: - - 1. `include` / `exclude` の各名前が `ALL_RUNTIMES` にあり、重ならないことを確かめる。 - `exclude` の名前が「`pool` ∪ `include`」に無ければ弾く(cross-review でホストを - 外す指定はここに当たる) - 2. 参加者 = `pool` ∪ `include` − `exclude`(`ALL_RUNTIMES` の順) - 3. `only` があれば、参加者に含まれ `exclude` に無いことを確かめ、参加者をその 1 者にする - 4. `probe(参加者)` で確かめる。飛ばされたら全員を通ったものとし `probe_skipped` を真にする - 5. `require_all` が真で通らない者がいれば `AssignmentError`(欠けた者と理由を並べる) - 6. 通った者を `available`、通らなかった者と理由を `unavailable` として返す - - 名前の綴りの検査(argparse の型)はこの前段で済んでいる前提だが、ここでも - `ALL_RUNTIMES` に無い名前は弾く。 - """ - pool = list(pool) - include = list(include) - exclude = list(exclude) - - base = _validate_and_base(pool, include, exclude) - participants = _select_participants(base, exclude, only) - available, unavailable, skipped = _classify_availability(participants, probe) + available, unavailable = list(participants), {} + else: + unavailable = { + n: str(results.get(n, {}).get("detail", "")) + for n in participants + if not results.get(n, {}).get("ok", False) + } + available = [n for n in participants if n not in unavailable] if require_all and unavailable: failed = " / ".join(f"{n}({d})" for n, d in unavailable.items()) diff --git a/plugins/ndf/scripts/lib/metrics.py b/plugins/ndf/scripts/lib/metrics.py index bf92c888e..af2d625c9 100644 --- a/plugins/ndf/scripts/lib/metrics.py +++ b/plugins/ndf/scripts/lib/metrics.py @@ -301,25 +301,16 @@ def _reviewer_rows(metrics: dict[str, Any]) -> list[str]: ] -# 表のヘッダ行と区切り行。同じ収束ループ共通層の `transcript_agents.py` が -# LIST_HEADER / LIST_RULE などを定数へ寄せているのに合わせ、書式を 1 か所へ集める。 -IMPL_HEADER = ( - "| ランタイム / モデル | 担当R | 適用 | 見送り | 初回承認率 | 平均修正R | 予算超過率 | テスト失敗率 | 所要秒 |" -) -IMPL_RULE = "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |" -REVIEWER_HEADER = ( - "| ランタイム / モデル | レビュー回数 | 指摘 | 修正に至った率 | 判定一致率 | 所要秒 |" -) -REVIEWER_RULE = "| --- | ---: | ---: | ---: | ---: | ---: |" - - def format_report(metrics: dict[str, Any]) -> str: """人が読む形へ整形する。比較の限界を必ず添える。""" lines: list[str] = [] _emit_table( lines, "実装担当", - (IMPL_HEADER, IMPL_RULE), + ( + "| ランタイム / モデル | 担当R | 適用 | 見送り | 初回承認率 | 平均修正R | 予算超過率 | テスト失敗率 | 所要秒 |", + "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |", + ), _impl_rows(metrics), ) @@ -327,7 +318,10 @@ def format_report(metrics: dict[str, Any]) -> str: _emit_table( lines, "レビュー担当", - (REVIEWER_HEADER, REVIEWER_RULE), + ( + "| ランタイム / モデル | レビュー回数 | 指摘 | 修正に至った率 | 判定一致率 | 所要秒 |", + "| --- | ---: | ---: | ---: | ---: | ---: |", + ), _reviewer_rows(metrics), ) diff --git a/plugins/ndf/scripts/lib/monitor.py b/plugins/ndf/scripts/lib/monitor.py index f49531f0e..f21a05422 100755 --- a/plugins/ndf/scripts/lib/monitor.py +++ b/plugins/ndf/scripts/lib/monitor.py @@ -646,30 +646,20 @@ def _scan_patterns( return None -# 致命の照合の並び。`(patterns, benign)` を優先順位の順に並べる。**利用上限を先に置く。** -# 上限で落ちた後に別の致命が続く形が普通で、上限のほうが原因(#729 の決定 4)。種類を -# 足すときはこの表へ 1 行足す。`benign` が `None` のときは `_scan_patterns` の既定 -# (`EARLY_ERROR_BENIGN`)を使う。 -_EARLY_FATAL_SCANS: tuple[tuple[list[re.Pattern[str]], Optional[list[re.Pattern[str]]]], ...] = ( - (USAGE_LIMIT_FATAL, None), - (EARLY_ERROR_FATAL, None), - (EARLY_ERROR_FATAL_WARNING_SHAPED, EARLY_ERROR_BENIGN_KEEP_WARNINGS), -) - - def _scan_early_fatal(path: pathlib.Path) -> Optional[str]: """err.log の致命の一致(kill 対象)。**利用上限も含む。** 理由(`usage_limit` か `early_error` か)の区別はここでは行わず、`_early_error` が `USAGE_LIMIT_FATAL` を先に照合して決める。この関数は「止めるべき文言があるか」だけを返す。 - - 照合は `_EARLY_FATAL_SCANS` の順に行い、最初のヒットを返す。 """ - for patterns, benign in _EARLY_FATAL_SCANS: - hit = _scan_patterns(path, patterns, benign=benign) - if hit: - return hit - return None + hit = _scan_patterns(path, USAGE_LIMIT_FATAL) or _scan_patterns(path, EARLY_ERROR_FATAL) + if hit: + return hit + return _scan_patterns( + path, + EARLY_ERROR_FATAL_WARNING_SHAPED, + benign=EARLY_ERROR_BENIGN_KEEP_WARNINGS, + ) def _scan_early_warn(path: pathlib.Path) -> Optional[str]: diff --git a/plugins/ndf/scripts/lib/result_posts.py b/plugins/ndf/scripts/lib/result_posts.py index 8b249e5c6..010e778e5 100644 --- a/plugins/ndf/scripts/lib/result_posts.py +++ b/plugins/ndf/scripts/lib/result_posts.py @@ -181,19 +181,6 @@ class ReviewOutcome(NamedTuple): detail: str -def _enqueue_seq(queue: post_queue.Queue, item: dict[str, Any], repo: str, pr: int, - actor: str | None) -> tuple[pathlib.Path, Any]: - """項目を 1 件積み、積んだファイルと連番を返す。 - - 「積む → 読み直す → 連番を取り出す」の 3 行はレビューの初回・退避の送り直し・ - 修正のループで同じ形で繰り返す。連番の取り出し方を 1 か所へ寄せる。 - """ - path = post_queue.enqueue(queue, item["kind"], repo, pr, item["fields"], - actor=actor, extra=item["extra"]) - seq = (post_queue.read_item(path) or {}).get("seq") - return path, seq - - def _find(items: list[dict[str, Any]], seq: Any) -> dict[str, Any] | None: return next((i for i in items if i.get("seq") == seq), None) @@ -250,7 +237,9 @@ def post_review(queue: post_queue.Queue, payload_path: pathlib.Path | str, findings = len(_findings(_read_json(payload_path))) item = review_posts(payload_path, result_path, repo, pr, round_no, seat, head_sha, is_own_pr, since=since)[0] - _, seq = _enqueue_seq(queue, item, repo, pr, actor) + path = post_queue.enqueue(queue, item["kind"], repo, pr, item["fields"], + actor=actor, extra=item["extra"]) + seq = (post_queue.read_item(path) or {}).get("seq") flushed = queue.flush() ours_failed = flushed.failed is not None and flushed.failed.get("seq") == seq @@ -258,7 +247,9 @@ def post_review(queue: post_queue.Queue, payload_path: pathlib.Path | str, queue.drop(flushed.failed.get("seq")) item = review_posts(payload_path, result_path, repo, pr, round_no, seat, head_sha, is_own_pr, evacuate_all=True, since=since)[0] - _, seq = _enqueue_seq(queue, item, repo, pr, actor) + path = post_queue.enqueue(queue, item["kind"], repo, pr, item["fields"], + actor=actor, extra=item["extra"]) + seq = (post_queue.read_item(path) or {}).get("seq") flushed = queue.flush() done = _find(flushed.sent, seq) or _find(flushed.skipped, seq) @@ -375,7 +366,9 @@ def post_fix(queue: post_queue.Queue, result_path: pathlib.Path | str, repo: str """返信・決着・まとめを待ち行列へ積んで流す。""" seqs: dict[int, str] = {} for item in fix_posts(result_path, repo, pr, round_no): - _, seq = _enqueue_seq(queue, item, repo, pr, actor) + path = post_queue.enqueue(queue, item["kind"], repo, pr, item["fields"], + actor=actor, extra=item["extra"]) + seq = (post_queue.read_item(path) or {}).get("seq") if seq is not None: seqs[int(seq)] = item["kind"] flushed = queue.flush() diff --git a/plugins/ndf/scripts/tests/test_monitor_early_fatal.py b/plugins/ndf/scripts/tests/test_monitor_early_fatal.py deleted file mode 100644 index 32d538647..000000000 --- a/plugins/ndf/scripts/tests/test_monitor_early_fatal.py +++ /dev/null @@ -1,61 +0,0 @@ -"""`monitor._scan_early_fatal` の現状固定テスト(構造改善 R3-005 の前置き)。 - -致命の照合を条件の連鎖から表引きへ置き換える前に、現状の戻り値を記録する。 -利用上限・致命・警告の見た目の致命・いずれも無しの 4 経路を通す。 -""" -from __future__ import annotations - -import importlib.util -import pathlib -import sys - -LIB = pathlib.Path(__file__).resolve().parents[1] / "lib" -if str(LIB) not in sys.path: - sys.path.insert(0, str(LIB)) - -SPEC = importlib.util.spec_from_file_location("monitor", LIB / "monitor.py") -assert SPEC is not None and SPEC.loader is not None -monitor = importlib.util.module_from_spec(SPEC) -# `monitor` は `@dataclass` を使うため、exec 前に sys.modules へ登録する -# (dataclasses がクラスのモジュールを名前で引く)。 -sys.modules[SPEC.name] = monitor -SPEC.loader.exec_module(monitor) - - -def _err_log(tmp_path: pathlib.Path, text: str) -> pathlib.Path: - path = tmp_path / "agent-err.log" - path.write_text(text, encoding="utf-8") - return path - - -def test_usage_limit_only_hits(tmp_path) -> None: - """利用上限の文言だけがあるとき、その行を返す。""" - path = _err_log(tmp_path, "Monthly request limit reached\n") - - assert monitor._scan_early_fatal(path) == "Monthly request limit reached" - - -def test_early_error_only_hits(tmp_path) -> None: - """致命(認証失敗)だけがあるとき、その行を返す。""" - path = _err_log(tmp_path, "Authentication failed\n") - - assert monitor._scan_early_fatal(path) == "Authentication failed" - - -def test_warning_shaped_only_hits(tmp_path) -> None: - """警告の見た目の致命だけがあるとき、その行を返す。""" - path = _err_log( - tmp_path, - "is rejected because it matches one or more rules on the denied list\n", - ) - - assert monitor._scan_early_fatal(path) == ( - "is rejected because it matches one or more rules on the denied list" - ) - - -def test_no_fatal_returns_none(tmp_path) -> None: - """致命の文言が無いときは `None` を返す。""" - path = _err_log(tmp_path, "just some ordinary progress output\n") - - assert monitor._scan_early_fatal(path) is None From 3a23aef47c5f7134bc0eeace84809a9f069bf3f4 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 19:15:51 +0000 Subject: [PATCH 16/22] =?UTF-8?q?Refactor:=20extract=5Fmethod=20/=20replac?= =?UTF-8?q?e=5Fwith=5Flookup=5Ftable=20=E2=80=94=20result=5Fposts.py#fix?= =?UTF-8?q?=5Fposts=20=E3=81=BB=E3=81=8B=202=20=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - R4-001: fix_posts から返信項目の組み立てを _reply_items、スレッド決着項目の組み立てを _closing_thread_items として抽出(extract_method) - R4-002: transcript_agents.py#interrupted の layer / depth / parent の等値フィルタを、 属性名 → 期待値の対応表を回す 1 本の判定へまとめる(replace_with_lookup_table) - R4-005: run_metrics.py#_select の repo / kind / version の等値フィルタを (args 属性名, 行キー) の対応表へまとめる(replace_with_lookup_table)。 先に _select の絞り込みを直接通す現状固定テストを test_run_metrics.py へ追加 Item-Id: R4-001 Round: 4 Impl-Runtime: claude Impl-Model: claude-opus-5-5 --- plugins/ndf/scripts/lib/result_posts.py | 41 +++++++++++++------ plugins/ndf/scripts/lib/run_metrics.py | 9 ++-- plugins/ndf/scripts/lib/transcript_agents.py | 10 ++--- plugins/ndf/scripts/tests/test_run_metrics.py | 31 ++++++++++++++ 4 files changed, 68 insertions(+), 23 deletions(-) diff --git a/plugins/ndf/scripts/lib/result_posts.py b/plugins/ndf/scripts/lib/result_posts.py index 010e778e5..e3a5497b7 100644 --- a/plugins/ndf/scripts/lib/result_posts.py +++ b/plugins/ndf/scripts/lib/result_posts.py @@ -302,19 +302,9 @@ def _fix_summary_body(fix: dict[str, Any], round_no: int | None, return "\n".join(lines) + "\n" -def fix_posts(result_path: pathlib.Path | str, repo: str, pr: int, - round_no: int | None = None) -> list[dict[str, Any]]: - """修正の結果ファイルから、待ち行列へ積む項目の列を組み立てる。 - - 並びは「返信 → 決着 → まとめ」である。返信を先に置くのは、決着したスレッドが - 畳まれた後に返信が届くと、読み手がその返信を開かないためである。 - """ - fix = _read_json(result_path) - resolved = _dict_items(fix.get("resolved_threads")) - deferred = _dict_items(fix.get("deferred")) - rejected = _dict_items(fix.get("rejected")) - commit = str(fix.get("fix_commit") or fix.get("commit_sha") or "") - +def _reply_items(resolved: list[dict[str, Any]], deferred: list[dict[str, Any]], + rejected: list[dict[str, Any]], commit: str) -> list[dict[str, Any]]: + """決着・見送り・却下の各要素へ付ける返信の項目を、その順に組み立てる。""" # (要素の列, 返信の定型句, 理由を取り出すキー)。決着は理由の代わりにコミットを添える reply_rules = ( (resolved, "対応しました。", None), @@ -331,15 +321,40 @@ def fix_posts(result_path: pathlib.Path | str, repo: str, pr: int, reply = _reply(entry.get("comment_id"), f"{lead}{note}".strip()) if reply: items.append(reply) + return items + + +def _closing_thread_items(resolved: list[dict[str, Any]], deferred: list[dict[str, Any]], + rejected: list[dict[str, Any]]) -> list[dict[str, Any]]: + """スレッドを決着させる項目を組み立てる。""" # 見送り・却下は既定では決着させない(次のラウンドで見直す)。最終スイープは # スレッドを残さないため、要素の `resolve` を真にして決着まで求める。 closing = resolved + [e for e in deferred + rejected if e.get("resolve")] + items: list[dict[str, Any]] = [] for entry in closing: thread_id = entry.get("thread_id") if thread_id: items.append({"kind": "thread-resolve", "fields": {"thread_id": str(thread_id)}, "extra": {"ident": f"resolve-{thread_id}"}}) + return items + + +def fix_posts(result_path: pathlib.Path | str, repo: str, pr: int, + round_no: int | None = None) -> list[dict[str, Any]]: + """修正の結果ファイルから、待ち行列へ積む項目の列を組み立てる。 + + 並びは「返信 → 決着 → まとめ」である。返信を先に置くのは、決着したスレッドが + 畳まれた後に返信が届くと、読み手がその返信を開かないためである。 + """ + fix = _read_json(result_path) + resolved = _dict_items(fix.get("resolved_threads")) + deferred = _dict_items(fix.get("deferred")) + rejected = _dict_items(fix.get("rejected")) + commit = str(fix.get("fix_commit") or fix.get("commit_sha") or "") + + items = _reply_items(resolved, deferred, rejected, commit) + items += _closing_thread_items(resolved, deferred, rejected) items.append({ "kind": "pr-comment", "fields": {"body": _fix_summary_body(fix, round_no, len(resolved), diff --git a/plugins/ndf/scripts/lib/run_metrics.py b/plugins/ndf/scripts/lib/run_metrics.py index e746a8da6..9dd8838e4 100755 --- a/plugins/ndf/scripts/lib/run_metrics.py +++ b/plugins/ndf/scripts/lib/run_metrics.py @@ -328,16 +328,15 @@ def _within_time_bound(started: Optional[_dt.datetime], def _select(rows: list[dict], args: argparse.Namespace) -> list[dict]: since, until = _bound(args.since, upper=False), _bound(args.until, upper=True) until_exclusive = bool(args.until and re.fullmatch(r"\d{4}-\d{2}-\d{2}", args.until)) + # (args の属性名, 行のキー)。指定の無い(偽の)軸は絞り込まない + equality_filters = (("repo", "repo"), ("kind", "kind"), ("version", "ndf_version")) out = [] for row in rows: started = _parse_time(row.get("started_at")) if not _within_time_bound(started, since, until, until_exclusive): continue - if args.repo and row.get("repo") != args.repo: - continue - if args.kind and row.get("kind") != args.kind: - continue - if args.version and row.get("ndf_version") != args.version: + if any(getattr(args, attr) and row.get(key) != getattr(args, attr) + for attr, key in equality_filters): continue out.append(row) return out diff --git a/plugins/ndf/scripts/lib/transcript_agents.py b/plugins/ndf/scripts/lib/transcript_agents.py index 73798f000..cc9600a6c 100644 --- a/plugins/ndf/scripts/lib/transcript_agents.py +++ b/plugins/ndf/scripts/lib/transcript_agents.py @@ -516,18 +516,18 @@ def interrupted( 再び現れない(契約の終わり方の表)。 """ picked = set(agents or ()) + # 記録の属性名 → 期待値。指定の無い(None の)軸は絞り込まない + wanted = {attr: value for attr, value in (("layer", layer), ("depth", depth), + ("parent_agent_id", parent)) + if value is not None} out: list[AgentRecord] = [] for record in records: if record.ending != "rate_limit": continue - if layer is not None and record.layer != layer: - continue - if depth is not None and record.depth != depth: + if any(getattr(record, attr) != value for attr, value in wanted.items()): continue if picked and record.agent_id not in picked: continue - if parent is not None and record.parent_agent_id != parent: - continue out.append(record) return out diff --git a/plugins/ndf/scripts/tests/test_run_metrics.py b/plugins/ndf/scripts/tests/test_run_metrics.py index c5b58999a..fb276d8d2 100644 --- a/plugins/ndf/scripts/tests/test_run_metrics.py +++ b/plugins/ndf/scripts/tests/test_run_metrics.py @@ -310,3 +310,34 @@ def test_aggregate_by_reason(metrics_tree): def test_aggregate_with_no_summaries(tmp_path): proc = _aggregate(tmp_path / "empty") assert proc.returncode == 0, proc.stderr + + +# ---------- _select の等値の絞り込み(現状固定) ---------- + +_SELECT_ROWS = [ + {"id": "a", "repo": "o/x", "kind": "cross-review", "ndf_version": "1.0.0"}, + {"id": "b", "repo": "o/y", "kind": "cross-review", "ndf_version": "1.0.0"}, + {"id": "c", "repo": "o/x", "kind": "cross-refactoring", "ndf_version": "2.0.0"}, + {"id": "d", "repo": "o/x", "kind": "cross-review", "ndf_version": "2.0.0"}, +] + + +def _select_args(**over): + import argparse + base = {"since": None, "until": None, "repo": None, "kind": None, "version": None} + base.update(over) + return argparse.Namespace(**base) + + +@pytest.mark.parametrize("over, expected", [ + ({}, ["a", "b", "c", "d"]), + ({"repo": "o/x"}, ["a", "c", "d"]), + ({"kind": "cross-refactoring"}, ["c"]), + ({"version": "1.0.0"}, ["a", "b"]), + ({"repo": "o/x", "kind": "cross-review", "version": "2.0.0"}, ["d"]), + ({"repo": "o/z"}, []), + ({"repo": "", "kind": "", "version": ""}, ["a", "b", "c", "d"]), +]) +def test_select_equality_filters(rm, over, expected): + picked = rm._select(_SELECT_ROWS, _select_args(**over)) + assert [row["id"] for row in picked] == expected From 363906464ec07a4a0b3b3d9467a2505c5ddca2bf Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 19:25:45 +0000 Subject: [PATCH 17/22] =?UTF-8?q?Refactor:=20extract=5Fmethod=20=E2=80=94?= =?UTF-8?q?=20NDF=E7=B5=90=E6=9E=9C=E9=9B=86=E8=A8=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit トークン・モデル統計とレビュー投稿の組み立て段階を、それぞれ名前付き関数へ抽出する。 Item-Id: R4-003 Round: 4 Impl-Runtime: codex Impl-Model: default --- plugins/ndf/scripts/lib/result_posts.py | 41 ++++++++++++------- plugins/ndf/scripts/lib/transcript_agents.py | 42 +++++++++++++------- 2 files changed, 55 insertions(+), 28 deletions(-) diff --git a/plugins/ndf/scripts/lib/result_posts.py b/plugins/ndf/scripts/lib/result_posts.py index e3a5497b7..a9213a7e8 100644 --- a/plugins/ndf/scripts/lib/result_posts.py +++ b/plugins/ndf/scripts/lib/result_posts.py @@ -122,6 +122,31 @@ def _review_body(payload: dict[str, Any], round_no: int, seat: str, intent: str, return "\n\n".join(parts) + "\n" +def _split_findings(findings: list[dict[str, Any]], evacuate_all: bool, + ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + """指摘をインラインと総評へ振り分ける。""" + inline = [] if evacuate_all else [f for f in findings if _can_be_inline(f)] + evacuated = [f for f in findings if f not in inline] + return inline, evacuated + + +def _review_fields(body: str, posted_as: str, inline: list[dict[str, Any]], + head_sha: str | None, since: str | None) -> dict[str, Any]: + """レビュー API へ渡す fields を組み立てる。""" + fields: dict[str, Any] = {"body": body, "event": posted_as} + if head_sha: + fields["commit_id"] = head_sha + if since: + fields["since"] = since + if inline: + fields["comments"] = [ + {"path": str(f.get("path")), "line": _line_no(f.get("line")), + "side": "RIGHT", "body": str(f.get("body") or "")} + for f in inline + ] + return fields + + def review_posts(payload_path: pathlib.Path | str, result_path: pathlib.Path | str, repo: str, pr: int, round_no: int, seat: str, head_sha: str | None, is_own_pr: bool, @@ -145,21 +170,9 @@ def review_posts(payload_path: pathlib.Path | str, result_path: pathlib.Path | s intent = str(result.get("event") or result.get("intent") or "COMMENT") posted_as = "COMMENT" if is_own_pr else intent - inline = [] if evacuate_all else [f for f in findings if _can_be_inline(f)] - evacuated = [f for f in findings if f not in inline] + inline, evacuated = _split_findings(findings, evacuate_all) body = _review_body(payload, round_no, seat, intent, evacuated) - - fields: dict[str, Any] = {"body": body, "event": posted_as} - if head_sha: - fields["commit_id"] = head_sha - if since: - fields["since"] = since - if inline: - fields["comments"] = [ - {"path": str(f.get("path")), "line": _line_no(f.get("line")), - "side": "RIGHT", "body": str(f.get("body") or "")} - for f in inline - ] + fields = _review_fields(body, posted_as, inline, head_sha, since) extra = {"ident": f"{seat}-r{round_no}", "agent": seat, "seat": seat, "round": round_no, "intent": intent, "posted_as": posted_as, diff --git a/plugins/ndf/scripts/lib/transcript_agents.py b/plugins/ndf/scripts/lib/transcript_agents.py index cc9600a6c..7f8f3411f 100644 --- a/plugins/ndf/scripts/lib/transcript_agents.py +++ b/plugins/ndf/scripts/lib/transcript_agents.py @@ -264,32 +264,46 @@ def _tool_use_ids(rows: list[dict]) -> list[str]: return ids -def _aggregate_token_metrics(rows: list[dict], record: AgentRecord) -> None: - """固定費・最大充填・応答数・モデルを合成でない応答だけで数える(AC24)。 +def _token_stats(rows: list[dict]) -> tuple[int | None, int | None, int | None]: + """合成でない応答から固定費・最大充填・実作業を返す。""" + fixed = None + peak = None + for row in rows: + if row.get("type") != "assistant" or _is_synthetic(row): + continue + total = _input_total(row) + if total is not None: + if fixed is None: + fixed = total + peak = total if peak is None else max(peak, total) + work = peak - fixed if fixed is not None and peak is not None else None + return fixed, peak, work - `record` の `fixed` / `peak` / `work` / `responses` / `model` を埋める。 - """ + +def _model_stats(rows: list[dict]) -> tuple[int, str | None]: + """合成でない応答から応答数と最頻出モデルを返す。""" seen: set[str] = set() models: Counter = Counter() for row in rows: if row.get("type") != "assistant" or _is_synthetic(row): continue - total = _input_total(row) - if total is not None: - if record.fixed is None: - record.fixed = total - record.peak = total if record.peak is None else max(record.peak, total) message_id = _message(row).get("id") if isinstance(message_id, str) and message_id and message_id not in seen: seen.add(message_id) model = _message(row).get("model") if isinstance(model, str) and model: models[model] += 1 - record.responses = len(seen) - if record.fixed is not None and record.peak is not None: - record.work = record.peak - record.fixed - if models: - record.model = models.most_common(1)[0][0] + model = models.most_common(1)[0][0] if models else None + return len(seen), model + + +def _aggregate_token_metrics(rows: list[dict], record: AgentRecord) -> None: + """固定費・最大充填・応答数・モデルを合成でない応答だけで数える(AC24)。 + + `record` の `fixed` / `peak` / `work` / `responses` / `model` を埋める。 + """ + record.fixed, record.peak, record.work = _token_stats(rows) + record.responses, record.model = _model_stats(rows) def _count_interruptions(rows: list[dict]) -> int: From b80f917561ac0de742bf0326180c0003498d6d5c Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 20:18:36 +0000 Subject: [PATCH 18/22] =?UTF-8?q?Docs:=20=E4=B8=8A=E9=99=90=E3=81=AE?= =?UTF-8?q?=E6=96=87=E8=A8=80=E3=81=AE=E5=87=BA=E6=89=80=E3=81=A8=E8=B5=B7?= =?UTF-8?q?=E5=8B=95=E3=81=A7=E3=81=8D=E3=81=AA=E3=81=84=E7=A2=BA=E8=AA=8D?= =?UTF-8?q?=E3=81=AE=E6=89=B1=E3=81=84=E3=82=92=E7=A2=BA=E5=AE=9A=E4=BB=95?= =?UTF-8?q?=E6=A7=98=E3=81=B8=E5=8F=96=E3=82=8A=E8=BE=BC=E3=82=80=EF=BC=88?= =?UTF-8?q?#811=20#813=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 利用上限の照合へ足した 2 行(codex と claude の実物)と、行頭で始まる形だけを読む理由・ 出所を持つ文言だけを足す規約を「起動 1 回の結末と上限の検知」へ書く。確認コマンドを 起動できないときに理由を問わず「通らない」として返す規則と、検索のパスが読めないときの 理由の出し方を「参加者と席」へ書く。取り込みが済んだ要求・設計・実装計画を削除する。 Co-Authored-By: Claude Opus 5 (1M context) --- .../cross-review-launch-outcome.md | 35 ++- .../cross-review-participants-and-seats.md | 21 +- issues/issue-811-813-design.md | 254 ------------------ issues/issue-811-813-implementation-plan.md | 132 --------- issues/issue-811-813-requirements.md | 132 --------- .../tests/test_monitor_usage_limit.py | 3 +- 6 files changed, 56 insertions(+), 521 deletions(-) delete mode 100644 issues/issue-811-813-design.md delete mode 100644 issues/issue-811-813-implementation-plan.md delete mode 100644 issues/issue-811-813-requirements.md diff --git a/docs/specifications/cross-review-launch-outcome.md b/docs/specifications/cross-review-launch-outcome.md index d6339487a..37b1e10c9 100644 --- a/docs/specifications/cross-review-launch-outcome.md +++ b/docs/specifications/cross-review-launch-outcome.md @@ -71,6 +71,24 @@ pid だけへシグナルを送っており、CLI が起こした子プロセス **既存の致命の照合は、kiro の実物と claude の JSON に一致しない。** #619 が再現した形である。 +**codex と claude の上限の文言は、2026-09-22 に `develop`(90c0f06f、Python 3.14.4、 +codex-cli 0.154.0、claude 2.1.278)で集めた実物から採った。** 出所と件数は次のとおりである。 + +| CLI | 集めた経路 | 件数 | +| --- | --- | ---: | +| codex | 記録の誤りの本文のうち、種別が利用上限のもの(1 形) | 62 | +| codex | 収束ループのラウンドで上限に当たった標準エラーの記録(1 形。行頭に印が付く) | 2 | +| codex | 導入済みの実行ファイルに埋め込まれた文字列 | 6 形 | +| claude | 会話の記録のうち、API の誤りの印が真の本文(4 形) | 5,103 | +| claude | 導入済みの実行ファイルの文字列(期間を書かない形を含む) | 1 形 | +| kiro / agy | 手元の記録 | 0 | + +**claude の旧い形(`Claude AI usage limit reached`)は、記録にも実行ファイルにも 0 件である。** +出所が無いため照合の表に置かない。記録に現れたら、同じ手順で出所を書いて足す。 + +**JSON の形で起動した claude が上限のときに標準エラーの記録へ書いた実物は、手元に 0 件で +ある。** 起動した claude の上限を読む経路は、標準出力の状態コードのままである。 + **プロセスグループの停止は 2026-09-15 に測った。** ジョブ制御を有効にして起動した CLI を グループへのシグナルで止めると、3 秒後に子プロセスが書く結果ファイルは書かれなかった。 @@ -86,6 +104,8 @@ pid だけへシグナルを送っており、CLI が起こした子プロセス | CLI 自身の上限は結果なしの状態のまま、理由だけを分ける | agy は自分の上限に当たると終了コード 0 で終わり、結果ファイルを書かない。監視から見れば「終わったが結果が無い」で正しい | | 利用上限の文言は、標準エラーの記録を全担当で見る | 既存の照合が引用・表・grep 形式を除外する。実測では claude の JSON もこの判定に飲み込まれなかった | | 標準出力の記録は claude だけ、JSON 向けの照合で見る | JSON は 1 行に引用符を多く含む。行単位の引用の判定が、引用の内側と判定してしまう | +| 照合の表へ足す文言は、記録か導入済みの実行ファイルに出所を持つものだけにする | 出所の無い行は、合っているかをテストで確かめられない。書いた側の思い込みがテストの期待値にも入り、通ってしまう | +| 利用上限の文言は、期間や種類を問わず 1 行の照合で読む | claude は書き出しの後に期間や種類(週・セッション・支出)を差し込み、codex も同じ書き出しで 5 形を持つ。形ごとに行を足すと、CLI が種類を増やすたびに照合が遅れる | | 読めない結果を共通の語彙に入れる | 結果ファイルが JSON として読めないことは、2 つの Skill が同じ形で見ている | | 判定の値が無い・未投稿は cross-review 固有に残す | 結果ファイルの中身とレビューの投稿の話で、監視も cross-refactoring も知りえない | | 監視の結末に理由の欄を持たせ、無ければ状態からの既定を使う | 利用上限と CLI の上限は、監視の状態からは決まらない | @@ -152,10 +172,21 @@ pid だけへシグナルを送っており、CLI が起こした子プロセス | `usage_limit` | 標準エラーの記録(全担当) | 生きている間の巡回ごと | `Monthly request limit reached` | | `usage_limit` | 同上 | 同上 | `"api_error_status"\s*:\s*429` | | `usage_limit` | 同上 | 同上 | `quota exceeded` / `rate limit exceeded`(大文字小文字を問わない)、`^HTTP/\d\S* 429 ` | +| `usage_limit` | 同上 | 同上 | `^(?:ERROR:\s*)?You['’]ve hit your (?:[\w'’ ]+ )?(?:limit\|budget)\b`(codex と claude の上限。期間や種類を差し込む形を 1 行で覆う) | +| `usage_limit` | 同上 | 同上 | `^(?:ERROR:\s*)?exceeded retry limit, last status: 429\b`(codex の再試行の上限) | | `usage_limit` | claude の標準出力の記録 | 同上 | `"api_error_status"\s*:\s*429` | | `early_error` | 標準エラーの記録 | 同上 | `^HTTP/\d\S* (?:401\|403) ` と、残りの既存の致命 | | `cli_timeout` | 標準エラーの記録 | **終了した後、結果ファイルが無いときだけ** | `print timeout after \S+ with turn in progress` | +**codex と claude の上限の 2 行は、行頭で始まる形だけを読む。** 担当は作業中のコマンドの +出力(差分・ファイルの中身)も標準エラーの記録へ書くため、文言を含む文書やテストを読み +上げた行が記録に入る。行頭に固定すると、文の途中・差分の行・字下げした文字列は一致しない。 +codex は誤りの行の先頭に印(`ERROR: `)を付けるため、その印を省略できる形にしてある。 +**見逃しは 1 度起動し直すだけで済むが、誤検知は結果を書ける担当を止める。** + +**再試行の上限は、最後の状態が 429 のときだけ利用上限と読む。** 同じ文言は状態コードを +差し込んで作られ、503 などの一時的な誤りでも出る。一時的な誤りは起動し直せば解けうる。 + **照合の順序は、利用上限 → 致命 → 警告の見た目の致命である。** 同じ記録に利用上限と他の 致命が両方あれば、理由は利用上限になる。上限で落ちた後に別の文言が続く形が普通で、上限の ほうが原因であるためである。 @@ -234,7 +265,7 @@ cross-refactoring の取り込みが従う契約を、ここで定める。**契 | 観点 | 確かめ方 | | --- | --- | | 理由の語彙と起動し直しの可否が 1 か所にあり、結末を読む関数が失敗しないこと | `plugins/ndf/scripts/tests/test_monitor_outcome_unit.py` | -| 利用上限の文言を検知し、引用・表・grep 形式で誤検知しないこと | `plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py` | +| 利用上限の文言を検知し、引用・表・grep 形式・差分の行で誤検知しないこと。実物の行を 1 つずつ標準エラーの記録へ書くと、理由が利用上限・起動し直しの可否が偽・終了コード 4 になること。再試行の上限の 503 の行が利用上限にならないこと | `plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py` | | CLI の上限の文言を、終了して結果ファイルが無いときだけ理由にすること | 同 `tests/test_launch_print_timeout.py` | | CLI が独立したプロセスグループで起動し、グループごと止まること | 同 `tests/test_launch_cli_process_group.py` | | 結果の取り込みが理由と監視の詳細を残し、終了コードを変えないこと | 同 `tests/test_read_result_reason.py` | @@ -248,8 +279,10 @@ cross-refactoring の取り込みが従う契約を、ここで定める。**契 - [issue #729](https://github.com/devbasex/ai-plugins/issues/729) — 結果なしの判断を共通層へ移す - [issue #619](https://github.com/devbasex/ai-plugins/issues/619) — 利用上限で止まった担当の空振りの起動し直し - [issue #584](https://github.com/devbasex/ai-plugins/issues/584) — 止めた担当が後から結果ファイルを書く +- [issue #811](https://github.com/devbasex/ai-plugins/issues/811) — codex と claude の実物の文言が照合の表に無い - [結果なしの取り込みと開き直し](cross-refactoring-apply-intake.md) — cross-refactoring 側の読み取りを使う仕様 - [PR #791](https://github.com/devbasex/ai-plugins/pull/791) — 実装 +- [PR #820](https://github.com/devbasex/ai-plugins/pull/820) — codex と claude の実物の文言を照合の表へ足す実装 - [`cross-review` の状態ファイルと入出力の契約](../../plugins/ndf/skills/cross-review/docs/04-contracts.md) - [`cross-review` の状態とレビューの手順](../../plugins/ndf/skills/cross-review/docs/01-state-and-review.md) - [`cross-review` の手順](../../plugins/ndf/skills/cross-review/SKILL.md) diff --git a/docs/specifications/cross-review-participants-and-seats.md b/docs/specifications/cross-review-participants-and-seats.md index fbe0175be..0409dd644 100644 --- a/docs/specifications/cross-review-participants-and-seats.md +++ b/docs/specifications/cross-review-participants-and-seats.md @@ -79,6 +79,8 @@ | --- | --- | | 使える者を決める規則を共通層の 1 つの関数へ置き、Skill は結果を状態ファイルと終了コードへ写すだけにする | 規則を Skill ごとに書くと、参加の母集合の作り方・除外の検査・確認の扱いが 2 か所にでき、片方だけが古くなる | | 認証の確認は止めずに結果だけを返す形にし、確認コマンド・未認証の文言・時間切れの秒数は変えない | 確認と中断が 1 つの関数にあると、使える者で回す経路から呼べない。止めるかどうかは全員を要する指定が決める | +| 確認コマンドを起動できないときは、例外の種類を問わず「通らない」として返す | 権限の拒否だけを足す形では、実行形式でないファイルで同じ落ち方が残る。起動できない理由が何であっても、その CLI は使えない | +| 読めない検索のパスで見つからないときも、理由は「実行できない」として出す | 見つからないことと実行権が無いことは同じ例外で返る。見分けるには検索のパスを自分で辿り直すことになり、確認を 1 つ走らせる関数の範囲を超える。理由に権限の拒否が出れば、利用者は検索のパスとファイルの権限を見ればよい | | 参加者は「既定に足す者を加え、外す者を除く」で決め、既定は Skill ごとの関数が持つ | 使う側を並べる形は、ホストが変わるたびに一覧を書き直すことになる | | 全員が揃わないなら始めたくない運用のために、従来の関門を指定で選べるようにする | 既定を変えるだけでは、揃っていることを要求する運用が選べなくなる | | 席は 2 つとし、使える者 → ホスト → 同じランタイムの 2 つ目の順で埋める | 各ラウンドで 2 つの目で見ることを最優先にする。同じ言語モデルの 2 つの文脈より、違う言語モデルの 2 つの文脈のほうが観点が分かれる | @@ -158,6 +160,20 @@ graph TD **外した者へは確認コマンドを呼ばない。** 確認の回数は「参加者の数 + 埋め合わせが要るときの ホストの 1 回」を超えない。 +**確認コマンドを起動できないときは、理由を問わず「通らない」として返す。** 理由は 3 つに +分かれる。 + +| 起動できない理由 | 返す理由 | +| --- | --- | +| コマンドがどこにも無い(見つからない例外) | `コマンドが見つかりません` | +| 確認の秒数の中で応答しない | `<秒数> 秒で応答しませんでした` | +| 実行できない(読めないディレクトリを含む検索のパスでの権限の拒否、実行権の無いファイル、実行形式でないファイル) | `コマンドを実行できません(<例外の説明>)` | + +**検索のパス(`PATH`)に読めないディレクトリがあると、コマンドがどこにも無いときでも +見つからない例外ではなく権限の例外が上がる。** 実行形式でないファイルも同じ形で落ちる。 +3 つ目をまとめて返すことで、どの理由でも参加者の解決は続き、使える者だけで収束ループが +始まる。 + cross-review の新規の初期化は、この結果に席の埋め合わせを足して状態ファイルへ書く。 ```mermaid @@ -373,7 +389,8 @@ graph TD | 観点 | 確かめ方 | | --- | --- | | 使える者の解決が、通らない者を外して続け、外した者へ確認を呼ばず、名前の矛盾と欠けを例外にすること | `plugins/ndf/scripts/tests/test_lib_participants.py` | -| 認証の確認が失敗で例外を上げず、理由を返すこと | 同 `test_auth_probe.py` | +| 認証の確認が失敗で例外を上げず、理由を返すこと。起動できない 2 形(読めないディレクトリを含む検索のパス・実行形式でないファイル)でも理由を返すこと | 同 `test_auth_probe.py` | +| 2 つの開始の手順が、読めないディレクトリを含む検索のパスで、欠けた者を外して始まること | `plugins/ndf/skills/cross-review/tests/test_state_review_pool.py` / `plugins/ndf/skills/cross-refactoring/tests/test_init.py` | | 席の埋め方が 3 者で従来の値と一致し、2 / 1 / 0 者で規則どおりに埋めること。席の名前の形 | `plugins/ndf/skills/cross-refactoring/tests/test_assignment.py` | | 適用の輪番が参加者の数で回ること | `plugins/ndf/scripts/tests/test_lib_assignment.py` | | 再開の反映が、表のとおりに書き換え・記録・知らせを行い、値が同じなら何もしないこと | 同 `test_lib_resume_args.py` | @@ -391,8 +408,10 @@ graph TD - [issue #727](https://github.com/devbasex/ai-plugins/issues/727) — 使える者から担当を割り当てる共通層 - [issue #687](https://github.com/devbasex/ai-plugins/issues/687) — 各ラウンドで 2 者を確保する規則と置き場所 - [issue #478](https://github.com/devbasex/ai-plugins/issues/478) — 認証の確認を関門から把握へ +- [issue #813](https://github.com/devbasex/ai-plugins/issues/813) — 読めない検索のパスで確認が権限の例外を上げ、開始の手順ごと落ちる - [issue #648](https://github.com/devbasex/ai-plugins/issues/648) — 再開で渡した引数が無視される - [PR #793](https://github.com/devbasex/ai-plugins/pull/793) — 実装 +- [PR #820](https://github.com/devbasex/ai-plugins/pull/820) — 起動できない例外を「通らない」として返す実装 - [`cross-review` のレビュワーの母集合と終了基準](../../plugins/ndf/skills/cross-review/docs/05-pool-and-convergence.md) - [`cross-review` の状態ファイルと入出力の契約](../../plugins/ndf/skills/cross-review/docs/04-contracts.md) - [`cross-review` の状態とレビューの手順](../../plugins/ndf/skills/cross-review/docs/01-state-and-review.md) diff --git a/issues/issue-811-813-design.md b/issues/issue-811-813-design.md deleted file mode 100644 index 66e361c35..000000000 --- a/issues/issue-811-813-design.md +++ /dev/null @@ -1,254 +0,0 @@ -# cross-review / cross-refactoring: codex と claude が利用上限で止まっても同じラウンドで起動し直され、PATH に読めないディレクトリがあると始まらない → 上限を理由として報告して起動し直さず、CLI が欠けても使える者だけで始まる(#811 #813) - -## 目的 - -- **壊れていること:** 監視の利用上限の照合の表が codex と claude の実物の文言に 1 つも一致しない(#811)。認証の確認が権限の例外を捕まえず、開始の手順ごと落ちる(#813) -- **誰が困るか:** 収束ループを回す利用者。上限の担当を打ち切りまで待たされ、読めない PATH の環境ではループが始まらない -- **直すと成り立つこと:** 実物の文言に一致する 2 行を照合の表へ足し、確認コマンドを起動できないときは「通らない」として返す。どちらも関数 1 つの範囲で直す - -要求と受け入れ条件は [issue-811-813-requirements.md](issue-811-813-requirements.md) にある。この文書は「どう作るか」だけを扱う。 - -## 用語の対応表 - -本文は左の業務用語で書く。識別子は表とコードブロックにだけ置く。 - -| 業務用語 | 識別子 | -| --- | --- | -| 監視 | `plugins/ndf/scripts/lib/monitor.py` | -| 利用上限の照合の表 | `USAGE_LIMIT_FATAL` | -| 行単位の照合 | `_scan_patterns`。表・引用・grep 形式の行と、引用符の内側の一致を除く | -| 標準エラーの記録 | 担当ごとの `-err.log` | -| 行頭の印 | codex が標準エラーの誤りの行に付ける `ERROR: ` | -| 認証の確認 | `plugins/ndf/scripts/lib/auth.py` の `probe_auth` | -| 確認コマンドを 1 つ走らせる関数 | `auth._run_probe` | -| 権限の例外 | `PermissionError`(`errno` 13)。`OSError` の下位 | -| 起動できない例外 | `OSError` とその下位のすべて | -| 実物 | CLI が実際に出した文言。手元の記録か、導入済みの CLI の実行ファイルに埋め込まれた文字列 | - -## なぜ要るか - -**利用上限の照合の表は 4 行で、どれも codex と claude の実物に一致しない。** #729 の要求の -時点で集めた文言は kiro と claude の JSON 出力だけだった。codex(既定の参加者)が上限に -当たると、理由が「結果ファイル無し」になり、同じラウンドで起動し直してまた上限で落ちる。 - -**認証の確認は、確認コマンドが見つからないことを「見つからない例外」だけで判定している。** -Python の子プロセスの起動は PATH を順に探し、見つからないまま読めないディレクトリを通ると、 -見つからない例外ではなく権限の例外を上げる。認証の確認は「例外は上げない」を約束しているが、 -この例外が開始の手順まで抜ける。 - -## 実測 - -測った環境: `develop`(90c0f06f)、Python 3.14.4、bash 5.3.9、codex-cli 0.154.0、 -claude 2.1.278(2026-09-22)。 - -### 文言を集めた経路と件数 - -| CLI | 集めた経路 | 件数 | 見つかった形 | -| --- | --- | --- | --- | -| codex | `~/.codex/sessions` の記録の誤りの本文(`error.message`)。誤りの種別が `usage_limit_exceeded` のもの | 62(1 形、2026-09-15) | `You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at <日時>.` | -| codex | この設計 Pull Request の cross-review のラウンド 2 で、codex が利用上限で止まった標準エラーの記録(2026-09-22 16:35 UTC、132 行) | 2 行(1 形) | `ERROR: You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at 5:44 PM.` | -| codex | 導入済みの実行ファイルに埋め込まれた文字列 | 6 形 | 上の形のほか、`You've hit your usage limit.` で始まる 3 形、`You've hit your usage limit for <モデル>.`、`exceeded retry limit, last status: <状態>` | -| claude | `~/.claude/projects` の記録のうち、API の誤りの印(`isApiErrorMessage`)が真の本文 | 5,103(4 形、2026-09-01〜22) | 週 3,751 / セッション 758 / 個人の支出 593 / 月の支出 1(下の表) | -| claude | 導入済みの実行ファイル | 期間を書かない形 `You've hit your limit` ほか | `You've hit your ` の後に期間や種類を差し込む作りである | -| claude | 同上。旧い形 `Claude AI usage limit reached` | 0 | 記録にも 0 件 | -| kiro | `~/.kiro` の記録 | 0 | 既存の照合の `Monthly request limit reached` は #619 の実物から採った | -| agy | `~/.gemini` の記録と `antigravity-cli/cli.log` | 0 | 一致したのは監視の照合の定義を写した行と、別のリポジトリのコード差分だけ | -| 全 CLI | 手元に残る標準エラーと標準出力の記録 296 件 | 上限の実物 0 | 一致したのは設計文書・テストを担当が読み上げた行だけ | - -**codex が誤りを標準エラーへ書く形は、行頭の印の後に誤りの本文を続けた 1 行である。** 存在しない -モデルを指定して `codex exec` を実行し、標準エラーの記録に次の行が出ることを確かめた。記録の -誤りの本文と同じ文字列である。利用上限の文言でも同じ形になることを、上の表のラウンド 2 の記録で確かめた。この記録を今の照合の表で読むと一致せず、監視の結末は理由「結果ファイル無し」になった。足す 2 行を加えると、行頭の印の付いた行に一致する。 - -```text -ERROR: {"type":"error","status":400,"error":{"type":"invalid_request_error","message":"The 'ndf-no-such-model-xyz' model is not supported when using Codex with a ChatGPT account."}} -``` - -**claude の JSON 出力の上限は、足さなくても読める。** 導入済みの実行ファイルは、API の誤りで -終わった結果行に `api_error_status` を載せる。10.16.0 のリリース後テストでも、この形は理由 -「利用上限」になった。 - -### 足す 2 行の照合の結果 - -次の 2 行を照合の表の末尾に足し、行単位の照合で入力 1 行ずつを読んだ結果である。 - -```python -re.compile(r"^(?:ERROR:\s*)?You['’]ve hit your (?:[\w'’ ]+ )?(?:limit|budget)\b", re.MULTILINE) -re.compile(r"^(?:ERROR:\s*)?exceeded retry limit, last status: 429\b", re.MULTILINE) -``` - -| 区分 | 入力 | 今の利用上限 | 足した後 | -| --- | --- | --- | --- | -| 実物 | codex の利用上限(行頭の印あり / なし) | — | 一致 | -| 実物(書き出し) | codex の再試行の上限 `ERROR: exceeded retry limit, last status: 429`。書き出しは実行ファイルの文字列、`429` は差し込まれる状態 | — | 一致 | -| 実物 | codex の `Quota exceeded. Check your plan and billing details.` | 一致 | 一致 | -| 実物 | claude の `You've hit your weekly limit · resets Sep 22, 6am (UTC)` | — | 一致 | -| 実物 | claude の `You've hit your session limit · resets 6:30pm (UTC)` | — | 一致 | -| 実物 | claude の `You've hit your individual spend limit · run /usage-credits …` | — | 一致 | -| 実物 | claude の `You've hit your monthly spend limit. Run /usage-credits …` | — | 一致 | -| 実物 | claude の `You've hit your limit`(実行ファイルの文字列) | — | 一致 | -| 実物 | kiro の `Monthly request limit reached` / HTTP 429 の状態行 | 一致 | 一致 | -| 一致しない | codex の再試行の上限 `… last status: 503 Service Unavailable` | — | — | -| 一致しない | codex の 400 の行(上のコードブロック) | — | — | -| 誤検知 | 表(バッククォートあり / なし)、本文のバッククォート、本文の「」 | — | — | -| 誤検知 | 文の途中、リスト、引用、grep 形式、Python の文字列 | — | — | -| 誤検知 | 差分の追加行 `+ERROR: You've hit …`、差分の文脈行 | — | — | - -**実物に無い末尾は入力に足さない。** 再試行の上限の状態の後ろ(`Too Many Requests` など)と、claude の期間を書かない形の後ろ(`· resets 3am (UTC)`)は、#811 の表にある形で、記録にも実行ファイルにも無い。照合はどちらも末尾を読まないため、入力は実物の範囲で止める。 - -### 起動できないときの例外 - -| 条件 | 上がる例外 | `errno` | -| --- | --- | --- | -| PATH に読めないディレクトリがあり、コマンドがどこにも無い | 権限の例外 | 13 `Permission denied` | -| 読めないディレクトリの無い PATH で、コマンドがどこにも無い | 見つからない例外 | 2 | -| 読めないディレクトリの後ろにコマンドがある | 例外なし(終了コード 0) | — | -| 実行権の無いファイル | 権限の例外 | 13 | -| 実行形式でないファイル | 起動できない例外 | 8 `Exec format error` | - -**cross-refactoring の開始の手順も落ちることを確かめた。** 参加者の解決の関数を、ホストの -claude が見つからず読めないディレクトリを含む PATH で呼ぶと、権限の例外で終了コード 1 になる。 -読めないディレクトリを外すと、codex だけが使える者として返る。 - -## 決定の記録 - -### 決定 1: 誤検知を防ぐために、足す文言は行頭で始まる形だけを照合する - -監視は担当が生きている間に照合し、一致すれば担当を止めて起動し直さない。文書の引用に一致すると、 -結果を書ける担当を止める。見逃すと 1 度起動し直すだけである。誤検知の費用のほうが大きい。 -**codex は作業中のコマンドの出力(差分やファイルの中身)も標準エラーへ書く**ため、文言を -含む文書やテストを読み上げた行が記録に入る。行頭に固定すると、文の途中・差分の行・字下げ -した文字列は一致しない。行頭の印は codex の形に合わせて省略可能にする。 - -行頭に固定しない照合は採らない。除外の規則(表・引用・grep 形式)は文の途中に裸で出た文言を -除けない。 - -### 決定 2: 利用上限の文言は、期間や種類を問わず 1 行の照合で読む - -claude は `You've hit your ` の後に期間や種類(週・セッション・個人の支出など)を差し込んで -文言を作る。codex も同じ書き出しで 5 形を持つ。書き出しと、末尾の `limit` / `budget` で読めば、 -記録と実行ファイルで見つけた書き出しの形をすべて 1 行で覆える。形ごとに行を足すと、CLI が種類を増やすたびに照合が遅れる。 - -### 決定 3: codex の再試行の上限は、最後の状態が 429 のときだけ利用上限と読む - -同じ文言は状態コードを差し込んで作られ、503 などの一時的な誤りでも出る。一時的な誤りは -起動し直せば解けうるため、利用上限にしない。 - -### 決定 4: 実物が見つからない文言は照合に足さない - -照合の表の行は、記録か導入済みの実行ファイルに出所を持つものだけにする。出所の無い行は、 -合っているかをテストで確かめられない。書いた側の思い込みがテストの期待値にも入り、通って -しまう。claude の旧い形(`Claude AI usage limit reached`)は記録にも実行ファイルにも無いため -足さない。記録に現れたら、同じ手順で出所を書いて足す。 - -### 決定 5: 照合を足す先は標準エラーの記録だけにし、claude の標準出力の照合は変えない - -claude は JSON を出す形で起動しており、上限の結果行は状態コードを持つ。JSON は 1 行に文章を -含むため、行頭の照合が効かず、担当のレビュー本文の引用に一致しうる。 - -claude の 5 形は、codex と共通の 1 行(決定 2)で読める。そのため標準エラーの記録に出れば利用上限になる。ただし、JSON 出力で起動した claude が上限のときに標準エラーへ書いた記録は無い。起動した claude の上限を読む経路は、標準出力の状態コードのままである。 - -### 決定 6: 確認コマンドを起動できないときは、例外の種類を問わず「通らない」として返す - -認証の確認は「例外は上げない」を約束しており、起動できない理由(権限・実行形式など)が -何であっても、その CLI は使えない。見つからない例外だけは従来の理由のまま残し、その他の -起動できない例外は `コマンドを実行できません(<理由>)` にまとめる。理由には例外の説明 -(`strerror`)を入れる。 - -権限の例外だけを足す形は採らない。実行形式でないファイルで同じ落ち方をする。 - -### 決定 7: 読めない PATH で見つからないときも、理由は起動できないとして出す - -Python の起動は、読めないディレクトリがあると「見つからない」と「実行権が無い」を同じ -例外で返す。見分けるには PATH を自分で探し直す必要があり、関数 1 つの範囲を超える。理由の -文言に `Permission denied` が出れば、利用者は PATH かファイルの権限を見ればよい。 - -## 構成要素 - -| 要素 | 変えること | -| --- | --- | -| 利用上限の照合の表(監視) | 決定 1〜3 の 2 行を末尾に足す | -| 確認コマンドを 1 つ走らせる関数(認証の確認) | 起動できない例外を捕まえ、「通らない」と理由を返す | -| 監視のテスト | 実測の表の実物と誤検知の形を入力にする | -| 認証の確認のテスト | 起動できない 2 形(権限・実行形式)を入力にする | -| 2 つの開始の手順のテスト | 読めないディレクトリを含む PATH で、欠けた CLI が外れることを確かめる | - -```text -plugins/ndf/ -├── scripts/lib/ -│ ├── monitor.py # 利用上限の照合の表に 2 行 -│ └── auth.py # 確認コマンドを 1 つ走らせる関数 -├── scripts/tests/ -│ └── test_auth_probe.py -└── skills/ - ├── cross-review/tests/ - │ ├── test_monitor_usage_limit.py - │ └── test_state_review_pool.py - └── cross-refactoring/tests/ - └── test_init.py -``` - -```mermaid -graph TD - CR["cross-review の開始の手順"] --> AU["認証の確認"] - RF["cross-refactoring の開始の手順"] --> AU - AU --> RP["確認コマンドを
1 つ走らせる関数"] - MON["監視"] --> UL["利用上限の照合の表"] - UL --> OUT["結末の理由
利用上限"] -``` - -## 処理の流れ - -### 監視が標準エラーの記録を読む - -```mermaid -graph TD - A["標準エラーの記録の
末尾 200KB"] --> B{"利用上限の
照合の表に一致"} - B -->|"一致"| C{"除外の規則
表・引用・grep 形式"} - C -->|"除外しない"| D["担当を止める
理由は利用上限"] - C -->|"除外する"| B - B -->|"一致なし"| E["既存の致命の照合へ"] -``` - -足した 2 行は照合の表の末尾に入るため、既存の 4 行より後に照合される。どの行に一致しても -理由は同じであり、順序は結果を変えない。 - -### 認証の確認が 1 つの CLI を確かめる - -```mermaid -graph TD - A["確認コマンドを起動"] --> B{"例外"} - B -->|"見つからない"| C["通らない
コマンドが見つかりません"] - B -->|"時間切れ"| D["通らない
応答しませんでした"] - B -->|"その他の起動できない例外"| E["通らない
コマンドを実行できません"] - B -->|"なし"| F["終了コードと文言で判定"] -``` - -見つからない例外は起動できない例外の下位にあるため、先に捕まえる。時間切れの例外は起動 -できない例外の下位ではないため、順序に依らない。 - -## テスト設計 - -| 受け入れ条件 | 何で確かめるか | -| --- | --- | -| AC1〜AC4 | 監視のテストに、実測の「実物」の行を 1 つずつ標準エラーの記録へ書き、結末を読む。理由が利用上限・起動し直しの可否が偽・終了コード 4 | -| AC2 の後半 | 同じテストで 503 の行を書き、利用上限にならないこと | -| AC5 | 同じテストで実測の「誤検知」の形を書き、担当が止まらないこと | -| AC6 | テストの入力の文字列が、実測の表の文言と一致すること(レビューで照らす) | -| AC7 / AC8 | 認証の確認のテストで、読めないディレクトリを含む PATH と、実行形式でないファイルを確認コマンドにする | -| AC9 | 同じテストの既存の 2 件(見つからない・時間切れ)が通ること | -| AC10 | cross-review の参加者の解決のテストに、読めないディレクトリを含む PATH の場合を足す | -| AC11 | cross-refactoring の開始の手順のテストに、同じ場合を足す | -| AC12〜AC14 | 10.16.1 のリリース後テスト(利用者の環境で手動) | - -読めないディレクトリはテストの中で作る(権限を外した一時ディレクトリ)。root で実行すると -権限が効かないため、そのときはテストを飛ばす。 - -## 未確認のまま残ること - -| 項目 | 内容 | -| --- | --- | -| claude の文言が標準エラーの記録へ出るか | 実測の claude の 5 形の出所は、会話の記録と実行ファイルである。JSON 出力で起動した claude が上限のときに標準エラーへ書いた記録は無い。起動した claude の上限は、標準出力の状態コード(10.16.0 で合格)で読む。10.16.1 の後に上限に当たった標準エラーの記録で確かめる | -| codex の再試行の上限の状態の書き方 | 実行ファイルの文字列は `last status: ` までで、状態の後ろの書き方は記録に無い。テストの入力は `429` までにし、照合も `429` の直後の語の区切りまでしか見ない | -| kiro と agy の利用上限の文言 | 手元の記録に 0 件。kiro は既存の #619 の実物、agy は照合なし | -| claude の旧い形 | 記録にも実行ファイルにも 0 件のため足さない(決定 4) | -| 行頭に固定しても残る誤検知 | 文言で始まる行をそのまま読み上げた場合(文書の本文の行頭に裸で置いた文言を `cat` したときなど)は一致する。この束の文書とテストでは、文言を表・バッククォート・文字列の中にだけ置く | diff --git a/issues/issue-811-813-implementation-plan.md b/issues/issue-811-813-implementation-plan.md deleted file mode 100644 index 5a901ade0..000000000 --- a/issues/issue-811-813-implementation-plan.md +++ /dev/null @@ -1,132 +0,0 @@ -# cross-review / cross-refactoring: codex と claude が利用上限で止まっても同じラウンドで起動し直され、PATH に読めないディレクトリがあると始まらない → 上限を理由として報告して起動し直さず、CLI が欠けても使える者だけで始まる(#811 #813) - -## 関連リンク - -- 要求と受け入れ条件: [issue-811-813-requirements.md](issue-811-813-requirements.md)(AC1〜AC14) -- 設計: [issue-811-813-design.md](issue-811-813-design.md)(決定 1〜7、実測の表) -- 課題: #811 / #813。直った後に閉じる課題: #478 #619 #729(10.16.1 のリリース後テストで AC12〜AC14 が合格したら) - -## モード - -`standard`。本番の振る舞いのバグ修正であり、対象にテストがある。 - -## 用語の対応表 - -本文は左の業務用語で書く。識別子は表とコードブロックにだけ置く(設計文書の表と同じ)。 - -| 業務用語 | 識別子 | -| --- | --- | -| 監視 | `plugins/ndf/scripts/lib/monitor.py` | -| 利用上限の照合の表 | `USAGE_LIMIT_FATAL` | -| 行単位の照合 | `_scan_patterns` | -| 標準エラーの記録 | 担当ごとの `-err.log` | -| 認証の確認 | `plugins/ndf/scripts/lib/auth.py` の `probe_auth` | -| 確認コマンドを 1 つ走らせる関数 | `auth._run_probe` | -| 起動できない例外 | `OSError` とその下位のすべて | -| 使える者の解決 | `plugins/ndf/scripts/lib/assignment.py` の `resolve_participants` | -| 開始の手順 | cross-review の `state.py init`、cross-refactoring の `refactor.py init` | - -## 目的と非目的 - -達成したい状態: - -- 利用上限で止まった担当が、実物の文言から理由「利用上限」として報告され、同じラウンドで起動し直されない -- 確認コマンドを起動できない CLI が、例外ではなく「通らない」として外れ、使える者だけで収束ループが始まる - -やらないこと: - -- 実物の出所が無い文言を照合へ足すこと(決定 4) -- 起動した claude の標準出力の照合を変えること(決定 5) -- 理由の語彙・監視の終了コード・標準出力のキーを変えること -- 必須の外部コマンドの呼び出しの例外処理を変えること - -## 前提 - -- 前提 1: 足す 2 行の照合は、設計の「実測」の実物 11 入力に一致し、誤検知の 12 入力に一致しない。テストの入力はその表から写す -- 前提 2: 読めないディレクトリを使うテストは、権限が効かない実行者(root)では条件が成り立たない。その場合は例外を差し込む形のテストで同じ理由の文言を確かめる - -## 受け入れ条件 - -要求の AC1〜AC11 をこの変更で満たす。AC12〜AC14 は 10.16.1 のリリース後テストで確かめるため、この Pull Request の範囲外である。 - -- [ ] AC1〜AC4: 実物の文言が利用上限として止まり、10.16.0 で合格した 4 形も止まり続ける(監視のテスト) -- [ ] AC5 / AC6: 誤検知の 12 形で止まらず、入力は実測の表の写しである(監視のテスト) -- [ ] AC7〜AC9: 起動できない CLI が「通らない」として返り、見つからない・時間切れの理由は変わらない(認証の確認のテスト) -- [ ] AC10 / AC11: 読めないディレクトリを含む PATH で CLI が 1 つ欠けても、2 つの開始の手順が終了コード 0 で終わる(開始の手順のテスト) -- [ ] 退行しないこと: 既存のテストがすべて通る(`uv run --with pytest pytest scripts/tests plugins/ndf -q`) - -## 代替案と採否 - -| 案 | 内容 | 採否 | 理由 | -| --- | --- | --- | --- | -| 行頭に固定した 2 行の照合 | 書き出しと種類の差し込みを 1 行で読む | 採用 | 設計の決定 1〜3。文書の引用・差分の行に一致しない | -| 形ごとに 1 行ずつ足す | 週・セッション・支出などを個別に書く | 不採用 | CLI が種類を増やすたびに照合が遅れる | -| 権限の例外だけを捕まえる | `PermissionError` に限って「通らない」とする | 不採用 | 実行形式でないファイルで同じ落ち方をする(決定 6) | - -## 不変条件 - -- 監視の結末の理由の語彙は `usage_limit` / `early_error` / `cli_timeout` / `missing` / `ok` のままである -- 認証の確認は例外を上げない。どの CLI の確認が失敗しても、残りの確認が続く - -## 互換性 - -| 対象 | 変更 | 互換性の扱い | -| --- | --- | --- | -| 公開インタフェース(監視の終了コード・標準出力のキー・開始の手順の引数) | 無し | 変えない | -| データ(状態ファイル・監視の結果ファイル) | 無し | 変えない | - -## 修正対象 - -- `plugins/ndf/scripts/lib/monitor.py` -- `plugins/ndf/scripts/lib/auth.py` -- `plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py` -- `plugins/ndf/scripts/tests/test_auth_probe.py` -- `plugins/ndf/skills/cross-review/tests/test_state_review_pool.py` -- `plugins/ndf/skills/cross-refactoring/tests/test_init.py` -- 配布物の同期(`bash scripts/build-runtime-plugins.sh` が揃える生成物) - -## タスク分解 - -### Task 1: 実物の文言を利用上限として読む - -- **対象ファイル:** `plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py`、`plugins/ndf/scripts/lib/monitor.py` -- **変更内容:** 設計の「実測」の実物 11 入力と誤検知 12 入力をテストの定数にし、利用上限の照合の表へ 2 行を足す。足す行は行頭で始まる形だけを読み、再試行の上限は最後の状態が 429 のときだけ一致する -- **満たす受け入れ条件:** AC1〜AC6 -- **進め方:** 失敗するテスト → 通す最小実装 → 整理 - -### Task 2: 起動できない確認コマンドを「通らない」として返す - -- **対象ファイル:** `plugins/ndf/scripts/tests/test_auth_probe.py`、`plugins/ndf/scripts/lib/auth.py` -- **変更内容:** 確認コマンドを 1 つ走らせる関数が起動できない例外を捕まえ、理由に起動できなかった理由を入れて返す。見つからない例外と時間切れの理由は変えない。テストは実際に権限を外した一時ディレクトリを PATH に置く形と、例外を差し込む形の両方を置く -- **満たす受け入れ条件:** AC7〜AC9 -- **進め方:** 失敗するテスト → 通す最小実装 → 整理 - -### Task 3: 読めない PATH でも 2 つの開始の手順が始まる - -- **対象ファイル:** `plugins/ndf/skills/cross-review/tests/test_state_review_pool.py`、`plugins/ndf/skills/cross-refactoring/tests/test_init.py` -- **変更内容:** 認証の確認を差し替えずに、読めないディレクトリを含む PATH と、CLI を 1 つ欠いた状態で開始の手順を通すテストを足す -- **満たす受け入れ条件:** AC10 / AC11 -- **進め方:** 失敗するテスト → 通す(Task 2 の実装で通る)→ 整理 - -## 影響範囲 - -- 収束ループ 2 つ(cross-review / cross-refactoring)の開始の手順と、担当の監視 -- 配布物(4 ランタイム分の生成物)は同期のコマンドで揃える - -## リスクと対処 - -| リスク | 対処 | -| --- | --- | -| 照合の追加で、文書やテストの本文を読み上げた行に一致する | 行頭に固定し、誤検知 12 形をテストの入力にする。触る範囲が狭くテストが厚いため、実装の後の構造改善で足りる | -| 権限を外したディレクトリが root では効かない | 例外を差し込むテストを併置し、どちらの実行者でも理由の文言を確かめる | -| 照合の表を写した配布物が古いまま残る | 同期のコマンドを実行し、差分をコミットに含める | - -## 切り戻し手順 - -- この Pull Request を revert すれば元へ戻る。データ移行も設定の変更も伴わない - -## 完了の定義 - -- [ ] AC1〜AC11 を満たし、条件ごとにテストが対応している -- [ ] 全体テストが通る(`uv run --with pytest pytest scripts/tests plugins/ndf -q`) -- [ ] 配布物の同期と Skill の frontmatter の検査が通る diff --git a/issues/issue-811-813-requirements.md b/issues/issue-811-813-requirements.md deleted file mode 100644 index 3c036e0c6..000000000 --- a/issues/issue-811-813-requirements.md +++ /dev/null @@ -1,132 +0,0 @@ -# cross-review / cross-refactoring: codex と claude が利用上限で止まっても同じラウンドで起動し直され、PATH に読めないディレクトリがあると始まらない → 上限を理由として報告して起動し直さず、CLI が欠けても使える者だけで始まる(#811 #813) - -## 目的 - -- **壊れていること:** codex と claude の利用上限の文言を監視が読まず、上限で止まった担当を同じラウンドで起動し直す(#811)。PATH に読めないディレクトリがあると、CLI が 1 つ欠けただけで開始の手順が落ちる(#813) -- **誰が困るか:** 収束ループ(cross-review / cross-refactoring)を回す利用者。上限は解けないため打ち切りまで待たされ、理由も出ない。読めない PATH を持つ環境では、ループが始まらない -- **直すと成り立つこと:** 実物の文言で止まった担当は理由「利用上限」で報告され、起動し直されない。確認コマンドを実行できない CLI は外され、使える者だけで始まる - -設計は [issue-811-813-design.md](issue-811-813-design.md) にある。この文書は「何を満たすか」だけを扱う。 - -## 用語の対応表 - -本文は左の業務用語で書く。識別子は表とコードブロックにだけ置く。 - -| 業務用語 | 識別子 | -| --- | --- | -| 監視 | `plugins/ndf/scripts/lib/monitor.py` | -| 利用上限の照合の表 | `USAGE_LIMIT_FATAL` | -| 標準エラーの記録 | 担当ごとの `-err.log` | -| 結末 | 起動 1 回の終わり方(`read_launch_outcome` が返す `LaunchOutcome`) | -| 理由「利用上限」 | `reason = "usage_limit"` | -| 起動し直しの可否 | `relaunch_same_agent` | -| 早期の致命 | 監視の状態 `EARLY_ERROR`(終了コード 4) | -| 認証の確認 | `plugins/ndf/scripts/lib/auth.py` の `probe_auth` | -| 確認コマンドを 1 つ走らせる関数 | `auth._run_probe` | -| 開始の手順 | cross-review の `state.py init`、cross-refactoring の `refactor.py init` | -| 実物 | CLI が実際に出した文言。手元の記録か、導入済みの CLI の実行ファイルに埋め込まれた文字列 | - -## なぜ要るか - -ndf 10.16.0 のリリース後テスト(PR #810 のコメント、2026-09-22)で 3 つの受け入れ条件が不合格になった。 - -| 課題 | 条件 | 不合格の中身 | 起票 | -| --- | --- | --- | --- | -| #729 / #619 | 利用上限で止まった担当が理由「利用上限」で報告され、起動し直されない | kiro と claude の JSON 出力は満たした。codex の文言 2 形式と claude のテキスト出力の文言 2 形式は理由が「結果ファイル無し」になり、同じラウンドで起動し直す | #811 | -| #478 | AC45 前半: CLI が 1 者欠けても開始の手順が終了コード 0 で終わり、使える者だけで 2 席を埋める | PATH の 13 番目にある読めないディレクトリ(`/root/.local/bin`)のため、確認コマンドが見つからないときに権限の例外が上がり、終了コード 1 で落ちた | #813 | - -**#729 の要求の時点で文言を集めたのは kiro と claude の JSON 出力だけだった**(10.16.0 の振り返り)。そのため、この束では文言を実物から集め、集めた経路と件数を設計の「実測」に残す。 - -## 対象範囲 - -含む: - -- 監視が読む利用上限の文言に、codex と claude の実物の形を足す(#811) -- 確認コマンドを実行できないときに、例外を上げず「通らない」として返す(#813) -- 上の 2 つを cross-review と cross-refactoring の両方の経路で確かめる - -含まない: - -- **実物が見つからない文言は足さない。** claude の旧い形(`Claude AI usage limit reached|<時刻>`)は、手元の記録に 0 件だった。導入済みの claude 2.1.278 の実行ファイルにも 0 件だった(設計の「実測」)。agy の利用上限の文言も記録に 0 件で、足さない -- claude の標準出力(JSON)の照合は変えない。JSON の結果行が上限の状態コードを持つことは実行ファイルで確かめてあり、10.16.0 のリリース後テストでも合格している -- 利用上限の後に担当を替えるかどうかの判断(Skill 側の既存の振る舞い)は変えない -- 理由の語彙・監視の終了コード・標準出力のキーは変えない -- 必須の外部コマンド(`git` / `gh`)の呼び出しの例外処理は変えない。無ければ動けないため、落ちる振る舞いのままでよい -- **設計の成果物のうち、対象が無いため省くもの:** システム構成図(外部の系・配置が変わらない)、クラス図(型を足さず変えない)、非機能設計表(非機能の条件が無い) - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| 公開インタフェース | 変わらない。監視の終了コード・標準出力のキー・結末の語彙はそのまま | -| データ | 変わらない。状態ファイル・監視の結果ファイルの形は同じ | -| 既存の振る舞い | 利用上限の照合の表に 2 行が増える。認証の確認が、起動できない CLI を例外ではなく「通らない」として返す | - -## 前提とする取り決め - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | `AGENTS.md`。監視と認証の確認は共通層(`plugins/ndf/scripts/lib/`)に置き、Skill 側へ写さない | -| コーディング規約 | `AGENTS.md` の「確かめる対象は外部コマンドに限らない」。照合の入力はこの束で集めた実物から作る | -| テスト戦略 | 照合の表は単体テスト(`plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py`)、認証の確認は単体テスト(`plugins/ndf/scripts/tests/test_auth_probe.py`)、2 つの開始の手順は既存の参加者の解決のテストに PATH の条件を足す | - -## 境界 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 全テストの実行。照合の文言を足すときは、出所(記録か実行ファイル)を設計の「実測」に書く | -| 確認してから行う | 監視の終了コード・理由の語彙を変えること | -| 行わない | 実物の無い文言を照合へ足すこと。必須の外部コマンドの例外処理を変えること | - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| テスト | `uv run --with pytest pytest scripts/tests plugins/ndf -q` | -| 文書の検査 | `python3 scripts/check-doc-line-limit.py`、`python3 scripts/check-markdown-links.py` | -| 手動確認 | AC10 / AC11 は、読めないディレクトリを先頭に置いた PATH で開始の手順を実際に実行する。AC12〜AC14 は 10.16.1 のリリース後テスト | - -## 受け入れ条件 - -### 利用上限の文言(#811) - -各条件の「利用上限として止まる」は、次の 3 つがそろうことを指す: 監視が担当を止めて早期の致命(終了コード 4)で終わる / 結末の理由が「利用上限」 / 起動し直しの可否が偽。 - -- [ ] AC1: 標準エラーの記録に codex の利用上限の文言の 1 行が出ると、利用上限として止まる。文言は `You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage …` である。行頭の `ERROR: ` の有無を問わない -- [ ] AC2: 標準エラーの記録に codex の再試行の上限の行が出て、最後の状態が 429 なら、利用上限として止まる。行は `exceeded retry limit, last status: 429` で始まる。最後の状態が 429 以外(例 `503 Service Unavailable`)なら利用上限にならない -- [ ] AC3: 標準エラーの記録に claude の利用上限の文言が行頭から出ると、利用上限として止まる。確かめる形は設計の「実測」の claude の 5 形(週・セッション・個人の支出・月の支出・期間を書かない形)である。JSON 出力で起動した claude がこの文言を標準エラーへ書くかは確かめていない。起動した claude の上限は AC4 の JSON の状態コードで読む -- [ ] AC4: 10.16.0 で合格した形は、引き続き利用上限として止まる。形は 4 つで、kiro の `Monthly request limit reached`、claude の JSON の上限の状態コード、HTTP 429 の状態行、`Quota exceeded` である -- [ ] AC5: AC1〜AC3 の文言が、表・バッククォート・「」・リスト・引用・grep 形式・Python の文字列・差分の追加行と文脈行・文の途中に出たときは、利用上限として止まらない -- [ ] AC6: AC1〜AC5 のテストの入力は、設計の「実測」の表にある実物の文言をそのまま写したものである(推測で作った文言を入力にしない) - -### 確認コマンドを実行できないとき(#813) - -- [ ] AC7: PATH に読めないディレクトリがあり、確認コマンドがどこにも無いとき、認証の確認は例外を上げず「通らない」を返す。理由は `コマンドを実行できません(Permission denied)` の形である -- [ ] AC8: 確認コマンドが実行形式でない(`Exec format error`)など、権限以外の理由で起動できないときも、AC7 と同じく「通らない」を返し、理由に起動できなかった理由を入れる -- [ ] AC9: 確認コマンドが見つからないときの理由(`コマンドが見つかりません`)と、時間切れのときの理由は変わらない -- [ ] AC10: AC7 の PATH で CLI を 1 つ欠いたとき、cross-review の開始の手順が終了コード 0 で終わり、欠けた CLI を外して使える者だけで席を埋める -- [ ] AC11: AC7 の PATH で CLI を 1 つ欠いたとき、cross-refactoring の開始の手順が終了コード 0 で終わり、欠けた CLI を外して使える者だけで始まる - -### 10.16.1 のリリース後テストで確かめ直す条件 - -**次の 3 行は、10.16.1 を配布した後に利用者の環境で確かめる。** 3 行とも合格したら #478 #619 #729 を閉じる。 - -- [ ] AC12(#478 の AC45 前半): 利用者の PATH から `kiro-cli` だけを隠し、検証用の Pull Request で cross-review の開始の手順を実行する。終了コード 0 で終わり、使える者だけで 2 席を埋める。PATH は読めない `/root/.local/bin` を含んだままにする -- [ ] AC13(#619 / #729): 導入先の監視に、設計の「実測」の codex の 2 形と claude の 5 形を 1 つずつ与えて結末を読む。すべて理由「利用上限」・起動し直しの可否が偽になる -- [ ] AC14(#619 / #729 の退行): 同じ手順で、10.16.0 で合格した kiro と claude の JSON の形も理由「利用上限」・起動し直しの可否が偽のままである - -## 依頼(原文) - -### #811 の本文(抜粋) - -> ndf 10.16.0(タグ `ndf--v10.16.0`)の監視は、**codex と claude(テキスト出力)の利用上限の文言を利用上限として読まない。** 担当が上限で止まると理由が `usage_limit` ではなく `missing` になり、`read_launch_outcome` は `relaunch_same_agent=True` を返す。つまり同じラウンドで起動し直す。 - -### #811 の追記 - -> 直すときは、各 CLI の実物の記録(`~/.codex/sessions` の `error.message`、`claude -p` のテキスト出力、kiro の err.log)から利用上限の文言を集め、テストの入力をその写しから作る。 - -### #813 の本文(抜粋) - -> PATH に**読めないディレクトリ**があると、参加者の CLI が入っていないときに `cross-review` の `init` が終了コード 0 ではなく、トレースバックを出して終了コード 1 で落ちる。#478 が直した「CLI が 1 者欠けても使える者だけで始まる」が、この PATH では成り立たない。 -> -> 直し方の候補は `except OSError` で捕まえて「コマンドを実行できません(<理由>)」として外すこと。 diff --git a/plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py b/plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py index 942693887..6bb11b5c9 100644 --- a/plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py +++ b/plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py @@ -62,7 +62,8 @@ ] -# 設計文書(issues/issue-811-813-design.md)の「実測」から写した実物の行(#811)。 +# 確定仕様(docs/specifications/cross-review-launch-outcome.md)の「背景」が出所を持つ +# 実物の行(#811)。 # 推測で作った文言は入れない。出所は記録(`~/.codex/sessions` / `~/.claude/projects`)と # 導入済みの実行ファイルの文字列である。 CODEX_USAGE_LIMIT = ("You've hit your usage limit. Visit " From a428f78d71b67821619703b47f2705123e70942d Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 20:27:15 +0000 Subject: [PATCH 19/22] =?UTF-8?q?Docs:=20=E3=83=AA=E3=83=AA=E3=83=BC?= =?UTF-8?q?=E3=82=B9=E5=BE=8C=E3=83=86=E3=82=B9=E3=83=88=E3=81=AE=E8=A6=B3?= =?UTF-8?q?=E7=82=B9=E3=81=A8=E5=AE=9F=E6=B8=AC=E3=81=AE=E5=86=8D=E7=8F=BE?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6=E3=82=92=E7=A2=BA=E5=AE=9A=E4=BB=95=E6=A7=98?= =?UTF-8?q?=E3=81=B8=E5=BC=95=E3=81=8D=E7=B6=99=E3=81=90=EF=BC=88#811=20#8?= =?UTF-8?q?13=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 10.16.1 の配布後に確かめる 3 つの観点を「テスト観点」へ追加 - 実測の表へ集めた経路の識別子・絞り込みの条件・対象期間を追加 - claude の 5 形の種類と件数の内訳を追加 - 逐語の文言の正本が照合のテストの入力であることを明記 Co-Authored-By: Claude Opus 5 (1M context) --- .../cross-review-launch-outcome.md | 47 +++++++++++++++---- .../tests/test_monitor_usage_limit.py | 5 +- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/docs/specifications/cross-review-launch-outcome.md b/docs/specifications/cross-review-launch-outcome.md index 37b1e10c9..b38ab8845 100644 --- a/docs/specifications/cross-review-launch-outcome.md +++ b/docs/specifications/cross-review-launch-outcome.md @@ -74,14 +74,30 @@ pid だけへシグナルを送っており、CLI が起こした子プロセス **codex と claude の上限の文言は、2026-09-22 に `develop`(90c0f06f、Python 3.14.4、 codex-cli 0.154.0、claude 2.1.278)で集めた実物から採った。** 出所と件数は次のとおりである。 -| CLI | 集めた経路 | 件数 | -| --- | --- | ---: | -| codex | 記録の誤りの本文のうち、種別が利用上限のもの(1 形) | 62 | -| codex | 収束ループのラウンドで上限に当たった標準エラーの記録(1 形。行頭に印が付く) | 2 | -| codex | 導入済みの実行ファイルに埋め込まれた文字列 | 6 形 | -| claude | 会話の記録のうち、API の誤りの印が真の本文(4 形) | 5,103 | -| claude | 導入済みの実行ファイルの文字列(期間を書かない形を含む) | 1 形 | -| kiro / agy | 手元の記録 | 0 | +| CLI | 集めた経路 | 絞り込みの条件 | 件数 | +| --- | --- | --- | ---: | +| codex | 記録(`~/.codex/sessions`)の誤りの本文(`error.message`) | 誤りの種別が `usage_limit_exceeded`。1 形 | 62 | +| codex | 収束ループのラウンドで上限に当たった標準エラーの記録(2026-09-22 16:35 UTC、132 行) | 行頭に `ERROR: ` の印が付く。1 形 | 2 | +| codex | 導入済みの実行ファイルに埋め込まれた文字列 | 書き出しが `You've hit your usage limit` または `exceeded retry limit` | 6 形 | +| claude | 会話の記録(`~/.claude/projects`)の本文 | API の誤りの印(`isApiErrorMessage`)が真。対象期間 2026-09-01〜22。4 形 | 5,103 | +| claude | 導入済みの実行ファイルに埋め込まれた文字列 | 期間を書かない形 | 1 形 | +| kiro / agy | 手元の記録(`~/.kiro` / `~/.gemini` / `antigravity-cli/cli.log`) | 利用上限の実物 | 0 | + +**claude の 5 形は、書き出しの後に差し込む期間と種類だけが違う。** 記録の 4 形と実行ファイルの +1 形の内訳は次のとおりである。 + +| 種類 | 記録の件数 | +| --- | ---: | +| 週 | 3,751 | +| セッション | 758 | +| 個人の支出 | 593 | +| 月の支出 | 1 | +| 期間を書かない形 | 0(出所は実行ファイルの文字列) | + +**逐語の文言の正本は、照合のテストの入力である** +(`plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py`)。この文書は出所と形の +種類だけを持ち、文言そのものを写さない。同じ文言を 2 か所に置くと、CLI が文言を変えたときに +片方だけが古くなり、どちらが実物かを読み手が決められなくなる。 **claude の旧い形(`Claude AI usage limit reached`)は、記録にも実行ファイルにも 0 件である。** 出所が無いため照合の表に置かない。記録に現れたら、同じ手順で出所を書いて足す。 @@ -274,6 +290,21 @@ cross-refactoring の取り込みが従う契約を、ここで定める。**契 | 文書の分量が分割の基準を超えないこと | `python3 scripts/check-doc-line-limit.py` | | 参照のリンクが解決できること | `python3 scripts/check-markdown-links.py` | +### リリース後テストで確かめる観点 + +**次の 3 つは、10.16.1 を配布した利用者の環境で確かめる。** 上の観点は監視へ文言を与えて +結末を読むが、配布物を導入した先で同じ結末になることと、利用者の検索のパスで開始の手順が +通ることは、配布した版を使う環境でしか確かめられない。 + +| 観点 | 確かめ方 | +| --- | --- | +| CLI が 1 者欠けても、開始の手順が終了コード 0 で終わり、使える者だけで 2 席を埋めること | 利用者の検索のパス(`PATH`)から `kiro-cli` だけを隠し、読めない `/root/.local/bin` を含んだまま、検証用の Pull Request でクロスレビューの開始の手順を実行する。席の決め方は[参加者と席](cross-review-participants-and-seats.md)にある | +| 導入先の監視で、codex の 2 形と claude の 5 形が理由「利用上限」・起動し直しの可否が偽になること | 導入先の監視へ実物の行を 1 つずつ標準エラーの記録として与え、結末を読む | +| 10.16.0 で合格した kiro の 1 行と claude の JSON の上限の状態コードが、同じ理由・同じ可否のままであること | 同じ手順で、2 つの形を 1 つずつ与えて結末を読む | + +**JSON の形で起動した claude が上限のときに標準エラーの記録へ書く文言も、このときに確かめる。** +手元の記録には 0 件のため、照合の表には足していない(「背景」)。 + ## 関連リンク - [issue #729](https://github.com/devbasex/ai-plugins/issues/729) — 結果なしの判断を共通層へ移す diff --git a/plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py b/plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py index 6bb11b5c9..d10f26d46 100644 --- a/plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py +++ b/plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py @@ -62,8 +62,9 @@ ] -# 確定仕様(docs/specifications/cross-review-launch-outcome.md)の「背景」が出所を持つ -# 実物の行(#811)。 +# 実物の行の逐語の正本(#811)。確定仕様 +# (docs/specifications/cross-review-launch-outcome.md)の「背景」は出所と形の種類だけを持ち、 +# 文言そのものはここにある。 # 推測で作った文言は入れない。出所は記録(`~/.codex/sessions` / `~/.claude/projects`)と # 導入済みの実行ファイルの文字列である。 CODEX_USAGE_LIMIT = ("You've hit your usage limit. Visit " From 2567f3677f49b9cd152d8c1baa50f8c155dfa655 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 20:37:29 +0000 Subject: [PATCH 20/22] =?UTF-8?q?Docs:=20=E4=B8=8A=E9=99=90=E3=81=AE?= =?UTF-8?q?=E7=85=A7=E5=90=88=E3=81=AE=E8=A1=A8=E8=A8=98=E3=81=A8=E7=B2=92?= =?UTF-8?q?=E5=BA=A6=E3=80=81=E3=83=86=E3=82=B9=E3=83=88=E8=A6=B3=E7=82=B9?= =?UTF-8?q?=E3=81=AE=E7=B5=8C=E8=B7=AF=E3=82=92=E5=AE=9F=E8=A3=85=E3=81=B8?= =?UTF-8?q?=E6=8F=83=E3=81=88=E3=82=8B=EF=BC=88#811=20#813=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 選択肢を持つ 2 つの正規表現を表のセルからコードブロックへ移し、退避した縦棒を無くす - 背景の表の codex 6 形に、照合では 2 形へ集約されることを添える - テスト観点を、照合の関数を直接呼ぶ経路と監視のプロセスを通す経路に書き分ける Co-Authored-By: Claude Opus 5 (1M context) --- .../cross-review-launch-outcome.md | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/docs/specifications/cross-review-launch-outcome.md b/docs/specifications/cross-review-launch-outcome.md index b38ab8845..dc849449c 100644 --- a/docs/specifications/cross-review-launch-outcome.md +++ b/docs/specifications/cross-review-launch-outcome.md @@ -83,6 +83,10 @@ codex-cli 0.154.0、claude 2.1.278)で集めた実物から採った。** 出 | claude | 導入済みの実行ファイルに埋め込まれた文字列 | 期間を書かない形 | 1 形 | | kiro / agy | 手元の記録(`~/.kiro` / `~/.gemini` / `antigravity-cli/cli.log`) | 利用上限の実物 | 0 | +**codex の 6 形は、照合では 2 形(利用上限・再試行の上限)に集約される。** 実行ファイルの +6 形は、書き出しに続く案内文の違いまで数えた文字列の数である。照合が読むのは書き出しの +2 通りだけで、続く案内文は読まない(「上限の検知」)。 + **claude の 5 形は、書き出しの後に差し込む期間と種類だけが違う。** 記録の 4 形と実行ファイルの 1 形の内訳は次のとおりである。 @@ -188,12 +192,28 @@ codex-cli 0.154.0、claude 2.1.278)で集めた実物から採った。** 出 | `usage_limit` | 標準エラーの記録(全担当) | 生きている間の巡回ごと | `Monthly request limit reached` | | `usage_limit` | 同上 | 同上 | `"api_error_status"\s*:\s*429` | | `usage_limit` | 同上 | 同上 | `quota exceeded` / `rate limit exceeded`(大文字小文字を問わない)、`^HTTP/\d\S* 429 ` | -| `usage_limit` | 同上 | 同上 | `^(?:ERROR:\s*)?You['’]ve hit your (?:[\w'’ ]+ )?(?:limit\|budget)\b`(codex と claude の上限。期間や種類を差し込む形を 1 行で覆う) | -| `usage_limit` | 同上 | 同上 | `^(?:ERROR:\s*)?exceeded retry limit, last status: 429\b`(codex の再試行の上限) | +| `usage_limit` | 同上 | 同上 | 下の**利用上限の行**(codex と claude。期間や種類を差し込む形を 1 行で覆う) | +| `usage_limit` | 同上 | 同上 | 下の**再試行の上限の行**(codex) | | `usage_limit` | claude の標準出力の記録 | 同上 | `"api_error_status"\s*:\s*429` | -| `early_error` | 標準エラーの記録 | 同上 | `^HTTP/\d\S* (?:401\|403) ` と、残りの既存の致命 | +| `early_error` | 標準エラーの記録 | 同上 | `^HTTP/\d\S* 401 ` / `^HTTP/\d\S* 403 ` と、残りの既存の致命 | | `cli_timeout` | 標準エラーの記録 | **終了した後、結果ファイルが無いときだけ** | `print timeout after \S+ with turn in progress` | +**選択肢を持つ 2 行は、表の外に置く。** 表のセルに入れると縦棒を退避する必要があり、退避した +縦棒は選択肢の区切りではなくリテラルの縦棒として読める。次の 2 行は実装 +(`plugins/ndf/scripts/lib/monitor.py`)の文字列と 1 字ずつ一致する。 + +**利用上限の行**(codex と claude): + +```text +^(?:ERROR:\s*)?You['’]ve hit your (?:[\w'’ ]+ )?(?:limit|budget)\b +``` + +**再試行の上限の行**(codex): + +```text +^(?:ERROR:\s*)?exceeded retry limit, last status: 429\b +``` + **codex と claude の上限の 2 行は、行頭で始まる形だけを読む。** 担当は作業中のコマンドの 出力(差分・ファイルの中身)も標準エラーの記録へ書くため、文言を含む文書やテストを読み 上げた行が記録に入る。行頭に固定すると、文の途中・差分の行・字下げした文字列は一致しない。 @@ -281,7 +301,8 @@ cross-refactoring の取り込みが従う契約を、ここで定める。**契 | 観点 | 確かめ方 | | --- | --- | | 理由の語彙と起動し直しの可否が 1 か所にあり、結末を読む関数が失敗しないこと | `plugins/ndf/scripts/tests/test_monitor_outcome_unit.py` | -| 利用上限の文言を検知し、引用・表・grep 形式・差分の行で誤検知しないこと。実物の行を 1 つずつ標準エラーの記録へ書くと、理由が利用上限・起動し直しの可否が偽・終了コード 4 になること。再試行の上限の 503 の行が利用上限にならないこと | `plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py` | +| 実物の行(codex の 2 形を行頭の印の有無で 3 行、claude の 5 形)が利用上限の照合に一致し、表・バッククォート・引用・grep 形式・差分・文の途中の行が一致しないこと。照合の関数を直接呼んで確かめる | `plugins/ndf/skills/cross-review/tests/test_monitor_usage_limit.py` | +| 監視のプロセスを通したとき、codex の 3 行(2 形と行頭の印あり)と claude の週・セッションの 2 行で理由が利用上限・終了コード 4 になること。再試行の上限の 503 の行と、引用・差分の行では止まらないこと | 同上 | | CLI の上限の文言を、終了して結果ファイルが無いときだけ理由にすること | 同 `tests/test_launch_print_timeout.py` | | CLI が独立したプロセスグループで起動し、グループごと止まること | 同 `tests/test_launch_cli_process_group.py` | | 結果の取り込みが理由と監視の詳細を残し、終了コードを変えないこと | 同 `tests/test_read_result_reason.py` | From 0f078373f63a3d9e9135e4fda1975a0b9ccb7781 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 20:50:14 +0000 Subject: [PATCH 21/22] Release: ndf v10.16.1-dev.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #811 #813 の修正を develop のチャネルへ載せる開発版。版数を持つ 15 箇所、 更新案内の本文と手元で確かめるコマンド、CHANGELOG.md の ndf 10.16.1 の節、 docs/ndf-version-decisions.md の v10.16.1 の判断を更新する。 Co-Authored-By: Claude Opus 5 (1M context) --- .claude-plugin/marketplace.json | 2 +- AGENTS.md | 2 +- CHANGELOG.md | 20 +++++++++++ README.md | 4 +-- docs/ndf-version-decisions.md | 21 +++++++++++- docs/versioning-and-distribution.md | 4 +-- plugins/ndf/.claude-plugin/plugin.json | 4 +-- plugins/ndf/.codex-plugin/plugin.json | 4 +-- plugins/ndf/README.md | 46 ++++++++++---------------- plugins/ndf/dev.agy/plugin.json | 4 +-- 10 files changed, 69 insertions(+), 42 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index cb96c3f51..fc88b612a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "ndf", "source": "./plugins/ndf", - "description": "Claude Code plugin (v10.16.0): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", + "description": "Claude Code plugin (v10.16.1-dev.1): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" diff --git a/AGENTS.md b/AGENTS.md index 2ed45aa94..5f2ab9473 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -126,7 +126,7 @@ ai-plugins/ ## NDFプラグインについて -**NDFプラグイン**は、このマーケットプレイスの主要プラグインです(v10.16.0)。plugin 名は全ランタイムで `ndf` を維持し、配布物は `plugins/ndf/` の1ディレクトリにまとまっています。 +**NDFプラグイン**は、このマーケットプレイスの主要プラグインです(v10.16.1-dev.1)。plugin 名は全ランタイムで `ndf` を維持し、配布物は `plugins/ndf/` の1ディレクトリにまとまっています。 - Skill の実体は `plugins/ndf/skills/` の1箇所。配布先は `plugins/ndf/manifests/*-skills.txt` が決める - Claude Code版は 8個の専門サブエージェント、公開Skills、PreToolUse/SessionStart/Stopフックを提供 - Codex版は Codex向け公開Skillsと任意Slack通知hookを提供 diff --git a/CHANGELOG.md b/CHANGELOG.md index 376f8c516..88becbe02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,26 @@ **開発版(接尾辞の付いた版)は載せない。** `9.8.0` は `9.8.0-dev.1` までしか出ておらず、 その内容は `10.0.0` で届いている。 +## [ndf 10.16.1] - 2026-09-22 + +### 修正 + +- **codex と claude の利用上限の実物の文言を、監視の照合の表へ足した**(#811)。 + `^(?:ERROR:\s*)?You['’]ve hit your (?:[\w'’ ]+ )?(?:limit|budget)\b` と + `^(?:ERROR:\s*)?exceeded retry limit, last status: 429\b` の 2 行で、codex の 2 形と claude の 5 形に + 一致する。結末の理由が「結果ファイル無し」ではなく「利用上限」になり、同じラウンドで起動し直さない。 + **行頭で始まる形だけを読む**ため、担当が差分・文書・テストを読み上げた行では止まらない。再試行の上限は + 最後の状態が 429 のときだけ利用上限と読む(503 などの一時的な誤りは起動し直せば解けうる) +- **認証の確認が、確認コマンドを起動できない理由を問わず「通らない」として返すようにした**(#813)。 + `PATH` に読めないディレクトリがあると、コマンドがどこにも無いときに見つからない例外ではなく権限の例外が + 上がり、`cross-review` と `cross-refactoring` の開始の手順ごと落ちていた。実行形式でないファイルも + 同じ形で落ちる。どちらも理由 `コマンドを実行できません(<理由>)` として返り、使える者だけで始まる + +### 変更 + +- 収束ループの共通層の内部構造を整理した(`lib/result_posts.py` の投稿の組み立て、`lib/run_metrics.py` の + 集計、`lib/transcript_agents.py` の記録の読み取り)。振る舞いは変えていない + ## [ndf 10.16.0] - 2026-09-22 ### 追加 diff --git a/README.md b/README.md index f861f3bdb..b567e8313 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Claude Code / Codex / Kiro CLI / agy 向けのスキル・MCP設定を共有す このマーケットプレイスは、チーム全体でAI開発ツール(Claude Code / Codex / Kiro CLI / agy)の導入を加速するための事前設定されたプラグインを提供します。 -**NDFプラグイン v10.16.0** は、同じ `ndf@ai-plugins` という名前で Claude Code / Codex / Kiro CLI / agy へ配布されるプラグインです。配布物は `plugins/ndf/` の1ディレクトリにまとまっており、Skill の実体は `plugins/ndf/skills/` の1箇所だけです。どのランタイムへ配るかは `plugins/ndf/manifests/*-skills.txt` が決めます。 +**NDFプラグイン v10.16.1-dev.1** は、同じ `ndf@ai-plugins` という名前で Claude Code / Codex / Kiro CLI / agy へ配布されるプラグインです。配布物は `plugins/ndf/` の1ディレクトリにまとまっており、Skill の実体は `plugins/ndf/skills/` の1箇所だけです。どのランタイムへ配るかは `plugins/ndf/manifests/*-skills.txt` が決めます。 - **公開Skills**: Claude Code向け core 45個、Kiro向け core 44個、Codex向け core 43個、agy向け core 43個に分離。 - **元Skills(45個)**: @@ -110,7 +110,7 @@ hook を効かせる手順と、新しい版へ入れ替える手順は | プラグイン名 | バージョン | 説明 | 詳細 | |------------|----------|------|------| -| **ndf** | 10.16.0 | Claude Code / Codex / Kiro CLI / agy へ 1 ディレクトリから配布する NDF プラグイン。8個の専門エージェント(Claude版)、公開Skills(Claude Code向け core 45個、Kiro向け core 44個、Codex向け core 43個、agy向け core 43個)、4ランタイム共通の作業ツリー運用フック(PreToolUse / SessionStart / userPromptSubmit / agentSpawn / PreInvocation)、Claude Stopフック、Codex/Kiro向け通知・実行補助を提供。v4.0.0 で Codex MCP サーバを廃止し、`/ndf:external-ai` skill + `corder` エージェント経由の CLI 直接実行に一本化。 | [README](./plugins/ndf/README.md) | +| **ndf** | 10.16.1-dev.1 | Claude Code / Codex / Kiro CLI / agy へ 1 ディレクトリから配布する NDF プラグイン。8個の専門エージェント(Claude版)、公開Skills(Claude Code向け core 45個、Kiro向け core 44個、Codex向け core 43個、agy向け core 43個)、4ランタイム共通の作業ツリー運用フック(PreToolUse / SessionStart / userPromptSubmit / agentSpawn / PreInvocation)、Claude Stopフック、Codex/Kiro向け通知・実行補助を提供。v4.0.0 で Codex MCP サーバを廃止し、`/ndf:external-ai` skill + `corder` エージェント経由の CLI 直接実行に一本化。 | [README](./plugins/ndf/README.md) | | **playwright-kit** | 2.0.3 | Playwright による E2E テストの計画・実装・証跡管理を提供するプラグイン。ページ役割からのテスト計画、動画 / trace 付きスクリプト実装、レポート生成と Drive 保管、playwright_kit ランタイム(init、a11y / CWV スキャン)の 4 Skill。NDF v7.0.0 で分離。 | [README](./plugins/playwright-kit/README.md) | ### 変更履歴 diff --git a/docs/ndf-version-decisions.md b/docs/ndf-version-decisions.md index 06f23abd4..512711f55 100644 --- a/docs/ndf-version-decisions.md +++ b/docs/ndf-version-decisions.md @@ -1,4 +1,4 @@ -# NDF の版ごとの決定と理由(v10.12.0〜v10.16.0) +# NDF の版ごとの決定と理由(v10.12.0〜v10.16.1) `CLAUDE.md` から移した、出た版の記録である。**その版で何を決め、なぜそう決めたか**を残す。 変更点の列挙は `CHANGELOG.md` にあり、こちらは判断の理由を持つ。**`CLAUDE.md` へ書くのは @@ -238,3 +238,22 @@ agy の 7 回だけで、提案の所要の中央値も agy が最も長かっ **テストの実行中は `MONITOR_` で始まる環境変数を、根の `conftest.py` の 1 か所で外す**(#678)。 名前を並べずに接頭辞で一致させるのは、上限の種類が増えるたびに一覧へ足し忘れる形を作らないためで、 収集より前に外すのは、テストの本体を読み込む時点で上限を決める実装があるためである。 + +v10.16.1 で 10.16.0 のリリース後テストの不合格 2 件を塞いだ(マイルストーン 17「10.16.0 の +リリース後テストで不合格だった条件」、#811 #813)。 + +**照合の表へ足す文言は、実物に出所を持つものだけにする**(#811)。記録(CLI の会話と収束ループの +標準エラー)か、導入済みの実行ファイルに埋め込まれた文字列に当たるものだけを足した。出所の無い行は +合っているかをテストで確かめられず、**書いた側の思い込みがテストの期待値にも入って通ってしまう**。 +claude の旧い形は記録にも実行ファイルにも 0 件のため足していない。**足す文言は行頭で始まる形に +固定する** ── 担当は作業中のコマンドの出力(差分・ファイルの中身)も標準エラーへ書くため、文言を +含む文書やテストを読み上げた行に一致させない。見逃しは 1 度起動し直すだけで済むが、誤検知は結果を +書ける担当を止める。**期間や種類は 1 行の照合で覆う** ── 形ごとに行を足すと、CLI が種類を増やす +たびに照合が遅れる。再試行の上限は最後の状態が 429 のときだけ利用上限と読む ── 同じ文言は 503 +などの一時的な誤りでも出て、そちらは起動し直せば解けうる。 + +**確認コマンドを起動できないときは、例外の種類を問わず「通らない」として返す**(#813)。権限の +拒否だけを足す形では、実行形式でないファイルで同じ落ち方が残る。**読めない検索のパスで見つからない +ときも、理由は「実行できない」として出す** ── Python の起動は「見つからない」と「実行権が無い」を +同じ例外で返し、見分けるには検索のパスを自分で辿り直すことになって、確認を 1 つ走らせる関数の範囲を +超える。理由に権限の拒否が出れば、利用者は検索のパスとファイルの権限を見ればよい。 diff --git a/docs/versioning-and-distribution.md b/docs/versioning-and-distribution.md index 00b9a7527..e4432a166 100644 --- a/docs/versioning-and-distribution.md +++ b/docs/versioning-and-distribution.md @@ -57,11 +57,11 @@ semver の順序で除外されるのは、プラグイン間の依存解決(` | 版 | 形 | 意味 | | --- | --- | --- | -| 正式版 | `10.16.0` | 利用者が常用してよい | +| 正式版 | `10.16.1` | 利用者が常用してよい | | 開発版 | `10.17.0-dev.1` | 検証中。入れたくない利用者は取得を控えられる | | 公開前の確認版 | `10.17.0-rc.1` | 正式版の候補。残るのは確認だけ | -- 接尾辞は**次に出す正式版の版数へ付ける**。`10.16.0` の次を開発するなら `10.17.0-dev.1` +- 接尾辞は**次に出す正式版の版数へ付ける**。`10.16.1` の次を開発するなら `10.17.0-dev.1` - 連番は開発版を出すたびに増やす。**同じ版数で中身を差し替えない**。差し替えると、利用者の 手元にある版と `main` の版が同じ番号で別物になり、何を確かめたのかが分からなくなる - **正式版を出すときは接尾辞を外す。** `10.17.0-dev.3` の次は `10.17.0` diff --git a/plugins/ndf/.claude-plugin/plugin.json b/plugins/ndf/.claude-plugin/plugin.json index 292dc6107..a34f70b2a 100644 --- a/plugins/ndf/.claude-plugin/plugin.json +++ b/plugins/ndf/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ndf", - "version": "10.16.0", - "description": "Claude Code plugin (v10.16.0): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", + "version": "10.16.1-dev.1", + "description": "Claude Code plugin (v10.16.1-dev.1): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", "author": { "name": "takemi-ohama", "url": "https://github.com/takemi-ohama" diff --git a/plugins/ndf/.codex-plugin/plugin.json b/plugins/ndf/.codex-plugin/plugin.json index 8882ae580..88f5e2bc8 100644 --- a/plugins/ndf/.codex-plugin/plugin.json +++ b/plugins/ndf/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ndf", - "version": "10.16.0", - "description": "Codex plugin (v10.16.0): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation (Codex/agy), and optional Slack completion notifications.", + "version": "10.16.1-dev.1", + "description": "Codex plugin (v10.16.1-dev.1): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation (Codex/agy), and optional Slack completion notifications.", "skills": [ "./skills/cherry-pick-pr", "./skills/cross-refactoring", diff --git a/plugins/ndf/README.md b/plugins/ndf/README.md index 1d209dda9..bcfc1e831 100644 --- a/plugins/ndf/README.md +++ b/plugins/ndf/README.md @@ -89,7 +89,7 @@ bash plugins/ndf/dev.kiro/install.sh --dry-run ```bash python3 -c "import json;print(json.load(open('.kiro/agents/ndf.json'))['description'])" -# => NDF統合開発エージェント(Kiro CLI用 / v10.16.0) +# => NDF統合開発エージェント(Kiro CLI用 / v10.16.1-dev.1) ``` ### agy @@ -119,32 +119,21 @@ agy plugin list # => {"imports":[{"name":"ndf","source":"antigravity","components":["skills","agents","hooks"]}]} ``` -## v10.16.0 へ更新するとき +## v10.16.1-dev.1 へ更新するとき -**`cross-review` と `cross-refactoring` の収束ループが、担当が揃わない・上限で止まる・結果を -残さないときにも止まらず終わるようにしました**(マイルストーン 13「agy の打ち切りと止まらない -収束ループ」、#478 #553 #583 #584 #592 #619 #624 #647 #648 #664 #678 #687 #706 #727 #728 #729 -#730 #732 #736)。Skill の数は変わりません。引数・Skill・スクリプトの削除や改名は無く、記録の -移行も要りません(前の版で始めた状態ファイルはそのまま読めます)。変更点の一覧は -[CHANGELOG.md](../../CHANGELOG.md) にあります。 +**収束ループが、codex と claude の利用上限で止まった担当を起動し直さなくなり、読めない +ディレクトリを含む `PATH` でも始まるようにしました**(マイルストーン 17「10.16.0 のリリース後 +テストで不合格だった条件」、#811 #813)。Skill の数は変わりません。引数・Skill・スクリプトの +削除や改名は無く、記録の移行も要りません(前の版で始めた状態ファイルはそのまま読めます)。 +変更点の一覧は [CHANGELOG.md](../../CHANGELOG.md) にあります。 -**正式版です。** `main` に載ります。開発版 `10.16.0-dev.1` の中身に、statusline の変更(#806)を -加えました。statusline の変更は開発版を経ていません。 - -**`cross-refactoring` の既定の参加者から agy が外れます。** 既定は codex / kiro とホストです。 -これまでどおり agy に提案と適用をさせるなら `--include agy` を渡します。 +**開発版です。** `develop` にだけ載ります。取得元へ `#develop` を足す手順は +[docs/versioning-and-distribution.md の「開発版を試す」](../../docs/versioning-and-distribution.md#開発版を試す)にあります。 | 変わったこと | 中身 | | --- | --- | -| **使える者だけで始まります**(#478 #727 #664 #687) | 認証の確認を通らない CLI があっても `init` は止まらず、その者を外して続けます。外した者と理由は状態ファイルと完了報告に残ります。全員が揃わないなら始めたくないときは `--require-all` を付けます。`cross-review` は毎ラウンド 2 席を確保し、足りなければホスト、次に同じランタイムの 2 つ目(`codex-2` など)で埋めます | -| **参加者を名指しで変えられます**(#664) | 両 Skill に `--exclude` / `--include` が増えました(カンマ区切り・繰り返し可)。`cross-refactoring` は提案と適用を同じ参加者で回し、レビュー担当の役を無くしました | -| **再開で渡した引数が効きます**(#648) | 中断した収束ループを引数を変えて再開すると、上限は反映され、反映しない引数は「反映しない」と表示されます。黙って捨てられる引数はありません。指定を外すときは `none` を渡します | -| **利用上限を理由として報告します**(#729 #619 #584) | 担当の CLI が利用上限で止まると「結果なし」ではなく理由「利用上限」として残り、同じラウンドで起動し直しません。監視が止めた担当の子プロセスは、止めた後に結果を書きません | -| **未解決の重大な指摘を残して承認で終わりません**(#732 #624 #706) | 収束の判定で数えないのは、棄却した指摘と `minor` 以下の指摘だけになりました。誤りを示されていない `major` 以上は残る指摘として数えます | -| **GitHub と git へ書くのはレビューを回す側だけです**(#730 #583) | レビューの担当は指摘の控えを書くだけで、投稿は取り込み(`state.py read-result`)が行います。修正の担当はコミットまでで、送信・返信・決着・まとめは `state.py merge-fix` が行います。同じ論点が 2 つのスレッドに分かれず、途中で止まってもやり直しで二度書きません。**`/ndf:fix` を単独で使うときは、最後に `lib/result_posts.py fix` の 1 行で送信と返信を行います**(手順は `fix` の SKILL.md にあります) | -| **適用ラウンドが上限なしに開き直されません**(#728 #647 #592 #553) | 実装担当が結果を残さないと未検証のコミットを取り消し、同じ適用ラウンドは別の担当で 2 回まで試します。採用 0 件のラウンドでは担当を起動しません。帰属の段落が後ろに付いたコミットでも必須の記名を読みます | -| **statusline にサブエージェントの使用量が並びます**(#806) | Claude Code の NDF 標準 statusline が、実行中のサブエージェントのコンテキスト使用量を多い順に 3 本まで並べます(残りは `+2` のように本数だけ)。500k(Haiku 4.5 は 150k)を超えると赤になります。メインの表示から上限・使用率・コンテナ名・ホスト名を外しました。NDF 標準の statusLine には `refreshInterval: 5` が足されます(利用者が書いた値は変えません) | -| テストが監視の環境変数に左右されません(#678) | `MONITOR_` で始まる環境変数を延ばしたシェルから全体のテストを起動しても、同じ件数が通ります | +| **codex と claude の利用上限を理由として報告します**(#811) | 担当が利用上限で止まったときの実物の文言(codex の 2 形・claude の 5 形)を照合に足しました。理由が「結果ファイル無し」ではなく「利用上限」になり、同じラウンドで起動し直しません。行頭で始まる行だけを読むため、担当が差分や文書を読み上げた行では止まりません | +| **読めないディレクトリを含む `PATH` でも始まります**(#813) | 認証の確認が、確認コマンドを起動できない理由(権限の拒否・実行形式でないファイル)を「通らない」として返します。`PATH` に読めないディレクトリがあり、CLI が 1 者欠けている環境でも、2 つの開始の手順は終了コード 0 で終わり、使える者だけで始まります | 正式版のチャネル(ref を指定せずに登録した取得元)なら、次で入れ替わります。**動いているセッションには 反映されない**ため、更新したあとは起動し直してください。開発版を試すために `develop` を登録した @@ -161,15 +150,14 @@ codex plugin add ndf@ai-plugins ### 手元で確かめる -どれも `--help` を読むだけで、課題もファイルも書き換えません。`$SCRIPTS` はプラグインの +どれもファイルを読むだけで、課題もファイルも書き換えません。`$SCRIPTS` はプラグインの `scripts/` の絶対パスで、決め方は [development-workflow/references/scripts-lookup.md](skills/development-workflow/references/scripts-lookup.md) にあります。 ```bash -python3 "$SCRIPTS/lib/result_posts.py" fix --help >/dev/null; echo "exit=$?" # 0 なら修正の送信を行う共通層が入っている -python3 "$SCRIPTS/../skills/cross-review/scripts/state.py" init --help | grep -q -- '--require-all'; echo "exit=$?" # 0 なら cross-review が使える者だけで始まる -python3 "$SCRIPTS/../skills/cross-refactoring/scripts/refactor.py" init --help | grep -q -- '--include'; echo "exit=$?" # 0 なら cross-refactoring の参加者を名指しで変えられる +grep -q "You\['’\]ve hit your" "$SCRIPTS/lib/monitor.py"; echo "exit=$?" # 0 なら codex と claude の上限の文言を読む +grep -q 'except OSError' "$SCRIPTS/lib/auth.py"; echo "exit=$?" # 0 なら起動できない確認コマンドで落ちない ``` ## Playwright テストについて @@ -313,7 +301,7 @@ agy models # 認証の確認 ```text # 動く: 実体パスを示して読ませる -~/.codex/plugins/cache/ai-plugins/ndf/10.16.0/skills/deploy/SKILL.md を読んで、その手順どおりに qa/staging へ deploy PR を作成してください。 +~/.codex/plugins/cache/ai-plugins/ndf/10.16.1-dev.1/skills/deploy/SKILL.md を読んで、その手順どおりに qa/staging へ deploy PR を作成してください。 # 動かない: 明示起動 ($ は展開されない) $deploy qa/staging @@ -335,14 +323,14 @@ marketplace 経由でインストールした場合、Skill の実体は **ワ ```text $CODEX_HOME/plugins/cache////skills//SKILL.md # 既定 ($CODEX_HOME=~/.codex) の例: -# ~/.codex/plugins/cache/ai-plugins/ndf/10.16.0/skills/deploy/SKILL.md +# ~/.codex/plugins/cache/ai-plugins/ndf/10.16.1-dev.1/skills/deploy/SKILL.md ``` そのため「`deploy` の SKILL.md を探して読んで」のような曖昧な依頼は、Codex のファイル探索がワークスペース内に限られる状況では失敗しえます。**抑止した Skill は `$` が展開されない**ので、`codex plugin list` で実体パスを確認し、絶対パスを渡してください。 ```bash codex plugin list | grep 'ndf@ai-plugins' -# => ndf@ai-plugins installed, enabled 10.16.0 +# => ndf@ai-plugins installed, enabled 10.16.1-dev.1 ``` 抑止していない Skill(`markdown-writing` など)はキャッシュ配下でも `$` で解決するため、そちらは `$` 起動が使えます。 diff --git a/plugins/ndf/dev.agy/plugin.json b/plugins/ndf/dev.agy/plugin.json index b371a4545..144b8a8ff 100644 --- a/plugins/ndf/dev.agy/plugin.json +++ b/plugins/ndf/dev.agy/plugin.json @@ -1,5 +1,5 @@ { "name": "ndf", - "version": "10.16.0", - "description": "Antigravity CLI plugin (v10.16.0): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation, and worktree guidance hooks." + "version": "10.16.1-dev.1", + "description": "Antigravity CLI plugin (v10.16.1-dev.1): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation, and worktree guidance hooks." } From 254a52328cb63007252fc0c0cee82b1ae32272f7 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 21:29:23 +0000 Subject: [PATCH 22/22] Release: ndf v10.16.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 開発版 10.16.1-dev.1 の接尾辞を外し、正式版 10.16.1 として `main` へ配る。 更新案内の本文を正式版の記述へ直した。 Co-Authored-By: Claude Opus 5 (1M context) --- .claude-plugin/marketplace.json | 2 +- AGENTS.md | 2 +- README.md | 4 ++-- plugins/ndf/.claude-plugin/plugin.json | 4 ++-- plugins/ndf/.codex-plugin/plugin.json | 4 ++-- plugins/ndf/README.md | 14 +++++++------- plugins/ndf/dev.agy/plugin.json | 4 ++-- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index fc88b612a..331f0fd6e 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "ndf", "source": "./plugins/ndf", - "description": "Claude Code plugin (v10.16.1-dev.1): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", + "description": "Claude Code plugin (v10.16.1): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" diff --git a/AGENTS.md b/AGENTS.md index 5f2ab9473..9532ad999 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -126,7 +126,7 @@ ai-plugins/ ## NDFプラグインについて -**NDFプラグイン**は、このマーケットプレイスの主要プラグインです(v10.16.1-dev.1)。plugin 名は全ランタイムで `ndf` を維持し、配布物は `plugins/ndf/` の1ディレクトリにまとまっています。 +**NDFプラグイン**は、このマーケットプレイスの主要プラグインです(v10.16.1)。plugin 名は全ランタイムで `ndf` を維持し、配布物は `plugins/ndf/` の1ディレクトリにまとまっています。 - Skill の実体は `plugins/ndf/skills/` の1箇所。配布先は `plugins/ndf/manifests/*-skills.txt` が決める - Claude Code版は 8個の専門サブエージェント、公開Skills、PreToolUse/SessionStart/Stopフックを提供 - Codex版は Codex向け公開Skillsと任意Slack通知hookを提供 diff --git a/README.md b/README.md index b567e8313..ac2b45696 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Claude Code / Codex / Kiro CLI / agy 向けのスキル・MCP設定を共有す このマーケットプレイスは、チーム全体でAI開発ツール(Claude Code / Codex / Kiro CLI / agy)の導入を加速するための事前設定されたプラグインを提供します。 -**NDFプラグイン v10.16.1-dev.1** は、同じ `ndf@ai-plugins` という名前で Claude Code / Codex / Kiro CLI / agy へ配布されるプラグインです。配布物は `plugins/ndf/` の1ディレクトリにまとまっており、Skill の実体は `plugins/ndf/skills/` の1箇所だけです。どのランタイムへ配るかは `plugins/ndf/manifests/*-skills.txt` が決めます。 +**NDFプラグイン v10.16.1** は、同じ `ndf@ai-plugins` という名前で Claude Code / Codex / Kiro CLI / agy へ配布されるプラグインです。配布物は `plugins/ndf/` の1ディレクトリにまとまっており、Skill の実体は `plugins/ndf/skills/` の1箇所だけです。どのランタイムへ配るかは `plugins/ndf/manifests/*-skills.txt` が決めます。 - **公開Skills**: Claude Code向け core 45個、Kiro向け core 44個、Codex向け core 43個、agy向け core 43個に分離。 - **元Skills(45個)**: @@ -110,7 +110,7 @@ hook を効かせる手順と、新しい版へ入れ替える手順は | プラグイン名 | バージョン | 説明 | 詳細 | |------------|----------|------|------| -| **ndf** | 10.16.1-dev.1 | Claude Code / Codex / Kiro CLI / agy へ 1 ディレクトリから配布する NDF プラグイン。8個の専門エージェント(Claude版)、公開Skills(Claude Code向け core 45個、Kiro向け core 44個、Codex向け core 43個、agy向け core 43個)、4ランタイム共通の作業ツリー運用フック(PreToolUse / SessionStart / userPromptSubmit / agentSpawn / PreInvocation)、Claude Stopフック、Codex/Kiro向け通知・実行補助を提供。v4.0.0 で Codex MCP サーバを廃止し、`/ndf:external-ai` skill + `corder` エージェント経由の CLI 直接実行に一本化。 | [README](./plugins/ndf/README.md) | +| **ndf** | 10.16.1 | Claude Code / Codex / Kiro CLI / agy へ 1 ディレクトリから配布する NDF プラグイン。8個の専門エージェント(Claude版)、公開Skills(Claude Code向け core 45個、Kiro向け core 44個、Codex向け core 43個、agy向け core 43個)、4ランタイム共通の作業ツリー運用フック(PreToolUse / SessionStart / userPromptSubmit / agentSpawn / PreInvocation)、Claude Stopフック、Codex/Kiro向け通知・実行補助を提供。v4.0.0 で Codex MCP サーバを廃止し、`/ndf:external-ai` skill + `corder` エージェント経由の CLI 直接実行に一本化。 | [README](./plugins/ndf/README.md) | | **playwright-kit** | 2.0.3 | Playwright による E2E テストの計画・実装・証跡管理を提供するプラグイン。ページ役割からのテスト計画、動画 / trace 付きスクリプト実装、レポート生成と Drive 保管、playwright_kit ランタイム(init、a11y / CWV スキャン)の 4 Skill。NDF v7.0.0 で分離。 | [README](./plugins/playwright-kit/README.md) | ### 変更履歴 diff --git a/plugins/ndf/.claude-plugin/plugin.json b/plugins/ndf/.claude-plugin/plugin.json index a34f70b2a..ddf8c0dee 100644 --- a/plugins/ndf/.claude-plugin/plugin.json +++ b/plugins/ndf/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ndf", - "version": "10.16.1-dev.1", - "description": "Claude Code plugin (v10.16.1-dev.1): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", + "version": "10.16.1", + "description": "Claude Code plugin (v10.16.1): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", "author": { "name": "takemi-ohama", "url": "https://github.com/takemi-ohama" diff --git a/plugins/ndf/.codex-plugin/plugin.json b/plugins/ndf/.codex-plugin/plugin.json index 88f5e2bc8..62ca0211b 100644 --- a/plugins/ndf/.codex-plugin/plugin.json +++ b/plugins/ndf/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ndf", - "version": "10.16.1-dev.1", - "description": "Codex plugin (v10.16.1-dev.1): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation (Codex/agy), and optional Slack completion notifications.", + "version": "10.16.1", + "description": "Codex plugin (v10.16.1): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation (Codex/agy), and optional Slack completion notifications.", "skills": [ "./skills/cherry-pick-pr", "./skills/cross-refactoring", diff --git a/plugins/ndf/README.md b/plugins/ndf/README.md index bcfc1e831..e13d1e946 100644 --- a/plugins/ndf/README.md +++ b/plugins/ndf/README.md @@ -89,7 +89,7 @@ bash plugins/ndf/dev.kiro/install.sh --dry-run ```bash python3 -c "import json;print(json.load(open('.kiro/agents/ndf.json'))['description'])" -# => NDF統合開発エージェント(Kiro CLI用 / v10.16.1-dev.1) +# => NDF統合開発エージェント(Kiro CLI用 / v10.16.1) ``` ### agy @@ -119,7 +119,7 @@ agy plugin list # => {"imports":[{"name":"ndf","source":"antigravity","components":["skills","agents","hooks"]}]} ``` -## v10.16.1-dev.1 へ更新するとき +## v10.16.1 へ更新するとき **収束ループが、codex と claude の利用上限で止まった担当を起動し直さなくなり、読めない ディレクトリを含む `PATH` でも始まるようにしました**(マイルストーン 17「10.16.0 のリリース後 @@ -127,8 +127,8 @@ agy plugin list 削除や改名は無く、記録の移行も要りません(前の版で始めた状態ファイルはそのまま読めます)。 変更点の一覧は [CHANGELOG.md](../../CHANGELOG.md) にあります。 -**開発版です。** `develop` にだけ載ります。取得元へ `#develop` を足す手順は -[docs/versioning-and-distribution.md の「開発版を試す」](../../docs/versioning-and-distribution.md#開発版を試す)にあります。 +**正式版です。** `main` に載ります。中身は開発版 `10.16.1-dev.1` と同じで、版数の接尾辞だけを +外しました。 | 変わったこと | 中身 | | --- | --- | @@ -301,7 +301,7 @@ agy models # 認証の確認 ```text # 動く: 実体パスを示して読ませる -~/.codex/plugins/cache/ai-plugins/ndf/10.16.1-dev.1/skills/deploy/SKILL.md を読んで、その手順どおりに qa/staging へ deploy PR を作成してください。 +~/.codex/plugins/cache/ai-plugins/ndf/10.16.1/skills/deploy/SKILL.md を読んで、その手順どおりに qa/staging へ deploy PR を作成してください。 # 動かない: 明示起動 ($ は展開されない) $deploy qa/staging @@ -323,14 +323,14 @@ marketplace 経由でインストールした場合、Skill の実体は **ワ ```text $CODEX_HOME/plugins/cache////skills//SKILL.md # 既定 ($CODEX_HOME=~/.codex) の例: -# ~/.codex/plugins/cache/ai-plugins/ndf/10.16.1-dev.1/skills/deploy/SKILL.md +# ~/.codex/plugins/cache/ai-plugins/ndf/10.16.1/skills/deploy/SKILL.md ``` そのため「`deploy` の SKILL.md を探して読んで」のような曖昧な依頼は、Codex のファイル探索がワークスペース内に限られる状況では失敗しえます。**抑止した Skill は `$` が展開されない**ので、`codex plugin list` で実体パスを確認し、絶対パスを渡してください。 ```bash codex plugin list | grep 'ndf@ai-plugins' -# => ndf@ai-plugins installed, enabled 10.16.1-dev.1 +# => ndf@ai-plugins installed, enabled 10.16.1 ``` 抑止していない Skill(`markdown-writing` など)はキャッシュ配下でも `$` で解決するため、そちらは `$` 起動が使えます。 diff --git a/plugins/ndf/dev.agy/plugin.json b/plugins/ndf/dev.agy/plugin.json index 144b8a8ff..5a522db25 100644 --- a/plugins/ndf/dev.agy/plugin.json +++ b/plugins/ndf/dev.agy/plugin.json @@ -1,5 +1,5 @@ { "name": "ndf", - "version": "10.16.1-dev.1", - "description": "Antigravity CLI plugin (v10.16.1-dev.1): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation, and worktree guidance hooks." + "version": "10.16.1", + "description": "Antigravity CLI plugin (v10.16.1): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation, and worktree guidance hooks." }