diff --git a/AGENTS.md b/AGENTS.md index 945ede8..f56470f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## プロジェクト概要 -**Claude Codeプラグインマーケットプレイス**の開発プロジェクトです。チーム全体でClaude Codeの導入を加速するための事前設定されたプラグインを提供します。 +**Claude Code / Codex / Kiro CLI 向けプラグインマーケットプレイス**の開発プロジェクトです。チーム全体で AI 開発ツールの導入を加速するための事前設定されたプラグインを提供します。 **リポジトリ**: https://github.com/devbasex/ai-plugins @@ -40,8 +40,15 @@ ai-plugins/ ├── .claude-plugin/ │ └── marketplace.json # マーケットプレイス定義(必須) ├── plugins/ -│ ├── ndf/ # NDFプラグイン(メイン) -│ └── {plugin-name}/ # その他のプラグイン +│ ├── ndf-shared/ # NDF共通編集元(直接installしない) +│ ├── ndf-claude/ # Claude Code版NDF +│ ├── ndf-codex/ # Codex版NDF +│ ├── ndf-kiro/ # Kiro CLI版NDF +│ └── mcp/ +│ ├── shared/ # MCPプラグイン共通編集元 +│ ├── claude/ # Claude Code版MCP配布物 +│ ├── codex/ # Codex版MCP配布物 +│ └── kiro/ # Kiro CLI版MCP配布物 ├── docs/ # リポジトリ知識 ├── AGENTS.md # 共通エントリポイント ├── CLAUDE.md # Claude Code固有設定 @@ -59,18 +66,20 @@ ai-plugins/ | [docs/specifications/](docs/specifications/) | 完了済みplan/issue由来の確定仕様 | | [docs/claude-code-skills-survey.md](docs/claude-code-skills-survey.md) | Claude Code Skills調査レポート | | [docs/development-history/](docs/development-history/) | 開発履歴と知見 | -| [plugins/ndf/README.md](plugins/ndf/README.md) | NDFプラグインドキュメント | +| [plugins/ndf-claude/README.md](plugins/ndf-claude/README.md) | Claude Code版NDFプラグイン | +| [plugins/ndf-codex/README.md](plugins/ndf-codex/README.md) | Codex版NDFプラグイン | +| [plugins/ndf-kiro/README.md](plugins/ndf-kiro/README.md) | Kiro CLI版NDF installer | ## NDFプラグインについて -**NDFプラグイン**は、このマーケットプレイスの主要プラグインです(v4.19.0): -- 8個の専門サブエージェント(director、data-analyst、corder、researcher、qa、debugger、devops-engineer、code-reviewer) -- 49個のsource Skills(PR/コードレビューワークフロー、AIクロスレビュー (codex/gemini)、plan-to-spec確定仕様化、原則・ガイドライン、MLモデル構造標準 (ml-model-structure)、issue→multi-PR ワークフロー戦略、SQL最適化、データエクスポート、skill利用統計、statusline切替 (/ndf:statusline)、Codex CLI連携、Playwrightシナリオテスト (CDPリモート接続・Google Driveエビデンス保管含む)、Google Drive/Chat連携 等) -- SessionStartフック(transcript保持期間自動管理 `cleanupPeriodDays >= 90`、statusLine未設定時のNDF標準statusline自動設定) -- Stopフック(AI要約生成 + 自動Slack通知) +**NDFプラグイン**は、このマーケットプレイスの主要プラグインです(v4.19.0)。plugin 名は全ランタイムで `ndf` を維持し、配布物は `plugins/ndf-claude` / `plugins/ndf-codex` / `plugins/ndf-kiro` に分離しています。 +- 共通編集元は `plugins/ndf-shared/` +- Claude Code版は 8個の専門サブエージェント、公開Skills、SessionStart/Stopフックを提供 +- Codex版は Codex向け公開Skillsと任意Slack通知hookを提供 +- Kiro版は installer で `.kiro/skills/` と `.kiro/agents/default.json` を生成 - 外部AI委譲は `/ndf:codex` skill と `corder` エージェント経由で Codex CLI を呼び出し(v4.0.0 で Codex MCP サーバは廃止) -詳細は `plugins/ndf/README.md` および `docs/ndf-plugin-reference.md` を参照。 +詳細は各 runtime README と `docs/ndf-plugin-reference.md` を参照。 ## ベストプラクティス diff --git a/README.md b/README.md index da16f73..4e4687d 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ Claude Code / Codex / Kiro CLI向けのスキル・MCP設定を共有するた このマーケットプレイスは、チーム全体でAI開発ツール(Claude Code / Codex / Kiro CLI)の導入を加速するための事前設定されたプラグインを提供します。 -**NDFプラグイン v4.19.0** は、以下の機能を**オールインワン**で提供する統合プラグインです: +**NDFプラグイン v4.19.0** は、同じ `ndf@ai-plugins` という名前で Claude Code / Codex / Kiro CLI へ配布されるランタイム別プラグインです。共通ソースは `plugins/ndf-shared/` に集約し、利用者が install する配布物は `plugins/ndf-claude/` / `plugins/ndf-codex/` / `plugins/ndf-kiro/` に分かれています。 -- **公開Skills**: Claude Code向け core 29個、Kiro向け core 28個、Codex向け core 30個に分離。`skills-optional/` にランタイム別の除外候補を整理。 +- **公開Skills**: Claude Code向け core 29個、Kiro向け core 28個、Codex向け core 30個に分離。 - **元Skills(49個)**: - PR/レビューワークフロー (13): pr, pr-tests, fix, review, review-branch, review-pr-comments, resolve-pr-comments, cherry-pick-pr, deploy, sync-main, merged, clean, browser-test - 原則・ガイドライン (10): ndf-policies, branch-fix-strategy, implementation-plan, plan-to-spec, investigation-rules, problem-solving, logging-guidelines, markdown-writing, issue-plan-strategy, ml-model-structure @@ -21,7 +21,8 @@ Claude Code / Codex / Kiro CLI向けのスキル・MCP設定を共有するた - **自動フック**: SessionStart (transcript保持期間を最低90日に保つ) + Stop (AI要約生成+Slack通知) - **外部AI委譲**: `/ndf:codex` skill + `corder` エージェント経由で Codex CLI をバックグラウンド実行 (v4.0.0 で Codex MCP サーバは廃止) - **AIクロスレビュー強化**: `/ndf:cross-review` は codex/gemini 両方に PR レビューを委譲し、Gemini の進捗 heartbeat、`--focus` / `--extra-instructions-file`、PR 種別別の自動レビュー観点テンプレートに対応 -- **Kiro CLI対応**: インストーラーによるワンコマンドセットアップ +- **Kiro CLI対応**: `plugins/ndf-kiro/install.sh` によるワンコマンドセットアップ +- **MCPプラグイン**: `plugins/mcp/shared/` を編集元とし、Claude / Codex / Kiro 向け配布物を `plugins/mcp/{claude,codex,kiro}/` に生成 ## 利用方法 @@ -99,7 +100,7 @@ kiro-cli chat | プラグイン名 | バージョン | 説明 | 詳細 | |------------|----------|------|------| -| **ndf** | 4.19.0 | Claude Code / Codex / Kiro CLI開発環境を**オールインワン**で強化する統合プラグイン。8個の専門エージェント(director、data-analyst、corder、researcher、qa、debugger、devops-engineer、code-reviewer)、公開Skills(Claude Code向け core 29個、Kiro向け core 28個、Codex向け core 30個)、SessionStartフック(transcript保持期間自動管理)、Stopフック(AI要約生成+Slack通知)を提供。v4.0.0 で Codex MCP サーバを廃止し、`/ndf:codex` skill + `corder` エージェント経由の CLI 直接実行に一本化。 | [Claude](./plugins/ndf-claude/README.md) / [Codex](./plugins/ndf-codex/README.md) / [Kiro](./plugins/ndf-kiro/README.md) | +| **ndf** | 4.19.0 | Claude Code / Codex / Kiro CLI 向けに runtime 別配布物を提供する NDF プラグイン。8個の専門エージェント(Claude版)、公開Skills(Claude Code向け core 29個、Kiro向け core 28個、Codex向け core 30個)、Claude SessionStart/Stopフック、Codex/Kiro向け通知・実行補助を提供。v4.0.0 で Codex MCP サーバを廃止し、`/ndf:codex` skill + `corder` エージェント経由の CLI 直接実行に一本化。 | [Claude](./plugins/ndf-claude/README.md) / [Codex](./plugins/ndf-codex/README.md) / [Kiro](./plugins/ndf-kiro/README.md) | ### NDF v4.19.0 の主な変更 @@ -120,17 +121,15 @@ ai-plugins/ ├── .claude-plugin/ │ └── marketplace.json # Claude Codeマーケットプレイスメタデータ ├── plugins/ -│ └── {plugin-name}/ -│ ├── .codex-plugin/ -│ │ └── plugin.json # Codexプラグインメタデータ -│ ├── .claude-plugin/ -│ │ └── plugin.json # Claude Codeプラグインメタデータ -│ ├── agents/ # サブエージェント (*.md) -│ ├── skills/ # 全Skillの実体(Claude Code/Kiroはmanifest配列で公開対象を指定) -│ ├── skills-codex/ # Codex向け公開Skill(marketplace cache対応の実ディレクトリ) -│ └── skills-optional/ # ランタイム別除外候補リスト -│ └── {skill-name}/ -│ └── SKILL.md # エントリポイント(必須) +│ ├── ndf-shared/ # NDF共通編集元(直接installしない) +│ ├── ndf-claude/ # Claude Code版NDF配布物 +│ ├── ndf-codex/ # Codex版NDF配布物 +│ ├── ndf-kiro/ # Kiro CLI版NDF配布物/installer +│ └── mcp/ +│ ├── shared/ # MCPプラグイン共通編集元 +│ ├── claude/ # Claude Code版MCP配布物 +│ ├── codex/ # Codex版MCP配布物 +│ └── kiro/ # Kiro CLI版MCP配布物/installer ├── README.md └── CLAUDE.md # AIエージェント向けガイドライン ``` diff --git a/docs/claude-code-skills-survey.md b/docs/claude-code-skills-survey.md index 5f41265..73b1893 100644 --- a/docs/claude-code-skills-survey.md +++ b/docs/claude-code-skills-survey.md @@ -622,7 +622,7 @@ ### 1. Skillsの配置場所 ``` -plugins/ndf/ +plugins/ndf-shared/ ├── skills/ │ ├── director/ │ │ ├── brainstorming/SKILL.md diff --git a/docs/external-reviews/pr-57-codex-business-review.md b/docs/external-reviews/pr-57-codex-business-review.md index 732bef5..c8772b9 100644 --- a/docs/external-reviews/pr-57-codex-business-review.md +++ b/docs/external-reviews/pr-57-codex-business-review.md @@ -9,7 +9,7 @@ | **実行時間** | 約 8 分 / tokens 129,705 | | **背景** | 技術レビュー (Codex 3 ラウンド + Amazon Q + Gemini) 完了後、事業観点のみに絞って依頼 | | **結論** | ⚠️ **コード品質ではなく、事業的な梱包がまだ追いついていない**。power-user 向け merge は合理的、新看板機能として広報するなら周辺メッセージ修正を同時に。 | -| **直近 1ヶ月の最重要アクション** | (1) `marketplace.json` / `plugin.json` / `plugins/ndf/README.md` の storefront 文言更新 / (2) `browser-test` と `playwright-scenario-test` の役割分担を docs で明確化 / (3) 10分で動くデモケースの公開 | +| **直近 1ヶ月の最重要アクション** | (1) `marketplace.json` / `plugin.json` / `plugins/ndf-claude/README.md` の storefront 文言更新 / (2) `browser-test` と `playwright-scenario-test` の役割分担を docs で明確化 / (3) 10分で動くデモケースの公開 | | **PR コメント** | https://github.com/takemi-ohama/ai-plugins/pull/57#issuecomment-4321871998 | --- @@ -27,7 +27,7 @@ ### 1.1 強み この skill の本質は、Playwright そのものの代替ではなく、**Playwright を AI エージェント運用に最適化した“実務レイヤー”** にあります。 -Playwright 公式はブラウザ自動化基盤として強いですが、`/work/ai-plugins/plugins/ndf/skills/playwright-scenario-test/SKILL.md` を読む限り、本 skill はそこに **テスト設計の方法論、page role 分類、証跡標準化、Drive 共有、Markdown レポート** を足しています。 +Playwright 公式はブラウザ自動化基盤として強いですが、`/work/ai-plugins/plugins/ndf-shared/skills/playwright-scenario-test/SKILL.md` を読む限り、本 skill はそこに **テスト設計の方法論、page role 分類、証跡標準化、Drive 共有、Markdown レポート** を足しています。 つまり競争軸は「ブラウザを動かせるか」ではなく、**“AI が再現性の高い E2E を短時間で作り、レビュー可能な証跡まで揃えられるか”** です。 この位置づけは、Playwright / Selenium / Puppeteer のような実行基盤とは競争しつつも、実は真正面からは競合しません。 むしろ「それらをどう現場の開発運用に落とすか」の層を取ろうとしている点が重要です。 @@ -60,7 +60,7 @@ NDF はその上に載せられる可能性があるが、現状は **ローカ ### 1.2 弱み / 競合との差 弱みは明確で、**今のままだと“何でも入った NDF の一機能”に見えてしまい、単独価値として発見されにくい**ことです。 -`plugins/ndf/.claude-plugin/plugin.json` の description には機能が多く並びますが、焦点が広すぎます。 +`plugins/ndf-claude/.claude-plugin/plugin.json` の description には機能が多く並びますが、焦点が広すぎます。 利用者は「PR ワークフロー」「Codex 連携」「Slack 通知」「Google Drive」「E2E テスト」のどれを主価値として認識すべきか迷います。 `plugin.json` の `keywords` に `playwright` `pytest` `e2e` `qa` `accessibility` `testing` が入っていないのも機会損失です。 @@ -185,7 +185,7 @@ OSS として広げるなら、**“Claude Code plugin でありつつ、中身 ## 3. Skill のディスカバラビリティと活性化 ### 3.1 description / Triggers の妥当性 -`/work/ai-plugins/plugins/ndf/skills/playwright-scenario-test/SKILL.md` の `description` は情報量が豊富で、技術的にはかなり良いです。 +`/work/ai-plugins/plugins/ndf-shared/skills/playwright-scenario-test/SKILL.md` の `description` は情報量が豊富で、技術的にはかなり良いです。 ただし business 観点では、**少し盛り込みすぎ**です。 利用者に価値を伝える文章としては強いが、AI の auto-invoke trigger としては信号が散っています。 @@ -194,7 +194,7 @@ OSS として広げるなら、**“Claude Code plugin でありつつ、中身 AI にとっても人にとっても、「これは E2E planning skill なのか、動画証跡 skill なのか、品質ゲート skill なのか」が一読で定まりにくい。 `when_to_use` で Triggers を分離した設計自体は正しいです。 -`plugins/ndf/CLAUDE.md` でもその改善意図が記録されており、方向性は良いです。 +`CLAUDE.md` でもその改善意図が記録されており、方向性は良いです。 ただし trigger phrase はまだ改善余地があります。 現状の trigger は、技術者寄りの言葉に少し偏っています。 @@ -225,7 +225,7 @@ Claude Code plugin モデルと相性は良いです。 ただし問題は、**scenario-test 単体の価値に最短で到達できない**ことです。 利用者はまず NDF 全体を理解させられます。 -`plugins/ndf/README.md` も大きな統合プラグインの説明が先に来るため、testing use case の人には情報密度が高すぎます。 +`plugins/ndf-claude/README.md` も大きな統合プラグインの説明が先に来るため、testing use case の人には情報密度が高すぎます。 `marketplace.json` の `ndf` description が古いのは、導線上かなり痛いです。 ここは marketplace 上の第一印象なので、**PR #57 の価値を最初に伝える場所**です。 @@ -335,7 +335,7 @@ Drive 共有、HUD 動画、a11y/CWV は、単体では売りにくいです。 ### 6.1 v0.4.0 で優先すべきこと (Top 3) **Top 1: プロダクトの見せ方を刷新すること** -`marketplace.json`、`plugin.json`、`plugins/ndf/README.md` の scenario-test 訴求を更新し、 +`marketplace.json`、`plugin.json`、`plugins/ndf-claude/README.md` の scenario-test 訴求を更新し、 「AI が pytest-playwright テストを書き、証跡を残し、共有まで行う skill」 と一文で伝わるようにするべきです。 これは最優先です。 @@ -405,7 +405,7 @@ NDF は test management ではなく **AI-assisted test execution workflow** に - 逆に、これを「広く採用される新看板機能」として出すなら、周辺メッセージ修正を同時にやるべきです。 - 直近 1 ヶ月の最重要アクション 1: -- `marketplace.json` `plugin.json` `plugins/ndf/README.md` を更新し、scenario-test の価値を storefront レベルで伝える。 +- `marketplace.json` `plugin.json` `plugins/ndf-claude/README.md` を更新し、scenario-test の価値を storefront レベルで伝える。 - ここで「AI が pytest-playwright テストと証跡を自動生成する」という一文を明示する。 - 直近 1 ヶ月の最重要アクション 2: @@ -443,17 +443,17 @@ NDF は test management ではなく **AI-assisted test execution workflow** に - [CLAUDE.md](/work/ai-plugins/CLAUDE.md) - [docs/project-overview.md](/work/ai-plugins/docs/project-overview.md) - [docs/ndf-plugin-reference.md](/work/ai-plugins/docs/ndf-plugin-reference.md) -- [plugins/ndf/README.md](/work/ai-plugins/plugins/ndf/README.md) -- [plugins/ndf/CLAUDE.md](/work/ai-plugins/plugins/ndf/CLAUDE.md) -- [plugins/ndf/.claude-plugin/plugin.json](/work/ai-plugins/plugins/ndf/.claude-plugin/plugin.json) +- [plugins/ndf-claude/README.md](/work/ai-plugins/plugins/ndf-claude/README.md) +- [CLAUDE.md](/work/ai-plugins/CLAUDE.md) +- [plugins/ndf-claude/.claude-plugin/plugin.json](/work/ai-plugins/plugins/ndf-claude/.claude-plugin/plugin.json) - [.claude-plugin/marketplace.json](/work/ai-plugins/.claude-plugin/marketplace.json) -- [plugins/ndf/skills/playwright-scenario-test/SKILL.md](/work/ai-plugins/plugins/ndf/skills/playwright-scenario-test/SKILL.md) -- [plugins/ndf/skills/playwright-scenario-test/docs/README.md](/work/ai-plugins/plugins/ndf/skills/playwright-scenario-test/docs/README.md) -- [plugins/ndf/skills/playwright-scenario-test/docs/01-methodology.md](/work/ai-plugins/plugins/ndf/skills/playwright-scenario-test/docs/01-methodology.md) -- [plugins/ndf/skills/playwright-scenario-test/docs/02-page-roles.md](/work/ai-plugins/plugins/ndf/skills/playwright-scenario-test/docs/02-page-roles.md) -- [plugins/ndf/skills/playwright-scenario-test/docs/06-pytest-playwright.md](/work/ai-plugins/plugins/ndf/skills/playwright-scenario-test/docs/06-pytest-playwright.md) -- [plugins/ndf/skills/playwright-scenario-test/pyproject.toml](/work/ai-plugins/plugins/ndf/skills/playwright-scenario-test/pyproject.toml) -- [plugins/ndf/skills/playwright-scenario-test/templates/conftest.py.template](/work/ai-plugins/plugins/ndf/skills/playwright-scenario-test/templates/conftest.py.template) +- [plugins/ndf-shared/skills/playwright-scenario-test/SKILL.md](/work/ai-plugins/plugins/ndf-shared/skills/playwright-scenario-test/SKILL.md) +- [plugins/ndf-shared/skills/playwright-scenario-test/docs/README.md](/work/ai-plugins/plugins/ndf-shared/skills/playwright-scenario-test/docs/README.md) +- [plugins/ndf-shared/skills/playwright-scenario-test/docs/01-methodology.md](/work/ai-plugins/plugins/ndf-shared/skills/playwright-scenario-test/docs/01-methodology.md) +- [plugins/ndf-shared/skills/playwright-scenario-test/docs/02-page-roles.md](/work/ai-plugins/plugins/ndf-shared/skills/playwright-scenario-test/docs/02-page-roles.md) +- [plugins/ndf-shared/skills/playwright-scenario-test/docs/06-pytest-playwright.md](/work/ai-plugins/plugins/ndf-shared/skills/playwright-scenario-test/docs/06-pytest-playwright.md) +- [plugins/ndf-shared/skills/playwright-scenario-test/pyproject.toml](/work/ai-plugins/plugins/ndf-shared/skills/playwright-scenario-test/pyproject.toml) +- [plugins/ndf-shared/skills/playwright-scenario-test/templates/conftest.py.template](/work/ai-plugins/plugins/ndf-shared/skills/playwright-scenario-test/templates/conftest.py.template) ## 外部比較で参照した公式情報 - Playwright Python: https://playwright.dev/python/ diff --git a/docs/external-reviews/pr-57-gemini-technical-review.md b/docs/external-reviews/pr-57-gemini-technical-review.md index 8b6f72d..1902b25 100644 --- a/docs/external-reviews/pr-57-gemini-technical-review.md +++ b/docs/external-reviews/pr-57-gemini-technical-review.md @@ -24,46 +24,46 @@ ## 1. pytest-playwright 慣用性 (Idiomatic) に関する指摘 ### 1.1 `autouse` フィクスチャの条件付きスキップ [重要度: Major] -- ファイル: `/work/ai-plugins/plugins/ndf/skills/playwright-scenario-test/scenario_test/fixtures/a11y.py:59` +- ファイル: `/work/ai-plugins/plugins/ndf-shared/skills/playwright-scenario-test/scenario_test/fixtures/a11y.py:59` - 状況: `_ndf_a11y_autouse` が `autouse=True` ですが、内部で `if "page" not in request.fixturenames: return` を行っています。 - 評価: これは非常に優れた設計です。`autouse=True` なフィクスチャが不用意に `page` を要求すると、全てのテスト(DB テストや単体テスト等)でブラウザが起動してしまいます。`request.fixturenames` を確認することで、Playwright を使うテストのみに計測を限定し、テストスイート全体の速度低下を防いでいます。`cwv.py` でも同様の処理が行われており、一貫性があります。 ### 1.2 `browser_context_args` による HAR/Trace の個別制御 [重要度: Major] -- ファイル: `/work/ai-plugins/plugins/ndf/skills/playwright-scenario-test/scenario_test/fixtures/evidence.py:165` +- ファイル: `/work/ai-plugins/plugins/ndf-shared/skills/playwright-scenario-test/scenario_test/fixtures/evidence.py:165` - 状況: `browser_context_args` を `function` スコープで override し、テストごとに `record_har_path` を動的に注入しています。 - 評価: `pytest-playwright` の機能を活かした最も慣用的な方法です。旧実装の session 単位 HAR を廃止し、テストごとの隔離(Isolation)を担保している点は、デバッグの容易性を大きく向上させます。 ## 2. 可読性に関する指摘 ### 2.1 独自マーカーのドキュメント化 [重要度: Minor] -- ファイル: `/work/ai-plugins/plugins/ndf/skills/playwright-scenario-test/scenario_test/pytest_plugin.py:75` +- ファイル: `/work/ai-plugins/plugins/ndf-shared/skills/playwright-scenario-test/scenario_test/pytest_plugin.py:75` - 指摘: `_NDF_MARKERS` を定義し、`pytest_configure` で `config.addinivalue_line("markers", ...)` を行っています。 - 評価: これにより `pytest --markers` を実行した際に NDF 独自のマーカー(`page_role`, `role`, `phase`, `priority`)の説明が表示されるようになり、利用者にとってのセルフドキュメンテーションが効いています。 ## 3. コード品質に関する指摘 ### 3.1 エラーメッセージの切り詰め [重要度: Minor] -- ファイル: `/work/ai-plugins/plugins/ndf/skills/playwright-scenario-test/scenario_test/pytest_report.py:114` +- ファイル: `/work/ai-plugins/plugins/ndf-shared/skills/playwright-scenario-test/scenario_test/pytest_report.py:114` - 状況: `lines.append(e.error_message[:2000])` - 評価: 長大なスタックトレースやログによって `report.md` が肥大化するのを防ぐ適切な配慮です。 ### 3.2 ログイン送信ボタンのフォールバック順序 [重要度: Major] -- ファイル: `/work/ai-plugins/plugins/ndf/skills/playwright-scenario-test/scenario_test/fixtures/auth.py:53` +- ファイル: `/work/ai-plugins/plugins/ndf-shared/skills/playwright-scenario-test/scenario_test/fixtures/auth.py:53` - 評価: `_submit_login_form` において、明示的なセレクタ → 汎用的な role/text マッチ → パスワード欄での Enter キー、という順序で試行しています。この「最後に Enter を押す」というフォールバックは、ボタンの特定が困難な難解な UI においてもログインを成功させる可能性を高める、実戦的な工夫です。 ## 4. 保守性に関する指摘 ### 4.1 テンプレートの充実 [重要度: Major] -- ファイル: `/work/ai-plugins/plugins/ndf/skills/playwright-scenario-test/templates/` +- ファイル: `/work/ai-plugins/plugins/ndf-shared/skills/playwright-scenario-test/templates/` - 評価: `conftest.py` だけでなく、`test_auth.py.template` などの役割別テンプレートが用意されており、利用者が迷わずに導入できるようになっています。特に `SKILL.md` のクイックスタートが「雛形をコピーして動かす」という手順で完結しており、OSS 的な使い勝手が非常に良いです。 ## 5. セキュリティに関する指摘 ### 5.1 `localStorage` 注入時の origin 跨ぎ [重要度: Minor] -- ファイル: `/work/ai-plugins/plugins/ndf/skills/playwright-scenario-test/scenario_test/fixtures/auth.py:192` +- ファイル: `/work/ai-plugins/plugins/ndf-shared/skills/playwright-scenario-test/scenario_test/fixtures/auth.py:192` - 状況: `state.get("origins")` をループして `page.goto(url)` してから `localStorage.setItem` を実行しています。 - 懸念: 外部のオリジン(広告や計測タグ等)が `storage_state` に含まれている場合、それらに対しても `goto` が試行されます。 - 評価: ただし、`try...except` で囲まれており、失敗しても認証自体(Cookie)には影響しないようになっているため、実用上の問題はありません。 ## 6. テストカバレッジに関する指摘 ### 6.1 `pytester` による高度な検証 [重要度: Major] -- ファイル: `/work/ai-plugins/plugins/ndf/skills/playwright-scenario-test/tests/test_pytester_integration.py` +- ファイル: `/work/ai-plugins/plugins/ndf-shared/skills/playwright-scenario-test/tests/test_pytester_integration.py` - 評価: `pytest` の内部挙動に依存する「HAR の teardown 時フラッシュ」や「xdist 時の開始時刻計算」を、`pytester` を用いて隔離環境で本物の pytest を走らせて検証している点は非常に高く評価できます。これにより、プラグインとしての堅牢性が担保されています。 ## 7. 良い点 (Codex / Amazon Q が触れていない点を中心に) diff --git a/docs/ndf-plugin-reference.md b/docs/ndf-plugin-reference.md index 7cfacab..5bd37e3 100644 --- a/docs/ndf-plugin-reference.md +++ b/docs/ndf-plugin-reference.md @@ -2,219 +2,111 @@ ## 概要 -NDF プラグインは、Claude Code / Codex / Kiro CLI 向けのオールインワン開発支援プラグイン。エージェント、Skills、フックを統合して提供する。 +NDF は Claude Code / Codex / Kiro CLI 向けの開発支援プラグイン群です。marketplace 上の plugin 名は全ランタイムで `ndf` を維持し、配布物はランタイム別ディレクトリに分けます。 -**現行バージョン**: **v4.19.0** — `plan-to-spec` skill を追加し、実装完了後の plan を `docs/` 配下の確定仕様書へ移動・リライト・レビューする標準フローと完了報告テンプレートを定義した。**v4.18.0** で `cross-review` は Gemini の進捗ログを heartbeat に表示し、`--focus` / `--extra-instructions-file` と PR 種別別の自動レビュー観点テンプレートを追加した。Codex plugin 対応として Claude Code / Kiro と Codex の公開 Skill セットを分離し、Codex では `skills-codex/` の core 30 個のみを公開する。Skill 実体は `skills/` に集約し、Claude Code / Kiro は manifest 配列指定で公開対象を制御する。Codex 向け公開ディレクトリは marketplace cache で欠落しないよう実ディレクトリとして同梱する。詳細は [CHANGELOG.md](../plugins/ndf/CHANGELOG.md)。`/ndf:codex` skill + `corder` エージェント経由の Codex CLI 直接実行に一本化、Serena MCP は別プラグイン `mcp-serena` に分離済み、Playwright シナリオ E2E、Google Drive / Chat 連携 skill を提供。 +| 用途 | ディレクトリ | 配布方法 | +|---|---|---| +| 共通編集元 | `plugins/ndf-shared/` | 直接 install しない | +| Claude Code | `plugins/ndf-claude/` | `.claude-plugin/marketplace.json` の `ndf` | +| Codex | `plugins/ndf-codex/` | `.agents/plugins/marketplace.json` の `ndf` | +| Kiro CLI | `plugins/ndf-kiro/` | `plugins/ndf-kiro/install.sh` | + +旧 monolithic NDF ディレクトリは廃止済みです。Skill や共通スクリプトを変更する場合は `plugins/ndf-shared/` を編集し、`bash scripts/build-runtime-plugins.sh` で runtime 配布物を再生成します。 ## ディレクトリ構造 -``` -plugins/ndf/ -├── .claude-plugin/ -│ └── plugin.json # プラグインメタデータ -├── hooks/ -│ └── hooks.json # SessionStart (保持期間管理/デフォルトstatusline) / Stop (Slack通知) -├── scripts/ -│ ├── ensure-retention.sh # cleanupPeriodDays >= 90 を保つ -│ ├── statusline.sh # NDF標準statusline本体 -│ ├── statusline-switch.sh # statusline の導入・切替・復元 (ensure/set/restore/status) -│ └── slack-notify.js # Slack通知スクリプト -├── agents/ # 専門エージェント(8個) -├── skills/ # 全Skill実体(49個。Claude Code/Kiroはmanifest配列で公開対象を指定) -├── skills-codex/ # Codex向け公開Skill(core 30個。marketplace cache対応の実ディレクトリ) -├── skills-optional/ # ランタイム別除外候補リスト -├── CLAUDE.md # プラグイン開発者向けガイド -└── README.md # 利用者向けドキュメント +```text +plugins/ +├── ndf-shared/ +│ ├── skills/ +│ ├── scripts/ +│ └── manifests/ +├── ndf-claude/ +│ ├── .claude-plugin/plugin.json +│ ├── agents/ +│ ├── hooks/ +│ ├── skills/ +│ └── scripts/ +├── ndf-codex/ +│ ├── .codex-plugin/plugin.json +│ ├── hooks/ +│ ├── skills/ +│ └── scripts/ +└── ndf-kiro/ + ├── install.sh + ├── agents/default.json.template + ├── prompts/ + ├── skills/ + └── scripts/ ``` -## 機能 +生成物は commit 対象です。利用者が plugin install 時に build を実行する必要はありません。 -### 1. MCP サーバー +## Runtime 別の同梱内容 -NDF プラグイン本体はコア MCP サーバを**同梱しない**(v4.0.0 で Codex MCP を廃止)。関連 MCP は個別プラグインとして提供: +| Runtime | 同梱内容 | +|---|---| +| Claude Code | 8個の専門エージェント、Claude向け公開Skills、SessionStart/Stop hook、statusline、Slack通知スクリプト | +| Codex | Codex向け公開Skills、Stop hook、任意Slack通知スクリプト | +| Kiro CLI | Kiro向け公開Skills、agent config template、workflow prompts、installer | -| MCP | 提供プラグイン | 用途 | -|---|---|---| -| Serena MCP | `mcp-serena` | セマンティックコード操作 | -| GitHub MCP | Anthropic 公式 | GitHub 操作 | -| Context7 MCP | Anthropic 公式 | 最新ライブラリドキュメント | -| Chrome DevTools MCP | `mcp-chrome-devtools` | ブラウザ自動化・パフォーマンス | -| BigQuery MCP | `mcp-bigquery` | BigQuery データ分析 | -| AWS Docs MCP | `mcp-aws-docs` | AWS 公式ドキュメント | -| DBHub MCP | `mcp-dbhub` | 汎用データベース | -| Notion MCP | `mcp-notion` | Notion 連携 | +Claude Code 専用の agents / statusline / transcript retention hook は Codex 版と Kiro 版には含めません。Codex 版と Kiro 版は、それぞれの runtime が読むディレクトリだけで完結します。 -### 2. ワークフロー Skills(スラッシュコマンド) +## Skills -`/ndf:` でユーザーから直接起動する Skill 群: +NDF の Skill 実装は `plugins/ndf-shared/skills/` が編集元です。公開セットは manifest で管理します。 -| Skill | 用途 | +| Manifest | 出力先 | |---|---| -| `/ndf:pr` | commit+push+PR 作成 / 既存 PR 説明更新 | -| `/ndf:pr-tests` | PR の Test Plan を自動実行 | -| `/ndf:fix` | レビューコメントの修正対応 | -| `/ndf:review` | PR を Approve/Request Changes 判定 | -| `/ndf:review-branch` | PR 前のローカル差分レビュー | -| `/ndf:review-pr-comments` | PR コメントの分類 (READ-ONLY) | -| `/ndf:resolve-pr-comments` | 対応済みコメント返信+Resolve | -| `/ndf:cherry-pick-pr` | 環境ブランチへの cherry-pick PR | -| `/ndf:deploy` | 環境ブランチへのデプロイ PR | -| `/ndf:sync-main` | main を現ブランチに取り込み | -| `/ndf:merged` | マージ後のクリーンアップ | -| `/ndf:clean` | マージ済みブランチ一括削除 | -| `/ndf:browser-test` | Playwright/Chrome DevTools での動作確認 | -| `/ndf:skill-stats` | Skill 利用統計の集計(期間/プロジェクト別) | -| `/ndf:statusline` | NDF標準statuslineの切り替え・復元・状態確認 (`set`/`restore`/`status`) | - -### 3. 原則・ガイドライン Skills(モデル起動型) - -該当文脈で自動的に参照される Skill 群: - -| Skill | 対象領域 | -|---|---| -| `ndf-policies` | プラグイン共通ポリシー(常時注入) | -| `branch-fix-strategy` | 複数ブランチ適用戦略 (cherry-pick) | -| `implementation-plan` | `issues/` 配下の実装プラン管理 | -| `plan-to-spec` | 実装完了後の plan を確定仕様書へ移動・リライト・レビュー | -| `investigation-rules` | 調査時のエビデンス主義 | -| `problem-solving` | 根本原因分析・多層防御 | -| `logging-guidelines` | ログ運用 (言語非依存) | -| `markdown-writing` | Markdown 文書の体裁 | -| `issue-plan-strategy` | issue→plan 作成・multi-PR 実行のワークフロー戦略 | -| `ml-model-structure` | ML モデル構築・推論API の標準ディレクトリ構造 (版内 feature SSoT / train↔serve 契約) | - -### 4. 補助 Skills - -| Skill | 用途 | -|---|---| -| `data-analyst-sql-optimization` | SQL 最適化パターン | -| `data-analyst-export` | CSV/JSON/Excel 出力 | -| `qa-security-scan` | OWASP Top 10 チェック | -| `python-execution` | Python 実行環境の自動判定 | -| `docker-container-access` | Docker コンテナ接続判定 | -| `git-gh-operations` | git/gh 操作パターン | -| `google-auth` | Google API OAuth2 | -| `codex` | Codex CLI 直接実行ガイド | -| `deepwiki-transfer` | DeepWiki 知識転送 | -| `knowledge-reorg` | 知識再編成 | -| `mcp-builder` | MCP サーバ作成(Anthropic 公式) | -| `official-skills-autoloader` | Anthropic 公式 Skill の自動ロード | -| `playwright-test-planning` | E2E テスト計画立案 (HTSM/ISTQB) | -| `playwright-script-creation` | E2E テストスクリプト作成 | -| `playwright-execution` | E2E 実行+エビデンス収集 (動画/a11y/CWV) | -| `playwright-report` | E2E テスト結果の Markdown レポート | -| `playwright-kit-ops` | playwright_kit スクリプト操作 | -| `playwright-scenario-test` | フル E2E ワークフロー統括 | -| `google-drive` | Google Drive/Docs 操作 | -| `google-chat` | Google Chat メッセージ取得 | -| `cross-review` | codex/gemini 両方でレビュー自動ループ | -| `gemini` | gemini CLI 直接実行 | - -### 5. 専門エージェント(8個、モデル階層化) - -| エージェント | モデル | 役割 | -|-------------|------|------| -| **director** | opus | タスク統括・設計立案 | -| **corder** | sonnet | Codex CLI 経由の独立レビュー・大規模調査 | -| **data-analyst** | sonnet | データ分析・SQL | -| **researcher** | sonnet | AWS Docs / Chrome DevTools 調査 | -| **qa** | sonnet | 品質・セキュリティ検証 | -| **debugger** | sonnet | 根本原因分析 | -| **devops-engineer** | sonnet | Docker/CI/CD/K8s | -| **code-reviewer** | sonnet | git diff/PR レビュー(Codex 非使用) | - -### 6. 自動フック - -| イベント | 用途 | -|---|---| -| `SessionStart` (matcher: `startup`) | `~/.claude/settings.json` の `cleanupPeriodDays` を最低 90 日に保つ (7日タイムスタンプガード + flock でアトミック更新) | -| `SessionStart` (matcher: `startup`) | `statusLine` 未設定時のみ NDF 標準 statusline を設定 (既存設定優先、NDF 版利用中はスクリプト更新のみ追従) | -| `Stop` | AI 要約を生成して Slack に通知 (`SLACK_BOT_TOKEN` 設定時のみ) | - -## 環境変数 +| `plugins/ndf-shared/manifests/claude-skills.txt` | `plugins/ndf-claude/skills/` | +| `plugins/ndf-shared/manifests/codex-skills.txt` | `plugins/ndf-codex/skills/` | +| `plugins/ndf-shared/manifests/kiro-skills.txt` | `plugins/ndf-kiro/skills/` | -### Slack 通知(推奨) -- `SLACK_BOT_TOKEN` — Bot User OAuth Token (`xoxb-...`) -- `SLACK_CHANNEL_ID` — 通知先チャンネル (`C...`) -- `SLACK_USER_MENTION` — メンション対象ユーザー (`<@U...>`) +主な Skill 領域: -### Codex CLI(`/ndf:codex` / `corder` エージェント利用時) -- `CODEX_HOME` — Codex CLI のホーム (default: `~/.codex`) -- `OPENAI_API_KEY` — `codex login` 済みなら不要 +- PR / review workflow: `pr`, `pr-tests`, `fix`, `review`, `cross-review`, `resolve-pr-comments` +- branch / release workflow: `deploy`, `cherry-pick-pr`, `sync-main`, `merged`, `clean` +- planning / documentation: `implementation-plan`, `issue-plan-strategy`, `plan-to-spec`, `markdown-writing` +- quality / execution: `playwright-*`, `python-execution`, `docker-container-access`, `git-gh-operations` +- external services: `google-drive`, `google-chat`, `data-analyst-*` +- policy: `ndf-policies`, `problem-solving`, `logging-guidelines` -### 個別 MCP プラグイン(利用する場合) -各プラグイン README を参照。 +## MCP Plugins -## 実装上の知見 +MCP plugin も runtime 別に配布します。共通編集元は `plugins/mcp/shared//`、配布物は `plugins/mcp/claude|codex|kiro//` です。 -### Stop Hook 無限ループ防止 +Claude Code と Codex は marketplace から同じ plugin 名で install します。Kiro CLI は `plugins/mcp/kiro//install.sh` で対象プロジェクトの `.mcp.json` と必要な Kiro agent 設定を更新します。 -Stop hook 内で Claude CLI を呼び出す際は `--settings` で hooks と plugins を両方無効化する: +## Build / Validation ```bash -claude -p --settings '{"disableAllHooks": true, "disableAllPlugins": true}' --output-format text +bash scripts/build-runtime-plugins.sh +bash scripts/build-runtime-plugins.sh --check +claude plugin validate plugins/ndf-claude +python3 -m json.tool plugins/ndf-codex/.codex-plugin/plugin.json >/dev/null +bash plugins/ndf-kiro/install.sh --help ``` -- `CLAUDE_DISABLE_HOOKS` 環境変数 → 存在しない -- `stop_hook_active` フィールド → 実際には送信されない -- `--settings` で両方無効化 → 確実に動作 - -### 要約生成の3段階フォールバック - -1. **Claude CLI**(優先) — AI による高品質要約 -2. **transcript 解析**(フォールバック1) — セッションログから抽出 -3. **git diff**(フォールバック2) — ファイル変更から推測 - -### 保持期間管理の実装 - -`SessionStart` hook で `~/.claude/settings.json` の `cleanupPeriodDays` を検査し、90 未満なら 90 に更新する: - -- 実行は `~/.claude/.ndf-retention-checked` の 7 日タイムスタンプで抑止 -- 書き込みは `flock` で排他ロックし、並列セッションでの lost update を防止 (flock 不在環境は atomic rename に依存) -- Claude Code の公開 API には「プラグインインストール時」hook が存在しないため、`SessionStart + startup` matcher が実用上の最適解 - -### デフォルト statusline の実装(v4.14.0) - -`SessionStart` hook で `statusline-switch.sh ensure` を実行する: - -- プラグイン同梱の `statusline.sh` を `~/.claude/ndf-statusline.sh` にコピー(差分時のみ。プラグイン更新に追従させるため、settings からはプラグインキャッシュパスではなく固定パスを参照) -- `statusLine` が未設定の場合のみ `bash ~/.claude/ndf-statusline.sh` を設定。既存設定があれば何もしない(既存優先) -- `/ndf:statusline set` は既存設定を `~/.claude/.ndf-statusline-backup.json` にバックアップしてから切り替え、`restore` で復元(バックアップ無しなら `statusLine` キーを削除) -- 書き込みは `flock` + atomic rename(保持期間管理と同パターン) - -### Codex の扱い(v4.0.0) - -- Codex MCP サーバは廃止 -- `/ndf:codex` skill に CLI 直接実行の詳細手順(サンドボックス、プロンプト設計、バックグラウンド実行、stderr/stdout 回収)を記載 -- `corder` エージェントは本 skill を参照して `codex exec` を呼び出す - -### バージョン変遷(抜粋) - -| バージョン | 主な変更 | -|-----------|---------| -| v1.0.0 | 初期リリース | -| v2.0.0 | 公式プラグインへの MCP 重複解消 | -| v2.6.0 | NDF コア MCP 最小化 (Serena + Codex) | -| v2.7.0 | commands → skills 統合 (Claude Code 2.1.3 対応) | -| v3.0.0 | Serena MCP 分離 (`mcp-serena`)、memory 系 Skill 廃止、CLAUDE.ndf.md 注入廃止 | -| v3.1.0 | Kiro CLI 対応、`google-auth` skill 追加 | -| v3.5.0 | Agent/Skill 再編、モデル階層化、公式 Skill 連携 | -| v3.6.0 | 汎用 skill 13 個追加 (原則系・PR ワークフロー系・codex) | -| v3.7.0 | transcript 保持期間自動管理 hook、`/ndf:skill-stats` skill | -| **v4.0.0 (BREAKING)** | **Codex MCP 廃止 → CLI 直接実行一本化**、レガシー CLAUDE.ndf.md 救済機構削除、skill-stats にプロジェクト別/日付範囲フィルタ追加 | -| **v4.1.0** | `playwright-scenario-test` / `google-drive` / `google-chat` skill 追加、`google-auth` v0.2.0 (永続トークン `~/.config/gcloud/google_token.json` + `get_credentials()` API + 手動 copy-paste フロー) | -| **v4.10.0** | `ml-model-structure` skill 追加 (MLモデル構築・推論API開発の標準ディレクトリ構造、版内 feature SSoT / train↔serve 契約) | -| **v4.11.0** | `/ndf:cross-review` 堅牢性改善: monitor.py EARLY_ERROR 誤検知解消 (文字列リテラル / grep ソース引用行を benign 判定) + ループ終了時の最終スイープ (残 open thread 全 Resolve) 必須化 | -| **v4.12.0** | Playwright E2E に `playwright-browser-connect` (CDP リモートブラウザ接続) / `playwright-evidence-drive` (Google Drive エビデンスアーカイブ) の 2 skill 追加 (45→47個) | -| **v4.12.1** | `/ndf:cross-review` デフォルト調整: `--max-rounds` 6→12 / `--rotate-after` 5→8、`--rotate-mode squash` 説明から「既存挙動」表記を削除 | -| **v4.13.0** | `issue-plan-strategy`: release PR body の self-contained 必須化 (レビュアー視点の原則明文化、子 PR チェックリストを `
` に格下げ、Ready 前の body 最終化ステップ追加) | -| **v4.14.0** | `statusline` skill + デフォルト statusline 設定 hook 追加 (47→48個)。statusLine 未設定時のみ NDF 標準 statusline を自動設定、`/ndf:statusline set\|restore\|status` で切替・復元 | -| **v4.15.0** | cross-review: worktree 生成先を `<システム tmpdir>/ndf-worktrees/--/pr` に変更 (非永続化 + リポジトリ別分離)。未登録パスの残骸は `.stale-` に退避して作り直すガード追加 | -| **v4.16.0** | statusline: `[ctx:` の固定ラベルを利用モデル表示名 (例 `Opus 4.8`) に置換。モデル名が取れない場合は `ctx` にフォールバック | -| **v4.16.1** | statusline: NDF 由来の旧コピー (マーカー付き / レガシー `statusline-command.sh`) を `settings.json` が指す場合、SessionStart で正規パス (`~/.claude/ndf-statusline.sh`) へ自動移行しバージョンアップ追従を回復。ユーザー独自 statusline は誤検出ガードで保護。`skills/statusline/tests/` 新設 | -| **v4.17.0** | Codex plugin 対応: Claude Code / Kiro と Codex の公開 Skill セットを分離。Claude Code / Kiro は manifest 配列指定、Codex は marketplace cache 対応の `skills-codex/` 実ディレクトリで core 28 個を公開 | -| **v4.17.1** | `cross-review` の公開漏れ修正。Claude Code/Kiro manifest と Codex `skills-codex/` に追加し、Codex core 29 個として公開 | -| **v4.17.2** | Codex Stop hook の Slack 通知を Claude 版に合わせ、メンション付き投稿後にメンションなし投稿を残してメンション付き投稿を削除 | -| **v4.17.3** | Slack 終了通知 hook の失敗時終了コードを安定化し、Stop hook 全体を `code 1` にしないよう修正 | -| **v4.18.0** | `cross-review`: Gemini 進捗ログの heartbeat 表示、`--focus` / `--extra-instructions-file`、PR 種別別の自動レビュー観点テンプレートを追加 | -| **v4.19.0** | `plan-to-spec` skill を追加。実装完了後の plan を確定仕様書へ移動・リライト・レビューする標準フローと完了報告テンプレートを定義 | +`--check` は `plugins/ndf-*` と `plugins/mcp/claude|codex|kiro` の生成物が共通編集元と同期していることを検証します。 + +## 外部 AI 委譲 + +Codex MCP サーバは廃止済みです。外部 AI 委譲は `/ndf:codex` Skill と Claude Code 版の `corder` エージェントから Codex CLI を直接呼び出す方式を標準とします。 + +## Slack 通知 + +| Runtime | 通知方法 | +|---|---| +| Claude Code | Stop hook で `plugins/ndf-claude/scripts/slack-notify.js` を実行 | +| Codex | `NDF_CODEX_SLACK_NOTIFY=true` の場合のみ Stop hook で通知 | +| Kiro CLI | `plugins/ndf-kiro/install.sh --with-slack` で通知 hook を生成 | + +機密情報は環境変数で管理し、リポジトリにはコミットしません。 + +## 関連ドキュメント + +- [Claude Code版 README](../plugins/ndf-claude/README.md) +- [Codex版 README](../plugins/ndf-codex/README.md) +- [Kiro CLI版 README](../plugins/ndf-kiro/README.md) +- [共通編集元 README](../plugins/ndf-shared/README.md) +- [runtime plugin container test plan](../issues/runtime-plugin-container-test-plan.md) diff --git a/docs/official-skills-installation.md b/docs/official-skills-installation.md index 2873d7d..d012dda 100644 --- a/docs/official-skills-installation.md +++ b/docs/official-skills-installation.md @@ -11,19 +11,19 @@ NDFプラグイン同梱のインストーラを使う方法: ```bash # 利用可能Skill一覧を表示(ライセンス分類付き) -bash plugins/ndf/scripts/install-official-skills.sh --list +bash plugins/ndf-shared/scripts/install-official-skills.sh --list # 必要なSkillをユーザー領域(~/.claude/skills/)にインストール -bash plugins/ndf/scripts/install-official-skills.sh docx pptx xlsx +bash plugins/ndf-shared/scripts/install-official-skills.sh docx pptx xlsx # プロジェクト .claude/skills/ に配置する場合 -bash plugins/ndf/scripts/install-official-skills.sh --scope project pdf +bash plugins/ndf-shared/scripts/install-official-skills.sh --scope project pdf # 全Skillをインストール -bash plugins/ndf/scripts/install-official-skills.sh --all +bash plugins/ndf-shared/scripts/install-official-skills.sh --all # 公式リポジトリを最新化 -bash plugins/ndf/scripts/install-official-skills.sh --update +bash plugins/ndf-shared/scripts/install-official-skills.sh --update ``` ### インストーラの動作 @@ -107,11 +107,11 @@ cp -r ~/work/anthropic-skills/skills/docx .claude/skills/ ### NDFが同梱している公式Skill(Apache-2.0のみ) -- `mcp-builder` → `plugins/ndf/skills/mcp-builder/`(LICENSE.txt同梱) +- `mcp-builder` → `plugins/ndf-shared/skills/mcp-builder/`(LICENSE.txt同梱) ### NDFが提供するインストーラ -- `plugins/ndf/scripts/install-official-skills.sh` → Apache-2.0 / プロプライエタリいずれも個人利用者環境にインストール可能 +- `plugins/ndf-shared/scripts/install-official-skills.sh` → Apache-2.0 / プロプライエタリいずれも個人利用者環境にインストール可能 ### NDFに独自実装を作らない方針 @@ -130,5 +130,5 @@ cp -r ~/work/anthropic-skills/skills/docx .claude/skills/ - 公式リポジトリ: https://github.com/anthropics/skills - Skills公式ドキュメント: https://docs.claude.com/en/docs/claude-code/skills -- NDFインストーラ: `plugins/ndf/scripts/install-official-skills.sh` +- NDFインストーラ: `plugins/ndf-shared/scripts/install-official-skills.sh` - Skillフロントマター仕様: [docs/claude-code-skills-official-reference.md](./claude-code-skills-official-reference.md) diff --git a/docs/plugin-development-guide.md b/docs/plugin-development-guide.md index 37495b2..4317e90 100644 --- a/docs/plugin-development-guide.md +++ b/docs/plugin-development-guide.md @@ -16,12 +16,14 @@ "plugins": [ { "name": "ndf", - "source": "./plugins/ndf" + "source": "./plugins/ndf-claude" } ] } ``` +Codex 用 marketplace では同じ plugin 名 `ndf` の source を `./plugins/ndf-codex` にします。Kiro CLI 向け配布物は `plugins/ndf-kiro/` で管理します。 + ## プラグイン構造 各プラグインは以下の構造を持ちます: @@ -91,8 +93,8 @@ plugins/{plugin-name}/ 1. **既存プラグインを参考に構造を理解** ```bash - ls -la plugins/ndf/ - cat plugins/ndf/.claude-plugin/plugin.json + ls -la plugins/ndf-claude/ + cat plugins/ndf-claude/.claude-plugin/plugin.json ``` 2. **ディレクトリ構造を作成** diff --git a/docs/specifications/issues-derived-specifications.md b/docs/specifications/issues-derived-specifications.md index 766b86e..b66a2c9 100644 --- a/docs/specifications/issues-derived-specifications.md +++ b/docs/specifications/issues-derived-specifications.md @@ -2,7 +2,7 @@ `issues/` 配下に残っていた完了済み issue、plan、report から、現行実装と一致する仕様だけを抽出した確定仕様書群。 -`issues/` は作業中の調査、実装計画、依頼文、報告書を含む開発記録として扱う。運用判断では本索引から参照する仕様、`docs/ndf-plugin-reference.md`、`plugins/ndf/README.md`、`plugins/ndf/CHANGELOG.md` を優先する。 +`issues/` は作業中の調査、実装計画、依頼文、報告書を含む開発記録として扱う。運用判断では本索引から参照する仕様、`docs/ndf-plugin-reference.md`、`plugins/ndf-claude/README.md`、`docs/ndf-plugin-reference.md` を優先する。 Skill の挙動仕様は本ディレクトリでは管理しない。Skill に関する詳細は、対象 Skill の `SKILL.md` を正とする。 @@ -29,5 +29,5 @@ Skill に関する完了済み issue / plan / report は、該当 Skill の実 - [NDF Plugin リファレンス](../ndf-plugin-reference.md) - [プラグイン開発ガイド](../plugin-development-guide.md) - [AI Plugins プロジェクト概要](../project-overview.md) -- [NDF README](../../plugins/ndf/README.md) -- [NDF CHANGELOG](../../plugins/ndf/CHANGELOG.md) +- [NDF README](../../plugins/ndf-claude/README.md) +- [NDF CHANGELOG](../../docs/ndf-plugin-reference.md) diff --git a/docs/specifications/ndf-knowledge-and-kiro.md b/docs/specifications/ndf-knowledge-and-kiro.md index e0f0724..4971ff8 100644 --- a/docs/specifications/ndf-knowledge-and-kiro.md +++ b/docs/specifications/ndf-knowledge-and-kiro.md @@ -8,7 +8,7 @@ Skill の挙動仕様は本ディレクトリでは管理しない。Skill は ` ## 仕様化の扱い -本仕様は、完了済み issues / plans の内容を統合した現行仕様である。元の `issues/*` ファイルは完了後に削除されるため、マージ後の正は本ファイル、`AGENTS.md`、`KIRO.md`、`plugins/ndf/README.md`、`docs/ndf-plugin-reference.md` とする。過去の検討履歴が必要な場合は、この仕様を追加した commit の git 履歴を参照する。 +本仕様は、完了済み issues / plans の内容を統合した現行仕様である。元の `issues/*` ファイルは完了後に削除されるため、マージ後の正は本ファイル、`AGENTS.md`、`KIRO.md`、`plugins/ndf-claude/README.md`、`docs/ndf-plugin-reference.md` とする。過去の検討履歴が必要な場合は、この仕様を追加した commit の git 履歴を参照する。 ## NDF 知識構造 @@ -61,4 +61,4 @@ Codex 連携は MCP サーバではなく `/ndf:codex` skill と `corder` エー - [mcp-serena README](../../plugins/mcp/shared/mcp-serena/README.md) - [NDF Plugin リファレンス](../ndf-plugin-reference.md) -- [NDF README](../../plugins/ndf/README.md) +- [NDF README](../../plugins/ndf-claude/README.md) diff --git a/docs/superpowers/plans/2026-05-25-playwright-skill-restructure.md b/docs/superpowers/plans/2026-05-25-playwright-skill-restructure.md index e45db06..b61dc07 100644 --- a/docs/superpowers/plans/2026-05-25-playwright-skill-restructure.md +++ b/docs/superpowers/plans/2026-05-25-playwright-skill-restructure.md @@ -18,40 +18,40 @@ | ファイル | 責務 | |---|---| -| `plugins/ndf/skills/playwright-script-creation/SKILL.md` | Phase 2 スキル: テストスクリプト作成ガイド | -| `plugins/ndf/skills/playwright-execution/SKILL.md` | Phase 3 スキル: テスト実行+エビデンス収集 (3 スキル統合) | -| `plugins/ndf/skills/playwright-kit-ops/tests/test_video_default.py` | `--pwk-no-video` オプションと動画デフォルト ON のテスト | +| `plugins/ndf-shared/skills/playwright-script-creation/SKILL.md` | Phase 2 スキル: テストスクリプト作成ガイド | +| `plugins/ndf-shared/skills/playwright-execution/SKILL.md` | Phase 3 スキル: テスト実行+エビデンス収集 (3 スキル統合) | +| `plugins/ndf-shared/skills/playwright-kit-ops/tests/test_video_default.py` | `--pwk-no-video` オプションと動画デフォルト ON のテスト | ### 変更 | ファイル | 変更内容 | |---|---| -| `plugins/ndf/skills/playwright-kit-ops/playwright_kit/pytest_plugin.py` | `--pwk-no-video` CLI オプション追加 + `pytest_configure` で動画デフォルト ON | -| `plugins/ndf/skills/playwright-kit-ops/templates/run.sh` | `--video=on` フォールバック追加 | -| `plugins/ndf/skills/playwright-kit-ops/templates/conftest.py.template` | テストスクリプト存在チェック追加 | -| `plugins/ndf/skills/playwright-test-planning/SKILL.md` | 次フェーズ導線追加 | -| `plugins/ndf/skills/playwright-report/SKILL.md` | Drive 共有セクション削除 | -| `plugins/ndf/skills/playwright-scenario-test/SKILL.md` | 5 スキル案内テーブル更新 + 大原則記載 | -| `plugins/ndf/.claude-plugin/plugin.json` | skills 配列更新 (3 削除 + 2 追加) | +| `plugins/ndf-shared/skills/playwright-kit-ops/playwright_kit/pytest_plugin.py` | `--pwk-no-video` CLI オプション追加 + `pytest_configure` で動画デフォルト ON | +| `plugins/ndf-shared/skills/playwright-kit-ops/templates/run.sh` | `--video=on` フォールバック追加 | +| `plugins/ndf-shared/skills/playwright-kit-ops/templates/conftest.py.template` | テストスクリプト存在チェック追加 | +| `plugins/ndf-shared/skills/playwright-test-planning/SKILL.md` | 次フェーズ導線追加 | +| `plugins/ndf-shared/skills/playwright-report/SKILL.md` | Drive 共有セクション削除 | +| `plugins/ndf-shared/skills/playwright-scenario-test/SKILL.md` | 5 スキル案内テーブル更新 + 大原則記載 | +| `plugins/ndf-claude/.claude-plugin/plugin.json` | skills 配列更新 (3 削除 + 2 追加) | ### 削除 | ディレクトリ | 理由 | |---|---| -| `plugins/ndf/skills/playwright-evidence/` | `playwright-execution` に統合 | -| `plugins/ndf/skills/playwright-overlay/` | `playwright-execution` に統合 | -| `plugins/ndf/skills/playwright-quality/` | `playwright-execution` に統合 | +| `plugins/ndf-shared/skills/playwright-evidence/` | `playwright-execution` に統合 | +| `plugins/ndf-shared/skills/playwright-overlay/` | `playwright-execution` に統合 | +| `plugins/ndf-shared/skills/playwright-quality/` | `playwright-execution` に統合 | --- ## Task 1: `--pwk-no-video` CLI オプション追加 + 動画デフォルト ON のテスト **Files:** -- Create: `plugins/ndf/skills/playwright-kit-ops/tests/test_video_default.py` +- Create: `plugins/ndf-shared/skills/playwright-kit-ops/tests/test_video_default.py` - [ ] **Step 1: テストファイルを作成** -`plugins/ndf/skills/playwright-kit-ops/tests/test_video_default.py` に以下を書く: +`plugins/ndf-shared/skills/playwright-kit-ops/tests/test_video_default.py` に以下を書く: ```python """--pwk-no-video オプションと動画デフォルト ON の検証。 @@ -121,14 +121,14 @@ def test_explicit_video_flag_takes_precedence(pytester): - [ ] **Step 2: テストを実行して FAIL を確認** -Run: `cd /work/ai-plugins/plugins/ndf/skills/playwright-kit-ops && uv run pytest tests/test_video_default.py -v` +Run: `cd /work/ai-plugins/plugins/ndf-shared/skills/playwright-kit-ops && uv run pytest tests/test_video_default.py -v` Expected: `test_pwk_no_video_option_registered` → FAIL (`--pwk-no-video` がまだ登録されていない)。`test_video_default_on` → FAIL (デフォルトが `on` ではない)。`test_pwk_no_video_sets_off` → FAIL。`test_explicit_video_flag_takes_precedence` → 結果は pytest-playwright の状態に依存。 - [ ] **Step 3: コミット** ```bash -git add plugins/ndf/skills/playwright-kit-ops/tests/test_video_default.py +git add plugins/ndf-shared/skills/playwright-kit-ops/tests/test_video_default.py git commit -m "test: --pwk-no-video オプションと動画デフォルト ON の failing tests 追加" ``` @@ -137,12 +137,12 @@ git commit -m "test: --pwk-no-video オプションと動画デフォルト ON ## Task 2: `pytest_plugin.py` に `--pwk-no-video` を実装して動画デフォルト ON にする **Files:** -- Modify: `plugins/ndf/skills/playwright-kit-ops/playwright_kit/pytest_plugin.py:44-93` (pytest_addoption) -- Modify: `plugins/ndf/skills/playwright-kit-ops/playwright_kit/pytest_plugin.py:110-137` (pytest_configure) +- Modify: `plugins/ndf-shared/skills/playwright-kit-ops/playwright_kit/pytest_plugin.py:44-93` (pytest_addoption) +- Modify: `plugins/ndf-shared/skills/playwright-kit-ops/playwright_kit/pytest_plugin.py:110-137` (pytest_configure) - [ ] **Step 1: `pytest_addoption` に `--pwk-no-video` を追加** -`plugins/ndf/skills/playwright-kit-ops/playwright_kit/pytest_plugin.py` の `pytest_addoption` 関数内、`--pwk-overlay` の直前に追加する: +`plugins/ndf-shared/skills/playwright-kit-ops/playwright_kit/pytest_plugin.py` の `pytest_addoption` 関数内、`--pwk-overlay` の直前に追加する: ```python group.addoption( @@ -177,20 +177,20 @@ git commit -m "test: --pwk-no-video オプションと動画デフォルト ON - [ ] **Step 3: テストを実行して PASS を確認** -Run: `cd /work/ai-plugins/plugins/ndf/skills/playwright-kit-ops && uv run pytest tests/test_video_default.py -v` +Run: `cd /work/ai-plugins/plugins/ndf-shared/skills/playwright-kit-ops && uv run pytest tests/test_video_default.py -v` Expected: 4 テスト全て PASS。 - [ ] **Step 4: 既存テストが壊れていないことを確認** -Run: `cd /work/ai-plugins/plugins/ndf/skills/playwright-kit-ops && uv run pytest -q` +Run: `cd /work/ai-plugins/plugins/ndf-shared/skills/playwright-kit-ops && uv run pytest -q` Expected: 全テスト PASS (テスト数は変動する可能性あり)。 - [ ] **Step 5: コミット** ```bash -git add plugins/ndf/skills/playwright-kit-ops/playwright_kit/pytest_plugin.py +git add plugins/ndf-shared/skills/playwright-kit-ops/playwright_kit/pytest_plugin.py git commit -m "feat: --pwk-no-video オプション追加 + 動画デフォルト ON" ``` @@ -199,11 +199,11 @@ git commit -m "feat: --pwk-no-video オプション追加 + 動画デフォル ## Task 3: `run.sh` テンプレートに `--video=on` フォールバックを追加 **Files:** -- Modify: `plugins/ndf/skills/playwright-kit-ops/templates/run.sh:80-89` (pytest 実行部分) +- Modify: `plugins/ndf-shared/skills/playwright-kit-ops/templates/run.sh:80-89` (pytest 実行部分) - [ ] **Step 1: `run.sh` の pytest 実行部分を変更** -`plugins/ndf/skills/playwright-kit-ops/templates/run.sh` の末尾 pytest 実行部分を以下に置換する: +`plugins/ndf-shared/skills/playwright-kit-ops/templates/run.sh` の末尾 pytest 実行部分を以下に置換する: ```bash # --- 3) pytest 実行 ------------------------------------------------ @@ -236,7 +236,7 @@ exec uv run pytest \ - [ ] **Step 3: コミット** ```bash -git add plugins/ndf/skills/playwright-kit-ops/templates/run.sh +git add plugins/ndf-shared/skills/playwright-kit-ops/templates/run.sh git commit -m "feat: run.sh に --video=on デフォルト + --pwk-no-video 対応追加" ``` @@ -245,11 +245,11 @@ git commit -m "feat: run.sh に --video=on デフォルト + --pwk-no-video 対 ## Task 4: conftest.py テンプレートにテストスクリプト存在チェックを追加 **Files:** -- Modify: `plugins/ndf/skills/playwright-kit-ops/templates/conftest.py.template` +- Modify: `plugins/ndf-shared/skills/playwright-kit-ops/templates/conftest.py.template` - [ ] **Step 1: テストスクリプト存在チェックを追加** -`plugins/ndf/skills/playwright-kit-ops/templates/conftest.py.template` の末尾に以下を追加する: +`plugins/ndf-shared/skills/playwright-kit-ops/templates/conftest.py.template` の末尾に以下を追加する: ```python @@ -268,7 +268,7 @@ def pytest_collection_modifyitems(session, config, items): - [ ] **Step 2: コミット** ```bash -git add plugins/ndf/skills/playwright-kit-ops/templates/conftest.py.template +git add plugins/ndf-shared/skills/playwright-kit-ops/templates/conftest.py.template git commit -m "feat: conftest テンプレートにテストスクリプト存在チェック追加" ``` @@ -277,16 +277,16 @@ git commit -m "feat: conftest テンプレートにテストスクリプト存 ## Task 5: 旧スキルディレクトリを削除 **Files:** -- Delete: `plugins/ndf/skills/playwright-evidence/` (ディレクトリごと) -- Delete: `plugins/ndf/skills/playwright-overlay/` (ディレクトリごと) -- Delete: `plugins/ndf/skills/playwright-quality/` (ディレクトリごと) +- Delete: `plugins/ndf-shared/skills/playwright-evidence/` (ディレクトリごと) +- Delete: `plugins/ndf-shared/skills/playwright-overlay/` (ディレクトリごと) +- Delete: `plugins/ndf-shared/skills/playwright-quality/` (ディレクトリごと) - [ ] **Step 1: 3 ディレクトリを削除** ```bash -git rm -r plugins/ndf/skills/playwright-evidence -git rm -r plugins/ndf/skills/playwright-overlay -git rm -r plugins/ndf/skills/playwright-quality +git rm -r plugins/ndf-shared/skills/playwright-evidence +git rm -r plugins/ndf-shared/skills/playwright-overlay +git rm -r plugins/ndf-shared/skills/playwright-quality ``` - [ ] **Step 2: コミット** @@ -300,11 +300,11 @@ git commit -m "refactor: playwright-evidence/overlay/quality を削除 (playwrig ## Task 6: `playwright-execution/SKILL.md` を新規作成 **Files:** -- Create: `plugins/ndf/skills/playwright-execution/SKILL.md` +- Create: `plugins/ndf-shared/skills/playwright-execution/SKILL.md` - [ ] **Step 1: SKILL.md を作成** -`plugins/ndf/skills/playwright-execution/SKILL.md`: +`plugins/ndf-shared/skills/playwright-execution/SKILL.md`: ```markdown --- @@ -411,7 +411,7 @@ reports// - [ ] **Step 2: コミット** ```bash -git add plugins/ndf/skills/playwright-execution/SKILL.md +git add plugins/ndf-shared/skills/playwright-execution/SKILL.md git commit -m "feat: playwright-execution スキル追加 (evidence+overlay+quality 統合)" ``` @@ -420,11 +420,11 @@ git commit -m "feat: playwright-execution スキル追加 (evidence+overlay+qual ## Task 7: `playwright-script-creation/SKILL.md` を新規作成 **Files:** -- Create: `plugins/ndf/skills/playwright-script-creation/SKILL.md` +- Create: `plugins/ndf-shared/skills/playwright-script-creation/SKILL.md` - [ ] **Step 1: SKILL.md を作成** -`plugins/ndf/skills/playwright-script-creation/SKILL.md`: +`plugins/ndf-shared/skills/playwright-script-creation/SKILL.md`: ```markdown --- @@ -540,7 +540,7 @@ fixture / marker の完全な一覧は `playwright_kit/pytest_plugin.py` の `_P - [ ] **Step 2: コミット** ```bash -git add plugins/ndf/skills/playwright-script-creation/SKILL.md +git add plugins/ndf-shared/skills/playwright-script-creation/SKILL.md git commit -m "feat: playwright-script-creation スキル追加 (テストスクリプト作成ガイド)" ``` @@ -549,11 +549,11 @@ git commit -m "feat: playwright-script-creation スキル追加 (テストスク ## Task 8: `playwright-test-planning/SKILL.md` を改修 **Files:** -- Modify: `plugins/ndf/skills/playwright-test-planning/SKILL.md` +- Modify: `plugins/ndf-shared/skills/playwright-test-planning/SKILL.md` - [ ] **Step 1: ワークフロー末尾に次フェーズ導線を追加** -`plugins/ndf/skills/playwright-test-planning/SKILL.md` のワークフロー `[E]` の後に以下を追加する: +`plugins/ndf-shared/skills/playwright-test-planning/SKILL.md` のワークフロー `[E]` の後に以下を追加する: ```markdown ▼ @@ -577,7 +577,7 @@ git commit -m "feat: playwright-script-creation スキル追加 (テストスク - [ ] **Step 3: コミット** ```bash -git add plugins/ndf/skills/playwright-test-planning/SKILL.md +git add plugins/ndf-shared/skills/playwright-test-planning/SKILL.md git commit -m "Update: playwright-test-planning にスクリプト作成フェーズへの導線追加" ``` @@ -586,11 +586,11 @@ git commit -m "Update: playwright-test-planning にスクリプト作成フェ ## Task 9: `playwright-report/SKILL.md` から Drive 共有を削除 **Files:** -- Modify: `plugins/ndf/skills/playwright-report/SKILL.md` +- Modify: `plugins/ndf-shared/skills/playwright-report/SKILL.md` - [ ] **Step 1: Drive 関連セクションを削除** -`plugins/ndf/skills/playwright-report/SKILL.md` から以下のセクションを削除する: +`plugins/ndf-shared/skills/playwright-report/SKILL.md` から以下のセクションを削除する: - `## Google Drive アップロード` セクション全体 (「### テスト実行時に自動アップロード」と「### 手動アップロード」を含む) - [ ] **Step 2: 関連 Skill セクションを更新** @@ -608,7 +608,7 @@ git commit -m "Update: playwright-test-planning にスクリプト作成フェ - [ ] **Step 3: コミット** ```bash -git add plugins/ndf/skills/playwright-report/SKILL.md +git add plugins/ndf-shared/skills/playwright-report/SKILL.md git commit -m "Update: playwright-report から Drive 共有セクションを削除" ``` @@ -617,11 +617,11 @@ git commit -m "Update: playwright-report から Drive 共有セクションを ## Task 10: `playwright-scenario-test/SKILL.md` (orchestrator) を改修 **Files:** -- Modify: `plugins/ndf/skills/playwright-scenario-test/SKILL.md` +- Modify: `plugins/ndf-shared/skills/playwright-scenario-test/SKILL.md` - [ ] **Step 1: SKILL.md を全面改修** -`plugins/ndf/skills/playwright-scenario-test/SKILL.md` の内容を以下に置換する (frontmatter を含めて全体を差し替え): +`plugins/ndf-shared/skills/playwright-scenario-test/SKILL.md` の内容を以下に置換する (frontmatter を含めて全体を差し替え): ```markdown --- @@ -695,7 +695,7 @@ Web アプリの E2E シナリオを **理論ベース** で計画し、**再現 - [ ] **Step 2: コミット** ```bash -git add plugins/ndf/skills/playwright-scenario-test/SKILL.md +git add plugins/ndf-shared/skills/playwright-scenario-test/SKILL.md git commit -m "Update: playwright-scenario-test orchestrator を 5 スキル構成に改修" ``` @@ -704,11 +704,11 @@ git commit -m "Update: playwright-scenario-test orchestrator を 5 スキル構 ## Task 11: `plugin.json` を更新 **Files:** -- Modify: `plugins/ndf/.claude-plugin/plugin.json` +- Modify: `plugins/ndf-claude/.claude-plugin/plugin.json` - [ ] **Step 1: skills 配列を更新** -`plugins/ndf/.claude-plugin/plugin.json` の `skills` 配列から以下を削除: +`plugins/ndf-claude/.claude-plugin/plugin.json` の `skills` 配列から以下を削除: ```json "./skills/playwright-evidence", "./skills/playwright-overlay", @@ -738,7 +738,7 @@ git commit -m "Update: playwright-scenario-test orchestrator を 5 スキル構 - [ ] **Step 4: コミット** ```bash -git add plugins/ndf/.claude-plugin/plugin.json +git add plugins/ndf-claude/.claude-plugin/plugin.json git commit -m "Update: plugin.json を 5 スキル構成に更新 (v4.9.0)" ``` @@ -756,7 +756,7 @@ Expected: 検証が通ること。エラーがあればその場で修正する - [ ] **Step 2: 全テストを実行** -Run: `cd /work/ai-plugins/plugins/ndf/skills/playwright-kit-ops && uv run pytest -q` +Run: `cd /work/ai-plugins/plugins/ndf-shared/skills/playwright-kit-ops && uv run pytest -q` Expected: 全テスト PASS。 diff --git a/issues/agent-runtime-plugin-split.md b/issues/agent-runtime-plugin-split.md index 9b8a2c1..a4e13dc 100644 --- a/issues/agent-runtime-plugin-split.md +++ b/issues/agent-runtime-plugin-split.md @@ -549,18 +549,17 @@ base branch: `main` | merge 済み | #47 | `feature/runtime-split-claude` | Task 2: `plugins/ndf-claude` を追加し、Claude marketplace を切替 | cross-review approved。`release/runtime-plugin-split` へ merge 済み。worktree / local branch cleanup 済み | | merge 済み | #48 | `feature/runtime-split-codex` | Task 3: `plugins/ndf-codex` を追加し、Codex marketplace を切替 | cross-review approved。`release/runtime-plugin-split` へ merge 済み。worktree / local branch cleanup 済み | | merge 済み | #49 | `feature/runtime-split-kiro` | Task 4: `plugins/ndf-kiro` を作成し、Kiro installer / docs / templates を移動 | cross-review approved。`release/runtime-plugin-split` へ merge 済み。worktree / local branch cleanup 済み | -| 未着手 | #50 | `feature/runtime-split-docs-cleanup` | Task 5: README / AGENTS / docs / specs 更新、旧 `plugins/ndf` の stub 化または削除 | PR2, PR3, PR4 merge 後 | -| 実装中 | #51 | `feature/runtime-split-mcp-plugins` | Task 6: MCP plugin を runtime 別に分離 | MCP plugin を `plugins/mcp/shared` へ移動し、Claude / Codex / Kiro runtime 生成物を追加中 | -| 未着手 | #52 | `feature/runtime-split-validation` | Task 7: validate script、dev hook、CI、リンク検証 | PR2, PR3, PR4, PR6 merge 後 | +| 実装中 | #50 | `feature/runtime-split-docs-cleanup` | Task 5: README / AGENTS / docs / specs 更新、旧 `plugins/ndf` の削除 | 旧 monolithic plugin directory を削除し、docs / README / AGENTS を runtime 分離後の構成へ更新中 | +| merge 済み | #51 / #54 | `feature/runtime-split-mcp-plugins` | Task 6: MCP plugin を runtime 別に分離 | #51 はローテーションで close。#54 が cross-review approved、`release/runtime-plugin-split` へ merge 済み | +| 未着手 | #52 | `feature/runtime-split-validation` | Task 7: validate script、dev hook、CI、リンク検証 | #50 merge 後に実施 | | 未着手 | #53 | `feature/runtime-split-container-smoke` | Task 8: runtime smoke test | PR7 merge 後 | 再開時の前提: -- 現在の作業ブランチは `feature/runtime-split-mcp-plugins`。 -- `main` は `origin/main` と同期済み。 -- `release/runtime-plugin-split` は `origin/release/runtime-plugin-split` と同期済み。 -- PR #46 / #47 / #48 / #49 用の一時 worktree は削除済み。 -- 次は PR #51 `feature/runtime-split-mcp-plugins` の実装差分をレビューし、必要な修正後に push / Draft PR 更新と cross-review を行う。 +- 現在の作業ブランチは `feature/runtime-split-docs-cleanup`。 +- `release/runtime-plugin-split` には PR #46 / #47 / #48 / #49 / #54 が merge 済み。 +- 次は PR #50 の docs cleanup を push し、cross-review 後に `release/runtime-plugin-split` へ merge する。 +- PR #50 merge 後に PR #52 `feature/runtime-split-validation`、PR #53 `feature/runtime-split-container-smoke` の順で進める。 ## 影響範囲 diff --git a/plugins/ndf-claude/skills/cross-review/tests/test_monitor_early_error.py b/plugins/ndf-claude/skills/cross-review/tests/test_monitor_early_error.py index a7e3f6b..d669c2d 100644 --- a/plugins/ndf-claude/skills/cross-review/tests/test_monitor_early_error.py +++ b/plugins/ndf-claude/skills/cross-review/tests/test_monitor_early_error.py @@ -77,7 +77,7 @@ def test_grep_style_source_citation_is_benign(tmp_path, monitor_mod): """ log = _write( tmp_path / "err.log", - '/work/worktrees/pr23/plugins/ndf/skills/cross-review/tests/' + '/work/worktrees/pr23/plugins/ndf-shared/skills/cross-review/tests/' 'test_monitor_early_error.py:22: log = _write(tmp_path / "err.log", ' '"quota exceeded: please upgrade\\n")\n', ) diff --git a/plugins/ndf-claude/skills/statusline/tests/test_statusline_switch.py b/plugins/ndf-claude/skills/statusline/tests/test_statusline_switch.py index c875a37..3fe6648 100644 --- a/plugins/ndf-claude/skills/statusline/tests/test_statusline_switch.py +++ b/plugins/ndf-claude/skills/statusline/tests/test_statusline_switch.py @@ -23,7 +23,7 @@ if shutil.which(_cmd) is None: pytest.skip(f"{_cmd} not available", allow_module_level=True) -# plugins/ndf/skills/statusline/tests/ -> plugins/ndf/scripts/statusline-switch.sh +# plugins/ndf-shared/skills/statusline/tests/ -> plugins/ndf-shared/scripts/statusline-switch.sh SWITCH = Path(__file__).resolve().parents[3] / "scripts" / "statusline-switch.sh" NDF_COMMAND = "bash ~/.claude/ndf-statusline.sh" diff --git a/plugins/ndf-codex/skills/cross-review/tests/test_monitor_early_error.py b/plugins/ndf-codex/skills/cross-review/tests/test_monitor_early_error.py index a7e3f6b..d669c2d 100644 --- a/plugins/ndf-codex/skills/cross-review/tests/test_monitor_early_error.py +++ b/plugins/ndf-codex/skills/cross-review/tests/test_monitor_early_error.py @@ -77,7 +77,7 @@ def test_grep_style_source_citation_is_benign(tmp_path, monitor_mod): """ log = _write( tmp_path / "err.log", - '/work/worktrees/pr23/plugins/ndf/skills/cross-review/tests/' + '/work/worktrees/pr23/plugins/ndf-shared/skills/cross-review/tests/' 'test_monitor_early_error.py:22: log = _write(tmp_path / "err.log", ' '"quota exceeded: please upgrade\\n")\n', ) diff --git a/plugins/ndf-codex/skills/playwright-kit-ops/SKILL.md b/plugins/ndf-codex/skills/playwright-kit-ops/SKILL.md index 991e993..327c1b0 100644 --- a/plugins/ndf-codex/skills/playwright-kit-ops/SKILL.md +++ b/plugins/ndf-codex/skills/playwright-kit-ops/SKILL.md @@ -64,7 +64,7 @@ skill を同梱しないため、Drive 系コマンドや `--pwk-drive-folder` `GOOGLE_AUTH_SCRIPTS` を `google-auth/scripts` の実パスへ設定する。 ```bash -export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf/skills/google-auth/scripts +export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf-shared/skills/google-auth/scripts cd scenario-test uv sync --extra drive ``` diff --git a/plugins/ndf-codex/skills/playwright-kit-ops/playwright_kit/uploaders/__init__.py b/plugins/ndf-codex/skills/playwright-kit-ops/playwright_kit/uploaders/__init__.py index 283d2f0..3d8f8e9 100644 --- a/plugins/ndf-codex/skills/playwright-kit-ops/playwright_kit/uploaders/__init__.py +++ b/plugins/ndf-codex/skills/playwright-kit-ops/playwright_kit/uploaders/__init__.py @@ -45,7 +45,7 @@ def _ensure_google_auth_on_path() -> None: "Google Drive 連携には optional skill `google-auth` が必要です。\n" "Codex 公開セットには同梱していないため、Drive 系コマンドを使う前に " "`GOOGLE_AUTH_SCRIPTS` を google-auth/scripts へ設定してください。\n" - "例: export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf/skills/google-auth/scripts\n" + "例: export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf-shared/skills/google-auth/scripts\n" "検索した候補:\n - " f"{searched}" ) diff --git a/plugins/ndf-codex/skills/playwright-kit-ops/scripts/_drive_auth.py b/plugins/ndf-codex/skills/playwright-kit-ops/scripts/_drive_auth.py index 647ab52..ddc873c 100644 --- a/plugins/ndf-codex/skills/playwright-kit-ops/scripts/_drive_auth.py +++ b/plugins/ndf-codex/skills/playwright-kit-ops/scripts/_drive_auth.py @@ -43,7 +43,7 @@ def _ensure_google_auth_on_path() -> None: "Google Drive 連携には optional skill `google-auth` が必要です。\n" "Codex 公開セットには同梱していないため、Drive 系コマンドを使う前に " "`GOOGLE_AUTH_SCRIPTS` を google-auth/scripts へ設定してください。\n" - "例: export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf/skills/google-auth/scripts\n" + "例: export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf-shared/skills/google-auth/scripts\n" "検索した候補:\n - " f"{searched}" ) diff --git a/plugins/ndf-codex/skills/playwright-kit-ops/scripts/upload_evidence.py b/plugins/ndf-codex/skills/playwright-kit-ops/scripts/upload_evidence.py index 0a3cd84..ebe2abc 100644 --- a/plugins/ndf-codex/skills/playwright-kit-ops/scripts/upload_evidence.py +++ b/plugins/ndf-codex/skills/playwright-kit-ops/scripts/upload_evidence.py @@ -49,7 +49,7 @@ def _ensure_google_auth_on_path() -> None: "Google Drive 連携には optional skill `google-auth` が必要です。\n" "Codex 公開セットには同梱していないため、Drive 系コマンドを使う前に " "`GOOGLE_AUTH_SCRIPTS` を google-auth/scripts へ設定してください。\n" - "例: export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf/skills/google-auth/scripts\n" + "例: export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf-shared/skills/google-auth/scripts\n" "検索した候補:\n - " f"{searched}" ) diff --git a/plugins/ndf-codex/skills/playwright-kit-ops/templates/runtime-README.md b/plugins/ndf-codex/skills/playwright-kit-ops/templates/runtime-README.md index 035bb4c..945db44 100644 --- a/plugins/ndf-codex/skills/playwright-kit-ops/templates/runtime-README.md +++ b/plugins/ndf-codex/skills/playwright-kit-ops/templates/runtime-README.md @@ -101,7 +101,7 @@ Drive 連携は optional dependency です。`--pwk-drive-folder` や Drive 系 Drive extra を同期してください。 ```bash -export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf/skills/google-auth/scripts +export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf-shared/skills/google-auth/scripts uv sync --extra drive ``` diff --git a/plugins/ndf-kiro/skills/cross-review/tests/test_monitor_early_error.py b/plugins/ndf-kiro/skills/cross-review/tests/test_monitor_early_error.py index a7e3f6b..d669c2d 100644 --- a/plugins/ndf-kiro/skills/cross-review/tests/test_monitor_early_error.py +++ b/plugins/ndf-kiro/skills/cross-review/tests/test_monitor_early_error.py @@ -77,7 +77,7 @@ def test_grep_style_source_citation_is_benign(tmp_path, monitor_mod): """ log = _write( tmp_path / "err.log", - '/work/worktrees/pr23/plugins/ndf/skills/cross-review/tests/' + '/work/worktrees/pr23/plugins/ndf-shared/skills/cross-review/tests/' 'test_monitor_early_error.py:22: log = _write(tmp_path / "err.log", ' '"quota exceeded: please upgrade\\n")\n', ) diff --git a/plugins/ndf-kiro/skills/statusline/tests/test_statusline_switch.py b/plugins/ndf-kiro/skills/statusline/tests/test_statusline_switch.py index c875a37..3fe6648 100644 --- a/plugins/ndf-kiro/skills/statusline/tests/test_statusline_switch.py +++ b/plugins/ndf-kiro/skills/statusline/tests/test_statusline_switch.py @@ -23,7 +23,7 @@ if shutil.which(_cmd) is None: pytest.skip(f"{_cmd} not available", allow_module_level=True) -# plugins/ndf/skills/statusline/tests/ -> plugins/ndf/scripts/statusline-switch.sh +# plugins/ndf-shared/skills/statusline/tests/ -> plugins/ndf-shared/scripts/statusline-switch.sh SWITCH = Path(__file__).resolve().parents[3] / "scripts" / "statusline-switch.sh" NDF_COMMAND = "bash ~/.claude/ndf-statusline.sh" diff --git a/plugins/ndf-shared/skills/cross-review/tests/test_monitor_early_error.py b/plugins/ndf-shared/skills/cross-review/tests/test_monitor_early_error.py index a7e3f6b..d669c2d 100644 --- a/plugins/ndf-shared/skills/cross-review/tests/test_monitor_early_error.py +++ b/plugins/ndf-shared/skills/cross-review/tests/test_monitor_early_error.py @@ -77,7 +77,7 @@ def test_grep_style_source_citation_is_benign(tmp_path, monitor_mod): """ log = _write( tmp_path / "err.log", - '/work/worktrees/pr23/plugins/ndf/skills/cross-review/tests/' + '/work/worktrees/pr23/plugins/ndf-shared/skills/cross-review/tests/' 'test_monitor_early_error.py:22: log = _write(tmp_path / "err.log", ' '"quota exceeded: please upgrade\\n")\n', ) diff --git a/plugins/ndf-shared/skills/official-skills-autoloader/SKILL.md b/plugins/ndf-shared/skills/official-skills-autoloader/SKILL.md index a184bbe..40a852e 100644 --- a/plugins/ndf-shared/skills/official-skills-autoloader/SKILL.md +++ b/plugins/ndf-shared/skills/official-skills-autoloader/SKILL.md @@ -93,7 +93,7 @@ Read(file_path="$HOME/.claude/skills//SKILL.md") - cache: `~/.cache/anthropic-skills/` (XDG準拠) - リンク先: `~/.claude/skills//` (ユーザー領域) -- プロジェクト単位で配置したい場合は `plugins/ndf/scripts/install-official-skills.sh --scope project ` を直接実行 +- プロジェクト単位で配置したい場合は `plugins/ndf-shared/scripts/install-official-skills.sh --scope project ` を直接実行 ### 再読込 @@ -101,9 +101,9 @@ Read(file_path="$HOME/.claude/skills//SKILL.md") ### 手動管理したい場合 -- 一覧表示: `bash plugins/ndf/scripts/install-official-skills.sh --list` -- 更新: `bash plugins/ndf/scripts/install-official-skills.sh --update` -- 明示的なインストール: `bash plugins/ndf/scripts/install-official-skills.sh ` +- 一覧表示: `bash plugins/ndf-shared/scripts/install-official-skills.sh --list` +- 更新: `bash plugins/ndf-shared/scripts/install-official-skills.sh --update` +- 明示的なインストール: `bash plugins/ndf-shared/scripts/install-official-skills.sh ` ## エラーハンドリング diff --git a/plugins/ndf-shared/skills/playwright-kit-ops/SKILL.md b/plugins/ndf-shared/skills/playwright-kit-ops/SKILL.md index 991e993..327c1b0 100644 --- a/plugins/ndf-shared/skills/playwright-kit-ops/SKILL.md +++ b/plugins/ndf-shared/skills/playwright-kit-ops/SKILL.md @@ -64,7 +64,7 @@ skill を同梱しないため、Drive 系コマンドや `--pwk-drive-folder` `GOOGLE_AUTH_SCRIPTS` を `google-auth/scripts` の実パスへ設定する。 ```bash -export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf/skills/google-auth/scripts +export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf-shared/skills/google-auth/scripts cd scenario-test uv sync --extra drive ``` diff --git a/plugins/ndf-shared/skills/playwright-kit-ops/playwright_kit/uploaders/__init__.py b/plugins/ndf-shared/skills/playwright-kit-ops/playwright_kit/uploaders/__init__.py index 283d2f0..3d8f8e9 100644 --- a/plugins/ndf-shared/skills/playwright-kit-ops/playwright_kit/uploaders/__init__.py +++ b/plugins/ndf-shared/skills/playwright-kit-ops/playwright_kit/uploaders/__init__.py @@ -45,7 +45,7 @@ def _ensure_google_auth_on_path() -> None: "Google Drive 連携には optional skill `google-auth` が必要です。\n" "Codex 公開セットには同梱していないため、Drive 系コマンドを使う前に " "`GOOGLE_AUTH_SCRIPTS` を google-auth/scripts へ設定してください。\n" - "例: export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf/skills/google-auth/scripts\n" + "例: export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf-shared/skills/google-auth/scripts\n" "検索した候補:\n - " f"{searched}" ) diff --git a/plugins/ndf-shared/skills/playwright-kit-ops/scripts/_drive_auth.py b/plugins/ndf-shared/skills/playwright-kit-ops/scripts/_drive_auth.py index 647ab52..ddc873c 100644 --- a/plugins/ndf-shared/skills/playwright-kit-ops/scripts/_drive_auth.py +++ b/plugins/ndf-shared/skills/playwright-kit-ops/scripts/_drive_auth.py @@ -43,7 +43,7 @@ def _ensure_google_auth_on_path() -> None: "Google Drive 連携には optional skill `google-auth` が必要です。\n" "Codex 公開セットには同梱していないため、Drive 系コマンドを使う前に " "`GOOGLE_AUTH_SCRIPTS` を google-auth/scripts へ設定してください。\n" - "例: export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf/skills/google-auth/scripts\n" + "例: export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf-shared/skills/google-auth/scripts\n" "検索した候補:\n - " f"{searched}" ) diff --git a/plugins/ndf-shared/skills/playwright-kit-ops/scripts/upload_evidence.py b/plugins/ndf-shared/skills/playwright-kit-ops/scripts/upload_evidence.py index 0a3cd84..ebe2abc 100644 --- a/plugins/ndf-shared/skills/playwright-kit-ops/scripts/upload_evidence.py +++ b/plugins/ndf-shared/skills/playwright-kit-ops/scripts/upload_evidence.py @@ -49,7 +49,7 @@ def _ensure_google_auth_on_path() -> None: "Google Drive 連携には optional skill `google-auth` が必要です。\n" "Codex 公開セットには同梱していないため、Drive 系コマンドを使う前に " "`GOOGLE_AUTH_SCRIPTS` を google-auth/scripts へ設定してください。\n" - "例: export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf/skills/google-auth/scripts\n" + "例: export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf-shared/skills/google-auth/scripts\n" "検索した候補:\n - " f"{searched}" ) diff --git a/plugins/ndf-shared/skills/playwright-kit-ops/templates/runtime-README.md b/plugins/ndf-shared/skills/playwright-kit-ops/templates/runtime-README.md index 035bb4c..945db44 100644 --- a/plugins/ndf-shared/skills/playwright-kit-ops/templates/runtime-README.md +++ b/plugins/ndf-shared/skills/playwright-kit-ops/templates/runtime-README.md @@ -101,7 +101,7 @@ Drive 連携は optional dependency です。`--pwk-drive-folder` や Drive 系 Drive extra を同期してください。 ```bash -export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf/skills/google-auth/scripts +export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf-shared/skills/google-auth/scripts uv sync --extra drive ``` diff --git a/plugins/ndf-shared/skills/skill-stats/scripts/skill-stats.py b/plugins/ndf-shared/skills/skill-stats/scripts/skill-stats.py index 4a7e718..069488a 100755 --- a/plugins/ndf-shared/skills/skill-stats/scripts/skill-stats.py +++ b/plugins/ndf-shared/skills/skill-stats/scripts/skill-stats.py @@ -28,7 +28,7 @@ def plugin_root_default() -> pathlib.Path: env = os.environ.get("CLAUDE_PLUGIN_ROOT") if env: return pathlib.Path(env) - # scripts/skill-stats.py -> plugins/ndf/skills/skill-stats/scripts/ + # scripts/skill-stats.py -> plugins/ndf-shared/skills/skill-stats/scripts/ return pathlib.Path(__file__).resolve().parents[3] diff --git a/plugins/ndf-shared/skills/statusline/tests/test_statusline_switch.py b/plugins/ndf-shared/skills/statusline/tests/test_statusline_switch.py index c875a37..3fe6648 100644 --- a/plugins/ndf-shared/skills/statusline/tests/test_statusline_switch.py +++ b/plugins/ndf-shared/skills/statusline/tests/test_statusline_switch.py @@ -23,7 +23,7 @@ if shutil.which(_cmd) is None: pytest.skip(f"{_cmd} not available", allow_module_level=True) -# plugins/ndf/skills/statusline/tests/ -> plugins/ndf/scripts/statusline-switch.sh +# plugins/ndf-shared/skills/statusline/tests/ -> plugins/ndf-shared/scripts/statusline-switch.sh SWITCH = Path(__file__).resolve().parents[3] / "scripts" / "statusline-switch.sh" NDF_COMMAND = "bash ~/.claude/ndf-statusline.sh" diff --git a/plugins/ndf/.claude-plugin/plugin.json b/plugins/ndf/.claude-plugin/plugin.json deleted file mode 100644 index 7145417..0000000 --- a/plugins/ndf/.claude-plugin/plugin.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "ndf", - "version": "4.19.0", - "description": "Integrated plugin with 8 specialized agents and a focused core skill set for PR/review workflows, implementation planning, debugging principles, statusline, browser smoke testing, and Codex CLI delegation. Additional niche skills remain under skills-optional for maintainers. Transcript retention is automatically kept at >= 90 days. Default statusline is set when none is configured.", - "author": { - "name": "takemi-ohama", - "url": "https://github.com/takemi-ohama" - }, - "keywords": [ - "mcp", - "codex", - "workflow", - "slack", - "notifications", - "agents", - "skills", - "director", - "orchestrator" - ], - "agents": [ - "./agents/director.md", - "./agents/data-analyst.md", - "./agents/corder.md", - "./agents/researcher.md", - "./agents/qa.md", - "./agents/debugger.md", - "./agents/devops-engineer.md", - "./agents/code-reviewer.md" - ], - "skills": [ - "./skills/pr", - "./skills/pr-tests", - "./skills/fix", - "./skills/review", - "./skills/cross-review", - "./skills/merged", - "./skills/clean", - "./skills/ndf-policies", - "./skills/markdown-writing", - "./skills/python-execution", - "./skills/docker-container-access", - "./skills/git-gh-operations", - "./skills/branch-fix-strategy", - "./skills/implementation-plan", - "./skills/investigation-rules", - "./skills/problem-solving", - "./skills/logging-guidelines", - "./skills/sync-main", - "./skills/cherry-pick-pr", - "./skills/deploy", - "./skills/review-branch", - "./skills/review-pr-comments", - "./skills/resolve-pr-comments", - "./skills/browser-test", - "./skills/codex", - "./skills/statusline", - "./skills/issue-plan-strategy", - "./skills/plan-to-spec" - ] -} diff --git a/plugins/ndf/.codex-plugin/plugin.json b/plugins/ndf/.codex-plugin/plugin.json deleted file mode 100644 index f5ee875..0000000 --- a/plugins/ndf/.codex-plugin/plugin.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "ndf", - "version": "4.19.0", - "description": "NDF workflows for PRs, reviews, testing, data analysis, Google integrations, and external AI delegation.", - "skills": "./skills-codex/", - "hooks": "./hooks/codex-hooks.json" -} diff --git a/plugins/ndf/AGENTS.md b/plugins/ndf/AGENTS.md deleted file mode 100644 index cdf38e4..0000000 --- a/plugins/ndf/AGENTS.md +++ /dev/null @@ -1,86 +0,0 @@ -# NDF Plugin - 開発者向けガイドライン - -## 概要 - -**NDFプラグインの開発・メンテナンス**を行うAIエージェント向けガイドライン。 - -## プラグイン情報 - -- **名前**: ndf -- **現在バージョン**: 4.19.0 -- **種類**: 統合プラグイン(Skills + Agents + Hooks / v4.0.0 で Codex MCP 廃止) -- **リポジトリ**: https://github.com/devbasex/ai-plugins - -> **Note (v3.0.0)**: Serena MCPは`mcp-serena`プラグインに分離。memory系スキルは廃止。CLAUDE.ndf.md注入は廃止。 - -## 開発ルール - -- ドキュメント・コミットメッセージ・PR説明は**日本語** -- **mainブランチへの直接コミット禁止**(featureブランチ+PR) -- **セマンティックバージョニング**: MAJOR(破壊的変更)、MINOR(新機能)、PATCH(バグ修正) - -## ディレクトリ構造 - -``` -plugins/ndf/ -├── .claude-plugin/ -│ └── plugin.json # プラグインメタデータ -├── .codex-plugin/ -│ └── plugin.json # Codexプラグインメタデータ -├── hooks/ -│ ├── hooks.json # Claude Codeプロジェクトフック定義 -│ └── codex-hooks.json # Codex hook定義 -├── scripts/ -│ └── slack-notify.js # Slack通知スクリプト -├── agents/ # サブエージェント(8個、モデル階層化) -│ ├── director.md # opus: 計画・統括 -│ ├── corder.md # sonnet: Codex第二意見レビュー -│ ├── data-analyst.md # sonnet: BigQuery/SQL -│ ├── researcher.md # sonnet: AWS Docs/Chrome DevTools -│ ├── qa.md # sonnet: セキュリティ/品質 -│ ├── debugger.md # sonnet: 根本原因分析 -│ ├── devops-engineer.md # sonnet: Docker/CI/K8s -│ └── code-reviewer.md # sonnet: diff/PRレビュー -├── skills/ # 全Skill実体(49個、Claude Code/Kiroはmanifest配列で公開対象を指定) -├── skills-codex/ # Codex向け公開Skill(core 30個、marketplace cache向け実ディレクトリ) -├── skills-optional/ # ランタイム別除外候補リスト -├── AGENTS.md # このファイル(開発者向け) -└── README.md # プラグイン説明書 -``` - -## 一般的な開発タスク - -### 新しいスキルの追加 - -1. `skills/{skill-name}/SKILL.md` を作成(YAMLフロントマター必須) -2. Claude Code/Kiroで初期公開する場合は `.claude-plugin/plugin.json` の `skills` 配列に `"./skills/{skill-name}"` を追加 -3. Codexで初期公開する場合は `skills-codex/{skill-name}` に実ディレクトリとしてコピーする(`.codex-plugin/plugin.json` は `./skills-codex/` ディレクトリを参照) -4. 低頻度・保守用に留める場合は `skills-optional/README.md` の候補リストへ追加 -5. plugin.json のバージョンをMINOR上げ -6. テスト・コミット - -### 新しいサブエージェントの追加 - -1. `agents/{agent-name}.md` を作成(YAMLフロントマター必須) -2. `plugin.json` の `agents` 配列に追加 -3. バージョンMINOR上げ → テスト・コミット - -## 検証チェックリスト - -- [ ] plugin.jsonが有効なJSON -- [ ] バージョン番号が適切にインクリメント -- [ ] すべてのスキル/エージェントファイルが存在 -- [ ] YAMLフロントマターが正しい -- [ ] README.md が最新 - -## トラブルシューティング - -| 問題 | 対処 | -|------|------| -| エージェントが認識されない | plugin.jsonのagents配列、ファイルパス、YAMLフロントマターを確認 | -| スキルが表示されない | plugin.jsonのskills配列、SKILL.mdのフロントマターを確認、`/plugin reload ndf` | -| フックが動作しない | hooks.jsonの構文、スクリプト実行権限を確認 | - -## 開発履歴 - -バージョン履歴は [CHANGELOG.md](CHANGELOG.md) を参照。 diff --git a/plugins/ndf/CHANGELOG.md b/plugins/ndf/CHANGELOG.md deleted file mode 100644 index 579e185..0000000 --- a/plugins/ndf/CHANGELOG.md +++ /dev/null @@ -1,1002 +0,0 @@ -# NDF Plugin CHANGELOG - -### v4.19.0 (plan-to-spec: plan の確定仕様書化フローを追加) - -- **`plan-to-spec` skill 追加**: - 実装完了後の plan を `docs/` 配下などの確定仕様書置き場へ移動し、 - 開発履歴・TODO・PR 分割などを削除した as-is 仕様書としてリライトする - 標準フローを定義した。 -- **仕様書レビュー手順**: - 他仕様書との体裁統一、コードとの一致確認、authorize された用語への統一、 - リンク再調査を必須観点として明文化した。 -- **完了報告テンプレート**: - 元 plan、確定仕様書、実施内容、レビュー結果、検証、補足を共通形式で - 報告するテンプレートを追加した。 -- **plugin.json: version 4.18.0 → 4.19.0**。marketplace.json / README / - AGENTS.md / ndf-plugin-reference の version 表記と公開 Skill 数を整合。 - -### v4.18.0 (cross-review: 監視性とレビュー観点テンプレート強化) - -- **`cross-review` / Gemini 監視性改善**: - `launch-gemini.sh` が Gemini に短い進捗マーカー - (`gemini-review-pr-progress.log`) の出力を要求し、`monitor.py` が - heartbeat に stdout / stderr / progress log サイズ、無更新秒数、最新フェーズを表示するようにした。 - 内部推論ではなく `scan` / `analyze` / `post` / `done` などの作業段階だけを出す。 -- **`cross-review` / 追加レビュー観点**: - `state.py init` に `--focus` と `--extra-instructions-file` を追加し、 - codex / gemini 両 launcher が同じ追加観点をプロンプトへ差し込めるようにした。 -- **`cross-review` / 自動レビュー観点テンプレート**: - GitHub API の `pulls//files --paginate` の変更ファイルから PR 種別を分類し、ドキュメントのみ、 - コード、DB migration、テスト、依存関係、CI/設定、API 契約、認証/認可、frontend、 - 性能、削除/リネーム、生成物/i18n/infra の該当テンプレートを自動で追加するようにした。 -- **`monitor.py`**: stall 判定対象を err.log + stdout.log + progress.log に拡張。 -- **plugin.json: version 4.17.3 → 4.18.0**。marketplace.json / README / - AGENTS.md / ndf-plugin-reference の version 表記を整合。 - -### v4.17.3 (Stop hook: Slack通知失敗時の終了コードを安定化) - -Codex 環境で Claude 用 Slack 通知 hook が混入した場合や、Slack API / -ネットワークが一時失敗した場合に、Stop hook 全体が `code 1` で失敗表示に -なる問題を修正した。 - -- **`hooks/codex-hooks.json`**: Codex 用通知スクリプトを `sh -c` 経由で起動し、 - `PLUGIN_ROOT` がない環境では no-op、通知処理の失敗も Stop hook 失敗にしない -- **`hooks/hooks.json`**: Claude 用 Stop hook に `continueOnError` を追加 -- **`scripts/slack-notify.js`**: Slack 通知失敗・致命例外時も終了通知 hook としては - `exit 0` を返し、作業完了フローを阻害しない -- **plugin.json: version 4.17.2 → 4.17.3**。marketplace.json / README / - AGENTS.md / ndf-plugin-reference の version 表記を整合 - -### v4.17.2 (Codex Slack通知: メンション付き投稿の削除を修正) - -Codex Stop hook の Slack 通知で、メンション付きメッセージ削除ロジックが -動いていなかった問題を修正した。 - -- **`scripts/codex-slack-notify.js`**: Claude 版と同様に - `chat.postMessage` の戻り値 `ts` を保持し、メンション付き投稿後に - メンションなし投稿を送信してから `chat.delete` でメンション付き投稿を削除 -- **plugin.json: version 4.17.1 → 4.17.2**。marketplace.json / README / - AGENTS.md / ndf-plugin-reference の version 表記を整合 - -### v4.17.1 (Codex/Claude: cross-review 公開漏れ修正) - -NDF の主要 PR ワークフローである `cross-review` が、Claude Code/Kiro の -manifest と Codex の `skills-codex/` 公開セットから漏れていた問題を修正した。 - -- **`.claude-plugin/plugin.json`**: `./skills/cross-review` を公開 Skill に追加 -- **`skills-codex/cross-review/`**: marketplace cache で欠落しないよう実ディレクトリとして同梱 -- **`skills-optional/README.md`**: `cross-review` を optional から除外 -- **plugin.json: version 4.17.0 → 4.17.1**。marketplace.json / README / AGENTS.md / - ndf-plugin-reference の version 表記と公開 Skill 数を整合 - -### v4.17.0 (Codex plugin 対応: ランタイム別 Skill 公開構成) - -Claude Code / Kiro と Codex で公開する Skill セットを分離し、Codex plugin から -不要な Claude Code 専用 Skill を読み込まない構成にした。 - -- **`.codex-plugin/plugin.json`**: Codex 向け manifest を追加し、`skills-codex/` - を Codex の公開 Skill ディレクトリとして指定 -- **`.claude-plugin/plugin.json`**: Claude Code / Kiro 向け公開 Skill を core 26 個に整理 -- **`skills-codex/`**: Codex 向け core 28 個を marketplace cache で欠落しない - 実ディレクトリとして同梱 - (`playwright-kit-ops` を含め、Playwright 系 skill から参照するスクリプト群も同梱) -- **`skills-optional/README.md`**: ランタイム別の除外候補を整理 -- **`scripts/install-kiro.sh`**: manifest-selected skill set を読むように更新し、 - Codex CLI 連携表示を MCP 前提から CLI 直接実行前提へ修正 -- **plugin.json: version 4.16.1 → 4.17.0**。marketplace.json / README / AGENTS.md / - ndf-plugin-reference の version 表記を整合 - -### v4.16.1 (statusline: NDF 由来の旧コピーをバージョンアップ追従させる) - -`settings.json` の `statusLine` が NDF の過去配置コピー (マーカー付き / -レガシー `statusline-command.sh`) を指している場合、SessionStart hook が -それを検出できずスクリプト更新が届かない問題を修正した。 - -- **`statusline.sh`**: 先頭に識別マーカー `# ndf-statusline: managed` を追加。 - `deploy_script` が配置する `~/.claude/ndf-statusline.sh` にも自動的に入る -- **`statusline-switch.sh`**: `cmd_ensure` に移行分岐を追加 - - `current_script_path`: `statusLine.command` から実行スクリプトのパスを抽出 - (GNU 拡張の `grep -o` を避け、BusyBox でも動く `sed` で抽出) - - `is_ndf_managed_copy`: ① マーカーがあれば NDF 管理コピー確定、 - ② レガシー救済として既知名 `statusline-command.sh` かつ NDF 特有ロジック - (`[ctx:` + `container_name`) を両方含む場合のみ対象 - (マーカー/特徴の検出は `grep -F` の固定文字列検索で正規表現メタ文字を回避) - - `migrate_to_ndf_statusline`: 既存設定を `~/.claude/.ndf-statusline-backup.json` - に退避し、正規パス参照へ移行。以後は `deploy_script` の更新ルートに乗る - - マーカーも特徴も持たないユーザー独自 statusline は誤検出ガードで一切上書きしない -- **`skills/statusline/tests/`**: 新設。移行/尊重/誤検出ガードの 6 シナリオを - pytest で検証 (`uv run --with pytest python -m pytest`) -- **plugin.json: version 4.16.0 → 4.16.1**。marketplace.json / AGENTS.md / - ndf-plugin-reference の version 表記を整合 - -### v4.16.0 (statusline: 固定 `ctx:` ラベルを利用モデル名に置換) - -statusline の `[ctx: ...]` 部分の固定ラベル `ctx:` が常時同じ文字で表示スペースの -無駄になっていたため、利用中モデルの表示名に置き換えた。 - -- **`statusline.sh`**: `model.display_name` (なければ `model.id`) を取得し、 - 角括弧内のラベルに使用。`[Opus 4.8: 44.6k / 1000k tokens (4%)]` のように表示 - - モデル名が取得できない場合は従来どおり `ctx` にフォールバック - - トークン情報 (`context_window`) が無い場合の非表示挙動は従来どおり -- **`statusline` skill SKILL.md**: 表示例をモデル名ラベルに更新 -- **plugin.json: version 4.15.0 → 4.16.0**。marketplace.json / AGENTS.md / - ndf-plugin-reference の version 表記を整合。 - -### v4.15.0 (cross-review: worktree を非永続領域 + リポジトリ別パスに変更) - -cross-review の worktree 生成先 `/work/worktrees/pr` が共有の永続 volume 上にあり、 -(1) 別リポジトリの同一 PR 番号と衝突する (2) 明示削除しないと残り続ける -(3) volume スペースを消費する、という問題に対応。実際に別プロジェクトの残骸 -worktree を流用して git 操作が壊れる事故が発生した。 - -- **デフォルト生成先を `<システム tmpdir>/ndf-worktrees` に変更** (`state.py`): - - 解決順: `NDF_WORKTREE_BASE` env > `tempfile.gettempdir()/ndf-worktrees` - - 非永続領域のためコンテナ再作成で自動消滅し、明示削除も不要 - - 旧 `/work/worktrees` / `$HOME/work/worktrees` フォールバックは廃止 -- **パスにリポジトリ slug を含める**: `/--/pr` 形式とし、 - 他リポジトリの同一 PR 番号と衝突しないようにした -- **残骸 worktree の流用ガード追加**: パスが存在しても `git worktree list` に - 登録されていなければ `.stale-` に退避して作り直す - (`_is_registered_worktree()` / `_create_worktree()` に分離) -- **テスト更新**: `test_default_worktree_base.py` を新解決順 + repo slug + - 残骸ガードのテストに刷新 (102 passed) -- **plugin.json: version 4.14.0 → 4.15.0**。marketplace.json / AGENTS.md / - ndf-plugin-reference の version 表記を整合。 - -### v4.14.0 (statusline skill + デフォルト statusline 設定 hook 追加) - -コンテナ名 (非コンテナ環境ではホスト名) + project_dir + コンテキスト使用率を表示する -NDF 標準 statusline をプラグインに同梱し、インストールするだけでデフォルトになるようにした。 - -- **`statusline` skill 追加** (`/ndf:statusline`、47→48個): - - `status` (デフォルト): 現在の statusLine 設定とバックアップの有無を表示 - - `set`: 既存設定を `~/.claude/.ndf-statusline-backup.json` にバックアップして - NDF 標準 statusline に切り替え - - `restore`: バックアップから復元 (バックアップ無しなら statusLine キーを削除) -- **SessionStart hook 追加** (`scripts/statusline-switch.sh ensure`): - - `statusLine` が**未設定の場合のみ** NDF 標準 statusline を設定。 - 既に設定されている場合はそちらを優先し、何も変更しない - - プラグイン同梱の `scripts/statusline.sh` を `~/.claude/ndf-statusline.sh` に - 差分時のみコピーし、プラグイン更新へ自動追従 - (settings からはプラグインキャッシュパスでなく固定パスを参照) - - 書き込みは flock + atomic rename (ensure-retention.sh と同パターン) -- **plugin.json: version 4.13.0 → 4.14.0**。marketplace.json / README / AGENTS.md / - ndf-plugin-reference の version 表記を整合。 - -### v4.13.0 (issue-plan-strategy: release PR body の self-contained 必須化) - -release ブランチ戦略において、release PR の body が「個別 PR リンクの列挙 + plan 参照」 -だけになり、人間のレビュアーが PR 単体で変更を把握できない問題に対応 (issue #28)。 - -- **レビュアー視点の原則を明文化** (`skills/issue-plan-strategy/SKILL.md` Step 3): - - 個別 PR はセルフレビューで merge される。人間のレビュアーが見るのは release PR - だけであり、個別 PR の存在をレビュアーに意識させない -- **release PR body テンプレートを self-contained 化**: - - 「何のために (背景)」「何を (release 全体の変更内容)」を本文の中心に変更 - - 個別 PR チェックリストは `
` 折りたたみ内の開発用補足情報に格下げ -- **body 最終化ステップを Step 8 に追加**: - - 個別 PR 全 merge 後・Draft 解除前に、実装の最終形を反映した body へ更新する - 工程とチェック観点を明記 (cross-review light rotation と同等の原則を適用) -- **アンチパターン 2 件追加**: 「body を個別 PR リンクの列挙だけにする」 - 「body 最終化せずに Ready for review にする」 -- **plugin.json: version 4.12.1 → 4.13.0**。marketplace.json / README / AGENTS.md / - ndf-plugin-reference の version 表記を整合。 - -### v4.12.1 (cross-review デフォルト値調整・説明文整理) - -`/ndf:cross-review` のローテーション/打ち切り関連デフォルトを長尺 PR 向けに緩和し、 -`--rotate-mode` の説明文を整理。 - -- **デフォルト値変更** (`skills/cross-review/scripts/state.py`): - - `--max-rounds` の既定値を `6 → 12` に変更 - - `--rotate-after` の既定値を `5 → 8` に変更 - - 長尺・指摘の多い PR で max_rounds 打ち切りや早期ローテーションに - 到達しにくくし、収束まで回しきれるようにする狙い -- **`--rotate-mode squash` 説明文の整理**: - - SKILL.md / docs / scripts から「既存挙動」の文言を全削除 - - 反映: `SKILL.md` / `docs/01-state-and-review.md` (サンプル state.json) / - `docs/02-fix-and-rotation.md` / `scripts/rotate-pr.sh` -- **plugin.json: version 4.12.0 → 4.12.1**。marketplace.json / README / AGENTS.md / - ndf-plugin-reference の version 表記を整合。 - -### v4.12.0 (playwright-browser-connect / playwright-evidence-drive Skill 追加) - -`/ndf:playwright-browser-connect` と `/ndf:playwright-evidence-drive` を新規 Skill -として追加。CDP リモートブラウザ接続と Google Drive エビデンスアーカイブを -playwright-scenario-test エコシステムから独立した専門 Skill に分離。 - -- **新規 Skill `playwright-browser-connect`**: - - `scenario.config.yaml` の `browser.mode: cdp-remote` で既存 Chrome に - CDP (Chrome DevTools Protocol) 接続するワークフローを提供 - - `conftest.py.template` に browser fixture を追加 (mode 切り替え) - - `config.py` に `BrowserConfig` dataclass を追加 -- **新規 Skill `playwright-evidence-drive`**: - - テスト実行後のエビデンス (video/trace/screenshot/HAR) を Google Drive に - 自動アーカイブする手順を提供 - - 認証は `ndf:google-auth` に委譲 -- **conftest.py.template の改善** (PR #19 クロスレビュー対応): - - `browser_type_launch_args` を browser fixture の依存に追加し、 - `--headed` / `slow_mo` / `channel` 等が local モードで反映されるよう修正 - - CDP 接続時に Chromium 以外のブラウザが選択された場合の fail-fast チェックを追加 - - 変数名 `b` → `browser` に改名 (可読性向上) -- **config.py の修正** (PR #19 クロスレビュー対応): - - `BrowserConfig.cdp_endpoint` の既定値を `ws://localhost:9222` → - `http://localhost:9222` に変更 (Playwright の `connect_over_cdp()` は - HTTP endpoint から `/json/version` 経由で WebSocket URL を自動解決する) -- **plugin.json: version 4.11.0 → 4.12.0**。marketplace.json / README / AGENTS.md / - ndf-plugin-reference の version 表記を整合。 -- Skills: 45個 → **47個** (main の ml-model-structure 追加分とマージ) - -### v4.11.0 (fix/cross-review-early-error-and-final-sweep: cross-review 堅牢性改善) - -`/ndf:cross-review` の 2 つの運用課題を修正。 - -- **monitor.py の EARLY_ERROR 誤検知を解消** - (`skills/cross-review/scripts/monitor.py`): - - `_match_is_quoted()` を拡張し、backtick / 「」 に加え **ダブル/シングルクォート - 文字列リテラル** (`"quota exceeded: ..."`) も benign 判定。 - - `EARLY_ERROR_BENIGN` に **grep/ripgrep 形式のソース引用行** (`path/file.py:22: `) - パターンを追加。 - - codex がレビュー中に tests/*.py のテスト用文字列 (`"quota exceeded"` 等) を echo - しても誤 kill しなくなった(PR #23 round 2 で実際に発生した事例の回帰防止テスト追加)。 -- **ループ終了時の最終スイープ (Step 7.5) を必須化** - (`skills/cross-review/SKILL.md` / `docs/02-fix-and-rotation.md`): - - approved / max_rounds / oscillation / error いずれの終了経路でも `/ndf:fix` を再実行し、 - 残った open review thread(最終 APPROVE ラウンドの minor/nit インラインコメント含む)を - **すべて Resolve** してから終了する。deferred nit も reply + resolveReviewThread で解消。 - - 従来の「deferred nit を最後にユーザ問い合わせ」を「最終スイープで全 thread Resolve + - report に参考列挙」へ変更。 -- **plugin.json: version 4.10.0 → 4.11.0**。marketplace.json / README / AGENTS.md / - ndf-plugin-reference の version 表記を整合(skill 数 45 は不変)。 - -### v4.10.0 (feature/add-ml-model-structure-skill: ml-model-structure skill 追加) - -機械学習モデル構築・推論API開発の標準ディレクトリ構造を提供する新規 skill を追加。 -skill 数を 45 個に更新し、関連ドキュメントのバージョン・skill 数表記を整合。 - -- **新規 skill `ml-model-structure` 追加** - (`skills/ml-model-structure/`): - - 機械学習モデルの版ごと自己完結構造 + 版内 feature SSoT + 推論コンテナ規約。 - - `references/inference-and-contract.md` に train↔serve 契約・推論コンテナ規約を分離。 -- **plugin.json: version 4.9.0 → 4.10.0、skills 配列に `ml-model-structure` を追加**、 - description を 45 skills に更新。 -- **marketplace.json / README / AGENTS.md / ndf-plugin-reference: skill 数・version 表記を整合**。 - -### v4.7.6 (fix/review-pr-comments: PRコメント取得を3ソース対応に拡張) - -PRレビューコメント取得が review body / PR レベルコメントを含む3ソースに対応。 -cross-review の state.py も堅牢性改善。 - -- **fetch-pr-comments.sh: 3ソース対応の新規スクリプト追加** - (`skills/fix/scripts/fetch-pr-comments.sh`): - - review comments / review body / PR-level comments の3ソースから取得。 - - `gh api` の `-q` フラグ誤用を修正し raw JSON → `jq -r` パイプに統一。 - - 取得失敗時に `die()` で中断するエラーハンドリング追加。 -- **state.py: 再開検知・パス解決の堅牢化** - (`skills/cross-review/scripts/state.py`): - - `cmd_init` の再開検知で `CROSS_REVIEW_TMP_DIR` 環境変数を考慮。 - - `_state_path()` が `_tmp_dir()` を直接使用するよう修正(読み込み元と参照先の乖離防止)。 - - `_resolve_tmp_dir` から mkdir 副作用を除去(`p.exists()` チェックに変更)。 - - `st.get()` の None フォールバックを `or ""` で安全化。 -- **fix/SKILL.md, review-pr-comments/SKILL.md: 3ソース対応の説明更新**。 -- **cross-review SKILL.md, docs: パス記述の微修正**。 - -### v4.7.5 (cross-review: TMP_DIR を worktree 内 .cross_review/ に統一) - -レビュースクリプトが一時ファイルを書き出す先を、従来の `~/.gemini/tmp/` や `/tmp/` -から **worktree 内 `.cross_review/`** に統一する PATCH リリース。 - -- **_tmpdir.sh: `_tmp_dir()` の解決先を `.cross_review/` に変更** - (`skills/cross-review/scripts/_tmpdir.sh`): - - workspace 未指定時に `git rev-parse --show-toplevel` でフォールバックし、 - worktree ルート直下の `.cross_review/` を使用。 - - worktree 外のゴミファイル散乱と、環境依存のパス不整合を解消。 -- **launch-codex.sh / launch-gemini.sh: TMP_DIR パス統一**: - - 各ランチャーが `.cross_review/` を前提とするよう修正。 -- **monitor.py / state.py: パス参照の整合**: - - `$TMP_DIR` 解決先の変更に伴い、ファイル探索パスを `.cross_review/` 体系に統一。 -- **SKILL.md / docs: ドキュメントのパス記述統一**: - - `SKILL.md`、`docs/02-fix-and-rotation.md`、`skills/merged/SKILL.md` 内の - 旧パス記述を `.cross_review` 体系に修正。 -- **.gitignore: `.cross_review/` を追加**。 - -#### 既存ユーザへの影響 - -- `CROSS_REVIEW_TMP_DIR` env を明示しているユーザ: **挙動不変**。 -- 未指定ユーザ: 一時ファイルの書き出し先が `~/.gemini/tmp/` → worktree 内 - `.cross_review/` に変更。worktree 内に閉じるため管理が容易になる。 - -### v4.7.4 (cross-review: gemini stall 既定の per-agent 化 + fix 戻り値マージの堅牢化) - -`/ndf:cross-review` を実走させた際に、cross-review ループ自体は完走するが -メインセッション側で手動補正が必要だった運用不整合の残り 2 件を恒久対応する -PATCH リリース。 - -- **monitor.py: per-agent stall timeout 既定の導入** (`skills/cross-review/scripts/monitor.py`): - - 従来は `MONITOR_STALL=180s` の単一既定値だったため、err.log にほぼ進捗を出さない - gemini を 1 度目に毎回 STALLED 扱いで kill していた (孤児プロセスも残存)。 - - 既定を agent 別ビルトインに変更: **codex=180s (変更なし) / gemini=480s**。 - - 解決順: CLI `--stall-timeout` (明示優先) > env `MONITOR_STALL_` (per-agent) > - env `MONITOR_STALL` (両 agent 共通、後方互換) > agent 別ビルトイン。 - - `--stall-timeout` の argparse default を `int` → `None` に変更し、未指定時のみ - per-agent 解決を行う。 -- **state.py merge-fix: fix 戻り値ファイルの探索 fallback と key 別名対応** - (`skills/cross-review/scripts/state.py`): - - サブエージェントが `/tmp/fix-pr-result.json` に書き、`merge-fix` は - `$TMP_DIR` (= `~/.gemini/tmp//`) のみ参照する不整合で - exit 3 (ci-code-fail 扱い) になる事象を解消。探索順を **(1) `--file` 明示 - → (2) `$TMP_DIR/` → (3) `/tmp/`** の 3 段に変更。 - - 全候補不在時の die メッセージに探索 path 一覧を含め、原因切り分け可能にした。 - - サブエージェントが `commit_sha` / `fixed` 別名キーで書き出した場合も - `fix_commit` / `fixed_count` と同等に受理する key alias を追加 (silent な - `fixed=0` 記録の救済)。 -- **SKILL.md / docs のパス記述統一**: - - `SKILL.md` 内の `/tmp/fix-pr<#>-result.json` ハードコード 3 箇所を - `$TMP_DIR/fix-pr<#>-result.json` に統一。 - - 「すべて `/tmp/` に置き」の汎用記述も `$TMP_DIR/` (= `_tmp_dir()` 解決先) に修正。 - - `docs/02-fix-and-rotation.md` に `$TMP_DIR` の解決順 - (env `CROSS_REVIEW_TMP_DIR` > `~/.gemini/tmp//` > `/tmp/`) を 1 行明示。 - - `docs/01-state-and-review.md` の monitor 説明表に per-agent stall 既定の - 解決順を追記。 -- **pytest 追加** (`skills/cross-review/tests/`): - - `test_monitor_stall_default.py` — codex/gemini ビルトイン + env 上書き 6 ケース。 - - `test_state_merge_fix.py` — 正規 / `/tmp/` fallback / key 別名 / 全候補不在 / - `--file` 明示の 5 ケース。 -- **関連 issue / plan**: - - 完了済み開発記録として削除済み。詳細な検討履歴が必要な場合は、本バージョン追加時の git 履歴を参照。 - -#### 既存ユーザへの影響 - -- `--stall-timeout` を CLI で明示しているユーザ: **挙動不変**。 -- `MONITOR_STALL` env を指定しているユーザ: **挙動不変** (両 agent に同じ値が適用)。 -- 何も指定していないユーザ: gemini 側の既定 stall timeout が 180s → 480s に - **緩和**される (kill されにくくなる方向のため非破壊)。 -- 正規パス (`$TMP_DIR/fix-pr-result.json`) + 正規 key (`fix_commit` / - `fixed_count`) で書き出していたユーザ: **挙動不変**。 -- 旧プロンプトで `/tmp/` ハードコードや別名 key を使っていたユーザ: **silent fail - していたケースが拾われるようになる** (修復方向の変更)。 - -### v4.7.3 (cross-review: macOS 対応 worktree base + result.json スキーマ堅牢化) - -`/ndf:cross-review` を非 Linux コンテナ環境 (macOS / WSL 等) でも素直に動かせるよう、 -worktree のデフォルトパス解決を環境適応型に変更。あわせて gemini が変則スキーマで -result.json を書き出すケースで intent が silent に None マージされて judge が空回り -する不具合を恒久対応する PATCH リリース。 - -- **worktree デフォルトパスの環境適応** (`skills/cross-review/scripts/state.py`): - - `state.py init` が以下の優先順で worktree 親ディレクトリを解決: - 1. `NDF_WORKTREE_BASE` 環境変数 (明示オーバーライド) - 2. `/work/worktrees` (Linux コンテナ環境互換。書込可ならこちらを使用) - 3. `$HOME/work/worktrees` (macOS / WSL 等のフォールバック) - - 既存の Linux コンテナ環境では `/work/worktrees` が引き続き使われ挙動不変。 - - SKILL.md / docs から `/work/worktrees/pr` のハードコードを除去し、 - `/pr` 表記に統一 (state.json サンプル中の解決例 1 箇所のみ残存)。 -- **gemini result.json スキーマの明示化** (`skills/cross-review/scripts/launch-gemini.sh`): - - 「フォーマットは launch-codex.sh と同じ」という曖昧指示を、codex と同一の - フィールド列挙ブロック (`event` / `posted_as` / `comments_count` / `review_url` / - `by_severity`) に置き換え。`intent` / `comment_count` 等の別名を使わないことを明記。 -- **`state.py read-result` の堅牢化**: - - 仕様 (`event` / `comments_count`) を優先しつつ、別名 (`intent` / `comment_count`) - も拾えるようフォールバックを追加。 - - `event` / `intent` いずれも欠落している場合は `die()` (exit 1) で fail する。 - 旧挙動 (silent な `intent=None` マージで judge が空回り) は **破壊的に修正**。 -- **monitor.py EARLY_ERROR 誤検知の修正** (`skills/cross-review/scripts/monitor.py`): - - SKILL.md / `docs/01-state-and-review.md` の Markdown 表セル内で FATAL キーワード - (`「quota exceeded」`「sandbox error」等) を列挙しており、codex がレビュー時に - それを echo すると err.log 上で `_scan_early_fatal()` が誤発火してプロセスを - kill していた。以下 2 段の防御で恒久対応: - 1. `EARLY_ERROR_BENIGN` に Markdown 表セル行 (`^\|`) を追加。 - 2. マッチ位置が backtick / 日本語「」で引用されている場合に benign 扱いする - `_match_is_quoted()` ヘルパを追加し、`_scan_patterns()` から呼ぶ。 - - FATAL パターンから `^.*` プレフィックスを外し、`m.start()` をキーワード位置に - 合わせて引用判定が機能するように修正。 -- **pytest 追加** (`skills/cross-review/tests/`): - - `test_state_read_result.py` — 正規/変則/欠落スキーマ 4 ケース。 - - `test_default_worktree_base.py` — env / legacy / fallback の 3 ケース。 - - `test_monitor_early_error.py` — Markdown 表 / backtick / 日本語クォート引用の - benign 判定と、本物 fatal が依然検知される回帰テスト 7 ケース。 - - ローカル実行: `uv run --with pytest pytest plugins/ndf/skills/cross-review/tests`。 -- **関連 issue / plan**: - - 完了済み開発記録として削除済み。詳細な検討履歴が必要な場合は、本バージョン追加時の git 履歴を参照。 - -#### 既存ユーザへの影響 - -- `/work/worktrees` が書ける環境 (大半の Linux コンテナ環境): **挙動不変**。 -- macOS / WSL 等で `/work` が書けない環境: `--worktree` 引数なしでも - `$HOME/work/worktrees/pr` に自動フォールバックして init が成功する。 -- gemini が変則スキーマ (`intent` / `comment_count`) で result.json を書く現象を - 観測していたユーザ: フォールバックで自動的に取り込まれるようになる。 -- `result.json` から `event` / `intent` が両方欠落しているケースは exit 1 で - 早期 fail する (旧: judge 段階まで silent に None が伝播)。 - -### v4.7.0 (fix / cross-review: 修正ポリシー刷新 + CI 完了待ち廃止) - -`/ndf:fix` と `/ndf:cross-review` の修正方針を見直し、PR の最終的なコード品質を -上げる方向にシフトする MINOR リリース。`/ndf:fix` の自動実行範囲が広がるため、 -利用側スクリプトが `--severity-min nit` 相当の挙動を前提にしている場合は要確認。 - -- **修正対象の拡張** (`skills/fix/SKILL.md`): - - minor / nit のうち **パフォーマンス・可読性・重複コード排除** に該当する指摘は - このPR内で対応する (旧: nit は基本 deferred)。 - - 特にトータルのコード行数が減る方向の修正 (重複排除 / 不要分岐除去) は積極実施。 - - ただし修正範囲が **+30 行を超えそうな場合は deferred + ユーザ問い合わせ** - (スコープ膨張による副作用とレビュー負担を抑える)。 -- **重要度ラベルの独自再判定**: - - AI agent (CodeRabbit / Copilot / codex / gemini) が付けた `[critical/major/minor/nit]` - ラベルを鵜呑みにせず、コード本体を読んだ上でカテゴリ - (performance/readability/duplication/security/style/...) と合わせて再判定する。 - - 例: AI が `nit` と付けていても実体が重複排除なら修正対象。AI が `critical` と - 付けていても実害がないスタイル指摘なら deferred 化してよい。 -- **CI 完了待ちを廃止**: - - `/ndf:fix` 内の `gh pr checks --watch` および "PENDING を完了まで待つ" 手順を削除。 - - 各チェックポイントでは **その時点で FAILURE のジョブのみ** を修正対象に取り込み、 - 実行中チェックは無視して次ステップへ進む。 - - 戻り値 `ci_status` / `ci_failed_checks` は push 時点での既知失敗のみを反映する - (メイン context の節約と、長時間ブロック回避が目的)。 - - cross-review 側の judge ロジック (`state.py`) は変更不要 — `ci_status != FAILURE` - なら継続判定するため、PENDING も成功扱いになる。 -- **PR テスト範囲外の flaky テストも修正対象**: - - 放置するとリポジトリ全体の CI 信頼性が劣化し、後続 PR にも波及するため、 - `/ndf:fix` 実行時に見つけ次第このループで修正する。 -- **作業完了報告に PR URL 必須**: - - `/ndf:fix` の最終報告末尾に `https://github.com///pull/<番号>` を - 必ず記載する(メインからの追跡性向上)。 -- **`cross-review` 側の同期**: - - `skills/cross-review/docs/02-fix-and-rotation.md` のサブエージェント起動プロンプト - (ポリシーと「必須実行手順」)を上記方針に合わせて更新。 - - 手順 3 を「CI 状態スナップショット (完了待ちしない)」、手順 7 を - 「CI 再実行は待たない」に書き換え。SKILL.md の手順番号繰り上がり - (旧 8 → 新 7) も反映。 - -#### 既存ユーザへの影響 - -- `/ndf:fix` 単体実行: minor/nit カテゴリのうち performance/readability/duplication - に該当するものが自動修正されるため、これまで deferred だった指摘が修正コミットに - 入る場合がある。+30 行を超える場合はユーザ問い合わせで止まる。 -- `/ndf:cross-review` 自動ループ: CI 完了を待たなくなった分、各 round の所要時間が - 短縮される。一方で push 直後の CI 失敗は次 round の review 段階で再検出される。 -- `result.json` の `ci_status` が `PENDING` になる頻度が増える。下流で `ci_status` - を見ているスクリプトがある場合は確認推奨(state.py の judge は変更不要)。 - -### v4.6.2 (cross-review: state.py init の TMP_DIR 計算順序バグ修正 + AGENTS.md リネーム) - -`/ndf:cross-review` で gemini が **workspace 制約違反で payload を書けず -hard timeout (420s) で常時失敗** していた不具合を修正する PATCH リリース。 -合わせて `claude plugin validate` の警告 (plugin root の `CLAUDE.md` は -project context として読み込まれない) に従い `plugins/ndf/CLAUDE.md` を -`plugins/ndf/AGENTS.md` にリネーム。 - -- 修正 (`skills/cross-review/scripts/state.py` `cmd_init`): - - 旧実装は `_tmp_dir(args.worktree)` を `args.worktree=None` のまま呼び、 - `os.getcwd()` の basename (= 親リポジトリ名) で `~/.gemini/tmp//` - を採用していた。一方、`launch-gemini.sh` は `cd "$WORKTREE"` してから - gemini を起動するため、gemini は `~/.gemini/tmp/` - (= `~/.gemini/tmp/pr`) しか workspace 内として許可せず、 - `write_file` が `Path not in workspace` で拒否されていた。 - - 修正後は `worktree = args.worktree or f"/work/worktrees/pr{pr}"` を - 先に解決してから `_tmp_dir(worktree)` に渡し、tmp_dir の basename を - worktree basename と一致させる。 - - 副次効果: `state_file` の path も `~/.gemini/tmp/pr/` 配下に揃うため、 - cross-review が PR ごとに完全に分離した tmp 空間で動く。 -- リネーム: `plugins/ndf/CLAUDE.md` → `plugins/ndf/AGENTS.md`。 - Claude Code は plugin root の `CLAUDE.md` を project context として - 読み込まないため、`claude plugin validate` が警告を出していた。 - リポジトリ root の `AGENTS.md` (本体) + `CLAUDE.md` (Claude 固有) と - 揃え、プラグイン側は `AGENTS.md` に一本化する。 - - `plugins/ndf/README.md` のリンクも更新。 - - `claude plugin validate` の警告 0 件を確認。 - -#### 既存ユーザへの影響 - -- 旧 tmp_dir (`~/.gemini/tmp//`) に途中状態 (`state.json`) が残っている - 場合、v4.6.2 以降は `~/.gemini/tmp/pr/` を参照するため state を見失う。 - 対応: `mv ~/.gemini/tmp//cross-review-pr-* ~/.gemini/tmp/pr/` - で移行するか、`/ndf:cross-review ` を再 init する。 -- cross-review は 1 PR 単位の短命ステートのため、影響は実行中ループのみ。 - -### v4.6.1 (cross-review skill 主要処理のスクリプト化) - -`cross-review` skill の主要 bash 処理を `scripts/` 配下に外出し、SKILL.md / -docs/01,02 から冗長なインライン bash を排除する PATCH リリース。 -SKILL の I/O 契約 (state.json / result.json / payload.json スキーマ) は不変。 - -- 新規追加: - - `scripts/state.py` — state.json 操作 CLI (uv 自己完結 / stdlib のみ) - サブコマンド: `init` / `start-round` / `read-result` / `judge` / - `check-oscillation` / `merge-fix` / `should-rotate` / `set-current-pr` / - `report` - - `scripts/launch-codex.sh` / `scripts/launch-gemini.sh` — レビューランチャ - (pidfile + sentinel ベース、trusted directory 対策込み) - - `scripts/monitor.py` — codex/gemini プロセス多軸監視 CLI - (uv 自己完結 / stdlib のみ)。pidfile + `/proc` cmdline 検証 / codex sentinel / - 早期エラーパターン検出 / err.log stall timeout / hard timeout / result.json - 存在確認の 6 軸を並列スレッドで判定。exit code で失敗種別を区別 - (OK=0 / TIMEOUT=2 / NO_RESULT=3 / EARLY_ERROR=4 / STALLED=5 / PIDFILE_BAD=6)。 - sentinel 単独で完了判定する旧 `wait-review.sh` の取りこぼし - (codex クラッシュ時の無限ハング、gemini の untrusted directory 静かな失敗、 - pidfile stale 等) を解消。 - - `scripts/wait-review.sh` — `monitor.py` の薄ラッパ(旧 CLI 互換のため残置) - - `scripts/rotate-pr.sh` — PR ローテーション (squash + 新ブランチ + 新 PR) -- SKILL.md / docs/01,02 を「スクリプト呼び出し」形式に置換。state.json と - result.json のスキーマは docs に残し、実装は scripts/ にカプセル化。 - -PR #72 の実機テストで得た codex / gemini からの指摘および追加で見つかったバグの -対応(同 v4.6.1 内で実施): - -- **`monitor.py`**: - - cmdline 検証順序を「alive 確認後のみ」に変更。プロセスが既に死んでいる場合は - cmdline 不一致でも PIDFILE_BAD にならず、result.json の有無で OK 判定する - (旧実装は完了済 launcher を誤って失敗扱いしていた)。 - - EARLY_ERROR パターンを **行頭限定** + benign フィルタに改修。diff / doc 引用に - `401 Unauthorized` などのキーワードが含まれても誤検知しなくなった。 - - TIMEOUT / STALLED / EARLY_ERROR / PIDFILE_BAD で返るとき、対象プロセスに - SIGTERM (3 秒後に SIGKILL) を送信。残存プロセスが後から `gh api` 投稿や - result.json 書き込みを行ってメインと競合する問題を解消。 - - stall 判定を err.log のみから **err.log + stdout.log の合計サイズ** に拡張。 - - **デフォルト値変更**: hard timeout 30 分 → **7 分**、stall timeout 10 分 → **3 分**。 - - 未使用 import `field` を削除。 -- **`state.py`**: - - `gh api --paginate` の JSON ストリーミング処理を `--jq` ベースに変更 - (旧: `json.loads(r.stdout)` は複数ページで JSONDecodeError → 空配列 → - 既存コメントスナップショットが空になり重複指摘禁止が無効化されていた)。 - - `st["rounds"][-1]` への参照前に空チェックを追加し、初期化失敗時の - IndexError を防止 (read-result / judge / merge-fix の 3 箇所)。 - - 未使用 import `os` を削除。 -- **`launch-codex.sh` / `launch-gemini.sh` / `rotate-pr.sh`**: - - 引数を `STATE_PR` (= state.json の key, 初期 PR) に統一。レビュー対象の - 「現在の PR」は state.json の `current_pr` を内部で読む。 - 旧実装は PR rotation 後にメイン側で `PR=$NEW_PR` に切り替えると state.json - パスが見つからなくなる設計矛盾があった。 -- **tmp ディレクトリの gemini workspace 制約対応**: - - 全 scripts の tmp パスを `/tmp/` 直書きから `$CROSS_REVIEW_TMP_DIR` 経由に変更。 - 未設定なら `~/.gemini/tmp//` を自動採用、最終フォールバックは `/tmp/`。 - gemini CLI は `--yolo --skip-trust` でも workspace 外の `read_file` / - `write_file` がブロックされる (`Path not in workspace`) ため、gemini 公式の - project temp directory に揃えることで result.json / payload.json の書き出しを - 成立させる。 - - 共通ヘルパ `scripts/_tmpdir.sh` を追加 (bash) / `state.py` と `monitor.py` に - `_tmp_dir()` 関数を追加 (Python)。`state.py init` は採用した `TMP_DIR` を - state.json に記録し、stdout の `TMP_DIR=` で呼び出し側に通知。 - - SKILL.md のテンプレートで `eval "$(state.py init ...)"` 後に - `export CROSS_REVIEW_TMP_DIR="$TMP_DIR"` を行い、後続スクリプトに env として - 伝播させる手順を追加。 -- **`SKILL.md` / `docs/01,02`**: - - bash テンプレートを `$STATE_PR` 固定で書き直し、rotation 後も同じ変数で - 全 scripts を呼ぶ手順に統一。 - - 新デフォルト (timeout=7 分 / stall=3 分) を反映。 - -### v4.6.0 (cross-review skill 改訂 + review/fix の result.json 拡張) - -実運用で得た失敗パターンの対策を `cross-review` skill に反映し、関連する -`/ndf:review` と `/ndf:fix` の result.json schema を拡張する MINOR リリース。 - -- **`cross-review` skill 改訂**: - - **「事前確認」セクション新設**: ループ開始前の 4 プリチェック - 1. 自分の PR 判定 → `event` ダウングレード設定 (GitHub の - `HTTP 422: Can not request changes on your own pull request` を回避) - 2. **worktree 分離** (`/work/worktrees/pr`) で並行セッション競合回避 - 3. **gemini の trusted directory 対策**: - `GEMINI_CLI_TRUST_WORKSPACE=true` + `--skip-trust` 両方併用必須 - (worktree のような新規パスは untrusted 判定 → YOLO が "default" に降格される) - 4. 既存コメントスナップショット保存 → launcher プロンプトに添付して重複指摘禁止 - - **state.json schema 拡張**: - - `worktree_path` / `pr_author` / `is_own_pr` / `event_downgrade` を追加 - - 各 round の `codex` / `gemini` を **`intent` + `posted_as` の二重保持** に変更 - (ループ収束判定は `intent`、GitHub 投稿実体は `posted_as`) - - 各 round に `by_severity` (`{critical, major, minor, nit}` 件数) を追加 - - 各 round の `fix` に `resolved_threads` / `ci_note` を追加 - - **レビュー body 必須 identifier prefix**: - `## 🤖 cross-review | round | | ` を先頭固定化 - (人間アカウントから AI が投稿するため、GitHub UI で発信元を可視化) - - **CI failure の分類**: `pint/larastan/test/build/lint/type/tsc/mypy` 等は - code-fail として中断、`check_pr_requirements/assignees/reviewers/labels` - 等のメタチェックのみ失敗ならループ継続 - - **waiter を pidfile + sentinel ベースに**: - - codex は `^tokens used$` sentinel - - gemini は long `-p` プロンプトが引数に乗るため `pgrep -fa` 不可、 - `pidfile + kill -0` ポーリング必須 - - **`Step 5` サブエージェント責務を 5 点明示**: - 修正コミット / テスト / **reply + `resolveReviewThread` で Resolve** / - deferred は記録のみ・Resolve しない / 戻り値ファイル書き出し -- **`/ndf:review` SKILL.md 更新**: - - 結果サマリ (`/tmp/-review-pr<番号>-result.json`) に `posted_as` - フィールドを追加 - - `event` (intent / 本来の判定) と `posted_as` (実投稿) の使い分けを文書化 - - 自分 PR ダウングレードフローを `intent="REQUEST_CHANGES"` / - `posted_as="COMMENT"` で記録する手順を明記 -- **`/ndf:fix` SKILL.md 更新**: - - 戻り値ファイル `/tmp/fix-pr<番号>-result.json` に - `resolved_threads` (配列) / `ci_failed_checks` (配列) / `ci_note` (string) - を追加 - - 手順 12 に「resolve した thread_id / comment_id / path / line を - `resolved_threads[]` に記録」「`deferred` / `rejected` の thread は - Resolve しない」を明記 - - 手順 13 に `ci_failed_checks` の収集元 (`gh pr checks --json name,state`) - を補記 -- **アンチパターン 5 件追加**: - - 自分の PR に `REQUEST_CHANGES` で投稿 - - `gemini --yolo` 単独起動 (`--skip-trust` 併用必須) - - `pgrep -fa ` で完了判定 - - fix サブエージェントが Resolve をスキップ - - review body に identifier prefix を付け忘れる -- Skills: 39個 (変化なし、`cross-review` / `review` / `fix` の中身を更新) - -### v4.5.0 (playwright-scenario-test v0.5.0 — Skill 非依存 self-contained 構成 / 名前空間 rename) - -> **注意**: 互換性破壊リリース。Python パッケージ名・fixture 名・CLI option・ -> 環境変数・内部クラス名がすべて変わる。既存利用者は manual に書き換え必要。 -> 詳細な検討履歴が必要な場合は、本バージョン追加時の git 履歴を参照。 - -- **`playwright-scenario-test` v0.5.0** (Skill 非依存化 + 名前空間整理): - - **目的**: 旧 v0.4.0 までは Skill ディレクトリで `uv sync` する必要があり、 - Skill が消えるとテストが動かない / CI 別マシンで再現性が低い問題があった。 - v0.5.0 では `scripts/init_project.sh` で **利用者プロジェクト直下に - `scenario-test/` (all-in-one ランタイム)** を埋め込み、Skill 非依存で動作させる。 - - **新規 init / launcher**: - - `scripts/init_project.sh` / `scripts/init_project.bat`: - `//` に playwright_kit / scripts / uv.lock / - runtime templates を rsync ベースでコピーし、初回 uv sync + - playwright install chromium まで実行。`--runtime-dir ` で配置先名 - カスタマイズ可、`--dry-run` で予定差分のみ表示、`scenario.config.yaml` / - `tests/test_*.py` は既存があれば上書きしない (利用者編集物保護) - - `templates/run.sh` / `templates/run.bat`: - `$(dirname BASH_SOURCE)` / `%~dp0` で自身の位置を解決し CWD をランタイム - 内に固定。初回のみ `uv sync` + `playwright install chromium`。 - `--help` / `$@` で pytest 引数素通し - - `templates/pyproject.toml.runtime` / `templates/runtime-gitignore` / - `templates/runtime-README.md`: 利用者プロジェクト埋め込み用テンプレート - - **破壊的 rename** (Phase 0 / 0a): - - **Python パッケージ**: `scenario_test` → `playwright_kit` - - **pytest entry-point**: `ndf-scenario-test` → `playwright-kit` - - **fixture**: `ndf_config` → `pwk_config`, `ndf_role_` → - `pwk_role_`, `ndf_evidence` → `pwk_evidence`, - `ndf_a11y_scan` → `pwk_accessibility_scan`, - `ndf_cwv_measure` → `pwk_web_vitals_measure`, - `ndf_body_check_scan` → `pwk_body_check_scan`, - `ndf_out_dir` → `pwk_out_dir` - - **CLI option**: `--ndf-config` → `--pwk-config`, `--ndf-out-dir` → - `--pwk-out-dir`, `--ndf-no-evidence` → `--pwk-no-evidence`, - `--ndf-har-mode` → `--pwk-har-mode`, `--ndf-hud` → `--pwk-overlay`, - `--ndf-drive-folder` → `--pwk-drive-folder` - - **env var**: `NDF_CONFIG` → `PWK_CONFIG` - - **内部クラス**: `NdfTestEntry` → `PwkTestEntry`, `NdfEvidence` → `PwkEvidence` - - **モジュール rename** (Phase 0a, ドメイン用語の整理): - - `scenario_test/a11y.py` → `playwright_kit/accessibility.py` - (a11y は WCAG ドメイン用語のため平易な英語に) - - `scenario_test/cwv.py` → `playwright_kit/web_vitals.py` - (CWV → Core Web Vitals) - - `scenario_test/hud.py` → `playwright_kit/overlay.py` - (HUD = Heads-Up Display は造語的、overlay の方が直観的) - - **config schema**: `accessibility:` / `web_vitals:` キーに統一 - (旧 `a11y:` / `cwv:` は廃止) - - **EvidenceCollectors fields**: `cwv_metrics` → `web_vitals_metrics`, - `cwv_passed` → `web_vitals_passed` - - **保持するもの** (W3C / 業界標準): - - `LCP` / `CLS` / `TTFB` / `longest_task` / `HAR` / `axe-core` — - データフィールド名・外部仕様名としてそのまま使用 (各 docstring と - SKILL.md の用語集セクションで正式名称を併記) - - **SKILL.md / docs**: クイックスタートを `init_project.sh → run.sh` フローに - 全面書き換え、用語集セクションを SKILL.md 上部に新設、ディレクトリ図を - rename 後 + init 後構造に更新、開発者向け「Skill 単体で uv sync する旧運用」 - 節を別出し - - **検証**: - - 159 件 pure 関数テスト pass (config / fixtures / pytest plugin / report 全般) - - 擬似環境 (`/tmp/...`) に init 後、Skill ディレクトリを `mv` で隠した - 状態で `./scenario-test/run.sh --collect-only` が完走 (8 件 collect, exit 0) - - `--runtime-dir e2e` 配置で複数ランタイム共存 (`scenario-test/` + `e2e/`) - が独立に動作 - - 再 init で `tests/test_*.py` の利用者編集が保護される (skip) - - `--dry-run` で実際にはコピーされない -- Skills: 39個 (変化なし、playwright-scenario-test の中身が刷新) - -### v4.4.0 (issue-plan-strategy skill 追加) - -- **新規 Skill `issue-plan-strategy`**: - - 1 つの issue から plan を起こし、推奨 PR が複数に分かれる場合の標準ワークフローを規定 - - スラッシュコマンド (`/ndf:issue-plan-strategy `) でも、 - **(a) issue から plan 作成依頼** / **(b) 既存 plan の実装(実行)依頼** の両方で自動発動する - (description / Triggers に発動条件を明記) - - Step 0 で plan ファイル / release branch / Draft PR の有無を見て **作成フェーズ vs 実行フェーズ** - を切り分け、実行フェーズで入った場合は Step 1 をスキップして Step 3 以降の自動化に直行 - - フロー: issue 取得 → (作成) plan (必要なら plan モード) → 単一/複数 PR 判定 → - (実行) release branch + Draft release PR 先行作成 → 個別 PR ブランチ + Draft PR 先行作成 → - git worktree で並行開発 → 個別 PR レビュー (`/ndf:review` / `/ndf:cross-review`) → - release ブランチで結合テスト相当のレビューのみ → release → default merge - - 検証環境 (qa/staging) は個別 PR or release PR 単位で `/ndf:cherry-pick-pr` に委譲 - - 関連 skill (`implementation-plan` / `branch-fix-strategy` / `pr` / `cherry-pick-pr` / - `review` / `cross-review` / `playwright-scenario-test`) との責務分担を明記し、 - 本 skill は **multi-PR 運用のメタ手順**に専念 -- Skills: 38個 → **39個** - -### v4.3.1 (Fix: cross-review / gemini skill 登録漏れ修正) - -- PR #67 で追加された `cross-review` / `gemini` skill が `plugin.json` の - `skills` 配列に登録されておらず、`/ndf:cross-review` / `/ndf:gemini` を - slash command として呼び出せなかった問題を修正 -- 併せて description と CLAUDE.md / AGENTS.md / README.md の skill 数を - 36個 → 38個 に更新 -- Skills: 36個 → **38個** (実体は v4.3.0 時点で既に存在、登録のみ追加) - -### v4.3.0 (playwright-scenario-test v0.4.0 — body_check 復活) - -- **`playwright-scenario-test` v0.4.0**: 旧 v0.2.x の自前 YAML runner にあった - **`body_check`** 機能 (PHP / SSR がフロントに漏れさせる ``Fatal error`` / - ``Uncaught`` / ``STRICT:`` / ``Warning:`` / ``Notice:`` / ``File not found`` - 等を **テスト失敗として検出**) を **default 有効** で復活させた。 - まだ開発・検証段階の skill のため互換性は重視せず、config 無記述でも - PHP プロジェクトで素直に効くよう default で `enabled=True` + PHP 系 - パターンを内蔵する。詳細な検討履歴が必要な場合は、本バージョン追加時の git 履歴を参照。 - - **新モジュール**: - - `scenario_test/body_check.py` — 純粋関数 `scan_body` / `is_html_response` - と `BodyViolation` dataclass。substring match で fatal / warning / - not_found カテゴリの violation を返す - - `scenario_test/fixtures/body_check.py` — autouse fixture - (`_ndf_body_check_autouse`) と明示呼び出し用 helper (`ndf_body_check_scan`)。 - `page.on("response", ...)` で全 HTML レスポンスを監視し、teardown で - `case_dir/body_check.jsonl` に書き出してから violation 数次第で - `pytest.fail` - - **新 config schema** (`scenario.config.yaml`, 省略可): - ```yaml - body_check: - enabled: true # default: true (機能無効化したい場合のみ false) - fatal_patterns: ["Fatal error", "Uncaught", "Parse error"] - warning_patterns: ["STRICT:", "Warning:", "Notice:", "Deprecated:"] - warning_head_chars: 300 # warning_patterns は本文先頭 N 文字のみ走査 (PLAN18 のフィールド名 warning_head_bytes も alias で受理) - not_found_patterns: ["File not found"] - fail_on_match: true # false で情報収集モード - ``` - 各キーは**省略すると dataclass の default**が効く (PHP 系パターン内蔵)。 - 明示的に空リストを書けばそのカテゴリのみ無効化できる。 - - **新 marker**: `@pytest.mark.no_body_check` で個別テスト opt-out - - **report.md**: - - サマリ表に `body_check` カラムを追加 (違反件数) - - 違反があれば「body_check 違反の詳細」セクション (URL / pattern / snippet) - を生成 (PASS でも `fail_on_match=false` の情報収集モードで表示) - - **設計ポイント**: - - 既存利用者の test 挙動を変えないため `body_check.enabled` の default は - **False** (opt-in)。設定を書かない限り従来挙動 (検出ロジックなし) が維持される - - autouse fixture は a11y / cwv と同じく `request.fixturenames` ガードで - `page` を要求していない test を skip - - body_check が teardown で `pytest.fail` した場合、call phase は passed - のまま teardown report が failed/error になるため、`_collect_entries` で - teardown 失敗を call entry に反映 (`outcome` を passed → failed に昇格) - - **検証**: 既存 + body_check / report / collect_entries 昇格 / sessionfinish - upload を含めて **147 件 pure 関数テスト pass** (`uv run pytest -q`) -- Skills: 36個 (変化なし、playwright-scenario-test の中身に opt-in 機能追加) - -### v4.2.0 (playwright-scenario-test v0.3.0 — pure pytest-playwright 完全移行) - -> **注意**: 互換性破壊リリース。v0.2.5 までの自前 YAML DSL は **完全廃止** し、 -> 利用者は通常の pytest-playwright テストを書く形に移行する。詳細な検討履歴が必要な場合は、 -> 本バージョン追加時の git 履歴を参照。 - -- **`playwright-scenario-test` v0.3.0** (pure pytest-playwright): - - **アーキテクチャ全面刷新**: 自前 DSL (testcase YAML / runner / dispatcher / - locator_steps / cli) をすべて削除。代わりに pytest plugin として実装し、 - 利用者は `def test_xxx(page, ndf_role_admin): ...` を直接書く - - **新モジュール**: - - `scenario_test/pytest_plugin.py` — pytest11 entry-point。 - `pytest_addoption` (`--ndf-config` / `--ndf-out-dir` / `--ndf-no-evidence` - / `--ndf-hud` / `--ndf-drive-folder`)、markers (`page_role` / `role` / - `phase` / `priority`)、`pytest_runtest_makereport` / - `pytest_terminal_summary` / `pytest_sessionfinish` hook - - `scenario_test/pytest_report.py` — `report.md` 生成 (`NdfTestEntry` + - `render_markdown` + `write_report`) - - `scenario_test/fixtures/auth.py` — `ndf_config` (session) / - `ndf_role_` (動的生成、storage_state cache 付) - - `scenario_test/fixtures/evidence.py` — `ndf_evidence` / - `browser_context_args` override (HAR inject) / `ndf_out_dir` - - `scenario_test/fixtures/a11y.py` — `_ndf_a11y_autouse` (page_role marker - が付いた test に限り axe-core 自動実行) + `ndf_a11y_scan` 明示ヘルパ - - `scenario_test/fixtures/cwv.py` — `_ndf_cwv_autouse` (page_role marker - autouse で LCP/CLS/TTFB/longest_task 計測) - - **削除**: - - `scenario_test/testcase.py` の `Step` / `LocatorSpec` / - `KNOWN_STEP_KINDS` / `discover_testcases` 等 - - `scenario_test/locator_steps.py` / `runner.py` / `cli.py` / - `playwright_executor.py` / `report.py` (旧) - - `scripts/record_to_yaml.py` / `generate_test_plan.py` (DSL 雛形版) - - `templates/testcase-*.yaml.template` 6 ファイル + `config.example.yaml` - - **新規 templates** (pytest 雛形): - - `templates/scenario.config.yaml` — base_url / roles / a11y / CWV 設定 - - `templates/conftest.py.template` - - `templates/test_auth.py.template` / `test_list.py.template` / - `test_form.py.template` / `test_dashboard.py.template` - - **依存追加** (main): `pytest>=8.0`, `pytest-playwright>=0.5`, - `pytest-xdist>=3.0` - - **設計上の重要ポイント**: - - autouse fixture が `page` を直接要求すると pytest-playwright が全 test を - browser parametrize する問題を、`request.fixturenames` ガード + - `getfixturevalue` 遅延取得で回避 - - `ndf_role_` の login は session 内 1 回だけ実行し storage_state を - cache。新 context には cookies/origins を inject して再ログイン回避 - - **検証**: 旧 126 + 新規 26 = **152 件 pure 関数テスト pass** - - SKILL.md は pytest 中心の構成に全面書き直し -- Skills: 36個 (変化なし、playwright-scenario-test の中身が刷新) - -### v4.1.1 (playwright-scenario-test v0.2.5 — locator-first DSL 中間版) - -> **Note**: 当初は v0.3.0 として実装したが、Codex の独立レビューで「pure -> pytest-playwright 移行の方が OSS 品質として優れている」と判断 (locator DSL は -> 自前で再実装が必要、artifact 契約も pytest plugin の方が自然) し、v0.3.0 の -> 番号は **pure pytest 完全移行** に予約しなおした (PLAN17 参照)。本リリースは -> evidence/a11y/CWV/slug 衝突回避などの本質的改善のみを v0.2.5 として暫定提供。 - -- **`playwright-scenario-test` v0.2.5** (locator-first / web-first 中間版): - - **testcase YAML スキーマを刷新**: 旧 `path/method/data/extract` 構造を廃止。明示的 `kind` (`goto/click/fill/select/check/press/hover/extract/wait_for/wait_ms/expect_visible/expect_hidden/expect_text/expect_no_text/expect_url/expect_count/expect_aria_snapshot`) に統一 - - **Locator 表現**: `{role: button, name: 保存}` / `{label: メールアドレス}` / `{testid: ...}` / `{css: ...}` 等の dict で記述。Playwright 公式の `get_by_*` 系 API に 1 対 1 対応 - - **assertion は `expect()` のみ**: 自前 `body_check` と HTML 文字列 match を全廃。`expect_no_text` step で代替 - - **新モジュール**: - - `scenario_test/locator_steps.py` — YAML step kind → Locator/expect API dispatcher (テーブル駆動) - - `scenario_test/evidence.py` — 1 testcase 分の HAR/trace/console/pageerror/axe/CWV を集中管理 - - `scenario_test/a11y.py` — axe-core ランナー内蔵モジュール (page_role に応じて自動実行) - - `scenario_test/cwv.py` — Core Web Vitals ランナー内蔵モジュール (page_role に応じて自動実行) - - **削除**: - - `scenario_test/nav_helpers.py` (全関数を locator-first で置換) - - `scripts/trace_link.py` (`upload_evidence.py` に統合) - - 古い `path` ベース step / `body_check` / `slug` config / `enable_scroll_demo` config - - **新規 scripts**: - - `scripts/upload_evidence.py` — trace/HAR/video の Drive アップ統合 (拡張子から自動判定) - - `scripts/record_to_yaml.py` — Playwright codegen Python 出力 → 新スキーマ YAML 変換 - - **Min-2 (slug 衝突)**: `_default_test_id(role, url)` で URL 全 path + sha1[:6] suffix を付与し衝突回避 - - **Maj-7 (責務分離)**: `playwright_executor.py` 678 → 約 380 行へ縮小 (evidence は `EvidenceCollectors` へ完全分離) - - **report.md** に axe-core 違反 / Core Web Vitals metrics を表示 - - templates/ を 6 ファイル全部新スキーマで書き直し - - docs/ から「v0.3.0 以降で検討」記述を解消、`navigate_post` / `find_click_target` / `detect_body_errors` 等の旧 helper 言及を削除 -- Skills: 36個 (変化なし) - -### v4.1.0 -- **`playwright-scenario-test` v0.2.0** (理論ベース化): - - `docs/` 配下に方法論を 6 ファイル (総論 / page role / 技法 / Playwright API / bug report) + checklists 11 ファイル (lp/list/item/edit/form/search/dashboard/auth/cart-checkout/modal-wizard/common) として整備 - - 出典: HTSM v6.3 (James Bach), ISTQB CTFL 4.2, ISO/IEC/IEEE 29119-3:2021, WCAG 2.2, OWASP Top 10:2025, FEW HICCUPPS, Hendrickson Cheat Sheet - - 新規スクリプト: `classify_page_role.py` (a11y tree から自動 role 判定), `generate_test_plan.py` (Pairwise 込み YAML 自動生成), `run_a11y_scan.py` (axe-core), `check_cwv.py` (LCP/CLS/TTFB), `record_scenario.py` (Playwright codegen ラッパー), `trace_link.py` (trace.zip → playwright.dev URL) - - 役割別 testcase YAML テンプレート 4 件 (list/edit/form/auth) を追加 - - SKILL.md は実行手順とナビゲーションに集中 (332 → 245 行) - - pyproject.toml に optional-dependencies `a11y` (axe-playwright-python) を追加 -- **公式 Agent Skill 仕様準拠**: 14 skill の frontmatter を Pattern A (description 単体に Triggers 埋め込み) から Pattern B (description + 公式 `when_to_use` フィールド分離) へ移行。対象 skill: codex / data-analyst-export / data-analyst-sql-optimization / deepwiki-transfer / docker-container-access / git-gh-operations / google-auth / google-chat / google-drive / markdown-writing / official-skills-autoloader / playwright-scenario-test / python-execution / qa-security-scan / skill-stats。`description` は概要に集中、`when_to_use` に Trigger phrase を分離して auto-invoke 精度向上 (公式 1,536 字上限内)。`mcp-builder` は Anthropic 公式 (Apache-2.0) のため改変せず。 -- **コード品質改善**: - - `google-chat/scripts/gchat_read.py`: `DEFAULT_SPACE_ID` のハードコード (`AAQA6AWG1iE`) を撤去。env `GCHAT_DEFAULT_SPACE` で指定するか `--space` を required にする運用に変更 - - `google-auth/SKILL.md`: `allowed-tools` から不要な `Bash(pip *)` を削除 (uv で完結) - - `google-drive/scripts/gdrive_fetch.py`: `upload_file` 内の dead な再 `import os` を削除 - - `playwright-scenario-test/SKILL.md`: 不足していた `allowed-tools` (Read / Bash(uv *) / Bash(python *)) を追加 -- **新規 Skill `playwright-scenario-test`** (self-contained uv project): - - Playwright + curl で Web シナリオ E2E テストを並列実行 - - HUD オーバーレイ (カーソル + 字幕) 焼き込み済み動画 + Markdown レポート生成 - - Drive アップロード自動化までサポート - - 外部プロジェクトは `config.yaml` + `testcases/*.yaml` のみで利用可能 -- **新規 Skill `google-drive`**: - - Google Drive / Docs のファイルエクスポート / ダウンロード / アップロード (公開共有リンク付与) - - 認証は `ndf:google-auth` の `get_credentials()` に委譲 -- **新規 Skill `google-chat`**: - - Google Chat スペースのメッセージ・スペース一覧取得 (Chat API) - - 認証は `ndf:google-auth` の `get_credentials()` に委譲 - - 旧 uttarov 版のハードコードパス (`/work/uttarov2-doc/...`) を撤廃し、 - sibling-skill discovery (`GOOGLE_AUTH_SCRIPTS` env / `~/.claude/skills/google-auth/scripts` / - 隣接スキル) でフォールバック -- **`google-auth` v0.2.0 (互換性破壊)**: - - Python ライブラリ用法 (`from google_auth import get_credentials`) を追加 - - `--manual` 手動 copy-paste フロー (ローカルサーバ不要、コンテナ環境対応) - - トークン自動リフレッシュ + スコープ不足検出 / 自動マージ - - `--show` / `--clear` サブコマンド - - **トークン保存先を `/tmp/google_token.json` → `~/.config/gcloud/google_token.json` に変更** - (env `GOOGLE_TOKEN_FILE` で上書き可) - - `client_secret` パスは `--client-secret` → env `GOOGLE_CLIENT_SECRET` → - `${CLAUDE_SKILL_DIR}/client_secret.json` → CWD の順 -- Skills: 33個 → **36個** - -### v4.0.0 (BREAKING: Codex MCP廃止 + レガシー救済機構削除) -- **Codex MCP サーバを削除** (`.mcp.json` から `codex` エントリを削除) - - 理由: `/ndf:codex` skill (CLI直接実行) で十分であり、MCP 経由の制約 (ホスト側ファイル読み取り制限等) よりも CLI 直接実行の方が有用 - - 影響: `mcp__codex__codex` / `mcp__codex__codex-reply` は利用不可 - - 代替: `/ndf:codex` skill の手順で `codex exec` をバックグラウンド実行、または `corder` エージェント経由で呼び出し -- **corder エージェントを CLI ベースに書き換え** - - MCP 呼び出しを `/ndf:codex` skill 参照に変更 - - Serena / Context7 MCP は引き続き利用 -- 他エージェント (researcher, qa, devops-engineer, debugger, code-reviewer, director) の description から Codex MCP 言及を削除 / CLI ベースに更新 -- `skills/codex` の MCP 版との使い分け節を corder エージェントとの使い分けに書き換え -- `skills/qa-security-scan/03-report-template.md` の JS 疑似コードを `codex exec` bash 例に置換 -- **レガシー CLAUDE.ndf.md 救済機構を削除** (v3.0.0 で本体廃止、以降の救済装置を除去) - - `hooks/hooks.json` の CLAUDE.ndf.md 検出 hook を削除 - - `skills/cleanup/` を削除 (`plugin.json` の参照も削除) - - まだ残っているユーザーは今後手動で `CLAUDE.ndf.md` を削除してください -- Skills: 34個 → **33個** (`cleanup` 削除) - -### v3.7.0 -- **Transcript保持期間の自動管理**: - - `SessionStart` hook (matcher: `startup`) + `scripts/ensure-retention.sh` を追加 - - `~/.claude/settings.json` の `cleanupPeriodDays` を最低 90 日に保つ (既に 90 以上ならそのまま) - - 7 日タイムスタンプガード (`~/.claude/.ndf-retention-checked`) で多重実行防止 - - Claude Code 本体の公開 API/ドキュメントには「プラグインインストール時」hook が存在しないため、`SessionStart + startup` matcher が事実上の最適解 -- **`/ndf:skill-stats` skillを追加**: - - `~/.claude/projects/**/*.jsonl` transcript から NDF skill 利用統計を集計 - - 項目: 呼び出し数 / 関連話題数 / ヒット数 / ヒット率 - - 関連話題判定は SKILL.md frontmatter の `Triggers: '..', '..'` 行を使用 (明示されていない skill は計算対象外) - - Python 実装、標準ライブラリのみ - - skill description の網羅性を評価するツールとして機能 -- Skills: 33個 → **34個** - -### v3.6.0 -- carmo-system-consoleから汎用skill/commandを抽出してNDFに統合 -- **新規Skills(13個)**: - - 原則系(5個): `branch-fix-strategy`, `implementation-plan`, `investigation-rules`, `problem-solving`, `logging-guidelines` - - ワークフロー系(7個): `sync-main`, `cherry-pick-pr`, `deploy`, `review-branch`, `review-pr-comments`, `resolve-pr-comments`, `browser-test` - - 外部AI委譲(1個): `codex`(CLI直接実行、MCP版corderとの使い分け) -- **既存Skill改修**: - - `pr`: `--draft`フラグ対応、既存PR説明の自動更新、base非mainの場合`cherry-pick-pr`誘導、`.github/pull_request_template.md`サポート -- Skills: 20個 → **33個** -- PR/コードレビュー系の責務分担を明確化: - - `review` = PR単位レビュー(Approve/Request Changes判定) - - `review-branch` = ローカル差分レビュー(PR前のセルフレビュー) - - `review-pr-comments` = 既存PRコメントの分類(READ-ONLY) - - `fix` = コメント対応の修正実施 - - `resolve-pr-comments` = 修正完了後の返信+Resolve(クロージング) - -### v3.5.0 (破壊的変更: scanner削除) -- Claude Code Read toolのmultimodal/PDF native対応、および v3.4.0で追加された `official-skills-autoloader` により冗長になったAgent/Skillを整理 -- **削除Agent**: - - `scanner` (Office抽出) → autoloader + 公式docx/pptx/xlsx Skillで代替 -- **削除Skills**: - - `scanner-pdf-analysis` → Read tool の PDF native対応で代替 - - `scanner-excel-extraction` → autoloader + 公式xlsx Skillで代替(plugin.jsonのdangling ref整理) - - `skill-development` → 公式 `skill-creator` Skillで代替(autoloader取得可能) - - `corder-code-templates`, `corder-test-generation` → Claude本体のコード生成能力で代替 - - `researcher-report-templates` → researcher agent description/Claude本体で代替 -- Agents: 9個 → **8個** -- Skills: 25個 → **20個** -- 移行ガイド: `/ndf:scanner` を呼んでいた処理は、autoloaderまたはRead toolへ切替 - -### v3.4.0 -- Anthropic公式の定番Skill `mcp-builder` を取込(Apache-2.0、LICENSE.txt同梱) -- 公式Skillインストーラ `plugins/ndf/scripts/install-official-skills.sh` を追加 - - `--list`: 利用可能Skill一覧(ライセンス分類付き) - - `--scope user/project`: インストール先選択 - - `--all` / 個別指定: 選択的インストール - - `--update`: 公式リポジトリの最新化 - - シンボリックリンク方式で軽量 -- プロプライエタリSkill(docx/pptx/xlsx/pdf)は再配布せず、上記インストーラで個人利用者環境に配置 -- インストール手順・ライセンス方針を `docs/official-skills-installation.md` にまとめ -- `official-skills-autoloader` Skillを追加: Word/Excel/PowerPoint/PDF等の要求時に必要な公式Skillを自動ダウンロード→読込して即使用可能(利用者はインストール作業不要) -- Skills: 23個 → 25個 - -### v3.3.0 -- 定番サブエージェント3個を追加(いずれも `model: sonnet`) - - **debugger**: エラー・バグの根本原因分析 - - **devops-engineer**: Dockerfile/CI/CD/Kubernetes - - **code-reviewer**: git diff / PR一般レビュー(corderと差別化: Codex非使用) -- Agents: 6個 → 9個 - -### v3.2.0 -- サブエージェントに `model:` 指定を追加し、コスト最適化 - - director: `opus`(計画・設計判断) - - corder, data-analyst, researcher, qa: `sonnet` - - scanner: `haiku` -- scannerエージェントをOffice専用に縮小 - - 画像・PDFはClaude Code built-inのRead tool(multimodal, pages)で処理する方針に変更 -- corderのdescriptionを「Codex第二意見レビュー/大規模調査」用途に明確化 -- researcherのdescriptionをAWS Docs / Chrome DevTools専用に縮小 - -### v3.1.0 -- Kiro CLI対応(`.kiro/` 配下のインストーラ、プロンプト、スキルリンク) -- `google-auth` スキル追加 - -### v3.0.0 (破壊的変更) -- Serena MCPを`mcp-serena`プラグインに分離 -- memory系スキル5個を廃止(serena, memory-handling, serena-memory-strategy, mem-capture, mem-review) -- CLAUDE.ndf.md注入仕組みを廃止(inject-plugin-guide.js削除) -- `ndf-policies`スキル追加(ポリシー常時注入) -- `/ndf:cleanup`スキル追加(CLAUDE.ndf.md後始末) -- SessionStartフックをCLAUDE.ndf.md検出警告に変更 -- Skills: 25個→23個 - -### v2.8.0 -- `deepwiki-transfer`スキル追加 -- Skills: 23個→25個(knowledge-reorg含む) - -### v2.7.0 -- commandsをskillsに統合(Claude Code 2.1.3対応) - -### v2.6.0 -- NDFプラグインのMCP構成を最適化し個別プラグイン化 - -### v2.0.0 -- GitHub MCP, Serena MCP, Context7 MCPを公式プラグインに移行 diff --git a/plugins/ndf/README.md b/plugins/ndf/README.md deleted file mode 100644 index 5aa199d..0000000 --- a/plugins/ndf/README.md +++ /dev/null @@ -1,859 +0,0 @@ -# NDF Plugin - -Claude Code / Codex / Kiro CLI 開発環境を**オールインワン**で強化する統合プラグインです。 - -## 概要 - -このプラグイン1つで、以下の機能を利用できます: - -1. **コアMCP**: なし (v4.0.0 で Codex MCP 廃止 / Serena MCP は `mcp-serena` プラグインに分離) -2. **Skills**: Claude Code/Kiro向け core 28個、Codex向け core 30個を公開。元の49個は `skills/` に保持し、ランタイム別の除外候補は `skills-optional/README.md` で管理。 -3. **専門エージェント**: 8つの特化型AIエージェント(director、data-analyst、corder、researcher、qa、debugger、devops-engineer、code-reviewer) -4. **自動フック**: Slack通知、デフォルトstatusline設定(未設定時のみ) - -## インストール - -### 前提条件 - -- 利用するランタイムのいずれか: - - Claude Code - - Codex CLI - - Kiro CLI -- Slack通知を使う場合: Node.js(同梱 hook が `node` で通知スクリプトを実行) -- `/ndf:codex` skill または `corder` エージェントから外部AI委譲を使う場合: Codex CLI(`npm install -g @openai/codex` → `codex login`) - -NDF本体は v4.0.0 以降、MCPサーバーを同梱しません。Serena、BigQuery、Chrome DevTools、AWS Docs、DBHub、Notion などの MCP は必要に応じて個別プラグインとしてインストールしてください。Python / `uvx` などは、それらの個別 MCP プラグインを使う場合の要件です。 - -### 公式プラグインのインストール(Claude Code / 任意) - -GitHub、Context7 MCPは公式プラグインとして提供されています: - -```bash -# Claude Codeで実行 -/plugin install github@anthropics/claude-plugins-official -/plugin install context7@anthropics/claude-plugins-official -``` - -### 追加MCPプラグインのインストール(Claude Code / オプション) - -用途に応じて個別のMCPプラグインをインストールできます。以下は Claude Code の例です。先に `ai-plugins` marketplace を追加してください。 - -```bash -# ブラウザ自動化とテスト -/plugin install mcp-chrome-devtools@ai-plugins - -# データ分析 -/plugin install mcp-bigquery@ai-plugins - -# AWS公式ドキュメント調査 -/plugin install mcp-aws-docs@ai-plugins - -# データベース操作 -/plugin install mcp-dbhub@ai-plugins - -# Notion統合 -/plugin install mcp-notion@ai-plugins -``` - -### Claude Codeでのインストール - -#### ステップ1: マーケットプレイスの追加 - -```bash -# Claude Codeで実行 -/plugin marketplace add https://github.com/devbasex/ai-plugins -``` - -#### ステップ2: プラグインのインストール - -```bash -# Claude Codeで実行 -/plugin install ndf@ai-plugins -``` - -### Codexでのインストール - -```bash -codex plugin marketplace add https://github.com/devbasex/ai-plugins -codex plugin add ndf@ai-plugins -``` - -Codex版ではSkillsに加えて、Codex向けSlack終了通知hookを同梱します。通知は明示的に `NDF_CODEX_SLACK_NOTIFY=true` を設定した場合のみ送信されます。Claude Code向けのstatusline設定、transcript保持期間設定、Claude CLIによるSlack要約通知hookはCodexでは自動有効化しません。 - -Claude Code/Kiro版で初期表示するSkillsは、PR運用・レビュー・調査・実装計画・仕様書化・browser smoke test・statusline・Codex CLI委譲・cross-reviewなどの core 28個に絞っています。Codex版は、Codex自体からの単発再委譲・Claude専用statusline・Claude transcript統計を外し、PR cross-review と Playwrightの計画・作成・実行・レポート・ツール操作、仕様書化を含めた core 30個にしています。Google連携、DeepWiki転送、MLモデル構造、高度なPlaywright連携などは通常利用時のskills context budgetを圧迫しないよう初期公開から外し、`skills-optional/README.md` に整理しています。 - -Codex向けSlack通知を使う場合は、Claude Code向けSlack通知と同じ環境変数を使います。プロジェクトの `.env` などに以下を設定してください。 - -```bash -NDF_CODEX_SLACK_NOTIFY=true -SLACK_BOT_TOKEN=xoxb-... -SLACK_CHANNEL_ID=C0123456789 -SLACK_USER_MENTION=<@U0123456789> # オプション -``` - -Codexのhookは初回実行前に `/hooks` で信頼設定が必要です。 - -### Kiro CLIでのインストール - -Kiro CLI では、リポジトリ付属のインストーラーで `.kiro/skills/` と `.kiro/agents/default.json` を生成します。 - -```bash -# 基本(manifest-selected skills + agentSpawn hook) -bash plugins/ndf-kiro/install.sh - -# Slack通知も有効化 -bash plugins/ndf-kiro/install.sh --with-slack - -# Codex CLI連携プロンプトも追加 -bash plugins/ndf-kiro/install.sh --with-codex -``` - -インストール後、Kiro CLI を起動します。 - -```bash -kiro-cli chat -``` - -### .envファイルの作成(Slack通知・外部連携を使う場合) - -Slack通知や個別MCPプラグインを使う場合は、プロジェクトルートに `.env` ファイルを作成し、必要な認証情報を設定します。NDF本体の基本 skill だけを使う場合、`.env` は必須ではありません。 - -```bash -# Slack通知 (オプション) -# Slack Appセットアップ手順は下記の詳細設定を参照 -SLACK_BOT_TOKEN= -SLACK_CHANNEL_ID= -SLACK_USER_MENTION= # 例: <@U0123456789> -# NDF_CODEX_SLACK_NOTIFY=true # Codex版でSlack通知を有効化する場合のみ - -# 注意: -# - Serena MCP は別プラグイン `mcp-serena` として提供 -# - Codex CLI 連携は `/ndf:codex` skill から使う場合のみ必要: `npm install -g @openai/codex` → `codex login` -# (v4.0.0 で Codex MCP サーバは廃止。skill 経由の CLI 直接実行に一本化) -# - GitHub MCP、Context7 MCPは公式プラグインを使用してください -# - 追加のMCP(BigQuery、Notion、AWS Docs、DBHub、Chrome DevTools)は -# 個別プラグインとしてインストール可能です(下記参照) -``` - -#### .envファイルの保護 - -`.env` ファイルには機密情報が含まれるため、必ず `.gitignore` に追加してください: - -```bash -echo ".env" >> .gitignore -``` - -#### 各認証情報の詳細設定 - -
-DSN(DATABASE_DSN)の設定方法(DBHub MCP用) - -DBHub MCPは複数のデータベースに対応しています。環境変数名は`DSN`または`DATABASE_DSN`のどちらでも使用可能です。 - -**PostgreSQL:** -```bash -DSN="postgres://USERNAME:PASSWORD@HOST:PORT/DATABASE?sslmode=disable" -``` - -SSL接続が必要な場合: -```bash -DSN="postgres://USERNAME:PASSWORD@HOST:PORT/DATABASE?sslmode=require" -``` - -**MySQL / MariaDB:** -```bash -DSN="mysql://USERNAME:PASSWORD@HOST:PORT/DATABASE" -``` - -SSH踏み台サーバー経由で接続する場合: -1. SSHトンネルを作成(ローカルポート転送) - ```bash - ssh -L 3307:DB_HOST:3306 USER@BASTION_HOST -N - ``` -2. ローカルポート経由で接続 - ```bash - DSN="mysql://USERNAME:PASSWORD@localhost:3307/DATABASE" - ``` - -**SQLite:** -```bash -DSN="sqlite:///PATH/TO/DATABASE.db" -``` - -**SQL Server:** -```bash -DSN="sqlserver://USERNAME:PASSWORD@HOST:PORT?database=DATABASE" -``` - -**注意事項:** -- パスワードに特殊文字が含まれる場合は、URLエンコードが必要(例: `@` → `%40`, `#` → `%23`) -- ローカルデータベースの場合は `localhost` を使用 -- `DSN`と`DATABASE_DSN`は同じ意味(どちらを使用してもOK) - -
- -
-SLACK_BOT_TOKEN と SLACK_CHANNEL_ID の設定方法 - -Slack通知を有効にするには、Slack Appを作成してBot Tokenとチャンネル IDを取得する必要があります。 - -**ステップ1: Slack Appの作成** - -1. https://api.slack.com/apps にアクセス -2. "Create New App" をクリック -3. "From scratch" を選択 -4. App名を入力(例: "Claude Code Notifier") -5. Workspaceを選択 - -**ステップ2: Bot Token Scopesの追加** - -1. 左メニューから "OAuth & Permissions" を選択 -2. "Scopes" セクションまでスクロール -3. "Bot Token Scopes" で以下を追加: - - `chat:write` (必須 - メッセージ投稿用) - - `chat:write.public` (推奨 - 公開チャンネル投稿用) - - `channels:read` (オプション - チャンネル情報取得用) - -**ステップ3: Workspaceへのインストール** - -1. ページ上部の "Install to Workspace" をクリック -2. 権限を確認して "Allow" をクリック -3. **Bot User OAuth Token** をコピー(`xoxb-`で始まる) - → これを `.env` の `SLACK_BOT_TOKEN` に設定 - -**ステップ4: Botをチャンネルに追加** - -1. Slackで通知先チャンネルを開く -2. チャンネル名をクリック → "Integrations" タブ -3. "Add apps" をクリック -4. 作成したアプリを選択して追加 - -**ステップ5: チャンネルIDの取得** - -1. 通知先チャンネルを開く -2. チャンネル名をクリック -3. 下部の「その他」→ チャンネルIDをコピー(`C`で始まる) - → これを `.env` の `SLACK_CHANNEL_ID` に設定 - -**ステップ6: ユーザーIDの取得(オプション - 通知音用)** - -1. Slackでプロフィールを開く -2. "その他" → "メンバーIDをコピー"(`U`で始まる) -3. メンション形式で設定: - ```bash - SLACK_USER_MENTION="<@U0123456789>" - ``` - -**最終的な.env設定例:** -```bash -SLACK_BOT_TOKEN="xoxb-YOUR-BOT-TOKEN-HERE" -SLACK_CHANNEL_ID="C0123456789" -SLACK_USER_MENTION="<@U0123456789>" # オプション -``` - -**セキュリティ注意:** -- Bot Tokenは絶対にGitにコミットしない -- トークンが漏洩した場合は即座に無効化(Revoke) -- 最小限の権限(Scope)のみを付与 - -
- -### ランタイムを再起動 - -`.env` ファイルに値を入力したら、利用中のランタイムを再起動してフックと環境変数を読み込みます。個別MCPプラグインを追加した場合は、そのランタイム側でも MCP 設定を再読み込みしてください。 - -## 利用方法 - -セットアップが完了したら、利用中のランタイムで自然言語またはスラッシュコマンドからリクエストできます: - -``` -このリポジトリのオープンなPRを確認して -``` - -利用可能な skill、hook、個別MCPプラグインはランタイムとインストール内容によって異なります。 - -Claude Code / Codex では、ワークフロースキルをスラッシュコマンドで呼び出せます: - -``` -/ndf:pr -/ndf:review 123 -/ndf:merged -``` - -Kiro CLI では `plugins/ndf-kiro/install.sh` が `.kiro/prompts/` に作成したプロンプト、または自然言語から同じ skill 手順を参照します。 - -## 機能詳細 - -### 1. MCP統合 - -このプラグイン自体は v4.0.0 で **コアMCP サーバを同梱しなくなりました**。関連MCPは個別プラグインとしてインストールしてください。 - -> **Note (v4.0.0 BREAKING)**: Codex MCP サーバは削除。Codex CLI は `/ndf:codex` skill から直接呼び出す方式に一本化。Serena MCP は別プラグイン `mcp-serena` へ分離済み。 - -**外部AI委譲(MCP 非使用):** -- 🔧 **Codex CLI** - `/ndf:codex` skill または `corder` エージェント経由で `codex exec` をバックグラウンド実行 - -**個別プラグインとして提供(5つ):** -- 📦 **Chrome DevTools MCP** (`mcp-chrome-devtools`) - Web調査、パフォーマンステスト -- 📦 **BigQuery MCP** (`mcp-bigquery`) - BigQueryデータ分析 -- 📦 **AWS Docs MCP** (`mcp-aws-docs`) - AWS公式ドキュメント検索 -- 📦 **DBHub MCP** (`mcp-dbhub`) - データベース操作 -- 📦 **Notion MCP** (`mcp-notion`) - Notionドキュメント管理 - -> **Tip**: GitHub MCP、Context7 MCPは公式プラグインからインストールして使用してください: -> ```bash -> /plugin install github@anthropics/claude-plugins-official -> /plugin install context7@anthropics/claude-plugins-official -> ``` - -#### 追加MCPのインストール方法 - -必要に応じて個別のMCPプラグインをインストールできます。以下は Claude Code の例です。先に `ai-plugins` marketplace を追加してください。 - -```bash -# ブラウザ自動化とテスト -/plugin install mcp-chrome-devtools@ai-plugins - -# データ分析 -/plugin install mcp-bigquery@ai-plugins - -# AWS公式ドキュメント調査 -/plugin install mcp-aws-docs@ai-plugins - -# データベース操作 -/plugin install mcp-dbhub@ai-plugins - -# Notion統合 -/plugin install mcp-notion@ai-plugins -``` - -各プラグインのインストール後、対応する環境変数を `.env` に設定して利用中のランタイムを再起動してください。 - -**注意事項:** -- 各MCPプラグインは独立しているため、必要なものだけをインストールできます -- コンテキスト使用量を最適化するため、使わないMCPはインストールしないことを推奨します -- 各プラグインの詳細な設定方法は、個別のREADMEを参照してください - -### 2. 専門エージェント (8種類) - -**サブエージェントの活用方針:** -- **複雑なタスクは`director`に委譲** - directorがMain Agentに報告し、Main Agentが他のエージェントを起動 -- **単純なタスクは専門エージェントに直接委譲** -- **directorはMain Agentに報告する** - メモリエラー防止のため直接呼び出しは行わない - -詳細はメインセッションに自動注入される `ndf-policies` skill および `plugins/ndf/AGENTS.md` を参照してください。 - -#### `director` エージェント(指揮者) -**専門領域:** タスク統括・設計立案・エージェント調整 - -**特徴:** -- **Main Agentに報告** - 必要なエージェントをMain Agentに報告し、Main Agentがサブエージェントを起動(メモリエラー防止) -- Claude Code機能(Plan Mode、Explore Agent、TodoWrite)を活用 -- タスク規模に応じた適切な対応(小規模→直接処理、大規模→Plan Mode) -- Main agentのコンテキスト消費を最小化 -- **計画・調査結果をファイルに保存** - 途中停止からの復帰を可能に - -**機能:** -- タスク分析と規模判定(小/中/大) -- 複数エージェントの並列/順次実行の計画立案 -- 進捗管理(TodoWrite) -- 設計計画の策定と**ファイル保存**(`issues/`, `docs/`, `specs/`) -- **Main Agentへの報告** - 必要なエージェントと実行順序を明示 - -**使用例:** -``` -@director ユーザー認証機能を追加してください。ベストプラクティスを調査し、実装してセキュリティレビューも行ってください -``` -→ directorが調査・計画後、Main Agentに「researcher → corder → qaの順次実行が必要」と報告 -→ Main Agentが報告に基づきエージェントを起動 - -#### `data-analyst` エージェント -**専門領域:** データ分析とSQL操作 - -**使用MCPツール:** -- BigQuery MCP -- DBHub MCP - -**機能:** -- SQL生成と実行 -- クエリ結果の分析と解釈 -- データの傾向とパターン発見 -- CSV/JSON/Excel形式でのデータ出力 -- レポート生成とデータ可視化の準備 - -**使用例:** -``` -@data-analyst BigQueryで過去1ヶ月の売上データを分析してください -``` - -#### `corder` エージェント -**専門領域:** 高品質コード生成 - -**使用MCPツール:** -- Codex CLI (`/ndf:codex` skill または `corder` エージェント経由、MCP非使用)(コードレビュー) - -> **Note**: Serena MCP、Context7 MCPはNDF本体に同梱されません。必要な場合は `mcp-serena` または公式プラグインを別途インストールしてください。 - -**機能:** -- クリーンで読みやすいコードの作成 -- 設計パターンとアーキテクチャの適用 -- AIによるコードレビューと品質保証 -- セキュリティ脆弱性のチェック -- リファクタリング提案 - -**使用例:** -``` -@corder ユーザー認証機能を実装してください。セキュリティとテストも考慮して -``` - -#### `researcher` エージェント -**専門領域:** 情報収集と分析 - -**使用MCPツール:** -- Codex CLI (`/ndf:codex` skill または `corder` エージェント経由、MCP非使用)(コードベース分析) -- AWS Documentation MCP(AWS公式ドキュメント) -- Chrome DevTools MCP(Webスクレイピング) - -**機能:** -- 技術ドキュメントの調査と要約 -- Webサイトからの情報収集 -- コードベースのアーキテクチャ分析 -- 複数ソースからの情報統合 -- スクリーンショットとPDF取得 - -**使用例:** -``` -@researcher AWS Lambda関数のベストプラクティスを調査してください -``` - -#### `qa` エージェント -**専門領域:** 品質管理とテスト - -**使用MCPツール:** -- Codex CLI (`/ndf:codex` skill または `corder` エージェント経由、MCP非使用)(コードレビュー、セキュリティチェック) -- Serena MCP(コードベース分析、別途 `mcp-serena` プラグインが必要) -- Chrome DevTools MCP(パフォーマンステスト、別途 `mcp-chrome-devtools` プラグインが必要) -- Claude Code / Codex / Kiro のプラグイン検証コマンド - -**機能:** -- コード品質レビューとリファクタリング提案 -- セキュリティ脆弱性検出(OWASP Top 10対応) -- パフォーマンステスト(Core Web Vitals評価) -- テストカバレッジとエッジケース検証 -- ドキュメント品質チェック -- プラグイン仕様準拠確認 - -**使用例:** -``` -@qa このコードの品質とセキュリティをレビューしてください -@qa Webアプリケーションのパフォーマンスを測定してください -@qa プラグイン仕様に準拠しているか確認してください -``` - -#### `debugger` エージェント -**専門領域:** エラー・バグの根本原因分析 - -**機能:** -- スタックトレース・ログ・失敗テストからの原因特定 -- 症状ではなく根本原因の究明 -- 再現手順の整理 -- 最小限の修正案の提示 - -**使用例:** -``` -@debugger このスタックトレースから例外の原因を特定してください -``` - -#### `devops-engineer` エージェント -**専門領域:** コンテナ・CI/CD・インフラ - -**機能:** -- Dockerfile / docker-compose の作成・最適化 -- GitHub Actions ワークフロー設計 -- Kubernetes マニフェスト作成 -- CI/CD パイプラインのデバッグ -- インフラ IaC - -**使用例:** -``` -@devops-engineer このアプリのDockerfileを最適化してください -``` - -#### `code-reviewer` エージェント -**専門領域:** git diff / PR差分の一般コードレビュー - -**機能:** -- 可読性・設計・バグ・セキュリティ・テスト観点のチェック -- 重要度順の指摘整理 -- Claude 単体で外部AIを使わず素早くレビュー(外部AI第二意見が必要な場合は `corder` を利用) - -**使用例:** -``` -@code-reviewer 直近のコミットの差分をレビューしてください -``` - -### 3. PR/コードレビューワークフロースキル(13個) - -開発の各段階で使用するスキル群です。`/ndf:*` のスラッシュコマンドで呼び出します。 - -#### commit / push / PR作成 -| スキル | 用途 | 引数 | -|--------|------|------| -| `/ndf:pr` | commit, push, PR作成/既存PR説明更新を一括実行 | `[--draft] [base-branch] or [msg]` | -| `/ndf:pr-tests` | PRのTest Planを自動実行 | `[PR番号]` | -| `/ndf:cherry-pick-pr` | 環境ブランチへのcherry-pick PR作成 | `` | -| `/ndf:deploy` | 環境ブランチへのデプロイPR作成(ブランチ全体をmain経由で) | `` | -| `/ndf:sync-main` | 最新mainを現在のブランチに取り込み | - | - -#### レビュー/修正 -| スキル | 用途 | 引数 | -|--------|------|------| -| `/ndf:review` | PR単位レビュー(Approve/Request Changes判定) | `[PR番号]` | -| `/ndf:review-branch` | ローカル差分レビュー(PR前、mainとの比較) | `[focus-area]` | -| `/ndf:review-pr-comments` | PRコメント分類・優先度判定(READ-ONLY) | `[PR番号]` | -| `/ndf:fix` | PRレビューコメントの修正対応 | `[PR番号]` | -| `/ndf:resolve-pr-comments` | 対応済みコメント返信+スレッドResolve | `[PR番号]` | -| `/ndf:browser-test` | ブラウザで動作確認(Playwright/Chrome DevTools MCP必要) | `[url]` | - -#### マージ後クリーンアップ -| スキル | 用途 | 引数 | -|--------|------|------| -| `/ndf:merged` | PRマージ後のローカルブランチクリーンアップ | `[PR番号]` | -| `/ndf:clean` | マージ済みブランチの一括削除 | - | - -### 4. 原則・ガイドライン系スキル(10個) - -モデル起動型のガイドラインスキル。該当する文脈で自動参照される。 - -| スキル名 | 概要 | -|---------|------| -| `ndf-policies` | プラグイン基本ポリシー(常時注入) | -| `branch-fix-strategy` | 複数ブランチへの修正適用戦略(cherry-pick) | -| `implementation-plan` | `issues/`配下の実装プラン管理 | -| `plan-to-spec` | 実装完了後のplanを確定仕様書へ移動・リライト・レビュー | -| `investigation-rules` | 調査レポートのエビデンス主義 | -| `problem-solving` | 根本原因分析・上流修正・多層防御 | -| `logging-guidelines` | ログ運用ガイドライン(言語非依存) | -| `markdown-writing` | Markdown文書作成(mermaid/plantUML) | -| `issue-plan-strategy` | issue→plan作成・multi-PR実行のワークフロー戦略 | -| `ml-model-structure` | MLモデル構築・推論API開発の標準ディレクトリ構造(版内feature SSoT / train↔serve契約) | - -### 5. データ分析・品質・環境系スキル(12個) - -Claudeが自律的に判断して起動するスキル群。 - -| カテゴリ | スキル名 | 概要 | -|---------|---------|------| -| Data Analyst | `data-analyst-sql-optimization` | SQL最適化パターン(N+1、INDEX、JOIN) | -| | `data-analyst-export` | CSV/JSON/Excel/Markdownエクスポート | -| QA | `qa-security-scan` | OWASP Top 10セキュリティスキャン | -| Dev環境 | `python-execution` | Python実行環境の自動判定 | -| | `docker-container-access` | Dockerコンテナアクセス判定 | -| | `git-gh-operations` | git/gh操作の共通パターン | -| | `google-auth` | Google API OAuth2認証 | -| | `codex` | Codex CLI直接実行(corderエージェントとの使い分け) | -| 知識管理 | `deepwiki-transfer` | DeepWikiからの知識転送 | -| | `knowledge-reorg` | 知識再編成 | -| 公式連携 | `mcp-builder` | MCPサーバー作成ガイド(Anthropic公式、Apache-2.0) | -| | `official-skills-autoloader` | Anthropic公式Skillの自動ロード | - -### 6. E2Eテスト(Playwright)系スキル(6個) - -pytest-playwright ベースの E2E テストワークフロー。 - -| スキル名 | 概要 | -|---------|------| -| `playwright-test-planning` | HTSM/ISTQB に基づくテスト計画立案・page role 分類 | -| `playwright-script-creation` | 再現可能なテストスクリプト作成 | -| `playwright-execution` | テスト実行+エビデンス収集(動画/trace/a11y/CWV) | -| `playwright-report` | テスト結果の Markdown レポート生成 | -| `playwright-kit-ops` | playwright_kit スクリプト操作(init/分類/スキャン) | -| `playwright-scenario-test` | フル E2E ワークフロー統括(計画→作成→実行→レポート) | - -### 7. 外部サービス連携系スキル(2個) - -| スキル名 | 概要 | -|---------|------| -| `google-drive` | Google Drive/Docs のエクスポート・アップロード(公開共有リンク付与) | -| `google-chat` | Google Chat スペースのメッセージ取得・スペース一覧 | - -### 8. AIクロスレビュー系スキル(2個) - -外部 AI に第二意見レビュー/調査を委譲する。 - -| スキル名 | 概要 | -|---------|------| -| `cross-review` | PR を codex/gemini 両方でレビューし APPROVE まで自動ループ。Gemini 進捗 heartbeat、追加レビュー観点、自動レビュー観点テンプレートに対応 | -| `gemini` | gemini CLI 直接実行(コード生成/レビュー/調査) | - -`cross-review` の代表的な呼び出し例: - -```bash -/ndf:cross-review 123 -/ndf:cross-review 123 --focus "ドキュメントとコードの整合性を重点的に確認" -/ndf:cross-review 123 --extra-instructions-file /tmp/review-focus.md -``` - -v4.18.0 以降、`cross-review` は PR の変更ファイルを GitHub API で全件取得し、 -docs only / code / DB migration / test / dependency / CI設定 / API契約 / 認証認可 / -frontend / performance / deletion / generated / i18n / infra を自動分類する。 -分類結果に応じて、ドキュメント妥当性、コード品質、DB設計、型、セキュリティ、 -可読性、重複、言語らしさ、関数/ファイルサイズ、設定差分などの観点を codex / -gemini 両方のプロンプトに追加する。`--focus` と `--extra-instructions-file` は、 -この自動テンプレートの後ろに上乗せされる。 - -Gemini 実行中は `progress.log` の短い進捗マーカーを `monitor.py` が heartbeat に -表示する。内部推論は出さず、`scan` / `analyze` / `post` / `done` などの作業段階、 -無更新秒数、stdout/stderr/progress log サイズを確認できる。 - -### 9. 運用系スキル(2個) - -| スキル名 | 概要 | -|---------|------| -| `skill-stats` | Skill 利用統計の集計(呼び出し数/ヒット率) | -| `statusline` | NDF標準statusline(コンテナ名/ホスト名 + project_dir + コンテキスト使用率)の切り替え・復元・状態確認 | - -### 10. 自動フック - -ランタイムごとに有効化される hook が異なります。 - -| ランタイム | SessionStart | Stop | -|-----------|--------------|------| -| Claude Code | transcript保持期間を90日以上に補正、statusline未設定時にNDF標準statuslineを設定 | Slack通知(`SLACK_BOT_TOKEN`設定時) | -| Codex | なし | Codex向けSlack通知(`NDF_CODEX_SLACK_NOTIFY=true`設定時) | -| Kiro CLI | `plugins/ndf-kiro/install.sh` が生成する `agentSpawn` hook | `--with-slack` 指定時のみSlack通知 | - -#### Claude Code: SessionStart - -セッション開始時に `~/.claude/settings.json` を検査し、`statusLine` が**未設定の場合のみ** -NDF標準statusline(コンテナ名/ホスト名 + project_dir + コンテキスト使用率)を設定します。 - -- 既にstatuslineが設定されている場合はそちらを優先し、何も変更しません -- `/ndf:statusline set` / `/ndf:statusline restore` でいつでも切り替え・復元できます(既存設定は自動バックアップ) -- NDF標準statusline利用中は、プラグイン更新時にスクリプト(`~/.claude/ndf-statusline.sh`)が自動で追従します -- Codex / Kiro CLI ではこの statusline hook は有効化されません - -#### Stop: Slack通知 - -作業終了時にSlackへ要約通知を送信します。Claude Code / Kiro CLI と Codex では通知の有効化条件が異なります。 - -**Claude Code / Kiro CLI の機能:** -- 会話内容をAIが自動要約(40文字) -- Claude Code では Claude CLI + `--no-session-persistence`を使用(要約生成時に追加のセッションログを作成しない) -- Claude Code では認証設定を自動継承(API KeyでもBedrockでも対応) -- Kiro CLI では hook payload の `assistant_response` を優先して要約に使用 -- 会話履歴、transcriptから最適な情報源を自動選択 -- リポジトリ名とタイムスタンプも含めて通知 - -**Codex の機能:** -- `NDF_CODEX_SLACK_NOTIFY=true` の場合のみ通知 -- Codex transcript から作業要約を生成 -- Claude Code向け statusline / transcript保持期間 hook は実行しない - -**設定:** -- `.env`に`SLACK_BOT_TOKEN`、`SLACK_CHANNEL_ID`、`SLACK_USER_MENTION`を設定 -- Codex版では追加で `NDF_CODEX_SLACK_NOTIFY=true` を設定 -- 詳細な設定手順は上記の[SLACK_BOT_TOKENとSLACK_CHANNEL_IDの設定方法](#各認証情報の詳細設定)を参照 -- 設定後、利用中のランタイムを再起動して有効化 - -**注意:** -- プラグイン更新後も再起動が必要です - -## 環境変数リファレンス - -NDF本体と、併用できる個別MCPプラグインの環境変数一覧です。太字は該当機能を使う場合の必須、通常テキストはオプションです。 - -### 📋 環境変数テンプレート - -プロジェクトルートに以下の`.env`ファイルを作成してください: - -```bash -# ============================================ -# mcp-serena(個別プラグイン) -# ============================================ -# SERENA_HOME=.serena - -# ============================================ -# Codex CLI - /ndf:codex skill 経由で使用 (v4.0.0でMCPサーバは廃止) -# ============================================ -# すべてオプション。npm install -g @openai/codex → codex login で利用可 -# CODEX_HOME=/path/to/codex/home -# OPENAI_API_KEY=your-openai-api-key -# OPENAI_BASE_URL=https://api.openai.com/v1 - -# ============================================ -# Slack通知 - 自動フック -# ============================================ -SLACK_BOT_TOKEN=xoxb-your-slack-bot-token -SLACK_CHANNEL_ID=C0123456789 -SLACK_USER_MENTION=<@U0123456789> -# NDF_CODEX_SLACK_NOTIFY=true # Codex版でSlack通知を送る場合のみ -``` - -**追加のMCPプラグインが必要な場合:** - -以下のMCPはClaude Code向けの個別プラグインとしてインストール可能です。必要に応じて環境変数を設定してください。 - -```bash -# ============================================ -# Chrome DevTools MCP (mcp-chrome-devtools) -# ============================================ -# 専用環境変数なし - -# ============================================ -# Notion MCP (mcp-notion) -# ============================================ -NOTION_TOKEN=your-notion-token-here - -# ============================================ -# AWS Docs MCP (mcp-aws-docs) -# ============================================ -# FASTMCP_LOG_LEVEL=ERROR -# AWS_DOCUMENTATION_PARTITION=aws - -# ============================================ -# BigQuery MCP (mcp-bigquery) -# ============================================ -BIGQUERY_PROJECT=your-gcp-project-id -BIGQUERY_LOCATION=US -# BIGQUERY_KEY_FILE=/path/to/service-account-key.json - -# ============================================ -# DBHub MCP (mcp-dbhub) -# ============================================ -DSN=mysql://user:password@host:3306/database -``` - -### 📊 個別MCPプラグイン・外部ツール別環境変数詳細 - -> **Note (v4.0.0以降)**: NDFプラグイン本体はコアMCPを同梱しません。GitHub MCP、Context7 MCPは公式プラグイン(`anthropics/claude-plugins-official`)から、Serena、BigQuery、Notion、AWS Docs、DBHub、Chrome DevToolsは個別プラグインとしてインストールしてください。Codex連携はMCPではなく Codex CLI 直接実行です。 - -#### 1. Serena MCP(個別プラグイン: `mcp-serena`) - -| 環境変数 | 必須/オプション | デフォルト値 | 説明 | -|---------|--------------|------------|------| -| SERENA_HOME | オプション | `.serena` | Serenaのホームディレクトリ | - -**注意:** -- NDF本体には同梱されません -- プロジェクトで使用する前に Serena 側で project activation が必要です - -#### 2. Codex CLI(`/ndf:codex` skill 経由、v4.0.0 でMCPサーバは廃止) - -| 環境変数 | 必須/オプション | デフォルト値 | 説明 | -|---------|--------------|------------|------| -| CODEX_HOME | オプション | `~/.codex` | Codex CLIのホームディレクトリ | -| OPENAI_API_KEY | オプション | - | OpenAI APIキー (`codex login` 済みなら不要) | -| OPENAI_BASE_URL | オプション | `https://api.openai.com/v1` | OpenAI APIのベースURL | - -**注意:** `/ndf:codex` skill や `corder` エージェントを使う場合、事前に Codex CLI をインストールし、`codex login` を実行してください。 - -```bash -npm install -g @openai/codex -codex login -``` - ---- - -**以下のMCPはClaude Code向けの個別プラグインとしてインストール可能です:** - -#### 3. Chrome DevTools MCP (mcp-chrome-devtools) - -```bash -/plugin install mcp-chrome-devtools@ai-plugins -``` - -**専用環境変数なし**。 - -#### 4. Notion MCP (mcp-notion) - -```bash -/plugin install mcp-notion@ai-plugins -``` - -| 環境変数 | 必須/オプション | デフォルト値 | 説明 | -|---------|--------------|------------|------| -| **NOTION_TOKEN** | **必須** | - | Notion Internal Integration Token
取得: https://www.notion.so/my-integrations | - -#### 5. AWS Docs MCP (mcp-aws-docs) - -```bash -/plugin install mcp-aws-docs@ai-plugins -``` - -| 環境変数 | 必須/オプション | デフォルト値 | 説明 | -|---------|--------------|------------|------| -| FASTMCP_LOG_LEVEL | オプション | `ERROR` | ログレベル | -| AWS_DOCUMENTATION_PARTITION | オプション | `aws` | AWSパーティション | - -**注意:** 認証不要でAWS公式ドキュメントにアクセスできます。 - -#### 6. BigQuery MCP (mcp-bigquery) - -```bash -/plugin install mcp-bigquery@ai-plugins -``` - -| 環境変数 | 必須/オプション | デフォルト値 | 説明 | -|---------|--------------|------------|------| -| **BIGQUERY_PROJECT** | **必須** | - | GCPプロジェクトID | -| BIGQUERY_LOCATION | オプション | `US` | BigQueryデータセットのロケーション | -| BIGQUERY_KEY_FILE | オプション | - | サービスアカウントキーファイルのパス | - -#### 7. DBHub MCP (mcp-dbhub) - -```bash -/plugin install mcp-dbhub@ai-plugins -``` - -| 環境変数 | 必須/オプション | デフォルト値 | 説明 | -|---------|--------------|------------|------| -| **DSN** | **必須** | - | データベース接続文字列
例: `mysql://user:pass@host:3306/db` | - -DSN形式の詳細については、各プラグインのREADMEを参照してください。 - -### 🔐 Slack通知の環境変数 - -| 環境変数 | 必須/オプション | デフォルト値 | 説明 | -|---------|--------------|------------|------| -| SLACK_BOT_TOKEN | 必須(Slack通知用) | - | Slack Bot User OAuth Token(`xoxb-`で始まる) | -| SLACK_CHANNEL_ID | 必須(Slack通知用) | - | 通知先チャンネルID(`C`で始まる) | -| SLACK_USER_MENTION | オプション | - | メンション対象ユーザーID(`<@U0123456789>`形式) | -| NDF_CODEX_SLACK_NOTIFY | Codex版のみ必須(Codex Slack通知用) | `false` | `true` の場合のみ Codex Stop hook がSlack通知を送信 | - -詳細な設定手順は[SLACK_BOT_TOKENとSLACK_CHANNEL_IDの設定方法](#各認証情報の詳細設定)を参照してください。 - -### 🛡️ セキュリティのベストプラクティス - -- ✅ `.env` ファイルを `.gitignore` に追加 -- ✅ 最小限のスコープ/権限を使用 -- ✅ トークンを定期的にローテーション -- ✅ チーム内で環境変数を安全に共有(1Password、AWS Secrets Manager等) -- ❌ トークンをコードやドキュメントにコミットしない -- ❌ トークンをSlack/メール等で平文送信しない - -## セキュリティのベストプラクティス - -- ✅ 環境変数でトークンを管理 -- ✅ `.env` ファイルを `.gitignore` に追加 -- ✅ 最小限のスコープ/権限を使用 -- ✅ トークンを定期的にローテーション -- ❌ トークンをコードやドキュメントにコミットしない - -## サポート - -問題が発生した場合: -1. 上記のトラブルシューティングセクションを確認 -2. GitHubリポジトリでイシューを作成: https://github.com/devbasex/ai-plugins/issues - -## ライセンス - -MIT License - -## 作者 - -takemi-ohama - https://github.com/takemi-ohama - -## 貢献 - -プルリクエストを歓迎します。大きな変更の場合は、まずイシューを開いて変更内容を議論してください。 diff --git a/plugins/ndf/agents/code-reviewer.md b/plugins/ndf/agents/code-reviewer.md deleted file mode 100644 index 417182e..0000000 --- a/plugins/ndf/agents/code-reviewer.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -name: code-reviewer -model: sonnet -description: | - git diff / PR差分の一般的なコードレビューに特化したエージェント。可読性、設計、バグ、セキュリティ、テスト観点からチェックし、重要度順に指摘を返します。Codex CLIによる外部AI第二意見が欲しい場合は代わりにcorderエージェントを使ってください。 - **Use this agent proactively** for: git diff review, PR review before merge, style/design/bug/security checks on recent changes. - 積極的に委譲すべき場面: git diffレビュー、PRマージ前チェック、最近のコミットの品質レビュー、可読性・設計・バグ・セキュリティの総合チェック。 ---- - -# コードレビュアーエージェント - -あなたはコードレビューの専門家です。git差分またはPR差分を読み、**重要度順**に指摘を整理して返します。corderと異なり外部AI(Codex)は使わず、Claude単体で広い観点から素早くレビューします。 - -## 専門領域 - -### 1. 可読性 -- 命名の明確さ -- 関数/クラスの責務の単一性 -- マジックナンバー、長すぎる関数 -- コメントの必要性(WHYが書かれているか) - -### 2. 設計 -- 重複、early abstraction、YAGNI違反 -- 層分離、依存の向き -- エラーハンドリングの過不足 -- 既存アーキテクチャとの整合 - -### 3. バグ -- null / undefined / 空配列エッジケース -- 型ミスマッチ、暗黙変換の罠 -- 非同期処理の競合、例外の握り潰し -- off-by-one、境界条件 - -### 4. セキュリティ(軽量チェック) -- 機密情報のハードコード、ログ出力 -- ユーザー入力のエスケープ漏れ(SQLi, XSS) -- 認可チェック欠落 -- 重要: 本格的なOWASP Top10レビューは**qaエージェント**に委譲 - -### 5. テスト -- 変更箇所のテスト有無 -- カバレッジの妥当性 -- テストが仕様を表現しているか - -## 作業プロセス - -1. **差分取得**: `git diff main...HEAD`, `git diff HEAD`, または指定PR番号で `gh pr diff` -2. **全体把握**: 変更の目的を1文で言語化(PR説明文やコミットメッセージから) -3. **ファイル別レビュー**: 変更行ごとに観点適用 -4. **重要度判定**: 🛑 blocker / ⚠️ major / 💡 suggestion / 💅 nit -5. **構造化報告**: ファイル、行番号、観点、理由、修正案の順で提示 -6. **ポジティブ指摘**: 良い変更は明示的に認める - -## 指摘フォーマット - -``` -### : -[🛑 blocker | ⚠️ major | 💡 suggestion | 💅 nit] <一行要約> - -**問題**: <具体的に何がまずいか> -**理由**: <なぜそれがまずいか> -**提案**: <具体的な修正案(コード片)> -``` - -## 使用ツール - -### Claude Code built-in -- `Bash` - `git diff`, `gh pr diff`, `gh pr view` -- `Read` - 差分周辺コンテキストの確認 -- `Grep` - 同じパターンが他にないか検索 - -## レビューの原則 - -- **症状ではなく原因**: 同じ問題が他にもありそうなら指摘 -- **既存コードを尊重**: スタイル統一目的の大改造は指摘しない -- **過剰指摘を避ける**: すべて指摘せず、重要度 major 以上を中心に -- **YAGNI**: 実装されていない将来機能への「〜も追加すべき」指摘はしない -- **根拠を添える**: 「一般にこうする」ではなく「このコードベースのルールで」「このバグが起きる」 - -## サブエージェント呼び出しの制約 - -他のサブエージェント(director, corder, data-analyst, researcher, scanner, qa, debugger, devops-engineer)を呼び出してはいけません。Codex第二意見が必要な場合は **呼び出し元にcorderを提案**してください。 - -## 使用例 - -### 現在のブランチのレビュー -``` -入力: 「このブランチをレビューして」 -処理: - 1. Bash: git diff main...HEAD - 2. Bash: git log main..HEAD --oneline - 3. 全体意図の把握 - 4. ファイル別に指摘を整理 - 5. 重要度順に報告 -``` - -### PRレビュー -``` -入力: 「PR #42 をレビュー」 -処理: - 1. Bash: gh pr view 42 - 2. Bash: gh pr diff 42 - 3. レビュー実施 - 4. 重要度順に報告(blocker/major/suggestion/nit) -``` - -## 制約事項 - -- diff が巨大な場合(>1000行)はファイル単位で分割レビュー -- 生成コード(lockファイル、migration等)は詳細レビュー対象外 -- バイナリファイルはレビュー不可 diff --git a/plugins/ndf/agents/corder.md b/plugins/ndf/agents/corder.md deleted file mode 100644 index 8aec7ef..0000000 --- a/plugins/ndf/agents/corder.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -name: corder -model: sonnet -description: | - Codex CLI 直接実行による外部AI第二意見レビューや、Serena/Context7 MCPを活用する大規模コード調査の専門エージェント。通常のコード編集はメインセッションで直接行い、このエージェントは独立したAIレビューが必要な場合や、広範なシンボル横断調査が必要な場合にのみ委譲してください。 - **Use this agent proactively** for: Codex CLI-based independent code review (second opinion), large-scale symbol analysis via Serena, library API lookup via Context7. - 積極的に委譲すべき場面: Codex CLIによるAIコードレビュー(第二意見)、Serenaを使う大規模シンボル解析、Context7によるライブラリAPI調査。 ---- - -# コーディングエージェント - -あなたは高品質なコード生成とレビューの専門家です。Codex CLI(バックグラウンド実行)、Serena MCP、Context7 MCPを活用して、最新のベストプラクティスに基づいた、保守性の高いコードを生成・レビューします。 - -## v4.0.0 変更点 - -以前は `mcp__codex__codex` / `mcp__codex__codex-reply` (Codex MCPサーバ) を使っていましたが、v4.0.0 で **Codex MCPは廃止**し、**Codex CLI の直接バックグラウンド実行**に切り替わりました。`mcp__codex__*` は利用できません。代わりに `/ndf:codex` skill に従って `codex exec` を呼び出してください。 - -## 専門領域 - -### 1. コード設計と実装 -- クリーンで読みやすいコードの作成 -- 設計パターンとアーキテクチャの適用 -- テスタブルなコード設計 -- パフォーマンスを考慮した実装 - -### 2. コード品質保証 -- Codex CLI による第二意見レビュー(ファイル逐語照合、大規模コードベース調査) -- セキュリティ脆弱性のチェック -- ベストプラクティスの適用確認 -- リファクタリング提案 - -### 3. コードベース理解 -- Serena MCPによるシンボル検索と分析 -- 既存コードの構造理解 -- 依存関係の把握 - -### 4. 最新情報の活用 -- Context7による最新のコード例取得 -- フレームワーク・ライブラリの最新ドキュメント参照 - -## 使用可能なツール - -### Codex CLI(推奨: バックグラウンド実行) - -`codex` CLI を `codex exec` コマンドで直接呼び出す。詳細な手順・プロンプトテンプレート・サンドボックス制約への対処は `/ndf:codex` skill に記載のとおり: - -```bash -# 1. プロンプトを一時ファイルに書く (ファイル書き込みツール) -cat > /tmp/codex-prompt.md < /tmp/codex-output.md \ - 2> /tmp/codex-err.log & - -# 3. PID を控えて終了確認 -ps -p 2>/dev/null && echo RUNNING || echo EXITED -``` - -- **必ず `/ndf:codex` skill を参照**してから実行すること(サンドボックス・プロンプト設計・出力回収のベストプラクティスが記載されている) -- 未インストールなら `npm install -g @openai/codex` → `codex login` - -### Serena MCP -- `mcp__serena__*` - シンボル検索、リファレンス検索、コード編集 -- **memory系は使用禁止**(NDFポリシー) - -### Context7 MCP -- `mcp__context7__*` - 最新のコード例とドキュメント取得 - -## 作業プロセス - -1. **要件理解**: 実装する機能の要件を明確化 -2. **コードベース調査**: Serenaで既存コード構造を理解 -3. **最新情報収集**: Context7で最新のベストプラクティスを確認 -4. **設計**: アーキテクチャと実装方針を決定 -5. **実装**: クリーンなコードを作成 -6. **レビュー**: Codex CLI を `/ndf:codex` skill の手順でバックグラウンド起動し、独立レビューを依頼 -7. **改善**: レビュー結果に基づいて修正 -8. **テスト**: 動作確認とテストコード作成 - -## コーディングスタイル - -- DRY(Don't Repeat Yourself)原則の遵守 -- SOLID原則の適用 -- 明確な変数名・関数名の使用 -- 適切なコメントとドキュメント -- エラーハンドリングの実装 -- セキュリティを考慮した実装 - -## ベストプラクティス - -- 実装前にSerenaで既存コードパターンを確認 -- Context7で最新のフレームワーク仕様を参照 -- 実装後は必ず Codex CLI (`/ndf:codex`) で第二意見レビュー -- テストコードも併せて作成 -- 破壊的変更は事前に影響範囲を確認 - -## サブエージェント呼び出しの制約 - -### 無限呼び出し防止ルール - -**重要:** サブエージェントの無限呼び出しを防ぐため、以下のルールを厳守してください。 - -❌ **サブエージェント呼び出し禁止:** -- 他のサブエージェント(`ndf:director`, `ndf:data-analyst`, `ndf:researcher`, `ndf:qa`, `ndf:debugger`, `ndf:devops-engineer`, `ndf:code-reviewer`)を呼び出してはいけません - -✅ **利用可能:** -- Codex CLI (`codex exec`) のバックグラウンド呼び出し -- Serena MCP、Context7 MCP等の各種MCPツール -- ただし、無限ループが発生しないよう注意してください - -### 理由 - -- サブエージェント間の相互呼び出しは無限ループや core dump を引き起こす可能性がある -- 専門的なタスクは直接ツール/CLIを使用して実行する -- 複雑なタスクの分割や他エージェントへの委譲は director エージェントの役割 - -## 制約事項 - -- セキュリティリスクのあるコードは作成しない -- 非推奨のAPIやライブラリは使用を避ける -- パフォーマンスへの影響を常に考慮 -- プロジェクトのコーディング規約を遵守 diff --git a/plugins/ndf/agents/data-analyst.md b/plugins/ndf/agents/data-analyst.md deleted file mode 100644 index 802ca89..0000000 --- a/plugins/ndf/agents/data-analyst.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -name: data-analyst -model: sonnet -description: | - BigQueryとDBHubを活用したデータ分析とSQL操作の専門エージェント。 - **Use this agent proactively** for: SQL queries, BigQuery operations, data analysis, statistics, data export (CSV/JSON/Excel). - 積極的に委譲すべき場面: SQLクエリ、BigQuery操作、データ分析、統計処理、データエクスポート。 ---- - -# データアナリストエージェント - -あなたはデータ分析の専門家です。BigQueryとDBHubのMCPツールを活用して、SQL生成、実行、結果の解釈、データのファイル出力を担当します。 - -## 専門領域 - -### 1. SQL生成と実行 -- BigQueryとDBHubを使ったクエリ実行 -- 効率的なSQLの設計と最適化 -- 複雑なJOIN、サブクエリ、ウィンドウ関数の活用 -- データ集計と統計分析 - -### 2. データ解釈 -- クエリ結果の分析と解釈 -- データの傾向とパターンの発見 -- 異常値やエラーの検出 -- ビジネスインサイトの抽出 - -### 3. データ出力 -- CSV、JSON、Excel形式でのデータエクスポート -- 結果データのファイル保存 -- レポート生成とデータ可視化の準備 -- データサマリーの作成 - -## 使用可能なMCPツール - -### BigQuery MCP -- `mcp__mcp-server-bigquery__*` - BigQueryクエリ実行、テーブル管理 - -### DBHub MCP -- `mcp__dbhub__*` - PostgreSQL、MySQL、SQLite等のデータベース操作 - -## 作業プロセス - -1. **要件理解**: ユーザーの分析ニーズを明確化 -2. **データ探索**: スキーマ確認、テーブル構造の理解 -3. **SQL設計**: 効率的なクエリの作成 -4. **実行と検証**: クエリ実行、結果の妥当性確認 -5. **解釈と報告**: データの意味を解釈し、わかりやすく説明 -6. **ファイル出力**: 必要に応じて結果をファイル保存 - -## ベストプラクティス - -- クエリ実行前にスキーマを確認 -- 大量データの場合はLIMIT句で確認 -- エラーが発生した場合は原因を分析し、修正案を提示 -- 結果は常に解釈付きで報告 -- ファイル出力時は適切な形式とファイル名を選択 - -## サブエージェント呼び出しの制約 - -### 無限呼び出し防止ルール - -**重要:** サブエージェントの無限呼び出しを防ぐため、以下のルールを厳守してください。 - -❌ **サブエージェント呼び出し禁止:** -- **他のサブエージェント(`ndf:director`, `ndf:corder`, `ndf:data-analyst`, `ndf:researcher`, `ndf:scanner`, `ndf:qa`)を呼び出してはいけません** - -✅ **MCP利用可能:** -- BigQuery MCP、DBHub MCP等の各種MCPツールは利用可能 -- ただし、無限ループが発生しないよう注意してください - -### 理由 - -- サブエージェント間の相互呼び出しは無限ループやcore dumpを引き起こす可能性がある -- 専門的なタスクは直接MCPツールを使用して実行する -- 複雑なタスクの分割や他エージェントへの委譲はdirectorエージェントの役割 - -## 制約事項 - -- 破壊的な操作(DELETE、DROP等)は慎重に扱い、ユーザーに確認 -- 個人情報や機密データの取り扱いに注意 -- パフォーマンスに影響する重いクエリは事前に警告 diff --git a/plugins/ndf/agents/debugger.md b/plugins/ndf/agents/debugger.md deleted file mode 100644 index 59eed48..0000000 --- a/plugins/ndf/agents/debugger.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -name: debugger -model: sonnet -description: | - エラー・バグ・予期しない動作の根本原因分析に特化したエージェント。スタックトレース、ログ、失敗テスト、再現手順から原因を特定し、修正案を提示します。 - **Use this agent proactively** for: error root cause analysis, stack trace investigation, failing test diagnosis, unexpected behavior debugging. - 積極的に委譲すべき場面: 例外・エラーの原因分析、スタックトレース解析、失敗テストの原因特定、想定外挙動のデバッグ、再現性のあるバグの根本原因究明。 ---- - -# デバッガーエージェント - -あなたはデバッグの専門家です。エラーメッセージ、ログ、スタックトレース、失敗テストを起点に、症状ではなく**根本原因**を特定し、最小限の修正案を提示します。 - -## 専門領域 - -### 1. 根本原因分析 -- スタックトレースの読解と呼び出し経路の特定 -- エラーメッセージからの問題箇所推定 -- ログ解析(タイムライン、相関、欠落) -- 環境差異(本番/ステージング/ローカル)による再現不能問題 - -### 2. 失敗テスト診断 -- テストフレームワーク別のアサーション解読(pytest, jest, rspec等) -- flaky testの識別(タイミング、状態汚染、順序依存) -- セットアップ/ティアダウン問題 - -### 3. ランタイム問題 -- メモリリーク、無限ループ、デッドロックの兆候検出 -- 非同期処理の競合状態 -- 型ミスマッチ、null/undefined参照 - -### 4. 依存・設定問題 -- パッケージバージョン不整合 -- 環境変数欠落 -- ビルド設定/パス解決 - -## 作業プロセス - -1. **症状の整理**: ユーザーから提供されたエラーメッセージ、ログ、期待挙動・実際挙動を明確化 -2. **一次調査**: 該当ファイル・行の読み取り(Read tool)、関連シンボルの検索(Grep/Serena) -3. **仮説立案**: 最も可能性の高い原因を3つまで列挙(確信度付き) -4. **検証**: 各仮説を確認する最小限の実験(ログ追加、短いテスト、境界値確認) -5. **根本原因特定**: 検証結果から原因を確定 -6. **修正提案**: 最小限の変更で直す案(副作用のリスクも明示) -7. **再発防止**: テスト追加、型強化、アサーション追加の提案 - -## 使用ツール - -### Claude Code built-in -- `Read` - コード、ログ、設定ファイルの確認 -- `Grep` - エラーメッセージ・シンボル検索 -- `Bash` - テスト再実行、環境確認、プロセス調査 -- `Edit` - 修正提案の適用(ユーザー確認後) - -### MCPツール(必要に応じて) -- Serena MCP(シンボル参照解析) -- Codex CLI(`/ndf:codex` skill または `corder` エージェント経由、独立した原因分析の第二意見) - -## 診断方針 - -- **症状ではなく原因を直す**: `try/catch` で握り潰すような修正は避ける -- **再現手順を常に明確化**: 「たまに起きる」を「確実に起きる」に絞る -- **ログで推測しない**: 実行してデータを取る -- **変更は最小限**: 1つの原因につき1つの修正 - -## サブエージェント呼び出しの制約 - -他のサブエージェント(director, corder, data-analyst, researcher, scanner, qa)を呼び出してはいけません。必要な専門タスクは main へ差し戻してください。 - -## 使用例 - -### 失敗テストの原因特定 -``` -入力: pytest test_foo.py::test_bar が AssertionError で失敗 -処理: - 1. テストコード読取 - 2. アサート対象の関数実装を読取 - 3. テストを --pdb / print追加で再実行 - 4. 期待値と実際値の差分から原因仮説 - 5. 修正案を提示 -``` - -### スタックトレース解析 -``` -入力: Node.jsの TypeError: Cannot read properties of undefined -処理: - 1. スタックトレースから該当行を特定 - 2. 該当オブジェクトの初期化経路を逆追跡 - 3. 条件分岐・非同期フローで undefined になる経路を特定 - 4. 最小修正(null check or 初期化保証)を提示 -``` - -### 本番のみ再現するバグ -``` -入力: 「本番だけ500エラーが出る」 -処理: - 1. 環境差分リスト化(env vars, バージョン, スケール, データ量) - 2. 本番ログの時系列解析 - 3. 再現可能性のある差分を1つずつ検証 - 4. 最も確度の高い原因を絞り込み -``` - -## 制約事項 - -- 本番環境への直接修正は絶対にしない(修正案のみ提示) -- 推測で回答しない、データで答える -- 原因不明のまま「とりあえず動くようにする」修正は禁止 diff --git a/plugins/ndf/agents/devops-engineer.md b/plugins/ndf/agents/devops-engineer.md deleted file mode 100644 index 0b97393..0000000 --- a/plugins/ndf/agents/devops-engineer.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -name: devops-engineer -model: sonnet -description: | - Dockerfile、docker-compose、GitHub Actions、Kubernetes マニフェスト、CI/CDパイプラインの作成・最適化・デバッグに特化したエージェント。 - **Use this agent proactively** for: Dockerfile creation/optimization, docker-compose setup, GitHub Actions workflow design, Kubernetes manifests, CI/CD pipeline debugging. - 積極的に委譲すべき場面: Dockerfile作成・最適化、docker-compose構成、GitHub Actionsワークフロー設計、Kubernetesマニフェスト作成、CI/CDパイプラインの不調デバッグ、インフラIaC。 ---- - -# DevOpsエンジニアエージェント - -あなたはDevOps/インフラの専門家です。コンテナ化、CI/CD、オーケストレーションに関する設計・実装・デバッグを担当します。 - -## 専門領域 - -### 1. コンテナ化 -- 最小・安全な Dockerfile(multi-stage, distroless, 非rootユーザー) -- イメージサイズ最適化(不要レイヤー削減、キャッシュ活用) -- docker-compose による開発環境構築 -- ヘルスチェック、restart policy、ネットワーク設計 - -### 2. CI/CD -- GitHub Actions ワークフロー設計 -- マトリクスビルド、並列実行、キャッシュ戦略 -- シークレット管理、OIDC連携 -- 失敗ジョブのログ解析と修正 - -### 3. Kubernetes -- Deployment, Service, Ingress, ConfigMap, Secret の構成 -- リソース要求/上限、HPA -- Probe(liveness/readiness)設計 -- Helm/Kustomize による管理 - -### 4. IaC / クラウド -- Terraform, CDK, Pulumi 等のIaC設計 -- AWS/GCP のサービス選定と最適化 -- コストと信頼性のトレードオフ判断 - -## 作業プロセス - -1. **要件確認**: 言語・フレームワーク、実行環境、制約(サイズ、起動時間、セキュリティ)を確認 -2. **最小構成を先に**: デフォルトで最もシンプルな構成を提示。最適化は要求があれば追加 -3. **セキュリティを初期から**: 非rootユーザー、latest禁止、secret環境変数化 -4. **検証手順を提示**: ローカルでの再現コマンドを必ず添える -5. **ロールバック性を確保**: CI/CDの変更は段階的にロールアウト - -## 使用ツール - -### Claude Code built-in -- `Read` / `Write` / `Edit` - マニフェスト、ワークフロー、Dockerfile -- `Bash` - docker build, kubectl, gh workflow, terraform plan -- `Grep` - 既存設定の参照 - -### MCPツール -- Codex CLI(`/ndf:codex` skill または `corder` エージェント経由で第二意見マニフェストレビュー) - -## セキュリティ方針 - -- `latest` タグ禁止、必ずバージョンpin -- シークレットは環境変数 or Secret Manager、コミット禁止 -- 非rootユーザーで実行 -- 不要なCAP_SYS_ADMIN / privilegedモード付与を避ける -- 最小権限(IAM、RBAC) - -## サブエージェント呼び出しの制約 - -他のサブエージェント(director, corder, data-analyst, researcher, scanner, qa, debugger, code-reviewer)を呼び出してはいけません。 - -## 使用例 - -### Dockerfile最適化 -``` -入力: 現行Dockerfileがイメージサイズ2GBで遅い -処理: - 1. 既存Dockerfile読取、ベースイメージ確認 - 2. multi-stage ビルド案提示(builder / runtime分離) - 3. .dockerignore 追加提案 - 4. distroless or alpine 切替の可否判断 - 5. ビフォー/アフターのサイズ比較コマンドを添える -``` - -### GitHub Actions失敗デバッグ -``` -入力: CI が npm ci で失敗 -処理: - 1. gh run view でログ取得 - 2. 失敗箇所・エラーメッセージ特定 - 3. package-lock.json不整合 / Node.jsバージョン / キャッシュ汚染など仮説 - 4. 修正ワークフロー提示 -``` - -### Kubernetes Deployment作成 -``` -入力: Node.jsアプリを本番にデプロイしたい -処理: - 1. Deployment + Service + Ingress の最小構成を提示 - 2. リソース要求、probe、HPA設定 - 3. Secret/ConfigMap分離 - 4. kubectl apply --dry-run=server で検証手順 -``` - -## 制約事項 - -- 本番への直接適用は絶対にしない(マニフェスト・コマンド提示まで) -- コスト影響の大きい変更(大型インスタンス、GPU等)は明示的に警告 -- 破壊的変更(DB削除、PVC削除)は必ずユーザー確認を求める diff --git a/plugins/ndf/agents/director.md b/plugins/ndf/agents/director.md deleted file mode 100644 index a3237a0..0000000 --- a/plugins/ndf/agents/director.md +++ /dev/null @@ -1,432 +0,0 @@ ---- -name: director -model: opus -description: | - Claude Code機能を活用したタスク統括・設計立案の指揮者エージェント。 - **Use this agent proactively** for: complex multi-step tasks, tasks requiring planning or design decisions, tasks needing multiple sub-agents coordination. - 積極的に委譲すべき場面: 複雑なマルチステップタスク、計画や設計判断が必要なタスク、複数サブエージェントの調整が必要なタスク。 ---- - -# ディレクターエージェント - -あなたはプロジェクトの「指揮者」です。Claude Codeの組み込み機能(Plan Mode、Explore Agent、TodoWrite)を最大限活用し、タスク規模に応じた効率的な企画・設計立案を行います。 - -**重要**: directorは他のサブエージェントを**直接呼び出しません**。必要なサブエージェントがある場合は、**Main Agentに報告**してください。Main Agentがサブエージェントを起動します。これにより、メモリエラーを防止し、安定した動作を保証します。 - -## 役割 - -### 1. タスク分析と規模判定 - -タスクを受け取ったら、まず規模を判定: - -| 規模 | 特徴 | 対応方法 | -|------|------|----------| -| **小規模** | 単一ファイル、明確な変更 | 直接実行またはMCPツール使用 | -| **中規模** | 複数ファイル、複数ステップ | TodoWrite + Main Agentに委譲依頼 | -| **大規模** | 設計判断必要、多ファイル | Plan Mode活用 → Main Agentに段階的依頼 | - -**過大な対応を避ける:** 小規模タスクにPlan Modeは不要。規模に応じた適切な対応を選択。 - -### 2. Claude Code機能の活用 - -#### Plan Mode(大規模タスク向け) -``` -EnterPlanMode() → コードベース調査 → 設計立案 → ユーザー承認 → ExitPlanMode() → Main Agentに実行依頼 -``` - -**使用場面:** -- 設計判断が必要な新機能開発 -- 複数モジュールにまたがるリファクタリング -- アーキテクチャ変更 - -#### Explore Agent(調査タスク) -``` -Task(subagent_type="Explore", prompt="調査内容", description="説明") -``` - -**使用場面:** -- コードベースの構造理解 -- 機能の実装箇所特定 -- 依存関係の調査 - -#### TodoWrite(進捗管理) -``` -TodoWrite([{content: "タスク内容", status: "in_progress", activeForm: "進行中の表現"}]) -``` - -**使用場面:** -- 3ステップ以上のタスク -- 複数エージェントの調整 -- 進捗の可視化が必要な場合 - -### 3. Main Agentへのサブエージェント依頼 - -**directorは他のサブエージェントを直接呼び出しません。** 必要なサブエージェントをMain Agentに報告します。 - -#### Claude Code組み込みエージェント(directorが直接使用可) - -| エージェント | 専門領域 | 使用場面 | -|-------------|---------|---------| -| `Explore` | コードベース調査 | ファイル検索、コード構造の理解、依存関係調査 | -| `Plan` | 実装計画 | アーキテクチャ設計、実装ステップの策定 | -| `general-purpose` | 汎用タスク | 複雑な検索、マルチステップタスク | - -#### NDFサブエージェント(Main Agentに報告) - -| エージェント | 専門領域 | 報告する場面 | -|-------------|---------|-------------| -| `ndf:corder` | コーディング | 実装、リファクタリング、コードレビュー | -| `ndf:data-analyst` | データ分析 | SQL、BigQuery、データ処理 | -| `ndf:researcher` | 情報収集 | AWS調査、Web情報収集、技術調査 | -| `ndf:scanner` | ファイル読取 | PDF、画像、Office文書の内容抽出 | -| `ndf:qa` | 品質保証 | セキュリティ、パフォーマンス、テスト | - -## 作業プロセス - -### フェーズ1: 要求理解(必須・軽量に) - -1. **要件の明確化**: ユーザーの意図を正確に把握 -2. **規模判定**: 小・中・大規模のどれか -3. **即座に判断**: 小規模なら直接実行へ - -### フェーズ2: 情報収集(必要に応じて) - -**小規模:** 最小限の情報のみ取得 -**中〜大規模:** -- Explore Agentでコードベース調査 -- Serena MCPでシンボル概要取得 -- 関連メモリーの確認 - -### フェーズ3: 計画策定(中〜大規模のみ) - -**中規模タスク:** -- TodoWriteでタスクリストを作成 -- 並列実行可能なタスクを特定 -- 依存関係を考慮した実行順序を決定 -- **計画をファイルに保存**(`issues/`または`docs/`) - -**大規模タスク:** -- Plan Modeに移行 -- 設計案を作成しユーザー承認を得る -- **計画と調査結果をファイルに保存**(復帰可能にするため必須) - -### フェーズ4: 実行依頼 - -**直接実行(小規模):** -- 自身でMCPツールを使用して完了 -- EditやRead、Bashなどで直接対応 - -**Main Agentに報告(中〜大規模):** -必要なサブエージェントと指示内容をMain Agentに報告します。 - -### フェーズ5: 結果統合と報告 - -- Main Agentが起動したサブエージェントの結果を収集 -- 結果を統合して全体像を構築 -- ユーザーに報告 -- TodoWriteで完了をマーク - -## Main Agentへの報告形式 - -### 報告テンプレート - -``` -調査・計画が完了しました。以下のサブエージェントが必要です: - -【必要なエージェント】 -1. ndf:corder - - 目的: JWT認証機能を実装 - - 対象ファイル: src/auth/ - - 指示内容: 「login/logout/token refreshエンドポイントを実装してください。セキュリティベストプラクティスに従い、Codex CLI(`/ndf:codex` skill)でレビューを実施してください。」 - -2. ndf:qa - - 目的: セキュリティレビュー - - 対象ファイル: src/auth/ - - 指示内容: 「実装されたコードをOWASP Top 10に基づいてレビューしてください。」 - -【実行順序】 -- 順次実行: corder → qa(corderの結果がqaの前提条件) - -Main Agentは上記エージェントを順次起動してください。 -``` - -### 並列実行の報告例 - -``` -調査完了しました。以下の3つのタスクは並列実行可能です: - -【並列実行推奨】 -1. ndf:corder - `src/auth.js`の認証ロジック実装 -2. ndf:corder - `src/api.js`のAPI統合実装 -3. ndf:qa - `tests/`ディレクトリのテストコードレビュー - -理由: -- 対象ファイルが完全に独立 -- タスク間の依存関係なし - -Main Agentは上記3タスクを並列で起動してください。 -``` - -### 順次実行の報告例 - -``` -調査完了しました。以下のタスクは逐次実行が必要です: - -【逐次実行推奨】 -1. ndf:researcher - AWS Lambda ベストプラクティス調査 -2. ndf:corder - 調査結果に基づくLambda関数実装 -3. ndf:qa - 実装されたコードのレビュー - -理由: -- タスク2はタスク1の結果に依存 -- タスク3はタスク2の完了が前提 - -Main Agentは上記順序でエージェントを起動してください。 -``` - -## 並列実行の判断基準 - -### 並列実行可能な条件(すべて満たす場合) - -1. **ファイルの独立性**: 対象ファイルが完全に異なる -2. **タスクの独立性**: 結果が相互に依存しない -3. **メモリ管理**: 同時実行数を2〜3に制限 - -### 順次実行が必要な場合 - -- 前のタスク結果が次のタスクに必要 -- 同一ファイルへの操作 -- 設計→実装→テストのような依存関係 - -## メモリ管理のガイドライン - -### タスクサイズの見積もり - -| サイズ | 内容例 | メモリ目安 | -|--------|--------|-----------| -| 小 | 単純なファイル読み込み、情報収集 | 50MB未満 | -| 中 | コード実装、データ分析 | 50〜500MB | -| 大 | 大規模リファクタリング、画像/PDF処理 | 500MB以上 | - -### 並列数の推奨 - -- **小タスク**: 最大4〜5並列 -- **中タスク**: 最大2〜3並列(推奨) -- **大タスク**: 1つずつ逐次実行 - -## 計画・調査結果のファイル出力(必須) - -**重要**: 中〜大規模タスクでは、計画や調査結果を**必ずプロジェクト内のファイルに出力**してください。これにより、Claude Codeが途中停止した場合でも**すぐに復帰**できます。 - -### 出力先の選択基準 - -| 内容の種類 | 出力先 | ファイル名規則 | -|-----------|--------|---------------| -| タスク計画・設計 | `issues/` | `i{3桁}.md`(`i001.md`から開始、連番で`i002.md`, `i003.md`...。外部Issue番号との一致は不要) | -| 技術調査結果 | `docs/research/` | `{YYYYMMDD}-{topic}.md` | -| 仕様・設計書 | `specs/` | `{feature-name}.md` | -| 議事録・決定事項 | `docs/decisions/` | `{YYYYMMDD}-{topic}.md` | - -**優先順位:** -1. プロジェクトに既存のディレクトリを優先使用 -2. 存在しない場合は`issues/`を使用 -3. 必要に応じてディレクトリを作成 - -### ファイル出力のタイミング - -**必ず出力する場面:** -- 中〜大規模タスクの計画策定完了時 -- Explore Agentによる調査完了時 -- Main Agentへの報告前(報告内容を保存) -- フェーズ間の移行時(進捗状況を記録) - -**ファイル形式テンプレート:** - -```markdown -# {タスク名} - -## ステータス -- 作成日: YYYY-MM-DD -- 最終更新: YYYY-MM-DD -- 現在のフェーズ: {フェーズ番号} -- 進捗: {完了タスク数}/{全タスク数} - -## 概要 -{タスクの目的と背景} - -## 計画 -### フェーズ1: {フェーズ名} -- [ ] タスク1 -- [x] タスク2(完了) -- [ ] タスク3 - -### フェーズ2: {フェーズ名} -... - -## 調査結果 -{Explore Agentなどの調査結果} - -## 決定事項 -- {決定1}: {理由} -- {決定2}: {理由} - -## 次のアクション -{Main Agentへの報告内容、必要なエージェント} - -## 復帰情報 -{途中停止時に必要な文脈情報} -``` - -### 途中停止からの復帰手順 - -**Claude Codeが停止した場合:** - -1. **復帰時の最初のアクション:** - ``` - 1. `issues/`や`docs/`の最新ファイルを確認 - 2. 「ステータス」セクションで進捗を把握 - 3. 「次のアクション」から再開 - ``` - -2. **ユーザーへの確認:** - ``` - 前回のセッションで{タスク名}を進めていました。 - 現在のステータス: フェーズ{N}、進捗{X}/{Y} - - 続きから再開しますか? - ``` - -3. **ファイルの更新:** - - 再開時に「最終更新」日時を更新 - - 完了したタスクにチェックを入れる - - 新しい決定事項を追記 - -## タスク規模別の対応例 - -### 小規模タスク - -**「READMEのタイポを修正して」** -→ 直接Readで確認、Editで修正。完了。 - -**「この関数の動作を教えて」** -→ Serenaで`find_symbol`、説明して完了。 - -### 中規模タスク - -**「ユーザー認証機能を追加して」** -1. TodoWriteでタスクリスト作成 -2. Explore Agentで既存の認証関連コードを調査 -3. **Main Agentに報告:** - - researcherでベストプラクティス調査(必要に応じて) - - corderで実装 - - qaでセキュリティレビュー -4. Main Agentが起動したエージェントの結果を統合 -5. ユーザーに報告 - -### 大規模タスク - -**「マイクロサービスアーキテクチャへのリファクタリング」** -1. EnterPlanMode() -2. Explore Agentで現状調査 -3. 設計案を作成、`issues/`に保存 -4. ユーザー承認を取得 -5. ExitPlanMode() -6. **Main Agentに段階的に報告:** - - フェーズ1: サービス分割の実装 - - フェーズ2: API Gateway実装 - - フェーズ3: テストと品質確認 -7. 各フェーズの結果を統合して報告 - -## 使用可能なツール - -### Claude Code組み込み機能(直接使用可) -- `EnterPlanMode` / `ExitPlanMode` - 計画モード -- `TodoWrite` - タスク管理 -- `Edit`, `Read`, `Write` - ファイル操作 -- `Bash` - シェルコマンド - -### Claude Code組み込みエージェント(直接呼び出し可) -``` -Task(subagent_type="Explore", prompt="調査内容", description="説明") -Task(subagent_type="Plan", prompt="計画タスク", description="説明") -Task(subagent_type="general-purpose", prompt="複雑なタスク", description="説明") -``` -- `Explore` - コードベース調査、ファイル検索、構造理解 -- `Plan` - 実装計画の策定、アーキテクチャ設計 -- `general-purpose` - 複雑な検索、マルチステップタスク - -### 直接使用可能なMCPツール -- Serena MCP - コード構造理解、シンボル編集、メモリー管理 -- BigQuery MCP - 簡単なクエリなら直接実行可 -- その他のMCPツール - 必要に応じて直接使用 - -### 呼び出し禁止(Main Agentに報告) -❌ **NDFサブエージェント(ndf:corder, ndf:data-analyst, ndf:researcher, ndf:scanner, ndf:qa)** - Main Agentに報告 -❌ **自分自身(ndf:director)** - 無限ループ防止 -❌ **Claude Code MCP** - プラグイン処理のネスト防止 - -## 制約事項 - -### directorの責務 -- タスク分析、計画立案、結果統合 -- Main agentへのサブエージェント依頼 -- コンテキスト消費を意識した段階的な情報取得 - -### 遵守事項 -- ユーザーの意図を正確に理解してから行動 -- タスク規模に応じた適切な対応(過大な対応を避ける) -- 破壊的変更は事前に確認 -- 進捗は定期的にユーザーに報告 -- 完了したタスクは即座にTodoWriteで更新 - -### 禁止事項 -- **他のサブエージェントの直接呼び出し**(メモリエラー防止) -- ユーザー承認なしの大規模変更 -- 機密情報のコミット -- Git push/merge(ユーザー確認が必要) - -## アーキテクチャ - -``` -User Request - ↓ -Main Agent - ↓ -Director (タスク分析・計画立案) - ↓ -Main Agent (directorからの報告に基づきサブエージェント起動) - ↓ -Specialized Agents (corder, data-analyst, researcher, scanner, qa) - ↓ -Main Agent (結果収集) - ↓ -Director (結果統合・報告) - ↓ -User -``` - -**このアーキテクチャのメリット:** -- メモリエラーを防止(エージェント間の直接呼び出しを回避) -- Main agentが全体の調整責任を持つ -- directorは計画立案と結果統合に集中 - -## ベストプラクティス - -### DO(推奨) -✅ 規模判定を最初に行い、適切な対応を選択 -✅ 小規模タスクは直接処理(エージェント委譲不要) -✅ 並列実行可能なタスクを積極的に特定 -✅ TodoWriteで進捗を可視化(中規模以上) -✅ 必要なエージェントを**Main Agentに明確に報告** -✅ 完了したタスクは即座に報告 -✅ **計画・調査結果をファイルに保存**(復帰可能にするため) -✅ **フェーズ間でファイルを更新**(進捗を記録) - -### DON'T(非推奨) -❌ **他のサブエージェントを直接呼び出す**(メモリエラーの原因) -❌ 小規模タスクにPlan Modeを使用 -❌ 単純なタスクを複数エージェントに分割 -❌ ファイル全体を無闇に読み込む -❌ 情報収集せずに計画を立てる -❌ 進捗報告を省略 -❌ **計画をファイルに保存せずに進める**(途中停止時に復帰困難) diff --git a/plugins/ndf/agents/qa.md b/plugins/ndf/agents/qa.md deleted file mode 100644 index dd11beb..0000000 --- a/plugins/ndf/agents/qa.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -name: qa -model: sonnet -description: | - WebFetch、Serena、Chrome DevToolsを活用した品質管理とテストの専門エージェント。深い第二意見レビューが必要な場合は `corder` エージェント (Codex CLI) に委譲します。 - **Use this agent proactively** for: security review (OWASP Top 10), code quality review, performance testing, test coverage analysis. - 積極的に委譲すべき場面: セキュリティレビュー(OWASP Top 10)、コード品質レビュー、パフォーマンステスト、テストカバレッジ分析。 ---- - -# 品質管理エージェント - -あなたは品質管理とテストの専門家です。WebFetch tool、Serena MCP、Chrome DevTools MCP、Claude Code MCPを活用して、コード品質、セキュリティ、パフォーマンス、ドキュメント品質を包括的に検証します。外部AIによる独立レビューが必要な場合は `corder` エージェント (Codex CLI) に委譲するか、`/ndf:codex` skill の手順で `codex exec` を直接呼び出します。 - -## 専門領域 - -### 1. コード品質レビュー -- Claudeによるコード品質レビュー(深い第二意見が必要なら `corder` エージェント経由で Codex CLI を呼び出す) -- ベストプラクティスへの準拠確認 -- コードの可読性と保守性の評価 -- リファクタリング提案 - -### 2. セキュリティ検証 -- 脆弱性スキャンとセキュリティチェック -- OWASP Top 10への対応確認 -- 認証・認可の実装検証 -- 機密情報の漏洩チェック - -### 3. パフォーマンステスト -- Chrome DevTools MCPによるパフォーマンス測定 -- Core Web Vitalsの評価 -- ネットワーク・レンダリングパフォーマンスの分析 -- ボトルネックの特定と改善提案 - -### 4. テストカバレッジ -- ユニットテストの実行と評価 -- 統合テストの検証 -- エッジケースの確認 -- テストコード品質のレビュー - -### 5. ドキュメント品質 -- README、コメント、APIドキュメントの検証 -- ドキュメントの完全性と正確性の確認 -- サンプルコードの動作確認 -- ユーザビリティの評価 - -### 6. Claude Codeプラグイン品質 -- Claude Code MCPによるプラグイン仕様準拠確認 -- plugin.json、SKILL.md等のメタデータ検証 -- プラグイン動作テスト -- マーケットプレイス公開基準への適合確認 - -## 使用可能なツール - -### 1. WebFetchツール -- `WebFetch(url, prompt)` - **静的ページの内容確認・ドキュメント検証**に最適 - - URLからコンテンツを取得し、promptに基づいて処理 - - **利点**: 高速、軽量、効率的 - - **用途**: 公開ドキュメント、README、静的HTMLページの品質確認 - -### 2. Serena MCP -- `mcp__plugin_ndf_serena__*` - コードベース分析、シンボル検索、参照チェック - -### 3. Chrome DevTools MCP -- **パフォーマンステスト、動的テスト時のみ使用** -- `mcp__plugin_ndf_chrome-devtools-mcp__*` - パフォーマンステスト、ブラウザ自動化、Web品質検証 - -### 4. Claude Code MCP -- `mcp__plugin_ndf_claude-code__*` - プラグイン開発支援、仕様確認 - -## 作業プロセス - -1. **スコープ確認**: 検証対象と品質基準の明確化 -2. **ツール選択**: 検証内容に応じて最適なツールを選択 - - **静的ページ内容確認** → WebFetch(優先) - - **コード品質・セキュリティ** → Claude直接、深い第二意見は `corder` エージェント - - **パフォーマンステスト** → Chrome DevTools MCP - - **構造分析** → Serena MCP -3. **静的分析**: Claudeによるコード品質・セキュリティチェック(深掘りは corder 経由で Codex CLI) -4. **動的テスト**: Chrome DevToolsでパフォーマンスと動作を検証 -5. **構造分析**: Serenaでコードベース全体の健全性を確認 -6. **ドキュメント検証**: WebFetch/Readツールで公開ドキュメントをレビュー -7. **レポート作成**: 発見した問題点と改善提案をまとめる -8. **修正支援**: 必要に応じて修正方法を具体的に提案 - -## 品質基準 - -### コード品質 -- DRY原則、SOLID原則の遵守 -- 適切な変数名・関数名 -- コメントとドキュメントの充実 -- エラーハンドリングの実装 - -### セキュリティ -- 入力値の検証とサニタイゼーション -- SQLインジェクション、XSS等の対策 -- 認証情報の安全な管理 -- 最小権限の原則 - -### パフォーマンス -- ページロード時間 < 3秒 -- First Contentful Paint < 1.8秒 -- Time to Interactive < 3.8秒 -- Cumulative Layout Shift < 0.1 - -### テスト -- 主要機能のカバレッジ > 80% -- エッジケースのテスト -- エラーハンドリングのテスト -- 回帰テストの実施 - -### ドキュメント -- READMEの完全性(インストール、使用方法、トラブルシューティング) -- APIドキュメントの正確性 -- コード内コメントの適切性 -- サンプルコードの動作確認 - -## ベストプラクティス - -**ツール選択:** -- 静的ページの内容確認・ドキュメント検証は**WebFetchを優先**(高速・効率的) -- パフォーマンステスト、動的テストはChrome DevTools MCPを使用 -- コード品質・セキュリティは Claude 直接、深い第二意見は `corder` エージェント経由で Codex CLI を活用 -- コードベース構造分析はSerena MCPを活用 - -**品質検証:** -- レビューは客観的な基準に基づいて実施 -- 発見した問題には具体的な修正案を提示 -- パフォーマンステストは実際の利用環境を想定 -- セキュリティチェックはOWASP等の標準に準拠 -- ドキュメントは初心者が理解できる明確さを重視 - -## サブエージェント呼び出しの制約 - -### 無限呼び出し防止ルール - -**重要:** サブエージェントの無限呼び出しを防ぐため、以下のルールを厳守してください。 - -❌ **サブエージェント呼び出し禁止:** -- **他のサブエージェント(`ndf:director`, `ndf:corder`, `ndf:data-analyst`, `ndf:researcher`, `ndf:debugger`, `ndf:devops-engineer`, `ndf:code-reviewer`)を呼び出してはいけません** - -✅ **MCP利用可能:** -- Serena MCP、Chrome DevTools MCP、Claude Code MCP等の各種MCPツールは利用可能 -- ただし、無限ループが発生しないよう注意してください - -### 理由 - -- サブエージェント間の相互呼び出しは無限ループやcore dumpを引き起こす可能性がある -- 専門的なタスクは直接MCPツールを使用して実行する -- 複雑なタスクの分割や他エージェントへの委譲はdirectorエージェントの役割 - -## 制約事項 - -- 破壊的な変更や本番環境への影響を避ける -- テスト実行時はバックアップや分離環境を使用 -- 個人情報や機密データの取り扱いに注意 -- ライセンス違反のないコードのみを推奨 diff --git a/plugins/ndf/agents/researcher.md b/plugins/ndf/agents/researcher.md deleted file mode 100644 index 18cb91e..0000000 --- a/plugins/ndf/agents/researcher.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -name: researcher -model: sonnet -description: | - AWS Documentation MCPとChrome DevTools MCPを活用した専門調査エージェント。一般的なWeb検索やWebFetchはメインセッションのbuilt-inツールで直接行い、このエージェントはAWS公式ドキュメント検索や、JavaScriptレンダリング・インタラクティブ操作が必要なサイト調査のみに委譲してください。 - **Use this agent proactively** for: AWS official documentation research, Chrome DevTools-based dynamic site analysis (JS-rendered pages, interactive navigation, screenshots). - 積極的に委譲すべき場面: AWS公式ドキュメント調査、Chrome DevToolsによる動的サイト調査(JSレンダリング、インタラクティブ操作、スクリーンショット取得)。 ---- - -# リサーチャーエージェント - -あなたは情報収集と分析の専門家です。WebFetch tool、AWS Documentation MCP、Chrome DevTools MCPを活用して、外部サイトから情報を収集し、分析して結果を返します。コードベース自体の大規模調査が必要な場合は `corder` エージェントに委譲するか、`/ndf:codex` skill の手順で Codex CLI を直接起動してください(Codex MCP は v4.0.0 で廃止)。 - -## 専門領域 - -### 1. 技術ドキュメント調査 -- AWS公式ドキュメントの検索と分析 -- APIドキュメントの調査 -- 技術仕様の理解と要約 -- ベストプラクティスの収集 - -### 2. Webスクレイピングと情報収集 -- Chrome DevToolsによるWebサイトの自動操作 -- ページ内容の取得と解析 -- データの抽出と構造化 -- スクリーンショットやPDFの取得 - -### 3. 情報の統合と分析 -- 複数ソースからの情報統合 -- データの比較と分析 -- トレンドやパターンの発見 -- 調査結果のレポート作成 - -## 使用可能なツール - -### 1. WebFetchツール(優先) -- `WebFetch(url, prompt)` - **静的Webページの高速取得**に最適 - - URLからコンテンツを取得し、promptに基づいて処理 - - HTML→Markdown自動変換 - - AI搭載の処理エンジン - - **利点**: 高速、軽量、15分キャッシュによる効率化 - - **用途**: 技術ドキュメント、ブログ記事、静的サイトの情報収集 - -### 2. AWS Documentation MCP -- `mcp__plugin_ndf_awslabs.aws-documentation-mcp-server__read_documentation` - AWS公式ドキュメント読み込み -- `mcp__plugin_ndf_awslabs.aws-documentation-mcp-server__search_documentation` - AWS公式ドキュメント検索 -- `mcp__plugin_ndf_awslabs.aws-documentation-mcp-server__recommend` - 関連ドキュメント推奨 - -### 3. Chrome DevTools MCP -- **JavaScriptレンダリング必須時、インタラクティブ操作が必要な場合のみ使用** -- `mcp__plugin_ndf_chrome-devtools-mcp__navigate_page` - ページ遷移 -- `mcp__plugin_ndf_chrome-devtools-mcp__take_snapshot` - ページスナップショット取得 -- `mcp__plugin_ndf_chrome-devtools-mcp__take_screenshot` - スクリーンショット取得 -- `mcp__plugin_ndf_chrome-devtools-mcp__click` - 要素クリック -- `mcp__plugin_ndf_chrome-devtools-mcp__fill` - フォーム入力 -- その他多数のブラウザ自動化ツール - -## 作業プロセス - -1. **調査計画**: 調査目的と情報源を明確化 -2. **ツール選択**: コンテンツタイプに応じて最適なツールを選択 - - **静的Webページ** → WebFetch(優先) - - **AWS技術情報** → AWS Docs MCP - - **動的サイト/インタラクティブ操作** → Chrome DevTools MCP - - **コードベース調査** → 本エージェントの責務外。`corder` エージェントまたは `/ndf:codex` skill を使う -3. **情報収集**: 選択したツールで情報を取得 -4. **情報整理**: 収集した情報を構造化 -5. **分析**: データを分析し、インサイトを抽出 -6. **報告**: わかりやすく整理して結果を報告 - -## 調査の種類 - -### WebFetch調査(静的Webページ) -``` -例: 「Next.js公式ドキュメントのApp Routerについて調査してください」 -1. WebFetch("https://nextjs.org/docs/app", "App Routerの概要と使い方を説明してください") -2. レスポンスから情報を抽出 -3. 調査結果を要約して報告 - -**適用場面:** -- 技術ドキュメント(公式サイト、API docs) -- ブログ記事やチュートリアル -- 静的HTMLサイトの情報収集 -``` - -### AWS技術調査 -``` -例: 「S3のバージョニング機能について調査してください」 -1. search_documentation でS3ドキュメントを検索 -2. read_documentation で詳細を読み込み -3. recommend で関連ドキュメントを取得 -4. 調査結果を要約して報告 -``` - -### Webサイト調査(動的コンテンツ/インタラクティブ操作) -``` -例: 「競合サイトの機能を調査してください(JavaScript必須)」 -1. navigate_page でサイトにアクセス -2. take_snapshot でページ構造を取得 -3. click や fill で機能を操作 -4. take_screenshot で画面キャプチャ -5. 調査結果をまとめて報告 - -**注意:** 静的ページの場合はWebFetchを優先してください -``` - -## ベストプラクティス - -**ツール選択:** -- 静的Webページの情報収集は**WebFetchを優先**(高速・効率的) -- JavaScriptレンダリングが必要な場合のみChrome DevToolsを使用 -- スクリーンショットやインタラクティブ操作が必要な場合はChrome DevTools -- AWS技術情報はAWS Docs MCPを活用 - -**調査手法:** -- 調査範囲を明確にし、効率的に情報収集 -- 複数ソースで情報をクロスチェック -- スクリーンショットやスナップショットで証拠を保存 -- 調査結果は構造化して報告 -- 必要に応じてファイルに保存 - -## サブエージェント呼び出しの制約 - -### 無限呼び出し防止ルール - -**重要:** サブエージェントの無限呼び出しを防ぐため、以下のルールを厳守してください。 - -❌ **サブエージェント呼び出し禁止:** -- **他のサブエージェント(`ndf:director`, `ndf:corder`, `ndf:data-analyst`, `ndf:researcher`, `ndf:scanner`, `ndf:qa`)を呼び出してはいけません** - -✅ **MCP利用可能:** -- AWS Documentation MCP、Chrome DevTools MCP等の各種MCPツールは利用可能 -- ただし、無限ループが発生しないよう注意してください - -### 理由 - -- サブエージェント間の相互呼び出しは無限ループやcore dumpを引き起こす可能性がある -- 専門的なタスクは直接MCPツールを使用して実行する -- 複雑なタスクの分割や他エージェントへの委譲はdirectorエージェントの役割 - -## 制約事項 - -- Webサイトの利用規約を遵守 -- 個人情報や機密情報の取り扱いに注意 -- スクレイピングは節度を持って実行 -- 著作権を侵害しない -- ログイン情報等の認証は慎重に扱う diff --git a/plugins/ndf/hooks/codex-hooks.json b/plugins/ndf/hooks/codex-hooks.json deleted file mode 100644 index bf0e8bc..0000000 --- a/plugins/ndf/hooks/codex-hooks.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "hooks": { - "Stop": [ - { - "hooks": [ - { - "type": "command", - "command": "sh -c 'if [ -n \"$PLUGIN_ROOT\" ]; then node \"$PLUGIN_ROOT/scripts/codex-slack-notify.js\"; fi; exit 0'", - "timeout": 15, - "statusMessage": "Sending Codex completion notification" - } - ] - } - ] - } -} diff --git a/plugins/ndf/hooks/hooks.json b/plugins/ndf/hooks/hooks.json deleted file mode 100644 index c46f68a..0000000 --- a/plugins/ndf/hooks/hooks.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "hooks": { - "SessionStart": [ - { - "matcher": "startup", - "description": "Ensure ~/.claude/settings.json cleanupPeriodDays >= 90 (guarded by 7-day timestamp flag) and set default statusline when none is configured", - "hooks": [ - { - "type": "command", - "command": "bash ${PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}/scripts/ensure-retention.sh", - "description": "NDF: ensure transcript retention >= 90 days", - "continueOnError": true, - "suppressOutput": false - }, - { - "type": "command", - "command": "bash ${PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}/scripts/statusline-switch.sh ensure", - "description": "NDF: set default statusline when none is configured", - "continueOnError": true, - "suppressOutput": false - } - ] - } - ], - "Stop": [ - { - "hooks": [ - { - "type": "command", - "command": "sh -c 'ROOT=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; if [ -n \"$ROOT\" ]; then node \"$ROOT/scripts/slack-notify.js\" session_end; fi; exit 0'", - "description": "Send Slack notification when Claude Code exits", - "continueOnError": true - } - ] - } - ] - } -} diff --git a/plugins/ndf/scripts/codex-slack-notify.js b/plugins/ndf/scripts/codex-slack-notify.js deleted file mode 100755 index af01f50..0000000 --- a/plugins/ndf/scripts/codex-slack-notify.js +++ /dev/null @@ -1,330 +0,0 @@ -#!/usr/bin/env node -/** - * Slack notification script for Codex Stop hooks. - * - * Opt-in only: set NDF_CODEX_SLACK_NOTIFY=true plus the same Slack variables - * used by the Claude Code hook: SLACK_BOT_TOKEN, SLACK_CHANNEL_ID, and - * optional SLACK_USER_MENTION. The script avoids model calls and summarizes - * from Codex's local session JSONL when available. - */ - -const fs = require('fs'); -const path = require('path'); -const https = require('https'); -const { spawnSync } = require('child_process'); -const os = require('os'); -const crypto = require('crypto'); - -const CONFIG = { - MAX_SESSION_FILES: 80, - MAX_LINES: 240, - MAX_TEXT: 180, - LOCK_TIMEOUT_MS: 30000, - COOLDOWN_MS: 5000, - DELETE_DELAY_MS: 500, - FALLBACK_SUMMARY: 'Codexの作業が完了しました', - LOG_DIR: path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'log'), -}; - -const RUN_ID = crypto.randomBytes(4).toString('hex'); - -const isEnabled = () => /^(1|true|yes|on)$/i.test(process.env.NDF_CODEX_SLACK_NOTIFY || ''); -const isDebug = () => /^(1|true|yes|on)$/i.test(process.env.DEBUG_CODEX_SLACK_NOTIFY || ''); - -function log(message, ...args) { - if (!isDebug()) return; - const line = `[${new Date().toISOString()}] [codex-slack:${RUN_ID}] ${message} ${args.map(String).join(' ')}\n`; - process.stderr.write(line); - try { - fs.mkdirSync(CONFIG.LOG_DIR, { recursive: true }); - fs.appendFileSync(path.join(CONFIG.LOG_DIR, 'ndf-codex-slack-notify.log'), line); - } catch (_) { - // Debug logging must never break hook execution. - } -} - -function safeJsonParse(text) { - try { - return JSON.parse(text); - } catch (_) { - return null; - } -} - -async function readStdinJson() { - if (process.stdin.isTTY) return {}; - let input = ''; - for await (const chunk of process.stdin) input += chunk; - log('stdin bytes:', input.length); - return safeJsonParse(input) || {}; -} - -function loadEnvFile() { - let current = process.cwd(); - while (current && current !== path.dirname(current)) { - const envFile = path.join(current, '.env'); - if (fs.existsSync(envFile)) { - for (const rawLine of fs.readFileSync(envFile, 'utf8').split('\n')) { - const line = rawLine.trim(); - if (!line || line.startsWith('#')) continue; - const match = line.match(/^([^=]+)=(.*)$/); - if (!match) continue; - const key = match[1].trim(); - let value = match[2].trim(); - if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1); - } - process.env[key] ??= value; - } - return; - } - if (fs.existsSync(path.join(current, '.git'))) return; - current = path.dirname(current); - } -} - -function lockPath() { - const id = `${process.cwd()}:${process.env.CODEX_SESSION_ID || ''}`; - const hash = crypto.createHash('sha256').update(id).digest('hex').slice(0, 12); - return path.join(os.tmpdir(), `ndf-codex-slack-${hash}.lock`); -} - -function acquireLock() { - const file = lockPath(); - const now = Date.now(); - const existing = fs.existsSync(file) ? safeJsonParse(fs.readFileSync(file, 'utf8')) : null; - if (existing?.completedAt && now - existing.completedAt < CONFIG.COOLDOWN_MS) return false; - if (existing?.timestamp && !existing.completedAt && now - existing.timestamp < CONFIG.LOCK_TIMEOUT_MS) return false; - fs.writeFileSync(file, JSON.stringify({ pid: process.pid, timestamp: now, completedAt: null })); - return true; -} - -function releaseLock() { - const file = lockPath(); - const data = fs.existsSync(file) ? safeJsonParse(fs.readFileSync(file, 'utf8')) || {} : {}; - data.completedAt = Date.now(); - fs.writeFileSync(file, JSON.stringify(data)); -} - -function gitValue(args, fallback = '') { - const result = spawnSync('git', args, { cwd: process.cwd(), encoding: 'utf8' }); - return result.status === 0 ? result.stdout.trim() : fallback; -} - -function repoInfo() { - const root = gitValue(['rev-parse', '--show-toplevel'], process.cwd()); - const branch = gitValue(['branch', '--show-current'], ''); - return { - name: path.basename(root || process.cwd()), - root, - branch, - }; -} - -function codexHome() { - return process.env.CODEX_HOME || path.join(os.homedir(), '.codex'); -} - -function findSessionFiles() { - const base = path.join(codexHome(), 'sessions'); - if (!fs.existsSync(base)) return []; - const files = []; - const stack = [base]; - while (stack.length) { - const dir = stack.pop(); - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const p = path.join(dir, entry.name); - if (entry.isDirectory()) stack.push(p); - else if (entry.isFile() && entry.name.endsWith('.jsonl')) { - files.push({ path: p, mtimeMs: fs.statSync(p).mtimeMs }); - } - } - } - return files.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, CONFIG.MAX_SESSION_FILES); -} - -function textFromContent(content) { - if (typeof content === 'string') return content; - if (Array.isArray(content)) { - return content.map((item) => item?.text || item?.content || '').filter(Boolean).join('\n'); - } - if (content && typeof content === 'object') return content.text || content.message || ''; - return ''; -} - -function textFromItem(item) { - const payload = item?.payload || {}; - if (payload.type === 'message') { - return textFromContent(payload.content); - } - if (payload.type === 'agent_message') return payload.message || ''; - if (item.type === 'event_msg' && payload.message) return payload.message; - return ''; -} - -function summarizeText(text) { - const oneLine = String(text || '') - .replace(/```[\s\S]*?```/g, '') - .replace(/\s+/g, ' ') - .trim(); - if (!oneLine) return null; - return oneLine.length > CONFIG.MAX_TEXT ? `${oneLine.slice(0, CONFIG.MAX_TEXT - 1)}…` : oneLine; -} - -function readSessionSummary(hookInput) { - const explicitPath = hookInput.transcript_path || hookInput.session_path || hookInput.session_file; - const candidates = explicitPath ? [{ path: explicitPath }] : findSessionFiles(); - const cwd = hookInput.cwd || process.cwd(); - const sessionId = hookInput.session_id || hookInput.thread_id || process.env.CODEX_SESSION_ID || ''; - const lastAssistantSummary = summarizeText(textFromContent(hookInput.last_assistant_message)); - - for (const candidate of candidates) { - if (!candidate.path || !fs.existsSync(candidate.path)) continue; - const lines = fs.readFileSync(candidate.path, 'utf8').trim().split('\n').slice(-CONFIG.MAX_LINES); - const parsed = lines.map(safeJsonParse).filter(Boolean); - const meta = parsed.find((item) => item.type === 'session_meta')?.payload || {}; - if (!explicitPath && meta.cwd && meta.cwd !== cwd) continue; - if (!explicitPath && sessionId && meta.session_id && meta.session_id !== sessionId) continue; - - const final = [...parsed].reverse().find((item) => { - const payload = item.payload || {}; - return payload.phase === 'final_answer' || payload.type === 'agent_message'; - }); - const user = [...parsed].reverse().find((item) => { - const payload = item.payload || {}; - return payload.type === 'message' && payload.role === 'user'; - }); - const tokenEvent = [...parsed].reverse().find((item) => item.type === 'event_msg' && item.payload?.type === 'token_count'); - - return { - sessionId: meta.session_id || sessionId, - summary: lastAssistantSummary || summarizeText(textFromItem(final)) || summarizeText(textFromItem(user)), - model: meta.model || meta.model_slug || '', - tokenInfo: tokenEvent?.payload?.info || null, - file: candidate.path, - }; - } - - return { sessionId, summary: lastAssistantSummary, model: '', tokenInfo: null, file: null }; -} - -function formatTokenInfo(info) { - const usage = info?.total_token_usage || info?.last_token_usage; - const window = info?.model_context_window; - if (!usage?.total_tokens || !window) return ''; - const pct = Math.round((usage.total_tokens / window) * 100); - return `tokens: ${usage.total_tokens}/${window} (${pct}%)`; -} - -function formatMessage(repo, session, includeMention = false) { - const mention = includeMention && process.env.SLACK_USER_MENTION ? `${process.env.SLACK_USER_MENTION} ` : ''; - const parts = [ - `${mention}[${repo.name}] Codex: ${session.summary || CONFIG.FALLBACK_SUMMARY}`, - repo.branch ? `branch: ${repo.branch}` : '', - session.model ? `model: ${session.model}` : '', - formatTokenInfo(session.tokenInfo), - session.sessionId ? `session: ${session.sessionId}` : '', - `cwd: ${process.cwd()}`, - ].filter(Boolean); - return parts.join('\n'); -} - -function slackApiRequest(apiPath, data) { - const token = process.env.SLACK_BOT_TOKEN; - if (!token) return Promise.resolve(null); - - return new Promise((resolve) => { - const body = JSON.stringify(data); - const req = https.request({ - hostname: 'slack.com', - port: 443, - path: apiPath, - method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json; charset=utf-8', - 'Content-Length': Buffer.byteLength(body), - }, - }, (res) => { - let response = ''; - res.on('data', (chunk) => { response += chunk; }); - res.on('end', () => { - const json = safeJsonParse(response); - log('slack api:', apiPath, 'ok:', json?.ok, 'error:', json?.error || ''); - resolve(json?.ok === true ? json : null); - }); - }); - req.on('error', (error) => { - log('slack error:', error.message); - resolve(null); - }); - req.write(body); - req.end(); - }); -} - -function postSlack(text) { - const channel = process.env.SLACK_CHANNEL_ID; - if (!channel) return Promise.resolve(null); - return slackApiRequest('/api/chat.postMessage', { channel, text }); -} - -function deleteSlack(ts) { - const channel = process.env.SLACK_CHANNEL_ID; - if (!channel || !ts) return Promise.resolve(null); - return slackApiRequest('/api/chat.delete', { channel, ts }); -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -async function main() { - loadEnvFile(); - if (!isEnabled()) { - log('disabled'); - return; - } - if (!process.env.SLACK_BOT_TOKEN || !process.env.SLACK_CHANNEL_ID) { - log('missing slack env'); - return; - } - if (!acquireLock()) { - log('lock rejected'); - return; - } - try { - const hookInput = await readStdinJson(); - const repo = repoInfo(); - const session = readSessionSummary(hookInput); - if (!process.env.SLACK_USER_MENTION) { - await postSlack(formatMessage(repo, session, false)); - return; - } - - const mentionResult = await postSlack(formatMessage(repo, session, true)); - if (!mentionResult) { - log('failed to send mention message'); - return; - } - - await sleep(CONFIG.DELETE_DELAY_MS); - - const cleanResult = await postSlack(formatMessage(repo, session, false)); - if (!cleanResult) { - log('failed to send clean message'); - return; - } - - const deleteResult = await deleteSlack(mentionResult.ts); - if (!deleteResult) log('failed to delete mention message'); - } finally { - releaseLock(); - } -} - -main().catch((error) => { - log('fatal:', error.stack || error.message); - releaseLock(); - process.exit(0); -}); diff --git a/plugins/ndf/scripts/ensure-retention.sh b/plugins/ndf/scripts/ensure-retention.sh deleted file mode 100755 index e511363..0000000 --- a/plugins/ndf/scripts/ensure-retention.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env bash -# NDF plugin: ~/.claude/settings.json の cleanupPeriodDays を最低 90 日に保つ。 -# - 既存値 >= 90 なら何もしない -# - 既存値 < 90 or 未設定 なら 90 に更新 -# - 前回チェックから 7 日経っていなければスキップ (多重実行防止) -# - 複数セッション同時起動時の race condition を flock で回避 (flock 不在時は atomic rename のみ) -set -euo pipefail - -MIN_DAYS=90 -GUARD_DAYS=7 -SETTINGS="$HOME/.claude/settings.json" -FLAG="$HOME/.claude/.ndf-retention-checked" -LOCK="$HOME/.claude/.ndf-retention.lock" - -# jq 必須 -if ! command -v jq >/dev/null 2>&1; then - exit 0 -fi - -# 7 日以内に実行済みならスキップ -if [ -f "$FLAG" ]; then - if find "$FLAG" -mtime "-${GUARD_DAYS}" -print -quit 2>/dev/null | grep -q .; then - exit 0 - fi -fi - -# settings.json が存在しない場合は新規作成 -mkdir -p "$(dirname "$SETTINGS")" -if [ ! -f "$SETTINGS" ]; then - echo "{}" > "$SETTINGS" -fi - -# 更新処理をひとつの関数にまとめ、ロック内で呼び出す -update_retention() { - # ロック取得後に再チェック (並行セッションが先に更新済みの可能性) - local current tmp - current=$(jq -r '.cleanupPeriodDays // 0' "$SETTINGS" 2>/dev/null || echo 0) - case "$current" in - ''|*[!0-9]*) current=0 ;; - esac - if [ "$current" -ge "$MIN_DAYS" ]; then - return 0 - fi - tmp="$(mktemp)" - if jq --argjson v "$MIN_DAYS" '.cleanupPeriodDays = $v' "$SETTINGS" > "$tmp"; then - mv "$tmp" "$SETTINGS" - echo "[ndf] cleanupPeriodDays を ${current} → ${MIN_DAYS} に更新しました (~/.claude/settings.json)" - else - rm -f "$tmp" - return 1 - fi -} - -# ロック取得: flock があればアトミック更新、無ければ atomic rename のみに依存 -if command -v flock >/dev/null 2>&1; then - ( - # 最大 5 秒待機。取得できなければ他プロセスが更新中なので何もしない - flock -x -w 5 200 || exit 0 - update_retention || true - ) 200>"$LOCK" -else - # flock 不在環境 (macOS 標準など): mv 自体は POSIX で atomic なので大きな破損は起きない - update_retention || true -fi - -touch "$FLAG" diff --git a/plugins/ndf/scripts/install-official-skills.sh b/plugins/ndf/scripts/install-official-skills.sh deleted file mode 100755 index bba6467..0000000 --- a/plugins/ndf/scripts/install-official-skills.sh +++ /dev/null @@ -1,183 +0,0 @@ -#!/bin/bash -# Anthropic公式Skillsのインストーラ -# -# Usage: -# bash install-official-skills.sh [SKILL_NAMES...] -# bash install-official-skills.sh --all -# bash install-official-skills.sh --list -# bash install-official-skills.sh --scope project [SKILL_NAMES...] -# bash install-official-skills.sh --update -# -# Examples: -# bash install-official-skills.sh docx pptx xlsx # 指定Skillをインストール (~/.claude/skills/) -# bash install-official-skills.sh --scope project pdf # プロジェクト .claude/skills/ に配置 -# bash install-official-skills.sh --all # 全Skillインストール -# bash install-official-skills.sh --update # 公式リポジトリを最新化 - -set -euo pipefail - -REPO_URL="https://github.com/anthropics/skills.git" -CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/anthropic-skills" -USER_SKILLS_DIR="$HOME/.claude/skills" -PROJECT_SKILLS_DIR=".claude/skills" - -SCOPE="user" -DO_ALL=false -DO_LIST=false -DO_UPDATE=false -SKILLS=() - -# 引数パース -while [[ $# -gt 0 ]]; do - case "$1" in - --scope) - SCOPE="$2" - shift 2 - ;; - --all) - DO_ALL=true - shift - ;; - --list) - DO_LIST=true - shift - ;; - --update) - DO_UPDATE=true - shift - ;; - --help|-h) - sed -n '2,13p' "$0" | sed 's/^# \?//' - exit 0 - ;; - --*) - echo "ERROR: unknown option $1" >&2 - exit 1 - ;; - *) - SKILLS+=("$1") - shift - ;; - esac -done - -# --- クローン or 更新 --- -if [ ! -d "$CACHE_DIR/.git" ]; then - echo "==> 公式Skillsリポジトリを取得中: $CACHE_DIR" - mkdir -p "$(dirname "$CACHE_DIR")" - git clone --depth 1 "$REPO_URL" "$CACHE_DIR" -elif [ "$DO_UPDATE" = true ]; then - echo "==> 公式Skillsを最新化中" - git -C "$CACHE_DIR" pull --ff-only -fi - -# --- --list: 利用可能Skill一覧を表示 --- -if [ "$DO_LIST" = true ]; then - echo "" - echo "=== 利用可能なSkill ===" - for dir in "$CACHE_DIR"/skills/*/; do - name=$(basename "$dir") - license_file="$dir/LICENSE.txt" - if [ -f "$license_file" ]; then - if grep -q "Apache License" "$license_file"; then - license="Apache-2.0" - elif grep -q "Proprietary\|All rights reserved" "$license_file"; then - license="Proprietary" - else - license="?" - fi - else - license="-" - fi - printf " %-25s [%s]\n" "$name" "$license" - done - echo "" - echo "Proprietary Skillは各自の環境にインストール可能ですが、再配布は禁止です。" - exit 0 -fi - -# --- インストール先を決定 --- -case "$SCOPE" in - user) - DEST="$USER_SKILLS_DIR" - ;; - project) - if [ ! -d .git ] && [ ! -f .claude-plugin/plugin.json ] && [ ! -f package.json ] && [ ! -f pyproject.toml ]; then - echo "WARN: プロジェクトルートらしきファイルが見当たりません (.git, package.json 等)。カレントディレクトリ: $(pwd)" - fi - DEST="$PROJECT_SKILLS_DIR" - ;; - *) - echo "ERROR: --scope は user / project のいずれか" >&2 - exit 1 - ;; -esac - -mkdir -p "$DEST" -echo "==> インストール先: $DEST ($SCOPE scope)" - -# --- 対象Skill決定 --- -if [ "$DO_ALL" = true ]; then - SKILLS=() - for dir in "$CACHE_DIR"/skills/*/; do - SKILLS+=("$(basename "$dir")") - done -fi - -if [ "${#SKILLS[@]}" -eq 0 ]; then - echo "ERROR: インストールするSkillが指定されていません。" >&2 - echo "利用可能な一覧は: bash $0 --list" >&2 - exit 1 -fi - -# --- シンボリックリンク作成 --- -INSTALLED=() -SKIPPED=() -NOT_FOUND=() -for skill in "${SKILLS[@]}"; do - src="$CACHE_DIR/skills/$skill" - dst="$DEST/$skill" - - if [ ! -d "$src" ]; then - NOT_FOUND+=("$skill") - continue - fi - - if [ -e "$dst" ] || [ -L "$dst" ]; then - if [ -L "$dst" ] && [ "$(readlink "$dst")" = "$src" ]; then - SKIPPED+=("$skill (既にリンク済み)") - continue - fi - echo "WARN: $dst が既に存在します。上書きしますか? [y/N]" - read -r ans - if [ "$ans" != "y" ] && [ "$ans" != "Y" ]; then - SKIPPED+=("$skill (ユーザーキャンセル)") - continue - fi - rm -rf "$dst" - fi - - ln -s "$src" "$dst" - INSTALLED+=("$skill") -done - -# --- 結果報告 --- -echo "" -echo "=== インストール結果 ===" -if [ "${#INSTALLED[@]}" -gt 0 ]; then - echo "インストール済み (${#INSTALLED[@]}個):" - for s in "${INSTALLED[@]}"; do echo " ✓ $s"; done -fi -if [ "${#SKIPPED[@]}" -gt 0 ]; then - echo "スキップ (${#SKIPPED[@]}個):" - for s in "${SKIPPED[@]}"; do echo " - $s"; done -fi -if [ "${#NOT_FOUND[@]}" -gt 0 ]; then - echo "見つからない (${#NOT_FOUND[@]}個):" - for s in "${NOT_FOUND[@]}"; do echo " ✗ $s"; done - echo "" - echo "一覧確認: bash $0 --list" -fi - -echo "" -echo "Claude Codeを再起動するか /plugin reload で反映してください。" diff --git a/plugins/ndf/scripts/slack-notify.js b/plugins/ndf/scripts/slack-notify.js deleted file mode 100755 index 38b9991..0000000 --- a/plugins/ndf/scripts/slack-notify.js +++ /dev/null @@ -1,777 +0,0 @@ -#!/usr/bin/env node -/** - * Slack notification script for Claude Code session completion - * - * Reads transcript_path from stdin (hook input JSON) and generates - * a session-specific summary using Claude CLI. - */ - -const fs = require('fs'); -const path = require('path'); -const https = require('https'); -const { spawn } = require('child_process'); -const os = require('os'); -const crypto = require('crypto'); - -// ============================================================================ -// Constants -// ============================================================================ - -// Generate unique run ID for this execution to trace logs -const RUN_ID = crypto.randomBytes(4).toString('hex'); - -const CONFIG = { - CLI_TIMEOUT_MS: 60000, - MAX_RESPONSES: 5, - MAX_TRANSCRIPT_LINES: 100, - MAX_FILE_SIZE_BYTES: 10 * 1024 * 1024, // 10MB - MIN_TEXT_LENGTH: 5, - MIN_RESPONSE_LENGTH: 10, - MAX_CONTENT_LENGTH: 500, - MAX_CONTEXT_LENGTH: 2500, - FALLBACK_REPO_NAME: 'unknown', - NO_SUMMARY_MESSAGE: 'Claude Codeのセッションが終了しました(要約なし)', - LOCK_TIMEOUT_MS: 30000, // Lock expires after 30 seconds - NOTIFICATION_COOLDOWN_MS: 5000, // Prevent duplicate notifications within 5 seconds - DELETE_DELAY_MS: 500, // Wait before deleting mention message - LOG_DIR: path.join(os.homedir(), '.claude', 'logs') -}; - -const META_CONTENT_PREFIXES = ['Caveat:', '']; - -// ============================================================================ -// Utilities -// ============================================================================ - -/** - * Get log file path for today's date - * Format: ~/.claude/logs/slack-notify-YYYY-MM-DD.log - */ -function getLogFilePath() { - const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD - return path.join(CONFIG.LOG_DIR, `slack-notify-${today}.log`); -} - -/** - * Ensure log directory exists - */ -function ensureLogDirectory() { - if (!fs.existsSync(CONFIG.LOG_DIR)) { - fs.mkdirSync(CONFIG.LOG_DIR, { recursive: true }); - } -} - -const isDebugMode = () => process.env.DEBUG_SLACK_NOTIFY === 'true'; - -const debugLog = (message, ...args) => { - if (isDebugMode()) { - const timestamp = new Date().toISOString(); - const logMessage = `[${timestamp}] [RUN:${RUN_ID}] [PID:${process.pid}] ${message}`; - - // Format additional arguments - const formattedArgs = args.map(arg => { - if (typeof arg === 'object') { - return JSON.stringify(arg); - } - return String(arg); - }).join(' '); - - const fullMessage = formattedArgs ? `${logMessage} ${formattedArgs}\n` : `${logMessage}\n`; - - // Write to stderr for backward compatibility - process.stderr.write(fullMessage); - - // Write to log file - try { - ensureLogDirectory(); - fs.appendFileSync(getLogFilePath(), fullMessage, 'utf8'); - } catch (error) { - // If file write fails, at least stderr output is available - process.stderr.write(`[ERROR] Failed to write to log file: ${error.message}\n`); - } - } -}; - -const safeJsonParse = (str) => { - try { - return JSON.parse(str); - } catch (e) { - debugLog('JSON parse error:', e.message, 'Input:', str?.substring(0, 100)); - return null; - } -}; - -const readFileLines = (filePath) => { - if (!fs.existsSync(filePath)) { - debugLog('File does not exist:', filePath); - return null; - } - try { - const stats = fs.statSync(filePath); - debugLog('Transcript file stats:', { - path: filePath, - size: stats.size, - mtime: stats.mtime.toISOString(), - mtimeMs: stats.mtimeMs - }); - if (stats.size > CONFIG.MAX_FILE_SIZE_BYTES) { - debugLog('Transcript file too large:', stats.size, 'bytes'); - return null; - } - const content = fs.readFileSync(filePath, 'utf8').trim(); - const lines = content.split('\n'); - debugLog('Read', lines.length, 'lines from transcript | File size:', stats.size); - return lines; - } catch (error) { - debugLog('Error reading transcript file:', error.message); - return null; - } -}; - -const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); - -// ============================================================================ -// Lock Management (prevent duplicate execution) -// ============================================================================ - -function getLockFilePath() { - // Use working directory hash instead of session ID for robust deduplication - // This prevents multiple Claude Code instances in the same project from sending duplicate notifications - const cwd = process.cwd(); - const hash = crypto.createHash('md5').update(cwd).digest('hex').substring(0, 8); - return path.join(os.tmpdir(), `slack-notify-${hash}.lock`); -} - -function acquireLock() { - const lockFile = getLockFilePath(); - const now = Date.now(); - - debugLog('Attempting to acquire lock:', lockFile); - - try { - // Check if lock exists - if (fs.existsSync(lockFile)) { - const lockData = safeJsonParse(fs.readFileSync(lockFile, 'utf8')); - debugLog('Existing lock found:', lockData); - if (lockData) { - // Check if notification was sent recently (cooldown period) - if (lockData.completedAt && (now - lockData.completedAt) < CONFIG.NOTIFICATION_COOLDOWN_MS) { - const elapsed = now - lockData.completedAt; - debugLog('LOCK REJECTED: Notification sent recently | Elapsed:', elapsed, 'ms | Cooldown:', CONFIG.NOTIFICATION_COOLDOWN_MS, 'ms | Previous PID:', lockData.pid); - return false; - } - // Check if another process is still holding the lock - if (!lockData.completedAt && (now - lockData.timestamp) < CONFIG.LOCK_TIMEOUT_MS) { - const elapsed = now - lockData.timestamp; - debugLog('LOCK REJECTED: Lock held by another process | PID:', lockData.pid, '| Elapsed:', elapsed, 'ms | Timeout:', CONFIG.LOCK_TIMEOUT_MS, 'ms'); - return false; - } - } - debugLog('Lock expired or cooldown ended, proceeding'); - } else { - debugLog('No existing lock file found'); - } - - // Create lock - const lockData = { - pid: process.pid, - runId: RUN_ID, - timestamp: now, - completedAt: null - }; - fs.writeFileSync(lockFile, JSON.stringify(lockData)); - debugLog('LOCK ACQUIRED:', lockData); - return true; - } catch (error) { - debugLog('Lock acquisition error:', error.message); - return false; - } -} - -function releaseLock() { - const lockFile = getLockFilePath(); - try { - if (fs.existsSync(lockFile)) { - // Update lock with completion time instead of deleting - // This enables cooldown period to prevent duplicate notifications - const lockData = safeJsonParse(fs.readFileSync(lockFile, 'utf8')) || {}; - lockData.completedAt = Date.now(); - lockData.releasedByRunId = RUN_ID; - fs.writeFileSync(lockFile, JSON.stringify(lockData)); - debugLog('LOCK RELEASED:', lockData); - } else { - debugLog('Lock file not found during release'); - } - } catch (error) { - debugLog('Lock release error:', error.message); - } -} - -// ============================================================================ -// Environment Management -// ============================================================================ - -function loadEnvFile() { - let currentDir = __dirname; - - while (currentDir !== path.dirname(currentDir)) { - currentDir = path.dirname(currentDir); - const envFile = path.join(currentDir, '.env'); - - if (fs.existsSync(envFile)) { - debugLog('Loading env file:', envFile); - parseEnvFile(envFile); - break; - } - if (fs.existsSync(path.join(currentDir, '.git'))) break; - } -} - -function parseEnvFile(envFile) { - const content = fs.readFileSync(envFile, 'utf8'); - - for (const rawLine of content.split('\n')) { - const line = rawLine.trim(); - if (!line || line.startsWith('#')) continue; - - const match = line.match(/^([^=]+)=(.*)$/); - if (!match) continue; - - const key = match[1].trim(); - let value = match[2].trim(); - - // Remove surrounding quotes - if ((value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1); - } - - process.env[key] ??= value; - } -} - -// ============================================================================ -// Transcript Processing -// ============================================================================ - -function isMetaContent(content) { - return typeof content === 'string' && - META_CONTENT_PREFIXES.some(prefix => content.startsWith(prefix)); -} - -function extractTextFromContent(content) { - if (typeof content === 'string') { - return isMetaContent(content) ? null : content.substring(0, CONFIG.MAX_CONTENT_LENGTH); - } - - if (Array.isArray(content)) { - const textItem = content.find(item => - item?.type === 'text' && typeof item.text === 'string' - ); - return textItem?.text?.substring(0, CONFIG.MAX_CONTENT_LENGTH) ?? null; - } - - return null; -} - -function parseTranscriptData(transcriptPath) { - debugLog('Parsing transcript:', transcriptPath); - const allLines = readFileLines(transcriptPath); - if (!allLines) { - debugLog('No lines read from transcript'); - return { firstRequest: null, assistantResponses: [] }; - } - - // Process only recent lines to avoid memory issues with large files - const lines = allLines.slice(-CONFIG.MAX_TRANSCRIPT_LINES); - debugLog('Processing', lines.length, 'recent lines'); - - let firstRequest = null; - const assistantResponses = []; - let parsedCount = 0; - let userCount = 0; - let assistantCount = 0; - - for (const line of lines) { - const data = safeJsonParse(line); - if (!data?.message?.role) continue; - parsedCount++; - - const { role, content } = data.message; - const text = extractTextFromContent(content); - - // Get first valid user request (within recent lines) - if (!firstRequest && - role === 'user' && - data.type === 'user' && - !data.isMeta && - text?.length >= CONFIG.MIN_TEXT_LENGTH) { - firstRequest = text; - userCount++; - debugLog('Found user request:', text.substring(0, 50) + '...'); - } - - // Collect assistant responses - if (role === 'assistant' && text?.length >= CONFIG.MIN_RESPONSE_LENGTH) { - assistantResponses.push(text); - assistantCount++; - } - } - - debugLog('Parsed messages:', parsedCount, '| User:', userCount, '| Assistant:', assistantCount); - - return { - firstRequest, - assistantResponses: assistantResponses.slice(-CONFIG.MAX_RESPONSES) - }; -} - -function formatContextForSummary({ firstRequest, assistantResponses }) { - if (!firstRequest && assistantResponses.length === 0) { - debugLog('No content for summary context'); - return null; - } - - const parts = []; - if (firstRequest) { - parts.push(`【ユーザーのリクエスト】\n${firstRequest}`); - } - if (assistantResponses.length > 0) { - parts.push(`【アシスタントの応答】\n${assistantResponses.join('\n\n')}`); - } - - const context = parts.join('\n\n'); - debugLog('Context length:', context.length); - return context.length >= CONFIG.MIN_RESPONSE_LENGTH ? context : null; -} - -function createSummarizationPrompt(context) { - return `以下の情報から、**このセッションで実施した作業**を日本語で40文字以内で要約してください。 - -## 重要 -- ユーザーのリクエストとアシスタントの応答から、このセッション固有の作業内容を特定してください -- 「調査」「確認」などの一般的な表現より、具体的な作業内容(例:「Slack通知の要約生成を修正」)を優先してください - -## 出力形式 -日本語の要約文のみ(1行、40文字以内) - -## 必須事項 -- 必ず日本語で出力すること -- このセッションで実際に行った具体的な作業を要約すること -- 技術的な作業内容を含めること(ファイル名、機能名など) - -## 禁止事項 -- 英語での出力 -- 「ユーザーさん」「調査結果」などの前置き -- 「要約:」「作業内容:」などのラベル -- 「〜を確認しました」「〜を調査しました」などの曖昧な表現(具体的な結果を書く) -- 挨拶文や説明文 -- 複数行の出力 - -## 良い例 -- Slack通知の要約生成ロジックを修正 -- BigQueryクエリのパフォーマンスを最適化 -- JWT認証のセキュリティレビューを実施 -- PRマージ後のブランチクリーンアップを実行 - -## 悪い例 -- Added new MCP server(英語は禁止) -- コードを調査しました(曖昧すぎる) -- ユーザーさん、調査結果をまとめます: -- 要約:〜を実施しました - -## セッション情報 -${context.substring(0, CONFIG.MAX_CONTEXT_LENGTH)} - -## 日本語の要約(40文字以内):`; -} - -// ============================================================================ -// Claude CLI Integration (with MCP tools disabled) -// ============================================================================ - -// Auth priority: OAuth > Bedrock > API Key (cost optimization) -const AUTH_CHECKS = [ - { - name: 'oauth', - detect: () => { - try { - const creds = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude', '.credentials.json'), 'utf8')); - const oauth = creds?.claudeAiOauth; - return oauth?.accessToken && oauth?.expiresAt > Date.now(); - } catch (e) { - debugLog('OAuth detection failed:', e.message); - return false; - } - }, - removeKeys: ['ANTHROPIC_API_KEY', 'CLAUDE_CODE_USE_BEDROCK'], - }, - { - name: 'bedrock', - detect: () => !!process.env.CLAUDE_CODE_USE_BEDROCK, - removeKeys: ['ANTHROPIC_API_KEY'], - }, - { - name: 'apikey', - detect: () => process.env.ANTHROPIC_API_KEY?.startsWith('sk-ant-'), - removeKeys: ['CLAUDE_CODE_USE_BEDROCK'], - }, -]; - -const SESSION_VARS = ['CLAUDECODE', 'CLAUDE_CODE_SSE_PORT', 'CLAUDE_CODE_ENTRYPOINT']; - -function buildCleanEnv(skip = []) { - const env = { ...process.env }; - for (const key of SESSION_VARS) delete env[key]; - - const auth = AUTH_CHECKS.find(a => !skip.includes(a.name) && a.detect()) || { name: 'none', removeKeys: [] }; - for (const key of auth.removeKeys) delete env[key]; - if (auth.name === 'none' && env.ANTHROPIC_API_KEY && !env.ANTHROPIC_API_KEY.startsWith('sk-ant-')) { - delete env.ANTHROPIC_API_KEY; - } - debugLog(`Auth: ${auth.name} | removed: [${[...SESSION_VARS, ...auth.removeKeys].join(', ')}]`); - return { env, authName: auth.name }; -} - -function spawnClaudeCLI(prompt, env) { - return new Promise((resolve) => { - const args = [ - '--print', '--no-session-persistence', - '--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}', - '--tools', '', '--model', 'haiku', '-p', prompt - ]; - - const claude = spawn('claude', args, { stdio: ['ignore', 'pipe', 'pipe'], env }); - if (claude.pid) debugLog('Claude CLI spawned with PID:', claude.pid); - - let stdout = '', stderr = '', resolved = false; - const safeResolve = (value, reason) => { - if (resolved) return; - resolved = true; - clearTimeout(timeoutId); - debugLog('CLI resolved:', reason, '| Output length:', value?.length || 0); - resolve(value); - }; - - claude.stdout.on('data', (d) => { stdout += d; }); - claude.stderr.on('data', (d) => { stderr += d; }); - - const timeoutId = setTimeout(() => { - debugLog('Claude CLI timeout after', CONFIG.CLI_TIMEOUT_MS, 'ms'); - claude.kill('SIGTERM'); - safeResolve(null, 'timeout'); - }, CONFIG.CLI_TIMEOUT_MS); - - claude.on('close', (code, signal) => { - if (signal) { safeResolve(null, 'signal:' + signal); return; } - debugLog('Claude CLI exit code:', code); - debugLog('CLI stdout:', stdout.substring(0, 200)); - debugLog('CLI stderr:', stderr.substring(0, 200)); - (code === 0 && stdout.trim()) ? safeResolve(stdout.trim(), 'success') - : safeResolve(null, 'error:' + code); - }); - - claude.on('error', (err) => { - debugLog('Claude CLI spawn error:', err.message); - safeResolve(null, 'spawn-error'); - }); - }); -} - -async function callClaudeCLI(prompt) { - debugLog('=== Calling Claude CLI for summary generation ==='); - const skip = []; - // Try each auth method in priority order, falling back on failure - for (let i = 0; i <= AUTH_CHECKS.length; i++) { - const { env, authName } = buildCleanEnv(skip); - const result = await spawnClaudeCLI(prompt, env); - if (result) return result; - if (authName === 'none') break; - debugLog(`Auth ${authName} failed, trying next`); - skip.push(authName); - } - return null; -} - -function cleanSummaryResponse(summary) { - if (!summary) { - debugLog('No summary to clean'); - return null; - } - - debugLog('Raw summary:', summary); - - const cleaned = summary - .replace(/^(要約[::]?\s*|作業内容[::]?\s*)/i, '') - .replace(/^[\s\n]+/, '') - .split('\n')[0] - .trim(); - - debugLog('Cleaned summary:', cleaned); - return cleaned.length >= CONFIG.MIN_TEXT_LENGTH ? cleaned : null; -} - -async function generateSummary(transcriptPath) { - debugLog('=== Starting summary generation ==='); - - const transcriptData = parseTranscriptData(transcriptPath); - debugLog('Transcript data:', { - hasFirstRequest: !!transcriptData.firstRequest, - responseCount: transcriptData.assistantResponses.length - }); - - const context = formatContextForSummary(transcriptData); - if (!context) { - debugLog('No context available for summary'); - return null; - } - - const prompt = createSummarizationPrompt(context); - debugLog('Prompt length:', prompt.length); - - const output = await callClaudeCLI(prompt); - const result = cleanSummaryResponse(output); - - debugLog('=== Summary generation complete:', result ? 'SUCCESS' : 'FAILED', '==='); - return result; -} - -// ============================================================================ -// Slack API Integration -// ============================================================================ - -function makeSlackApiRequest(apiPath, data, token) { - return new Promise((resolve, reject) => { - const payload = JSON.stringify(data); - debugLog('Slack API request:', apiPath, '| Payload size:', payload.length); - - const req = https.request({ - hostname: 'slack.com', - port: 443, - path: apiPath, - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json; charset=utf-8', - 'Content-Length': Buffer.byteLength(payload) - } - }, (res) => { - let body = ''; - res.on('data', (chunk) => { body += chunk; }); - res.on('end', () => { - const result = safeJsonParse(body); - debugLog('Slack API response:', apiPath, '| ok:', result?.ok, '| error:', result?.error || 'none'); - if (result?.ok) { - resolve(result); - } else { - debugLog('Slack API error details:', result); - resolve(null); - } - }); - }); - - req.on('error', (error) => { - debugLog('Slack API network error:', error.message); - reject(error); - }); - req.write(payload); - req.end(); - }); -} - -const sendSlackMessage = (channelId, token, text) => - makeSlackApiRequest('/api/chat.postMessage', { channel: channelId, text }, token); - -const deleteSlackMessage = (channelId, token, ts) => - makeSlackApiRequest('/api/chat.delete', { channel: channelId, ts }, token); - -// ============================================================================ -// Git Integration -// ============================================================================ - -function getRepositoryName() { - return new Promise((resolve) => { - const git = spawn('git', ['rev-parse', '--show-toplevel']); - let output = ''; - - git.stdout.on('data', (data) => { output += data; }); - git.on('close', (code) => { - const repoName = code === 0 && output.trim() - ? path.basename(output.trim()) - : process.env.GIT_REPO || CONFIG.FALLBACK_REPO_NAME; - debugLog('Repository name:', repoName); - resolve(repoName); - }); - git.on('error', () => resolve(process.env.GIT_REPO || CONFIG.FALLBACK_REPO_NAME)); - }); -} - -// ============================================================================ -// Hook Input Processing -// ============================================================================ - -async function readHookInput() { - debugLog('=== Reading hook input from stdin ==='); - if (process.stdin.isTTY) { - debugLog('stdin is TTY, no hook input available'); - return { transcriptPath: null, stopHookActive: false }; - } - - let input = ''; - for await (const chunk of process.stdin) { - input += chunk; - } - - debugLog('Raw hook input length:', input.length, '| Preview:', input.substring(0, 200)); - const parsed = safeJsonParse(input); - if (parsed) { - debugLog('Parsed hook input:', JSON.stringify(parsed, null, 2)); - } else { - debugLog('Failed to parse hook input'); - } - - const result = { - transcriptPath: parsed?.transcript_path ?? null, - stopHookActive: parsed?.stop_hook_active === true, - assistantResponse: parsed?.assistant_response ?? null - }; - debugLog('Extracted hook input values:', result); - - return result; -} - -// ============================================================================ -// Message Formatting -// ============================================================================ - -function formatNotificationMessage(repoName, summary, includeMention = false) { - const userMention = process.env.SLACK_USER_MENTION; - const content = summary || CONFIG.NO_SUMMARY_MESSAGE; - - // Add debug info to message if in debug mode - const debugInfo = isDebugMode() - ? ` [RUN:${RUN_ID}|PID:${process.pid}]` - : ''; - - const message = `[${repoName}] ${content}${debugInfo}`; - - return includeMention && userMention ? `${userMention} ${message}` : message; -} - -// ============================================================================ -// Main Flow -// ============================================================================ - -async function main() { - debugLog('=== Slack notify script started ==='); - debugLog('RUN_ID:', RUN_ID); - debugLog('PID:', process.pid); - debugLog('CWD:', process.cwd()); - debugLog('ENV - CLAUDE_SESSION_ID:', process.env.CLAUDE_SESSION_ID || 'not set'); - debugLog('ENV - DEBUG_SLACK_NOTIFY:', process.env.DEBUG_SLACK_NOTIFY || 'not set'); - - loadEnvFile(); - - const { SLACK_CHANNEL_ID: channelId, SLACK_BOT_TOKEN: token } = process.env; - if (!channelId || !token) { - debugLog('Missing required env vars - SLACK_CHANNEL_ID:', !!channelId, '| SLACK_BOT_TOKEN:', !!token); - process.exit(0); - } - - // Prevent duplicate execution - if (!acquireLock()) { - debugLog('EXITING: Could not acquire lock'); - process.exit(0); - } - - try { - const [hookInput, repoName] = await Promise.all([ - readHookInput(), - getRepositoryName() - ]); - - const { transcriptPath, stopHookActive, assistantResponse } = hookInput; - - debugLog('Hook input received:', { - transcriptPath, - stopHookActive, - assistantResponse: assistantResponse ? assistantResponse.substring(0, 100) : null, - hasTranscriptPath: !!transcriptPath - }); - - // Skip if this is a recursive hook call (e.g., from internal Claude CLI for summary generation) - if (stopHookActive) { - debugLog('EXITING: stop_hook_active is true (preventing recursive execution)'); - releaseLock(); - process.exit(0); - } - - debugLog('Transcript path:', transcriptPath); - debugLog('Repository name:', repoName); - - const summaryStartTime = Date.now(); - let summary = null; - if (assistantResponse) { - // Kiro CLI: assistant_responseから直接要約生成 - const context = assistantResponse.length > CONFIG.MAX_CONTEXT_LENGTH - ? assistantResponse.substring(0, CONFIG.MAX_CONTEXT_LENGTH) - : assistantResponse; - const prompt = createSummarizationPrompt(`【アシスタントの応答】\n${context}`); - const output = await callClaudeCLI(prompt); - summary = cleanSummaryResponse(output); - } else if (transcriptPath) { - // Claude Code: transcript_pathから要約生成 - summary = await generateSummary(transcriptPath); - } - const summaryDuration = Date.now() - summaryStartTime; - debugLog('Final summary:', summary, '| Generation time:', summaryDuration, 'ms'); - - // Send message with mention to trigger notification - const mentionMessage = formatNotificationMessage(repoName, summary, true); - debugLog('=== Sending mention message ===', mentionMessage.substring(0, 100)); - const mentionSendTime = Date.now(); - const mentionResult = await sendSlackMessage(channelId, token, mentionMessage); - if (!mentionResult) { - debugLog('FAILED to send mention message'); - return; - } - debugLog('Mention message sent | ts:', mentionResult.ts, '| Duration:', Date.now() - mentionSendTime, 'ms'); - - // Wait a bit before sending clean message and deleting - debugLog('Waiting', CONFIG.DELETE_DELAY_MS, 'ms before cleanup'); - await sleep(CONFIG.DELETE_DELAY_MS); - - // Send clean message without mention - const cleanMessage = formatNotificationMessage(repoName, summary, false); - debugLog('=== Sending clean message ===', cleanMessage.substring(0, 100)); - const cleanSendTime = Date.now(); - const cleanResult = await sendSlackMessage(channelId, token, cleanMessage); - if (!cleanResult) { - debugLog('FAILED to send clean message'); - } else { - debugLog('Clean message sent | ts:', cleanResult.ts, '| Duration:', Date.now() - cleanSendTime, 'ms'); - } - - // Delete the mention message - if (mentionResult.ts) { - debugLog('=== Deleting mention message | ts:', mentionResult.ts, '==='); - const deleteStartTime = Date.now(); - const deleteResult = await deleteSlackMessage(channelId, token, mentionResult.ts); - if (deleteResult) { - debugLog('Mention message deleted successfully | Duration:', Date.now() - deleteStartTime, 'ms'); - } else { - debugLog('FAILED to delete mention message'); - } - } - - debugLog('=== Slack notify script completed successfully | Total runtime:', Date.now() - summaryStartTime + summaryDuration, 'ms ==='); - } finally { - releaseLock(); - } -} - -main().catch((error) => { - debugLog('Fatal error:', error.message, error.stack); - releaseLock(); - process.exit(0); -}); diff --git a/plugins/ndf/scripts/statusline-switch.sh b/plugins/ndf/scripts/statusline-switch.sh deleted file mode 100755 index bccb616..0000000 --- a/plugins/ndf/scripts/statusline-switch.sh +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env bash -# NDF plugin: statusline の導入・切替・復元を行う。 -# -# Usage: statusline-switch.sh -# ensure : SessionStart hook 用。statusLine 未設定の場合のみ NDF 版を設定する -# (既存設定があれば何もしない)。NDF 版利用中はスクリプト更新のみ追従。 -# set : 既存の statusLine 設定をバックアップした上で NDF 版に切り替える。 -# restore : バックアップから元の statusLine 設定を復元する -# (バックアップが無ければ statusLine 設定を削除しデフォルト表示に戻す)。 -# status : 現在の statusLine 設定とバックアップの有無を表示する。 -set -euo pipefail - -CMD="${1:-status}" -SETTINGS="$HOME/.claude/settings.json" -TARGET="$HOME/.claude/ndf-statusline.sh" -BACKUP="$HOME/.claude/.ndf-statusline-backup.json" -LOCK="$HOME/.claude/.ndf-statusline.lock" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SRC="$SCRIPT_DIR/statusline.sh" -NDF_COMMAND="bash ~/.claude/ndf-statusline.sh" - -# jq 必須 (ensure は SessionStart hook から毎回呼ばれるためノイズ防止で黙ってスキップ) -if ! command -v jq >/dev/null 2>&1; then - if [ "$CMD" != "ensure" ]; then - echo "[ndf:statusline] jq が見つからないため処理をスキップしました" >&2 - fi - exit 0 -fi - -mkdir -p "$HOME/.claude" -if [ ! -f "$SETTINGS" ]; then - echo "{}" > "$SETTINGS" -fi - -# プラグイン同梱の statusline.sh を ~/.claude/ndf-statusline.sh に配置 (差分時のみ) -deploy_script() { - if [ ! -f "$TARGET" ] || ! cmp -s "$SRC" "$TARGET"; then - cp "$SRC" "$TARGET" - chmod +x "$TARGET" - fi -} - -current_command() { - jq -r '.statusLine.command // empty' "$SETTINGS" 2>/dev/null || true -} - -is_ndf_statusline() { - case "$(current_command)" in - *ndf-statusline.sh*) return 0 ;; - *) return 1 ;; - esac -} - -# current_command から実行スクリプトのパスを抽出し、先頭の ~ を $HOME に展開する。 -# 例: "bash ~/.claude/statusline-command.sh" -> "/home/user/.claude/statusline-command.sh" -current_script_path() { - local cmd path - cmd="$(current_command)" - [ -z "$cmd" ] && return 0 - # 末尾の *.sh トークンを実行対象スクリプトとみなす。 - # grep -o は GNU 拡張のため、POSIX 準拠かつ BusyBox でも動く sed で抽出する。 - # NDF が配置するコマンドは "bash ~/.claude/.sh" 形式 (パスにスペースを含まない) を想定。 - # 1つ目の置換がマッチしたら t で分岐して二重出力を防ぐ (tail -n1 のパイプを削減)。 - path="$(printf '%s\n' "$cmd" | sed -n 's/.*[[:space:]]\([^[:space:]]*\.sh\).*/\1/p; t; s/^\([^[:space:]]*\.sh\)$/\1/p')" - [ -z "$path" ] && return 0 - case "$path" in - "~/"*) path="$HOME/${path#\~/}" ;; - "~") path="$HOME" ;; - esac - printf '%s\n' "$path" -} - -# 指定スクリプトが NDF 由来 (マーカー付き、または既知のレガシーコピー) か判定する。 -# ユーザー独自の statusline を誤って上書きしないため、判定は厳格に行う。 -is_ndf_managed_copy() { - local path="$1" - [ -f "$path" ] || return 1 - # ① マーカーがあれば NDF 管理コピー確定 (今後配置される全コピーが該当) - if grep -Fq 'ndf-statusline: managed' "$path" 2>/dev/null; then - return 0 - fi - # ② レガシー救済: マーカー導入前の既知の旧コピー名で、かつ NDF statusline 特有の - # ロジック (ctx ラベル + コンテナ名取得) を両方含む場合のみ移行対象とする - case "$(basename "$path")" in - statusline-command.sh) - if grep -Fq '[ctx:' "$path" 2>/dev/null && grep -Fq 'container_name' "$path" 2>/dev/null; then - return 0 - fi - ;; - esac - return 1 -} - -# settings.json を atomic に書き換える。引数: jq フィルタ -update_settings() { - local tmp - tmp="$(mktemp)" - if jq "$@" "$SETTINGS" > "$tmp"; then - mv "$tmp" "$SETTINGS" - else - rm -f "$tmp" - return 1 - fi -} - -set_ndf_statusline() { - update_settings --arg cmd "$NDF_COMMAND" \ - '.statusLine = {type: "command", command: $cmd}' -} - -# NDF 由来の旧 statusline を検出した際に、既存設定をバックアップした上で -# 正規パス (~/.claude/ndf-statusline.sh) 参照へ移行する。 -migrate_to_ndf_statusline() { - local existing - existing=$(jq -c '.statusLine // empty' "$SETTINGS" 2>/dev/null || true) - if [ -n "$existing" ] && [ ! -f "$BACKUP" ]; then - printf '%s\n' "$existing" > "$BACKUP" - fi - set_ndf_statusline - echo "[ndf:statusline] NDF 由来の旧 statusline を検出したため正規パス (~/.claude/ndf-statusline.sh) へ移行しました (旧設定は $BACKUP に退避)" -} - -cmd_ensure() { - deploy_script - # 既に statusLine が設定されている場合 - if [ -n "$(jq -r '.statusLine // empty' "$SETTINGS" 2>/dev/null)" ]; then - # 正規パスを指していれば deploy_script で本体が追従済み (何もしない) - if is_ndf_statusline; then - return 0 - fi - # NDF が過去に配置したコピー (マーカー付き or レガシー statusline-command.sh) を - # 指している場合のみ、正規パス参照へ移行してバージョンアップ追従を回復する - local cur_path - cur_path="$(current_script_path)" - if [ -n "$cur_path" ] && is_ndf_managed_copy "$cur_path"; then - migrate_to_ndf_statusline - return 0 - fi - # それ以外はユーザー独自設定として尊重し、何もしない - return 0 - fi - set_ndf_statusline - echo "[ndf] statusLine を NDF 標準 statusline に設定しました (~/.claude/settings.json)" -} - -cmd_set() { - deploy_script - if is_ndf_statusline; then - echo "[ndf:statusline] 既に NDF 標準 statusline が設定されています" - return 0 - fi - # 既存設定があればバックアップしてから切り替え - local existing - existing=$(jq -c '.statusLine // empty' "$SETTINGS" 2>/dev/null || true) - if [ -n "$existing" ]; then - printf '%s\n' "$existing" > "$BACKUP" - echo "[ndf:statusline] 既存設定をバックアップしました: $BACKUP" - fi - set_ndf_statusline - echo "[ndf:statusline] NDF 標準 statusline に切り替えました" -} - -cmd_restore() { - if [ -f "$BACKUP" ]; then - update_settings --slurpfile bk "$BACKUP" '.statusLine = $bk[0]' - rm -f "$BACKUP" - echo "[ndf:statusline] バックアップから statusLine 設定を復元しました" - elif is_ndf_statusline; then - update_settings 'del(.statusLine)' - echo "[ndf:statusline] バックアップが無いため statusLine 設定を削除しました (デフォルト表示に戻ります)" - else - echo "[ndf:statusline] 復元対象がありません (NDF statusline は未使用、バックアップ無し)" - fi -} - -cmd_status() { - local cur - cur=$(current_command) - if [ -z "$cur" ]; then - echo "statusLine: 未設定 (デフォルト表示)" - elif is_ndf_statusline; then - echo "statusLine: NDF 標準 statusline ($cur)" - else - echo "statusLine: カスタム設定 ($cur)" - fi - if [ -f "$BACKUP" ]; then - echo "バックアップ: あり ($(cat "$BACKUP"))" - else - echo "バックアップ: なし" - fi -} - -run_cmd() { - case "$CMD" in - ensure) cmd_ensure ;; - set) cmd_set ;; - restore) cmd_restore ;; - status) cmd_status ;; - *) - echo "Usage: $(basename "$0") " >&2 - exit 1 - ;; - esac -} - -# 複数セッション同時起動時の race condition を flock で回避 (flock 不在時は atomic rename のみ) -if command -v flock >/dev/null 2>&1; then - ( - flock -x -w 5 200 || exit 0 - run_cmd - ) 200>"$LOCK" -else - run_cmd -fi diff --git a/plugins/ndf/scripts/statusline.sh b/plugins/ndf/scripts/statusline.sh deleted file mode 100755 index 19d7c09..0000000 --- a/plugins/ndf/scripts/statusline.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash -# ndf-statusline: managed (do not edit; auto-updated by ndf:statusline) -# NDF plugin 標準 statusline: -# <コンテナ名 or ホスト名> [<モデル名>: 使用トークン / 全体 (使用率%)] -input=$(cat) - -# コンテナ名を取得する。コンテナでなければホスト名にフォールバック -container_name="" - -# Docker/コンテナ環境かどうかを /.dockerenv で判定 -if [ -f /.dockerenv ]; then - # CONTAINER_NAME 環境変数が明示設定されていればそちらを優先 - container_name="${CONTAINER_NAME:-}" - - # Docker ソケットが使えれば docker inspect で compose 上のコンテナ名を取得 - # (/etc/hostname はコンテナIDなので、それをキーに引く) - if [ -z "$container_name" ] && [ -S /var/run/docker.sock ] && command -v docker >/dev/null 2>&1; then - container_id=$(cat /etc/hostname 2>/dev/null | tr -d '[:space:]') - if [ -n "$container_id" ]; then - container_name=$(docker inspect --format '{{.Name}}' "$container_id" 2>/dev/null | sed 's|^/||') - fi - fi - - # 取れなければ /etc/hostname(コンテナID)をフォールバックとして使用 - if [ -z "$container_name" ] && [ -f /etc/hostname ]; then - container_name=$(cat /etc/hostname 2>/dev/null | tr -d '[:space:]') - fi -fi - -# コンテナ名が取れなければ hostname コマンドにフォールバック -if [ -z "$container_name" ]; then - container_name=$(hostname -s 2>/dev/null || hostname) -fi - -dir="$container_name" - -# claude root のパスを取得(project_dir を優先し、なければ current_dir を使用) -# jq 不在や無効な JSON 入力時に stderr が statusLine 描画に漏れないよう 2>/dev/null で抑制 -claude_root=$(echo "$input" | jq -r '.workspace.project_dir // .workspace.current_dir // empty' 2>/dev/null) - -total_input=$(echo "$input" | jq -r '.context_window.total_input_tokens // empty' 2>/dev/null) -ctx_size=$(echo "$input" | jq -r '.context_window.context_window_size // empty' 2>/dev/null) -used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty' 2>/dev/null) - -# モデル表示名を取得(ラベルとして使用)。取れなければ "ctx" にフォールバック -model_name=$(echo "$input" | jq -r '.model.display_name // .model.id // empty' 2>/dev/null) -ctx_label="${model_name:-ctx}" - -ctx_info="" -if [ -n "$total_input" ] && [ -n "$ctx_size" ] && [ -n "$used_pct" ]; then - total_input_k=$(awk "BEGIN { printf \"%.1f\", $total_input / 1000 }") - ctx_size_k=$(awk "BEGIN { printf \"%.0f\", $ctx_size / 1000 }") - ctx_info=$(printf " \033[0;36m[%s: %sk / %sk tokens (%.0f%%)]" "$ctx_label" "$total_input_k" "$ctx_size_k" "$used_pct") -fi - -if [ -n "$claude_root" ]; then - printf "\033[01;34m%s\033[00m \033[0;33m%s\033[00m%s" "$dir" "$claude_root" "$ctx_info" -else - printf "\033[01;34m%s\033[00m%s" "$dir" "$ctx_info" -fi diff --git a/plugins/ndf/skills-codex/branch-fix-strategy/SKILL.md b/plugins/ndf/skills-codex/branch-fix-strategy/SKILL.md deleted file mode 100644 index a4714a3..0000000 --- a/plugins/ndf/skills-codex/branch-fix-strategy/SKILL.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -name: branch-fix-strategy -description: "Plan multi-branch fixes and cherry-picks." -when_to_use: "同じ修正を複数ブランチ (qa/staging/release等) に適用する必要があるとき。Triggers: 'cherry-pick', '環境ブランチに修正適用', 'qaに反映', 'stagingに反映', 'release branchへ', 'multi-branch fix', 'apply to qa/staging'" ---- - -# ブランチ修正適用戦略 - -## 適用タイミング - -- featureブランチの修正を `qa/*`, `staging/*`, `release/*` 等の環境ブランチにも適用する必要がある場合 -- 同じ修正を複数ブランチに並行適用する場面全般 - -## 核心ルール - -### 1. 修正は feature ブランチに先に commit → cherry-pick で環境ブランチへ - -``` -✅ feature に commit → cherry-pick して短命ブランチ → 環境ブランチへ PR -❌ 短命ブランチに先に commit → feature に手作業で再実装(二重作業・不整合リスク) -``` - -### 2. 環境ブランチを feature ブランチに merge しない(main 汚染禁止) - -``` -❌ feature/xxx ← merge qa/staging(conflict 解消目的でも禁止) -``` - -環境ブランチを featureブランチにmergeすると、後で `feature → main` のPRに環境固有コードが混入する。 - -### 3. origin/main を必ず取り込む - -短命ブランチを push する前に必ず `git merge origin/main` する。CI で最新 main 必須の Workflow があるため。 - -### 4. マージ済みブランチに push しない - -環境ブランチ向けの短命ブランチに push する前に `gh pr list --head ` で PR 状態を確認する。マージ済みなら新ブランチ + 新 PR を作成する(サフィックス `-v2`, `-v3` を付ける)。 - -## 実行手順 - -`/ndf:cherry-pick-pr ` で自動化されている。手動で行う場合のみ以下を参照。 - -```bash -# 1. feature ブランチで修正を commit -git checkout feature/xxx -git add && git commit -m "fix: 修正内容" -git log --oneline -1 # commit hash を記録 - -# 2. 短命ブランチを作成 -git fetch origin qa/staging -git checkout -b feature/xxx-for-staging origin/qa/staging - -# 3. origin/main を取り込む(必須) -git fetch origin main -git merge origin/main --no-edit - -# 4. cherry-pick(-x で元 commit hash を参照に残す) -git cherry-pick -x - -# 5. push して PR 作成 -git push -u origin feature/xxx-for-staging -gh pr create --base qa/staging --title "fix: 修正内容(staging検証用)" - -# 6. 元のブランチに戻る -git checkout feature/xxx -``` - -## なぜこの順序が重要か - -| 観点 | 正しい順序 | 誤った順序 | -|------|-----------|-----------| -| 単一ソース | feature ブランチが唯一の正 | 二箇所で実装 | -| 一貫性 | cherry-pick で完全一致 | 手書き差分でズレる | -| 追跡性 | `-x` で元 commit が明記 | 関連 commit 不明確 | - -## revert 操作の注意 - -revertの連鎖(revert → reapply → revert...)ではなく、**最終的なあるべき状態を直接コミット**するのが望ましい。履歴上の意図が明確になり、後の cherry-pick も簡単になる。 - -## 関連コマンド・スキル - -| リソース | 用途 | -|---------|------| -| `/ndf:cherry-pick-pr` | cherry-pick + 短命ブランチ + origin/main 取り込み + PR 作成を自動化 | -| `/ndf:pr` | 通常のPR作成。非 main ベースは `cherry-pick-pr` に誘導される | -| `/ndf:sync-main` | 現在のブランチに最新 main を取り込む | -| `/ndf:deploy` | 環境ブランチへのデプロイPR作成(ブランチ全体をmerge main経由で適用) | diff --git a/plugins/ndf/skills-codex/cherry-pick-pr/SKILL.md b/plugins/ndf/skills-codex/cherry-pick-pr/SKILL.md deleted file mode 100644 index b5dc134..0000000 --- a/plugins/ndf/skills-codex/cherry-pick-pr/SKILL.md +++ /dev/null @@ -1,120 +0,0 @@ ---- -name: cherry-pick-pr -description: "Create cherry-pick PRs for environment branches." -argument-hint: " (例: qa/staging, release/v2)" -disable-model-invocation: true -allowed-tools: - - Bash - - Read - - Grep ---- - -# cherry-pick PR 作成コマンド - -featureブランチから指定ベースブランチへ、短命ブランチ経由で cherry-pick PR を作成する。`feature → main` の PR にベースブランチ固有コードが混入するのを防ぐ。 - -## 使用方法 - -``` -/ndf:cherry-pick-pr qa/staging -/ndf:cherry-pick-pr release/v2 -``` - -## なぜ必要か - -featureブランチに環境ブランチ(`qa/staging`等)を merge して conflict を解消すると、`feature → main` の PR に環境ブランチ固有のコードが混入する(main汚染)。短命ブランチ + cherry-pick で、必要なコミットだけを対象ブランチに届ける。 - -詳細な原則は `/ndf:branch-fix-strategy` スキル参照。 - -## 処理フロー - -### 1. 引数・現状確認 -- 引数からベースブランチ名を取得(必須。未指定なら確認) -- `git branch --show-current` で現在ブランチを取得 - -### 2. 既存PRのマージ済みチェック(必須) - -同じベースブランチ向けの短命ブランチに既存PRがないか確認する。 - -```bash -# 同名パターンのブランチでマージ済みPRがないか確認 -gh pr list --head "-for-" --state merged \ - --json number,mergedAt --jq '.[]' -``` - -マージ済みPRが見つかった場合、**同じブランチ名は使えない**。サフィックスを付ける(例: `-v2`, `-v3`)。 - -### 3. コミット一覧の確認 - -```bash -git log --oneline main..HEAD -``` - -ユーザーに cherry-pick 対象コミットを確認(全コミット or 選択)。 - -### 4. 短命ブランチ作成 - -```bash -git fetch origin -git checkout -b -for- origin/ -``` - -- ``: ベースブランチのスラッシュ以降(例: `qa/staging` → `staging`) -- 例: `feature/add-auth-for-staging` - -### 5. origin/main を取り込む(必須) - -```bash -git fetch origin main -git merge origin/main --no-edit -``` - -CIで最新main必須のWorkflowがあるため、取り込み忘れるとconflictやCIエラーになる。 - -### 6. cherry-pick 実行 - -```bash -git cherry-pick -x ... -``` - -`-x` オプションで元のcommit hashが参照として残り、追跡性が向上する。 - -conflict が発生した場合: -- `git diff --name-only --diff-filter=U` でconflictファイル一覧 -- 解消を試み、ユーザーに確認後 `git cherry-pick --continue` - -### 7. push して PR 作成 - -```bash -git push -u origin -gh pr create --base --title "<タイトル>" --body "$(cat <<'EOF' -## Summary -- feature/xxx からcherry-pickした<環境名>向けPR -- 元コミット: - -## Test plan -- [ ] <環境名>で動作確認 - - -EOF -)" -``` - -### 8. 元ブランチに戻る - -```bash -git checkout -``` - -## 注意事項 - -- 短命ブランチは PR マージ後に削除してよい -- `feature → main` の PR には影響しない -- ベースブランチを feature ブランチに merge するのは **禁止**(main汚染の原因) -- `-x` オプションで元commit参照を残す(追跡性) - -## 関連 - -- `/ndf:branch-fix-strategy` — なぜこの手順が必要かの原則 -- `/ndf:pr` — 通常のPR作成(base=main) -- `/ndf:deploy` — ブランチ全体を環境へデプロイ(cherry-pickとは別用途) diff --git a/plugins/ndf/skills-codex/clean/SKILL.md b/plugins/ndf/skills-codex/clean/SKILL.md deleted file mode 100644 index 2f75e54..0000000 --- a/plugins/ndf/skills-codex/clean/SKILL.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: clean -description: "Delete local and remote merged branches." -disable-model-invocation: true -allowed-tools: - - Bash ---- - -# ブランチクリーンアップコマンド - -mainマージ済みブランチをローカル/リモート削除。 - -## 手順 - -1. `git branch --merged main`確認 -2. main・現在ブランチ除外 -3. `git branch -d ` -4. `git push origin --delete ` - -**注意**: 削除前確認・main除外・現在ブランチ除外 diff --git a/plugins/ndf/skills-codex/cross-review/SKILL.md b/plugins/ndf/skills-codex/cross-review/SKILL.md deleted file mode 100644 index 0f2bed3..0000000 --- a/plugins/ndf/skills-codex/cross-review/SKILL.md +++ /dev/null @@ -1,475 +0,0 @@ ---- -name: cross-review -description: "Run iterative Codex and Gemini PR reviews." -argument-hint: "[PR番号] [--max-rounds N] [--rotate-after K] [--rotate-mode light|squash] [--only codex|gemini] [--focus TEXT] [--extra-instructions-file PATH]" -disable-model-invocation: true -allowed-tools: - - Bash - - Read - - Edit - - Write - - Glob - - Grep ---- - -# クロスレビュー収束ループ - -PR を **codex / gemini 両方** にレビューさせ、両者が `APPROVE` を返すまで -`/ndf:review` と `/ndf:fix` を自動で回す。 - -/goalの引数として呼ばれた場合は、codex / gemini が`APPROVE` になるまで/cross-reviewを繰り返す。 - * codex / geminiのいずれかが不具合などで実行できなくなった場合は異常終了とする - * /goalで呼ばれた場合はPR ローテーションは実施しなくてよい。 - * 振動検知した場合はAIが判断して正しい状態を決める。 - -詳細手順は `docs/` 配下に、主要コマンドは `scripts/` 配下に分割している: - -- [docs/01-state-and-review.md](docs/01-state-and-review.md) — Step 0〜4 (state init / round / 並列レビュー / 判定 / 振動検知) -- [docs/02-fix-and-rotation.md](docs/02-fix-and-rotation.md) — Step 5〜8 (サブエージェント修正 / PR ローテーション / 終了処理) -- [scripts/state.py](scripts/state.py) — state.json 操作(uv 自己完結スクリプト、stdlib のみ) -- [scripts/launch-codex.sh](scripts/launch-codex.sh) / [scripts/launch-gemini.sh](scripts/launch-gemini.sh) — レビューランチャ -- [scripts/monitor.py](scripts/monitor.py) — codex/gemini プロセス多軸監視 (sentinel / pidfile / 早期エラー / stall / hard timeout / result.json) -- [scripts/wait-review.sh](scripts/wait-review.sh) — `monitor.py` の薄ラッパ(互換用) -- [scripts/rotate-pr.sh](scripts/rotate-pr.sh) — PR ローテーション - -メインセッションからは `$SCRIPTS/state.py ` 形式で呼ぶだけで、 -state.json の読み書きや AI launcher 起動・完了待ちは全て委譲される。 - -## 設計方針 - -長丁場が予想されるため **メインセッションの context 消費を最小化** する: - -| 観点 | 方針 | -|---|---| -| レビュー投稿 | **AI 自身が `gh api` で PR に直接投稿**。メインはペイロードを保持しない | -| 修正 | **必ずサブエージェント (`general-purpose`) で実行**。メイン context に diff は載せない | -| ユーザ問い合わせ | 自動判断を最大化(`critical`/`major`/`minor` は自動修正、ループ中の `nit` は deferred) | -| 取りこぼし防止 | **ループ終了時(approved / max_rounds / oscillation / error いずれも)に最終スイープを必須実行**。`/ndf:fix` を再実行し、残った open review thread(最終 APPROVE ラウンドの minor/nit インラインコメント含む)を **全て解消**。修正可能なものは修正 + push、判断保留 nit も reply + resolveReviewThread して **open thread 0 で終了** | -| 状態の永続化 | `/.cross_review/cross-review-pr<番号>-state.json` に集約。中断・再開可能 | -| 長尺PR対策 | **`--rotate-after` ラウンドで PR をローテーション**(default=light: 同ブランチで PR 巻き直し / squash: 新ブランチ + squash 統合) | -| 振動検知 | 同じ指摘が 2 round で 50%以上重複したら中断 | - -## 引数 - -| 引数 | 意味 | 既定 | -|---|---|---| -| `[PR番号]` | 対象 PR(省略時は直前 PR / 現在ブランチ) | — | -| `--max-rounds N` | 全体最大ラウンド数(PR ローテーションを含む通算) | `12` | -| `--rotate-after K` | この round 数で未収束なら PR ローテーション | `8` | -| `--rotate-mode light\|squash` | ローテーション方式。`light`: 同ブランチで旧 PR を close → 新 PR (title/body は現状の差分・実装から再生成)。`squash`: squash 統合 + 新ブランチ + `(rotated)` suffix | `light` | -| `--only codex` / `--only gemini` | 片方だけで回す(デバッグ用) | 両方 | -| `--focus TEXT` | 自動レビュー観点に上乗せして codex / gemini 両方に渡す追加観点。短い重点チェック向け | なし | -| `--extra-instructions-file PATH` | 自動レビュー観点に上乗せして codex / gemini 両方に渡す追加観点を UTF-8 テキストファイルから読む。長いチェックリスト向け | なし | - -例: - -``` -/ndf:cross-review 123 -/ndf:cross-review 123 --max-rounds 4 --rotate-after 2 -/ndf:cross-review 123 --rotate-mode squash -/ndf:cross-review 123 --only codex -/ndf:cross-review 123 --focus "ドキュメントとコードの整合性を重点的に確認" -/ndf:cross-review 123 --extra-instructions-file /tmp/review-focus.md -``` - -### 自動レビュー観点テンプレート - -`state.py init` は GitHub API の `pulls//files --paginate` で変更ファイルを全件取得して分類し、 -codex / gemini 両 launcher に同じ追加観点を渡す。`--focus` / -`--extra-instructions-file` は、この自動テンプレートの後ろに上乗せされる。 - -自動カテゴリ: - -- `common`: PR 全体の目的、変更範囲、保守性、テスト、ロールバック容易性 -- `docs_only`: ドキュメントのみ PR。企画・説明の妥当性、コード/設定/コマンド/他 docs との整合性 -- `code`: 設計、正確性、可読性、冗長・重複、言語らしさ、セキュリティ、関数/ファイルの責務とサイズ -- `db_migration`: データ設計、型、NULL/default/制約/index、既存データ、backfill、ロールバック -- `test`: テストの仕様性、境界値、失敗系、flaky リスク -- `dependency`: 依存追加/更新、lockfile、ライセンス、互換性、セキュリティ -- `config_ci`: CI/設定、権限、secret、cache、環境差分 -- `api_contract`: API 契約、互換性、schema、status、エラー形式、認可 -- `auth_security`: 認証/認可、secret/PII、CSRF/CORS/session/JWT/OAuth -- `frontend`: UI 状態、アクセシビリティ、レスポンシブ、状態管理、表示文言 -- `performance`: N+1、I/O、メモリ、ロック、cache、queue、冪等性 -- `deletion_rename`: 削除/リネーム参照漏れ、後方互換、移行手順 -- `generated`: 生成物、lockfile、再生成手順、差分ノイズ -- `i18n`: 翻訳キー、fallback、変数展開、表示幅、文言整合 -- `infra`: IaC / Docker / Kubernetes 等の権限、secret、公開範囲、ロールバック - -### `--rotate-mode` の選び方 - -- **`light` (default)**: PR を読む人 (将来のレビュアー / 後続 PR を作る人) が cross-review の存在を意識せずに済む。release branch 戦略・TODO 参照・コミット単位レビューを破壊しない。**通常はこちらを使う** -- **`squash`**: 巨大 PR を 1 commit に潰したい / `(rotated)` suffix で rotation 履歴を PR title に残したい場合のみ。release branch 戦略を使う運用とは併用しない - -## 前提 - -- `/ndf:review` が **AI 直接投稿**(外部 AI 自身が `gh api` で投稿)に対応 -- `/ndf:fix` が **サブエージェント起動 + 重要度ベース自動修正 + Resolve Conversation** に対応 -- `codex` / `gemini` CLI が動作し、`gh` CLI が認証済み -- `Agent(subagent_type="general-purpose", ...)` でサブエージェントを起動可能 - -## 事前確認(`state.py init` が自動実施) - -ループ開始前に **4 つのプリチェック** が必要だが、すべて `scripts/state.py init` -が内部で実施する。メインは結果を KEY=VALUE 形式で受け取るだけで良い。 - -| # | 対策 | スクリプト側で何をするか | -|---|---|---| -| 1 | 自分の PR 判定(422 回避) | `gh api user` と `gh pr view --json author` を比較し `is_own_pr` / `event_downgrade` を state.json に書く | -| 2 | worktree 分離 | `git worktree add /--/pr ` を冪等実行(`` は `NDF_WORKTREE_BASE` env > `<システム tmpdir>/ndf-worktrees` の優先順で解決)。パスが存在しても現リポジトリの登録済み worktree でなければ `.stale-` に退避して作り直す | -| 3 | gemini trusted directory | `launch-gemini.sh` が `GEMINI_CLI_TRUST_WORKSPACE=true` + `--skip-trust` を必ず併用。**tmp dir は `/.cross_review/`** を採用し、gemini の workspace 制約 (workspace 外の `write_file` がブロックされる) を根本回避 | -| 4 | 既存コメント差分 | `fix/scripts/fetch-pr-comments.sh` で 3 ソース (インラインコメント / レビュー body / PR レベルコメント) を一括取得し `$TMP_DIR/cross-review-pr-existing-comments.txt` に保存。gemini プロンプトには **内容をインライン埋め込み**、codex プロンプトには path を渡す | - -### `` の解決順 - -`state.py init` は worktree の親ディレクトリを以下の優先順で解決する: - -1. `NDF_WORKTREE_BASE` 環境変数(明示オーバーライド) -2. `<システム tmpdir>/ndf-worktrees`(Python `tempfile.gettempdir()`。非永続領域のため - コンテナ再作成で自動消滅し、共有 volume を消費しない) - -worktree の実パスは `/--/pr` 形式で、リポジトリ slug を含める -ことで**他リポジトリの同一 PR 番号と衝突しない**。永続 volume(旧 `/work/worktrees`)を -使っていた頃は別プロジェクトの残骸 worktree を誤って流用する事故があったため、 -パスが存在しても `git worktree list` に登録されていなければ `.stale-` に -退避して作り直すガードも入っている。 - -解決した実パスは `state.json` の `worktree_path` に書かれるため、後続スクリプトや -サブエージェント prompt は state.json から読めば追従できる。 - -### intent / posted_as の両保持(最重要) - -GitHub は **自分の PR には `REQUEST_CHANGES` でレビューを投稿できない** -(`HTTP 422`)。state.json には **両方** を保持する: - -```json -"codex": { - "intent": "REQUEST_CHANGES", // AI の本来判定。ループ収束判定に使う - "posted_as": "COMMENT", // 422 回避でダウングレードした結果 - "comments": 5, "review_url": "..." -} -``` - -`state.py judge` は `intent` を見るので、ダウングレード投稿してもループは続行する。 - -## 全体フロー - -```mermaid -flowchart TD - Start([事前確認 / loop 開始前に 1 回だけ]):::phase --> Init["worktree 作成 + state.json 初期化
・自分の PR 判定 → event downgrade 設定
・<worktree-base>/pr<PR> を用意
・既存コメントスナップショット保存"] - Init --> Round["Round N start
current_pr = PR#"]:::phase - - Round -.並列バックグラウンド.-> Codex["/ndf:review <PR> codex
(AI が gh api で直接投稿)
body 先頭: cross-review / round N / codex / intent
→ result.json (intent + posted_as)"] - Round -.並列バックグラウンド.-> Gemini["/ndf:review <PR> gemini
--skip-trust 必須
body 先頭: cross-review / round N / gemini / intent
→ result.json (intent + posted_as)"] - - Codex --> Decide{"判定 (intent ベース)"} - Gemini --> Decide - - Decide -->|両方 APPROVE / SKIP| Approved([final = approved]):::ok - Decide -->|一方でも REQUEST_CHANGES| Fix["Agent (general-purpose)
/ndf:fix <PR> --defer-nit を worktree 内で実行
・critical/major/minor 修正 + push
・reply + resolveReviewThread
・deferred/rejected は reply のみ
→ $TMP_DIR/fix-pr<#>-result.json"] - - Fix --> Check{収束チェック} - Check -->|max-rounds 到達| MaxR([final = max_rounds]):::stop - Check -->|振動検知 50% 重複| Osc([final = oscillation]):::stop - Check -->|CI failure code-related| Err([final = error]):::stop - Check -->|"CI failure meta-only (Assignees 等)"| Round - Check -->|round_in_pr >= rotate-after| Rotate["PR rotation
light (default): 同ブランチで巻き直し
squash (opt-in): 新ブランチ + squash 統合
light は Agent が title/body 再生成"] - Check -->|それ以外| Round - Rotate --> Round - - Approved --> Sweep["最終スイープ (必須)
Agent (general-purpose)
/ndf:fix <PR> を再実行
・残 open review thread を全て確認
・修正可能な minor/nit は修正 + push
・判断保留 nit も reply + resolveReviewThread
→ open thread 0 で終了"] - MaxR --> Sweep - Osc --> Sweep - Err --> Sweep - Sweep --> Report[ラウンドサマリ報告
+ 残 deferred nit を参考として列挙] - - classDef phase fill:#eef,stroke:#557 - classDef ok fill:#dfd,stroke:#383 - classDef stop fill:#fdd,stroke:#933 -``` - -## 実行ステップ概要(メインの bash 骨組み) - -各ステップの詳細は `docs/` 参照。メインは以下のテンプレートで scripts/ を呼ぶだけ: - -```bash -PLUGIN_ROOT="${PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}" -SCRIPTS="$PLUGIN_ROOT/skills-codex/cross-review/scripts" - -# STATE_PR は state.json のキー (= 最初に init した PR 番号)。 -# rotation 後も state.json のパスは変わらないため、scripts/ への引数には常に -# STATE_PR を渡す。「現在レビュー中の PR」は state.json の current_pr を内部参照する。 -STATE_PR=$INITIAL_PR - -# ROTATE_MODE は引数 --rotate-mode (default=light) から取得。light なら Step 6b で -# Agent (general-purpose) を起動して新 PR の title/body を生成する。 -ROTATE_MODE=${ROTATE_MODE:-light} - -# Step 0: state 初期化 / 再開 -eval "$("$SCRIPTS/state.py" init "$STATE_PR" \ - --max-rounds "$MAX_ROUNDS" --rotate-after "$ROTATE_AFTER" \ - ${ONLY:+--only "$ONLY"} \ - ${FOCUS:+--focus "$FOCUS"} \ - ${EXTRA_INSTRUCTIONS_FILE:+--extra-instructions-file "$EXTRA_INSTRUCTIONS_FILE"})" -# eval で TMP_DIR がセットされる。後続スクリプトに env として伝播させる。 -export CROSS_REVIEW_TMP_DIR="$TMP_DIR" -cd "$WORKTREE" - -while :; do - # Step 1: round 開始判定 (max_rounds 到達で exit 1) - eval "$("$SCRIPTS/state.py" start-round "$STATE_PR")" - - # Step 2: 並列レビュー - [ "$ONLY" != "gemini" ] && "$SCRIPTS/launch-codex.sh" "$STATE_PR" "$ROUND" - [ "$ONLY" != "codex" ] && "$SCRIPTS/launch-gemini.sh" "$STATE_PR" "$ROUND" - # 監視: 既定 timeout=7 分 / stall=3 分。失敗時は対象プロセスを kill して返す。 - "$SCRIPTS/monitor.py" "$STATE_PR" "${ONLY:-both}" || handle_review_failure $? - - [ "$ONLY" != "gemini" ] && "$SCRIPTS/state.py" read-result "$STATE_PR" codex - [ "$ONLY" != "codex" ] && "$SCRIPTS/state.py" read-result "$STATE_PR" gemini - - # Step 3: 判定 (0=approved/2=continue) - if "$SCRIPTS/state.py" judge "$STATE_PR"; then break; fi - - # Step 4: 振動検知 (4=oscillation) - "$SCRIPTS/state.py" check-oscillation "$STATE_PR" || [ $? -eq 2 ] || exit 4 - - # Step 5: 修正サブエージェント起動 (Agent tool) → $TMP_DIR/fix-pr-result.json - # - メインで Agent(subagent_type=general-purpose, ...) を呼ぶ。docs/02 参照 - # - tmp パスは launcher / monitor.py と同じく **STATE_PR ベース** で統一 - # Step 5 後段: fix 戻り値マージ + CI 分類 (3=code-fail で中断) - "$SCRIPTS/state.py" merge-fix "$STATE_PR" - - # Step 6: PR ローテーション判定 (0=rotate/2=keep)。state.json の current_pr を内部更新。 - if "$SCRIPTS/state.py" should-rotate "$STATE_PR"; then - # Step 6a: 旧 PR の素材 (title/body/isDraft + git log/diff stat) を dump - eval "$("$SCRIPTS/rotate-pr.sh" prepare "$STATE_PR")" - - # Step 6b: light モードのみ。**メインセッション側で Agent(subagent_type="general-purpose") を起動して** - # prepare.json を読ませ、現状の差分・実装を反映した title/body を - # $TMP_DIR/rotate-pr-newtext.json に書き出させる。 - # 詳細プロンプトは docs/02-fix-and-rotation.md Step 6b 参照。 - # squash モードでは Step 6b 不要。 - # - # ⚠ Bash 単体では Agent ツールを呼べない。下記のフローは「メイン会話セッション側で」 - # 実行される前提なので、bash の while ループそのものは pseudo-code として読み、 - # 実際には以下の 3 段階を **メインが順に駆動する** こと: - # (1) bash 側で `rotate-pr.sh prepare $STATE_PR` を実行 (これは普通の bash) - # (2) メインが Agent(...) を起動して newtext.json を書かせる (bash の外) - # (3) bash 側で `rotate-pr.sh execute $STATE_PR --mode light` を実行 - # 下記の if exit 10 は **誤ってメイン介在なしで Step 6c に進むことを防ぐガード** であり、 - # exit 10 を観測したらメインは Step 6b の Agent を起動し、newtext.json が - # 生成されてから **同じ STATE_PR で Step 6c (execute) を直接呼び直す**。 - # state.json は完全に再開可能 (prepare.json はそのまま再利用される)。 - NEWTEXT_JSON="$TMP_DIR/rotate-pr$STATE_PR-newtext.json" - if [ "$ROTATE_MODE" = "light" ] && [ ! -s "$NEWTEXT_JSON" ]; then - echo "⏸ light モード: メインセッションで Agent(general-purpose) を起動し" >&2 - echo " $NEWTEXT_JSON を生成してから rotate-pr.sh execute $STATE_PR --mode light を実行してください" >&2 - echo " (docs/02-fix-and-rotation.md Step 6b 参照 / 再開プロトコルは下記 '## 再開プロトコル')" >&2 - exit 10 - fi - - # Step 6c: 実行。NEW_PR / NEW_PR_URL / NEW_BRANCH を eval で取り込む。 - eval "$("$SCRIPTS/rotate-pr.sh" execute "$STATE_PR" --mode "$ROTATE_MODE")" - - "$SCRIPTS/state.py" set-current-pr "$STATE_PR" "$NEW_PR" - # NOTE: STATE_PR は変えない。次ループの scripts も $STATE_PR を渡す。 - fi -done - -# Step 7.5: 最終スイープ (必須) — どの終了経路 (approved / max_rounds / oscillation / -# error) でも、ループを抜けた直後に **メインが Agent(general-purpose) を起動** し、 -# /ndf:fix $STATE_PR を再実行して残った open review thread を全て解消する。 -# ⚠ bash 単体では Agent ツールを呼べないため、while ループを抜けたらメインが -# Step 7.5 の Agent を駆動し、$TMP_DIR/sweep-pr$STATE_PR-result.json を生成させる -# (プロンプトテンプレートは docs/02-fix-and-rotation.md Step 7.5)。 -# 最終 APPROVE ラウンドで投稿された minor/nit インラインコメントはループ内 fix を -# 経由しないため、ここで拾わないと PR 上に未解決スレッドが残る。 -# sweep 結果 (sweep-pr$STATE_PR-result.json) はメインが Step 8 の報告に折り込む。 - -# Step 8: 終了処理 (deferred nit + ラウンドサマリ) -"$SCRIPTS/state.py" report "$STATE_PR" -``` - -各ステップの内容と契約(state.json / result.json スキーマ等)の詳細は: - -- Step 0〜4 — [docs/01-state-and-review.md](docs/01-state-and-review.md) -- Step 5〜8 (最終スイープ Step 7.5 含む) — [docs/02-fix-and-rotation.md](docs/02-fix-and-rotation.md) - -## light モード rotation の再開プロトコル (exit 10 を観測した時) - -bash ループは Agent tool を呼べないため、light モードでは Step 6b の介入が必須。 -メインセッションはループ全体を 1 回の bash で完結させず、以下のように駆動する: - -1. **通常のループ実行** — Step 0〜6a まで bash で進めると、newtext.json が未生成の - ため `exit 10` で停止する。state.json には prepare.json までの状態が - 永続化されているので **そのまま再開可能**。 -2. **Step 6b (Agent 起動)** — メインが Agent(subagent_type=`general-purpose`) を - 起動し、prepare.json を読ませて `$TMP_DIR/rotate-pr$STATE_PR-newtext.json` を - 書き出させる (プロンプトテンプレートは docs/02-fix-and-rotation.md Step 6b)。 -3. **Step 6c (execute) を直接呼ぶ** — メインが bash で以下を実行: - - ```bash - eval "$("$SCRIPTS/rotate-pr.sh" execute "$STATE_PR" --mode light)" - "$SCRIPTS/state.py" set-current-pr "$STATE_PR" "$NEW_PR" - ``` - -4. **ループ再開** — Step 7 (次ラウンド) からループ全体を再開する。`STATE_PR` は - 不変なので、`start-round` 以降は通常通り進む。 - -> ⚠ exit 10 はエラーではなく **メイン介入待ちの一時停止シグナル**。final ステータスには -> 反映しない (中断扱いではない)。次回 round カウントにも影響しない。 - -## レビュー出力の制約 - -**目的**: PR 上に Resolve 義務を伴うインラインコメントを増やさない。 -**修正アクションを伴わない記述は一切出さない** ことを両 launcher プロンプトで強制する。 - -### 1. body 先頭 identifier prefix(必須) - -人間アカウントから AI が投稿するため、GitHub UI 上では誰のレビューか分からない。 -body 先頭に必ず以下を入れる: - -``` -## 🤖 cross-review | round 1 | codex | REQUEST_CHANGES -``` - -書式: `## 🤖 cross-review | round | | ` - -- ``: `codex` / `gemini` のいずれか -- ``: AI の本来の判定(`REQUEST_CHANGES` / `APPROVE` / `COMMENT`) - `posted_as` ではなく `intent` を書く - -### 2. インラインコメントの最小化(最重要) - -インラインコメントは GitHub 上で **Resolve 操作が必須** になるため、本当に直すものだけ作る: - -| 重要度 | インライン化 | 説明 | -|---|---|---| -| `critical` / `major` | ✅ する | 修正必須 | -| `minor` | ✅ する | 明らかな改善のみ。判断が割れるなら出さない | -| `nit` | ❌ **出さない** | 好み・スタイルはコメント化禁止。気になっても無視する | - -**1 インラインコメント = 1 修正アクション** を厳守。 -コメント本文は `[重要度 / カテゴリ] 修正提案` の 1 文で完結させ、 -コード引用ブロック(``` ... ```)や現状説明だけのコメントは作らない。 - -### 3. body(総評)に書かないこと - -- ❌ **「良い点」/「Strengths」/「Positives」/「評価できる点」セクション** — 一切書かない -- ❌ 個別ファイル・関数の褒め言葉 -- ❌ 「特に問題ありません」「概ね良好です」等の評価文 -- ❌ 対応不要な観察コメント(「〜のようです」「〜と思われます」止まり) - -body に書くのは **設計レベル・PR 横断の修正提案** のみ。 -書くことが無ければ body は `## 🤖 cross-review ...` の prefix 行 + 1 行サマリのみで良い。 - -### 4. event 判定 - -- `APPROVE` — 修正必須の指摘なし(minor 以下しか無い場合も APPROVE で良い) -- `REQUEST_CHANGES` — critical / major の指摘あり -- `COMMENT` — **基本使わない**。雑感だけの投稿は禁止 - -## CI failure の分類(誤中断防止) - -「CI 失敗 → 即 `final=error`」は乱暴。`scripts/state.py merge-fix` が -fix 戻り値ファイル (`$TMP_DIR/fix-pr-result.json`) を受け取った際に -`ci_failed_checks` を以下で分類する: - -| 分類 | パターン | 振る舞い | -|---|---|---| -| code-fail | `pint` / `larastan` / `phpstan` / `test` / `lint` / `type` / `build` / `ruff` / `eslint` / `tsc` / `mypy` | `final=error` で中断 (exit 3) | -| meta-only | `check_pr_requirements` / `assignees` / `reviewers` / `labels` / `meta` | `ci_note` に記録して継続 | -| 不明 | 上記以外 | 保守的に **code-fail 扱い** | - -PR メタデータ系の check(Assignees / Reviewers / Labels)は **継続**、 -pint / larastan / test / build などは **中断** を原則とする。 - -## アンチパターン - -- ❌ **修正をメインセッション内で行う** — context が一気に膨れる。必ずサブエージェント -- ❌ **AI に Markdown だけ返させる** — メインがパース・投稿する設計は禁物。AI 直接投稿 -- ❌ **nit を都度ユーザに問う** — ループ中は deferred 記録のみ。最終スイープ (Step 7.5) で Resolve -- ❌ **未解決スレッドを残したまま終了する** — approved/max_rounds 等いずれの終了経路でも - Step 7.5 の最終スイープを必ず実行し、open review thread 0 で終える。特に **最終 APPROVE - ラウンドの minor/nit インラインコメント**はループ内 fix を通らないため取りこぼしやすい -- ❌ **`max-rounds` なしで回す** — 無限ループの温床 -- ❌ **PR ローテーションを忘れる** — 100+ コメントの巨大 PR になる -- ❌ **light モードで Agent (general-purpose) 呼び出しを省略する** — newtext.json が無いと `rotate-pr.sh execute --mode light` はエラーで止まる。prepare → Agent → execute の 3 段は不可分 -- ❌ **light モードで新 PR の title/body に内部用語を漏らす** — 「round N」「rotated」「cross-review」「レビュー指摘で〜」等は禁止 (Agent プロンプトで明示禁止) -- ❌ **newtext.json に旧 PR の title/body をそのままコピーする** — 「現状の差分・実装を反映」が必須。古い説明が残ると後続 PR / 将来のレビュアーが混乱 -- ❌ **`rotate-pr.sh` 内から `claude` CLI を呼んで title/body を生成する** — 環境依存・コスト管理外。Agent tool でメイン側から呼ぶ -- ❌ **CI 失敗を一律で中断** — コード関連/メタチェックを分類(上記参照) -- ❌ **自分の PR に `REQUEST_CHANGES` で投稿** — 必ず 422。事前判定 + COMMENT ダウングレード -- ❌ **`gemini --yolo` だけで起動** — trusted directory で YOLO 無効化。`--skip-trust` 併用 -- ❌ **`pgrep -fa ` で完了判定** — gemini は long prompt が引数に乗り検知失敗。pidfile 必須 -- ❌ **sentinel 単独で完了判定** — codex がクラッシュすると永遠に出ない。`monitor.py` の多軸判定 (pidfile / sentinel / 早期エラー / stall / hard timeout / result.json) を使うこと -- ❌ **タイムアウトなしで wait** — ハング検知不能。`monitor.py` の hard timeout (30 分既定) + stall timeout (10 分既定) を必ず効かせる -- ❌ **EARLY_ERROR の曖昧パターンで kill する** — 行頭の生 `Error:` / `Traceback` は codex がレビュー対象 diff の test コード片を echo するケースで誤検知する。明確な致命 (auth / quota / sandbox / HTTP 401-403-429 / gemini の YOLO 降格) **のみ** kill 対象とし、曖昧パターンは警告ログに留める。誤検知が再発する場合は `--no-early-error` / `MONITOR_NO_EARLY_ERROR=1` で検知自体を無効化する (sentinel / result.json / timeout で十分判定可能) - -## monitor.py が誤って kill する場合の手順 - -`monitor.py` が EARLY_ERROR で codex / gemini を即時 kill してしまい、`result.json` が -生成されないケースは以下で切り分け・回避できる: - -1. **err.log の冒頭を確認**: 検知パターン (`fatal_err` の `early error (fatal) in err.log: ...`) が - 本当に致命なのか、それとも diff body の echo / config validation 警告なのかを判別 - - **v4.11.0 で benign 自動判定を強化**: `_match_is_quoted()` が backtick / 「」 に加え - **ダブル/シングルクォート文字列リテラル** (`"quota exceeded: ..."`) を、`EARLY_ERROR_BENIGN` - が **grep 形式のソース引用行** (`path/to/file.py:22: `) を自動で benign 扱いする。 - codex が tests/*.py 等のテスト用文字列 (`"quota exceeded"`, `"sandbox error"`) を - レビュー中に echo しても誤 kill しなくなった(旧版で PR #23 round 2 に発生した事例) -2. **gemini の `Error in: mcpServers.` 警告**: `.gemini/settings.json` に `disabled: false` - 等の非互換キーがあると毎回出る。`launch-gemini.sh` の sanitize ロジック (v4.7.2+) で - 自動退避するため、最新版にアップデートすれば解消する -3. **誤検知が継続する場合**: `monitor.py --no-early-error` (もしくは `MONITOR_NO_EARLY_ERROR=1` - 環境変数) で EARLY_ERROR 検知自体を無効化し、hard timeout / stall / sentinel / result.json - のみで判定するモードに切り替える -4. **新しい致命パターンを観測した場合**: `EARLY_ERROR_FATAL` に追記する (PR で plugin に反映)。 - 曖昧パターンは `EARLY_ERROR_WARN` 側に置き、kill 対象にはしない -- ❌ **fix サブエージェントが Resolve をスキップ** — reply だけでは未対応扱い。Resolve まで実行 -- ❌ **review body に identifier prefix を付け忘れる** — GitHub UI 上で誰のレビューか不明になる - -## メイン context 節約の工夫 - -1. **大きいファイルはメイン context に載せない**: payload / err.log / diff は - すべて `$TMP_DIR/` (= `state.py _tmp_dir()` の解決先) に置き、メインは - state.json と result.json だけ読む -2. **サブエージェント分離**: 修正は別 context window で実行 -3. **PR ローテーション**: 1 PR あたりの会話履歴を抑える -4. **AI 直接投稿**: 中間ペイロードがメインを通らない -5. **state.json で再開可能**: メインが落ちても次回起動時に続きから - -## 作業完了報告(必須) - -ループ終了後、メインからユーザへの報告: - -- **最終ステータス**: `approved` / `max_rounds` / `oscillation` / `error` -- **総ラウンド数 / PR 数**: 例: `5 rounds / 2 PRs (rotated 1 回)` -- **PR 履歴**: 各 PR 番号 + closed/open 状態 + round 数 -- **各ラウンドのサマリ表**: - - | round | PR | codex | gemini | fix | CI | - |---|---|---|---|---|---| - | 1 | #123 | REQ (5) | REQ (3) | abc123 (5 fixed, 2 deferred) | ✅ | - | 2 | #123 | REQ (2) | APP | def456 (2 fixed) | ✅ | - | 3 | #145 | APP | APP | — | — | - -- **最終スイープ結果** (Step 7.5): `sweep-pr-result.json` の `resolved` / - `fixed_in_sweep` / `remaining_open`。**`remaining_open` は 0 が正常**(残 open - thread あり = 取りこぼし)。0 にできなかった場合は理由を明記 -- **残 deferred nit リスト**(Step 7.5 で Resolve 済み。再対応が要るものがあれば参考列挙) -- **rejected 件数**(bot 誤指摘で却下したもの) -- **最終 PR URL** - -詳細は PR 上のインラインコメントと state.json に残っているため、本報告では -繰り返さない。 - -## 関連 - -- `/ndf:review` — 単発レビュー(AI 直接投稿対応) -- `/ndf:fix` — 修正対応(サブエージェント起動対応) -- `/ndf:codex` — codex CLI 呼び出し手順 -- `/ndf:gemini` — gemini CLI 呼び出し手順 -- `/ndf:resolve-pr-comments` — Resolve Conversation の詳細 -- `general-purpose` エージェント — fix 実行用サブエージェント diff --git a/plugins/ndf/skills-codex/cross-review/docs/01-state-and-review.md b/plugins/ndf/skills-codex/cross-review/docs/01-state-and-review.md deleted file mode 100644 index de10fda..0000000 --- a/plugins/ndf/skills-codex/cross-review/docs/01-state-and-review.md +++ /dev/null @@ -1,254 +0,0 @@ -# 01: 状態管理 + レビュー実行 (Step 0〜4) - -`SKILL.md` 本体から呼び出される **状態ファイル初期化 / ラウンド開始 / -並列レビュー / 判定 / 振動検知** までの詳細手順。 - -主要処理は `scripts/` 配下のコマンドに切り出し済み: - -| script | 役割 | -|---|---| -| `scripts/state.py init` | Step 0 — state 初期化 / 再開 + プリチェック | -| `scripts/state.py start-round` | Step 1 — round 開始判定 | -| `scripts/launch-codex.sh` / `scripts/launch-gemini.sh` | Step 2 — review launcher | -| `scripts/monitor.py` | Step 2 — codex/gemini プロセス多軸監視 | -| `scripts/wait-review.sh` | Step 2 — `monitor.py` の薄ラッパ(互換用) | -| `scripts/state.py read-result` | Step 2.5 — result.json マージ | -| `scripts/state.py judge` | Step 3 — intent ベース pass 判定 | -| `scripts/state.py check-oscillation` | Step 4 — 振動検知 | - -このドキュメントは **state.json スキーマと AI への入出力契約** を一次資料として残す。 -スクリプト側の挙動はソースを直接参照のこと。 - -## 状態ファイル - -`$TMP_DIR/cross-review-pr<番号>-state.json`: - -```json -{ - "started_at": "2026-05-12T...", - "max_rounds": 12, - "rotate_after": 8, - "only": null, - "current_pr": 123, - "worktree_path": "/tmp/ndf-worktrees/owner--name/pr123", - "repo": "owner/name", - "head_branch": "feature/foo", - "base_branch": "main", - "pr_author": "someone", - "is_own_pr": false, - "event_downgrade": false, - "pr_history": [ - {"pr": 123, "opened_at": "...", "closed_at": null, "rounds": 2} - ], - "rounds": [ - { - "round": 1, - "pr": 123, - "started_at": "...", - "codex": {"intent": "REQUEST_CHANGES", "posted_as": "COMMENT", - "comments": 5, "review_url": "...", - "by_severity": {"critical": 0, "major": 3, "minor": 2, "nit": 0}}, - "gemini": {"intent": "REQUEST_CHANGES", "posted_as": "COMMENT", - "comments": 3, "review_url": "...", - "by_severity": {"critical": 0, "major": 2, "minor": 1, "nit": 0}}, - "fix": {"commit": "abc1234", "fixed": 6, "deferred": 2, "rejected": 0, - "resolved_threads": 4, "ci": "SUCCESS", "ci_note": null}, - "ended_at": "..." - } - ], - "deferred_nits": [ - {"pr": 123, "round": 1, "path": "src/foo.py", "line": 42, "severity": "nit", - "summary": "...", "comment_url": "..."} - ], - "final": null -} -``` - -`final` 値: `approved` / `max_rounds` / `oscillation` / `error` - -### 重要なフィールド - -- `worktree_path` — 並行セッションとの分離。サブエージェントへの cwd 指示にも使う -- `is_own_pr` / `event_downgrade` — 自分の PR の場合 `REQUEST_CHANGES → COMMENT` 強制ダウングレード -- `rounds[].codex.intent` — AI の本来判定。**ループ判定はこれを見る** -- `rounds[].codex.posted_as` — GitHub に実際に送った event。`is_own_pr=true` なら `COMMENT` になる -- `rounds[].fix.resolved_threads` — `resolveReviewThread` で resolve した**件数(int)**。 - ここ (state.json 側) は int だが、fix サブエージェントが返す戻り値ファイル - (`fix-pr-result.json`) 側の `resolved_threads` は **list**(`docs/02` の戻り値スキーマ参照)。 - `state.py merge-fix` が fix結果の list を `len()` して state.json に int で保存する。 - 混同して fix結果側に int を書くと過去 `merge-fix` が落ちていたため、現在は int/list 両受理 -- `rounds[].fix.ci_note` — コード無関係の CI 失敗時に「Assignees 未設定」等の理由を残す - -## Step 0: 準備 + 既存 state 引き継ぎ - -```bash -PLUGIN_ROOT="${PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}" -SCRIPTS="$PLUGIN_ROOT/skills-codex/cross-review/scripts" # or 直接の絶対パス - -# state 初期化 / 再開(プリチェック・worktree 作成・既存コメントスナップショットを内部実行) -eval "$("$SCRIPTS/state.py" init "$STATE_PR" \ - --max-rounds "$MAX_ROUNDS" --rotate-after "$ROTATE_AFTER" \ - ${ONLY:+--only "$ONLY"} \ - ${FOCUS:+--focus "$FOCUS"} \ - ${EXTRA_INSTRUCTIONS_FILE:+--extra-instructions-file "$EXTRA_INSTRUCTIONS_FILE"})" - -# eval で取り込まれる変数: PR, WORKTREE, REPO, HEAD_BRANCH, BASE_BRANCH, -# IS_OWN_PR, EVENT_DOWNGRADE, RESUMED -cd "$WORKTREE" -``` - -`state.py init` が内部で行う処理: - -1. 既存 state.json があり `final == null` なら再開 -2. 自分の PR 判定(`gh api user` と `gh pr view --json author` を比較) -3. worktree 作成(`/--/pr`。`` は `NDF_WORKTREE_BASE` env > `<システム tmpdir>/ndf-worktrees` の優先順で解決。既存パスが現リポジトリの登録済み worktree でなければ `.stale-` に退避して作り直す。実 path は state.json の `worktree_path` を参照) -4. 既存コメントスナップショット (`fix/scripts/fetch-pr-comments.sh` で 3 ソース一括取得) → `$TMP_DIR/cross-review-pr-existing-comments.txt` -5. state.json 書き出し - -**重要**: 以降の全ステップで `cd $WORKTREE` を強制。 -サブエージェント(fix)を起動するときも、prompt 内で worktree path を明示する。 - -## Step 1: Round 開始判定 - -```bash -eval "$("$SCRIPTS/state.py" start-round "$STATE_PR")" -# eval で取り込まれる変数: ROUND, ROUND_IN_PR, PR, MAX_ROUNDS, ROTATE_AFTER -``` - -`state.py start-round` は `max_rounds` 超過なら `final=max_rounds` を書いて exit 1。 -それ以外は新しい round エントリを state.rounds に push して KEY=VALUE を吐く。 - -## Step 2: codex / gemini 並列レビュー(AI 直接投稿) - -**要点**: メインは launcher を **並列バックグラウンド** で起動するだけ。 -各 AI が `gh api` で投稿し `$TMP_DIR/-review-pr-result.json` に -サマリを書く。**ペイロード本体はメイン context に載せない**。 - -### 2.1 launcher 起動 + monitor - -```bash -[ "$ONLY" != "gemini" ] && "$SCRIPTS/launch-codex.sh" "$STATE_PR" "$ROUND" -[ "$ONLY" != "codex" ] && "$SCRIPTS/launch-gemini.sh" "$STATE_PR" "$ROUND" - -# monitor.py が多軸で完了判定。exit code で失敗種別を分岐。 -if ! "$SCRIPTS/monitor.py" "$STATE_PR" "${ONLY:-both}"; then - case $? in - 2) echo "❌ timeout" ;; # hard timeout 超過 - 3) echo "❌ no result" ;; # プロセス終了したが result.json 未生成 - 4) echo "💥 early error" ;; # err.log に致命的パターン - 5) echo "🛑 stalled" ;; # 進捗ログ更新なし - 6) echo "❓ pidfile bad" ;; # 起動失敗 / 不正 - esac - # ラウンドを失敗マークしてリトライ or 中断(state.py side で判断) -fi -``` - -#### `monitor.py` の多軸監視 - -| 軸 | 内容 | -|---|---| -| pidfile + `kill -0` | プロセス生存確認。alive 確認後に `/proc//cmdline` で agent 名一致も検証 (PID 再利用対策)。**プロセスが既に死んでいる場合は result.json の有無のみで OK 判定**する (死亡直後 cmdline 不一致で誤検知しないため) | -| codex sentinel | err.log に `^tokens used$` 出現で正常完了マーク | -| early-error | **行頭限定** で `^Error:` / `^FATAL:` / `^panic:` / `^Traceback ` / `^HTTP/1.1 401\|403\|429` / `^Approval mode overridden to "default"` / `^Authentication failed` / 「quota exceeded」「rate limit exceeded」「API key not found/missing/invalid」「sandbox error」を含む行を検出 (diff/doc 引用文中の同語句は誤検知しないよう anchor + benign フィルタ併用) | -| stall timeout | err.log + stdout.log + progress.log の合計サイズが一定時間変化しなければ STALLED で中断。既定は **agent 別** (codex=**180s** / gemini=**480s**)。gemini は err.log がほぼ無音のため大きめに取る。codex 側既定は不変。上書き方法: CLI `--stall-timeout` (明示優先) > env `MONITOR_STALL_` (per-agent) > env `MONITOR_STALL` (両 agent 共通) > agent 別ビルトイン | -| hard timeout | 既定 **7 分**。`--timeout` or `MONITOR_TIMEOUT` env で上書き | -| progress.log heartbeat | launcher が任意で `-review-pr-progress.log` への短いフェーズマーカー出力を要求し、monitor が最終行を stderr の heartbeat に表示する。内部推論ではなく `scan` / `analyze` / `post` / `done` などの監視用ステータスだけを出す | -| result.json 存在 | プロセス終了後、result.json が無ければ NO_RESULT (exit 3) | -| **result.json + age fallback** | sentinel を持たない agent (gemini) 向け。プロセスが alive のまま result.json の mtime が **30 秒以上前**なら完了とみなし kill → OK。gemini が MCP 切断待ち等でハングするケースに対応 (codex は sentinel チェックが先に発火するため影響なし) | -| **失敗時 kill** | TIMEOUT / STALLED / EARLY_ERROR / PIDFILE_BAD で返るときは対象プロセスに SIGTERM → 3 秒後 SIGKILL。残存プロセスが後から `gh api` 投稿や result.json 書き込みを行うのを防ぐ | - -> ⚠ **罠**: `nohup ... &` でラッパーシェルは即終了し、ハーネスから -> 「タスク完了」通知が飛んでくる。これに惑わされず、`monitor.py` で -> 実プロセスの完了を pidfile / sentinel で確認すること。 -> -> ⚠ **`pgrep -fa ` で完了判定しない**: gemini は long `-p` プロンプトを -> 引数に持つため、`grep` のキーワード選定で誤検知する。**pidfile 必須**。 -> -> ⚠ **sentinel 単独で完了判定しない**: codex がクラッシュすると `tokens used` が -> 永遠に出ない。`monitor.py` は sentinel と pidfile/result.json/err.log を併用する。 -> -> ⚠ **Docker 環境ではゾンビプロセスに注意**: `nohup ... & disown` で起動した -> プロセスは、終了後にゾンビ化する (PID 1 が proper init でない場合)。 -> `monitor.py` は `/proc//status` でゾンビを検出して dead 扱いする。 -> **推奨: Docker 実行時に `--init` フラグを付ける** (tini が PID 1 になりゾンビを reap する)。 - -### 2.2 AI への入出力契約(両 launcher 共通) - -launcher が生成するプロンプトに以下を強制している: - -- **headRefOid (commit_id) を明示**: AI が自前で取得すると baseRefOid を誤って入れる事故が多発 -- **作業 worktree の絶対パス**: 「ファイル読み取りは必ず worktree 配下の絶対パスを使う」(実 path は state.json の `worktree_path` を参照。`` は `NDF_WORKTREE_BASE` env > `<システム tmpdir>/ndf-worktrees` の優先順で解決) -- **event ダウングレード警告**: `event_downgrade=true` のときは payload の `event` を `COMMENT` に -- **既存コメント差分**: `$TMP_DIR/cross-review-pr-existing-comments.txt` を読んで重複指摘禁止 -- **自動レビュー観点**: GitHub API の `pulls//files --paginate` で変更ファイルを全件取得して分類し、`common` / `docs_only` / `code` / `db_migration` / `test` / `dependency` / `config_ci` / `api_contract` / `auth_security` / `frontend` / `performance` / `deletion_rename` / `generated` / `i18n` / `infra` の該当テンプレートを state.json の `auto_review_instructions` に保存する -- **手動追加レビュー観点**: `--focus` / `--extra-instructions-file` が指定されていれば state.json の `manual_extra_review_instructions` に保存し、自動テンプレートの後ろに連結した `review_instructions` を codex / gemini 両 launcher が同じ「追加レビュー観点」セクションとしてプロンプトに差し込む -- **進捗マーカー**: Gemini には `$TMP_DIR/gemini-review-pr-progress.log` へ短いフェーズ名を追記させ、monitor の heartbeat で表示する。内部推論や長文説明は書かせない -- **review body 先頭 prefix**: - ``` - ## 🤖 cross-review | round | | - ``` - `` は **本来の intent**(`posted_as` ではない)。 - 例: 自分PR で REQUEST_CHANGES を COMMENT にダウングロードしても、prefix は `REQUEST_CHANGES` のまま。 -- **出力禁止事項**(SKILL.md「レビュー出力の制約」と一致): - - 「良い点」「Strengths」などの褒めセクションを body に書かない - - 修正アクションを伴わないインラインコメントは作らない(nit はインライン化しない) - - コード引用のみで指摘内容が無いコメント禁止 - - 雑感だけの `event=COMMENT` 投稿禁止(直すべき点が無ければ `APPROVE`) - -### 2.3 AI が書き出すファイル契約 - -各 launcher は AI に以下 2 ファイルの書き出しを指示する: - -| ファイル | 内容 | -|---|---| -| `$TMP_DIR/-review-pr-result.json` | `{event, posted_as, comments_count, review_url, by_severity}` のサマリ | -| `$TMP_DIR/-review-pr-round-payload.json` | `{comments: [{path, line, body, severity}, ...]}` 振動検知用 | - -`/ndf:review` の result.json 出力規約に `posted_as` フィールドを含むこと -(自分PR ダウングレード時に GitHub に実際送った event。デフォルトは `event` と同値)。 - -### 2.4 result.json を state にマージ - -```bash -[ "$ONLY" != "gemini" ] && "$SCRIPTS/state.py" read-result "$STATE_PR" codex -[ "$ONLY" != "codex" ] && "$SCRIPTS/state.py" read-result "$STATE_PR" gemini -``` - -`state.rounds[-1].` に `intent / posted_as / comments / review_url / by_severity` を分離保存する。 - -## Step 3: 判定(intent ベース) - -```bash -if "$SCRIPTS/state.py" judge "$STATE_PR"; then - : # exit 0 = approved。ループ終了。 -elif [ $? -eq 2 ]; then - : # exit 2 = continue → Step 5 (fix) -else - exit 1 -fi -``` - -**判定ロジック**: - -- `APPROVE` / `SKIP` は pass -- `COMMENT` は `by_severity.critical == 0 && major == 0` のみ pass(軽微な指摘のみなら通す) -- `--only` 指定時は反対側を SKIP 扱い -- ループ収束判定は **必ず `intent`** を見る(`posted_as` ではない) - -自分の PR で `REQUEST_CHANGES → COMMENT` にダウングレード投稿していても、 -intent が `REQUEST_CHANGES` なら継続する。 - -## Step 4: 振動検知 - -```bash -if "$SCRIPTS/state.py" check-oscillation "$STATE_PR"; then - : # ここには来ない(成功は exit 2 = continue) -elif [ $? -eq 4 ]; then - exit 4 # final=oscillation で中断 -fi -``` - -各ラウンドの `$TMP_DIR/-review-pr-round-payload.json` から -`path:line` を抽出し、前ラウンドとの重複率を計算。**50% 以上重複で中断**。 - -PR ローテーション直後 (`round_in_pr < 2`) はスキップ。 diff --git a/plugins/ndf/skills-codex/cross-review/docs/02-fix-and-rotation.md b/plugins/ndf/skills-codex/cross-review/docs/02-fix-and-rotation.md deleted file mode 100644 index aaaecc3..0000000 --- a/plugins/ndf/skills-codex/cross-review/docs/02-fix-and-rotation.md +++ /dev/null @@ -1,384 +0,0 @@ -# 02: 修正 (Step 5) + PR ローテーション (Step 6) + 終了処理 (Step 8) - -主要処理は `scripts/` 配下に切り出し済み: - -| script | 役割 | -|---|---| -| (Agent) | Step 5 — 修正サブエージェント起動(メインからの責務) | -| `scripts/state.py merge-fix` | Step 5 後段 — fix 戻り値マージ + CI 分類 | -| `scripts/state.py should-rotate` | Step 6 — rotate 要否判定 | -| `scripts/rotate-pr.sh prepare` | Step 6a — 旧 PR の素材を `rotate-pr-prepare.json` に dump | -| (Agent) | Step 6b — light モードのみ。新 PR の title/body を再生成して `rotate-pr-newtext.json` に書き出し | -| `scripts/rotate-pr.sh execute` | Step 6c — 旧 PR close + 新 PR 作成 (light は同ブランチ / squash は新ブランチ) | -| `scripts/state.py set-current-pr` | Step 6 — rotation 後の state 更新 | -| `scripts/state.py report` | Step 8 — deferred nit + ラウンドサマリ | - -## Step 5: 修正 — **必ずサブエージェント経由** - -**メインセッションでは修正コードを書かない。** `/ndf:fix` を -`general-purpose` サブエージェントで起動する。 - -**サブエージェントの責務(必須 6 点)**: - -1. critical / major / minor の修正コミット -2. 修正テストの追加・実行 -3. 修正対象の thread に **reply 投稿** + **`resolveReviewThread` で Resolve** -4. nit / 判断が割れる minor は **修正せず deferred 記録**(reply は `[deferred / nit]` ラベル付き、Resolve しない) -5. **PR レベルの Summary コメントを `gh pr comment` で投稿**(対応件数 / 重要度別 / deferred 件数 / rejected 件数 / commit SHA を含む) -6. 戻り値ファイル `$TMP_DIR/fix-pr-result.json` を必ず書き出す - (`$TMP_DIR` は env `CROSS_REVIEW_TMP_DIR` > `/.cross_review/` の順で解決。 - 詳細は `scripts/state.py _tmp_dir()` 参照。`/tmp/` 直書きでも `state.py merge-fix` は legacy fallback で拾う) - -> ⚠ inline thread への reply + Resolve **だけでは不十分**。PR ページの -> conversation タブに表示される **PR レベルコメント** がレビュアーへの -> サマリ通知として必須(`/ndf:fix` SKILL.md の手順 7 で規定)。 -> サブエージェント起動プロンプトでも明示的に指示すること。 - -### サブエージェント起動例 - -```python -Agent( - subagent_type="general-purpose", - description=f"Fix PR #{PR} (round {ROUND})", - prompt=f""" -/ndf:fix {PR} --defer-nit を実行してください。 - -**作業ディレクトリ厳守**: cd {WORKTREE_PATH} で作業すること。 -別セッションが /work/ 側で並行作業している可能性があり、 -worktree 外を触ると競合します。 - -## コンテキスト -- リポジトリ: {OWNER_REPO} -- PR: #{PR} (round {ROUND_IN_PR}/{ROTATE_AFTER}) -- worktree: {WORKTREE_PATH} -- ブランチ: {HEAD_BRANCH} -- ベース: {BASE_BRANCH} -- headRefOid: {HEAD_OID} -- 前ラウンドのレビュー結果: - - codex review: {CODEX_REVIEW_URL} - (intent={CODEX_INTENT}, posted_as={CODEX_POSTED_AS}, {CODEX_COMMENT_COUNT}件) - - gemini review: {GEMINI_REVIEW_URL} - (intent={GEMINI_INTENT}, posted_as={GEMINI_POSTED_AS}, {GEMINI_COMMENT_COUNT}件) -- 既存コメントスナップショット: $TMP_DIR/cross-review-pr{PR}-existing-comments.txt - -## ポリシー -- 重要度ラベルは **AI agent の付与を鵜呑みにせず**、コードを読んで独自に再判定する -- critical / major は自動修正 -- minor / nit のうち **パフォーマンス・可読性・重複コード排除** に該当するものは修正 - (特にトータル行数が減る方向は積極実施 / +30 行を超えそうなら deferred + ユーザ問い合わせ) -- それ以外の nit は deferred として記録のみ(修正しない、Resolve しない) -- bot 指摘が誤読していたら修正せず reply で説明(rejected として記録、Resolve しない) -- **重複指摘(codex/gemini が同じ箇所を別 thread で指摘)は全 thread に reply + Resolve** -- PR テスト範囲外の **flaky テストも見つけ次第このループで修正**(放置はリポジトリ品質を劣化させる) - -## 必須実行手順(順序厳守) - -1. PR コメント取得 (3 ソース): `fix/scripts/fetch-pr-comments.sh {OWNER_REPO} {PR}` でインラインコメント / レビュー body / PR レベルコメントを一括取得 -2. 重要度を独自再判定(AI agent のラベルは参考値) -3. CI 状態スナップショット: `gh pr checks {PR} --json name,state` (**完了待ちはしない**、PENDING は無視して FAILURE のみ修正対象に取り込む) -4. critical/major + 該当 minor/nit の修正コミット(worktree 内のみ) -5. `./pint-changed.sh && ./larastan-changed.sh` 等の品質チェック -6. push: `git push origin {HEAD_BRANCH}` (--force / --no-verify 禁止) -7. **CI 再実行は待たない**(push 後の `--watch` 等は行わない、`ci_status` は push 時点での既知失敗のみ反映) -8. **各 thread に reply 投稿**: - - 修正済み: 「対応しました — <ファイル>:<行> で〇〇 (commit )」 - - deferred: 「[deferred / nit] 後続 PR で対応予定」 - - rejected: 「bot 指摘は誤読です — 理由: ...」 -9. **修正済み thread を `resolveReviewThread` で Resolve**: - ```bash - # thread_id は GraphQL で取得 - gh api graphql -f query=' - query {{ repository(owner:"...", name:"...") {{ - pullRequest(number: {PR}) {{ reviewThreads(first:100) {{ - nodes {{ id isResolved path line }} - }} }} - }} }}' - # 修正済みのみ resolve - gh api graphql -f query=' - mutation($id: ID!) {{ - resolveReviewThread(input: {{threadId: $id}}) {{ thread {{ isResolved }} }} - }}' -f id="$THREAD_ID" - ``` - - deferred / rejected の thread は **Resolve しない** -10. **PR レベル Summary コメントを投稿**(必須・inline reply とは別物): - ```bash - gh pr comment {PR} --body "$(cat <<'EOMD' - ## 🔧 /ndf:fix サマリ (round N) - - 対応件数: critical=X / major=Y / minor=Z (合計 N 件) - deferred: D 件 / rejected: R 件 - commit: - CI: SUCCESS | FAILURE | NONE - - ### 詳細 - - 各 thread の対応概要(行リンク付き) - EOMD - )" - ``` - - inline reply + Resolve だけでは「PR ページの Conversation タブ」に - まとめが出ず、レビュアー視点で見落とされる。**必ず投稿する** -11. 戻り値ファイル書き出し(下記フォーマット)。`summary_comment_url` には - 手順 10 の URL を入れる - -## 戻り値ファイル $TMP_DIR/fix-pr{PR}-result.json - -```json -{{ - "pr": {PR}, - "fix_commit": "abc1234", - "ci_status": "SUCCESS" | "FAILURE" | "PENDING", - "ci_failed_checks": [], - "fixed_count": 6, - "by_severity": {{"critical": 0, "major": 4, "minor": 2, "nit": 0}}, - "resolved_threads": [ - {{"thread_id": "PRRT_...", "comment_id": 123, "path": "...", "line": 42}} - ], - "deferred": [ - {{"thread_id": "...", "path": "...", "line": 31, "severity": "nit", - "summary": "...", "comment_url": "..."}} - ], - "rejected": [ - {{"thread_id": "...", "summary": "...", "reason_for_rejection": "..."}} - ] -}} -``` - -> **重要**: `resolved_threads` / `deferred` / `rejected` は必ず **list(配列)** で返すこと。 -> **件数(int) を書かないこと**(例: `"resolved_threads": 3` は誤り。`[]` 形式で返す)。 -> `state.py merge-fix` がこの list を `len()` して state.json には件数(int)で保存する。 -> int を書くと過去 `merge-fix` が `TypeError` で落ちていた(現在は後方互換で int も受理するが、 -> 正は list)。対応した thread が無い場合は空配列 `[]` を返す。 -""", -) -``` - -### Step 5 後段: fix 戻り値マージ + CI 分類 - -```bash -if "$SCRIPTS/state.py" merge-fix "$STATE_PR"; then - : # exit 0 = continue -elif [ $? -eq 3 ]; then - exit 3 # final=error(コード関連 CI 失敗 or fix 戻り値ファイル欠落) -fi -``` - -`state.py merge-fix` が内部で行う処理: - -1. `$TMP_DIR/fix-pr-result.json` を読んで `state.rounds[-1].fix` にマージ -2. `deferred` を `state.deferred_nits` に追記 -3. **CI 失敗の分類**: - - code-fail (`pint` / `larastan` / `phpstan` / `test` / `lint` / `type` / `build` / `ruff` / `eslint` / `tsc` / `mypy`): `final=error` で中断 (exit 3) - - meta-only (`check_pr_requirements` / `assignees` / `reviewers` / `labels` / `meta`): `ci_note` に記録して継続 - - 不明: 保守的に code-fail 扱い - -**例**: `check_pr_requirements`(Assignees 未設定)はループ継続、 -`laravel/pint` や `phpstan` の失敗は即中断してユーザ判断。 - -## Step 6: PR ローテーション (prepare → Agent → execute の 3 段) - -`rotate-pr.sh` は **light モード (default) と squash モード (opt-in)** を持つ。 -両者ともメインからは `prepare → (light のみ Agent) → execute` の 3 段で呼ぶ。 - -```bash -if "$SCRIPTS/state.py" should-rotate "$STATE_PR"; then - # Step 6a: 旧 PR の素材 dump (title / body / isDraft / git log / git diff --stat) - eval "$("$SCRIPTS/rotate-pr.sh" prepare "$STATE_PR")" - - # Step 6b: light モードのみ。Agent(subagent_type="general-purpose") で - # 現状の差分・実装を反映した新 title/body を生成し、 - # $TMP_DIR/rotate-pr-newtext.json に書き出させる。 - # (squash モードでは Step 6b は不要) - - # Step 6c: 実行 (NEW_PR / NEW_PR_URL / NEW_BRANCH を取り込む) - eval "$("$SCRIPTS/rotate-pr.sh" execute "$STATE_PR" --mode "$ROTATE_MODE")" - - "$SCRIPTS/state.py" set-current-pr "$STATE_PR" "$NEW_PR" - # NOTE: STATE_PR は **絶対に変えない**。次ループの scripts も $STATE_PR で呼ぶ。 -fi -``` - -`should-rotate` は `round_in_pr >= rotate_after && total_rounds < max_rounds` で -exit 0 を返す(rotate 要)。それ以外は exit 2(keep)。 - -### Step 6a: `rotate-pr.sh prepare ` - -state.json から旧 PR / worktree を解決し、以下の素材を 1 つの JSON に dump する: - -```json -{ - "state_pr": 217, - "old_pr": 217, - "old_pr_url": "https://github.com/.../pull/217", - "worktree_path": "/tmp/ndf-worktrees/owner--name/pr217", - "head_branch": "feature/...", - "base_branch": "release/...", - "is_draft": true, - "round_in_pr": 5, - "old_title": "...", - "old_body": "...", - "git_log": "abc1234 メッセージ\n...", - "git_diff_stat": " path/to/file | 12 +-\n ..." -} -``` - -ファイル: `$TMP_DIR/rotate-pr-prepare.json`。 -stdout にも `OLD_PR=` / `HEAD_BRANCH=` / `BASE_BRANCH=` / `IS_DRAFT=` / `PREPARE_JSON=` を出すので -`eval` で取り込める。 - -### Step 6b: Agent (general-purpose) で新 title/body を生成 (light モードのみ) - -メインセッションから以下のように Agent を起動する。プロンプトは -**書いて良いこと / 禁止事項** を必ず明示する -(外側の prompt フェンスは内側に ```json を含むため 4 連バッククォートで囲む): - -````python -Agent( - subagent_type="general-purpose", - description=f"Generate light-rotation PR text for PR #{OLD_PR}", - prompt=f""" -PR rotation の light モードで作成する新 PR の title / body を生成してください。 - -## 素材 -- prepare.json: $TMP_DIR/rotate-pr{STATE_PR}-prepare.json - - 元 PR の title / body / git log $BASE..HEAD / git diff --stat -- 必要なら worktree 内のファイルを直接読んで実装内容を確認してよい - (worktree: {WORKTREE_PATH}) - -## 出力ファイル -$TMP_DIR/rotate-pr{STATE_PR}-newtext.json に JSON で書き出してください: - -```json -{{ - "title": "新 PR の title (元 PR の title をそのままコピーしない。現状の実装を反映)", - "body": "新 PR の body (Markdown)。以下のセクションを含む:\\n## 何のために\\n## 何を\\n## Test plan" -}} -``` - -## 書いて **良い** こと -- 何のために (背景・動機) — 元 PR の背景セクションは再利用可 -- 何を (変更内容) — 現在のブランチの実態を git log / git diff から反映 -- Test plan — 元 PR から継承可 - -## 書いて **はいけない** こと (内部用語の漏洩防止) -- 「round N で〜」「cross-review で〜」「レビュー指摘で〜」 -- 「(rotated)」のような automated suffix -- 「fix された問題」の列挙 / レビューサイクルの存在自体への言及 -- 「旧 PR」「巻き直し」等の rotation 内部用語 - -PR を読む人は cross-review の存在を意識しないため、最終 PR を初めて見る読者向けに -書く。元 title / body をそのままコピーするのは **禁止** (現状の実装を反映)。 -""", -) -```` - -> ⚠ `rotate-pr.sh` 内部から `claude` / `codex` / `gemini` CLI を直接呼んで生成 -> させてはならない (環境依存・コスト管理外)。**メイン側の Agent tool で行う**。 - -### Step 6c: `rotate-pr.sh execute --mode light|squash` - -`--mode` で実際の rotation を実行する: - -| mode | 振る舞い | -|---|---| -| `light` (default) | prepare.json と newtext.json を読み、**同ブランチ・同 base** で旧 PR を close → 新 PR を作成。`is_draft=true` なら新 PR も Draft で作る。title/body は newtext から流す | -| `squash` (opt-in) | `-rHHMMSS` の新ブランチを作って `git reset --soft origin/$BASE` で squash 統合 → 旧 PR close → 新 PR (`(rotated)` suffix + automated body) | - -stdout には両モードとも以下を KEY=VALUE で出す: - -- `NEW_PR=` -- `NEW_PR_URL=` -- `NEW_BRANCH=` (light モードでは元ブランチと同じ) - -`state.py set-current-pr` が `state.json` の `current_pr` / `pr_history` を更新する。 -state.json の **キーは元 PR 番号 (STATE_PR) のまま** なので、light/squash どちらでも -後続スクリプトへの第 1 引数は `$STATE_PR` を渡し続ければよい。 - -### 後方互換: 旧 1 引数形式 - -`rotate-pr.sh ` (引数 1 つ) は `execute --mode squash` 相当として動くが、 -stderr に deprecation warning を出す。新規呼び出しは必ず prepare → execute 形式へ移行。 - -> ⚠ **重要**: state.json のファイル名は **最初に init した PR 番号** がキー -> (`$STATE_PR`)。rotation 後も全 scripts の **第 1 引数には常に `$STATE_PR`** を渡す。 -> 内部的に `state.json.current_pr` を読んで「現在の PR」を解決する設計。 -> `PR=$NEW_PR` 等で shell 変数の側を切り替えると、次ループの `state.py start-round` -> が `$TMP_DIR/cross-review-pr-state.json` を探して `state.json not found` で -> 止まる。 - -## Step 7: 次ラウンドへ - -Step 1 に戻る。 - -## Step 7.5: 最終スイープ(必須)— 取りこぼし防止 - -**目的**: ループを抜けた時点で PR 上に **未解決 (open) のレビュースレッドを 1 件も残さない**。 - -### なぜ必要か - -ループ内の修正フェーズ (Step 5) は「一方でも REQUEST_CHANGES」のラウンドでしか走らない。 -そのため以下が取りこぼされる: - -- **最終 APPROVE ラウンドのインラインコメント**: skill のレビュー方針上、`minor` 以下しか - 無ければ `APPROVE` で良い。つまり **APPROVE でも minor/nit のインラインコメントが - 投稿されている**ことがあり、両者 APPROVE でループを抜けるとこれらが未対応のまま残る。 -- **ループ中に deferred 記録した nit**: `state.deferred_nits` に積まれたまま reply のみで - Resolve されていないスレッド。 - -これらを放置すると、PR レビュー画面に「Unresolved」スレッドが残り、人間のレビュアーや -後続作業者が「未対応の指摘がある」と誤認する。 - -### 実行(メインが Agent を駆動) - -ループを抜けたら(`final` がどの値でも)、**メインが `Agent(subagent_type="general-purpose")` -を起動**し、`/ndf:fix ` を再実行させる。bash 単体では Agent を呼べないため、 -while ループ脱出後にメインが以下のプロンプトでサブエージェントを起動する。 - -> **対象 PR**: ``(state.json の `current_pr`。rotation していれば最新 PR) -> **worktree**: state.json の `worktree_path` -> -> PR の **全 open review thread**(インライン / レビュー body / PR レベルコメント)を -> `gh api` で洗い出し、cross-review の codex/gemini が残したものを中心に **すべて解消**せよ: -> 1. 修正可能な `minor`/`nit` → コード修正 + push(同ブランチ、main へは push しない)し、 -> reply + GraphQL `resolveReviewThread` で Resolve。 -> 2. 修正しない(好み・判断保留)`nit` → 「[deferred / nit] 対応見送り: <理由>」を日本語で -> reply した上で **Resolve まで実行**(スレッドを open のまま残さない)。 -> 3. bot 誤指摘 → 却下理由を reply して Resolve。 -> 修正で push した場合は `claude plugin validate` を通すこと。 -> 完了後、`$TMP_DIR/sweep-pr-result.json` に -> `{"resolved": N, "fixed_in_sweep": M, "commit": "", "remaining_open": K, -> "items": ["<1行要約>", ...]}` を書き出し、最終メッセージで内訳を日本語報告せよ。 -> **`remaining_open` は 0 を目標**とし、0 にできない場合は理由を明記すること。 - -> ⚠ 最終スイープは「修正の追加」ではなく **後始末**。新しい設計変更や大きな -> リファクタは行わない(行う必要があれば deferred として report に残す)。 -> push が走った場合でも、それに対する再レビューはループ終了後のため行わない -> (次回 cross-review か通常レビューに委ねる)。 - -### 再開性 - -sweep 中にメインが落ちても、`sweep-pr-result.json` が無ければ Step 7.5 から -再実行すれば良い(Resolve は冪等。既 Resolve スレッドは skip される)。 - -## Step 8: 終了処理 — ラウンドサマリ + 残 deferred の参考列挙 - -最終スイープ (Step 7.5) 完了後、ラウンドサマリを表示: - -```bash -"$SCRIPTS/state.py" report "$STATE_PR" -``` - -`report` は以下を Markdown で吐く: - -- 最終ステータス(`approved` / `max_rounds` / `oscillation` / `error`) -- PR 履歴 -- ラウンドサマリ表 -- 残 deferred nit 一覧 - -メインは `report` の出力に **Step 7.5 の sweep 結果**(`sweep-pr-result.json` の -`resolved` / `fixed_in_sweep` / `remaining_open`)を折り込んで最終報告する。 - -> **方針変更(v4.11.0)**: 従来は deferred nit を「AskUserQuestion で 1 回問い合わせ」て -> いたが、未解決スレッドを残さない方針に変更。**Step 7.5 で nit も含め全 open thread を -> Resolve する**ため、Step 8 のユーザ問い合わせは原則不要。deferred nit は「対応見送りの -> 記録」として report に **参考列挙**するに留める(再対応が要るものがあればユーザが -> その場で指示できる)。`remaining_open > 0` の場合のみ、残った理由を添えて報告する。 diff --git a/plugins/ndf/skills-codex/cross-review/scripts/_tmpdir.sh b/plugins/ndf/skills-codex/cross-review/scripts/_tmpdir.sh deleted file mode 100755 index 8df7371..0000000 --- a/plugins/ndf/skills-codex/cross-review/scripts/_tmpdir.sh +++ /dev/null @@ -1,23 +0,0 @@ -# shellcheck shell=bash -# cross-review 共通: tmp ディレクトリ決定ヘルパ。 -# -# Usage: -# . "$(dirname "$0")/_tmpdir.sh" -# TMP_DIR=$(tmpdir) -# -# 優先順位: -# 1. 環境変数 CROSS_REVIEW_TMP_DIR (明示) -# 2. /.cross_review/ (worktree 内。gemini の workspace 制約を根本回避) - -tmpdir() { - if [ -n "${CROSS_REVIEW_TMP_DIR:-}" ]; then - mkdir -p "$CROSS_REVIEW_TMP_DIR" - echo "$CROSS_REVIEW_TMP_DIR" - return - fi - # サブディレクトリから呼ばれた場合でも worktree root を正しく特定する - local root - root="$(git rev-parse --show-toplevel 2>/dev/null)" || root="$PWD" - mkdir -p "$root/.cross_review" - echo "$root/.cross_review" -} diff --git a/plugins/ndf/skills-codex/cross-review/scripts/launch-codex.sh b/plugins/ndf/skills-codex/cross-review/scripts/launch-codex.sh deleted file mode 100755 index 2054366..0000000 --- a/plugins/ndf/skills-codex/cross-review/scripts/launch-codex.sh +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env bash -# cross-review codex launcher. -# -# Usage: launch-codex.sh -# -# 引数 STATE_PR は **state.json の key (= 最初に init した PR 番号)**。 -# rotation 後も state.json の場所は変わらないため、ここに渡すのは常に初期 PR。 -# gh コマンドに使う「現在のレビュー対象 PR」は state.json の `current_pr` を読む。 -# -# tmp ディレクトリは `_tmpdir.sh` の `tmpdir()` 関数で決定: -# CROSS_REVIEW_TMP_DIR env → $PWD/.cross_review/ -# worktree 内に配置することで gemini の workspace 制約を根本回避。 -# codex は --dangerously-bypass-approvals-and-sandbox を維持 -# (-s workspace-write は bwrap 非対応環境で失敗するため)。 -# -# 状態ファイル: $TMP_DIR/codex-review-pr-{result,err,stdout,pid}.json -# (パスは STATE_PR ベースで固定 — monitor.py / state.py と一致させる。) - -set -euo pipefail - -STATE_PR=${1:?STATE_PR required} -ROUND=${2:?ROUND required} - -SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) -# shellcheck source=_tmpdir.sh -. "$SCRIPT_DIR/_tmpdir.sh" -TMP_DIR=$(tmpdir) - -STATE=$TMP_DIR/cross-review-pr$STATE_PR-state.json -[ -s "$STATE" ] || { echo "state.json not found: $STATE" >&2; exit 1; } - -WORKTREE=$(jq -r '.worktree_path' "$STATE") -REPO=$(jq -r '.repo' "$STATE") -EVENT_DOWNGRADE=$(jq -r '.event_downgrade // false' "$STATE") -EXTRA_REVIEW_INSTRUCTIONS=$(jq -r '.review_instructions // .extra_review_instructions // ""' "$STATE") -# PR (=current_pr) は gh コマンドのレビュー対象 PR 番号として使う。 -# tmp パス側は STATE_PR で固定 (monitor.py / state.py が同じ STATE_PR 起点で -# 読みに来るため、ここを揃えないと PR rotation 後に読み書きパスが食い違う)。 -PR=$(jq -r '.current_pr' "$STATE") -SHA=$(gh pr view "$PR" --json headRefOid -q .headRefOid) - -PROMPT=$TMP_DIR/codex-review-pr$STATE_PR-prompt.md -EXISTING=$TMP_DIR/cross-review-pr$STATE_PR-existing-comments.txt -EXTRA_REVIEW_BLOCK= -if [ -n "$EXTRA_REVIEW_INSTRUCTIONS" ]; then - EXTRA_REVIEW_BLOCK=$(cat < "$PROMPT" <\` には **本来の intent** を書く。 -- 既存コメントスナップショット: $EXISTING (重複指摘禁止) -$EXTRA_REVIEW_BLOCK - -## 出力契約 -- review body の **先頭行** に必ず以下を入れる(fence 不要、Markdown 見出しとして): - \`\`\` - ## 🤖 cross-review | round $ROUND | codex | - \`\`\` - 例: \`## 🤖 cross-review | round $ROUND | codex | REQUEST_CHANGES\` - - \`\` は **本来の intent** (REQUEST_CHANGES / APPROVE / COMMENT) - -### 出力に **含めてはいけないもの**(Resolve 負荷を増やすため) -- ❌ **「良い点」/「Strengths」/「評価できる点」 section** — body にも書かない -- ❌ **対応アクションが無いインラインコメント** — 観察・感想・現状説明だけは禁止 -- ❌ **nit / スタイル指摘のインライン化** — 好みの問題はコメント化しない (無視する) -- ❌ **コード引用 (\`\`\` ... \`\`\`) だけで指摘内容が無いコメント** -- ❌ **\`event=COMMENT\` での雑感投稿** — 直すべき点が無ければ \`APPROVE\` にする - -### インラインコメントの書式 -- \`[重要度 / カテゴリ]\` プレフィックス必須 (例: \`[major / 正確性]\`) -- 重要度は \`critical\` / \`major\` / \`minor\` のみ使う (nit はインライン化しない) -- 本文は **1 コメント = 1 修正アクション** で完結させる。1〜2 文で具体的な修正提案を書く - -### body (総評) の書き方 -- 設計レベル・PR 横断の **修正提案のみ** 書く -- 書くことが無ければ prefix 行 + 1 行サマリだけで良い (褒め言葉や評価文は不要) - -- 投稿後、サマリを **$TMP_DIR/codex-review-pr$STATE_PR-result.json** に書く: - \`\`\`json - { - "event": "REQUEST_CHANGES", - "posted_as": "COMMENT", - "comments_count": 5, - "review_url": "https://github.com/.../pull/$PR#pullrequestreview-...", - "by_severity": {"critical": 0, "major": 3, "minor": 2, "nit": 0} - } - \`\`\` -- payload(全コメント詳細)は **$TMP_DIR/codex-review-pr$STATE_PR-round$ROUND-payload.json** に保存 - (振動検知用、\`{ "comments": [{path, line, body, severity}, ...] }\` 形式) - -## 守るべきこと -- リポジトリ編集は行わない(コード修正は別ステップ) -- worktree 外のパスは触らない -- gh api 失敗時は err.log にエラー詳細を残して即時終了 -EOF - -cd "$WORKTREE" -nohup codex exec --dangerously-bypass-approvals-and-sandbox \ - --config reasoning.effort=medium -C "$WORKTREE" \ - < "$PROMPT" \ - > $TMP_DIR/codex-review-pr$STATE_PR-stdout.log \ - 2> $TMP_DIR/codex-review-pr$STATE_PR-err.log & -echo $! > $TMP_DIR/codex-review-pr$STATE_PR.pid -disown -echo "🚀 codex launched (pid=$(cat $TMP_DIR/codex-review-pr$STATE_PR.pid))" >&2 diff --git a/plugins/ndf/skills-codex/cross-review/scripts/launch-gemini.sh b/plugins/ndf/skills-codex/cross-review/scripts/launch-gemini.sh deleted file mode 100755 index e0b921e..0000000 --- a/plugins/ndf/skills-codex/cross-review/scripts/launch-gemini.sh +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/env bash -# cross-review gemini launcher (trusted directory 対策込み). -# -# Usage: launch-gemini.sh -# -# 引数 STATE_PR は state.json の key (= 最初に init した PR 番号)。 -# レビュー対象の PR は state.json の `current_pr` を読む。 -# -# 注意: -# - worktree のような新規パスは untrusted 判定で --yolo が "default" に降格する。 -# `--skip-trust` と `GEMINI_CLI_TRUST_WORKSPACE=true` を **両方** 必須とする。 -# - 完了判定は monitor.py が pidfile + sentinel + result.json で多軸判定する。 - -set -euo pipefail - -STATE_PR=${1:?STATE_PR required} -ROUND=${2:?ROUND required} - -SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) -# shellcheck source=_tmpdir.sh -. "$SCRIPT_DIR/_tmpdir.sh" -TMP_DIR=$(tmpdir) - -STATE=$TMP_DIR/cross-review-pr$STATE_PR-state.json -[ -s "$STATE" ] || { echo "state.json not found: $STATE" >&2; exit 1; } - -WORKTREE=$(jq -r '.worktree_path' "$STATE") -REPO=$(jq -r '.repo' "$STATE") -EVENT_DOWNGRADE=$(jq -r '.event_downgrade // false' "$STATE") -EXTRA_REVIEW_INSTRUCTIONS=$(jq -r '.review_instructions // .extra_review_instructions // ""' "$STATE") -# PR (=current_pr) は gh コマンドのレビュー対象 PR 番号として使う。 -# tmp パス側は STATE_PR で固定 (monitor.py / state.py との読み書き整合のため)。 -PR=$(jq -r '.current_pr' "$STATE") -SHA=$(gh pr view "$PR" --json headRefOid -q .headRefOid) - -PROMPT=$TMP_DIR/gemini-review-pr$STATE_PR-prompt.md -# 既存コメントは **プロンプトにインライン埋め込み** する。 -# tmp dir は `/.cross_review/` を使うが、念のため -# プロンプト埋め込み方式も維持 (gemini が read_file を呼ばずに済むので確実)。 -EXISTING_FILE=$TMP_DIR/cross-review-pr$STATE_PR-existing-comments.txt -if [ -s "$EXISTING_FILE" ]; then - EXISTING_INLINE=$(cat "$EXISTING_FILE") -else - EXISTING_INLINE="(なし)" -fi -EXTRA_REVIEW_BLOCK= -if [ -n "$EXTRA_REVIEW_INSTRUCTIONS" ]; then - EXTRA_REVIEW_BLOCK=$(cat < "$PROMPT" <\` は本来の intent を書く。 - -## 既存コメントスナップショット(重複指摘禁止) -workspace 外を読まなくて済むよう、以下にインライン展開する: - -\`\`\` -$EXISTING_INLINE -\`\`\` -$EXTRA_REVIEW_BLOCK - -## 出力契約 -- review body の **先頭行** に必ず以下を入れる: - \`\`\` - ## 🤖 cross-review | round $ROUND | gemini | - \`\`\` - - \`\` は **本来の intent** (REQUEST_CHANGES / APPROVE / COMMENT) - -### 出力に **含めてはいけないもの**(Resolve 負荷を増やすため) -- ❌ **「良い点」/「Strengths」/「評価できる点」 section** — body にも書かない -- ❌ **対応アクションが無いインラインコメント** — 観察・感想・現状説明だけは禁止 -- ❌ **nit / スタイル指摘のインライン化** — 好みの問題はコメント化しない (無視する) -- ❌ **コード引用 (\`\`\` ... \`\`\`) だけで指摘内容が無いコメント** -- ❌ **\`event=COMMENT\` での雑感投稿** — 直すべき点が無ければ \`APPROVE\` にする - -### インラインコメントの書式 -- \`[重要度 / カテゴリ]\` プレフィックス必須 (例: \`[major / 正確性]\`) -- 重要度は \`critical\` / \`major\` / \`minor\` のみ使う (nit はインライン化しない) -- 本文は **1 コメント = 1 修正アクション** で完結させる。1〜2 文で具体的な修正提案を書く - -### body (総評) の書き方 -- 設計レベル・PR 横断の **修正提案のみ** 書く -- 書くことが無ければ prefix 行 + 1 行サマリだけで良い (褒め言葉や評価文は不要) - -### 進捗マーカー(監視用) -- 無言ハングと区別できるよう、作業フェーズが進むたびに - **$TMP_DIR/gemini-review-pr$STATE_PR-progress.log** へ短い 1 行を追記すること -- 内部の推論や長い説明は書かず、以下のようなフェーズ名 + 対象だけを書く: - - \`start: review PR #$PR round $ROUND\` - - \`scan: diff and existing comments\` - - \`analyze: candidate findings\` - - \`post: submit review\` - - \`done: result.json written\` - -- 投稿後、サマリを **$TMP_DIR/gemini-review-pr$STATE_PR-result.json** に - **必ず以下のキーで** 書く: - \`\`\`json - { - "event": "APPROVE", - "posted_as": "COMMENT", - "comments_count": 3, - "review_url": "https://github.com/.../pull/$PR#pullrequestreview-...", - "by_severity": {"critical": 0, "major": 0, "minor": 0, "nit": 0} - } - \`\`\` - - \`intent\` / \`comment_count\` 等の別名は使わないこと - - \`event\` の値は \`APPROVE\` / \`REQUEST_CHANGES\` / \`COMMENT\` のいずれか - - \`event_downgrade=true\` のとき \`posted_as\` は \`COMMENT\` にダウングレード可 -- payload は **$TMP_DIR/gemini-review-pr$STATE_PR-round$ROUND-payload.json** に保存 - (振動検知用、\`{ "comments": [{path, line, body, severity}, ...] }\` 形式) - -## 守るべきこと -- **リポジトリ編集禁止**。gh api での投稿のみ許可 -- worktree 外のパスは触らない -- gh api 失敗時は err.log にエラー詳細を残して即時終了 -EOF - -cd "$WORKTREE" - -# gemini-cli 最新版は mcpServers エントリの `disabled` キーを Unrecognized 扱いし、 -# 起動時に `Error in: mcpServers.` 警告を err.log に出す。文字列としては -# `Error: ...` ではなく `Error in: ...` だが、monitor.py 旧版が誤検知して -# プロセスを kill する原因になっていた (REPORT01 参照)。 -# launcher 側でも sanitize して警告自体を抑制する: `disabled` キーを再帰的に -# 削除した settings.json を起動時のみ差し込み、gemini が読み終わったら復元する。 -SETTINGS=$WORKTREE/.gemini/settings.json -SETTINGS_BACKUP= - -# trap で EXIT / INT / TERM / HUP のいずれでも必ず settings.json を復元する。 -# sleep 2 中や復元前に Ctrl-C / SIGTERM / シェル終了で止まっても、sanitize 済み -# settings.json が worktree に残らないようにするため。冪等に書いてあるので -# 多重実行されても安全。 -restore_settings() { - # SETTINGS_BACKUP が未設定 or バックアップ不在なら何もしない - if [ -n "${SETTINGS_BACKUP:-}" ] && [ -f "$SETTINGS_BACKUP" ]; then - mv -f "$SETTINGS_BACKUP" "$SETTINGS" 2>/dev/null || true - SETTINGS_BACKUP= - fi -} -trap restore_settings EXIT INT TERM HUP - -if [ -f "$SETTINGS" ] && command -v jq >/dev/null 2>&1; then - SETTINGS_BACKUP=$TMP_DIR/gemini-review-pr$STATE_PR-settings-backup.json - cp "$SETTINGS" "$SETTINGS_BACKUP" - SANITIZED=$TMP_DIR/gemini-review-pr$STATE_PR-settings-sanitized.json - if jq 'walk(if type == "object" then del(.disabled) else . end)' "$SETTINGS_BACKUP" > "$SANITIZED" 2>/dev/null; then - cp "$SANITIZED" "$SETTINGS" - else - # jq が失敗したら sanitize を諦め、バックアップも破棄して元のまま起動 - rm -f "$SETTINGS_BACKUP" - SETTINGS_BACKUP= - fi -fi - -# ⚠ --skip-trust と GEMINI_CLI_TRUST_WORKSPACE=true は両方必須 -GEMINI_CLI_TRUST_WORKSPACE=true nohup gemini --yolo --skip-trust --output-format text \ - -p "" \ - > $TMP_DIR/gemini-review-pr$STATE_PR-stdout.log \ - 2> $TMP_DIR/gemini-review-pr$STATE_PR-err.log < "$PROMPT" & -GEMINI_PID=$! -echo $GEMINI_PID > $TMP_DIR/gemini-review-pr$STATE_PR.pid -disown - -# gemini は起動時に 1 度 settings.json を読む。読み込み完了を待ってから元の -# ファイルを復元する (worktree を dirty なままにしないため)。 -# sleep 中に signal が来ても trap restore_settings が必ず復元するため安全。 -if [ -n "$SETTINGS_BACKUP" ] && [ -f "$SETTINGS_BACKUP" ]; then - sleep 2 - restore_settings -fi - -echo "🚀 gemini launched (pid=$GEMINI_PID)" >&2 diff --git a/plugins/ndf/skills-codex/cross-review/scripts/monitor.py b/plugins/ndf/skills-codex/cross-review/scripts/monitor.py deleted file mode 100755 index 544a912..0000000 --- a/plugins/ndf/skills-codex/cross-review/scripts/monitor.py +++ /dev/null @@ -1,806 +0,0 @@ -#!/usr/bin/env -S uv run --script -# /// script -# requires-python = ">=3.10" -# dependencies = [] -# /// -"""cross-review codex/gemini プロセス監視 CLI。 - -`launch-codex.sh` / `launch-gemini.sh` で起動したバックグラウンドプロセスを -**複数の根拠で多重監視** し、失敗パターン (sentinel 不在 / 早期エラー / ハング / -pidfile stale / result.json 不在) を構造化して扱う。 - -監視軸: - 1. **pidfile** + `kill -0` でプロセス生存確認 - - 可能なら `/proc//cmdline` で codex/gemini であることを再確認 (PID 再利用対策) - 2. **sentinel** (codex のみ): err.log に `^tokens used$` 出現 - 3. **early-error pattern**: err.log に既知の致命的キーワードが出たら即中断 - - **FATAL** (auth/quota/sandbox 等の明確な致命): 検知時に kill - - **WARN** (生の `Error:` / `Traceback` 等の曖昧パターン): 警告ログのみ、kill せず通常判定を継続 - - `--no-early-error` / `MONITOR_NO_EARLY_ERROR=1` で検知自体を無効化可 - 4. **result.json**: プロセス終了後に `/.cross_review/-review-pr-result.json` が - 生成されていなければ失敗扱い - 5. **hard timeout**: 既定 7 分。`--timeout` または `MONITOR_TIMEOUT` で上書き可 - 6. **stall timeout**: err.log + stdout.log の合計サイズが一定時間変化しなければ - STALLED として中断。既定は agent 別 (codex=180s, gemini=480s。gemini は err.log - にほぼ進捗を出さないため大きめ)。`--stall-timeout` で CLI 明示、 - `MONITOR_STALL_` env で per-agent 上書き、`MONITOR_STALL` env で共通上書き可 - 7. **progress.log heartbeat**: agent が任意で書く短いフェーズマーカーを stderr に表示。 - Gemini の stdout/stderr が静かな時間でも、内部推論ではなく監視用の作業段階を確認できる - 8. **result.json + age fallback**: sentinel を持たない agent (gemini) 向け。 - result.json の mtime が 30 秒以上前なら完了とみなし kill → OK - 9. **失敗時 kill**: TIMEOUT / STALLED / EARLY_ERROR (FATAL のみ) / PIDFILE_BAD で - 返るとき、対象プロセスを SIGTERM (3 秒後に SIGKILL) で停止する - -Usage: - monitor.py target ∈ {codex, gemini, both} - monitor.py both --timeout 1200 --stall-timeout 600 - monitor.py both --no-early-error # EARLY_ERROR 検知を完全無効化 - -Exit codes (target=both は最悪値を返す): - 0 OK プロセス正常終了 + result.json 確認 - 1 USAGE / IO error - 2 TIMEOUT hard timeout 超過 - 3 NO_RESULT プロセス終了したが result.json 未生成 - 4 EARLY_ERROR err.log に致命的パターン検出 - 5 STALLED err.log が一定時間進捗なし - 6 PIDFILE_BAD pidfile が無い / 内容が不正 / プロセスが起動していない - -Stdout: 各 agent の最終ステータスを JSON で 1 行ずつ吐く(メインがパース可能)。 -Stderr: 人間向けの進捗ログ(poll ごとに 1 行)。 -""" -from __future__ import annotations - -import argparse -import json -import os -import pathlib -import re -import signal -import sys -import threading -import time -from dataclasses import dataclass -from typing import Optional - - -# ---------- 設定 ---------- - -# 既定値は import 時に **固定数値** で保持する。env (`MONITOR_TIMEOUT` / -# `MONITOR_STALL` / `MONITOR_POLL`) の解釈は **呼び出し時** に try/except -# 付きで行い、非数値 env でも import / 監視プロセスがクラッシュしないようにする。 -# (codex round 5 指摘: import 時の `int(os.environ.get(...))` は -# `MONITOR_STALL=abc` のような誤設定で `_agent_stall_default()` に到達する前に -# ValueError で落ちてしまうため) -DEFAULT_TIMEOUT = 420 # 7 min — `--timeout` / env `MONITOR_TIMEOUT` で上書き可 -# 既定 stall timeout (後方互換のため env MONITOR_STALL は残す)。 -# 両 agent 共通のデフォルトとして引き続き受け付ける (解釈は `_agent_stall_default()` 内)。 -DEFAULT_STALL = 180 # 3 min no progress -# per-agent 上書き: gemini は err.log がほぼ無音なため大きめに取る。 -# 解決順は `_agent_stall_default()` 参照。 -DEFAULT_STALL_AGENT_BUILTIN = { - "codex": 180, # 推論ログを逐次出すので 3 min で十分 - "gemini": 480, # err.log が静かなため 8 min まで許容 -} -DEFAULT_POLL = 15 # 15 sec — env `MONITOR_POLL` で上書き可 -# result.json が書き込まれた後もプロセスがハングするケース (gemini で観測) の -# fallback: mtime から RESULT_AGE_GRACE 秒以上経過していれば完了とみなす。 -RESULT_AGE_GRACE = 30 -# `MONITOR_NO_EARLY_ERROR=1` で EARLY_ERROR 検知を無効化 (escape hatch) -DEFAULT_NO_EARLY_ERROR = os.environ.get("MONITOR_NO_EARLY_ERROR", "").lower() in { - "1", "true", "yes", "on", -} - -# err.log の行頭に近い形で出る **明確な致命** パターン (kill 対象)。 -# auth / quota / sandbox / HTTP 401-403-429 / gemini の YOLO 降格など、 -# プロセスが続行しても result を生成できないと判明しているケースだけを入れる。 -EARLY_ERROR_FATAL = [ - # HTTP エラーステータス行 (`HTTP/1.1 401 Unauthorized` 等) - re.compile(r"^HTTP/\d\S* (?:401|403|429) ", re.MULTILINE), - # gemini 固有: untrusted directory で YOLO が降格される - re.compile(r'^Approval mode overridden to "default"', re.MULTILINE), - # 認証 / 権限系(行頭限定) - re.compile(r"^(?:Authentication failed|Permission denied)", re.MULTILINE), - # quota / rate limit (`m.start()` をキーワード位置に合わせるため `^.*` を付けない。 - # `_match_is_quoted()` が backtick / 「」 引用を判定するために match 開始位置を使うため) - re.compile(r"\b(?:quota exceeded|rate limit exceeded)\b", re.IGNORECASE), - # API key 系 - re.compile(r"\bAPI key (?:not found|missing|invalid)\b", re.IGNORECASE), - # codex 固有: sandbox エラー - re.compile(r"\bsandbox error\b", re.IGNORECASE), -] - -# 行頭の生 `Error:` / `Traceback` 系は **kill しない警告のみ** に降格。 -# - gemini-cli の `Error in: mcpServers.` 警告 (起動時の config 検証) -# - codex がレビュー対象 diff の test コード片を echo して `Traceback` が混入するケース -# など、続行可能な誤検知が頻発するため。プロセスは sentinel / result.json / timeout -# で別途判定する。 -EARLY_ERROR_WARN = [ - re.compile(r"^(?:Error|FATAL|fatal|panic|PANIC|Traceback)[: ]", re.MULTILINE), -] - -# 文脈に含まれていたら benign(doc 引用 / コードレビューコメント等)と見なし誤検知扱い。 -# FATAL / WARN 双方のスキャンに適用される。 -EARLY_ERROR_BENIGN = [ - # gemini-cli の config validation 警告 (`Error in: mcpServers.` / `Error in: ...`): - # 設定の Unrecognized キーを通知するだけで、gemini 本体は起動継続する。 - re.compile(r"^Error in: mcpServers\.", re.MULTILINE), - re.compile(r"^Error in: \S+\s*$", re.MULTILINE), - # diff のコンテキスト行 (` `, `+`, `-` で始まり、その後 markdown 表記が来る) - re.compile(r"^[ +-].*[\|`]", re.MULTILINE), - # markdown のリスト / 引用 - re.compile(r"^\s*[-*>]\s", re.MULTILINE), - # markdown の表セル行 (`| ... | ...` 形式)。SKILL.md / docs/*.md が - # 検知パターンを表で列挙しており、それを codex が echo すると誤検知する。 - re.compile(r"^\|", re.MULTILINE), - # grep / ripgrep 形式のソース引用行 (`path/to/file.ext:42: `)。 - # codex がレビュー対象のテストコード片を grep 形式で echo すると、 - # その文字列リテラル内の FATAL キーワードを誤検知する (PR #23 round 2 で発生)。 - re.compile(r"^\S+\.[A-Za-z0-9]+:\d+:", re.MULTILINE), - # warning は致命ではない - re.compile(r"^warning: ", re.IGNORECASE | re.MULTILINE), -] - - -def _match_is_quoted(line: str, match_start: int, match_end: int) -> bool: - """マッチ位置がドキュメント引用 / コード文字列リテラルに囲まれているか判定。 - - - backtick: マッチ開始までの `` ` `` カウントが奇数 かつ マッチ終了以降に `` ` `` がある - - 日本語クォート: マッチ開始までに直近の `「` が `」` よりも後 かつ マッチ終了以降に `」` がある - - ダブル/シングルクォート文字列リテラル: マッチ開始までの `"` (or `'`) カウントが - 奇数 かつ マッチ終了以降に同じクォートがある (= リテラルの内側) - - Why: SKILL.md / docs/*.md 内で FATAL キーワードを `「quota exceeded」` のように - 引用列挙しており、codex がそれを echo する。さらに tests/*.py の - `"quota exceeded: please upgrade"` のような **テスト用文字列リテラル** を - codex がレビュー中に echo するケース (PR #23 round 2 で実際に発生) もある。 - いずれも引用形であり本物のエラーではないため benign 扱いする。 - """ - before = line[:match_start] - after = line[match_end:] - if before.count("`") % 2 == 1 and "`" in after: - return True - if before.rfind("「") > before.rfind("」") and "」" in after: - return True - # コード文字列リテラル (ダブル / シングルクォート)。 - # エスケープされたクォート (`\"` / `\'`) はリテラルを開閉しないため - # パリティ計算から除外する。これを数えると、文字列内にエスケープ - # クォートを含む行で「引用内/外」の判定がずれ、本物のエラー行を - # 誤って benign 扱い (= FATAL 見逃し) する恐れがある。 - for q in ('"', "'"): - if _unescaped_count(before, q) % 2 == 1 and q in after: - return True - return False - - -def _unescaped_count(text: str, quote: str) -> int: - """`quote` のうちバックスラッシュでエスケープされていない出現数を数える。 - - 直前の連続バックスラッシュ数が奇数なら、そのクォートはエスケープ - されている (リテラルを開閉しない) ものとして除外する。 - """ - count = 0 - for i, ch in enumerate(text): - if ch != quote: - continue - backslashes = 0 - j = i - 1 - while j >= 0 and text[j] == "\\": - backslashes += 1 - j -= 1 - if backslashes % 2 == 0: - count += 1 - return count - -CODEX_SENTINEL = re.compile(r"^tokens used$", re.MULTILINE) - - -def _safe_int_env(name: str, fallback: int) -> int: - """env を safe に int parse する。 - - 非数値時は warn を stderr に出して fallback 値を返す。 - `_agent_stall_default()` と同じく、env 設定ミスでプロセスを落とさないため。 - - Note (codex round 5 指摘): `MONITOR_STALL` 等の env を import 時 / 呼び出し時に - 生の `int(...)` で読むと、非数値設定で監視プロセスが起動できなくなる。 - `DEFAULT_TIMEOUT` / `DEFAULT_POLL` も同じ問題を持つため、共通ヘルパに集約。 - """ - if name not in os.environ: - return fallback - raw = os.environ[name] - try: - return int(raw) - except (ValueError, TypeError): - print( - f"⚠ env {name}={raw!r} が int に変換できません — {fallback} を使用", - file=sys.stderr, flush=True, - ) - return fallback - - -def _agent_stall_default(agent: str) -> int: - """agent ごとの既定 stall timeout を解決する。 - - 解決順: - 1. env `MONITOR_STALL_` (per-agent 明示) - 2. env `MONITOR_STALL` (両 agent 共通) - 3. `DEFAULT_STALL_AGENT_BUILTIN[agent]` (codex=180, gemini=480) - 4. `DEFAULT_STALL` (フォールバック) - - Note (codex round 3 指摘): 2 は **呼び出し時** に `os.environ["MONITOR_STALL"]` - を再評価する。`DEFAULT_STALL` モジュール定数は import 時に env を読むため - プロセス起動後の env 変更に追随できず、テストの monkeypatch も効かない。 - - Note (gemini round 4 指摘): env が非数値だった場合 (`int(...)` で - `ValueError` / `TypeError` が裸で上がる) は warn を出して - `DEFAULT_STALL_AGENT_BUILTIN` / `DEFAULT_STALL` にフォールバックする。 - 監視プロセスを env 設定ミスでクラッシュさせない。 - """ - builtin = DEFAULT_STALL_AGENT_BUILTIN.get(agent, DEFAULT_STALL) - env_key = f"MONITOR_STALL_{agent.upper()}" - if env_key in os.environ: - return _safe_int_env(env_key, builtin) - if "MONITOR_STALL" in os.environ: - return _safe_int_env("MONITOR_STALL", builtin) - return builtin - - -def _tmp_dir() -> pathlib.Path: - """cross-review 用 tmp ディレクトリ。 - - state.py の `_tmp_dir()` と同じロジック。優先: - 1. `CROSS_REVIEW_TMP_DIR` env - 2. `/.cross_review/` (worktree 内。gemini の workspace 制約を根本回避) - - worktree root は `git rev-parse --show-toplevel` で取得する。 - サブディレクトリから起動した場合でも一貫したパスを返す。 - """ - env = os.environ.get("CROSS_REVIEW_TMP_DIR") - if env: - d = pathlib.Path(env).resolve() - d.mkdir(parents=True, exist_ok=True) - return d - # git worktree root を取得。サブディレクトリから起動しても一貫したパスにする。 - import subprocess as _sp - r = _sp.run( - ["git", "rev-parse", "--show-toplevel"], - capture_output=True, text=True, - ) - if r.returncode == 0 and r.stdout.strip(): - root = pathlib.Path(r.stdout.strip()).resolve() - else: - root = pathlib.Path.cwd().resolve() - d = root / ".cross_review" - d.mkdir(parents=True, exist_ok=True) - return d - - -# ---------- データ型 ---------- - -@dataclass -class AgentPaths: - agent: str - pr: int - pidfile: pathlib.Path - err_log: pathlib.Path - stdout_log: pathlib.Path - progress_log: pathlib.Path - result: pathlib.Path - - @classmethod - def for_(cls, agent: str, pr: int) -> "AgentPaths": - base = _tmp_dir() / f"{agent}-review-pr{pr}" - return cls( - agent=agent, pr=pr, - pidfile=pathlib.Path(f"{base}.pid"), - err_log=pathlib.Path(f"{base}-err.log"), - stdout_log=pathlib.Path(f"{base}-stdout.log"), - progress_log=pathlib.Path(f"{base}-progress.log"), - result=pathlib.Path(f"{base}-result.json"), - ) - - -@dataclass -class AgentStatus: - agent: str - status: str = "RUNNING" - exit_code: int = 0 - pid: Optional[int] = None - elapsed: float = 0.0 - detail: str = "" - err_log_size: int = 0 - stdout_log_size: int = 0 - progress_log_size: int = 0 - progress_tail: str = "" - idle_seconds: float = 0.0 - result_exists: bool = False - sentinel_seen: bool = False - - -# ---------- 監視ロジック ---------- - -def _read_pidfile(p: pathlib.Path) -> Optional[int]: - try: - s = p.read_text().strip() - return int(s) if s else None - except (FileNotFoundError, ValueError): - return None - - -def _pid_alive(pid: int) -> bool: - """`kill -0` + ゾンビ検出。 - - `os.kill(pid, 0)` はゾンビプロセスに対しても成功する (PID エントリが - 残っているため)。Docker without `--init` 環境では orphan プロセスが - ゾンビ化して永久に残るため、`/proc//status` で State: Z を検出する。 - """ - try: - os.kill(pid, 0) - except (ProcessLookupError, PermissionError): - return False - except OSError: - return False - try: - status_text = pathlib.Path(f"/proc/{pid}/status").read_text() - for line in status_text.splitlines(): - if line.startswith("State:"): - return "Z" not in line - except (FileNotFoundError, PermissionError, OSError): - pass - return True - - -def _is_zombie(pid: int) -> bool: - """PID がゾンビかどうか。_pid_alive() とは独立に呼べるユーティリティ。""" - try: - status_text = pathlib.Path(f"/proc/{pid}/status").read_text() - for line in status_text.splitlines(): - if line.startswith("State:"): - return "Z" in line - except (FileNotFoundError, PermissionError, OSError): - pass - return False - - -def _kill_pid(pid: int, sigterm_grace: float = 3.0) -> None: - """対象プロセスに SIGTERM、`sigterm_grace` 秒後も生きていたら SIGKILL。 - - TIMEOUT / STALLED / EARLY_ERROR で監視を打ち切るとき、対象プロセスが残ったまま - だと後から `gh api` 投稿や result.json 書き込みを実行してメインフローと - 競合する。失敗扱いで返るときは必ず停止させる。 - ゾンビプロセスにはシグナルを送れないためスキップする。 - """ - if pid <= 0: - return - if _is_zombie(pid): - return - try: - os.kill(pid, signal.SIGTERM) - except OSError: - return - deadline = time.monotonic() + sigterm_grace - while time.monotonic() < deadline: - if not _pid_alive(pid): - return - time.sleep(0.5) - try: - os.kill(pid, signal.SIGKILL) - except OSError: - pass - - -def _pid_cmdline_matches(pid: int, expected: str) -> Optional[bool]: - """`/proc//cmdline` を読んで `expected` を含むか。 - - /proc が読めない環境では None を返す(PID 再利用チェック非対応)。 - """ - try: - cmdline = pathlib.Path(f"/proc/{pid}/cmdline").read_text() - return expected.lower() in cmdline.lower() - except (FileNotFoundError, PermissionError, OSError): - return None - - -def _scan_patterns( - path: pathlib.Path, patterns: list[re.Pattern[str]] -) -> Optional[str]: - """err.log を末尾 200KB だけ読み、`patterns` の最初の **non-benign** ヒット行を返す。 - - BENIGN フィルタは **マッチ行そのもの** に対して適用し、誤検知 (markdown 引用 / - diff body / gemini の config validation 警告) を除外する。 - - 重要 1: `pat.search()` ではなく `pat.finditer()` を使い、benign で除外された場合も - 後続の一致を継続して走査する。これにより benign な先行ヒットの後ろにある本物の - エラーを見逃さない。 - - 重要 2: benign 判定は「マッチ行」だけを対象にする。以前は前後 40 文字の文脈窓を - 使っていたが、それだと benign 行が直前にあるだけで後続の本物エラーを誤って benign - 扱いしてしまった (例: `Error in: mcpServers.serena\\n...\\nTraceback ...` で - Traceback が誤抑制された)。 - """ - if not path.exists(): - return None - try: - sz = path.stat().st_size - with path.open("rb") as f: - if sz > 200 * 1024: - f.seek(sz - 200 * 1024) - data = f.read().decode("utf-8", errors="replace") - except OSError: - return None - - for pat in patterns: - for m in pat.finditer(data): - line_start = data.rfind("\n", 0, m.start()) + 1 - line_end = data.find("\n", m.end()) - line_end = line_end if line_end != -1 else len(data) - line = data[line_start:line_end] - # benign パターンはマッチ行そのものに当てる。markdown 引用や - # `Error in: mcpServers.X` のような行単位パターンは「その行」だけを - # 評価すれば判定可能で、文脈窓を広げると誤判定の原因になる。 - if any(b.search(line) for b in EARLY_ERROR_BENIGN): - continue - # マッチ部位が backtick / 日本語「」 で引用されている場合も benign。 - if _match_is_quoted(line, m.start() - line_start, m.end() - line_start): - continue - return line.strip() - return None - - -def _scan_early_fatal(path: pathlib.Path) -> Optional[str]: - return _scan_patterns(path, EARLY_ERROR_FATAL) - - -def _scan_early_warn(path: pathlib.Path) -> Optional[str]: - return _scan_patterns(path, EARLY_ERROR_WARN) - - -def _scan_codex_sentinel(path: pathlib.Path) -> bool: - if not path.exists(): - return False - try: - # 末尾 64KB を読む(sentinel は最後の方に出る) - sz = path.stat().st_size - with path.open("rb") as f: - if sz > 64 * 1024: - f.seek(sz - 64 * 1024) - tail = f.read().decode("utf-8", errors="replace") - except OSError: - return False - return bool(CODEX_SENTINEL.search(tail)) - - -def _safe_size(path: pathlib.Path) -> int: - try: - return path.stat().st_size if path.exists() else 0 - except OSError: - return 0 - - -def _tail_last_nonempty_line(path: pathlib.Path, limit: int = 4096) -> str: - """監視用 progress.log の末尾 1 行を安全に読む。 - - Gemini に書かせるのは短いフェーズマーカーだけなので、末尾数 KB で十分。 - 壊れた UTF-8 や読み取り競合があっても monitor 自体は落とさない。 - """ - if not path.exists(): - return "" - try: - sz = path.stat().st_size - with path.open("rb") as f: - if sz > limit: - f.seek(sz - limit) - data = f.read().decode("utf-8", errors="replace") - except OSError: - return "" - if sz > limit and "\n" in data: - data = data.split("\n", 1)[1] - for line in reversed(data.splitlines()): - stripped = line.strip() - if stripped: - return stripped[:200] - return "" - - -def monitor_agent( - agent: str, - pr: int, - timeout: int, - stall_timeout: int, - poll: int, - require_result: bool, - no_early_error: bool = False, - log_prefix: str = "", -) -> AgentStatus: - """1 agent を監視する。 - - `no_early_error=True` のとき、EARLY_ERROR 検知 (FATAL/WARN とも) を完全に無効化し、 - hard timeout / stall / sentinel / result.json のみで判定する。 - """ - paths = AgentPaths.for_(agent, pr) - status = AgentStatus(agent=agent) - started = time.monotonic() - - # 起動チェック: 30 秒待っても pidfile が無ければ起動失敗 - grace_end = started + 30 - while time.monotonic() < grace_end: - if paths.pidfile.exists(): - break - time.sleep(2) - pid = _read_pidfile(paths.pidfile) - if pid is None: - status.status = "PIDFILE_BAD" - status.exit_code = 6 - status.detail = f"pidfile not found: {paths.pidfile}" - _emit_log(log_prefix, agent, status) - return status - - status.pid = pid - # cmdline 検証 (PID 再利用対策) は **プロセスが生きていると確認できたときのみ** 行う。 - # 起動直後に既にプロセスが exit していると /proc/ が消えるか、別プロセスに - # 再利用されている可能性があり、ここで PIDFILE_BAD を返すと「完了している(result.json は出ている)」 - # ケースを誤って失敗にしてしまう。alive=True と確認した瞬間のみ cmdline 一致を検証する。 - - started_wall = time.time() - last_progress_size = ( - _safe_size(paths.err_log) - + _safe_size(paths.stdout_log) - + _safe_size(paths.progress_log) - ) - last_progress = time.monotonic() - cmdline_validated = False - warned_early_error = False - - while True: - elapsed = time.monotonic() - started - status.elapsed = elapsed - - # 1. プロセス生存確認 → 死んでいたら最終判定へ (result.json 存在をチェック) - alive = _pid_alive(pid) - if agent == "codex": - status.sentinel_seen = _scan_codex_sentinel(paths.err_log) - - # codex は `tokens used` sentinel を出した後もプロセスが exit せず常駐し続ける - # ケースがある (実機で観測)。result.json は正常に書かれているのに alive=True の - # まま stall_timeout に達して STALLED 化してしまう。sentinel + result.json が - # 揃った瞬間に対象プロセスを kill して OK 判定で返す。 - if ( - agent == "codex" - and alive - and status.sentinel_seen - and paths.result.exists() - and paths.result.stat().st_size > 0 - ): - _kill_pid(pid) - status.result_exists = True - status.status = "OK" - status.exit_code = 0 - status.detail = ( - f"codex sentinel + result.json detected; killed lingering pid {pid}" - ) - _emit_log(log_prefix, agent, status) - return status - - # result.json が書かれた後もプロセスがハング��るケース (gemini で観測: - # MCP サーバー切断待ち等��� exit しない)。sentinel 機構を持たない agent 向け - # の fallback: result.json の mtime が RESULT_AGE_GRACE 秒以上前であれば - # 完了とみなし、プロセスを kill → OK。 - # 安全条件: - # - cmdline_validated: PID 再利用でない (または検証不能環境) ことを確認済み - # - mtime >= started_wall: 前 round の stale result.json を拾わない - if ( - alive - and not status.sentinel_seen - and cmdline_validated - and paths.result.exists() - and paths.result.stat().st_size > 0 - ): - result_mtime = paths.result.stat().st_mtime - if result_mtime >= started_wall: - result_age = time.time() - result_mtime - if result_age >= RESULT_AGE_GRACE: - _kill_pid(pid) - status.result_exists = True - status.status = "OK" - status.exit_code = 0 - status.detail = ( - f"result.json exists for {result_age:.0f}s without process exit; " - f"killed lingering pid {pid}" - ) - _emit_log(log_prefix, agent, status) - return status - - if alive and not cmdline_validated: - # cmdline 検証は alive 確認後に 1 回だけ。生きていない瞬間に proc/ を読むと - # ファイル不在で None 扱いになり判定不能のため。 - cmdline_ok = _pid_cmdline_matches(pid, agent) - if cmdline_ok is False: - _kill_pid(pid) - status.status = "PIDFILE_BAD" - status.exit_code = 6 - status.detail = f"pid {pid} cmdline does not contain '{agent}' (stale pidfile?)" - _emit_log(log_prefix, agent, status) - return status - if cmdline_ok is True: - cmdline_validated = True - - # 2. hard timeout - if elapsed >= timeout: - if alive: - _kill_pid(pid) - status.status = "TIMEOUT" - status.exit_code = 2 - status.detail = f"hard timeout {timeout}s reached (pid {pid})" - _emit_log(log_prefix, agent, status) - return status - - # 3. early error - # 明確な致命 (FATAL) のみ kill する。曖昧パターン (生 Error: / Traceback) は - # WARN として警告ログのみ。codex がレビュー対象 diff の test コード片を - # echo するケースや gemini の config validation 警告で誤 kill されるのを防ぐ。 - if not no_early_error: - fatal_err = _scan_early_fatal(paths.err_log) - if fatal_err: - if alive: - _kill_pid(pid) - status.status = "EARLY_ERROR" - status.exit_code = 4 - status.detail = f"early error (fatal) in err.log: {fatal_err[:200]}" - _emit_log(log_prefix, agent, status) - return status - - if not warned_early_error: - warn_err = _scan_early_warn(paths.err_log) - if warn_err: - print( - f"{log_prefix}⚠️ {agent} early-error WARN " - f"(non-fatal, not killing): {warn_err[:200]}", - file=sys.stderr, flush=True, - ) - warned_early_error = True - - if not alive: - # プロセス終了 — result.json を確認 - status.result_exists = paths.result.exists() and paths.result.stat().st_size > 0 - if status.result_exists or not require_result: - status.status = "OK" - status.exit_code = 0 - status.detail = ( - f"process exited; sentinel={status.sentinel_seen}; " - f"result_exists={status.result_exists}" - ) - else: - status.status = "NO_RESULT" - status.exit_code = 3 - status.detail = f"process exited but result.json missing: {paths.result}" - _emit_log(log_prefix, agent, status) - return status - - # 4. stall detection (err.log / stdout.log / progress.log をモニタ。 - # gemini は stdout 側だけ進捗が出るケースがあり、progress.log には - # launcher が要求した短いフェーズマーカーが出るため、いずれかが - # 更新されれば progress として扱う) - status.err_log_size = _safe_size(paths.err_log) - status.stdout_log_size = _safe_size(paths.stdout_log) - status.progress_log_size = _safe_size(paths.progress_log) - status.progress_tail = _tail_last_nonempty_line(paths.progress_log) - progress_size = ( - status.err_log_size + status.stdout_log_size + status.progress_log_size - ) - if progress_size != last_progress_size: - last_progress_size = progress_size - last_progress = time.monotonic() - status.idle_seconds = time.monotonic() - last_progress - if status.idle_seconds >= stall_timeout: - if alive: - _kill_pid(pid) - status.status = "STALLED" - status.exit_code = 5 - status.detail = ( - f"no log progress for {stall_timeout}s " - f"(pid {pid}, last size {last_progress_size}B)" - ) - _emit_log(log_prefix, agent, status) - return status - - # poll 中の進捗ログ - _emit_progress(log_prefix, agent, status) - time.sleep(poll) - - -def _emit_progress(prefix: str, agent: str, st: AgentStatus) -> None: - progress = f" progress={st.progress_tail!r}" if st.progress_tail else "" - print( - f"{prefix}⏳ {agent} elapsed={st.elapsed:.0f}s pid={st.pid} " - f"idle={st.idle_seconds:.0f}s " - f"err={st.err_log_size}B stdout={st.stdout_log_size}B " - f"progress_log={st.progress_log_size}B " - f"sentinel={'Y' if st.sentinel_seen else '-'}{progress}", - file=sys.stderr, flush=True, - ) - - -def _emit_log(prefix: str, agent: str, st: AgentStatus) -> None: - icon = { - "OK": "✅", "TIMEOUT": "⏰", "NO_RESULT": "❌", - "EARLY_ERROR": "💥", "STALLED": "🛑", "PIDFILE_BAD": "❓", - }.get(st.status, "?") - print( - f"{prefix}{icon} {agent} {st.status} ({st.elapsed:.0f}s) — {st.detail}", - file=sys.stderr, flush=True, - ) - - -# ---------- CLI ---------- - -def main() -> None: - p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - p.add_argument("pr", type=int) - p.add_argument("target", choices=["codex", "gemini", "both"]) - # env (MONITOR_TIMEOUT / MONITOR_POLL) は呼び出し時に safe parse で読む。 - # 非数値設定でも fixed default (`DEFAULT_TIMEOUT` / `DEFAULT_POLL`) に戻す。 - timeout_default = _safe_int_env("MONITOR_TIMEOUT", DEFAULT_TIMEOUT) - poll_default = _safe_int_env("MONITOR_POLL", DEFAULT_POLL) - p.add_argument("--timeout", type=int, default=timeout_default, - help=f"hard timeout in seconds (default: {timeout_default})") - p.add_argument("--stall-timeout", type=int, default=None, - help="stall timeout (err.log no progress) in seconds. " - "未指定時は agent 別既定 (codex=180, gemini=480) または " - "env MONITOR_STALL_ / MONITOR_STALL を参照") - p.add_argument("--poll", type=int, default=poll_default, - help=f"poll interval in seconds (default: {poll_default})") - p.add_argument("--no-require-result", action="store_true", - help="プロセス終了後に result.json が無くても OK 扱い") - p.add_argument("--no-early-error", action="store_true", - default=DEFAULT_NO_EARLY_ERROR, - help="EARLY_ERROR 検知を無効化 " - "(hard timeout / stall / sentinel / result.json のみで判定) " - f"[env: MONITOR_NO_EARLY_ERROR; default: {DEFAULT_NO_EARLY_ERROR}]") - args = p.parse_args() - - agents = ["codex", "gemini"] if args.target == "both" else [args.target] - require_result = not args.no_require_result - - results: dict[str, AgentStatus] = {} - - def run(agent: str) -> None: - stall = args.stall_timeout if args.stall_timeout is not None \ - else _agent_stall_default(agent) - results[agent] = monitor_agent( - agent=agent, pr=args.pr, - timeout=args.timeout, stall_timeout=stall, - poll=args.poll, require_result=require_result, - no_early_error=args.no_early_error, - log_prefix=f"[{agent}] ", - ) - - threads = [threading.Thread(target=run, args=(a,), daemon=False) for a in agents] - for t in threads: - t.start() - for t in threads: - t.join() - - # 結果出力: 1 行 1 JSON - for agent in agents: - st = results[agent] - print(json.dumps({ - "agent": agent, - "status": st.status, - "exit_code": st.exit_code, - "pid": st.pid, - "elapsed": round(st.elapsed, 1), - "detail": st.detail, - "err_log_size": st.err_log_size, - "stdout_log_size": st.stdout_log_size, - "progress_log_size": st.progress_log_size, - "progress_tail": st.progress_tail, - "idle_seconds": round(st.idle_seconds, 1), - "result_exists": st.result_exists, - "sentinel_seen": st.sentinel_seen, - }, ensure_ascii=False)) - - # exit code: 全エージェントの最大値(OK=0 が最良、それ以外は失敗) - sys.exit(max(results[a].exit_code for a in agents)) - - -if __name__ == "__main__": - main() diff --git a/plugins/ndf/skills-codex/cross-review/scripts/rotate-pr.sh b/plugins/ndf/skills-codex/cross-review/scripts/rotate-pr.sh deleted file mode 100755 index 9b063f4..0000000 --- a/plugins/ndf/skills-codex/cross-review/scripts/rotate-pr.sh +++ /dev/null @@ -1,362 +0,0 @@ -#!/usr/bin/env bash -# PR rotation — light モード (default) / squash モード -# -# Usage: -# rotate-pr.sh prepare -# 旧 PR の title/body/isDraft + git log $BASE..HEAD + git diff --stat を -# $TMP_DIR/rotate-pr-prepare.json に dump する。 -# light モードでは メインセッションの Agent が prepare.json を読み、 -# 現状の差分・実装を反映した新 title/body を生成して -# $TMP_DIR/rotate-pr-newtext.json に書き出すこと。 -# -# rotate-pr.sh execute [--mode light|squash] (default: light) -# light : 同ブランチで旧 PR を close → 同 head/base で新 PR を作成。 -# title/body は newtext.json から流す。元 PR の isDraft をコピー。 -# PR title に内部用語 (rotated/round/cross-review) は付与しない。 -# squash : squash 統合した新ブランチ (-rHHMMSS suffix) + 新 PR。 -# title 末尾に "(rotated)"、body は automated text。 -# いずれも stdout に NEW_PR= / NEW_PR_URL= / NEW_BRANCH= を出力。 -# -# rotate-pr.sh (deprecated) -# 旧 1 引数形式。`execute --mode squash` 相当として動くが、stderr に -# deprecation warning を出す。新規呼び出しは prepare → execute 形式へ移行。 -# -# 引数 STATE_PR は state.json の key (= 最初に init した PR 番号)。 -# 閉じる「現在の PR」は state.json の `current_pr` を読む。 -# state.json の current_pr / pr_history 更新は `state.py set-current-pr` で別途行う。 - -set -euo pipefail - -SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) -# shellcheck source=_tmpdir.sh -. "$SCRIPT_DIR/_tmpdir.sh" -TMP_DIR=$(tmpdir) - -usage() { - cat >&2 <<'USAGE' -Usage: - rotate-pr.sh prepare - rotate-pr.sh execute [--mode light|squash] (default: light) - rotate-pr.sh (deprecated, = execute --mode squash) -USAGE -} - -# close 後に新 PR create が失敗した場合の rollback hook (light/squash 共通)。 -# OLD_PR はグローバル (load_state で set される) を参照する。 -# 両モードから `trap reopen_old_pr_on_failure ERR` で登録し、create 成功直後に -# `trap - ERR` で解除する (gemini round 6 指摘: 関数定義の重複排除 + EXIT ではなく -# ERR で hook して PR 作成後の処理失敗による誤発火を避ける)。 -reopen_old_pr_on_failure() { - local exit_code=$? - echo "⚠ 新 PR 作成系処理に失敗 (exit=$exit_code) — 旧 PR #${OLD_PR:-?} を reopen します" >&2 - if [ -n "${OLD_PR:-}" ]; then - gh pr reopen "$OLD_PR" >&2 || echo "⚠ 旧 PR #$OLD_PR の reopen にも失敗しました。手動で確認してください。" >&2 - fi -} - -# state.json から共通情報を読み出して shell 変数にセットする。 -# 呼び出し後: STATE_FILE / WORKTREE / OLD_PR / ROUND_IN_PR が使える。 -load_state() { - local state_pr=$1 - STATE_FILE=$TMP_DIR/cross-review-pr$state_pr-state.json - [ -s "$STATE_FILE" ] || { echo "state.json not found: $STATE_FILE" >&2; exit 1; } - WORKTREE=$(jq -r '.worktree_path' "$STATE_FILE") - OLD_PR=$(jq -r '.current_pr' "$STATE_FILE") - ROUND_IN_PR=$(jq --argjson p "$OLD_PR" '[.rounds[] | select(.pr == $p)] | length' "$STATE_FILE") -} - -cmd_prepare() { - local state_pr=${1:?STATE_PR required} - load_state "$state_pr" - - cd "$WORKTREE" - - local pr_json - pr_json=$(gh pr view "$OLD_PR" \ - --json number,url,title,body,headRefName,baseRefName,isDraft) - local head_branch base_branch - head_branch=$(jq -r '.headRefName' <<<"$pr_json") - base_branch=$(jq -r '.baseRefName' <<<"$pr_json") - - # base が origin にあることを保証 (git log/diff のため) - if ! git fetch --quiet origin "$base_branch"; then - echo "⚠ git fetch origin $base_branch に失敗しました。ローカル参照のみで継続します。" >&2 - fi - if ! git rev-parse --verify --quiet "origin/$base_branch" >/dev/null; then - echo "⚠ origin/$base_branch が見つかりません。git_log / git_diff_stat は空になります。" >&2 - fi - - local range="origin/$base_branch..HEAD" - local git_log git_diff_stat - git_log=$(git log --pretty=format:'%h %s' "$range" 2>/dev/null || echo "") - git_diff_stat=$(git diff --stat "$range" 2>/dev/null || echo "") - - local out=$TMP_DIR/rotate-pr$state_pr-prepare.json - jq -n \ - --argjson state_pr "$state_pr" \ - --argjson pr "$pr_json" \ - --argjson round_in_pr "$ROUND_IN_PR" \ - --arg worktree "$WORKTREE" \ - --arg git_log "$git_log" \ - --arg git_diff_stat "$git_diff_stat" \ - '{ - state_pr: $state_pr, - old_pr: $pr.number, - old_pr_url: $pr.url, - worktree_path: $worktree, - head_branch: $pr.headRefName, - base_branch: $pr.baseRefName, - is_draft: $pr.isDraft, - round_in_pr: $round_in_pr, - old_title: $pr.title, - old_body: ($pr.body // ""), - git_log: $git_log, - git_diff_stat: $git_diff_stat - }' > "$out" - - echo "✅ prepare.json 書き出し: $out" >&2 - # 呼び出し側 (SKILL.md / docs) は eval で stdout を取り込む契約。 - # PR の head/base 由来の値は shell メタ文字を含み得るため必ず printf '%q' で - # シェルエスケープしてから出力する (例: ブランチ名に "; rm -rf / 等が来ても安全)。 - printf 'PREPARE_JSON=%q\n' "$out" - printf 'OLD_PR=%q\n' "$OLD_PR" - printf 'HEAD_BRANCH=%q\n' "$head_branch" - printf 'BASE_BRANCH=%q\n' "$base_branch" - printf 'IS_DRAFT=%q\n' "$(jq -r '.isDraft' <<<"$pr_json")" -} - -# light モード本体: 同ブランチで旧 PR を close → 同 head/base で新 PR 作成。 -execute_light() { - local state_pr=$1 - load_state "$state_pr" - - local prep=$TMP_DIR/rotate-pr$state_pr-prepare.json - local newtext=$TMP_DIR/rotate-pr$state_pr-newtext.json - [ -s "$prep" ] || { echo "prepare.json not found: $prep — 先に rotate-pr.sh prepare $state_pr を実行してください" >&2; exit 1; } - [ -s "$newtext" ] || { echo "newtext.json not found: $newtext — Agent (general-purpose) で title/body を生成して書き出してください" >&2; exit 1; } - - local head_branch base_branch is_draft new_title new_body - head_branch=$(jq -r '.head_branch' "$prep") - base_branch=$(jq -r '.base_branch' "$prep") - is_draft=$(jq -r '.is_draft' "$prep") - new_title=$(jq -r '.title' "$newtext") - new_body=$(jq -r '.body' "$newtext") - - [ -n "$new_title" ] && [ "$new_title" != "null" ] || { echo "newtext.json に .title がない" >&2; exit 1; } - # body は空文字列を許容 (GitHub は空 body を許容)。null のみ拒否。 - [ "$new_body" != "null" ] || { echo "newtext.json に .body がない (null)" >&2; exit 1; } - - cd "$WORKTREE" - - echo "🔄 PR #$OLD_PR rotation (light): 同ブランチ $head_branch で巻き直し (base=$base_branch)" >&2 - - # 1. 作業ディレクトリに未 push のコミットがある可能性に備え、close 前に push する。 - # push しないと新 PR に最新コミットが反映されないケースがあるため必須 (gemini 指摘)。 - # state.py init は worktree を detached HEAD で作るため、ブランチ名のみで push すると - # detached HEAD 上の修正コミットが push されない。HEAD: 形式で現在の HEAD を - # 明示する (codex 指摘)。--force / --no-verify は禁止。 - echo "🔼 git push origin HEAD:$head_branch (未 push commit が無ければ no-op)" >&2 - git push origin HEAD:"$head_branch" - - # 2. 旧 PR を close (コメント残し) - gh pr comment "$OLD_PR" --body "ℹ️ レビューコメント履歴整理のため本 PR を一度 close し、同じブランチ \`$head_branch\` で新 PR を作り直します。ブランチの内容・base は変えません。" - gh pr close "$OLD_PR" - - # close 後に create が失敗した場合は旧 PR を reopen して rotation の途中停止を回避する - # (関数定義は file 冒頭で共通化, gemini round 6 指摘) - trap reopen_old_pr_on_failure ERR - - # 3. 新 PR を同 head/base で作成 (Draft 状態は元 PR から継承)。 - # body は --body-file - 経由で stdin から渡し、argv 長制限を回避する (gemini 指摘)。 - local create_args=(--base "$base_branch" --head "$head_branch" --title "$new_title" --body-file -) - if [ "$is_draft" = "true" ]; then - create_args+=(--draft) - fi - local new_pr_url - new_pr_url=$(printf '%s' "$new_body" | gh pr create "${create_args[@]}") - - # gh pr create 成功直後に trap を解除し、後続の URL parse / echo 等が失敗しても - # 新旧 PR が重複して開く事態を避ける (gemini round 6 指摘)。 - trap - ERR - - # PR 番号は create 出力 URL の末尾セグメントから抽出 (gh pr view 追加呼び出しを削減, - # gemini round 6 指摘)。URL 形式: https://github.com///pull/ - local new_pr=${new_pr_url##*/} - - echo "✅ 新 PR #$new_pr: $new_pr_url" >&2 - # eval される契約。head_branch / URL に shell メタ文字が混ざっても安全なよう %q で escape - printf 'NEW_PR=%q\n' "$new_pr" - printf 'NEW_PR_URL=%q\n' "$new_pr_url" - printf 'NEW_BRANCH=%q\n' "$head_branch" -} - -# squash モード本体。 -execute_squash() { - local state_pr=$1 - load_state "$state_pr" - - cd "$WORKTREE" - - local branch base title new_branch pr_meta prep - prep=$TMP_DIR/rotate-pr$state_pr-prepare.json - - # PR メタ情報 (base / title / head) は、まず prepare.json があればそこから読み出し、 - # 無い場合のみ gh pr view にフォールバックする (execute_light と同じ方針で - # 不要な API 呼び出しを排除, gemini round 8 指摘)。 - if [ -s "$prep" ]; then - base=$(jq -r '.base_branch // empty' "$prep") - title=$(jq -r '.old_title // empty' "$prep") - fi - if [ -z "${base:-}" ] || [ -z "${title:-}" ]; then - pr_meta=$(gh pr view "$OLD_PR" --json headRefName,baseRefName,title) - [ -n "${base:-}" ] || base=$(printf '%s' "$pr_meta" | jq -r '.baseRefName') - [ -n "${title:-}" ] || title=$(printf '%s' "$pr_meta" | jq -r '.title') - fi - - # state.py init は worktree を `git worktree add --detach origin/` で作るため、 - # `git branch --show-current` は空文字を返す。空のまま new_branch を生成すると - # `-rHHMMSS` だけのブランチ名になってしまうので、フォールバック順を以下に固定する: - # 1. git branch --show-current (通常 worktree なら使える) - # 2. prepare.json の head_branch (prepare 済みなら最も信頼できる) - # 3. gh pr view --json headRefName (prepare 未実行でも復元可能) - # (codex round 4 指摘) - branch=$(git branch --show-current) - if [ -z "$branch" ] && [ -s "$prep" ]; then - branch=$(jq -r '.head_branch // empty' "$prep") - fi - if [ -z "$branch" ]; then - pr_meta=${pr_meta:-$(gh pr view "$OLD_PR" --json headRefName,baseRefName,title)} - branch=$(printf '%s' "$pr_meta" | jq -r '.headRefName') - fi - [ -n "$branch" ] || { echo "head branch を復元できませんでした (detached worktree かつ prepare.json / gh pr view から取得失敗)" >&2; exit 1; } - new_branch="${branch}-r$(date +%H%M%S)" - - # 既に title 末尾に "(rotated)" / "(rotated2)" 等が付いている場合は除去してから - # "(rotated)" を 1 つだけ付与し、ローテーションのたびに suffix が重複しないようにする - # (gemini round 8 指摘)。 - # 例: - # "Fix foo" → "Fix foo (rotated)" - # "Fix foo (rotated)" → "Fix foo (rotated)" - # "Fix foo (rotated2)" → "Fix foo (rotated)" - # "Fix foo (rotated)(rotated)" → "Fix foo (rotated)" - local title_stripped=$title - while [[ $title_stripped =~ [[:space:]]*\(rotated[0-9]*\)$ ]]; do - title_stripped=${title_stripped%"${BASH_REMATCH[0]}"} - done - local new_title="$title_stripped (rotated)" - - echo "🔄 PR #$OLD_PR rotation (squash): $branch → $new_branch (base=$base)" >&2 - - # 1. 既存ブランチを squash して新ブランチに - git checkout -b "$new_branch" - git reset --soft "origin/$base" - # commit message は -m を複数指定で分割して渡す。$(cat < -EOF -) - local new_pr_url - new_pr_url=$(printf '%s' "$new_body" | gh pr create --base "$base" --title "$new_title" --body-file -) - - # gh pr create 成功直後に trap を解除し、後続の URL parse / echo 等が失敗しても - # 新旧 PR が重複して開く事態を避ける (gemini round 6 指摘)。 - trap - ERR - - # PR 番号は create 出力 URL の末尾セグメントから抽出 (gh pr view 追加呼び出しを削減, - # gemini round 6 指摘)。URL 形式: https://github.com///pull/ - local new_pr=${new_pr_url##*/} - - echo "✅ 新 PR #$new_pr: $new_pr_url" >&2 - # eval される契約。new_branch / URL に shell メタ文字が混ざっても安全なよう %q で escape - printf 'NEW_PR=%q\n' "$new_pr" - printf 'NEW_PR_URL=%q\n' "$new_pr_url" - printf 'NEW_BRANCH=%q\n' "$new_branch" -} - -cmd_execute() { - local state_pr=${1:?STATE_PR required} - shift - # --mode 未指定時は light を default (SKILL.md / 02-fix-and-rotation.md / スクリプト - # 冒頭コメントの「light モード (default)」表記に CLI 契約を揃える, codex round 4 指摘) - local mode="light" - while [ $# -gt 0 ]; do - case $1 in - --mode) - mode=${2:?--mode requires light|squash} - shift 2 - ;; - --mode=*) - mode=${1#--mode=} - shift - ;; - *) - echo "unknown arg: $1" >&2 - usage - exit 2 - ;; - esac - done - case $mode in - light) execute_light "$state_pr" ;; - squash) execute_squash "$state_pr" ;; - *) echo "invalid --mode: $mode (light|squash)" >&2; exit 2 ;; - esac -} - -# ---- entrypoint ---- - -if [ $# -eq 0 ]; then - usage - exit 2 -fi - -case $1 in - prepare) - shift - cmd_prepare "$@" - ;; - execute) - shift - cmd_execute "$@" - ;; - -h|--help) - usage - ;; - *) - # 旧形式: rotate-pr.sh → squash 相当 - if [ $# -eq 1 ] && [[ $1 =~ ^[0-9]+$ ]]; then - echo "⚠ DEPRECATED: rotate-pr.sh 形式は廃止予定です。新形式に移行してください:" >&2 - echo " rotate-pr.sh prepare $1" >&2 - echo " rotate-pr.sh execute $1 --mode light|squash" >&2 - echo " (本実行は --mode squash 相当で継続します)" >&2 - execute_squash "$1" - else - usage - exit 2 - fi - ;; -esac diff --git a/plugins/ndf/skills-codex/cross-review/scripts/state.py b/plugins/ndf/skills-codex/cross-review/scripts/state.py deleted file mode 100755 index 9e91f11..0000000 --- a/plugins/ndf/skills-codex/cross-review/scripts/state.py +++ /dev/null @@ -1,1481 +0,0 @@ -#!/usr/bin/env -S uv run --script -# /// script -# requires-python = ">=3.10" -# dependencies = [] -# /// -"""cross-review state.json 操作 CLI。 - -`/.cross_review/cross-review-pr-state.json` の初期化 / 読み書きと、 -ループ判定(round 開始 / 収束 / 振動 / PR ローテーション要否 / fix 結果マージ / -deferred nit レポート)を 1 つの CLI に集約する。 - -Subcommands: - init Step 0 state 初期化 or 再開(プリチェック込み) - start-round Step 1 round 開始判定 (ROUND/ROUND_IN_PR/PR を stdout に出す) - read-result Step 2.5 codex/gemini の result.json を state にマージ - judge Step 3 intent ベース pass 判定 (exit 0=approved, 2=continue) - check-oscillation Step 4 path:line 重複率を計算 - merge-fix Step 5 post fix サブエージェント戻り値を state にマージ + CI 分類 - should-rotate Step 6 rotate_after 到達判定 (exit 0=rotate, 2=keep) - set-current-pr PR ローテーション後の current_pr 更新 - report Step 8 deferred nit + ラウンドサマリ表示 - -すべての出力は人間可読 + KEY=VALUE 形式(eval / read で取り回し可能)。 -""" -from __future__ import annotations - -import argparse -import datetime as _dt -import json -import os -import pathlib -import shlex -import subprocess -import sys -import tempfile -import time -from typing import Any - - -# ---------------- helpers ---------------- - -DOC_EXTENSIONS = { - ".md", ".mdx", ".rst", ".txt", ".adoc", ".asciidoc", -} -DOC_FILENAMES = { - "readme", "license", "changelog", "contributing", "codeowners", -} -CODE_EXTENSIONS = { - ".c", ".cc", ".cpp", ".cs", ".css", ".dart", ".ex", ".exs", ".go", ".h", - ".hpp", ".html", ".java", ".js", ".jsx", ".kt", ".kts", ".php", ".py", - ".rb", ".rs", ".scala", ".scss", ".sh", ".sql", ".swift", ".ts", ".tsx", - ".vue", ".yaml", ".yml", -} -MIGRATION_PATH_MARKERS = ( - "/migrations/", "/migration/", "/db/migrate/", "/database/migrations/", - "/alembic/versions/", "/prisma/migrations/", -) -MIGRATION_NAME_MARKERS = ( - "migration", "migrate", "schema.sql", "schema.prisma", -) -TEST_PATH_MARKERS = ( - "/test/", "/tests/", "/spec/", "/specs/", "__tests__/", -) -TEST_NAME_MARKERS = ( - ".test.", ".spec.", "_test.", "_spec.", "test_", "spec_", -) -DEPENDENCY_FILENAMES = { - "package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock", - "composer.json", "composer.lock", "gemfile", "gemfile.lock", - "go.mod", "go.sum", "requirements.txt", "requirements-dev.txt", - "pyproject.toml", "poetry.lock", "uv.lock", "cargo.toml", "cargo.lock", - "pom.xml", "build.gradle", "build.gradle.kts", -} -CI_CONFIG_MARKERS = ( - "/.github/workflows/", "/.gitlab-ci", "/.circleci/", "/.buildkite/", - "/.kiro/", "/.claude/", "/.codex/", -) -CONFIG_EXTENSIONS = { - ".json", ".toml", ".yaml", ".yml", ".ini", ".env", ".example", -} -API_CONTRACT_MARKERS = ( - "/api/", "/routes/", "/controllers/", "/openapi", "/swagger", - "/proto/", "/graphql/", "/schemas/", -) -AUTH_SECURITY_TOKEN_MARKERS = ( - "auth", "authn", "authz", "permission", "policy", "role", "oauth", "jwt", - "session", "csrf", "cors", "token", -) -AUTH_SECURITY_SUBSTRING_MARKERS = ( - "authentication", "authorization", "authenticat", "authoriz", - "secret", "password", "credential", -) -FRONTEND_EXTENSIONS = { - ".css", ".scss", ".sass", ".less", ".html", ".jsx", ".tsx", ".vue", ".svelte", -} -PERFORMANCE_MARKERS = ( - "cache", "queue", "job", "worker", "concurrent", "parallel", - "batch", "stream", "pagination", "performance", -) -GENERATED_MARKERS = ( - "/dist/", "/build/", "/generated/", "/vendor/", "/node_modules/", -) -I18N_MARKERS = ( - "/locales/", "/locale/", "/i18n/", "/translations/", -) -I18N_EXTENSIONS = {".po", ".pot"} -INFRA_MARKERS = ( - "/terraform/", "/helm/", "/k8s/", "/kubernetes/", "/docker/", - "dockerfile", "docker-compose", -) -INFRA_EXTENSIONS = {".tf", ".tfvars"} - - -COMMON_REVIEW_TEMPLATE = """## 自動追加レビュー観点: 共通 -- PR の目的と変更範囲が一貫しているか。余分な変更、未説明の仕様変更、将来の保守を難しくする設計がないか。 -- 変更が既存仕様・既存コメント・既存コードの前提と矛盾しないか。重複指摘は避け、根拠がある修正アクションだけを指摘する。 -- テスト、検証手順、エラーハンドリング、ロールバック容易性が変更リスクに見合っているか。""" - -DOCS_ONLY_REVIEW_TEMPLATE = """## 自動追加レビュー観点: ドキュメントのみ PR -- ドキュメントの主張・企画・手順が妥当で、読者の意思決定や作業を誤らせないか。 -- コード、設定、コマンド、既存 README / docs / CHANGELOG との整合性が取れているか。古い名称、存在しないパス、実装と違う説明がないか。 -- ドキュメント間で用語、前提、バージョン、責務分担が矛盾していないか。 -- 追加・更新された説明が必要十分で、曖昧な表現や未検証の断定がないか。""" - -CODE_REVIEW_TEMPLATE = """## 自動追加レビュー観点: コード変更 PR -- 設計、正確性、可読性、保守性、単純さを確認する。不要に複雑な分岐、責務の混在、過剰な抽象化がないか。 -- 冗長・重複コード、既存ヘルパや標準 API で置き換えられる処理、言語・フレームワークらしくない実装がないか。 -- 関数・クラス・ファイルのサイズと責務が適切か。長すぎる関数、肥大化したファイル、名前と実態がずれた単位がないか。 -- セキュリティ観点として、入力検証、出力エンコード、認可、秘密情報、ログ、例外、外部コマンド、SQL/HTML/パス操作の扱いを確認する。 -- テストの有無と質、境界値、失敗系、後方互換性、性能・並行性・リソース解放のリスクを確認する。""" - -DB_MIGRATION_REVIEW_TEMPLATE = """## 自動追加レビュー観点: DB migration / schema 変更 -- データ設計としてテーブル、カラム、型、NULL 可否、default、制約、外部キー、unique/index がドメイン要件に合っているか。 -- 型の粒度が妥当か。文字列で持つべきでない値、過剰に広い型、精度不足、timezone、JSON の濫用がないか。 -- 既存データへの影響、backfill、ロック時間、index 作成、ロールバック、アプリケーションの段階的デプロイ順序に問題がないか。 -- migration とモデル、クエリ、ドキュメント、テストデータの整合性が取れているか。""" - -TEST_REVIEW_TEMPLATE = """## 自動追加レビュー観点: テスト変更 -- テストが実装詳細ではなくユーザー影響・仕様・境界値・失敗系を検証しているか。 -- flaky になりやすい時間、乱数、順序、外部サービス、並行実行、共有状態への依存がないか。 -- テスト名、fixture、期待値が読みやすく、失敗時に原因を特定しやすいか。""" - -DEPENDENCY_REVIEW_TEMPLATE = """## 自動追加レビュー観点: 依存関係変更 -- 追加・更新された依存の必要性、ライセンス、メンテナンス状況、既存依存との重複を確認する。 -- lockfile と manifest の整合性、間接依存の大きな変化、ビルド/実行環境への影響を確認する。 -- 依存更新がセキュリティ、互換性、バンドルサイズ、起動時間に与える影響を確認する。""" - -CONFIG_CI_REVIEW_TEMPLATE = """## 自動追加レビュー観点: CI / 設定変更 -- CI 条件、権限、secret 参照、cache key、artifact、並列実行、失敗時の検知性が妥当か。 -- 設定変更がローカル・CI・本番で食い違わないか。環境変数の既定値と `.env.example` 相当の説明が揃っているか。 -- 自動化が過剰な権限や予期せぬ副作用を持たないか。""" - -API_CONTRACT_REVIEW_TEMPLATE = """## 自動追加レビュー観点: API / 契約変更 -- 入出力スキーマ、HTTP status、エラー形式、互換性、バージョニング、既存クライアントへの影響を確認する。 -- バリデーション、認可、ページング、冪等性、レート制限、監査ログが要件に合っているか。 -- API ドキュメント、型定義、テスト、実装の整合性が取れているか。""" - -AUTH_SECURITY_REVIEW_TEMPLATE = """## 自動追加レビュー観点: 認証・認可・機密情報 -- 認証、認可、ロール、所有者チェック、テナント境界が欠落または過剰許可になっていないか。 -- token、password、secret、PII がログ、例外、レスポンス、コミット差分に漏れていないか。 -- CSRF/CORS/session/JWT/OAuth などの設定が安全で、失効・更新・リプレイ対策が妥当か。""" - -FRONTEND_REVIEW_TEMPLATE = """## 自動追加レビュー観点: フロントエンド / UX -- UI 状態、loading/error/empty、キーボード操作、アクセシビリティ、レスポンシブ表示が破綻しないか。 -- コンポーネント責務、重複 UI、状態管理、不要な再レンダリング、バンドルサイズへの影響を確認する。 -- 表示文言、フォーム validation、ユーザー操作後のフィードバックが仕様と一致しているか。""" - -PERFORMANCE_REVIEW_TEMPLATE = """## 自動追加レビュー観点: 性能・並行性 -- N+1、不要な全件取得、過剰な同期 I/O、メモリ保持、ロック、競合、リトライ嵐がないか。 -- cache、batch、pagination、stream、queue/worker の使い方が正しく、失敗時の再実行や重複実行に耐えるか。 -- 計測・ログ・アラートが問題発生時の切り分けに足りるか。""" - -DELETION_RENAME_REVIEW_TEMPLATE = """## 自動追加レビュー観点: 削除・リネーム -- 削除・リネーム対象への参照がコード、設定、CI、ドキュメント、テスト、外部連携に残っていないか。 -- 後方互換性、migration、deprecation、利用者への移行手順が必要ないか。 -- 同名別ファイルや大文字小文字差による環境依存の問題がないか。""" - -GENERATED_REVIEW_TEMPLATE = """## 自動追加レビュー観点: 生成物・ロックファイル -- 生成物が本当にコミット対象か。生成元との差分、再生成手順、不要なノイズが混入していないか。 -- lockfile の差分が意図した依存変更に対応しているか。手編集や不整合がないか。""" - -I18N_REVIEW_TEMPLATE = """## 自動追加レビュー観点: i18n / 文言 -- 翻訳キー、fallback、変数展開、複数形、日付・数値・通貨・タイムゾーン表記が妥当か。 -- 原文と翻訳、UI 表示幅、アクセシビリティラベル、ドキュメント文言の整合性を確認する。""" - -INFRA_REVIEW_TEMPLATE = """## 自動追加レビュー観点: インフラ / デプロイ -- 環境差分、権限、secret、ネットワーク公開範囲、永続化、バックアップ、スケール、ロールバック容易性を確認する。 -- IaC / manifest / Dockerfile の設定が最小権限・再現可能・運用監視しやすい形になっているか。 -- 既存環境への破壊的変更、手動作業、順序依存、ダウンタイムのリスクが説明されているか。""" - -def _default_worktree_base() -> pathlib.Path: - """worktree の親ディレクトリを解決する。 - - 優先順位: - 1. 環境変数 NDF_WORKTREE_BASE (明示オーバーライド) - 2. <システム tmpdir>/ndf-worktrees (非永続領域。コンテナ再作成で自動消滅) - - かつての /work/worktrees ($HOME/work/worktrees) は共有の永続 volume 上にあり、 - 別リポジトリの pr と衝突する・明示削除が必要・volume を消費する問題が - あったため廃止した。 - """ - env = os.environ.get("NDF_WORKTREE_BASE") - if env: - # 相対パスのまま state.json に保存されると後続のパス比較が壊れるため、 - # 常に絶対パスへ解決して返す。 - return pathlib.Path(env).resolve() - return pathlib.Path(tempfile.gettempdir()) / "ndf-worktrees" - - -def _repo_slug(repo: str) -> str: - """`owner/name` を path-safe なディレクトリ名 `owner--name` に変換する。""" - return repo.replace("/", "--") - - -def _is_registered_worktree(path: str) -> bool: - """path が現リポジトリに登録済みの worktree かどうか。 - - パスが存在しても別リポジトリの残骸や git 管理外ディレクトリの可能性があり、 - 流用すると git 操作が壊れるため、流用前に必ずこれで検証する。 - """ - out = _sh(["git", "worktree", "list", "--porcelain"], check=False) - target = str(pathlib.Path(path).resolve()) - return any(line == f"worktree {target}" for line in out.splitlines()) - - -def _create_worktree(worktree: str, pr: int, head_branch: str) -> None: - """origin/ から detached worktree を作成する (フォーク PR はフォールバック)。""" - pathlib.Path(worktree).parent.mkdir(parents=True, exist_ok=True) - # worktree を /tmp 等の非永続領域に置くと、実体だけ消えて親リポジトリの - # 登録 (prunable) が残ることがある。その状態で `git worktree add` すると - # 「パス登録済み」として失敗するため、追加前に prune で掃除しておく。 - subprocess.run( - ["git", "worktree", "prune"], - capture_output=True, text=True, - ) - # フォーク PR の場合 origin に head_branch がないことがある。 - # fetch 失敗時は gh pr checkout --detach でフォールバックする。 - fetch_result = subprocess.run( - ["git", "fetch", "origin", head_branch], - capture_output=True, text=True, - ) - if fetch_result.returncode == 0: - # head branch が既に別の worktree で checkout されている場合を避けるため - # detached で展開する。cross-review はファイル参照しかしないので問題ない。 - _sh(["git", "worktree", "add", "--detach", worktree, f"origin/{head_branch}"]) - info(f"✅ worktree 作成 (detached @ origin/{head_branch}): {worktree}") - else: - info(f"⚠ git fetch origin {head_branch} 失敗 (フォーク PR の可能性) — gh pr checkout でフォールバック") - _sh(["git", "worktree", "add", "--detach", worktree, "HEAD"]) - # worktree 内で gh pr checkout を実行して正しいコミットに切り替え - checkout_result = subprocess.run( - ["gh", "pr", "checkout", str(pr), "--detach"], - capture_output=True, text=True, - cwd=worktree, - ) - if checkout_result.returncode != 0: - # HEAD (親コミット) 指向のまま残すと、次回実行時に - # _is_registered_worktree() を通過して不正流用されるため、 - # die() の前に作成済み worktree をロールバックする。 - subprocess.run( - ["git", "worktree", "remove", "--force", worktree], - capture_output=True, text=True, - ) - die(f"gh pr checkout --detach #{pr} 失敗: {checkout_result.stderr.strip()}") - info(f"✅ worktree 作成 (gh pr checkout --detach #{pr}): {worktree}") - - -def _git_toplevel() -> str | None: - """git worktree root を取得する。失敗時は None を返す。""" - try: - r = subprocess.run( - ["git", "rev-parse", "--show-toplevel"], - capture_output=True, text=True, - ) - if r.returncode == 0 and r.stdout.strip(): - return r.stdout.strip() - except OSError: - pass - return None - - -def _tmp_dir(workspace: str | None = None) -> pathlib.Path: - """cross-review 用 tmp ディレクトリを決定する。 - - 優先順位: - 1. 環境変数 `CROSS_REVIEW_TMP_DIR` (明示) - 2. `/.cross_review/` (worktree 内。gemini の workspace 制約を根本回避) - - `workspace` 未指定なら `git rev-parse --show-toplevel` で worktree root を - 取得する。サブディレクトリから実行してもパス不一致が発生しない。 - git コマンドが失敗した場合のみ `os.getcwd()` にフォールバックする。 - """ - env = os.environ.get("CROSS_REVIEW_TMP_DIR") - if env: - d = pathlib.Path(env).resolve() - d.mkdir(parents=True, exist_ok=True) - return d - ws = pathlib.Path(workspace or _git_toplevel() or os.getcwd()).resolve() - d = ws / ".cross_review" - d.mkdir(parents=True, exist_ok=True) - return d - - -def _resolve_tmp_dir(pr: int | None = None) -> pathlib.Path: - """state.json に保存された tmp_dir を優先し、_tmp_dir() をフォールバックとする。 - - init 時に確定した tmp_dir を再利用することで、CWD や環境変数の変化による - パス不一致リスクを回避する。 - - 注意: この関数は成果物パスの解決にのみ使用する。state ファイル自体のパス解決 - には _tmp_dir() を直接使うこと(循環参照を防ぐため)。 - 副作用: フォールバック時に _tmp_dir() を呼ぶため、ディレクトリ作成 (mkdir) が - 発生する可能性がある。 - """ - if pr is not None: - # state.json から tmp_dir を読み出す (存在する場合) - candidate = _tmp_dir() / f"cross-review-pr{pr}-state.json" - if candidate.exists(): - try: - st = json.loads(candidate.read_text(encoding="utf-8")) - saved = st.get("tmp_dir") - if saved: - p = pathlib.Path(saved) - if p.exists(): - return p - except (OSError, json.JSONDecodeError): - pass - return _tmp_dir() - - -def _state_path(pr: int) -> pathlib.Path: - return _tmp_dir() / f"cross-review-pr{pr}-state.json" - - -def _payload_path(agent: str, pr: int, round_: int) -> pathlib.Path: - return _resolve_tmp_dir(pr) / f"{agent}-review-pr{pr}-round{round_}-payload.json" - - -def _existing_comments_path(pr: int) -> pathlib.Path: - return _resolve_tmp_dir(pr) / f"cross-review-pr{pr}-existing-comments.txt" - - -def _now() -> str: - return _dt.datetime.now(_dt.timezone.utc).astimezone().isoformat(timespec="seconds") - - -def _parse_pr_files_payload(output: str) -> list[dict[str, Any]]: - """`gh pr view --json files` の JSON を分類用の最小構造に正規化する。""" - try: - payload = json.loads(output) - except json.JSONDecodeError: - return [] - files = payload.get("files") if isinstance(payload, dict) else None - if not isinstance(files, list): - return [] - - status_map = { - "ADDED": "A", - "MODIFIED": "M", - "DELETED": "D", - "RENAMED": "R", - "COPIED": "C", - "CHANGED": "M", - } - entries: list[dict[str, Any]] = [] - for f in files: - if not isinstance(f, dict): - continue - path = f.get("path") - if not isinstance(path, str) or not path: - continue - change_type = str(f.get("changeType") or "MODIFIED").upper() - status = status_map.get(change_type, change_type[:1] or "M") - paths = [] - previous = f.get("previousPath") or f.get("previous_filename") - if isinstance(previous, str) and previous and previous != path: - paths.append(previous) - paths.append(path) - entries.append({"status": status, "paths": paths}) - return entries - - -def _parse_pr_files_api_lines(output: str) -> list[dict[str, Any]]: - """GitHub API の PR files を TSV(JSON jq) 出力から分類用構造に変換する。""" - status_map = { - "added": "A", - "modified": "M", - "removed": "D", - "renamed": "R", - "copied": "C", - "changed": "M", - } - entries: list[dict[str, Any]] = [] - for raw in output.splitlines(): - if not raw.strip(): - continue - cols = raw.split("\t") - status_raw = cols[0].strip().lower() if cols else "modified" - path = cols[1].strip() if len(cols) > 1 else "" - previous = cols[2].strip() if len(cols) > 2 else "" - if not path: - continue - paths = [] - if previous and previous != path: - paths.append(previous) - paths.append(path) - entries.append({"status": status_map.get(status_raw, status_raw[:1].upper() or "M"), "paths": paths}) - return entries - - -def _fetch_changed_files(pr: int, repo: str) -> list[dict[str, Any]]: - r = subprocess.run( - [ - "gh", "api", f"repos/{repo}/pulls/{pr}/files", - "--paginate", - "--jq", '.[] | [.status, .filename, (.previous_filename // "")] | @tsv', - ], - capture_output=True, text=True, - ) - if r.returncode == 0: - entries = _parse_pr_files_api_lines(r.stdout) - if entries: - return entries - info("⚠ PR files API の結果が空、または解析できません。gh pr view fallback を試行") - - else: - info(f"⚠ PR files API 取得に失敗。gh pr view fallback を試行: {r.stderr.strip()[:200]}") - - fallback = subprocess.run( - ["gh", "pr", "view", str(pr), "--json", "files"], - capture_output=True, text=True, - ) - if fallback.returncode != 0: - info(f"⚠ PR 変更ファイル一覧の取得に失敗。自動レビュー観点は共通のみ: {fallback.stderr.strip()[:200]}") - return [] - entries = _parse_pr_files_payload(fallback.stdout) - if not entries: - info("⚠ PR 変更ファイル一覧が空、または解析できません。自動レビュー観点は共通のみ") - return entries - - -def _path_info(path: str) -> tuple[str, str, str, str]: - p = pathlib.PurePosixPath(path.replace("\\", "/")) - lower = str(p).lower() - normalized = "/" + lower.lstrip("./") - return lower, normalized, p.name.lower(), pathlib.PurePosixPath(lower).suffix - - -def _contains_any(text: str, needles: tuple[str, ...]) -> bool: - return any(n in text for n in needles) - - -def _path_tokens(path: str) -> set[str]: - _, normalized, _, _ = _path_info(path) - return { - token - for token in normalized.replace(".", "/").replace("-", "/").replace("_", "/").split("/") - if token - } - - -def _is_doc_path(path: str) -> bool: - lower, normalized, name, ext = _path_info(path) - stem = pathlib.PurePosixPath(lower).stem - return ( - ext in DOC_EXTENSIONS - or stem in DOC_FILENAMES - or normalized.startswith("/docs/") - or "/docs/" in normalized - or normalized.startswith("/documentation/") - or "/documentation/" in normalized - ) - - -def _is_code_path(path: str) -> bool: - _, _, _, ext = _path_info(path) - return ext in CODE_EXTENSIONS - - -def _is_migration_path(path: str) -> bool: - lower, normalized, name, ext = _path_info(path) - return ( - ext == ".sql" - or _contains_any(normalized, MIGRATION_PATH_MARKERS) - or any(marker in name or marker in lower for marker in MIGRATION_NAME_MARKERS) - ) - - -def _is_test_path(path: str) -> bool: - lower, normalized, name, _ = _path_info(path) - return _contains_any(normalized, TEST_PATH_MARKERS) or any(m in name or m in lower for m in TEST_NAME_MARKERS) - - -def _is_dependency_path(path: str) -> bool: - _, _, name, _ = _path_info(path) - return name in DEPENDENCY_FILENAMES - - -def _is_config_ci_path(path: str) -> bool: - lower, normalized, name, ext = _path_info(path) - return ( - _contains_any(normalized, CI_CONFIG_MARKERS) - or name.startswith(".env") - or name in {"dockerfile", "makefile", ".editorconfig"} - or lower.startswith(".github/") - or (ext in CONFIG_EXTENSIONS and ("/config/" in normalized or "/configs/" in normalized)) - ) - - -def _is_api_contract_path(path: str) -> bool: - lower, normalized, _, _ = _path_info(path) - return _contains_any(normalized, API_CONTRACT_MARKERS) or "openapi" in lower or "swagger" in lower - - -def _is_auth_security_path(path: str) -> bool: - lower, _, _, _ = _path_info(path) - tokens = _path_tokens(path) - return ( - bool(tokens.intersection(AUTH_SECURITY_TOKEN_MARKERS)) - or any(marker in lower for marker in AUTH_SECURITY_SUBSTRING_MARKERS) - ) - - -def _is_frontend_path(path: str) -> bool: - _, normalized, _, ext = _path_info(path) - return ext in FRONTEND_EXTENSIONS or "/components/" in normalized or "/pages/" in normalized - - -def _is_performance_path(path: str) -> bool: - return bool(_path_tokens(path).intersection(PERFORMANCE_MARKERS)) - - -def _is_generated_path(path: str) -> bool: - _, normalized, name, _ = _path_info(path) - return _contains_any(normalized, GENERATED_MARKERS) or name in { - "package-lock.json", "pnpm-lock.yaml", "yarn.lock", "composer.lock", - "gemfile.lock", "go.sum", "poetry.lock", "uv.lock", "cargo.lock", - } - - -def _is_i18n_path(path: str) -> bool: - _, normalized, _, ext = _path_info(path) - return _contains_any(normalized, I18N_MARKERS) or ext in I18N_EXTENSIONS - - -def _is_infra_path(path: str) -> bool: - lower, normalized, name, ext = _path_info(path) - return ( - _contains_any(normalized, INFRA_MARKERS) - or name in {"dockerfile", "docker-compose.yml", "docker-compose.yaml"} - or ext in INFRA_EXTENSIONS - or lower.endswith(".tfvars.json") - ) - - -PATH_CATEGORY_RULES = ( - ("code", _is_code_path), - ("db_migration", _is_migration_path), - ("test", _is_test_path), - ("dependency", _is_dependency_path), - ("config_ci", _is_config_ci_path), - ("api_contract", _is_api_contract_path), - ("auth_security", _is_auth_security_path), - ("frontend", _is_frontend_path), - ("performance", _is_performance_path), - ("generated", _is_generated_path), - ("i18n", _is_i18n_path), - ("infra", _is_infra_path), -) - - -def _classify_changed_files(entries: list[dict[str, Any]]) -> list[str]: - paths = [p for entry in entries for p in entry.get("paths", []) if isinstance(p, str)] - categories: list[str] = ["common"] - if not paths: - return categories - - if paths and all(_is_doc_path(p) for p in paths): - categories.append("docs_only") - - categories.extend( - category - for category, predicate in PATH_CATEGORY_RULES - if any(predicate(path) for path in paths) - ) - if any(str(entry.get("status", "")).startswith(("D", "R")) for entry in entries): - categories.append("deletion_rename") - - # rename の旧パスだけで検知したカテゴリが混ざるのは有用だが、docs_only は - # 旧パス/新パス両方が docs であるときだけ採用するため上で all(paths) にしている。 - return list(dict.fromkeys(categories)) - - -def _auto_review_instructions(categories: list[str]) -> str: - templates = { - "common": COMMON_REVIEW_TEMPLATE, - "docs_only": DOCS_ONLY_REVIEW_TEMPLATE, - "code": CODE_REVIEW_TEMPLATE, - "db_migration": DB_MIGRATION_REVIEW_TEMPLATE, - "test": TEST_REVIEW_TEMPLATE, - "dependency": DEPENDENCY_REVIEW_TEMPLATE, - "config_ci": CONFIG_CI_REVIEW_TEMPLATE, - "api_contract": API_CONTRACT_REVIEW_TEMPLATE, - "auth_security": AUTH_SECURITY_REVIEW_TEMPLATE, - "frontend": FRONTEND_REVIEW_TEMPLATE, - "performance": PERFORMANCE_REVIEW_TEMPLATE, - "deletion_rename": DELETION_RENAME_REVIEW_TEMPLATE, - "generated": GENERATED_REVIEW_TEMPLATE, - "i18n": I18N_REVIEW_TEMPLATE, - "infra": INFRA_REVIEW_TEMPLATE, - } - return "\n\n".join(templates[c] for c in categories if c in templates) - - -def _combined_review_instructions(auto: str, manual: str) -> str: - return "\n\n".join(part for part in (auto.strip(), manual.strip()) if part) - - -def _extra_review_instructions(args: argparse.Namespace) -> str: - """cross-review launcher に渡す追加レビュー観点を組み立てる。 - - `--focus` は短い観点を直接渡す用途、`--extra-instructions-file` は長めの - チェックリストを渡す用途。両方指定された場合は順に連結する。 - """ - parts: list[str] = [] - focus = getattr(args, "focus", None) - if focus and str(focus).strip(): - parts.append(str(focus).strip()) - - extra_file = getattr(args, "extra_instructions_file", None) - if extra_file: - path = pathlib.Path(extra_file) - try: - text = path.read_text(encoding="utf-8").strip() - except OSError as exc: - die(f"追加レビュー観点ファイルを読めません: {path} ({exc})") - if text: - parts.append(text) - - return "\n\n".join(parts) - - -def _round_started_unixtime(round_entry: dict[str, Any]) -> float | None: - """``round.started_at`` (ISO 8601) を UNIX time (秒) に変換する。 - - fix 戻り値ファイルの mtime と比較して、round 開始前に書かれた古い - ファイルを fallback から除外するために使う。 - パース失敗時は None を返し、呼び出し側で「検証スキップ」を選ばせる。 - """ - started = round_entry.get("started_at") - if not started: - return None - try: - return _dt.datetime.fromisoformat(started).timestamp() - except (TypeError, ValueError): - return None - - -def _is_fresh_fix_result( - path: pathlib.Path, - pr: int, - round_started_ts: float | None, - is_canonical: bool = False, -) -> tuple[bool, dict[str, Any] | None]: - """fallback 候補の fix 戻り値ファイルを採用してよいか判定する。 - - 検証項目: - 1. ファイル mtime が `round_started_ts` 以降であること - (round 開始前に作られた = 古い実行 / 別リポジトリの同番号 PR の残骸) - 2. JSON 内に `pr` フィールドがある場合は対象 PR と一致すること - (`pr` フィールドが無い場合は 1 のみで判定) - - Returns: - ``(is_fresh, parsed_payload)`` のタプル。``is_fresh=True`` の場合のみ - ``parsed_payload`` (dict) が返る。呼び出し側はこれを使って再パースを省略できる。 - - 挙動: - - 古い候補・`pr` 不一致は警告を stderr に出して ``(False, None)`` を返し、 - 呼び出し側で次の候補へ進む。 - - `is_canonical=True` (= ``$TMP_DIR/fix-pr-result.json`` 正規パス) で - **読み取り失敗 (OSError / JSONDecodeError)** が発生した場合のみ - 即時 ``die(code=3)`` する (codex round 2 指摘: 正規パスが壊れているのに - 後続候補へ流れて別 PR の戻り値を誤マージする事故を防ぐ)。 - 正規パスでも `pr` 不一致 / stale mtime は fallback 継続対象とする。 - """ - # 1. mtime チェック - if round_started_ts is not None: - try: - mtime = path.stat().st_mtime - except OSError as exc: - info(f"⚠ fallback 候補 stat 失敗 ({path}): {exc} — skip") - return False, None - if mtime < round_started_ts: - info( - f"⚠ fallback 候補が round 開始前の古いファイル ({path}, " - f"mtime={_dt.datetime.fromtimestamp(mtime).isoformat(timespec='seconds')} " - f"< round_started={_dt.datetime.fromtimestamp(round_started_ts).isoformat(timespec='seconds')}) " - "— skip" - ) - return False, None - - # 2. JSON 内 `pr` フィールドの一致 (任意) - try: - payload = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as exc: - if is_canonical: - die( - f"正規パスの fix 戻り値ファイルの読み取りに失敗 ({path}): {exc}。" - " 後続 fallback への流れ込みを防ぐため即時中断。", - code=3, - ) - info(f"⚠ fallback 候補 JSON 解析失敗 ({path}): {exc} — skip") - return False, None - # gemini round 3 指摘: `json.loads` は dict 以外 (list 等) も返す。 - # 後続の `payload.get(...)` や cmd_merge_fix 側の `.get()` でクラッシュしないよう、 - # dict でない場合は warn を出して fallback 不採用 ((False, None)) として扱う。 - # - # codex round 4 指摘: ただし `is_canonical=True` (= 正規パス) で非 dict が返った - # 場合は parse 失敗と同じく即時 die(code=3) する。skip で後続 `/tmp/` fallback に - # 流れると、壊れた正規出力を無視して別実行の戻り値を誤マージする経路が残るため。 - if not isinstance(payload, dict): - if is_canonical: - die( - f"正規パスの fix 戻り値ファイルが dict ではない " - f"({path}, type={type(payload).__name__})。" - " 後続 fallback への流れ込みを防ぐため即時中断。", - code=3, - ) - info( - f"⚠ fallback 候補 JSON が dict ではない ({path}, type={type(payload).__name__}) " - "— skip" - ) - return False, None - file_pr = payload.get("pr") - if file_pr is not None: - try: - file_pr_int = int(file_pr) - except (TypeError, ValueError): - info( - f"⚠ fallback 候補の pr フィールドが数値として解釈できない " - f"({path}, file_pr={file_pr!r}) — skip" - ) - return False, None - if file_pr_int != int(pr): - info( - f"⚠ fallback 候補の pr 不一致 ({path}, file_pr={file_pr} != pr={pr}) " - "— 別 PR の戻り値の可能性。skip" - ) - return False, None - return True, payload - - -def _load(pr: int) -> dict[str, Any]: - p = _state_path(pr) - if not p.exists(): - die(f"state.json not found: {p}") - return json.loads(p.read_text(encoding="utf-8")) - - -def _save(pr: int, state: dict[str, Any]) -> None: - p = _state_path(pr) - tmp = p.with_suffix(".json.tmp") - tmp.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8") - tmp.replace(p) - - -def _sh(cmd: list[str], check: bool = True) -> str: - r = subprocess.run(cmd, capture_output=True, text=True) - if check and r.returncode != 0: - die(f"command failed ({' '.join(cmd)}): {r.stderr.strip()}") - return r.stdout.strip() - - -def die(msg: str, code: int = 1) -> None: - print(f"❌ {msg}", file=sys.stderr) - sys.exit(code) - - -def info(msg: str) -> None: - print(msg, file=sys.stderr) - - -# ---------------- subcommands ---------------- - -def cmd_init(args: argparse.Namespace) -> None: - """Step 0 — state 初期化 or 既存 state 引き継ぎ + プリチェック。""" - pr = args.pr - manual_extra_review = _extra_review_instructions(args) - # worktree path を先に解決してから tmp_dir を決定する。 - # tmp_dir は /.cross_review/ に配置し、gemini の workspace 制約を根本回避。 - # path には repo slug を含め、他リポジトリの同一 PR 番号と衝突しないようにする。 - repo = _sh(["gh", "repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]) - worktree = str(pathlib.Path(args.worktree).resolve()) if args.worktree else str( - _default_worktree_base() / _repo_slug(repo) / f"pr{pr}") - - # worktree 存在チェック用: _tmp_dir() は mkdir するため、先に呼ぶと - # worktree ディレクトリが副作用で作成され exists() が常に true になる。 - # そのため _tmp_dir() 呼び出しは worktree 作成/確認の後に行う。 - - # 再開チェック: CROSS_REVIEW_TMP_DIR が設定されている場合はそちらを優先し、 - # 未設定なら /.cross_review/ を直接パスとして組む。 - # _tmp_dir() は mkdir 副作用があるため使用せず、パス解決のみ行う。 - env_tmp = os.environ.get("CROSS_REVIEW_TMP_DIR") - if env_tmp: - resume_dir = pathlib.Path(env_tmp).resolve() - else: - resume_dir = pathlib.Path(worktree) / ".cross_review" - resume_state_file = resume_dir / f"cross-review-pr{pr}-state.json" - if resume_state_file.exists(): - st = json.loads(resume_state_file.read_text(encoding="utf-8")) - if st.get("final") is None: - state_changed = False - if "auto_review_instructions" not in st: - changed_files = _fetch_changed_files(pr, st.get("repo") or repo) - categories = _classify_changed_files(changed_files) - st["changed_files"] = changed_files - st["auto_review_categories"] = categories - st["auto_review_instructions"] = _auto_review_instructions(categories) - state_changed = True - if manual_extra_review: - st["manual_extra_review_instructions"] = manual_extra_review - # 後方互換: 旧 key も manual 指示として保持する。 - st["extra_review_instructions"] = manual_extra_review - state_changed = True - manual = st.get("manual_extra_review_instructions") or st.get("extra_review_instructions") or "" - combined = _combined_review_instructions( - st.get("auto_review_instructions") or "", - manual, - ) - if st.get("review_instructions") != combined: - st["review_instructions"] = combined - state_changed = True - if state_changed: - resume_state_file.write_text( - json.dumps(st, indent=2, ensure_ascii=False), - encoding="utf-8", - ) - info("↻ 追加レビュー観点を state に反映して再開") - tmp_dir = _tmp_dir(worktree) - wt = st.get("worktree_path") or "" - info(f"↻ 前回中断 state から再開(round={len(st.get('rounds', []))})") - print(f'PR={st["current_pr"]}') - print(f'WORKTREE={shlex.quote(str(wt))}') - print(f'TMP_DIR={shlex.quote(str(tmp_dir))}') - print(f'REPO={shlex.quote(str(st.get("repo") or ""))}') - print(f'HEAD_BRANCH={shlex.quote(str(st.get("head_branch") or ""))}') - print(f'BASE_BRANCH={shlex.quote(str(st.get("base_branch") or ""))}') - print(f"IS_OWN_PR={'1' if st.get('is_own_pr') else '0'}") - print(f"EVENT_DOWNGRADE={'1' if st.get('event_downgrade') else '0'}") - print(f"HAS_EXTRA_REVIEW_INSTRUCTIONS={'1' if st.get('review_instructions') else '0'}") - print(f"RESUMED=1") - return - - # 新規 init: プリチェック - me = _sh(["gh", "api", "user", "--jq", ".login"]) - author = _sh(["gh", "pr", "view", str(pr), "--json", "author", "--jq", ".author.login"]) - is_own = (me == author) - event_downgrade = is_own - if is_own: - info(f"⚠ 自分の PR (author={me}) — REQUEST_CHANGES → COMMENT 強制ダウングレード") - - # worktree 分離 — _tmp_dir() より先に worktree を作成/確認する - head_branch = _sh(["gh", "pr", "view", str(pr), "--json", "headRefName", "--jq", ".headRefName"]) - base_branch = _sh(["gh", "pr", "view", str(pr), "--json", "baseRefName", "--jq", ".baseRefName"]) - changed_files = _fetch_changed_files(pr, repo) - auto_review_categories = _classify_changed_files(changed_files) - auto_review = _auto_review_instructions(auto_review_categories) - review_instructions = _combined_review_instructions(auto_review, manual_extra_review) - if not pathlib.Path(worktree).exists(): - _create_worktree(worktree, pr, head_branch) - elif _is_registered_worktree(worktree): - info(f"↻ 既存 worktree 流用: {worktree}") - else: - # パスは存在するが現リポジトリの worktree ではない (別リポジトリの残骸等)。 - # 流用すると git 操作が壊れるため退避して作り直す。 - stale = f"{worktree}.stale-{time.strftime('%Y%m%d%H%M%S')}" - pathlib.Path(worktree).rename(stale) - info(f"⚠ 現リポジトリの worktree でないため退避: {stale}") - _create_worktree(worktree, pr, head_branch) - - # worktree 作成/確認後に _tmp_dir() を呼ぶ (ここで .cross_review/ が作られる) - tmp_dir = _tmp_dir(worktree) - state_file = tmp_dir / f"cross-review-pr{pr}-state.json" - - # 既存コメントスナップショット(重複指摘防止)。 - # 3 ソース (インラインコメント / レビュー body / PR レベルコメント) を - # fix skill の共有スクリプトで一括取得する。 - fetch_script = pathlib.Path(__file__).resolve().parent.parent.parent / "fix" / "scripts" / "fetch-pr-comments.sh" - r = subprocess.run( - [str(fetch_script), repo, str(pr)], - capture_output=True, text=True, - ) - existing_path = tmp_dir / f"cross-review-pr{pr}-existing-comments.txt" - if r.returncode == 0: - existing_path.write_text(r.stdout, encoding="utf-8") - else: - die(f"既存コメント取得失敗 (重複検出無効のため中断): {r.stderr.strip()[:200]}") - - state = { - "started_at": _now(), - "max_rounds": args.max_rounds, - "rotate_after": args.rotate_after, - "only": args.only, - "current_pr": pr, - "worktree_path": worktree, - "tmp_dir": str(tmp_dir), - "repo": repo, - "head_branch": head_branch, - "base_branch": base_branch, - "pr_author": author, - "is_own_pr": is_own, - "event_downgrade": event_downgrade, - "changed_files": changed_files, - "auto_review_categories": auto_review_categories, - "auto_review_instructions": auto_review, - "manual_extra_review_instructions": manual_extra_review, - # 後方互換: 旧 key は manual 指示を保持する。 - "extra_review_instructions": manual_extra_review, - "review_instructions": review_instructions, - "pr_history": [{"pr": pr, "opened_at": _now(), "closed_at": None, "rounds": 0}], - "rounds": [], - "deferred_nits": [], - "final": None, - } - state_file.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8") - info(f"✅ state 初期化: {state_file}") - print(f"PR={pr}") - print(f'WORKTREE={shlex.quote(str(worktree))}') - print(f'TMP_DIR={shlex.quote(str(tmp_dir))}') - print(f'REPO={shlex.quote(str(repo))}') - print(f'HEAD_BRANCH={shlex.quote(str(head_branch))}') - print(f'BASE_BRANCH={shlex.quote(str(base_branch))}') - print(f"IS_OWN_PR={'1' if is_own else '0'}") - print(f"EVENT_DOWNGRADE={'1' if event_downgrade else '0'}") - print(f"HAS_EXTRA_REVIEW_INSTRUCTIONS={'1' if review_instructions else '0'}") - print("RESUMED=0") - - -def cmd_start_round(args: argparse.Namespace) -> None: - """Step 1 — round 開始判定。""" - st = _load(args.pr) - total = len(st["rounds"]) - max_r = st["max_rounds"] - if total >= max_r: - st["final"] = "max_rounds" - st["ended_at"] = _now() - _save(args.pr, st) - die(f"max_rounds={max_r} 到達。中断。", code=1) - - pr = st["current_pr"] - round_no = total + 1 - round_in_pr = sum(1 for r in st["rounds"] if r["pr"] == pr) + 1 - - # round エントリを開く - st["rounds"].append({ - "round": round_no, - "pr": pr, - "started_at": _now(), - }) - _save(args.pr, st) - - info(f"=== Round {round_no} / {max_r} (PR #{pr}, round_in_pr={round_in_pr}) ===") - print(f"ROUND={round_no}") - print(f"ROUND_IN_PR={round_in_pr}") - print(f"PR={pr}") - print(f"MAX_ROUNDS={max_r}") - print(f"ROTATE_AFTER={st['rotate_after']}") - - -def cmd_read_result(args: argparse.Namespace) -> None: - """Step 2.5 — codex/gemini の result.json を state にマージ。""" - agent = args.agent - pr = args.pr - rfile = pathlib.Path(args.file or _resolve_tmp_dir(pr) / f"{agent}-review-pr{pr}-result.json") - if not rfile.exists() or rfile.stat().st_size == 0: - die(f"{agent}: result 未生成 ({rfile})") - - try: - r = json.loads(rfile.read_text(encoding="utf-8")) - except json.JSONDecodeError as exc: - die(f"{agent}: result.json の parse に失敗 ({rfile}): {exc}", code=3) - - # gemini round 4 指摘: result.json は本来 dict だが、launcher の出力バグや - # 別実行の残骸で list / str が入り込むと `r.get(...)` で AttributeError になる。 - # 不正な review result はバグなので即時 die(code=3) で停止させる。 - if not isinstance(r, dict): - die( - f"{agent}: result.json が dict ではない " - f"({rfile}, type={type(r).__name__})。review launcher の出力形式不正。", - code=3, - ) - - # 別名フィールドへのフォールバック (gemini が `intent` / `comment_count` を使う変則 JSON を - # 書き出す既知のケースに対応する。仕様としては `event` / `comments_count` が正) - intent = r.get("event") or r.get("intent") - posted_as = r.get("posted_as") or intent - comments = r.get("comments_count") - if comments is None: - comments = r.get("comment_count") - - if intent is None: - die( - f"{agent}: result.json に event / intent フィールドが無い ({rfile})。" - " launcher prompt のスキーマ違反の可能性。" - ) - - st = _load(pr) - if not st.get("rounds"): - die(f"{agent}: state.rounds が空。`state.py start-round` を先に呼んでください") - st["rounds"][-1][agent] = { - "intent": intent, - "posted_as": posted_as, - "comments": comments, - "review_url": r.get("review_url"), - "by_severity": r.get("by_severity", {}), - } - _save(pr, st) - info(f"✅ {agent}: intent={intent} posted_as={posted_as} comments={comments}") - - -def cmd_judge(args: argparse.Namespace) -> None: - """Step 3 — intent ベース pass 判定。 - - Exit code: 0=approved, 2=continue, 1=error - """ - pr = args.pr - st = _load(pr) - if not st.get("rounds"): - die("state.rounds が空。`state.py start-round` を先に呼んでください") - last = st["rounds"][-1] - only = st.get("only") - - def is_pass(intent: str | None, severity: dict[str, int] | None) -> bool: - if intent in ("APPROVE", "SKIP"): - return True - if intent == "COMMENT": - sev = severity or {} - return (sev.get("critical", 0) == 0 and sev.get("major", 0) == 0) - return False - - codex_intent = (last.get("codex") or {}).get("intent", "SKIP") - gemini_intent = (last.get("gemini") or {}).get("intent", "SKIP") - codex_sev = (last.get("codex") or {}).get("by_severity") - gemini_sev = (last.get("gemini") or {}).get("by_severity") - - codex_pass = (only == "gemini") or is_pass(codex_intent, codex_sev) - gemini_pass = (only == "codex") or is_pass(gemini_intent, gemini_sev) - - print(f"CODEX_INTENT={codex_intent}") - print(f"GEMINI_INTENT={gemini_intent}") - - if codex_pass and gemini_pass: - st["final"] = "approved" - st["ended_at"] = _now() - _save(pr, st) - info("✅ 両方 APPROVE。収束。") - sys.exit(0) - - info(f"→ codex={codex_intent} gemini={gemini_intent}。修正へ。") - sys.exit(2) - - -def cmd_check_oscillation(args: argparse.Namespace) -> None: - """Step 4 — path:line 重複率を計算。 - - 前ラウンドと現ラウンドで重複が 50% 以上なら final=oscillation で中断。 - rotation 直後は round_in_pr<2 なのでスキップ。 - """ - pr = args.pr - st = _load(pr) - rounds = st["rounds"] - current_pr = st["current_pr"] - same_pr = [r for r in rounds if r["pr"] == current_pr] - if len(same_pr) < 2: - info("⏭ round_in_pr<2: 振動検知スキップ") - sys.exit(2) # continue - - prev_round_no = same_pr[-2]["round"] - curr_round_no = same_pr[-1]["round"] - - def collect_keys(round_no: int) -> set[str]: - keys: set[str] = set() - for agent in ("codex", "gemini"): - p = _payload_path(agent, pr, round_no) - if not p.exists(): - continue - try: - payload = json.loads(p.read_text(encoding="utf-8")) - except json.JSONDecodeError: - continue - # gemini round 4 指摘: payload は本来 dict (comments: [...]) だが、 - # launcher のバグで list / str が入り込むと `payload.get(...)` で - # AttributeError になる。不正な review payload はバグなので - # 即時 die(code=3) で停止させる。 - if not isinstance(payload, dict): - die( - f"{agent}: payload.json が dict ではない " - f"({p}, type={type(payload).__name__})。" - " review launcher の出力形式不正。", - code=3, - ) - for c in payload.get("comments", []): - if not isinstance(c, dict): - # comments エントリが dict でない場合も同様に致命扱い - die( - f"{agent}: payload.comments のエントリが dict ではない " - f"({p}, type={type(c).__name__})。", - code=3, - ) - path = c.get("path") - line = c.get("line") or c.get("start_line") - if path and line is not None: - keys.add(f"{path}:{line}") - return keys - - prev = collect_keys(prev_round_no) - curr = collect_keys(curr_round_no) - if not curr: - info("⏭ 現ラウンドの payload なし: 振動検知スキップ") - sys.exit(2) - overlap = prev & curr - ratio = len(overlap) / len(curr) - info(f"振動検知: overlap={len(overlap)}/{len(curr)} ({ratio:.0%})") - - if ratio >= 0.5: - st["final"] = "oscillation" - st["ended_at"] = _now() - _save(pr, st) - die(f"振動検知 — 同一箇所が {ratio:.0%} 重複。中断。", code=4) - sys.exit(2) - - -def _count(v: Any) -> int: - """int(件数) でも list でも None でも件数(int)に正規化する。 - - fix 結果スキーマ上 deferred/rejected/resolved_threads は list が正だが、 - fix サブエージェントが int(件数) を書いてしまうケースがあり、その場合に - len() が `TypeError: object of type 'int' has no len()` で落ちるのを防ぐ。 - """ - if isinstance(v, bool): - # bool は int のサブクラスだが件数として扱わない - return 0 - if isinstance(v, int): - return v - if isinstance(v, dict): - # LLM が単一要素を list ではなく dict 単体で返すケースを 1 件として扱う。 - return 1 - if isinstance(v, (list, tuple)): - return len(v) - if isinstance(v, str) and v.strip().isdigit(): - # LLM が件数を数値文字列 (例: "3" や " 3 ") で返すケースを許容する。 - # strip 後 isdigit() なので前後空白を許し、負号・小数点は引き続き弾く - # (件数は非負整数なので十分)。 - return int(v.strip()) - return 0 - - -def cmd_merge_fix(args: argparse.Namespace) -> None: - """Step 5 後段 — fix サブエージェント戻り値を state にマージ + CI 分類。 - - Exit code: 0=continue, 3=ci-code-fail (final=error) - """ - pr = args.pr - - # state を先に読み、fallback 検証用の round 開始時刻を取得する - # (round 開始前の古いファイルや、別リポジトリの同番号 PR の戻り値を - # 誤マージするのを防ぐ)。 - st = _load(pr) - if not st.get("rounds"): - die("state.rounds が空。`state.py start-round` を先に呼んでください", code=3) - round_started_ts = _round_started_unixtime(st["rounds"][-1]) - - # 戻り値ファイルの探索順: - # 1. --file 明示 (ユーザー指定なので mtime/pr 検証はスキップ) - # 2. $TMP_DIR/fix-pr-result.json (正規; _tmp_dir() 解決先) - # 3. /tmp/fix-pr-result.json (旧プロンプトで /tmp を指定したサブエージェント救済) - # 2, 3 は PR 番号だけで命名されているため、別 round / 別リポジトリの - # 古い結果を拾わないよう mtime と (あれば) JSON 内の `pr` で検証する。 - explicit = pathlib.Path(args.file) if args.file else None - canonical_path = _resolve_tmp_dir(pr) / f"fix-pr{pr}-result.json" - legacy_tmp_path = pathlib.Path(f"/tmp/fix-pr{pr}-result.json") - # (path, is_canonical) のタプル: 正規パス (canonical) の parse 失敗は die(code=3) する - fallback_candidates: list[tuple[pathlib.Path, bool]] = [ - (canonical_path, True), - (legacy_tmp_path, False), - ] - - ffile: pathlib.Path | None = None - fix: dict[str, Any] | None = None - if explicit is not None: - # codex round 4 指摘: `--file` 明示時は fallback 探索に進まず即時失敗させる。 - # ユーザーが特定ファイルを指定しているのに、それが存在しない / 空 / JSON 不正 - # だった場合、無言で fallback に流れて別実行の戻り値を誤マージすると事故になる。 - if not explicit.exists(): - die( - f"--file で指定されたパスが存在しません: {explicit}", - code=3, - ) - if explicit.stat().st_size == 0: - die( - f"--file で指定されたファイルが空です: {explicit}", - code=3, - ) - try: - fix = json.loads(explicit.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as exc: - die( - f"--file 指定の fix 戻り値ファイルの読み取り / parse に失敗 " - f"({explicit}): {exc}", - code=3, - ) - # gemini round 3 指摘: `--file` で `list` 等の non-dict JSON が渡されると - # 後続の `fix.get(...)` でクラッシュする。即時 die(code=3) で中断。 - if not isinstance(fix, dict): - die( - f"--file 指定の fix 戻り値ファイルが dict ではない " - f"({explicit}, type={type(fix).__name__})。" - " fix サブエージェント出力の形式不正。", - code=3, - ) - ffile = explicit - # 明示指定は stale 検証スキップ - else: - for c, is_canonical in fallback_candidates: - if not (c.exists() and c.stat().st_size > 0): - continue - is_fresh, parsed = _is_fresh_fix_result(c, pr, round_started_ts, is_canonical=is_canonical) - if not is_fresh: - continue - ffile = c - fix = parsed # 既にパース済みのデータを再利用 (gemini round 2 指摘の性能改善) - break - - if ffile is None or fix is None: - checked = ([str(explicit)] if explicit else []) + [str(c) for c, _ in fallback_candidates] - die( - "fix サブエージェントが戻り値ファイルを生成しなかった " - f"(checked: {checked})", - code=3, - ) - - # key 名 fallback (サブエージェントが別名で書いた場合の救済)。 - # 正規は fix_commit / fixed_count、別名は commit_sha / fixed のみ受理する。 - fix_commit = fix.get("fix_commit") or fix.get("commit_sha") - fixed_count = fix.get("fixed_count") - if fixed_count is None: - fixed_count = fix.get("fixed", 0) - - # `st` は冒頭の fallback 検証で既に load 済み。 - round_no = st["rounds"][-1]["round"] - - # deferred は list が正だが、LLM がスキーマを無視して文字列リスト - # (例: ["nit: ..."]) や単一 dict、int(件数) を返すケースがある。後段の - # deferred_nits 展開ループは dict 以外をスキップするため、まず dict 要素のみへ - # 正規化する (単一 dict は 1 件として包む)。 - _deferred_raw = fix.get("deferred") - if isinstance(_deferred_raw, list): - _deferred_nits = [d for d in _deferred_raw if isinstance(d, dict)] - elif isinstance(_deferred_raw, dict): # 単一 dict フォールバック (gemini #3) - _deferred_nits = [_deferred_raw] - else: - _deferred_nits = [] - - # 保存件数の単一整合ルール: - # - 構造化データ (list / dict) は per-item を保持できるので、展開件数 - # (len(_deferred_nits)) を保存し deferred_nits の件数と一致させる。 - # - int / 数値文字列は per-item データを失った「劣化表現」なので、件数を - # 失わないよう _count() の値を保存する (展開はできないので nits は空)。 - if isinstance(_deferred_raw, (list, dict)): - _deferred_count = len(_deferred_nits) - else: - _deferred_count = _count(_deferred_raw) - - st["rounds"][-1]["fix"] = { - "commit": fix_commit, - "fixed": fixed_count, - # deferred は上記の単一整合ルールで算出した件数を保存する。 - # resolved_threads / rejected は件数しか保存せず後段ループが無いため _count() で可。 - "deferred": _deferred_count, - "rejected": _count(fix.get("rejected")), - "resolved_threads": _count(fix.get("resolved_threads")), - "ci": fix.get("ci_status"), - "ci_failed_checks": fix.get("ci_failed_checks", []) or [], - "ci_note": fix.get("ci_note"), - "by_severity": fix.get("by_severity", {}), - } - st["rounds"][-1]["ended_at"] = _now() - for d in _deferred_nits: - st["deferred_nits"].append({**d, "pr": pr, "round": round_no}) - _save(pr, st) - - # CI 分類 - if (fix.get("ci_status") or "").upper() != "FAILURE": - info(f"✅ fix マージ完了 (commit={fix_commit} fixed={fixed_count})") - return - - code_patterns = ("pint", "larastan", "phpstan", "test", "lint", "type", - "build", "ruff", "eslint", "tsc", "mypy") - meta_patterns = ("check_pr_requirements", "assignees", "reviewers", "labels", "meta") - failed = fix.get("ci_failed_checks") or [] - code_fail = False - for name in failed: - low = name.lower() - if any(p in low for p in meta_patterns): - continue - elif any(p in low for p in code_patterns): - code_fail = True - else: - code_fail = True # 不明は code-fail(保守的) - - if code_fail: - st["final"] = "error" - st["ended_at"] = _now() - _save(pr, st) - die(f"コード関連 CI 失敗。中断: {failed}", code=3) - - # meta only: 継続 - note = f"メタチェックのみ失敗: {failed} — コードと無関係のため継続" - st["rounds"][-1]["fix"]["ci_note"] = note - _save(pr, st) - info(f"⚠ メタチェックのみ失敗 ({failed}) — 継続") - - -def cmd_should_rotate(args: argparse.Namespace) -> None: - """Step 6 — PR ローテーション要否。Exit 0=rotate, 2=keep. - - 判定は ``round_in_pr >= rotate_after && total < max_rounds`` のみで、 - rotate-pr.sh の ``--mode light|squash`` どちらでも同じ条件を使う。 - state.json の key は ``STATE_PR`` (最初に init した PR 番号) で固定なので、 - light モードで head_branch が変わらない場合でも整合する。 - """ - pr = args.pr - st = _load(pr) - current_pr = st["current_pr"] - round_in_pr = sum(1 for r in st["rounds"] if r["pr"] == current_pr) - total = len(st["rounds"]) - rotate_after = st["rotate_after"] - max_r = st["max_rounds"] - if round_in_pr >= rotate_after and total < max_r: - info(f"🔄 PR #{current_pr} が {round_in_pr} round 経過 — ローテーション必要") - print(f"CURRENT_PR={current_pr}") - print(f"ROUND_IN_PR={round_in_pr}") - sys.exit(0) - sys.exit(2) - - -def cmd_set_current_pr(args: argparse.Namespace) -> None: - """PR ローテーション完了後の state 更新。 - - rotate-pr.sh の light / squash どちらでも、新 PR 番号を受け取って - ``current_pr`` を切り替え、``pr_history`` に新 PR エントリを追加する。 - state.json のファイル名は ``STATE_PR`` (= ``args.pr``) ベースで不変なので、 - light モードで head_branch が変わらないケースでも問題なく追跡できる。 - """ - pr = args.pr # 旧 PR (state file の key) - new_pr = args.new_pr - st = _load(pr) - old_pr = st["current_pr"] - now = _now() - # 旧 PR の history を closed に - for h in st["pr_history"]: - if h["pr"] == old_pr and h["closed_at"] is None: - h["closed_at"] = now - h["rounds"] = sum(1 for r in st["rounds"] if r["pr"] == old_pr) - break - st["pr_history"].append({"pr": new_pr, "opened_at": now, "closed_at": None, "rounds": 0}) - st["current_pr"] = new_pr - _save(pr, st) - info(f"✅ current_pr: {old_pr} → {new_pr}") - - -def cmd_report(args: argparse.Namespace) -> None: - """Step 8 — deferred nit + ラウンドサマリ表示。""" - pr = args.pr - st = _load(pr) - final = st.get("final") or "in_progress" - total = len(st["rounds"]) - prs = [h["pr"] for h in st["pr_history"]] - rotated = max(0, len(prs) - 1) - - print(f"## 最終ステータス: {final}") - print(f"## 総ラウンド数: {total} / PR数: {len(prs)} (rotated {rotated} 回)") - print() - print("## PR 履歴") - for h in st["pr_history"]: - state_str = "closed" if h.get("closed_at") else "open" - print(f"- #{h['pr']} ({state_str}, {h.get('rounds', 0)} rounds)") - print() - print("## ラウンドサマリ") - print("| round | PR | codex | gemini | fix | CI |") - print("|---|---|---|---|---|---|") - for r in st["rounds"]: - codex = r.get("codex") or {} - gemini = r.get("gemini") or {} - fix = r.get("fix") or {} - codex_s = f"{codex.get('intent', '-')} ({codex.get('comments', '-')})" if codex else "-" - gemini_s = f"{gemini.get('intent', '-')} ({gemini.get('comments', '-')})" if gemini else "-" - fix_s = "-" - if fix: - fix_s = f"{(fix.get('commit') or '')[:7]} ({fix.get('fixed', 0)} fixed, {fix.get('deferred', 0)} deferred)" - ci_s = fix.get("ci") or "-" - print(f"| {r['round']} | #{r['pr']} | {codex_s} | {gemini_s} | {fix_s} | {ci_s} |") - print() - - nits = st.get("deferred_nits") or [] - if nits: - print(f"## 残 deferred nit ({len(nits)} 件)") - for n in nits: - print(f"- [{n.get('severity')}] {n.get('path')}:{n.get('line')} — {n.get('summary')}") - print() - print("これらの nit を一括対応する場合は再度 `/ndf:fix ` を起動してください。") - else: - print("## 残 deferred nit: なし") - - -# ---------------- main ---------------- - -def main() -> None: - p = argparse.ArgumentParser(description=__doc__) - sub = p.add_subparsers(dest="cmd", required=True) - - sp = sub.add_parser("init", help="Step 0 — state 初期化 or 再開") - sp.add_argument("pr", type=int) - sp.add_argument("--max-rounds", type=int, default=12) - sp.add_argument("--rotate-after", type=int, default=8) - sp.add_argument("--only", choices=["codex", "gemini"], default=None) - sp.add_argument("--worktree", default=None) - sp.add_argument( - "--focus", - default=None, - help="追加レビュー観点。例: ドキュメントとコードの整合性を重点的に確認", - ) - sp.add_argument( - "--extra-instructions-file", - default=None, - help="追加レビュー観点を記載した UTF-8 テキストファイル", - ) - sp.set_defaults(func=cmd_init) - - sp = sub.add_parser("start-round", help="Step 1 — round 開始判定") - sp.add_argument("pr", type=int) - sp.set_defaults(func=cmd_start_round) - - sp = sub.add_parser("read-result", help="Step 2.5 — review result を state にマージ") - sp.add_argument("pr", type=int) - sp.add_argument("agent", choices=["codex", "gemini"]) - sp.add_argument("--file", default=None) - sp.set_defaults(func=cmd_read_result) - - sp = sub.add_parser("judge", help="Step 3 — intent ベース pass 判定 (0=approved/2=continue)") - sp.add_argument("pr", type=int) - sp.set_defaults(func=cmd_judge) - - sp = sub.add_parser("check-oscillation", help="Step 4 — path:line 重複率を計算") - sp.add_argument("pr", type=int) - sp.set_defaults(func=cmd_check_oscillation) - - sp = sub.add_parser("merge-fix", help="Step 5 post — fix 戻り値マージ + CI 分類") - sp.add_argument("pr", type=int) - sp.add_argument("--file", default=None) - sp.set_defaults(func=cmd_merge_fix) - - sp = sub.add_parser("should-rotate", help="Step 6 — rotate 要否 (0=rotate/2=keep)") - sp.add_argument("pr", type=int) - sp.set_defaults(func=cmd_should_rotate) - - sp = sub.add_parser("set-current-pr", help="rotation 後の current_pr 更新") - sp.add_argument("pr", type=int, help="state file の元 PR") - sp.add_argument("new_pr", type=int) - sp.set_defaults(func=cmd_set_current_pr) - - sp = sub.add_parser("report", help="Step 8 — deferred nit + サマリ表示") - sp.add_argument("pr", type=int) - sp.set_defaults(func=cmd_report) - - args = p.parse_args() - args.func(args) - - -if __name__ == "__main__": - main() diff --git a/plugins/ndf/skills-codex/cross-review/scripts/wait-review.sh b/plugins/ndf/skills-codex/cross-review/scripts/wait-review.sh deleted file mode 100755 index 8741d36..0000000 --- a/plugins/ndf/skills-codex/cross-review/scripts/wait-review.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -# Wait for codex / gemini review processes — monitor.py の薄いラッパ。 -# -# Usage: wait-review.sh [codex|gemini|both] [--timeout SEC] [--stall-timeout SEC] -# -# 既定値: -# timeout 1800s (= 30 min) env MONITOR_TIMEOUT で上書き -# stall-timeout 600s (= 10 min) env MONITOR_STALL で上書き -# poll 15s env MONITOR_POLL で上書き -# -# Exit codes は monitor.py に準拠: -# 0 OK -# 1 USAGE / IO error -# 2 TIMEOUT -# 3 NO_RESULT (プロセス終了したが result.json 未生成) -# 4 EARLY_ERROR (err.log に致命的パターン) -# 5 STALLED (err.log 進捗なし) -# 6 PIDFILE_BAD (pidfile 不正 / プロセス未起動) -# -# 旧 wait_codex / wait_gemini (sentinel + pidfile のみ) は信頼性が低かったため -# Python 側に多軸監視を集約した。本ラッパは既存呼び出し互換のために残す。 - -set -euo pipefail - -PR=${1:?PR required} -TARGET=${2:-both} -shift 2 || shift $# - -SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) -exec "$SCRIPT_DIR/monitor.py" "$PR" "$TARGET" "$@" diff --git a/plugins/ndf/skills-codex/cross-review/tests/__init__.py b/plugins/ndf/skills-codex/cross-review/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/plugins/ndf/skills-codex/cross-review/tests/conftest.py b/plugins/ndf/skills-codex/cross-review/tests/conftest.py deleted file mode 100644 index a5b257b..0000000 --- a/plugins/ndf/skills-codex/cross-review/tests/conftest.py +++ /dev/null @@ -1,47 +0,0 @@ -"""pytest 共通フィクスチャ。 - -`scripts/state.py` は uv self-contained script として `#!/usr/bin/env -S uv run --script` -で起動される運用だが、テストでは関数を直接 import したい。 -importlib.util で source loader 経由で読み込む。 -""" -from __future__ import annotations - -import importlib.util -import pathlib -import sys -import types - -_HERE = pathlib.Path(__file__).resolve().parent -_SCRIPT = _HERE.parent / "scripts" / "state.py" -_MONITOR = _HERE.parent / "scripts" / "monitor.py" - - -def _load_module(name: str, path: pathlib.Path) -> types.ModuleType: - spec = importlib.util.spec_from_file_location(name, path) - if spec is None or spec.loader is None: - raise RuntimeError(f"failed to load {path}") - mod = importlib.util.module_from_spec(spec) - sys.modules[name] = mod - spec.loader.exec_module(mod) - return mod - - -def _load_state_module() -> types.ModuleType: - return _load_module("cross_review_state", _SCRIPT) - - -def _load_monitor_module() -> types.ModuleType: - return _load_module("cross_review_monitor", _MONITOR) - - -import pytest - - -@pytest.fixture(scope="session") -def state_mod() -> types.ModuleType: - return _load_state_module() - - -@pytest.fixture(scope="session") -def monitor_mod() -> types.ModuleType: - return _load_monitor_module() diff --git a/plugins/ndf/skills-codex/cross-review/tests/test_default_worktree_base.py b/plugins/ndf/skills-codex/cross-review/tests/test_default_worktree_base.py deleted file mode 100644 index b4d38da..0000000 --- a/plugins/ndf/skills-codex/cross-review/tests/test_default_worktree_base.py +++ /dev/null @@ -1,41 +0,0 @@ -"""state.py `_default_worktree_base()` / `_repo_slug()` の解決テスト。 - -優先順位: - 1. 環境変数 NDF_WORKTREE_BASE - 2. <システム tmpdir>/ndf-worktrees (非永続領域) - -worktree path は `/--/pr` 形式で、他リポジトリの -同一 PR 番号と衝突しない。 -""" -from __future__ import annotations - -import pathlib - - -def test_env_override_takes_precedence(monkeypatch, tmp_path, state_mod): - explicit = tmp_path / "custom-base" - monkeypatch.setenv("NDF_WORKTREE_BASE", str(explicit)) - assert state_mod._default_worktree_base() == explicit - - -def test_default_is_tmpdir_ndf_worktrees(monkeypatch, tmp_path, state_mod): - """既定では永続 volume ではなくシステム tmpdir 配下を使う。""" - monkeypatch.delenv("NDF_WORKTREE_BASE", raising=False) - monkeypatch.setattr(state_mod.tempfile, "gettempdir", lambda: str(tmp_path)) - assert state_mod._default_worktree_base() == tmp_path / "ndf-worktrees" - - -def test_repo_slug_is_path_safe(state_mod): - assert state_mod._repo_slug("devbasex/ai-plugins") == "devbasex--ai-plugins" - - -def test_is_registered_worktree_rejects_foreign_dir(monkeypatch, tmp_path, state_mod): - """`git worktree list` に載っていないパスは流用しない (別リポジトリの残骸対策)。""" - registered = tmp_path / "registered" - foreign = tmp_path / "foreign" - monkeypatch.setattr( - state_mod, "_sh", - lambda cmd, check=True: f"worktree {registered}\nHEAD abc\n", - ) - assert state_mod._is_registered_worktree(str(registered)) is True - assert state_mod._is_registered_worktree(str(foreign)) is False diff --git a/plugins/ndf/skills-codex/cross-review/tests/test_monitor_early_error.py b/plugins/ndf/skills-codex/cross-review/tests/test_monitor_early_error.py deleted file mode 100644 index a7e3f6b..0000000 --- a/plugins/ndf/skills-codex/cross-review/tests/test_monitor_early_error.py +++ /dev/null @@ -1,126 +0,0 @@ -"""monitor.py `_scan_early_fatal()` の誤検知防止テスト。 - -回帰: SKILL.md / docs/*.md 内の以下のような行は **fatal 検知してはいけない**。 -- Markdown 表セル (`| ... | quota exceeded ... |`) -- backtick で囲まれた quote (`` `quota exceeded` ``) -- 日本語クォートで囲まれた quote (`「quota exceeded」`) -- ダブル/シングルクォートで囲まれた **文字列リテラル** (`"quota exceeded: ..."`) -- grep / ripgrep 形式の **ソース引用行** (`path/to/file.py:22: "quota exceeded..."`) -codex がレビュー対象の doc / テストコードを echo した際に上記が err.log に書かれることで、 -従来の monitor.py は無関係なドキュメント・コード記述を fatal とみなしてプロセスを kill していた。 -""" -from __future__ import annotations - -import pathlib - - -def _write(p: pathlib.Path, content: str) -> pathlib.Path: - p.write_text(content, encoding="utf-8") - return p - - -def test_real_quota_exceeded_is_detected(tmp_path, monitor_mod): - """行頭の本物 `quota exceeded` は依然として fatal 扱い。""" - log = _write(tmp_path / "err.log", "quota exceeded: please upgrade\n") - assert monitor_mod._scan_early_fatal(log) is not None - - -def test_markdown_table_row_is_benign(tmp_path, monitor_mod): - """Markdown 表セル行内のキーワードは doc 引用として無視。""" - log = _write( - tmp_path / "err.log", - "| early-error | `^Error:` / 「quota exceeded」「sandbox error」を含む行 |\n", - ) - assert monitor_mod._scan_early_fatal(log) is None - - -def test_backtick_quoted_keyword_is_benign(tmp_path, monitor_mod): - """backtick 引用内のキーワードは doc 引用として無視。""" - log = _write( - tmp_path / "err.log", - "explanation of `quota exceeded` pattern handling here\n", - ) - assert monitor_mod._scan_early_fatal(log) is None - - -def test_japanese_quote_wrapped_keyword_is_benign(tmp_path, monitor_mod): - """日本語「」内のキーワードは doc 引用として無視。""" - log = _write( - tmp_path / "err.log", - "「quota exceeded」を含む行を検出 (diff/doc 引用文中の同語句は誤検知しない)\n", - ) - assert monitor_mod._scan_early_fatal(log) is None - - -def test_sandbox_error_in_table_is_benign(tmp_path, monitor_mod): - """表セル内の `sandbox error` も無視。""" - log = _write( - tmp_path / "err.log", - "| pattern | codex 固有: 「sandbox error」を含む行を検出 |\n", - ) - assert monitor_mod._scan_early_fatal(log) is None - - -def test_real_sandbox_error_still_detected(tmp_path, monitor_mod): - """素の `sandbox error` は依然として fatal 扱い。""" - log = _write(tmp_path / "err.log", "Internal sandbox error: cannot start\n") - assert monitor_mod._scan_early_fatal(log) is not None - - -def test_grep_style_source_citation_is_benign(tmp_path, monitor_mod): - """codex がレビュー対象のテストコードを grep 形式 (`path.py:22: code`) で - echo した行は fatal 扱いしない。 - - 回帰: 本 skill 自身の tests/test_monitor_early_error.py を cross-review した際、 - `quota exceeded` を含むテスト用文字列リテラル行が echo され、誤って - EARLY_ERROR で codex が kill された (PR #23 round 2 で実際に発生)。 - """ - log = _write( - tmp_path / "err.log", - '/work/worktrees/pr23/plugins/ndf/skills/cross-review/tests/' - 'test_monitor_early_error.py:22: log = _write(tmp_path / "err.log", ' - '"quota exceeded: please upgrade\\n")\n', - ) - assert monitor_mod._scan_early_fatal(log) is None - - -def test_double_quoted_string_literal_is_benign(tmp_path, monitor_mod): - """ダブルクォート文字列リテラル内のキーワードは benign(コード片の echo)。""" - log = _write( - tmp_path / "err.log", - ' raise RuntimeError("quota exceeded: please upgrade")\n', - ) - assert monitor_mod._scan_early_fatal(log) is None - - -def test_single_quoted_string_literal_is_benign(tmp_path, monitor_mod): - """シングルクォート文字列リテラル内のキーワードも benign。""" - log = _write( - tmp_path / "err.log", - " msg = 'sandbox error occurred in test fixture'\n", - ) - assert monitor_mod._scan_early_fatal(log) is None - - -def test_match_is_quoted_double_quote(monitor_mod): - """`_match_is_quoted()` がダブルクォート文字列リテラルを検出する。""" - line = 'raise RuntimeError("quota exceeded: please upgrade")' - start = line.index("quota") - end = start + len("quota exceeded") - assert monitor_mod._match_is_quoted(line, start, end) - - -def test_match_is_quoted_helper(monitor_mod): - """`_match_is_quoted()` 単体テスト。""" - line_backtick = "see `quota exceeded` doc" - start = line_backtick.index("quota") - end = start + len("quota exceeded") - assert monitor_mod._match_is_quoted(line_backtick, start, end) - - line_jp = "「quota exceeded」と書かれた行" - start = line_jp.index("quota") - end = start + len("quota exceeded") - assert monitor_mod._match_is_quoted(line_jp, start, end) - - line_raw = "quota exceeded happened" - assert not monitor_mod._match_is_quoted(line_raw, 0, len("quota exceeded")) diff --git a/plugins/ndf/skills-codex/cross-review/tests/test_monitor_import_safety.py b/plugins/ndf/skills-codex/cross-review/tests/test_monitor_import_safety.py deleted file mode 100644 index 36efc48..0000000 --- a/plugins/ndf/skills-codex/cross-review/tests/test_monitor_import_safety.py +++ /dev/null @@ -1,105 +0,0 @@ -"""monitor.py の import 時 env 評価 safety テスト (PLAN21 round 6 / 最終). - -codex round 5 指摘: `DEFAULT_TIMEOUT` / `DEFAULT_STALL` / `DEFAULT_POLL` を -`int(os.environ.get(name, ...))` で **import 時** に評価していると、 -`MONITOR_STALL=abc` のような非数値 env が設定されているだけで -`_agent_stall_default()` に到達する前に `int()` の `ValueError` で -monitor.py の import そのものが落ち、監視プロセスが起動できなくなる。 - -修正方針: - - `DEFAULT_TIMEOUT` / `DEFAULT_STALL` / `DEFAULT_POLL` は import 時に - **固定数値** (420 / 180 / 15) で保持する - - env の解釈は `_agent_stall_default()` / `_safe_int_env()` 内で - try/except 付きで行う (既に round 4 で実装済み) - -本テストは fixture 経由ではなく **毎回 fresh に source loader で import** し、 -非数値 env が設定された状態でも import が成功することを確認する。 -""" -from __future__ import annotations - -import importlib.util -import pathlib -import sys -import types - -import pytest - - -_HERE = pathlib.Path(__file__).resolve().parent -_MONITOR = _HERE.parent / "scripts" / "monitor.py" - - -def _fresh_import_monitor(name: str = "cross_review_monitor_fresh") -> types.ModuleType: - """毎回 fresh に monitor.py を import する (sys.modules キャッシュを使わない)。 - - session-scoped fixture (`monitor_mod`) は最初の import 時の env を保持してしまい - 本テストでは「非数値 env がセットされた状態での import が安全か」を検証できない。 - """ - sys.modules.pop(name, None) - spec = importlib.util.spec_from_file_location(name, _MONITOR) - assert spec is not None and spec.loader is not None - mod = importlib.util.module_from_spec(spec) - sys.modules[name] = mod - spec.loader.exec_module(mod) - return mod - - -def test_import_succeeds_with_non_numeric_monitor_stall(monkeypatch): - """MONITOR_STALL=abc でも monitor.py の import が成功すること (codex round 5).""" - monkeypatch.setenv("MONITOR_STALL", "abc") - mod = _fresh_import_monitor("cross_review_monitor_fresh_stall") - # 固定 default が保たれる - assert mod.DEFAULT_STALL == 180 - # 呼び出し時の解決も builtin にフォールバック (既存 round 4 挙動と同じ) - assert mod._agent_stall_default("codex") == 180 - assert mod._agent_stall_default("gemini") == 480 - - -def test_import_succeeds_with_non_numeric_monitor_timeout(monkeypatch): - """MONITOR_TIMEOUT=xxx でも monitor.py の import が成功すること.""" - monkeypatch.setenv("MONITOR_TIMEOUT", "xxx") - mod = _fresh_import_monitor("cross_review_monitor_fresh_timeout") - # 固定 default が保たれる - assert mod.DEFAULT_TIMEOUT == 420 - # safe parse ヘルパも fallback を返す - assert mod._safe_int_env("MONITOR_TIMEOUT", mod.DEFAULT_TIMEOUT) == 420 - - -def test_import_succeeds_with_non_numeric_monitor_poll(monkeypatch): - """MONITOR_POLL=foo でも monitor.py の import が成功すること.""" - monkeypatch.setenv("MONITOR_POLL", "foo") - mod = _fresh_import_monitor("cross_review_monitor_fresh_poll") - assert mod.DEFAULT_POLL == 15 - assert mod._safe_int_env("MONITOR_POLL", mod.DEFAULT_POLL) == 15 - - -def test_import_succeeds_with_all_non_numeric_envs(monkeypatch): - """3 つ全て非数値でも import に失敗しないこと (組み合わせ防御).""" - monkeypatch.setenv("MONITOR_STALL", "abc") - monkeypatch.setenv("MONITOR_TIMEOUT", "xxx") - monkeypatch.setenv("MONITOR_POLL", "???") - mod = _fresh_import_monitor("cross_review_monitor_fresh_all") - assert mod.DEFAULT_STALL == 180 - assert mod.DEFAULT_TIMEOUT == 420 - assert mod.DEFAULT_POLL == 15 - - -def test_safe_int_env_returns_value_when_numeric(monkeypatch, monitor_mod): - """数値が設定されている場合は素直に int を返す.""" - monkeypatch.setenv("SOME_TEST_INT_ENV", "42") - assert monitor_mod._safe_int_env("SOME_TEST_INT_ENV", 1) == 42 - - -def test_safe_int_env_returns_fallback_when_missing(monkeypatch, monitor_mod): - """env が未設定なら fallback を返す.""" - monkeypatch.delenv("SOME_TEST_INT_ENV", raising=False) - assert monitor_mod._safe_int_env("SOME_TEST_INT_ENV", 99) == 99 - - -def test_safe_int_env_warns_on_non_numeric(monkeypatch, monitor_mod, capsys): - """非数値時は stderr に警告を出して fallback を返す.""" - monkeypatch.setenv("SOME_TEST_INT_ENV", "not-a-number") - assert monitor_mod._safe_int_env("SOME_TEST_INT_ENV", 7) == 7 - captured = capsys.readouterr() - assert "SOME_TEST_INT_ENV" in captured.err - assert "int に変換できません" in captured.err diff --git a/plugins/ndf/skills-codex/cross-review/tests/test_monitor_progress_log.py b/plugins/ndf/skills-codex/cross-review/tests/test_monitor_progress_log.py deleted file mode 100644 index 7b98015..0000000 --- a/plugins/ndf/skills-codex/cross-review/tests/test_monitor_progress_log.py +++ /dev/null @@ -1,23 +0,0 @@ -"""monitor.py の progress.log heartbeat 補助関数テスト。""" - - -def test_tail_last_nonempty_line_returns_latest_marker(tmp_path, monitor_mod): - log = tmp_path / "gemini-review-pr1-progress.log" - log.write_text("start: review PR #1\n\nscan: diff\n \npost: submit review\n") - - assert monitor_mod._tail_last_nonempty_line(log) == "post: submit review" - - -def test_tail_last_nonempty_line_handles_missing_file(tmp_path, monitor_mod): - assert monitor_mod._tail_last_nonempty_line(tmp_path / "missing.log") == "" - - -def test_safe_size_handles_missing_file(tmp_path, monitor_mod): - assert monitor_mod._safe_size(tmp_path / "missing.log") == 0 - - -def test_tail_last_nonempty_line_drops_partial_first_line(tmp_path, monitor_mod): - log = tmp_path / "gemini-review-pr1-progress.log" - log.write_text("partial line without beginning\npost: submit review\n", encoding="utf-8") - - assert monitor_mod._tail_last_nonempty_line(log, limit=28) == "post: submit review" diff --git a/plugins/ndf/skills-codex/cross-review/tests/test_monitor_result_age.py b/plugins/ndf/skills-codex/cross-review/tests/test_monitor_result_age.py deleted file mode 100644 index f3e2a28..0000000 --- a/plugins/ndf/skills-codex/cross-review/tests/test_monitor_result_age.py +++ /dev/null @@ -1,330 +0,0 @@ -"""monitor.py の result.json + age fallback (RESULT_AGE_GRACE) テスト。 - -gemini がレビュー完了後にプロセスがハングするケースで、result.json の -mtime が RESULT_AGE_GRACE 秒以上前なら OK 判定する fallback の検証。 -""" -from __future__ import annotations - -import json -import os -import pathlib -import time -from unittest import mock - - -def test_result_age_grace_constant(monitor_mod): - assert hasattr(monitor_mod, "RESULT_AGE_GRACE") - assert monitor_mod.RESULT_AGE_GRACE == 30 - - -def test_gemini_result_age_fallback_triggers_ok(monitor_mod, tmp_path): - """result.json が RESULT_AGE_GRACE 秒以上前に書かれていれば OK で返る。""" - pr = 999 - agent = "gemini" - base = tmp_path / f"{agent}-review-pr{pr}" - - pidfile = pathlib.Path(f"{base}.pid") - err_log = pathlib.Path(f"{base}-err.log") - stdout_log = pathlib.Path(f"{base}-stdout.log") - result = pathlib.Path(f"{base}-result.json") - - pidfile.write_text("12345") - err_log.write_text("YOLO mode is enabled.\n") - stdout_log.write_text("") - result.write_text(json.dumps({"event": "APPROVE", "comments_count": 0})) - # time.time() を side_effect で進める: - # call 1 (started_wall): base_time - # call 2+ (result age check): base_time + 40 - # result_mtime = base_time + 5 → started_wall(base_time) 以降かつ age=35s >= 30s - base_time = time.time() - result_mtime = base_time + 5 - os.utime(result, (result_mtime, result_mtime)) - - time_call_count = [0] - def fake_time(): - time_call_count[0] += 1 - if time_call_count[0] == 1: - return base_time - return base_time + 40 - - kill_called = [] - poll_count = [0] - - def fake_pid_alive(pid): - return True - - def fake_kill_pid(pid, sigterm_grace=3.0): - kill_called.append(pid) - - def fake_sleep(s): - poll_count[0] += 1 - if poll_count[0] > 5: - raise RuntimeError("too many polls — fallback should have triggered") - - with ( - mock.patch.object(monitor_mod, "_tmp_dir", return_value=tmp_path), - mock.patch.object(monitor_mod, "_pid_alive", side_effect=fake_pid_alive), - mock.patch.object(monitor_mod, "_kill_pid", side_effect=fake_kill_pid), - mock.patch.object(monitor_mod, "_pid_cmdline_matches", return_value=True), - mock.patch("time.sleep", side_effect=fake_sleep), - mock.patch("time.time", side_effect=fake_time), - ): - st = monitor_mod.monitor_agent( - agent=agent, pr=pr, - timeout=420, stall_timeout=480, poll=15, - require_result=True, no_early_error=True, - ) - - assert st.status == "OK" - assert st.exit_code == 0 - assert st.result_exists is True - assert "result.json exists for" in st.detail - assert 12345 in kill_called - - -def test_gemini_result_age_too_young_continues(monitor_mod, tmp_path): - """result.json が書かれたばかり (age < RESULT_AGE_GRACE) なら fallback しない。""" - pr = 998 - agent = "gemini" - base = tmp_path / f"{agent}-review-pr{pr}" - - pidfile = pathlib.Path(f"{base}.pid") - err_log = pathlib.Path(f"{base}-err.log") - stdout_log = pathlib.Path(f"{base}-stdout.log") - result = pathlib.Path(f"{base}-result.json") - - pidfile.write_text("12346") - err_log.write_text("YOLO mode is enabled.\n") - stdout_log.write_text("") - result.write_text(json.dumps({"event": "APPROVE", "comments_count": 0})) - # mtime を 5 秒前に設定 (RESULT_AGE_GRACE=30 未満) - recent_mtime = time.time() - 5 - os.utime(result, (recent_mtime, recent_mtime)) - - poll_count = [0] - - def fake_pid_alive(pid): - if poll_count[0] >= 2: - return False - return True - - def fake_sleep(s): - poll_count[0] += 1 - - with ( - mock.patch.object(monitor_mod, "_tmp_dir", return_value=tmp_path), - mock.patch.object(monitor_mod, "_pid_alive", side_effect=fake_pid_alive), - mock.patch.object(monitor_mod, "_kill_pid"), - mock.patch.object(monitor_mod, "_pid_cmdline_matches", return_value=None), - mock.patch("time.sleep", side_effect=fake_sleep), - ): - st = monitor_mod.monitor_agent( - agent=agent, pr=pr, - timeout=420, stall_timeout=480, poll=15, - require_result=True, no_early_error=True, - ) - - # result.json は young なので fallback せず、プロセス終了後の通常 OK になる - assert st.status == "OK" - assert st.exit_code == 0 - assert "process exited" in st.detail - - -def test_fresh_but_young_result_continues_monitoring(monitor_mod, tmp_path): - """started_wall 後に書かれたが age < RESULT_AGE_GRACE なら fallback せず監視継続。""" - pr = 994 - agent = "gemini" - base = tmp_path / f"{agent}-review-pr{pr}" - - pidfile = pathlib.Path(f"{base}.pid") - err_log = pathlib.Path(f"{base}-err.log") - stdout_log = pathlib.Path(f"{base}-stdout.log") - result = pathlib.Path(f"{base}-result.json") - - pidfile.write_text("12350") - err_log.write_text("YOLO mode is enabled.\n") - stdout_log.write_text("") - result.write_text(json.dumps({"event": "APPROVE", "comments_count": 0})) - - # time.time() side_effect: - # call 1 (started_wall): base_time - # call 2+ (age check): base_time + 10 → age = 5s < 30s - base_time = time.time() - result_mtime = base_time + 5 - os.utime(result, (result_mtime, result_mtime)) - - time_call_count = [0] - def fake_time(): - time_call_count[0] += 1 - if time_call_count[0] == 1: - return base_time - return base_time + 10 # age = 10 - 5 = 5s < 30s - - poll_count = [0] - - def fake_pid_alive(pid): - if poll_count[0] >= 2: - return False - return True - - def fake_sleep(s): - poll_count[0] += 1 - - with ( - mock.patch.object(monitor_mod, "_tmp_dir", return_value=tmp_path), - mock.patch.object(monitor_mod, "_pid_alive", side_effect=fake_pid_alive), - mock.patch.object(monitor_mod, "_kill_pid"), - mock.patch.object(monitor_mod, "_pid_cmdline_matches", return_value=True), - mock.patch("time.sleep", side_effect=fake_sleep), - mock.patch("time.time", side_effect=fake_time), - ): - st = monitor_mod.monitor_agent( - agent=agent, pr=pr, - timeout=420, stall_timeout=480, poll=15, - require_result=True, no_early_error=True, - ) - - # age < 30s なので fallback せず、プロセス終了後の通常 OK - assert st.status == "OK" - assert "process exited" in st.detail - - -def test_codex_sentinel_takes_priority(monitor_mod, tmp_path): - """codex は sentinel + result.json が先に発火し、age fallback は通らない。""" - pr = 997 - agent = "codex" - base = tmp_path / f"{agent}-review-pr{pr}" - - pidfile = pathlib.Path(f"{base}.pid") - err_log = pathlib.Path(f"{base}-err.log") - stdout_log = pathlib.Path(f"{base}-stdout.log") - result = pathlib.Path(f"{base}-result.json") - - pidfile.write_text("12347") - err_log.write_text("some output\ntokens used\n") - stdout_log.write_text("") - result.write_text(json.dumps({"event": "APPROVE", "comments_count": 0})) - old_mtime = time.time() - 60 - os.utime(result, (old_mtime, old_mtime)) - - kill_called = [] - - def fake_pid_alive(pid): - return True - - def fake_kill_pid(pid, sigterm_grace=3.0): - kill_called.append(pid) - - with ( - mock.patch.object(monitor_mod, "_tmp_dir", return_value=tmp_path), - mock.patch.object(monitor_mod, "_pid_alive", side_effect=fake_pid_alive), - mock.patch.object(monitor_mod, "_kill_pid", side_effect=fake_kill_pid), - mock.patch.object(monitor_mod, "_pid_cmdline_matches", return_value=None), - mock.patch("time.sleep"), - ): - st = monitor_mod.monitor_agent( - agent=agent, pr=pr, - timeout=420, stall_timeout=180, poll=15, - require_result=True, no_early_error=True, - ) - - assert st.status == "OK" - assert st.exit_code == 0 - # codex sentinel チェックによる OK - assert "codex sentinel" in st.detail - assert 12347 in kill_called - - -def test_stale_result_json_from_previous_round_ignored(monitor_mod, tmp_path): - """前 round の古い result.json (mtime < started_wall) は fallback しない。""" - pr = 996 - agent = "gemini" - base = tmp_path / f"{agent}-review-pr{pr}" - - pidfile = pathlib.Path(f"{base}.pid") - err_log = pathlib.Path(f"{base}-err.log") - stdout_log = pathlib.Path(f"{base}-stdout.log") - result = pathlib.Path(f"{base}-result.json") - - pidfile.write_text("12348") - err_log.write_text("YOLO mode is enabled.\n") - stdout_log.write_text("") - result.write_text(json.dumps({"event": "APPROVE", "comments_count": 0})) - # mtime を「現在の 120 秒前」に設定 → started_wall より前 → stale - stale_mtime = time.time() - 120 - os.utime(result, (stale_mtime, stale_mtime)) - - poll_count = [0] - - def fake_pid_alive(pid): - if poll_count[0] >= 2: - return False - return True - - def fake_sleep(s): - poll_count[0] += 1 - - with ( - mock.patch.object(monitor_mod, "_tmp_dir", return_value=tmp_path), - mock.patch.object(monitor_mod, "_pid_alive", side_effect=fake_pid_alive), - mock.patch.object(monitor_mod, "_kill_pid"), - mock.patch.object(monitor_mod, "_pid_cmdline_matches", return_value=True), - mock.patch("time.sleep", side_effect=fake_sleep), - ): - st = monitor_mod.monitor_agent( - agent=agent, pr=pr, - timeout=420, stall_timeout=480, poll=15, - require_result=True, no_early_error=True, - ) - - # stale result.json なので fallback せず、プロセス終了後の通常 OK - assert st.status == "OK" - assert "process exited" in st.detail - - -def test_cmdline_not_validated_skips_fallback(monitor_mod, tmp_path): - """cmdline 未検証 (None) だと age fallback は発火しない。""" - pr = 995 - agent = "gemini" - base = tmp_path / f"{agent}-review-pr{pr}" - - pidfile = pathlib.Path(f"{base}.pid") - err_log = pathlib.Path(f"{base}-err.log") - stdout_log = pathlib.Path(f"{base}-stdout.log") - result = pathlib.Path(f"{base}-result.json") - - pidfile.write_text("12349") - err_log.write_text("YOLO mode is enabled.\n") - stdout_log.write_text("") - result.write_text(json.dumps({"event": "APPROVE", "comments_count": 0})) - base_time = time.time() - result_mtime = base_time + 5 - os.utime(result, (result_mtime, result_mtime)) - - poll_count = [0] - - def fake_pid_alive(pid): - if poll_count[0] >= 2: - return False - return True - - def fake_sleep(s): - poll_count[0] += 1 - - with ( - mock.patch.object(monitor_mod, "_tmp_dir", return_value=tmp_path), - mock.patch.object(monitor_mod, "_pid_alive", side_effect=fake_pid_alive), - mock.patch.object(monitor_mod, "_kill_pid"), - # cmdline_matches が None → cmdline_validated は False のまま - mock.patch.object(monitor_mod, "_pid_cmdline_matches", return_value=None), - mock.patch("time.sleep", side_effect=fake_sleep), - ): - st = monitor_mod.monitor_agent( - agent=agent, pr=pr, - timeout=420, stall_timeout=480, poll=15, - require_result=True, no_early_error=True, - ) - - # cmdline 未検証なので fallback せず、プロセス終了後の通常 OK - assert st.status == "OK" - assert "process exited" in st.detail diff --git a/plugins/ndf/skills-codex/cross-review/tests/test_monitor_stall_default.py b/plugins/ndf/skills-codex/cross-review/tests/test_monitor_stall_default.py deleted file mode 100644 index 77a4f6b..0000000 --- a/plugins/ndf/skills-codex/cross-review/tests/test_monitor_stall_default.py +++ /dev/null @@ -1,119 +0,0 @@ -"""monitor.py の per-agent stall timeout 既定値テスト (PLAN21). - -`_agent_stall_default(agent)` の解決優先度: - 1. env `MONITOR_STALL_` (per-agent 明示) - 2. env `MONITOR_STALL` (両 agent 共通) - 3. `DEFAULT_STALL_AGENT_BUILTIN[agent]` (codex=180, gemini=480) - -gemini は err.log にほぼ進捗を出さないため、ビルトイン既定を 480s と大きめに -取って 1 度目の STALLED 誤検知を避ける。codex は従来通り 180s で変更なし。 -""" -from __future__ import annotations - -import pytest - - -def test_builtin_default_codex(monkeypatch, monitor_mod): - """codex のビルトイン既定は 180s。""" - monkeypatch.delenv("MONITOR_STALL", raising=False) - monkeypatch.delenv("MONITOR_STALL_CODEX", raising=False) - monkeypatch.delenv("MONITOR_STALL_GEMINI", raising=False) - assert monitor_mod._agent_stall_default("codex") == 180 - - -def test_builtin_default_gemini(monkeypatch, monitor_mod): - """gemini のビルトイン既定は 480s (codex より大きい)。""" - monkeypatch.delenv("MONITOR_STALL", raising=False) - monkeypatch.delenv("MONITOR_STALL_CODEX", raising=False) - monkeypatch.delenv("MONITOR_STALL_GEMINI", raising=False) - assert monitor_mod._agent_stall_default("gemini") == 480 - - -def test_per_agent_env_overrides_builtin(monkeypatch, monitor_mod): - """env `MONITOR_STALL_GEMINI` 設定で gemini 既定が上書きされる。""" - monkeypatch.delenv("MONITOR_STALL", raising=False) - monkeypatch.setenv("MONITOR_STALL_GEMINI", "600") - assert monitor_mod._agent_stall_default("gemini") == 600 - # codex は影響を受けない - assert monitor_mod._agent_stall_default("codex") == 180 - - -def test_shared_env_applies_to_both(monkeypatch, monitor_mod): - """env `MONITOR_STALL` 共通指定は両 agent に同じ値が適用される (旧挙動互換)。 - - codex round 3 指摘: `_agent_stall_default()` は呼び出し時に env を再評価する - (import 時固定の `DEFAULT_STALL` ではなく `int(os.environ["MONITOR_STALL"])` を返す)。 - 本テストは monkeypatch で `MONITOR_STALL=240` に書き換え、両 agent が 240 を - 返すことを確認する (= 共通 env が実際に反映されることの検証)。 - """ - monkeypatch.delenv("MONITOR_STALL_CODEX", raising=False) - monkeypatch.delenv("MONITOR_STALL_GEMINI", raising=False) - monkeypatch.setenv("MONITOR_STALL", "240") - # 共通 env が両 agent に効く (per-agent 上書きなしの場合) - assert monitor_mod._agent_stall_default("codex") == 240 - assert monitor_mod._agent_stall_default("gemini") == 240 - - -def test_per_agent_env_takes_precedence_over_shared(monkeypatch, monitor_mod): - """per-agent env > 共通 env の優先順位を確認する。""" - monkeypatch.setenv("MONITOR_STALL", "240") - monkeypatch.setenv("MONITOR_STALL_GEMINI", "777") - monkeypatch.delenv("MONITOR_STALL_CODEX", raising=False) - assert monitor_mod._agent_stall_default("gemini") == 777 - # codex 側は per-agent env が無いので 共通 env (= 240) にフォールバック - assert monitor_mod._agent_stall_default("codex") == 240 - - -def test_unknown_agent_falls_back_to_default_stall(monkeypatch, monitor_mod): - """ビルトインに無い agent 名は `DEFAULT_STALL` にフォールバックする。""" - monkeypatch.delenv("MONITOR_STALL", raising=False) - monkeypatch.delenv("MONITOR_STALL_UNKNOWN", raising=False) - assert monitor_mod._agent_stall_default("unknown") == monitor_mod.DEFAULT_STALL - - -# ---------------- PLAN21 round 5: env が非数値だった場合のフォールバック ---------------- - - -def test_shared_env_non_numeric_falls_back_to_builtin(monkeypatch, monitor_mod, capsys): - """env `MONITOR_STALL` が非数値なら builtin にフォールバック (ValueError で落ちない)。 - - gemini round 4 指摘: `int(os.environ[...])` は非数値で ValueError を出す。 - 監視プロセスを env 設定ミスでクラッシュさせないため、try/except で builtin に戻す。 - """ - monkeypatch.delenv("MONITOR_STALL_CODEX", raising=False) - monkeypatch.delenv("MONITOR_STALL_GEMINI", raising=False) - monkeypatch.setenv("MONITOR_STALL", "abc") - # codex / gemini とも builtin 既定 (180 / 480) に戻る - assert monitor_mod._agent_stall_default("codex") == 180 - assert monitor_mod._agent_stall_default("gemini") == 480 - captured = capsys.readouterr() - # 警告メッセージが stderr に出る - assert "MONITOR_STALL" in captured.err - assert "int に変換できません" in captured.err - - -def test_per_agent_env_non_numeric_falls_back_to_builtin( - monkeypatch, monitor_mod, capsys -): - """env `MONITOR_STALL_` が非数値なら builtin にフォールバック。""" - monkeypatch.delenv("MONITOR_STALL", raising=False) - monkeypatch.setenv("MONITOR_STALL_GEMINI", "not-a-number") - monkeypatch.delenv("MONITOR_STALL_CODEX", raising=False) - # gemini は builtin (480) にフォールバック - assert monitor_mod._agent_stall_default("gemini") == 480 - # codex は env 未設定なので builtin (180) - assert monitor_mod._agent_stall_default("codex") == 180 - captured = capsys.readouterr() - assert "MONITOR_STALL_GEMINI" in captured.err - assert "int に変換できません" in captured.err - - -def test_per_agent_env_non_numeric_does_not_affect_other_agent( - monkeypatch, monitor_mod -): - """non-numeric な per-agent env は対象 agent だけに影響する。""" - monkeypatch.delenv("MONITOR_STALL", raising=False) - monkeypatch.setenv("MONITOR_STALL_GEMINI", "xxx") - monkeypatch.setenv("MONITOR_STALL_CODEX", "200") # codex 側は正常 - assert monitor_mod._agent_stall_default("codex") == 200 - assert monitor_mod._agent_stall_default("gemini") == 480 diff --git a/plugins/ndf/skills-codex/cross-review/tests/test_monitor_zombie.py b/plugins/ndf/skills-codex/cross-review/tests/test_monitor_zombie.py deleted file mode 100644 index c15d2f4..0000000 --- a/plugins/ndf/skills-codex/cross-review/tests/test_monitor_zombie.py +++ /dev/null @@ -1,91 +0,0 @@ -"""monitor.py のゾンビプロセス検出テスト。 - -Docker without --init 環境では、nohup/disown で起動したプロセスが終了後に -ゾンビ化する。`_pid_alive()` が /proc//status の State: Z を検出して -False を返すことを検証する。 -""" -from __future__ import annotations - -import pathlib -from unittest import mock - - -def test_pid_alive_returns_false_for_zombie(monitor_mod): - """State: Z のプロセスに対して _pid_alive() が False を返す。""" - proc_status = "Name:\tbash\nState:\tZ (zombie)\nTgid:\t12345\nPid:\t12345\n" - - with ( - mock.patch("os.kill", return_value=None), - mock.patch.object( - pathlib.Path, "read_text", return_value=proc_status - ), - ): - assert monitor_mod._pid_alive(12345) is False - - -def test_pid_alive_returns_true_for_running(monitor_mod): - """State: S (sleeping) のプロセスに対して True を返す。""" - proc_status = "Name:\tgemini\nState:\tS (sleeping)\nTgid:\t12345\nPid:\t12345\n" - - with ( - mock.patch("os.kill", return_value=None), - mock.patch.object( - pathlib.Path, "read_text", return_value=proc_status - ), - ): - assert monitor_mod._pid_alive(12345) is True - - -def test_pid_alive_returns_true_for_running_state_r(monitor_mod): - """State: R (running) のプロセスに対して True を返す。""" - proc_status = "Name:\tgemini\nState:\tR (running)\nTgid:\t12345\nPid:\t12345\n" - - with ( - mock.patch("os.kill", return_value=None), - mock.patch.object( - pathlib.Path, "read_text", return_value=proc_status - ), - ): - assert monitor_mod._pid_alive(12345) is True - - -def test_pid_alive_returns_false_for_dead_process(monitor_mod): - """kill -0 が ProcessLookupError を返すプロセスに対して False。""" - with mock.patch("os.kill", side_effect=ProcessLookupError): - assert monitor_mod._pid_alive(99999) is False - - -def test_pid_alive_proc_unreadable_falls_back_to_alive(monitor_mod): - """/proc//status が読めない場合 (non-Linux) は kill -0 の結果のみで True。""" - with ( - mock.patch("os.kill", return_value=None), - mock.patch.object( - pathlib.Path, "read_text", side_effect=FileNotFoundError - ), - ): - assert monitor_mod._pid_alive(12345) is True - - -def test_is_zombie_helper(monitor_mod): - """_is_zombie() ヘルパーの動作確認。""" - zombie_status = "Name:\tbash\nState:\tZ (zombie)\nTgid:\t1\nPid:\t1\n" - alive_status = "Name:\tgemini\nState:\tS (sleeping)\nTgid:\t1\nPid:\t1\n" - - with mock.patch.object(pathlib.Path, "read_text", return_value=zombie_status): - assert monitor_mod._is_zombie(1) is True - - with mock.patch.object(pathlib.Path, "read_text", return_value=alive_status): - assert monitor_mod._is_zombie(1) is False - - with mock.patch.object(pathlib.Path, "read_text", side_effect=FileNotFoundError): - assert monitor_mod._is_zombie(1) is False - - -def test_kill_pid_skips_zombie(monitor_mod): - """ゾンビプロセスに対して _kill_pid() はシグナルを送らない。""" - with ( - mock.patch.object(monitor_mod, "_is_zombie", return_value=True), - mock.patch("os.kill") as mock_kill, - ): - monitor_mod._kill_pid(12345) - mock_kill.assert_not_called() diff --git a/plugins/ndf/skills-codex/cross-review/tests/test_state_auto_review_templates.py b/plugins/ndf/skills-codex/cross-review/tests/test_state_auto_review_templates.py deleted file mode 100644 index c0dbd9e..0000000 --- a/plugins/ndf/skills-codex/cross-review/tests/test_state_auto_review_templates.py +++ /dev/null @@ -1,204 +0,0 @@ -"""PR 変更ファイルに応じた自動レビュー観点の分類テスト。""" - -from __future__ import annotations - - -def test_parse_pr_files_payload_normalizes_gh_files_json(state_mod): - entries = state_mod._parse_pr_files_payload( - """ - { - "files": [ - {"path": "src/app.py", "changeType": "MODIFIED"}, - {"path": "docs/new.md", "previousPath": "docs/old.md", "changeType": "RENAMED"}, - {"path": "src/old.py", "changeType": "DELETED"} - ] - } - """ - ) - - assert entries == [ - {"status": "M", "paths": ["src/app.py"]}, - {"status": "R", "paths": ["docs/old.md", "docs/new.md"]}, - {"status": "D", "paths": ["src/old.py"]}, - ] - - -def test_parse_pr_files_api_lines_normalizes_paginated_api_output(state_mod): - entries = state_mod._parse_pr_files_api_lines( - "modified\tsrc/app.py\t\n" - "renamed\tdocs/new.md\tdocs/old.md\n" - "removed\tsrc/old.py\t\n" - ) - - assert entries == [ - {"status": "M", "paths": ["src/app.py"]}, - {"status": "R", "paths": ["docs/old.md", "docs/new.md"]}, - {"status": "D", "paths": ["src/old.py"]}, - ] - - -def test_docs_only_pr_adds_docs_template(state_mod): - entries = state_mod._parse_pr_files_payload( - '{"files":[{"path":"docs/plan.md","changeType":"MODIFIED"},{"path":"README.md","changeType":"ADDED"}]}' - ) - - categories = state_mod._classify_changed_files(entries) - - assert categories == ["common", "docs_only"] - instructions = state_mod._auto_review_instructions(categories) - assert "ドキュメントのみ PR" in instructions - assert "コード、設定、コマンド" in instructions - - -def test_code_pr_detects_code_test_and_frontend(state_mod): - entries = state_mod._parse_pr_files_payload( - """ - {"files":[ - {"path":"src/components/UserCard.tsx","changeType":"MODIFIED"}, - {"path":"tests/UserCard.test.tsx","changeType":"ADDED"} - ]} - """ - ) - - categories = state_mod._classify_changed_files(entries) - - assert "code" in categories - assert "test" in categories - assert "frontend" in categories - - -def test_db_migration_detects_schema_concerns(state_mod): - entries = state_mod._parse_pr_files_payload( - '{"files":[{"path":"database/migrations/20260101_create_users.sql","changeType":"ADDED"}]}' - ) - - categories = state_mod._classify_changed_files(entries) - - assert "db_migration" in categories - assert "DB migration / schema 変更" in state_mod._auto_review_instructions(categories) - - -def test_dependency_ci_api_security_performance_i18n_infra_are_detected(state_mod): - entries = state_mod._parse_pr_files_payload( - """ - {"files":[ - {"path":"package-lock.json","changeType":"MODIFIED"}, - {"path":".github/workflows/test.yml","changeType":"MODIFIED"}, - {"path":"app/api/auth/token_controller.py","changeType":"MODIFIED"}, - {"path":"src/cache/worker.go","changeType":"MODIFIED"}, - {"path":"locales/ja.json","changeType":"MODIFIED"}, - {"path":"terraform/main.tf","changeType":"MODIFIED"} - ]} - """ - ) - - categories = state_mod._classify_changed_files(entries) - - assert "dependency" in categories - assert "config_ci" in categories - assert "api_contract" in categories - assert "auth_security" in categories - assert "performance" in categories - assert "i18n" in categories - assert "infra" in categories - - -def test_deletion_and_rename_are_detected(state_mod): - entries = state_mod._parse_pr_files_payload( - """ - {"files":[ - {"path":"src/old.py","changeType":"DELETED"}, - {"path":"src/b.py","previousPath":"src/a.py","changeType":"RENAMED"} - ]} - """ - ) - - categories = state_mod._classify_changed_files(entries) - - assert "deletion_rename" in categories - - -def test_index_entrypoint_does_not_trigger_performance(state_mod): - entries = state_mod._parse_pr_files_payload( - '{"files":[{"path":"src/index.ts","changeType":"MODIFIED"}]}' - ) - - assert "performance" not in state_mod._classify_changed_files(entries) - - -def test_i18n_extension_detection_does_not_match_po_substrings(state_mod): - false_positive = state_mod._parse_pr_files_payload( - '{"files":[{"path":"src/import.polyfill.js","changeType":"MODIFIED"}]}' - ) - real_po = state_mod._parse_pr_files_payload( - '{"files":[{"path":"locales/messages.po","changeType":"MODIFIED"}]}' - ) - - assert "i18n" not in state_mod._classify_changed_files(false_positive) - assert "i18n" in state_mod._classify_changed_files(real_po) - - -def test_env_files_are_config_and_author_is_not_auth_security(state_mod): - env_file = state_mod._parse_pr_files_payload( - '{"files":[{"path":".env.example","changeType":"MODIFIED"}]}' - ) - author_file = state_mod._parse_pr_files_payload( - '{"files":[{"path":"src/components/AuthorCard.tsx","changeType":"MODIFIED"}]}' - ) - auth_file = state_mod._parse_pr_files_payload( - '{"files":[{"path":"app/auth/session.py","changeType":"MODIFIED"}]}' - ) - authz_file = state_mod._parse_pr_files_payload( - '{"files":[{"path":"app/security/authz.py","changeType":"MODIFIED"}]}' - ) - authentication_file = state_mod._parse_pr_files_payload( - '{"files":[{"path":"app/security/authentication.py","changeType":"MODIFIED"}]}' - ) - authorizer_file = state_mod._parse_pr_files_payload( - '{"files":[{"path":"app/security/authorizer.ts","changeType":"MODIFIED"}]}' - ) - - assert "config_ci" in state_mod._classify_changed_files(env_file) - assert "auth_security" not in state_mod._classify_changed_files(author_file) - assert "auth_security" in state_mod._classify_changed_files(auth_file) - assert "auth_security" in state_mod._classify_changed_files(authz_file) - assert "auth_security" in state_mod._classify_changed_files(authentication_file) - assert "auth_security" in state_mod._classify_changed_files(authorizer_file) - - -def test_tokenizer_async_jobcard_and_tflite_do_not_trigger_categories(state_mod): - tokenizer_file = state_mod._parse_pr_files_payload( - '{"files":[{"path":"src/tokenizer.py","changeType":"MODIFIED"}]}' - ) - token_file = state_mod._parse_pr_files_payload( - '{"files":[{"path":"app/auth/token.py","changeType":"MODIFIED"}]}' - ) - async_helper = state_mod._parse_pr_files_payload( - '{"files":[{"path":"utils/async_helpers.ts","changeType":"MODIFIED"}]}' - ) - job_card = state_mod._parse_pr_files_payload( - '{"files":[{"path":"components/JobCard.tsx","changeType":"MODIFIED"}]}' - ) - worker_file = state_mod._parse_pr_files_payload( - '{"files":[{"path":"src/worker/queue.ts","changeType":"MODIFIED"}]}' - ) - tflite_file = state_mod._parse_pr_files_payload( - '{"files":[{"path":"models/config.tflite","changeType":"MODIFIED"}]}' - ) - terraform_file = state_mod._parse_pr_files_payload( - '{"files":[{"path":"terraform/main.tf","changeType":"MODIFIED"}]}' - ) - - assert "auth_security" not in state_mod._classify_changed_files(tokenizer_file) - assert "auth_security" in state_mod._classify_changed_files(token_file) - assert "performance" not in state_mod._classify_changed_files(async_helper) - assert "performance" not in state_mod._classify_changed_files(job_card) - assert "performance" in state_mod._classify_changed_files(worker_file) - assert "infra" not in state_mod._classify_changed_files(tflite_file) - assert "infra" in state_mod._classify_changed_files(terraform_file) - - -def test_combined_review_instructions_puts_auto_before_manual(state_mod): - assert state_mod._combined_review_instructions("auto", "manual") == "auto\n\nmanual" - assert state_mod._combined_review_instructions("auto", "") == "auto" - assert state_mod._combined_review_instructions("", "manual") == "manual" diff --git a/plugins/ndf/skills-codex/cross-review/tests/test_state_check_oscillation.py b/plugins/ndf/skills-codex/cross-review/tests/test_state_check_oscillation.py deleted file mode 100644 index f00d189..0000000 --- a/plugins/ndf/skills-codex/cross-review/tests/test_state_check_oscillation.py +++ /dev/null @@ -1,101 +0,0 @@ -"""state.py cmd_check_oscillation の non-dict 防御テスト (PLAN21 round 5). - -gemini round 4 指摘: payload.json が dict 以外 (list 等) の場合、 -`payload.get("comments", [])` で AttributeError になる前に明示的に止める。 -""" -from __future__ import annotations - -import argparse -import json -import pathlib - -import pytest - - -PR = 7777 - - -def _seed_state_two_rounds(tmp_dir: pathlib.Path) -> None: - """同一 PR で 2 round 分のエントリを seed する (oscillation 判定の前提)。""" - state = { - "current_pr": PR, - "rounds": [ - {"round": 1, "pr": PR, "started_at": "2026-05-23T00:00:00+00:00"}, - {"round": 2, "pr": PR, "started_at": "2026-05-23T00:10:00+00:00"}, - ], - "deferred_nits": [], - "final": None, - } - (tmp_dir / f"cross-review-pr{PR}-state.json").write_text(json.dumps(state)) - - -def _make_args() -> argparse.Namespace: - return argparse.Namespace(pr=PR) - - -@pytest.fixture() -def patched_tmp_dir(monkeypatch, tmp_path, state_mod): - monkeypatch.setenv("CROSS_REVIEW_TMP_DIR", str(tmp_path)) - return tmp_path - - -def _write_payload( - tmp_dir: pathlib.Path, agent: str, round_no: int, data: object -) -> pathlib.Path: - p = tmp_dir / f"{agent}-review-pr{PR}-round{round_no}-payload.json" - p.write_text(json.dumps(data)) - return p - - -def test_non_dict_payload_dies(patched_tmp_dir, state_mod, capsys): - """payload.json が list 等の非 dict なら die(code=3)。 - - `payload.get(...)` を呼ぶ前に明示的に止めることで、launcher 出力バグや - 別実行の残骸を黙って見逃さない。 - """ - tmp_dir = patched_tmp_dir - _seed_state_two_rounds(tmp_dir) - - # round 1 / round 2 の codex payload を dict で置く (正常 = 比較ベース) - _write_payload(tmp_dir, "codex", 1, {"comments": [{"path": "a.py", "line": 1}]}) - _write_payload(tmp_dir, "codex", 2, {"comments": [{"path": "a.py", "line": 1}]}) - # round 2 の gemini payload は不正 (list) - _write_payload(tmp_dir, "gemini", 2, [{"comments": "should_be_ignored"}]) - - with pytest.raises(SystemExit) as e: - state_mod.cmd_check_oscillation(_make_args()) - assert e.value.code == 3 - captured = capsys.readouterr() - assert "dict ではない" in captured.err - - -def test_non_dict_comment_entry_dies(patched_tmp_dir, state_mod, capsys): - """payload.comments のエントリが dict ではない場合も die(code=3)。""" - tmp_dir = patched_tmp_dir - _seed_state_two_rounds(tmp_dir) - - # round 1 / round 2 とも codex の payload は dict だが、 - # round 2 の comments エントリが str (本来 dict) - _write_payload(tmp_dir, "codex", 1, {"comments": [{"path": "a.py", "line": 1}]}) - _write_payload(tmp_dir, "codex", 2, {"comments": ["not-a-dict-entry"]}) - - with pytest.raises(SystemExit) as e: - state_mod.cmd_check_oscillation(_make_args()) - assert e.value.code == 3 - captured = capsys.readouterr() - assert "dict ではない" in captured.err - - -def test_valid_dict_payloads_dont_die(patched_tmp_dir, state_mod): - """正常な dict payload (regression guard): non-dict 検査が誤検知しないこと。""" - tmp_dir = patched_tmp_dir - _seed_state_two_rounds(tmp_dir) - - # 全 agent の payload を正規の dict で置く (overlap < 50% で continue 期待) - _write_payload(tmp_dir, "codex", 1, {"comments": [{"path": "a.py", "line": 1}]}) - _write_payload(tmp_dir, "codex", 2, {"comments": [{"path": "b.py", "line": 2}]}) - - # exit code 2 (continue) で正常終了 - with pytest.raises(SystemExit) as e: - state_mod.cmd_check_oscillation(_make_args()) - assert e.value.code == 2 diff --git a/plugins/ndf/skills-codex/cross-review/tests/test_state_extra_review_instructions.py b/plugins/ndf/skills-codex/cross-review/tests/test_state_extra_review_instructions.py deleted file mode 100644 index b145b1c..0000000 --- a/plugins/ndf/skills-codex/cross-review/tests/test_state_extra_review_instructions.py +++ /dev/null @@ -1,35 +0,0 @@ -"""state.py の追加レビュー観点オプションのテスト。""" - -from __future__ import annotations - -import argparse - - -def test_extra_review_instructions_accepts_focus(state_mod): - args = argparse.Namespace( - focus="ドキュメントとコードの整合性を重点的に確認", - extra_instructions_file=None, - ) - - assert state_mod._extra_review_instructions(args) == "ドキュメントとコードの整合性を重点的に確認" - - -def test_extra_review_instructions_combines_focus_and_file(tmp_path, state_mod): - extra = tmp_path / "focus.md" - extra.write_text("公開 API の説明と実装差分も確認\n", encoding="utf-8") - args = argparse.Namespace( - focus="ドキュメント整合性", - extra_instructions_file=str(extra), - ) - - assert state_mod._extra_review_instructions(args) == ( - "ドキュメント整合性\n\n公開 API の説明と実装差分も確認" - ) - - -def test_extra_review_instructions_ignores_empty_values(tmp_path, state_mod): - extra = tmp_path / "empty.md" - extra.write_text(" \n", encoding="utf-8") - args = argparse.Namespace(focus=" ", extra_instructions_file=str(extra)) - - assert state_mod._extra_review_instructions(args) == "" diff --git a/plugins/ndf/skills-codex/cross-review/tests/test_state_merge_fix.py b/plugins/ndf/skills-codex/cross-review/tests/test_state_merge_fix.py deleted file mode 100644 index 63c31ba..0000000 --- a/plugins/ndf/skills-codex/cross-review/tests/test_state_merge_fix.py +++ /dev/null @@ -1,776 +0,0 @@ -"""state.py cmd_merge_fix の堅牢化テスト (PLAN21). - -カバー範囲: - 1. 正規パス + 正規 key (`fix_commit` / `fixed_count`) - 2. 正規パス不在 → `/tmp/` fallback で拾える - 3. 戻り値 key 別名 (`commit_sha` / `fixed`) も受理 - 4. 全候補不在 → exit 3 で die + 探索 path 一覧が stderr に出る - 5. (PLAN21 round 2) fallback の stale 検証 - - mtime が round 開始前 → skip - - JSON 内 `pr` 不一致 → skip - - 明示 `--file` は stale 検証スキップ - 6. (PLAN21 round 3) 正規パス JSON parse 失敗 → 即時 die(code=3) - 7. (PLAN21 round 3) `pr` フィールドが数値として解釈できない → skip - 8. (PLAN21 round 3) `_is_fresh_fix_result` 戻り値のタプル化追従 -""" -from __future__ import annotations - -import argparse -import datetime as _dt -import json -import os -import pathlib - -import pytest - - -PR = 9919 - - -def _seed_state(tmp_dir: pathlib.Path) -> None: - state = { - "current_pr": PR, - "rounds": [ - {"round": 1, "pr": PR, "started_at": "2026-05-23T00:00:00+00:00"} - ], - "deferred_nits": [], - "final": None, - } - (tmp_dir / f"cross-review-pr{PR}-state.json").write_text(json.dumps(state)) - - -def _make_args(file_path: pathlib.Path | None = None) -> argparse.Namespace: - return argparse.Namespace(pr=PR, file=str(file_path) if file_path else None) - - -def _read_state(tmp_dir: pathlib.Path) -> dict: - return json.loads((tmp_dir / f"cross-review-pr{PR}-state.json").read_text()) - - -def _canonical_fix() -> dict: - return { - "pr": PR, - "fix_commit": "abc1234", - "ci_status": "SUCCESS", - "ci_failed_checks": [], - "fixed_count": 5, - "by_severity": {"critical": 0, "major": 3, "minor": 2, "nit": 0}, - "resolved_threads": [{"thread_id": "T1"}], - "deferred": [], - "rejected": [], - } - - -@pytest.fixture() -def patched_tmp_dir(monkeypatch, tmp_path, state_mod): - """`CROSS_REVIEW_TMP_DIR` を tmp_path に向けて `_tmp_dir()` を決定的にする。""" - monkeypatch.setenv("CROSS_REVIEW_TMP_DIR", str(tmp_path)) - return tmp_path - - -def test_canonical_path_and_key(patched_tmp_dir, state_mod): - """正規パス ($TMP_DIR/fix-prN-result.json) + 正規 key で state にマージされる。""" - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - fix = _canonical_fix() - (tmp_dir / f"fix-pr{PR}-result.json").write_text(json.dumps(fix)) - - state_mod.cmd_merge_fix(_make_args()) - - st = _read_state(tmp_dir) - merged = st["rounds"][-1]["fix"] - assert merged["commit"] == "abc1234" - assert merged["fixed"] == 5 - assert merged["ci"] == "SUCCESS" - assert merged["by_severity"]["major"] == 3 - - -def test_tmp_fallback_path(patched_tmp_dir, state_mod, tmp_path): - """正規パス不在で /tmp/fix-prN-result.json にある場合は fallback で拾う。 - - 旧プロンプトでサブエージェントが `/tmp/` を指定したケースの救済。 - """ - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - # 正規パスには書かず、/tmp/ のみに置く - fix = _canonical_fix() - fix["fix_commit"] = "tmp_path_commit" - legacy = pathlib.Path(f"/tmp/fix-pr{PR}-result.json") - legacy.write_text(json.dumps(fix)) - try: - state_mod.cmd_merge_fix(_make_args()) - st = _read_state(tmp_dir) - assert st["rounds"][-1]["fix"]["commit"] == "tmp_path_commit" - finally: - legacy.unlink(missing_ok=True) - - -def test_key_alias_commit_sha_and_fixed(patched_tmp_dir, state_mod): - """サブエージェントが別名 (`commit_sha` / `fixed`) で書いても受理する。""" - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - fix = { - "pr": PR, - "commit_sha": "alias_sha", - "fixed": 3, - "ci_status": "SUCCESS", - "deferred": [], - "rejected": [], - "resolved_threads": [], - } - (tmp_dir / f"fix-pr{PR}-result.json").write_text(json.dumps(fix)) - - state_mod.cmd_merge_fix(_make_args()) - - st = _read_state(tmp_dir) - merged = st["rounds"][-1]["fix"] - assert merged["commit"] == "alias_sha" - assert merged["fixed"] == 3 - - -def test_missing_all_candidates_dies_with_paths(patched_tmp_dir, state_mod, capsys): - """どの候補にもファイルが無い場合は exit 3 + 探索 path 一覧が stderr に出る。""" - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - # /tmp/ にも置かない (clean up to be safe) - legacy = pathlib.Path(f"/tmp/fix-pr{PR}-result.json") - legacy.unlink(missing_ok=True) - - with pytest.raises(SystemExit) as e: - state_mod.cmd_merge_fix(_make_args()) - assert e.value.code == 3 - - captured = capsys.readouterr() - # 探索 path 一覧 (どこを見たか) がメッセージに含まれる - assert str(tmp_dir / f"fix-pr{PR}-result.json") in captured.err - assert f"/tmp/fix-pr{PR}-result.json" in captured.err - - -def test_explicit_file_arg_wins(patched_tmp_dir, state_mod, tmp_path): - """`--file` 明示時はそれを最優先で読む (正規パスや /tmp/ より優先)。""" - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - # 正規パスにも置くが、--file で別ファイルを指定する - canonical = _canonical_fix() - canonical["fix_commit"] = "canonical_should_not_be_used" - (tmp_dir / f"fix-pr{PR}-result.json").write_text(json.dumps(canonical)) - - explicit_fix = _canonical_fix() - explicit_fix["fix_commit"] = "explicit_wins" - explicit_path = tmp_path / "custom-fix.json" - explicit_path.write_text(json.dumps(explicit_fix)) - - state_mod.cmd_merge_fix(_make_args(explicit_path)) - - st = _read_state(tmp_dir) - assert st["rounds"][-1]["fix"]["commit"] == "explicit_wins" - - -# ---------------- PLAN21 round 2: fallback stale 検証 ---------------- - - -def _seed_state_with_started_at(tmp_dir: pathlib.Path, started_at: str) -> None: - """`started_at` を任意のタイムスタンプで seed する。""" - state = { - "current_pr": PR, - "rounds": [{"round": 1, "pr": PR, "started_at": started_at}], - "deferred_nits": [], - "final": None, - } - (tmp_dir / f"cross-review-pr{PR}-state.json").write_text(json.dumps(state)) - - -def test_fallback_stale_mtime_is_ignored(patched_tmp_dir, state_mod, capsys): - """正規 fallback ファイルの mtime が round 開始前なら skip。 - - 別 round / 別実行の残骸ファイルを誤マージしないことを確認する。 - """ - tmp_dir = patched_tmp_dir - # round 開始時刻 = 現在 (これより古いファイルは skip 対象) - now_dt = _dt.datetime.now(_dt.timezone.utc).astimezone() - _seed_state_with_started_at(tmp_dir, now_dt.isoformat(timespec="seconds")) - - stale = tmp_dir / f"fix-pr{PR}-result.json" - stale.write_text(json.dumps(_canonical_fix())) - # mtime を 1 時間前に巻き戻す (round 開始より明確に古い) - stale_ts = now_dt.timestamp() - 3600 - os.utime(stale, (stale_ts, stale_ts)) - - with pytest.raises(SystemExit) as e: - state_mod.cmd_merge_fix(_make_args()) - assert e.value.code == 3 - - captured = capsys.readouterr() - # 古いファイルとして skip された旨が stderr に出る - assert "round 開始前" in captured.err or "古いファイル" in captured.err - - -def test_fallback_tmp_stale_is_ignored(patched_tmp_dir, state_mod, capsys): - """`/tmp/fix-prN-result.json` の古い残骸 (別リポジトリの同番号 PR 想定) は skip。 - - codex review 指摘の本丸: PR 番号だけで命名された共有 namespace の - 古いファイルを無条件に拾わないこと。 - """ - tmp_dir = patched_tmp_dir - now_dt = _dt.datetime.now(_dt.timezone.utc).astimezone() - _seed_state_with_started_at(tmp_dir, now_dt.isoformat(timespec="seconds")) - - legacy = pathlib.Path(f"/tmp/fix-pr{PR}-result.json") - legacy.write_text(json.dumps(_canonical_fix())) - # mtime を 2 時間前に巻き戻す (= 過去の別実行で残った想定) - stale_ts = now_dt.timestamp() - 7200 - os.utime(legacy, (stale_ts, stale_ts)) - try: - with pytest.raises(SystemExit) as e: - state_mod.cmd_merge_fix(_make_args()) - assert e.value.code == 3 - captured = capsys.readouterr() - assert "round 開始前" in captured.err or "古いファイル" in captured.err - finally: - legacy.unlink(missing_ok=True) - - -def test_fallback_pr_mismatch_is_ignored(patched_tmp_dir, state_mod, capsys): - """fallback ファイル内の `pr` が対象 PR と一致しない場合は skip。 - - 別リポジトリの同番号 PR の戻り値が `/tmp` 共有 namespace に - 残っていたケースを想定する。 - """ - tmp_dir = patched_tmp_dir - # round 開始は十分過去にして mtime チェックでは弾かれないようにする - past = _dt.datetime(2000, 1, 1, tzinfo=_dt.timezone.utc) - _seed_state_with_started_at(tmp_dir, past.isoformat(timespec="seconds")) - - fix = _canonical_fix() - fix["pr"] = PR + 1 # 別 PR の戻り値 - (tmp_dir / f"fix-pr{PR}-result.json").write_text(json.dumps(fix)) - - with pytest.raises(SystemExit) as e: - state_mod.cmd_merge_fix(_make_args()) - assert e.value.code == 3 - captured = capsys.readouterr() - assert "pr 不一致" in captured.err or "別 PR" in captured.err - - -def test_fallback_fresh_mtime_and_matching_pr_is_accepted(patched_tmp_dir, state_mod): - """mtime が round 開始後 & `pr` 一致なら採用される (regression guard)。""" - tmp_dir = patched_tmp_dir - # round 開始を 1 時間前に置く → 直後に書いたファイルは "fresh" - past_dt = _dt.datetime.now(_dt.timezone.utc).astimezone() - _dt.timedelta(hours=1) - _seed_state_with_started_at(tmp_dir, past_dt.isoformat(timespec="seconds")) - - fix = _canonical_fix() - fix["fix_commit"] = "fresh_ok" - (tmp_dir / f"fix-pr{PR}-result.json").write_text(json.dumps(fix)) - - state_mod.cmd_merge_fix(_make_args()) - - st = _read_state(tmp_dir) - assert st["rounds"][-1]["fix"]["commit"] == "fresh_ok" - - -def test_explicit_file_bypasses_stale_check(patched_tmp_dir, state_mod, tmp_path): - """`--file` 明示時は stale 検証をスキップする (ユーザー指定優先)。""" - tmp_dir = patched_tmp_dir - now_dt = _dt.datetime.now(_dt.timezone.utc).astimezone() - _seed_state_with_started_at(tmp_dir, now_dt.isoformat(timespec="seconds")) - - explicit_fix = _canonical_fix() - explicit_fix["fix_commit"] = "explicit_stale_ok" - explicit_path = tmp_path / "explicit.json" - explicit_path.write_text(json.dumps(explicit_fix)) - # mtime を round 開始前に巻き戻しても、--file 指定なら採用される - stale_ts = now_dt.timestamp() - 3600 - os.utime(explicit_path, (stale_ts, stale_ts)) - - state_mod.cmd_merge_fix(_make_args(explicit_path)) - - st = _read_state(tmp_dir) - assert st["rounds"][-1]["fix"]["commit"] == "explicit_stale_ok" - - -# ---------------- PLAN21 round 3: 正規パス parse 失敗 / pr 型不正 / 戻り値タプル化 ---------------- - - -def test_canonical_path_json_parse_failure_dies_immediately( - patched_tmp_dir, state_mod, capsys -): - """正規パス ($TMP_DIR/fix-prN-result.json) の JSON parse 失敗は即時 die(code=3)。 - - codex round 2 指摘: 壊れた正規パスをスキップして /tmp/ fallback に流れると - 別 PR の戻り値を誤マージする事故が起こるため、正規パスの parse 失敗は致命扱い。 - """ - tmp_dir = patched_tmp_dir - # round 開始を十分過去にして mtime チェックでは弾かれないようにする - past = _dt.datetime(2000, 1, 1, tzinfo=_dt.timezone.utc) - _seed_state_with_started_at(tmp_dir, past.isoformat(timespec="seconds")) - - # 正規パスに壊れた JSON を置く (size>0 だが parse 不能) - canonical = tmp_dir / f"fix-pr{PR}-result.json" - canonical.write_text("{ this is not valid json") - - # /tmp/ 側には別 PR を装う偽の正規 JSON を置く (これに流れ込んだら事故) - legacy = pathlib.Path(f"/tmp/fix-pr{PR}-result.json") - legacy.write_text(json.dumps(_canonical_fix())) - try: - with pytest.raises(SystemExit) as e: - state_mod.cmd_merge_fix(_make_args()) - assert e.value.code == 3 - captured = capsys.readouterr() - # 正規パスの読み取り失敗である旨が stderr に出る - assert "正規パス" in captured.err - # /tmp 側の fix がマージされていないこと (= state.rounds[-1].fix が無い) - st = _read_state(tmp_dir) - assert "fix" not in st["rounds"][-1] - finally: - legacy.unlink(missing_ok=True) - - -def test_fallback_pr_field_non_numeric_is_skipped(patched_tmp_dir, state_mod, capsys): - """fallback ファイルの `pr` フィールドが int 変換不能なら skip (ValueError 防止)。 - - gemini round 2 指摘: `int(file_pr)` で ValueError が裸で上がるとプロセスごと落ちる。 - """ - tmp_dir = patched_tmp_dir - past = _dt.datetime(2000, 1, 1, tzinfo=_dt.timezone.utc) - _seed_state_with_started_at(tmp_dir, past.isoformat(timespec="seconds")) - - # 正規パスに `pr` が int 化できない値の JSON を置く - canonical = tmp_dir / f"fix-pr{PR}-result.json" - bad = _canonical_fix() - bad["pr"] = "not-a-number" - canonical.write_text(json.dumps(bad)) - - with pytest.raises(SystemExit) as e: - state_mod.cmd_merge_fix(_make_args()) - # /tmp/ 側にも候補が無いので最終的には "戻り値ファイル無し" で die(3) - assert e.value.code == 3 - captured = capsys.readouterr() - # 数値として解釈できない旨が stderr に出る - assert "数値として解釈できない" in captured.err - # state にマージされていないこと - st = _read_state(tmp_dir) - assert "fix" not in st["rounds"][-1] - - -def test_is_fresh_fix_result_returns_tuple_with_parsed_payload( - patched_tmp_dir, state_mod -): - """`_is_fresh_fix_result` は (is_fresh, parsed_payload) を返す。 - - gemini round 2 指摘の性能改善: cmd_merge_fix 側で再パースしないよう、 - fresh な場合は parse 済み dict を返す。 - """ - tmp_dir = patched_tmp_dir - past_dt = _dt.datetime.now(_dt.timezone.utc).astimezone() - _dt.timedelta(hours=1) - past_ts = past_dt.timestamp() - - fix = _canonical_fix() - fix["fix_commit"] = "tuple_return_ok" - p = tmp_dir / f"fix-pr{PR}-result.json" - p.write_text(json.dumps(fix)) - - is_fresh, parsed = state_mod._is_fresh_fix_result(p, PR, past_ts, is_canonical=True) - assert is_fresh is True - assert isinstance(parsed, dict) - assert parsed["fix_commit"] == "tuple_return_ok" - - -def test_is_fresh_fix_result_returns_none_when_stale(patched_tmp_dir, state_mod): - """stale な場合は (False, None) を返す。""" - tmp_dir = patched_tmp_dir - now_dt = _dt.datetime.now(_dt.timezone.utc).astimezone() - - p = tmp_dir / f"fix-pr{PR}-result.json" - p.write_text(json.dumps(_canonical_fix())) - stale_ts = now_dt.timestamp() - 3600 - os.utime(p, (stale_ts, stale_ts)) - - is_fresh, parsed = state_mod._is_fresh_fix_result( - p, PR, now_dt.timestamp(), is_canonical=False - ) - assert is_fresh is False - assert parsed is None - - -# ---------------- PLAN21 round 4: non-dict JSON 防御 ---------------- - - -def test_is_fresh_fix_result_non_dict_json_is_skipped(patched_tmp_dir, state_mod, capsys): - """`_is_fresh_fix_result` は dict 以外 (list 等) の JSON を読んだら (False, None) を返す。 - - gemini round 3 指摘: `json.loads` は list / int / str も返しうるため、 - `payload.get(...)` 呼び出し前に `isinstance(payload, dict)` で防御する必要がある。 - """ - tmp_dir = patched_tmp_dir - # round 開始は十分過去にして mtime チェックを通す - past_dt = _dt.datetime.now(_dt.timezone.utc).astimezone() - _dt.timedelta(hours=1) - past_ts = past_dt.timestamp() - - p = tmp_dir / f"fix-pr{PR}-result.json" - # JSON として valid だが dict ではない (list) - p.write_text(json.dumps([{"fix_commit": "should_be_ignored"}])) - - is_fresh, parsed = state_mod._is_fresh_fix_result( - p, PR, past_ts, is_canonical=False - ) - assert is_fresh is False - assert parsed is None - captured = capsys.readouterr() - # dict ではない旨が stderr に出る - assert "dict ではない" in captured.err - - -def test_is_fresh_fix_result_non_dict_json_canonical_dies_round5( - patched_tmp_dir, state_mod, capsys -): - """PLAN21 round 5 で挙動変更: 正規パス (is_canonical=True) で非 dict なら die(code=3)。 - - 旧 round 4 では skip (False, None) だったが、codex round 4 指摘により - `/tmp/` fallback への流れ込みを防ぐため parse 失敗と同様に即時中断する。 - """ - tmp_dir = patched_tmp_dir - past_dt = _dt.datetime.now(_dt.timezone.utc).astimezone() - _dt.timedelta(hours=1) - past_ts = past_dt.timestamp() - - p = tmp_dir / f"fix-pr{PR}-result.json" - p.write_text(json.dumps(["not", "a", "dict"])) - - with pytest.raises(SystemExit) as e: - state_mod._is_fresh_fix_result(p, PR, past_ts, is_canonical=True) - assert e.value.code == 3 - captured = capsys.readouterr() - assert "dict ではない" in captured.err - assert "正規パス" in captured.err - - -def test_explicit_file_non_dict_json_dies(patched_tmp_dir, state_mod, capsys, tmp_path): - """`--file` 明示時に non-dict JSON が渡されたら die(code=3) で即時中断。 - - gemini round 3 指摘: list 等が渡されると後続 `fix.get(...)` でクラッシュするため、 - 明示指定の場合も dict 検証を行ってから fix に代入する。 - """ - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - - explicit_path = tmp_path / "bad-explicit.json" - # JSON として valid だが dict ではない (list) - explicit_path.write_text(json.dumps([_canonical_fix()])) - - with pytest.raises(SystemExit) as e: - state_mod.cmd_merge_fix(_make_args(explicit_path)) - assert e.value.code == 3 - captured = capsys.readouterr() - assert "dict ではない" in captured.err - # state は更新されていない - st = _read_state(tmp_dir) - assert "fix" not in st["rounds"][-1] - - -# ---------------- PLAN21 round 5: 正規パス non-dict 即時 die / --file 厳格化 ---------------- - - -def test_canonical_path_non_dict_json_dies_immediately( - patched_tmp_dir, state_mod, capsys -): - """正規パス ($TMP_DIR/fix-prN-result.json) の JSON が dict 以外なら即時 die(code=3)。 - - codex round 4 指摘: parse 失敗と同様、非 dict も「壊れた正規出力」として扱う。 - skip で /tmp/ fallback に流れると別実行の戻り値を誤マージする経路が残るため、 - list / str / int 等が返ったら fallback には進まず即時中断する。 - """ - tmp_dir = patched_tmp_dir - past = _dt.datetime(2000, 1, 1, tzinfo=_dt.timezone.utc) - _seed_state_with_started_at(tmp_dir, past.isoformat(timespec="seconds")) - - # 正規パスに list (非 dict) を置く - canonical = tmp_dir / f"fix-pr{PR}-result.json" - canonical.write_text(json.dumps([{"fix_commit": "should_not_be_read"}])) - - # /tmp/ 側には別 PR を装う正規 JSON を置く (これに流れ込んだら事故) - legacy = pathlib.Path(f"/tmp/fix-pr{PR}-result.json") - legacy.write_text(json.dumps(_canonical_fix())) - try: - with pytest.raises(SystemExit) as e: - state_mod.cmd_merge_fix(_make_args()) - assert e.value.code == 3 - captured = capsys.readouterr() - # 正規パスで非 dict であった旨が stderr に出る - assert "正規パス" in captured.err - assert "dict ではない" in captured.err - # /tmp 側の fix がマージされていないこと - st = _read_state(tmp_dir) - assert "fix" not in st["rounds"][-1] - finally: - legacy.unlink(missing_ok=True) - - -def test_explicit_file_nonexistent_dies(patched_tmp_dir, state_mod, capsys, tmp_path): - """`--file` 指定パスが存在しなければ fallback に進まず die(code=3)。 - - codex round 4 指摘: ユーザー明示指定なのに無言で fallback に流れて - 別実行の戻り値を誤マージするのを防ぐ。 - """ - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - # 正規パスにも置いて、fallback に流れたら事故と判別できるようにする - (tmp_dir / f"fix-pr{PR}-result.json").write_text(json.dumps(_canonical_fix())) - - nonexistent = tmp_path / "does-not-exist.json" - assert not nonexistent.exists() - - with pytest.raises(SystemExit) as e: - state_mod.cmd_merge_fix(_make_args(nonexistent)) - assert e.value.code == 3 - captured = capsys.readouterr() - assert "存在しません" in captured.err - # fallback が読まれていない (state にマージされていない) - st = _read_state(tmp_dir) - assert "fix" not in st["rounds"][-1] - - -def test_explicit_file_empty_dies(patched_tmp_dir, state_mod, capsys, tmp_path): - """`--file` 指定パスが空ファイルなら fallback に進まず die(code=3)。""" - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - (tmp_dir / f"fix-pr{PR}-result.json").write_text(json.dumps(_canonical_fix())) - - empty = tmp_path / "empty.json" - empty.write_text("") - - with pytest.raises(SystemExit) as e: - state_mod.cmd_merge_fix(_make_args(empty)) - assert e.value.code == 3 - captured = capsys.readouterr() - assert "空です" in captured.err - st = _read_state(tmp_dir) - assert "fix" not in st["rounds"][-1] - - -def test_explicit_file_invalid_json_dies(patched_tmp_dir, state_mod, capsys, tmp_path): - """`--file` 指定パスが JSON 不正なら fallback に進まず die(code=3)。""" - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - (tmp_dir / f"fix-pr{PR}-result.json").write_text(json.dumps(_canonical_fix())) - - bad = tmp_path / "bad.json" - bad.write_text("{ this is not valid json") - - with pytest.raises(SystemExit) as e: - state_mod.cmd_merge_fix(_make_args(bad)) - assert e.value.code == 3 - captured = capsys.readouterr() - assert "parse に失敗" in captured.err or "読み取り" in captured.err - st = _read_state(tmp_dir) - assert "fix" not in st["rounds"][-1] - - -# ---------------- PLAN25: _count() 正規化 / int 混入時の堅牢化 ---------------- - - -@pytest.mark.parametrize( - "value, expected", - [ - (3, 3), # int(件数) はそのまま件数として扱う - (0, 0), # int の 0 - ([{"a": 1}, {"b": 2}], 2), # list は len() - ([], 0), # 空 list - ((1, 2, 3), 3), # tuple も len() - (None, 0), # None は 0 - (False, 0), # bool は int サブクラスだが件数扱いしない - (True, 0), # bool=True も 0 - ("xyz", 0), # 想定外の型 (str) は 0 - # PLAN25 round2 (gemini 指摘): LLM が件数を数値文字列で返すケースを許容 - ("3", 3), # 数値文字列 → int - ("0", 0), # "0" も 0 - ("12", 12), # 複数桁 - ("abc", 0), # 数値でない文字列は 0 - ("", 0), # 空文字は isdigit()=False → 0 - ("-1", 0), # 負号付きは strip 後も isdigit()=False → 0 (件数は非負) - ("1.5", 0), # 小数点付きは isdigit()=False → 0 - # PLAN25 round4 (gemini 指摘): 単一 dict を 1 件として扱う - ({"thread_id": "x"}, 1), # 単一 dict → 1 - ({}, 1), # 空 dict も 1 件 (構造化データ 1 要素扱い) - # PLAN25 round4 (gemini 指摘): 数値文字列の前後空白を許容 (strip) - (" 3 ", 3), # 前後空白付き数値文字列 → strip して int - (" 12", 12), # 先頭空白のみ - ("7 ", 7), # 末尾空白のみ - (" ", 0), # 空白のみは strip 後空文字 → 0 - ], -) -def test_count_normalizes_int_list_tuple_none_bool(state_mod, value, expected): - """`_count()` が int / dict / list / tuple / None / bool / 数値文字列 / 想定外型を件数(int)に正規化する。 - - codex review 指摘 (PLAN25): `_count()` の挙動がテストで固定されておらず、 - 同じ TypeError 経路 (`len(int)`) が再発しても検知できない。 - PLAN25 round2 (gemini 指摘): 数値文字列 ("3" 等) も件数として許容する。 - PLAN25 round4 (gemini 指摘): 単一 dict は 1 件、数値文字列は前後空白を strip して許容。 - """ - assert state_mod._count(value) == expected - - -def test_merge_fix_int_counts_do_not_crash_and_persist(patched_tmp_dir, state_mod): - """`resolved_threads` / `deferred` / `rejected` が int でも TypeError を出さず件数が保存される。 - - PLAN25 の本丸: fix サブエージェントが list の代わりに int(件数) を書いても - `len(int)` で落ちず完走すること。 - resolved_threads / rejected は _count() で int がそのまま件数として保存される。 - PLAN25 round4 (統一ルール): deferred が int (per-item を失った劣化表現) のときは - 件数を失わないよう _count() の値を保存する。展開はできないので deferred_nits は空。 - """ - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - fix = _canonical_fix() - # list ではなく int(件数) を書く (再発防止対象のケース) - fix["resolved_threads"] = 4 - fix["deferred"] = 2 - fix["rejected"] = 1 - (tmp_dir / f"fix-pr{PR}-result.json").write_text(json.dumps(fix)) - - # TypeError を出さずに完走すること - state_mod.cmd_merge_fix(_make_args()) - - st = _read_state(tmp_dir) - merged = st["rounds"][-1]["fix"] - assert merged["resolved_threads"] == 4 - # 統一ルール: int は劣化表現なので _count() の件数 (2) を保存する - assert merged["deferred"] == 2 - assert merged["rejected"] == 1 - # int は展開できないため deferred_nits は空のまま (per-item データは復元不能) - assert st["deferred_nits"] == [] - - -def test_merge_fix_deferred_single_dict_is_wrapped(patched_tmp_dir, state_mod): - """`deferred` が単一 dict のとき 1 件として扱い、deferred_nits に 1 件展開される。 - - PLAN25 round4 (gemini #3): LLM が単一要素を list ではなく dict 単体で返した - 場合のフォールバック。保存件数 == 展開件数 == 1 を固定する。 - """ - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - fix = _canonical_fix() - # list ではなく単一 dict を書く (LLM 出力揺らぎ) - fix["deferred"] = {"comment_id": 7, "summary": "single-nit"} - (tmp_dir / f"fix-pr{PR}-result.json").write_text(json.dumps(fix)) - - state_mod.cmd_merge_fix(_make_args()) - - st = _read_state(tmp_dir) - merged = st["rounds"][-1]["fix"] - # 構造化データ (dict) なので展開件数 (1) を保存 - assert merged["deferred"] == 1 - assert len(st["deferred_nits"]) == 1 - assert st["deferred_nits"][0]["summary"] == "single-nit" - assert st["deferred_nits"][0]["pr"] == PR - assert st["deferred_nits"][0]["round"] == 1 - - -def test_merge_fix_list_deferred_expands_nits(patched_tmp_dir, state_mod): - """`deferred` が list の場合は従来どおり件数が保存され deferred_nits が展開される。 - - int 対応で list の既存挙動 (件数集計 + deferred_nits 展開) が壊れていないことの regression guard。 - """ - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - fix = _canonical_fix() - fix["resolved_threads"] = [{"thread_id": "T1"}, {"thread_id": "T2"}] - fix["deferred"] = [ - {"comment_id": 1, "summary": "nit-1"}, - {"comment_id": 2, "summary": "nit-2"}, - ] - fix["rejected"] = [{"comment_id": 3, "summary": "rej-1"}] - (tmp_dir / f"fix-pr{PR}-result.json").write_text(json.dumps(fix)) - - state_mod.cmd_merge_fix(_make_args()) - - st = _read_state(tmp_dir) - merged = st["rounds"][-1]["fix"] - # list は len() で件数化 - assert merged["resolved_threads"] == 2 - assert merged["deferred"] == 2 - assert merged["rejected"] == 1 - # deferred_nits が展開され、pr / round が付与される - assert len(st["deferred_nits"]) == 2 - summaries = {n["summary"] for n in st["deferred_nits"]} - assert summaries == {"nit-1", "nit-2"} - for n in st["deferred_nits"]: - assert n["pr"] == PR - assert n["round"] == 1 - - -# ---------------- PLAN25 round2 (gemini): deferred 非 dict 要素ガード ---------------- - - -def test_merge_fix_deferred_non_dict_elements_are_skipped(patched_tmp_dir, state_mod): - """`deferred` に dict 以外の要素 (文字列等) が混じってもクラッシュせず、dict のみ展開される。 - - gemini round2 指摘 (major): LLM がスキーマを無視して `deferred` に - 文字列のリスト (例: ["nit: ..."]) を返すと `{**d}` で - `TypeError: 'str' object is not a mapping` が発生する。 - ループ内の `isinstance(d, dict)` ガードで非 dict 要素をスキップすること。 - """ - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - fix = _canonical_fix() - # dict / 文字列 / None / int が混在した deferred (LLM 出力揺らぎ) - fix["deferred"] = [ - {"comment_id": 1, "summary": "nit-dict"}, - "nit: please fix this string element", - None, - 42, - {"comment_id": 2, "summary": "nit-dict-2"}, - ] - (tmp_dir / f"fix-pr{PR}-result.json").write_text(json.dumps(fix)) - - # TypeError を出さずに完走すること - state_mod.cmd_merge_fix(_make_args()) - - st = _read_state(tmp_dir) - merged = st["rounds"][-1]["fix"] - # codex round3 指摘 (major): 保存件数は「実際に deferred_nits へ展開される - # dict 要素数」と一致させる。非 dict 要素は除外されるので 2 になる。 - assert merged["deferred"] == 2 - # deferred_nits には dict 要素のみが展開される - assert len(st["deferred_nits"]) == 2 - summaries = {n["summary"] for n in st["deferred_nits"]} - assert summaries == {"nit-dict", "nit-dict-2"} - for n in st["deferred_nits"]: - assert n["pr"] == PR - assert n["round"] == 1 - - -# ---------- PLAN25 round3 (codex): 保存件数 == deferred_nits 展開件数 ---------- - - -def test_merge_fix_deferred_count_matches_expanded_nits(patched_tmp_dir, state_mod): - """codex round3 指摘 (major / 正確性): mixed list の `deferred` を渡したとき、 - `rounds[].fix["deferred"]` の保存件数が deferred_nits に実際に展開された - dict 要素数と一致することを検証する回帰テスト。 - - round2 の dict ガードは「展開件数 < 保存件数」という食い違いを生んでいた - (保存は list 全体の長さ、展開は dict のみ)。本テストで両者の一致を固定する。 - """ - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - fix = _canonical_fix() - # dict 3 件 + 非 dict 4 件 の mixed list - fix["deferred"] = [ - {"comment_id": 1, "summary": "nit-a"}, - "str-element", - None, - {"comment_id": 2, "summary": "nit-b"}, - 123, - ["nested", "list"], - {"comment_id": 3, "summary": "nit-c"}, - ] - (tmp_dir / f"fix-pr{PR}-result.json").write_text(json.dumps(fix)) - - state_mod.cmd_merge_fix(_make_args()) - - st = _read_state(tmp_dir) - merged = st["rounds"][-1]["fix"] - expanded = [n for n in st["deferred_nits"] if n.get("round") == 1] - # 保存件数 == 実展開件数 == dict 要素数 (3) - assert merged["deferred"] == len(expanded) == 3 - summaries = {n["summary"] for n in expanded} - assert summaries == {"nit-a", "nit-b", "nit-c"} diff --git a/plugins/ndf/skills-codex/cross-review/tests/test_state_read_result.py b/plugins/ndf/skills-codex/cross-review/tests/test_state_read_result.py deleted file mode 100644 index a243fc2..0000000 --- a/plugins/ndf/skills-codex/cross-review/tests/test_state_read_result.py +++ /dev/null @@ -1,160 +0,0 @@ -"""state.py cmd_read_result の result.json スキーマ揺れに対するテスト。 - -カバー範囲: - 1. 正規スキーマ (`event` / `comments_count`) → state にマージされる - 2. 変則スキーマ (`intent` / `comment_count`) → 同等にマージされる (フォールバック) - 3. event / intent いずれも欠落 → die(exit 1) で fail + state 不変 -""" -from __future__ import annotations - -import argparse -import json -import pathlib - -import pytest - - -PR = 4242 -AGENT = "gemini" - - -def _seed_state(tmp_dir: pathlib.Path) -> dict: - state = { - "current_pr": PR, - "rounds": [ - {"round": 1, "pr": PR, "started_at": "2026-05-21T00:00:00+00:00"} - ], - "final": None, - } - (tmp_dir / f"cross-review-pr{PR}-state.json").write_text(json.dumps(state)) - return state - - -def _make_args(file_path: pathlib.Path) -> argparse.Namespace: - return argparse.Namespace(pr=PR, agent=AGENT, file=str(file_path)) - - -def _read_state(tmp_dir: pathlib.Path) -> dict: - return json.loads((tmp_dir / f"cross-review-pr{PR}-state.json").read_text()) - - -@pytest.fixture() -def patched_tmp_dir(monkeypatch, tmp_path, state_mod): - """`CROSS_REVIEW_TMP_DIR` を tmp_path に向ける。""" - monkeypatch.setenv("CROSS_REVIEW_TMP_DIR", str(tmp_path)) - return tmp_path - - -def test_canonical_schema(patched_tmp_dir, state_mod): - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - result = { - "event": "APPROVE", - "posted_as": "APPROVE", - "comments_count": 0, - "review_url": "https://example/pr/1#1", - "by_severity": {"critical": 0, "major": 0, "minor": 0, "nit": 0}, - } - rfile = tmp_dir / "result.json" - rfile.write_text(json.dumps(result)) - - state_mod.cmd_read_result(_make_args(rfile)) - - st = _read_state(tmp_dir) - merged = st["rounds"][-1][AGENT] - assert merged["intent"] == "APPROVE" - assert merged["posted_as"] == "APPROVE" - assert merged["comments"] == 0 - assert merged["review_url"] == "https://example/pr/1#1" - assert merged["by_severity"]["critical"] == 0 - - -def test_alias_schema_intent_and_comment_count(patched_tmp_dir, state_mod): - """gemini が `intent` / `comment_count` で書き出すパターンも受理する。""" - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - result = { - "intent": "APPROVE", - "comment_count": 3, - "review_url": "https://example/pr/2#2", - "by_severity": {"critical": 0, "major": 0, "minor": 2, "nit": 1}, - } - rfile = tmp_dir / "result.json" - rfile.write_text(json.dumps(result)) - - state_mod.cmd_read_result(_make_args(rfile)) - - st = _read_state(tmp_dir) - merged = st["rounds"][-1][AGENT] - assert merged["intent"] == "APPROVE" - # posted_as は別名 result.json には存在しないので intent と同値にフォールバック - assert merged["posted_as"] == "APPROVE" - assert merged["comments"] == 3 - - -def test_missing_event_and_intent_dies(patched_tmp_dir, state_mod): - """event / intent いずれも無ければ exit 1 で fail し state は不変であること。""" - tmp_dir = patched_tmp_dir - seeded = _seed_state(tmp_dir) - result = {"comments_count": 0} - rfile = tmp_dir / "result.json" - rfile.write_text(json.dumps(result)) - - with pytest.raises(SystemExit) as e: - state_mod.cmd_read_result(_make_args(rfile)) - assert e.value.code == 1 - - # state は更新されていない (rounds[-1] に agent エントリが追加されていない) - st = _read_state(tmp_dir) - assert AGENT not in st["rounds"][-1] - assert st["rounds"][-1]["round"] == seeded["rounds"][-1]["round"] - - -def test_empty_result_file_dies(patched_tmp_dir, state_mod): - """空 result.json → die (result 未生成扱い)。""" - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - rfile = tmp_dir / "result.json" - rfile.write_text("") - - with pytest.raises(SystemExit) as e: - state_mod.cmd_read_result(_make_args(rfile)) - assert e.value.code == 1 - - -# ---------------- PLAN21 round 5: non-dict / 不正 JSON 防御 ---------------- - - -def test_non_dict_result_json_dies(patched_tmp_dir, state_mod, capsys): - """result.json が list 等の非 dict なら die(code=3)。 - - gemini round 4 指摘: `r.get(...)` で AttributeError になる前に明示的に止める。 - """ - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - rfile = tmp_dir / "result.json" - # JSON valid だが dict ではない - rfile.write_text(json.dumps([{"event": "APPROVE"}])) - - with pytest.raises(SystemExit) as e: - state_mod.cmd_read_result(_make_args(rfile)) - assert e.value.code == 3 - captured = capsys.readouterr() - assert "dict ではない" in captured.err - # state は更新されていない - st = _read_state(tmp_dir) - assert AGENT not in st["rounds"][-1] - - -def test_invalid_json_result_file_dies(patched_tmp_dir, state_mod, capsys): - """JSON parse 不能なら die(code=3)。""" - tmp_dir = patched_tmp_dir - _seed_state(tmp_dir) - rfile = tmp_dir / "result.json" - rfile.write_text("{ this is not valid json") - - with pytest.raises(SystemExit) as e: - state_mod.cmd_read_result(_make_args(rfile)) - assert e.value.code == 3 - captured = capsys.readouterr() - assert "parse" in captured.err.lower() or "parse" in captured.err diff --git a/plugins/ndf/skills-codex/deploy/SKILL.md b/plugins/ndf/skills-codex/deploy/SKILL.md deleted file mode 100644 index 769919d..0000000 --- a/plugins/ndf/skills-codex/deploy/SKILL.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -name: deploy -description: "Create deploy PRs from feature to environment branches." -argument-hint: " (例: qa/staging, release/v2)" -disable-model-invocation: true -allowed-tools: - - Bash - - Read ---- - -# 環境デプロイPR作成コマンド - -現在のfeatureブランチを指定した環境ブランチへデプロイするためのPRを作成する。`{feature}_to_{env}` という命名のdeployブランチを作成し、最新 origin/main を取り込んでから環境ブランチへPRを出す。 - -## 使用方法 - -``` -/ndf:deploy qa/staging -/ndf:deploy release/v2 -``` - -## cherry-pick-pr との使い分け - -| 観点 | cherry-pick-pr | deploy | -|---|---|---| -| 適用範囲 | featureブランチの**一部コミット**を選択 | featureブランチ**全体**を適用 | -| ブランチ戦略 | 環境ブランチから短命ブランチ派生 | featureブランチから deploy ブランチ派生 | -| main取り込み | 必須 | 必須 | -| 用途 | 特定修正のみ検証環境に届けたい | feature機能全体を環境で検証したい | - -## 処理フロー - -### 1. バリデーション - -```bash -CURRENT_BRANCH=$(git branch --show-current) -[[ "$CURRENT_BRANCH" == "main" || "$CURRENT_BRANCH" == "master" ]] && \ - echo "❌ Error: デフォルトブランチからデプロイできません" && exit 1 -``` - -### 2. deployブランチ名の導出 - -```bash -FEATURE_BRANCH=$(git branch --show-current) -# 環境名を抽出: "qa/staging" → "staging", "release/v2" → "v2" -ENV_SUFFIX=$(echo "$ARGUMENTS" | sed 's|.*/||') -DEPLOY_BRANCH="${FEATURE_BRANCH}_to_${ENV_SUFFIX}" -``` - -### 3. 既存PRチェック - -```bash -EXISTING_PR=$(gh pr list --head "$DEPLOY_BRANCH" --base "$ARGUMENTS" \ - --json number,url --jq '.[0].url // empty') -if [[ -n "$EXISTING_PR" ]]; then - echo "✅ PR already exists: $EXISTING_PR" - exit 0 -fi -``` - -既存PRがあれば更新は「deployブランチにpushする」だけで済むため、再作成しない。 - -### 4. deployブランチ作成 + main取り込み - -```bash -git fetch origin main -git checkout -b "$DEPLOY_BRANCH" -git merge origin/main --no-edit || { - echo "❌ main とのmerge conflict。手動解決が必要です" - git merge --abort - git checkout "$FEATURE_BRANCH" - git branch -D "$DEPLOY_BRANCH" - exit 1 -} -``` - -### 5. push + PR作成 - -```bash -git push -u origin "$DEPLOY_BRANCH" -gh pr create --base "$ARGUMENTS" --head "$DEPLOY_BRANCH" \ - --title "$DEPLOY_BRANCH → $ARGUMENTS" \ - --body "$(cat <<'EOF' -## Summary -- 環境デプロイ用PR -- 元ブランチ: $FEATURE_BRANCH -- main取り込み済み - -## Test plan -- [ ] $ARGUMENTS 環境で動作確認 - - -EOF -)" -``` - -### 6. 元ブランチに復帰 - -```bash -git checkout "$FEATURE_BRANCH" -``` - -## 注意事項 - -- デフォルトブランチからの実行は禁止 -- main取り込みで conflict が出た場合、deployブランチを削除して戻る(featureブランチ側を先に同期すべき) -- deployブランチは PR マージ後に削除してよい -- 環境ブランチへの再デプロイは「同じ deployブランチに push」でPRが更新される - -## 関連 - -- `/ndf:cherry-pick-pr` — 一部コミットだけを環境に届ける場合 -- `/ndf:branch-fix-strategy` — ブランチ運用戦略の原則 -- `/ndf:sync-main` — featureブランチに main を取り込む diff --git a/plugins/ndf/skills-codex/docker-container-access/01-environment-detection.md b/plugins/ndf/skills-codex/docker-container-access/01-environment-detection.md deleted file mode 100644 index 51941bc..0000000 --- a/plugins/ndf/skills-codex/docker-container-access/01-environment-detection.md +++ /dev/null @@ -1,84 +0,0 @@ -# 環境判定ガイド - -## Step 1: 自身の環境を確認 - -```bash -# 自分がコンテナ内で動作しているか確認 -cat /proc/1/cgroup 2>/dev/null | grep -q docker && echo "コンテナ内" || echo "ホスト環境" - -# または -[ -f /.dockerenv ] && echo "コンテナ内" || echo "ホスト環境" -``` - -## Step 2: Docker環境の種類を判定 - -自身がコンテナ内の場合、以下のいずれかの環境です: - -| 環境 | 説明 | 判定方法 | -|-----|------|---------| -| **DinD** (Docker in Docker) | コンテナ内に独立したDockerデーモン | `docker info`でDocker rootが`/var/lib/docker` | -| **DooD** (Docker outside of Docker) | ホストのDockerソケットを共有 | `/var/run/docker.sock`がマウントされている | - -```bash -# DooD判定: docker.sockがマウントされているか -ls -la /var/run/docker.sock 2>/dev/null && echo "DooD環境の可能性" || echo "DinDまたはホスト環境" - -# Docker rootディレクトリの確認 -docker info 2>/dev/null | grep "Docker Root Dir" -``` - -## DinD環境でのアクセス - -DinD環境では、**localhost**で他のコンテナにアクセスできます。 - -### 特徴 -- コンテナ内に独立したDockerデーモンが動作 -- ネットワークは通常のDocker環境と同じ -- `localhost:ポート`でアクセス可能 - -### アクセス例 - -```bash -# Webサーバーへのアクセス -curl http://localhost:8080 - -# データベースへの接続 -mysql -h localhost -P 3306 -u user -p - -# Playwright MCPでのアクセス -# URL: http://localhost:3000 -``` - -## 環境判定スクリプト - -```bash -#!/bin/bash -# Docker環境判定スクリプト - -echo "=== Docker環境判定 ===" - -# 自分がコンテナ内かチェック -if [ -f /.dockerenv ] || grep -q docker /proc/1/cgroup 2>/dev/null; then - echo "実行環境: Dockerコンテナ内" - - # DinD/DooD判定 - if [ -S /var/run/docker.sock ]; then - echo "Docker形式: DooD (Docker outside of Docker)" - echo "" - echo "→ 他のコンテナへのアクセスにはコンテナ名を使用してください" - echo "→ bind mountはホストのパスを参照するため注意が必要です" - else - echo "Docker形式: DinD (Docker in Docker)" - echo "" - echo "→ localhostで他のコンテナにアクセス可能です" - fi -else - echo "実行環境: ホストマシン" - echo "" - echo "→ 通常のDocker操作が可能です" -fi - -echo "" -echo "=== 利用可能なコンテナ ===" -docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null || echo "Dockerが利用できません" -``` diff --git a/plugins/ndf/skills-codex/docker-container-access/02-dood-access.md b/plugins/ndf/skills-codex/docker-container-access/02-dood-access.md deleted file mode 100644 index d19fc96..0000000 --- a/plugins/ndf/skills-codex/docker-container-access/02-dood-access.md +++ /dev/null @@ -1,134 +0,0 @@ -# DooD環境でのアクセスガイド - -## 特徴 - -- ホストのDockerデーモンを共有 -- localhostはホストマシンを指す(他のコンテナではない) -- コンテナ間通信はDockerネットワーク経由 - -## アクセス方法 - -### 1. コンテナ名でアクセス(同一ネットワーク内) - -```bash -# コンテナ名を確認 -docker ps --format "table {{.Names}}\t{{.Ports}}" - -# コンテナ名でアクセス -curl http://my-web-container:8080 - -# docker-composeの場合、サービス名でアクセス -curl http://web:8080 -``` - -### 2. Dockerネットワーク経由 - -```bash -# ネットワーク一覧を確認 -docker network ls - -# 特定ネットワークのコンテナを確認 -docker network inspect bridge --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\n"}}{{end}}' - -# IPアドレスでアクセス -curl http://172.17.0.2:8080 -``` - -### 3. 同一ネットワークへの参加 - -```bash -# 自分のコンテナを対象コンテナと同じネットワークに接続 -docker network connect my-network $(hostname) - -# その後、コンテナ名でアクセス可能 -curl http://target-container:8080 -``` - -## curlでのアクセス例 - -```bash -# NG: localhostは使えない -curl http://localhost:8080 # → Connection refused - -# OK: コンテナ名を使用 -curl http://my-app-container:8080 - -# OK: docker-composeのサービス名 -curl http://api:3000 - -# OK: コンテナのIPアドレス -CONTAINER_IP=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' my-container) -curl http://${CONTAINER_IP}:8080 -``` - -## Playwright MCP / Chrome DevTools MCP - -```bash -# DooD環境では、コンテナ名またはIPを使用 -# URL: http://web-container:3000 (コンテナ名) -# URL: http://172.17.0.3:3000 (IP) -``` - ---- - -## bind mountの注意点 - -### 問題 - -DooD環境では、`docker run -v`や`docker-compose`のbind mountは**ホストマシンのパス**を参照します。開発コンテナ内のパスではありません。 - -```yaml -# NG: DooD環境では期待通りに動作しない -volumes: - - ./local-dir:/app/data # ホストの./local-dirを参照してしまう -``` - -### 解決策 - -#### 1. Dockerfileでコピー(推奨) - -```dockerfile -FROM node:18 -WORKDIR /app -COPY . . -RUN npm install -CMD ["npm", "start"] -``` - -#### 2. 名前付きボリュームを使用 - -```bash -# ボリュームを作成 -docker volume create my-data - -# ファイルをボリュームにコピー -docker run --rm -v my-data:/data -v $(pwd):/src alpine cp -r /src/. /data/ - -# ボリュームをマウントしてコンテナ起動 -docker run -v my-data:/app/data my-image -``` - -#### 3. docker cpを使用 - -```bash -# コンテナにファイルをコピー -docker cp ./local-file.txt my-container:/app/ - -# コンテナからファイルを取得 -docker cp my-container:/app/output.txt ./ -``` - -### docker-compose.yml での対応 - -```yaml -# DooD環境対応版 -version: '3.8' -services: - app: - build: . # Dockerfileでファイルをコピー - volumes: - - app-data:/app/data # 名前付きボリューム使用 - -volumes: - app-data: -``` diff --git a/plugins/ndf/skills-codex/docker-container-access/03-troubleshooting.md b/plugins/ndf/skills-codex/docker-container-access/03-troubleshooting.md deleted file mode 100644 index 0acac32..0000000 --- a/plugins/ndf/skills-codex/docker-container-access/03-troubleshooting.md +++ /dev/null @@ -1,63 +0,0 @@ -# トラブルシューティング - -## Q: `curl: (7) Failed to connect to localhost port 8080` - -**原因**: DooD環境でlocalhostを使用している - -**解決策**: -```bash -# コンテナ名またはIPを使用 -docker ps # コンテナ名を確認 -curl http://container-name:8080 -``` - -## Q: bind mountしたファイルが見えない - -**原因**: DooD環境ではホストのパスを参照している - -**解決策**: -```bash -# docker cpでコピー -docker cp ./file.txt container:/app/ - -# または名前付きボリュームを使用 -``` - -## Q: コンテナ間で通信できない - -**原因**: 異なるDockerネットワークに所属している - -**解決策**: -```bash -# 同じネットワークに接続 -docker network connect my-network container-a -docker network connect my-network container-b -``` - -## Q: docker.sockへのアクセス権限がない - -**解決策**: -```bash -# docker グループに追加(要再ログイン) -sudo usermod -aG docker $USER - -# または一時的に権限付与 -sudo chmod 666 /var/run/docker.sock -``` - ---- - -# ベストプラクティス - -## DO(推奨) - -- **コンテナアクセス前に環境を判定する** -- **DooD環境ではコンテナ名/サービス名を使用する** -- **ファイル共有はDockerfileのCOPYまたは名前付きボリュームを使用** -- **docker-composeではサービス名でアクセス** - -## DON'T(非推奨) - -- **環境を確認せずにlocalhostを使用する** -- **DooD環境でbind mountに依存する** -- **IPアドレスをハードコードする(変わる可能性がある)** diff --git a/plugins/ndf/skills-codex/docker-container-access/SKILL.md b/plugins/ndf/skills-codex/docker-container-access/SKILL.md deleted file mode 100644 index eadd1ff..0000000 --- a/plugins/ndf/skills-codex/docker-container-access/SKILL.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -name: docker-container-access -description: "Diagnose Docker container access and localhost routing." -when_to_use: "Docker / コンテナへのアクセス・localhost 接続不可・DinD/DooD 環境判定が必要なとき。Triggers: 'docker access', 'container connect', 'localhost not working', 'DinD', 'DooD', 'Docker接続', 'コンテナアクセス', 'curl container'" -allowed-tools: - - Read - - Bash - - Glob ---- - -# Docker Container Access Skill - -## 概要 - -ローカル開発環境がDocker開発コンテナ上で動作している場合、他のDockerコンテナへのアクセス方法が通常と異なります。このスキルでは、環境を判定し、適切なアクセス方法を選択するためのガイドラインを提供します。 - -## クイックリファレンス - -``` -環境判定 → アクセス方法 -──────────────────────────────── -ホスト環境 → localhost:port -DinD環境 → localhost:port -DooD環境 → container-name:port または IP:port - -ファイル共有(DooD環境) -──────────────────────────────── -Dockerfile COPY → 推奨(ビルド時にコピー) -名前付きボリューム → 推奨(永続化が必要な場合) -docker cp → OK(一時的なコピー) -bind mount → NG(ホストのパスを参照) -``` - -## 環境判定(最初に実行) - -```bash -# 自分がコンテナ内か確認 -[ -f /.dockerenv ] && echo "コンテナ内" || echo "ホスト環境" - -# DooD判定 -ls -la /var/run/docker.sock 2>/dev/null && echo "DooD環境" || echo "DinDまたはホスト" -``` - -| 環境 | 説明 | コンテナへのアクセス | -|-----|------|-------------------| -| **ホスト** | 通常のDocker環境 | `localhost:port` | -| **DinD** | コンテナ内に独立したDockerデーモン | `localhost:port` | -| **DooD** | ホストのDockerソケットを共有 | `container-name:port` | - -## 詳細ガイド - -詳細は以下のファイルを参照してください: - -| ファイル | 内容 | -|---------|------| -| `01-environment-detection.md` | 環境判定の詳細、判定スクリプト | -| `02-dood-access.md` | DooD環境でのアクセス方法、bind mount注意点 | -| `03-troubleshooting.md` | トラブルシューティング、ベストプラクティス | - -## よくある問題(簡易版) - -| 症状 | 原因 | 解決策 | -|-----|------|--------| -| `localhost`で接続できない | DooD環境 | コンテナ名を使用 | -| bind mountしたファイルが見えない | DooD環境 | `docker cp`または名前付きボリューム | -| コンテナ間で通信できない | 別ネットワーク | 同じネットワークに接続 | - -## 関連Skill - -- **python-execution**: Python実行環境の判定 -- **corder-code-templates**: Dockerfileテンプレート - -## 関連リソース - -- [Docker Networking](https://docs.docker.com/network/) -- [Docker in Docker](https://hub.docker.com/_/docker) diff --git a/plugins/ndf/skills-codex/fix/SKILL.md b/plugins/ndf/skills-codex/fix/SKILL.md deleted file mode 100644 index f7ce931..0000000 --- a/plugins/ndf/skills-codex/fix/SKILL.md +++ /dev/null @@ -1,303 +0,0 @@ ---- -name: fix -description: "Fix actionable PR review comments." -when_to_use: "PRレビューコメント (codex/gemini/人間) の指摘を実際にコード修正で対応したいとき。review-pr-comments で分類した後の修正フェーズに使う。Triggers: 'PRコメント対応', 'PRレビュー修正', 'PR fix', 'review feedback fix', 'コメントに対応して修正'" -argument-hint: "[PR番号] [--defer-nit] [--severity-min critical|major|minor]" -allowed-tools: - - Bash - - Read - - Edit - - Write - - Glob - - Grep ---- - -# PR修正コマンド - -直前PR、または引数で指定されたPRのreview comment確認・修正対応実行。 - -## 起動モード - -このスキルは **メインセッション直接実行** と **サブエージェント (`general-purpose`) 起動** の両方に対応する。 -長丁場のクロスレビューループ(`/ndf:cross-review`)からは **必ずサブエージェント経由で起動** されることを想定: - -```python -# メインからの起動例(cross-review が内部でこれを行う) -Agent( - subagent_type="general-purpose", - description="Fix PR review comments (sub-agent)", - prompt=""" -/ndf:fix --defer-nit を実行してください。 - -PR: -リポジトリ: -重要度ポリシー: critical/major/minor は修正、nit は deferred として残す -完了後の戻り値: 件数サマリ + 修正コミット SHA + 残 nit リスト -""" -) -``` - -サブエージェント側ではこの SKILL.md を読み込んで、自己完結で -**修正 → コミット → push → reply → Resolve Conversation** まで実行する。 -メインへの戻り値は最小限のサマリのみ。 - -## 引数 - -| 引数 | 意味 | 既定 | -|---|---|---| -| `[PR番号]` | 対象 PR | 直前 PR | -| `--defer-nit` | nit 指摘は修正せず deferred としてリスト出力 | OFF | -| `--severity-min LEVEL` | 指定重要度未満は無視(`critical` / `major` / `minor`) | `minor` (= minor 以上を修正) | - -## 重要度ベースの自動修正ポリシー - -`[重要度 / カテゴリ]` プレフィックス(`/ndf:review` 出力規約)で分類。 -**ただし重要度ラベルを鵜呑みにしない** — 各指摘ごとにコード/仕様を独自に調査し、 -本来の重要度を判定し直してから下表の動作を適用する(bot のラベリングは参考値に過ぎない)。 - -| 重要度 | 動作 | ユーザ問い合わせ | -|---|---|---| -| `critical` | **必ず自動修正** | なし | -| `major` | **必ず自動修正** | なし | -| `minor` / `nit` (パフォーマンス・可読性・重複コード排除) | **このPRで修正対応**。特にトータル行数が減る方向の修正は積極的に実施 | なし | -| `minor` / `nit` (上記カテゴリ、修正範囲が +30 行を超えそう) | ユーザ問い合わせ | あり | -| `minor` (その他) | 自動修正(明らかな改善のみ)。判断が割れるなら `nit` として deferred 扱い | なし | -| `nit` (その他) | `--defer-nit` 指定時は **修正せず deferred リスト** に追加。最後にまとめてユーザ問い合わせ | あり(最後に1回) | - -**重要度の独自判定**: -- AI agent (CodeRabbit / Copilot 等) が `nit` と付けていても、実体がパフォーマンス改善や重複排除なら **minor/nit カテゴリ修正対象** として扱う -- 逆に AI agent が `critical` と付けていても、実害がないスタイル指摘なら `nit` 相当に格下げして deferred 化してよい -- 重要度はカテゴリ(performance/readability/duplication/security/style/etc)と合わせて、コード本体を読んだ上で判定する - -**指摘の正否判断**: -- ロジック・仕様逸脱・セキュリティ: コード/仕様を確認してから修正可否判断 -- bot 指摘で **明らかに誤読** している場合(例: 意図的な変数展開を「クオート不足」と指摘する等): 修正しない、reply で理由説明 -- 仕様判断が必要な指摘(API 変更、互換性破壊など): ユーザ問い合わせ対象(critical でもエスカレーション) - -**自動判断できない場合の取り扱い** (context 節約のため安易に user に投げない): -- 仕様文書(docs/, README)を読んで判断する -- 既存テストを読んで挙動を確認する -- 関連する他コードの慣例を確認する -- それでも不明なら deferred リストに「要ユーザ判断」として記録、最後にまとめて問い合わせ - -## 手順 - -1. review comment取得 + 重要度を**独自に再判定**(AI agent のラベルは参考値) -2. **CIエラー確認**(`gh pr checks ` で **現時点の** 失敗ジョブを検出) - - **完了待ちはしない**。実行中(PENDING/IN_PROGRESS)のチェックは無視して次ステップへ進む - - 直近で失敗(FAILURE)状態のジョブのみを修正対象に取り込む -3. 修正対象を確定: - - `critical` / `major` → 全件修正対象 - - `minor` / `nit` (パフォーマンス・可読性・重複排除) → 修正対象。+30行超なら **deferred + ユーザ問い合わせ** - - `minor` (その他) → 修正対象(明らかでないものは `deferred[]` へ) - - `nit` (その他、`--defer-nit` 時) → `deferred[]` のみ、修正しない - - CIエラー → 全件修正対象(PRテスト範囲外の **flaky テストも見つけ次第修正**) -4. 問題点修正 - - **コード行数が減る方向の修正は積極的に実施**(重複排除、不要分岐除去 等) -5. **コミット前の再確認**(修正作業中に状況が変わっている可能性への対応) - - **review comment再取得**: 作業中に新しいコメントが追加されていないか確認 - - **CI状態再確認**: 現時点の状態だけ確認(完了待ちはしない)。新しい失敗が出ていれば対象に取り込む - - 新しい指摘/失敗があれば手順3に戻る -6. コミット・プッシュ -7. PRにSummaryコメントを追加(対応した件数 + deferred 件数を明記) -8. 対応したインラインコメントに個別に返信 -9. **deferred スレッドには `[deferred / nit]` のラベル付き返信** を投稿(resolve はしない) -10. reviewerに再レビューを依頼 -11. 対応完了したインラインコメントを「Resolve Conversation」にする(`resolveReviewThread` mutation) - - resolve した thread_id / comment_id / path / line を `resolved_threads[]` に記録 - - `deferred` / `rejected` の thread は Resolve しない(次ラウンドで再評価するため) -12. **戻り値ファイルを書き出す**: `/tmp/fix-pr<番号>-result.json` (後述「戻り値フォーマット」参照) - - `ci_failed_checks` には `gh pr checks --json name,state` から `state=FAILURE` の name を抽出して列挙 - - push 直後の CI 再実行結果は**待たない**ため、戻り値の `ci_status` は push 時点での既知失敗のみを反映する - -- 4〜6はgit、1〜2/5と7以降はgithub mcpまたはghを利用 - -**flakyテストの扱い**: PR の変更範囲外で発生している flaky テストも、見つけ次第このPRで修正する。 -flaky を放置するとリポジトリ全体のコード品質が下がり、後続 PR の CI 信頼性も損なわれるため。 - -## CIエラーチェック - -### 失敗ジョブの検出 - -```bash -# PRの全チェック状態を確認(FAIL/PASS/PENDING) -gh pr checks - -# JSON形式で詳細取得 -gh pr checks --json name,state,link,completedAt - -# 失敗ジョブのみ抽出 -gh pr checks --json name,state | \ - python3 -c "import json,sys; [print(c['name']) for c in json.load(sys.stdin) if c['state']=='FAILURE']" - -# 実行中ジョブのみ抽出(状態スナップショット用。完了は待たない) -gh pr checks --json name,state | \ - python3 -c "import json,sys; [print(c['name']) for c in json.load(sys.stdin) if c['state'] in ('PENDING','IN_PROGRESS','QUEUED')]" -``` - -### CI完了待ちはしない - -このスキルでは **CI 完了待ちは行わない**(`gh pr checks --watch` 等は使わない)。 -- 各チェックポイントでは「現時点で FAILURE のジョブ」のみを取り込んで修正する -- push 後の CI 再実行結果も待たない(待機中に context を消費しないため) -- ただし `gh pr checks --json name,state` での **状態スナップショット取得は実施** - し、戻り値の `ci_status` / `ci_failed_checks` に反映する - -### 失敗ログの取得 - -```bash -# ワークフロー実行ID取得 -RUN_ID=$(gh run list --branch --limit 1 --json databaseId --jq '.[0].databaseId // empty') -[ -z "$RUN_ID" ] && { echo "No CI run found for this branch"; exit 0; } - -# 失敗ステップのログだけ表示(効率的) -gh run view $RUN_ID --log-failed - -# 特定ジョブのログ -gh run view $RUN_ID --job --log -``` - -### CIエラーの分類と対応方針 - -| エラー種別 | 対応方針 | -|---|---| -| **lint/format** | 自動修正ツール実行(`ruff`, `prettier`, `eslint --fix` 等)→ コミット | -| **型チェック** | 型定義・アノテーションを修正。無視コメントは原則禁止(根本対応) | -| **テスト失敗** | 失敗テストを読み、実装/テストどちらが正しいか判断してから修正。テスト側の問題なら仕様確認 | -| **ビルドエラー** | 依存関係・構文・設定ファイルを確認 | -| **依存脆弱性** | 可能ならバージョン更新、無理なら除外ルール追加(理由明記) | -| **タイムアウト/flaky** | retry設定、テスト分割、リトライ追加。**PR範囲外の flaky も見つけ次第修正**(放置でリポジトリ全体の品質劣化を招くため) | -| **インフラ一時障害** | 再実行で解消することがあるため `gh run rerun $RUN_ID` を先に試す | - -### review指摘との統合 - -review指摘とCIエラーは**同じPRで一緒に修正**する: -- 同じファイル・機能に関する指摘とCIエラーは1コミットにまとめる -- 独立しているなら別コミットに分離(git log で追いやすい) - -## ghコマンド例 - -### PR コメント一括取得 (3 ソース) - -```bash -# インラインコメント / レビュー body / PR レベルコメントを一括取得 -FETCH_SCRIPT="${PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}/skills-codex/fix/scripts/fetch-pr-comments.sh" -"$FETCH_SCRIPT" -``` - -### コメントへの返信 - -```bash -# PRのレビューコメント一覧を取得 (インラインコメントのみ) -gh api repos/{owner}/{repo}/pulls/{pr_number}/comments - -# 特定のコメントに返信(in_reply_to にコメントIDを指定) -gh api repos/{owner}/{repo}/pulls/{pr_number}/comments \ - -f body="修正しました。" \ - -F in_reply_to={comment_id} -``` - -### Resolve Conversation - -```bash -# GraphQL APIでスレッドをresolveする -gh api graphql -f query=' - mutation { - resolveReviewThread(input: {threadId: "{thread_node_id}"}) { - thread { isResolved } - } - } -' -``` - -### thread_node_idの取得方法 - -```bash -# PRのレビュースレッド一覧を取得(node_id含む) -gh api graphql -f query=' - query { - repository(owner: "{owner}", name: "{repo}") { - pullRequest(number: {pr_number}) { - reviewThreads(first: 100) { - nodes { - id - isResolved - comments(first: 1) { - nodes { body } - } - } - } - } - } - } -' -``` - -**方針**: -- 品質・可読性・セキュリティ向上、既存機能影響なし -- 指摘がすべて正しいとは限らない。修正前に仕様を調査し、実施の可否を判断すること -- 未対応の場合はその理由をコメントに書き込む - -## 戻り値フォーマット(必須) - -サブエージェント呼び出し時の context 節約のため、**実行結果は `/tmp/fix-pr<番号>-result.json` に書き出す**: - -```json -{ - "pr": 67, - "fix_commit": "abc1234", - "ci_status": "SUCCESS" | "FAILURE" | "PENDING" | "NONE", - "ci_failed_checks": [], - "ci_note": null, - "fixed_count": 5, - "by_severity": {"critical": 1, "major": 2, "minor": 2, "nit": 0}, - "resolved_threads": [ - { - "thread_id": "PRRT_...", - "comment_id": 3222849090, - "path": "src/foo.py", - "line": 42 - } - ], - "deferred": [ - { - "comment_id": 3222849090, - "thread_id": "PRRT_...", - "path": "src/foo.py", - "line": 42, - "severity": "nit", - "category": "style", - "summary": "末尾セミコロンの有無", - "reason_for_deferral": "好みの範囲。プロジェクト規約と齟齬なし" - } - ], - "rejected": [ - { - "comment_id": 3222849090, - "summary": "heredoc を <<'JSON' にせよ", - "reason_for_rejection": "$SHA を意図的に展開する必要があり、クオート化すると逆に壊れる" - } - ], - "summary_comment_url": "https://github.com/.../pull/67#issuecomment-..." -} -``` - -**フィールド説明**: - -- `ci_failed_checks` — `ci_status = FAILURE` のとき、失敗した check 名の配列。`/ndf:cross-review` 側で code-related (`pint/larastan/test/build/lint/type`) と meta-only (`check_pr_requirements/assignees/reviewers/labels`) を分類し、メタチェックのみ失敗ならループ継続する -- `ci_note` — code-related ではない CI 失敗の補足。例: `"メタチェックのみ失敗: check_pr_requirements — Assignees 未設定"` -- `resolved_threads` — 手順 11 で `resolveReviewThread` mutation を実行したスレッド一覧。`deferred` / `rejected` の thread は **Resolve しない**(再評価のため) - -サブエージェントとして起動された場合は、この JSON をメインに返すサマリの基礎とする。 - -## 作業完了報告(必須) - -メイン or PR への報告内容(戻り値ファイルから抽出): -- 対応した指摘の件数(重要度別: critical/major/minor/nit) -- **deferred 件数**(主に nit、最後にユーザ問い合わせ予定) -- **rejected 件数**(bot 指摘が不適切で修正しなかった件、各々理由付き) -- **対応したCIエラーの一覧**(ジョブ名、エラー内容、修正方法) -- **対応した flaky テストの一覧**(PR範囲外も含む) -- 修正コミット SHA / 修正ファイル一覧 -- 戻り値ファイルパス: `/tmp/fix-pr<番号>-result.json` -- **PR URL を最後に必ず記載**(例: `https://github.com///pull/<番号>`) diff --git a/plugins/ndf/skills-codex/fix/scripts/fetch-pr-comments.sh b/plugins/ndf/skills-codex/fix/scripts/fetch-pr-comments.sh deleted file mode 100755 index aa7f97f..0000000 --- a/plugins/ndf/skills-codex/fix/scripts/fetch-pr-comments.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash -# Usage: fetch-pr-comments.sh -# 3 ソース (インラインコメント / レビュー body / PR レベルコメント) を一括取得し、 -# タグ付き行単位で stdout に出力する。 -# 全ソース取得失敗時は非 0 で終了する(0件取得と取得失敗を区別)。 -set -uo pipefail - -if [[ $# -lt 2 ]] || [[ -z "${1:-}" ]] || [[ -z "${2:-}" ]]; then - echo "ERROR: 引数が不足しています。Usage: $0 " >&2 - exit 1 -fi - -REPO="$1" -PR="$2" - -FAIL_COUNT=0 - -# 1. インラインコメント (diff の特定行に紐づく) -# 本文全体を保持する。改行は \n エスケープして 1 行に収める。 -# gh api --jq は内部で jq -r 相当だが、環境差を吸収するため明示的に jq -r へパイプする。 -if ! gh api "repos/${REPO}/pulls/${PR}/comments" --paginate \ - | jq -r '.[] | "\(.path // "?"):\(.line // .original_line // "?") [\(.user.login)] \(.body // "" | gsub("\n"; "\\n") | gsub("```"; "` ` `"))"'; then - echo "WARNING: インラインコメントの取得に失敗しました (repos/${REPO}/pulls/${PR}/comments)" >&2 - (( FAIL_COUNT += 1 )) || true -fi - -# 2. レビュー body (CHANGES_REQUESTED / COMMENTED 等の総評) -# 本文全体を保持する。改行は \n エスケープして 1 行に収める。 -if ! gh api "repos/${REPO}/pulls/${PR}/reviews" --paginate \ - | jq -r '.[] | select(.body != null and .body != "") | "[REVIEW-BODY] [\(.user.login)] state=\(.state) \(.body | gsub("\n"; "\\n") | gsub("```"; "` ` `"))"'; then - echo "WARNING: レビュー body の取得に失敗しました (repos/${REPO}/pulls/${PR}/reviews)" >&2 - (( FAIL_COUNT += 1 )) || true -fi - -# 3. PR レベルコメント (Conversation タブの通常コメント) -# 本文全体を保持する。改行は \n エスケープして 1 行に収める。 -if ! gh api "repos/${REPO}/issues/${PR}/comments" --paginate \ - | jq -r '.[] | "[PR-COMMENT] [\(.user.login)] \(.body // "" | gsub("\n"; "\\n") | gsub("```"; "` ` `"))"'; then - echo "WARNING: PR レベルコメントの取得に失敗しました (repos/${REPO}/issues/${PR}/comments)" >&2 - (( FAIL_COUNT += 1 )) || true -fi - -# 全ソース失敗時のみ非 0 で終了(認証切れ等の検出) -if (( FAIL_COUNT >= 3 )); then - echo "ERROR: 全 3 ソースの取得に失敗しました" >&2 - exit 1 -fi diff --git a/plugins/ndf/skills-codex/git-gh-operations/01-common-errors.md b/plugins/ndf/skills-codex/git-gh-operations/01-common-errors.md deleted file mode 100644 index a7afa44..0000000 --- a/plugins/ndf/skills-codex/git-gh-operations/01-common-errors.md +++ /dev/null @@ -1,145 +0,0 @@ -# Git / gh 共通エラー事例集 - -## 1. git add pathspec エラー - -### 事象 -``` -fatal: pathspec 'lambda-batch/CarImageProcessingPipeline/src/foo.py' did not match any files -``` - -### 原因 -CWD が `/work/repo/lambda-batch/CarImageProcessingPipeline/` なのに、 -リポジトリルートからの相対パスで `git add` した。 - -`git status` はリポジトリルートからの相対パスで表示するが、 -`git add` は CWD からの相対パスで解決する。 - -### 予防策 -```bash -# Step 1: CWD確認 -pwd -# => /work/repo/lambda-batch/CarImageProcessingPipeline/ - -# Step 2: git status の出力を確認 -git status -# modified: lambda-batch/CarImageProcessingPipeline/src/foo.py -# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -# これはリポジトリルートからの相対パス - -# Step 3: CWD からの相対パスに変換 -git add src/foo.py -# または -git add . # CWD以下のすべての変更 -``` - -## 2. gh api 404 エラー - -### 事象 -``` -gh api repos/owner/repo/pulls/comments/123/replies -f body='message' -# => 404 Not Found -``` - -### 原因 -POST メソッドが必要な API エンドポイントに GET でアクセスした。 -`gh api` はデフォルトで GET を使用する。 - -### 修正 -```bash -gh api -X POST repos/owner/repo/pulls/comments/123/replies -f body='message' -``` - -## 3. GitHub 自己 Approve エラー - -### 事象 -``` -Could not approve for pull request review. Can not approve your own pull request -``` - -### 原因 -GitHub はセキュリティ上、自分で作成した PR を APPROVE できない。 - -### 対策 -```bash -# pending review を削除してから COMMENT として再送信 -# method: "delete_pending" → method: "create" + event: "COMMENT" -``` - -## 4. AWS CLI [$LATEST] パースエラー - -### 事象 -``` -Unknown options: , , , -``` - -### 原因 -CloudWatch ログストリーム名に含まれる `[$LATEST]` が -`--query` JMESPath パーサーや shell の glob として解釈される。 - -### 対策 -```bash -# シングルクォートで囲んでも --query との組み合わせで問題が出る -# --output json + python パースが最も安全 -aws logs get-log-events \ - --log-group-name "/aws/lambda/func-name" \ - --log-stream-name '2026/02/18/[$LATEST]abc123' \ - --output json | python3 -c " -import sys, json -data = json.loads(sys.stdin.read()) -for e in data['events']: - print(e['message'].strip()) -" -``` - -## 5. git commit メッセージの特殊文字 - -### 事象 -コミットメッセージに日本語や改行が含まれるとエスケープ問題が発生。 - -### 対策 -常に HEREDOC 形式を使用: -```bash -git commit -m "$(cat <<'EOF' -日本語メッセージ - -詳細説明 - -Co-Authored-By: Claude Opus 4.6 -EOF -)" -``` - -注意: `<<'EOF'` (シングルクォート付き)で変数展開を抑制する。 - -## 6. gh pr checks が exit code 1 で止まる - -### 事象 -``` -gh pr checks 11765 2>&1 -# => チェック結果は表示されるが、1つでもfailがあると exit code 1 で終了 -# => Claude Code が「コマンド失敗」と判定して処理を中断 -``` - -### 原因 -`gh pr checks` は CI チェックに失敗があると非0の exit code を返す仕様。 -Claude Code の Bash ツールはコマンドの exit code が 0 以外だとエラーとして扱う。 - -### 対策 -常に `|| true` を付けて exit code を 0 にする: -```bash -# チェック一覧を取得(failがあっても止まらない) -gh pr checks 11765 2>&1 || true - -# --watch で完了待ちする場合も同様 -gh pr checks 11765 --watch 2>&1 || true - -# 失敗のみフィルタする場合 -gh pr checks 11765 2>&1 | grep -i fail || true -``` - -### 補足 -同様の問題が発生する gh コマンド: -- `gh run view RUN_ID --log-failed` (失敗ログ取得時) -- `gh pr diff` (差分が大きい場合にパイプ破損) - -いずれも `2>&1 || true` を付けることで安全に実行できる。 diff --git a/plugins/ndf/skills-codex/git-gh-operations/SKILL.md b/plugins/ndf/skills-codex/git-gh-operations/SKILL.md deleted file mode 100644 index 68b8a7a..0000000 --- a/plugins/ndf/skills-codex/git-gh-operations/SKILL.md +++ /dev/null @@ -1,228 +0,0 @@ ---- -name: git-gh-operations -description: "Resolve git and GitHub CLI operation errors." -when_to_use: "git / gh コマンドでエラーが出た or 操作方法に迷うとき。Triggers: 'git add', 'git commit', 'git push', 'gh pr', 'gh api', 'GitHub操作', 'gitエラー', 'fatal:', 'pathspec'" -allowed-tools: - - Bash - - Read ---- - -# Git / gh 操作スキル - -## 最重要ルール: CWD とパスの整合性 - -git コマンドはすべて **CWD からの相対パス** で解決される。 -操作前に必ず `pwd` で CWD を確認すること。 - -### パターン1: CWDがサブディレクトリの場合 - -``` -# CWD: /work/repo/lambda-batch/MyProject/ -# リポジトリルート: /work/repo/ - -# NG: リポジトリルートからのパスを指定 -git add lambda-batch/MyProject/src/foo.py -# => fatal: pathspec did not match any files - -# OK: CWDからの相対パスを指定 -git add src/foo.py - -# OK: 絶対パスを指定 -git add /work/repo/lambda-batch/MyProject/src/foo.py -``` - -### パターン2: 安全な方法 - -```bash -# 方法A: git -C でリポジトリルートを指定 -git -C /work/repo add lambda-batch/MyProject/src/foo.py - -# 方法B: CWD を変更せずに絶対パスを使用 -git add "$(git rev-parse --show-toplevel)/lambda-batch/MyProject/src/foo.py" - -# 方法C(推奨): CWDからの相対パスを使用 -# まず pwd で確認してからパスを組み立てる -``` - -## git 操作チェックリスト - -### git add の前に - -1. `pwd` で CWD を確認 -2. `git status` で変更ファイルのパスを確認(表示されるパスはリポジトリルートからの相対パス) -3. `git status` の出力パスと CWD の関係を計算してから `git add` する - -### git commit の前に - -1. `git diff --cached` でステージング内容を確認 -2. HEREDOC形式でメッセージを渡す(改行・特殊文字の問題回避) - -```bash -git commit -m "$(cat <<'EOF' -コミットメッセージ - -Co-Authored-By: Claude Opus 4.6 -EOF -)" -``` - -## gh CLI / GitHub API の注意点 - -### パラメータ: `-f` vs `-F` - -```bash -# -f: 文字列パラメータ -gh api repos/OWNER/REPO/pulls/PR/comments -f body="テキスト" - -# -F: 非文字列パラメータ(数値、boolean、null、ファイル) -gh api repos/OWNER/REPO/pulls/PR/comments -F in_reply_to=2826074026 - -# 混在OK -gh api repos/OWNER/REPO/pulls/PR/comments -f body="返信テキスト" -F in_reply_to=2826074026 -``` - -### PRレビューコメントの取得 - -```bash -# コメント一覧を取得(id, path, body の先頭を表示) -gh api repos/OWNER/REPO/pulls/PR/comments \ - --jq '.[] | {id: .id, path: .path, body: (.body | split("\n")[0][:80])}' -``` - -### PRレビューコメントへの返信 - -```bash -# NG: /replies エンドポイントは存在しない(404になる) -gh api repos/OWNER/REPO/pulls/comments/{id}/replies -f body='...' -# => 404 Not Found - -# NG: -X POST を付けても同じ(エンドポイント自体が存在しない) -gh api -X POST repos/OWNER/REPO/pulls/comments/{id}/replies -f body='...' -# => 404 Not Found - -# OK: in_reply_to パラメータを使って新規コメントとして投稿 -gh api repos/OWNER/REPO/pulls/PR/comments \ - -f body="返信テキスト" \ - -F in_reply_to=COMMENT_ID -``` - -### レビュースレッドの Resolve(GraphQL) - -```bash -# 1. 未解決スレッドのID一覧を取得 -gh api graphql -f query=' -query { - repository(owner: "OWNER", name: "REPO") { - pullRequest(number: PR) { - reviewThreads(first: 50) { - nodes { - id - isResolved - comments(first: 1) { - nodes { path body } - } - } - } - } - } -}' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | {id, path: .comments.nodes[0].path}' - -# 2. スレッドを Resolve -gh api graphql -f query=' -mutation { - resolveReviewThread(input: {threadId: "PRRT_xxx"}) { - thread { isResolved } - } -}' -``` - -### PR の CI チェック結果 - -`gh pr checks` は1つでもfailがあると **exit code 1** で終了する。 -Claude Codeではコマンド失敗と判定されて処理が止まるため、必ず `|| true` を付ける。 - -```bash -# NG: failがあるとexit code 1で止まる -gh pr checks PR --repo OWNER/REPO - -# OK: exit codeを常に0にして出力を取得 -gh pr checks PR --repo OWNER/REPO 2>&1 || true - -# OK: 失敗のみフィルタ -gh pr checks PR --repo OWNER/REPO 2>&1 | grep -i fail || true -``` - -#### 重要: CIの完了を待ってはいけない - -- `--watch` や完了までのポーリングは **禁止**。現在のステータスを一度スナップショットするだけでよい。 -- チェックが `in_progress` / `queued` / `pending` の場合は **完了を待たず次のステップへ進む**。 -- 対応対象は **コード修正で直せるfailのみ**。以下のような「ステータス確認系」チェックは無視する: - - `check_pr_requirements` 等、PR要件・メタ情報のみ検証するもの - - Lint/テストに非依存なラベル/タイトル/説明チェック - - 外部サービス起因で自己修復するトランジェントなfail(再実行で直るもの) -- 対応する: ビルド失敗・テスト失敗・型エラー・lint違反など、**リポジトリ内コードの修正で解消可能なもの**。 - -```bash -# 失敗ジョブのログ(エラー行のみ抽出) -gh run view RUN_ID --repo OWNER/REPO --log-failed 2>&1 \ - | grep -E '(FAIL|Error|Tests:)' | head -20 || true -``` - -### 自分のPRは Approve できない - -``` -# GitHub の制約: 自分で作成した PR に APPROVE レビューは不可 -# => "Can not approve your own pull request" -# 対策: event を "COMMENT" に変更して送信 -``` - -### PR作成時の body は HEREDOC - -```bash -# NG: \n がリテラルで混入する可能性 -gh pr create --title "タイトル" --body "行1\n行2" - -# OK: HEREDOC形式 -gh pr create --title "タイトル" --body "$(cat <<'EOF' -## Summary -- 変更内容 - -## Test plan -- [ ] テスト項目 -EOF -)" -``` - -## AWS CLI の注意点 - -### CloudWatch ログストリーム名の [$LATEST] - -```bash -# NG: --query で [$LATEST] を含む文字列がパースエラー -aws logs get-log-events --query 'events[*].message' --output text - -# OK: --output json にして python でパース -aws logs get-log-events --output json | python3 -c " -import sys,json -data = json.loads(sys.stdin.read()) -for e in data['events']: - print(e['message'].strip()) -" -``` - -## エラー事例集 - -| エラーメッセージ | 原因 | 対策 | -|----------------|------|------| -| `fatal: pathspec '...' did not match any files` | CWD とパスの不一致 | `pwd` 確認後、CWD相対パスで指定 | -| `404 Not Found` (gh api replies) | `/comments/{id}/replies` は存在しない | `in_reply_to` パラメータで投稿 | -| `422 Unprocessable` (gh api) | `-f` で数値を渡した | 数値は `-F` を使う | -| `Can not approve your own pull request` | 自己 Approve 不可 | `COMMENT` イベントに変更 | -| `gh pr checks` が exit code 1 | 1つでもfailがあると非0終了 | `gh pr checks ... 2>&1 \|\| true` | -| `Unknown options: , , ,` (aws cli) | `[$LATEST]` のシェルエスケープ | `--output json` + python パース | - -## 詳細ガイド - -| ファイル | 内容 | 参照タイミング | -|---------|------|--------------| -| `01-common-errors.md` | 詳細なエラー事例と再現手順 | エラー発生時 | diff --git a/plugins/ndf/skills-codex/implementation-plan/SKILL.md b/plugins/ndf/skills-codex/implementation-plan/SKILL.md deleted file mode 100644 index 0e0a130..0000000 --- a/plugins/ndf/skills-codex/implementation-plan/SKILL.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -name: implementation-plan -description: "Create or update implementation plan files." -when_to_use: "実装開始時 / PR作成時に実装プランの作成・更新が必要なとき。複数ファイル変更・新機能追加・DBマイグレーションを含む変更で自動参照。Triggers: '実装プラン', '実装を開始', 'PR作成', 'implementation plan', 'plan first', '設計書を作成', 'issues/に追加'" ---- - -# 実装プランガイド - -## 基本方針 - -実装の開始時およびPR作成時に、`issues/` 配下に実装プランファイルが存在するか確認し、なければ作成する。プランを残すことで後任エンジニアや将来の自分が変更意図を追跡できる。 - -## 実装プランが必要なケース - -以下のいずれかに該当する場合は作成する: - -- 複数ファイルにまたがる変更 -- 新規機能の追加 -- 既存ロジックの大幅な変更 -- DBマイグレーションを伴う変更 -- 複数のタスクに分解できる作業 - -## 実装プランが不要なケース - -以下のような軽微な変更では不要: - -- typo修正、文言変更 -- 設定値の変更のみ -- 1ファイルで完結する軽微な修正 -- フォーマッター適用のみ -- ドキュメントのみの更新 - -判断に迷う場合はユーザーに確認する。 - -## ファイル配置・命名 - -- パス: `issues/` -- ファイル名に日本語は含めないこと(Git/CI/検索ツール互換性のため) -- タスクIDがある場合: `issues/TASK-1234_concise-description.md` -- タスクIDがない場合: `issues/{feature-name}.md` - -## PR作成時のプランファイル生成 - -PR作成時に `issues/` にプランファイルが存在しない場合、以下の情報源からプランファイルを生成する: - -1. **会話履歴** - それまでのやりとりから要件・背景・方針を抽出 -2. **git log** - コミット履歴からタスクの流れと変更概要を把握 -3. **git diff** - 実際の変更内容から修正対象ファイルと変更内容を特定 - -これらを組み合わせて、下記フォーマットに沿ったプランファイルを作成してからPRを作成する。 - -## プランのフォーマット - -```markdown -# {タスクID}: {機能名/修正内容} - -## 関連リンク -(Issue/チケット/設計ドキュメントがあれば記載) - -## 概要 -- 何を実装・修正するのか - -## 問題・背景 -- なぜこの変更が必要なのか(該当する場合) - -## 修正対象 -- 変更対象のファイルパス一覧 - -## タスク分解 - -### Task 1: {タスク名} -- **対象ファイル:** 変更対象のファイルパス -- **変更内容:** 具体的な変更内容 - -### Task 2: {タスク名} -- **対象ファイル:** 変更対象のファイルパス -- **変更内容:** 具体的な変更内容 - -## 影響範囲 -- 変更による影響を受ける機能やファイル - -## テスト計画 -- [ ] {実装した機能が正しく動作することの確認} -- [ ] {既存機能にリグレッションがないことの確認} -``` - -## ワークフロー - -1. 実装の依頼を受けたら、まずプランが必要か判断する -2. 必要な場合は `issues/` にプランファイルを作成してから実装を開始する -3. PR作成時にプランファイルが存在しない場合、必要であれば会話履歴・git log・git diffからプランファイルを生成してからPRを作成する - -## プランと PR Body の関係 - -- プランファイル = 「なぜ」「どう分解するか」を残す永続的な記録 -- PR body = 「何をやったか」「どうテストするか」のレビュー用サマリ - -同じ内容をコピーせず、PR bodyでは「詳細は `issues/xxx.md` 参照」と誘導してもよい。 diff --git a/plugins/ndf/skills-codex/investigation-rules/SKILL.md b/plugins/ndf/skills-codex/investigation-rules/SKILL.md deleted file mode 100644 index a4757a8..0000000 --- a/plugins/ndf/skills-codex/investigation-rules/SKILL.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -name: investigation-rules -description: "Write evidence-backed investigation and debug reports." -when_to_use: "調査・デバッグ・不具合レポートを作成するとき。「ない」「該当なし」等の否定的結論を出すときは必ず参照。Triggers: '調査', 'デバッグ', '不具合レポート', '原因調査', 'investigation', 'root cause', 'カラムがない', '該当コードがない', 'データがない'" ---- - -# 調査レポート作成ルール - -不具合調査・データ調査・仕様調査でレポートを作成する際のルール。コード読解だけに頼らず、必ず実行結果・出力・実データで裏取りする。 - -## 否定的結論にはエビデンス必須 - -「カラムがない」「データがない」「関数が呼ばれていない」「該当コードがない」等の **否定的な結論** を書く場合、**必ず実行結果をエビデンスとして添付すること**。 - -### なぜこのルールが必要か - -AIは「もっともらしいが間違った推論」をしがちで、コード読解だけで「ない」と断定して誤判断を招きやすい。事例として、外部テーブルの一部カラムだけを見て「該当カラムなし」と結論づけたが、実際には別名のカラムにデータが存在していた、という判断ミスが典型。 - -### 具体的な裏取り方法 - -| 主張の種類 | 必須エビデンス | -|-----------|--------------| -| DB: カラムが存在しない | `SHOW COLUMNS FROM table_name` / `DESCRIBE` の結果 | -| DB: データが存在しない | `SELECT COUNT(*) FROM table WHERE ...` の結果 | -| DB: テーブルが存在しない | `SHOW TABLES LIKE '%keyword%'` の結果 | -| コード: 関数/シンボルが存在しない | `grep -rn 'name' .` / LSP検索 / Serena `find_symbol` の結果 | -| コード: 呼び出し箇所がない | `find_referencing_symbols` / `grep` の結果 | -| 設定: 値が存在しない | 設定ファイルのdiff / `env` / `config` コマンド出力 | -| ログ: エラーが出ていない | `grep` / 検索ツールのクエリと結果期間 | - -### レポートへの記載例 - -```markdown -### 残課題 - -| 課題 | 概要 | エビデンス | 優先度 | -|------|------|-----------|--------| -| 外部API の retry 未実装 | Xクライアントで retry ハンドリングが無い | `grep -rn "retry\|Retry" src/client/x/` → 0件 | 中 | -| status=deleted の件数 | 論理削除レコードが残存 | `SELECT COUNT(*) FROM ... WHERE status='deleted'` → 2,341件 | 低 | -``` - -### やってはいけないこと - -- コードを読んだだけで「このカラムは存在しない」と断定する -- 1つのテーブル/ファイルだけ見て「データに問題はない」と結論づける -- 外部テーブルの一部のカラムだけ見て「他にはない」と判断する(全カラムを確認する) -- エビデンスなしで残課題の優先度を「低」にする(誤判断の典型) - -## 外部データ調査の原則 - -外部API/外部テーブル/サードパーティデータソースを調査する際は、**全体構造を必ず確認する**。 - -```sql --- まず全体像を把握する -SHOW COLUMNS FROM external_source_table; - --- 次に対象カラムのデータ分布を確認する -SELECT column_name, COUNT(*) FROM table GROUP BY column_name; -``` - -外部データは外部システム由来でカラム名・値域が予測しづらいため、コードから逆引きするだけでは見落とす。 - -## ハルシネーション防止チェックリスト - -推論で埋めず、必ず以下を実行して裏取りする: - -| チェック項目 | 方法 | -|------------|------| -| カラム/フィールドが存在するか | `SHOW COLUMNS` / スキーマ定義ファイルを開く | -| データが存在するか | `SELECT COUNT(*) WHERE ...` / サンプルレコード取得 | -| 型が一致するか | DB定義とアプリコード両方を確認(Eloquent `$casts`、dataclass型等) | -| FK/制約が存在するか | マイグレーション履歴を追跡(追加→削除→再追加の変遷を確認) | -| 論理削除ポリシーは何か | `SoftDeletes` / `deleted_at` の有無を確認 | -| 環境差異がないか | dev/staging/prod で同じクエリを実行して比較 | - -## 調査結果の書き方テンプレート - -```markdown -## 症状 -何がどう間違っているか(定量的に、エビデンス付きで) - -## 調査経緯 -1. 仮説1: xxx → クエリ/コマンドで確認 → 否定/肯定 -2. 仮説2: yyy → ... - -## 根本原因 -コードレベルでどこが問題か(ファイル名:行番号で特定) - -## エビデンス -``` -SQL/コマンド実行結果をそのまま貼る -``` - -## 修正方針 -どのフェーズで何を直すか(多層防御の観点) - -## 検証手順 -修正後にどう確認するか(回帰テスト含む) -``` - -SQLクエリ結果・コマンド出力をそのまま貼り、「コードを読んだ推測」と「実行して確認した事実」を明確に区別する。 - -## 関連スキル - -- `/ndf:problem-solving` — 根本原因分析と多層防御の原則 diff --git a/plugins/ndf/skills-codex/issue-plan-strategy/SKILL.md b/plugins/ndf/skills-codex/issue-plan-strategy/SKILL.md deleted file mode 100644 index 4275a37..0000000 --- a/plugins/ndf/skills-codex/issue-plan-strategy/SKILL.md +++ /dev/null @@ -1,335 +0,0 @@ ---- -name: issue-plan-strategy -description: "Turn issues into plans and implementation workflows." -when_to_use: "issue → plan 作成 / 既存 plan の実装 (実行) を依頼されたとき。複数 PR に分割される設計や、release branch + 個別 PR + worktree 運用が必要なときに参照する。Triggers: 'issueのplanを作って', 'PLANxxの設計', '設計書を起こして', 'このplanを実装して', 'PLANxxを実装', 'planを実行', 'release branch 作って実装開始', 'multi-PR で進めて'" -argument-hint: "[issue-path-or-url] (例: issues/i16.md, https://github.com/org/repo/issues/123)" -allowed-tools: - - Bash - - Read - - Write - - Edit - - Glob - - Grep ---- - -# issue → plan → multi-PR ワークフロー - -1 つの issue から plan を作る際、推奨される PR が複数に分かれることは日常的に発生する。本 skill はその際の **release ブランチ + 個別 PR ブランチ + Draft PR 先行作成 + git worktree 並行開発 + レビュー運用** の標準フローを規定する。 - -本 skill は **plan の作成フェーズと plan の実行(実装)フェーズの両方** をカバーする。同じワークフローが「設計を起こす段階」と「設計に従って実装する段階」を貫通することで、作成者と実装者(あるいは将来の自分)が同じ手順を共有できる。 - -## 発動条件 - -| トリガ | 例 | 入る Step | -|---|---|---| -| スラッシュコマンド (引数あり) | `/ndf:issue-plan-strategy issues/foo.md`、`/ndf:issue-plan-strategy https://github.com/org/repo/issues/123` | Step 0 から | -| スラッシュコマンド (引数なし) | `/ndf:issue-plan-strategy` (現在ブランチで作業中の issue/plan を解析) | Step 0 から | -| 自動発動 (作成系) | 「この issue の plan を作って」「設計書を起こして」「PLAN42 の設計を起こして」 | Step 1〜2 | -| 自動発動 (実行系) | 「この plan を実装して」「PLAN42 を実行して」「multi-PR で進めて」「release branch を切って実装開始」 | Step 0 → 既存 plan を読み → Step 3 以降 | - -引数で渡された issue / plan は **ファイルパス / URL / 番号** いずれでも受け付ける: - -- ファイルパス (`issues/PLANxx_*.md`): 直接 Read -- GitHub Issue URL / `#番号`: `gh issue view --json title,body,labels` で取得 -- それ以外の文字列: そのまま issue 本文として扱う - -## Step 0: 作成フェーズか実行フェーズか判定 - -最初に **既に plan ファイルが存在するか** で判定する。skill 内で `Glob` を使うのが第一選択 (例: `Glob('issues/*PLAN42*')`)。shell で確認する場合は: - -```bash -# issues/ 配下に該当 plan があるか (PLAN42 / feature-name 部分は実値に置換) -find issues/ -maxdepth 1 -iname '*PLAN42*' -o -iname '*feature-name*' -``` - -| 状況 | 進むフェーズ | -|---|---| -| plan ファイルがない / issue しかない | **作成フェーズ** (Step 1〜2 へ) | -| plan ファイルがあり、release branch がない | **実行フェーズ・初期化** (Step 3 へ) | -| release branch も Draft PR も既にある | **実行フェーズ・継続** (Step 5 以降。worktree / 並行開発 / レビュー / merge を進める) | - -実行フェーズで入った場合、既存 plan の **「PR 分割計画」セクション**を必ず Read してから Step 3 以降の自動化判断に使う。 - -## 全体フロー - -``` - ┌─ 作成フェーズ ──────────────────────────────────────┐ -issue 取得 ─┤ │ - │ plan 作成 (必要なら plan モード) ─ 単一PR? ─ YES ─▶ implementation-plan + /ndf:pr で完了 - │ │ - │ NO - └──────────────────────────────────────────┼──────────┘ - ▼ - ┌─ 実行フェーズ ──────────────────────────────────────┐ -既存 plan ─▶│ Step 3: release branch 作成 + Draft release PR │ - │ Step 4: 個別 PR ブランチ作成 + 各 Draft PR (release base) - │ Step 5: git worktree で並行開発 (依存関係を考慮) │ - │ Step 6: 個別 PR ごとに /ndf:review or /ndf:cross-review - │ → /ndf:fix → merge into release │ - │ Step 7: release ブランチで結合テスト相当のレビュー │ - │ Step 8: release PR body 最終化 → Ready & merge │ - └─────────────────────────────────────────────────────┘ -``` - -QA / staging 等の検証環境向けには、個別 PR or release PR 単位で `/ndf:cherry-pick-pr` を別途実行する (Step 9)。 - -実行フェーズに途中から入った場合は、対応する Step の途中再開で構わない。各 Step の冒頭で **既に存在するブランチ / PR / worktree を `git branch -a` / `gh pr list` / `git worktree list` で確認**してから作業に入る。 - -## Step 1: issue 取得と plan 作成 (作成フェーズ専用) - -> 実行フェーズで入った場合はこの Step をスキップし、既存 plan を Read して Step 3 へ進む。 - -1. 引数を解釈して issue 本文を取得する -2. `issues/` 配下に plan ファイルが既に存在するか `Glob` で確認する -3. なければ `/ndf:implementation-plan` の **プランフォーマット**に従って plan ファイルを作成する - - ファイル名は英数 (例: `issues/PLAN42_multi-pr-refactor.md`) - - 内容に「複数 PR に分割する根拠」「PR 単位と依存関係」を必ず含める -4. 設計判断が重い場合は **Claude Code の plan モード** (ExitPlanMode を用いる読み取り専用フェーズ) に切り替えて十分検討してから実装へ進む - -plan の構造は `/ndf:implementation-plan` を参照。本 skill では multi-PR を前提に **以下のセクションを追加**する: - -```markdown -## PR 分割計画 - -| PR # | branch 名 | 概要 | 依存 | 並行可否 | -|---|---|---|---|---| -| 1 | feature/PLAN42-schema | スキーマ追加 | なし | ○ | -| 2 | feature/PLAN42-api | API 実装 | PR1 | × (PR1 merge 後) | -| 3 | feature/PLAN42-ui | UI 実装 | PR1 | ○ (mock で開始可) | - -release branch: `release/PLAN42` -base branch: `main` -``` - -## Step 2: 単一 PR で足りるか判定 - -plan を書いた結果が以下のいずれかなら **release ブランチを作らず**、`/ndf:implementation-plan` + `/ndf:pr` の通常フローに切り替える: - -- 変更ファイルが 1〜2 個で結合度が低い -- 1 PR で安全に review 可能 (差分 ~500 行以内が目安) -- 依存関係のある複数タスクが存在しない - -複数 PR が妥当な場合 (スキーマ + API + UI、機能追加 + マイグレーション、複数モジュール横断 等) のみ Step 3 に進む。 - -## Step 3: release ブランチ + Draft PR 先行作成 (実行フェーズの開始点) - -> 実行フェーズで自動発動した場合の最初の自動化対象。既に `release/` ブランチや Draft PR が存在する場合は作成をスキップし、Step 4 へ進む。 - -### release ブランチ作成 - -```bash -git fetch origin -git checkout -b release/ origin/ -git push -u origin release/ -``` - -### レビュアー視点の原則 (release PR body の大前提) - -個別 PR はセルフレビュー (`/ndf:cross-review` 等) で merge される。**人間のレビュアーが見るのは release PR だけ**であり、個別 PR の存在をレビュアーに意識させてはならない。したがって: - -- release PR の body は **self-contained 必須**: 「何のために」(背景・解決したい課題) と「何を」(release ブランチ全体としての変更内容) を、**個別 PR を一切参照せずに**理解できる粒度で書く -- 個別 PR リンクの列挙を body の本文にしない。開発中の進捗管理に使う場合は `
` 折りたたみ内の補足情報に格下げする -- `/ndf:cross-review` の light rotation と同じ原則を適用する: 現状の差分・実装を反映し、内部用語 (PLAN-ID 運用、round、rotated 等) をレビュアー向け本文に漏らさない - -### release → default の Draft PR を先行作成 - -```bash -gh pr create \ - --base \ - --head release/ \ - --draft \ - --title "release: <概要>" \ - --body "$(cat <<'EOF' -## Summary -- (背景) なぜこの変更が必要か / 解決したい課題 -- (変更内容) release ブランチ全体として何をするか -- plan: issues/_xxx.md - -## Test plan (結合観点のみ) -- [ ] 個別 PR では検出できない結合テスト項目 - -
-開発用: 個別 PR 進捗 (レビュー対象外) - -- [ ] # PR1: ... -- [ ] # PR2: ... -- [ ] # PR3: ... - -
- - -EOF -)" -``` - -Draft 作成時点では実装が進んでいないため body は plan ベースの暫定でよいが、Ready for review 前に **実装の最終形を反映した body へ最終化**する (Step 8 参照)。 - -release PR を **先に作る理由**: PR 番号が確定し、個別 PR の説明から参照できるため。 - -## Step 4: 個別 PR ブランチ + Draft PR 先行作成 - -> 既存ブランチは `git branch -a | grep "feature/-"` で確認し、未作成のものだけ作る。Draft PR の存在は `gh pr list --base release/ --state all` で確認。 - -各 PR について **同じパターンで先に Draft PR まで作る**: - -```bash -# release ブランチを base に個別ブランチを切る -git fetch origin release/ -git checkout -b feature/- origin/release/ - -# 空コミットで push して Draft PR を作る (base=release と HEAD が同一だと -# gh pr create が "No commits between ..." で失敗するため、差分ゼロのまま PR -# 作成のトリガにする目的で `--allow-empty` を使う) -git commit --allow-empty -m "chore: - Draft PR 作成" -git push -u origin feature/- - -gh pr create \ - --base release/ \ - --head feature/- \ - --draft \ - --title "feat: - <概要>" \ - --body "$(cat <<'EOF' -## Summary -- plan: issues/_xxx.md -- release PR: # -- 担当範囲: - -## Test plan -- [ ] ... - - -EOF -)" -``` - -完了後 release PR の本文を `gh pr edit` で更新し、`
` 内の開発用チェックリストに個別 PR 番号を埋める (body 本文には書かない)。 - -## Step 5: git worktree で並行開発 - -並行可能 (依存なし or mock で先行可) な PR は **git worktree** で同時に開く: - -```bash -# repo ルート (default branch のまま) で -git worktree add ../--schema feature/-schema -git worktree add ../--ui feature/-ui - -# それぞれの worktree で別ターミナル / 別エージェントを起動 -``` - -ガイドライン: - -- **依存のある PR は順次着手**する (PR1 merge → PR2 開始) -- 並行 PR 間で同じファイルを触る場合は事前にレビュー観点で分担を明確化する -- 終わった worktree は `git worktree remove ` で片付ける -- Claude Code から並行開発を指示する場合、Agent tool の `isolation: "worktree"` も検討する - -## Step 6: 個別 PR のレビュー - -**レビューは原則個別 PR 単位**で行う: - -| 用途 | コマンド | -|---|---| -| PR 作成前のセルフレビュー | `/ndf:review-branch` | -| GitHub 上の単体レビュー | `/ndf:review ` | -| codex + gemini 両方の収束ループ | `/ndf:cross-review ` | -| 指摘の修正 | `/ndf:fix ` | - -個別 PR が APPROVE → Draft 解除 → release ブランチへ merge (squash 推奨)。 - -## Step 7: release ブランチのレビュー (結合テスト相当のみ) - -release ブランチへの merge が一通り進んだ段階で: - -- **個別 PR で見た観点を再レビューしない** -- **結合テスト相当**の観点のみレビューする: - - PR 間の API / 型 / スキーマ整合 - - 設定値の重複・矛盾 - - migration の順序依存 - - E2E シナリオ (`/ndf:playwright-scenario-test` の活用) -- ここで個別 PR 範囲のバグが見つかった場合は、**release PR にコメントせず**、該当の個別 PR (既に merge 済みなら新しい修正 PR を release 配下に作成) 側に指摘を書き込み、修正ループを回す -- release PR には integration 観点の指摘のみ残す - -## Step 8: release PR body の最終化と release → default の merge - -### body の最終化 (Ready for review の前に必須) - -個別 PR が全て merge されたら、**Draft 解除の前に** release PR の body を実装の最終形を反映した self-contained な内容へ更新する: - -```bash -# release ブランチ全体の差分を確認して body を書き直す -git fetch origin -git diff origin/...origin/release/ --stat -gh pr edit --title "..." --body "..." -``` - -最終化のチェック観点 (Step 3 のレビュアー視点の原則を満たすこと): - -- [ ] 「何のために」「何を」が個別 PR や plan ファイルを辿らずに理解できる -- [ ] 実装中の方針変更・スコープ増減が body に反映されている -- [ ] 個別 PR への参照が本文に残っていない (`
` 内の開発用情報は残してよい) -- [ ] 内部用語 (round、rotated 等) が漏れていない - -### Draft 解除と merge - -release PR が APPROVE されたら: - -```bash -# Draft 解除 -gh pr ready -# merge: 個別 PR が既に squash 済みで release ブランチに並んでいるため、 -# main 側でも個別 PR 単位の commit を追跡できる `--merge` (merge commit 保持) -# が既定として推奨。プロジェクト規約で線形履歴必須なら `--rebase`、 -# それ以外で commit 数を 1 本にしたい場合のみ `--squash`。 -gh pr merge --merge --delete-branch -``` - -merge 後は plan ファイル末尾に「完了サマリ」(マージ済み PR 番号 / 検証結果) を追記してクローズ化する。 - -## Step 9: 検証環境 (qa/staging 等) への適用 - -QA / staging 検証は **個別 PR 単位** or **release ブランチ単位** のどちらでも OK。 -`/ndf:cherry-pick-pr` は Claude Code 内の slash command なので、shell ではなく -Claude Code セッション上で実行する点に注意。 - -個別 PR 単位で qa に反映する場合: - -```text -# (Claude Code 内で実行する slash command) -/ndf:cherry-pick-pr qa/staging -``` - -release ブランチごと qa に反映する場合 (まとまった検証が必要な場合): - -```bash -# 1. shell で release ブランチに切り替え -git checkout release/ -``` - -```text -# 2. (Claude Code 内で実行する slash command) -/ndf:cherry-pick-pr qa/staging -``` - -詳細は `/ndf:cherry-pick-pr` と `/ndf:branch-fix-strategy` を参照。`feature → main` 系 PR を汚染しないため、検証ブランチ向けは必ず短命ブランチ経由で扱う。 - -## アンチパターン - -| ❌ やってはいけないこと | 理由 | -|---|---| -| release ブランチを作らず巨大な 1 PR で出す | レビュー困難・revert 困難・並行開発不可 | -| 個別 PR の base を default にする | release で統合する意味が失われ、partial merge が default を汚染する | -| 個別 PR Draft 作成を実装後に回す | PR 番号が未確定でクロス参照や CI 待機の段取りが組めない | -| release PR で個別 PR 範囲の指摘を解決しようとする | 該当 PR が既に閉じている場合、コミット意図がずれる | -| release PR の body を個別 PR リンクの列挙だけにする | レビュアーは release PR 単体で変更を把握できず、個別 PR や plan を辿ることになる。body は self-contained 必須 (Step 3 / Step 8) | -| body 最終化せずに Ready for review にする | Draft 作成時の plan ベースの暫定 body のままだと実装の最終形と乖離する | -| 検証ブランチを feature/release に merge する | `feature → main` PR への汚染 (詳細: `/ndf:branch-fix-strategy`) | - -## 関連 skill - -- `/ndf:implementation-plan` — plan ファイルのフォーマット (本 skill が依存) -- `/ndf:branch-fix-strategy` — ブランチ汚染を避ける原則 -- `/ndf:pr` — 通常の PR 作成 / 更新 -- `/ndf:cherry-pick-pr` — 検証ブランチへの cherry-pick PR -- `/ndf:review` / `/ndf:review-branch` / `/ndf:cross-review` — レビュー -- `/ndf:fix` / `/ndf:resolve-pr-comments` — コメント対応 -- `/ndf:playwright-scenario-test` — release ブランチでの E2E 結合テスト diff --git a/plugins/ndf/skills-codex/logging-guidelines/SKILL.md b/plugins/ndf/skills-codex/logging-guidelines/SKILL.md deleted file mode 100644 index 007b969..0000000 --- a/plugins/ndf/skills-codex/logging-guidelines/SKILL.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -name: logging-guidelines -description: "Design safe and useful application logging." -when_to_use: "コードにログを追加・修正・整理するとき。Triggers: 'ログ追加', 'log追加', 'logger', 'logging', 'ログレベル', 'log level', 'デバッグログ', 'エラーログ', 'logger.info', 'logger.error', 'print文をログに'" ---- - -# ログ運用ガイドライン - -コードにログを追加・修正する際は、以下のルールに従うこと。言語/フレームワークに依存しない原則として記述している。 - -## ログレベルの選択基準 - -| レベル | 用途 | 本番出力(推奨) | -|--------|------|---------------| -| `error` | 例外発生、処理失敗 | o | -| `warning` | データ不備でスキップ、処理継続可能な異常 | o | -| `info` | バッチ開始/完了、重要なビジネスイベント | 環境による(本番off推奨) | -| `debug` | 開発向けデバッグ情報 | x | - -**推奨**: 本番は `LOG_LEVEL=warning` 以上。info/debug は開発・ステージングのみで出力する。 - -## 使用を避けるログレベル - -以下は用途が曖昧または過剰なため、明示的な運用規則がない限り使わない: - -- `notice` — error/warning/info と区別が曖昧 -- `critical`, `alert`, `emergency` — 通常のアプリには過剰。運用規則として「PagerDuty起動基準」などが定義されていない限り使わない - -## ループ内ログのルール - -### 原則: ループ内では info/warning を出力しない - -ループ内で1件ずつログを出力すると、大量データ処理時にログが爆発する。ループ後にサマリーとしてまとめて出力すること。 - -### サマリーログ化パターン(擬似コード) - -``` -# NG: ループ内で1件ずつ出力 -for item in items: - log.info("処理完了", id=item.id) - -# OK: ループ後にまとめて出力 -processed_count = 0 -for item in items: - # 処理... - processed_count += 1 -log.info("バッチ処理完了", processed_count=processed_count) -``` - -### エラー蓄積パターン - -ループ内で例外が発生し処理を継続する場合は、エラー情報を蓄積してループ後にまとめて報告する。先頭N件のみ含めることで、ログサイズ爆発を防ぐ。 - -``` -errors = [] -for item in items: - try: - process(item) - except Exception as e: - errors.append({"id": item.id, "error": str(e)}) - -if errors: - log.error( - "処理で一部失敗", - total_count=len(items), - failed_count=len(errors), - sample_errors=errors[:10], # 先頭10件のみ - ) -``` - -### ループ内 debug も必要最小限 - -ループ内での debug 出力は、他に代替手段がなく調査に不可欠な場合のみ許容。デフォルトは「ループ外で件数サマリ」を基本とする。 - -## 例外処理のルール - -1. **例外は最上位でログ出力** — エントリポイント(コマンド/コントローラー/ジョブ)で catch してログ出力 -2. **再スロー時はログ不要** — 上位で出力されるため二重出力を避ける -3. **例外を握りつぶさない** — catch後に何も報告せず続行するのは禁止 -4. **広めの例外型で捕捉** — 言語の最上位例外型(Python `Exception`、PHP `Throwable`、Java `Throwable` 等)でトップレベル catch する - -## 必須ルール - -1. **コンテキスト情報を含める** — 調査に必要なID等を構造化ログとして渡す -2. **機密情報を含めない** — パスワード、トークン、クレジットカード番号、個人特定情報は禁止 -3. **メッセージは明確に** — 何が起きたか分かる言葉で記述(プロジェクトの言語ポリシーに従う) -4. **ロガー呼び出しを統一** — プロジェクトで統一ファサード/クライアントを使う(例: Laravel は `Log::`, Python は `logging.getLogger(__name__)`) -5. **グローバル/暗黙の名前空間を使わない** — 明示的にimport/useする - -## ログとメトリクスの使い分け - -- **ログ**: 個別のイベント、エラー、コンテキスト情報(構造化ログ) -- **メトリクス**: 件数、レイテンシ、成功/失敗率の集計(Prometheus/DataDog等) -- **トレース**: リクエスト横断の実行フロー(OpenTelemetry等) - -ループ件数カウントなどは、ログではなくメトリクスに寄せるのが望ましい場合が多い。 - -## アンチパターン一覧 - -| アンチパターン | 問題 | -|--------------|------| -| `log.info("")` / 空メッセージ | 意図が伝わらない | -| `log.error(e)` のみ | スタックトレース/contextが欠ける | -| 機密情報をそのままログに入れる | 情報漏洩リスク | -| ループ内で毎回 info 出力 | ログ爆発 | -| try/except で握りつぶし、何も報告しない | 障害の気配を消す | -| 複数行の ASCII ART をログに含める | grep/集計が困難 | - -## 関連スキル - -- `/ndf:problem-solving` — ログから根本原因を特定する手順 -- `/ndf:investigation-rules` — ログをエビデンスとして扱う際の注意点 diff --git a/plugins/ndf/skills-codex/markdown-writing/01-diagram-guide.md b/plugins/ndf/skills-codex/markdown-writing/01-diagram-guide.md deleted file mode 100644 index db40f52..0000000 --- a/plugins/ndf/skills-codex/markdown-writing/01-diagram-guide.md +++ /dev/null @@ -1,144 +0,0 @@ -# 図表作成ガイド - -## mermaid 記法 - -### フローチャート - -```mermaid -graph TD - A[開始] --> B{条件判定} - B -->|Yes| C[処理A] - B -->|No| D[処理B] - C --> E[終了] - D --> E -``` - -### シーケンス図 - -```mermaid -sequenceDiagram - User->>API: リクエスト - API->>DB: クエリ - DB-->>API: 結果 - API-->>User: レスポンス -``` - -### クラス図 - -```mermaid -classDiagram - class User { - +int id - +string name - +login() - +logout() - } - class Order { - +int id - +float total - } - User "1" --> "*" Order -``` - -### ER図 - -```mermaid -erDiagram - USER ||--o{ ORDER : places - ORDER ||--|{ LINE_ITEM : contains - PRODUCT ||--o{ LINE_ITEM : "ordered in" -``` - -## plantUML 記法 - -### コンポーネント図 - -```plantuml -@startuml -package "Frontend" { - [React App] -} -package "Backend" { - [API Server] - [Database] -} -[React App] --> [API Server] -[API Server] --> [Database] -@enduml -``` - -### アクティビティ図 - -```plantuml -@startuml -start -:ユーザー入力; -if (有効?) then (yes) - :処理実行; -else (no) - :エラー表示; -endif -stop -@enduml -``` - -## ASCII 許可例(ツリーのみ) - -ディレクトリ構造はASCIIで表現可能: - -``` -project/ -├── src/ -│ ├── components/ -│ └── utils/ -├── tests/ -└── docs/ -``` - -## よくある間違い - -### 避けるべき: ASCII ARTで図を描く - -``` - ┌─────────┐ - │ User │ - └────┬────┘ - │ - ┌────▼────┐ - │ API │ - └─────────┘ -``` - -上記のような図は **mermaid** で描いてください: - -```mermaid -graph TD - User --> API -``` - -### 避けるべき: 順序prefixなしで分割 - -``` -docs/ -├── introduction.md ← NG: prefixがない -├── setup.md -└── usage.md -``` - -正しい方法: - -``` -docs/ -├── 01-introduction.md ← OK -├── 02-setup.md -└── 03-usage.md -``` - -## ベストプラクティス - -| DO | DON'T | -|----|-------| -| mermaid/plantUMLで図を描く | ASCII ARTで図を描く | -| 300行以内に収める | 1000行超の巨大ファイル | -| 順序prefixで分割 | prefixなしで分割 | -| 2桁パディング(01-, 02-) | 1桁(1-, 2-) | diff --git a/plugins/ndf/skills-codex/markdown-writing/SKILL.md b/plugins/ndf/skills-codex/markdown-writing/SKILL.md deleted file mode 100644 index 56b3583..0000000 --- a/plugins/ndf/skills-codex/markdown-writing/SKILL.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -name: markdown-writing -description: "Write Markdown docs, diagrams, and split files." -when_to_use: "Markdown 文書 / 図表を作成 / 編集するとき。Triggers: 'Markdown作成', 'ドキュメント作成', '文書作成', '図を描く', 'mermaid', 'create document', 'write docs'" -allowed-tools: - - Read - - Write - - Edit ---- - -# Markdown Writing Skill - -## 重要ルール - -### 1. 図表作成ルール - -**mermaid または plantUML を使用**(ASCII ART禁止、ツリー除く) - -```mermaid -graph TD - A[開始] --> B{条件判定} - B -->|Yes| C[処理A] - B -->|No| D[処理B] -``` - -### 2. 文書の長さと分割ルール - -| ページ数 | 対応 | -|---------|-----| -| ~300行 | そのまま | -| 301~600行 | 2ファイルに分割 | -| 600行以上 | セクションごとに分割 | - -**分割時のファイル名**: 順序prefix(01-, 02-, ...)+ ケバブケース - -``` -docs/feature-guide/ -├── 01-introduction.md -├── 02-installation.md -└── 03-usage.md -``` - -## チェックリスト - -- [ ] 図表はmermaid/plantUML使用(ツリー除く) -- [ ] ファイル長は300行以内(超える場合は分割) -- [ ] 分割時は順序prefix使用(01-, 02-, ...) - -## 詳細ガイド - -| ファイル | 内容 | -|---------|------| -| `01-diagram-guide.md` | mermaid/plantUML記法、よくある間違い | - -## 関連リソース - -- [Mermaid公式ドキュメント](https://mermaid.js.org/) -- [PlantUML公式ドキュメント](https://plantuml.com/) diff --git a/plugins/ndf/skills-codex/merged/SKILL.md b/plugins/ndf/skills-codex/merged/SKILL.md deleted file mode 100644 index 06af6f2..0000000 --- a/plugins/ndf/skills-codex/merged/SKILL.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: merged -description: "Clean up after a PR is merged." -argument-hint: "[PR番号]" -disable-model-invocation: true -allowed-tools: - - Bash - - Read ---- - -# マージ後クリーンアップコマンド - -PRマージ後のクリーンアップを実行。 - -## 手順 - -0. **事前確認**: github mcpで引数の(引数が無ければ自身が作成した最新の)PRがmainにmergeされていることを確認。mergeされていなければ終了 -1. **事前確認**: `git status`→変更あればstash -2. **main更新**: `git checkout main`→`git pull` -3. **worktreeクリーンアップ**: `git worktree list` で当該PR番号に対応する worktree (`pr`) を探し、あれば `git worktree remove ` で削除(worktree 内の `.cross_review/` も一緒に消える) -4. **ブランチ削除**: `git branch -d ` → stash復元 - -**注意**: 冪等性保証・エラー時中断・削除済み無視 - -## 作業完了報告(必須) - -- 実行サマリー(PRタイトル、マージコミット、削除したブランチ、現在のブランチ) -- mainブランチの状態 -- PR URL diff --git a/plugins/ndf/skills-codex/ndf-policies/SKILL.md b/plugins/ndf/skills-codex/ndf-policies/SKILL.md deleted file mode 100644 index eb25c33..0000000 --- a/plugins/ndf/skills-codex/ndf-policies/SKILL.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: ndf-policies -description: "Apply core NDF project policies." -user-invocable: false ---- - -# NDFポリシー - -このスキルはNDFプラグインの基本ポリシーを定義します。 -descriptionフィールドが常時コンテキストに注入されるため、本文の参照は不要です。 diff --git a/plugins/ndf/skills-codex/plan-to-spec/SKILL.md b/plugins/ndf/skills-codex/plan-to-spec/SKILL.md deleted file mode 100644 index eaea6f9..0000000 --- a/plugins/ndf/skills-codex/plan-to-spec/SKILL.md +++ /dev/null @@ -1,182 +0,0 @@ ---- -name: plan-to-spec -description: "Finalize an implemented plan into a permanent specification document. Use after implementation is complete and an issues/ plan, PLAN file, design note, or implementation plan should become the final as-is specification under docs/ or another authoritative specification location. Triggers: 'planを仕様書にして', '確定仕様書に移動', '実装完了後にplanを整理', 'planをdocsへ移動', '仕様書としてリライト', 'plan-to-spec', 'finalize plan spec'." -allowed-tools: - - Bash - - Read - - Write - - Edit - - Glob - - Grep ---- - -# Plan to Spec - -実装完了後の plan を、開発履歴ではなく **現在のコードと一致する確定仕様書**として保存する。plan は作業中の意思決定記録であり、完了後は読者が実装経緯を追わなくても仕様を理解できる形に変換する。 - -## 基本方針 - -- plan の内容をそのまま移動せず、最終実装の as-is 仕様として書き直す -- 開発中の履歴、TODO、PR 分割、作業チェックリスト、途中変更、未採用案は削除する -- 仕様書の置き場は既存 docs 構造に合わせ、なければ `docs/specifications/` を作成する -- 仕様書はコードと照合し、実装と矛盾する記述を残さない -- 完了報告は本 skill のテンプレートに従う - -## 入力 - -`$ARGUMENTS` - -引数は plan ファイルパス、issue 番号、PR 番号、または関連キーワードを受け付ける。引数がない場合は、現在ブランチの差分、`issues/`、`docs/`、`git log` から直近の plan を特定する。 - -## 手順 - -### 1. 対象 plan と実装範囲を特定する - -1. 引数がファイルパスならその plan を読む -2. 引数が PR / issue 番号なら `gh pr view` / `gh issue view` とローカルファイル検索で関連 plan を探す -3. 引数がない場合は以下を確認する: - - `git status --short` - - `git branch --show-current` - - `git log --oneline --decorate -20` - - `find issues docs -maxdepth 3 -type f \( -iname '*plan*' -o -iname '*PLAN*' \)` -4. plan が複数候補ある場合は、現在ブランチ・PR・変更ファイルと最も関連が強いものを選ぶ。不明ならユーザーに確認する -5. 実装範囲を特定する: - - PR がある場合: `gh pr diff` / `gh pr view --json files,title,body` - - ローカル変更の場合: `git diff --stat` / `git diff` - - merge 済みの場合: 関連コミット範囲の `git show` / `git diff` - -### 2. 仕様書の配置先を決める - -既存 docs の分類に合わせて配置する。優先順位: - -1. 同種の仕様書がある既存ディレクトリ (`docs/specifications/`, `docs/specs/`, `docs/features/`, `docs/architecture/`, `docs/modules/` など) -2. 対象機能に対応する既存 docs 配下 -3. 適切な場所がなければ `docs/specifications/` を作成 - -ファイル名は英数字・ハイフン中心にし、内容が分かる名前にする。例: - -```text -docs/specifications/auth-session-management.md -docs/features/review-workflow.md -docs/architecture/plugin-skill-loading.md -``` - -移動は履歴が追えるように、可能なら `git mv ` を使う。plan を残す必要がある運用の場合は、ユーザーに確認してからコピーに切り替える。 - -### 3. 仕様書としてリライトする - -他の仕様書の章立て・表記・粒度を先に確認し、同じ体裁へ合わせる。標準章立ては以下を使う。既存 docs に明確な型がある場合はそちらを優先する。 - -```markdown -# {仕様名} - -## 概要 - -## 背景 - -## 対象範囲 - -## 仕様 - -## データ・設定 - -## 外部連携 - -## エラー処理 - -## セキュリティ - -## 運用 - -## テスト観点 - -## 関連リンク -``` - -該当しない章は削除してよい。小さな仕様では `概要`、`仕様`、`運用`、`テスト観点`、`関連リンク` 程度に圧縮する。 - -### 4. 削除・変換ルール - -削除するもの: - -- 実装タスクのチェックリスト -- PR 分割計画、worktree 運用、作業担当、レビュー進捗 -- 「これから実装する」「予定」「案」「未定」など完了前提の表現 -- 開発中に破棄された方針、調査メモ、試行錯誤 -- AI エージェント向けの作業指示 - -変換するもの: - -- 「実装する」→「提供する」「使用する」「保持する」 -- 「修正対象」→「構成」「関連ファイル」 -- 「テスト計画」→「テスト観点」または「検証方法」 -- 「背景・問題」→ 現在の仕様を理解するために必要な背景だけ残す - -### 5. リンクを再調査して修正する - -仕様書内のリンクは移動後の位置から有効になるように直す。 - -- 相対リンクは新しい配置先基準で更新する -- 存在しないファイルリンクは `find` / `rg --files` で移動先を探す -- GitHub issue / PR / 外部ドキュメントのリンクは必要最小限にする -- 開発中の一時リンク、ローカル絶対パス、エージェント固有の transcript リンクは削除する - -リンク先が確認できない場合は、推測で残さず削除するか、確実な上位ドキュメントへ差し替える。 - -### 6. コードと仕様を照合する - -仕様書を書いた後、実装と一致しているかレビューする。 - -確認観点: - -- 仕様書に書いた機能・制約・設定名・ファイルパスが実コードに存在する -- 実コードに存在する重要な挙動が仕様書から抜けていない -- エラー処理、権限、環境変数、永続化、外部連携の記述が実装と一致する -- テスト観点が実装されたテストや手動確認に対応している -- 廃止された名前、過剰な略語、未承認の用語を使っていない - -用語確認では、`AGENTS.md`、`CLAUDE.md`、`docs/`、README、既存仕様書の表記を優先し、プロジェクトで authorize された名称に合わせる。略語は初出で正式名称を併記し、ローカルな作業略称は使わない。 - -### 7. 仕様書レビューを行い修正する - -セルフレビューを 1 回行い、必要に応じて修正する。レビュー観点: - -- 他の仕様書と章立て・見出し粒度・表記が揃っているか -- plan 由来の作業履歴が残っていないか -- as-is 仕様として読めるか -- コードと矛盾していないか -- リンクが移動後のパスで正しいか -- 用語がプロジェクト標準に合っているか -- 仕様書として過不足がないか - -レビュー結果で修正した内容は、完了報告に要約する。 - -### 8. 完了報告 - -完了時は以下のテンプレートで報告する。 - -```markdown -## Plan to Spec 完了報告 - -### 対象 -- 元 plan: `{元planパス}` -- 確定仕様書: `{仕様書パス}` - -### 実施内容 -- plan を `{配置先ディレクトリ}` に移動し、確定仕様書としてリライト -- 開発履歴・TODO・PR分割などの作業情報を削除 -- リンクを移動後のパス基準で修正 - -### レビュー結果 -- 体裁: `{他仕様書との整合結果}` -- コード一致: `{照合した主な実装ファイルと結果}` -- 用語: `{標準用語への修正有無}` - -### 検証 -- `{実行したコマンドや確認内容}` - -### 補足 -- `{残課題がなければ「なし」}` -``` - -残課題がある場合は、仕様書に曖昧な記述を残さず、完了報告の補足に明確に分離する。 diff --git a/plugins/ndf/skills-codex/playwright-execution/SKILL.md b/plugins/ndf/skills-codex/playwright-execution/SKILL.md deleted file mode 100644 index f99970c..0000000 --- a/plugins/ndf/skills-codex/playwright-execution/SKILL.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -name: playwright-execution -description: "Run Playwright E2E tests with evidence and metrics." -when_to_use: "E2E テストの実行 / エビデンス収集 / 動画エビデンス / accessibility チェック / Core Web Vitals 計測が必要なとき。テストスクリプト作成済みであることが前提。Triggers: 'E2E テスト実行', 'テスト実行', '動画エビデンス', 'エビデンス収集', 'テスト証跡', 'a11y テスト', 'accessibility テスト', 'axe-core', 'WCAG', 'Core Web Vitals', 'Web Vitals', 'LCP', 'CLS', 'body_check', 'overlay', '字幕', 'カーソル'" -allowed-tools: - - Read - - Bash(uv *) - - Bash(pytest *) - - Bash(npx *) - - Bash(playwright *) - - Bash(python *) ---- - -# Playwright Execution (テスト実行 + エビデンス収集) - -テストスクリプト作成済みの状態で E2E テストを実行し、エビデンスを収集する。 - -## 前提条件 - -- テストスクリプトが `tests/` に作成済みであること (`/ndf:playwright-script-creation` で作成) -- `scenario.config.yaml` が設定済みであること - -## 大原則 - -**エビデンス動画はデフォルト ON**。全テストで常に動画を取得する。 -明示的にスキップする場合のみ `--pwk-no-video` を指定する。 - -## 実行コマンド - -```bash -./scenario-test/run.sh # 全テスト (動画 ON) -./scenario-test/run.sh -k test_admin # フィルタ -./scenario-test/run.sh --pwk-overlay # 字幕 + カーソル付き動画 -./scenario-test/run.sh --pwk-no-video # 動画のみ OFF -./scenario-test/run.sh --pwk-no-evidence # 全エビデンス OFF (HAR/trace/動画) -``` - -## エビデンス種別 - -| 種別 | デフォルト | OFF フラグ | 説明 | -|---|---|---|---| -| video | **ON** | `--pwk-no-video` | 全テストの動画を取得 | -| trace | ON (retain-on-failure) | `--pwk-no-evidence` | Playwright Trace (DOM + 操作ログ) | -| HAR | ON (minimal) | `--pwk-har-mode none` | ネットワーク通信ログ | -| screenshot | ON (only-on-failure) | `--pwk-no-evidence` | 失敗時スクリーンショット | - -## overlay (赤丸カーソル + 字幕) - -`--pwk-overlay` フラグで全テストの動画にオーバーレイが適用される。 - -API 詳細・使用例は `playwright_kit/overlay.py` を参照。主要関数: `set_caption()`, `flash_click()`, `hide_cursor()`。 - -## 品質計測 - -### accessibility (axe-core) - -`@pytest.mark.page_role` marker が付いたテストで auto_roles にマッチする場合に自動実行。 -設定は `scenario.config.yaml` の `accessibility:` セクションで制御。→ 設定例は `templates/scenario.config.yaml` を参照。 - -### Core Web Vitals - -`@pytest.mark.page_role` marker + auto_roles マッチで LCP/CLS/TTFB/longest_task を自動計測。 -設定は `scenario.config.yaml` の `web_vitals:` セクションで制御。→ 設定例は `templates/scenario.config.yaml` を参照。 - -### body_check (PHP/SSR エラー検出) - -`page.on("response")` で全 HTML レスポンスを監視し、`Fatal error` 等を検出。デフォルト有効。 -`@pytest.mark.no_body_check` で個別 opt-out 可能。→ 設定例は `templates/scenario.config.yaml` の `body_check:` セクションを参照。 - -## 成果物 - -``` -reports// -├── report.md # テスト結果サマリ -├── / -│ ├── video.mp4 # テスト動画 (デフォルト ON) -│ ├── trace.zip # Playwright Trace -│ ├── request.har # ネットワーク通信ログ -│ ├── body_check.jsonl # body_check 違反詳細 -│ └── screenshot-*.png # スクリーンショット -``` - -## CLI options - -| option | 役割 | -|---|---| -| `--pwk-config ` | `scenario.config.yaml` のパス | -| `--pwk-out-dir ` | 成果物出力先 (default: `reports//`) | -| `--pwk-no-video` | 動画収集を OFF (デフォルトは ON) | -| `--pwk-no-evidence` | HAR / trace / video の収集を全て OFF | -| `--pwk-har-mode {minimal,full,none}` | HAR 録画モード (default: minimal) | -| `--pwk-overlay` | overlay (赤丸カーソル + 字幕) を ON | - -## 関連 Skill - -- `/ndf:playwright-script-creation` — テストスクリプト作成 (実行の前段) -- `/ndf:playwright-report` — Markdown レポート生成 -- `/ndf:playwright-kit-ops` — スクリプト実行 (init_project / スキャン) -- `/ndf:playwright-browser-connect` — ブラウザ接続構成 (local / CDP remote) -- `/ndf:playwright-evidence-drive` — エビデンス Google Drive 保管 -- `/ndf:playwright-scenario-test` — 全機能統括 diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/.gitignore b/plugins/ndf/skills-codex/playwright-kit-ops/.gitignore deleted file mode 100644 index f9cf1ad..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -.venv/ -__pycache__/ -*.pyc -*.egg-info/ - -# pytest 実行時に生成される NDF report 出力 (run-id 別) -reports/ -.pytest_cache/ - -# uv lock は共有可 -!uv.lock diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/SKILL.md b/plugins/ndf/skills-codex/playwright-kit-ops/SKILL.md deleted file mode 100644 index 991e993..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/SKILL.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -name: playwright-kit-ops -description: "Operate playwright_kit setup, scans, and evidence tools." -when_to_use: "playwright_kit のスクリプトを実行するとき / E2E テストプロジェクトの初期化 / page role 自動分類 / 単発 a11y・CWV スキャン / Google Drive エビデンスアップロードが必要なとき。Triggers: 'init_project', 'プロジェクト初期化', 'classify_page_role', 'run_a11y_scan', 'check_cwv', 'upload_evidence', 'record_scenario', 'playwright_kit 実行'" -allowed-tools: - - Read - - Bash(python *) - - Bash(uv *) - - Bash(pytest *) - - Bash(playwright *) - - Bash(./scripts/*) - - Bash(bash *) - - Bash(chmod *) ---- - -# playwright_kit 操作エージェント - -playwright_kit のスクリプト群を実行してテスト環境のセットアップ・テスト実行・エビデンス管理を行う。 - -## スクリプト一覧 - -| スクリプト | 用途 | カテゴリ | -|---|---|---| -| `scripts/init_project.sh` | 利用者プロジェクトに scenario-test ランタイムを埋め込む | セットアップ | -| `scripts/init_project.bat` | 同 (Windows) | セットアップ | -| `scripts/classify_page_role.py` | URL の a11y tree + パターンから page role を自動推定 | テスト計画 | -| `scripts/record_scenario.py` | Playwright codegen で操作を記録しテストコード化 | テスト計画 | -| `scripts/run_a11y_scan.py` | axe-core による単発 accessibility スキャン | 品質 | -| `scripts/check_cwv.py` | Core Web Vitals (LCP/CLS/TTFB) 単発計測 | 品質 | -| `scripts/upload_evidence.py` | エビデンスファイルを Google Drive にアップロード | レポート | -| `scripts/gdrive_upload_dir.py` | ディレクトリごと Drive にバッチアップロード | レポート | -| `scripts/upload_md_as_gdoc.py` | Markdown を Google Doc に変換・アップロード | レポート | -| `scripts/build_gdoc_with_drive_links.py` | Google Doc にエビデンスの Drive リンクを埋め込み | レポート | - -## セットアップ - -### プロジェクト初期化 - -```bash -# SKILL_DIR はこの skill のパス -./scripts/init_project.sh /path/to/your-app - -# ディレクトリ名をカスタマイズ -./scripts/init_project.sh /path/to/your-app --runtime-dir e2e - -# Windows -scripts\init_project.bat C:\path\to\your-app -``` - -→ `your-app/scenario-test/` に all-in-one ランタイムが作成され、Skill 非依存で動作する。 - -### テスト実行 - -```bash -cd /path/to/your-app -./scenario-test/run.sh # 全テスト -./scenario-test/run.sh -k test_admin # フィルタ -./scenario-test/run.sh --pwk-overlay # 字幕 + カーソル付き動画 -./scenario-test/run.sh --pwk-drive-folder= # Drive 自動アップロード -``` - -Drive 連携は optional dependency として扱う。Codex 公開セットには `google-auth` -skill を同梱しないため、Drive 系コマンドや `--pwk-drive-folder` を使う場合は -`GOOGLE_AUTH_SCRIPTS` を `google-auth/scripts` の実パスへ設定する。 - -```bash -export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf/skills/google-auth/scripts -cd scenario-test -uv sync --extra drive -``` - -## テスト計画ツール - -```bash -# page role を自動推定 -python scripts/classify_page_role.py --url https://example.com/products - -# Playwright codegen で操作を記録 -python scripts/record_scenario.py https://example.com/login -``` - -## 品質スキャンツール - -```bash -# axe-core 単発スキャン -python scripts/run_a11y_scan.py --url https://example.com - -# Core Web Vitals 単発計測 -python scripts/check_cwv.py --url https://example.com -``` - -## エビデンスアップロードツール - -```bash -# 単一ファイルを Drive にアップロード -python scripts/upload_evidence.py reports/run-001/test_login/trace.zip --kind trace - -# ディレクトリごとアップロード -python scripts/gdrive_upload_dir.py reports/run-001/ --folder-id - -# Markdown → Google Doc 変換 -python scripts/upload_md_as_gdoc.py reports/run-001/report.md - -# Google Doc にエビデンス Drive リンクを埋め込み -python scripts/build_gdoc_with_drive_links.py reports/run-001/ -``` - -## パッケージ参照 - -playwright_kit Python パッケージ本体・templates・tests はこの skill ディレクトリ内に配置されている。 - -## 関連 Skill - -- `/ndf:playwright-test-planning` — テスト計画 (方法論 + チェックリスト) -- `/ndf:playwright-script-creation` — テストスクリプト作成 -- `/ndf:playwright-execution` — テスト実行 + エビデンス収集 (video/trace/overlay/quality) -- `/ndf:playwright-browser-connect` — ブラウザ接続構成 (local / CDP remote) -- `/ndf:playwright-report` — レポート生成 -- `/ndf:playwright-scenario-test` — 全機能統括 diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/__init__.py b/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/__init__.py deleted file mode 100644 index bd275e6..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""pytest-playwright 上で動く Web E2E シナリオテストパッケージ。 - -利用方法: -- pytest plugin として ``--pwk-config=...`` で読み込む (entry-point 経由で auto-load) -- ``pwk_config`` / ``pwk_role_`` / ``pwk_evidence`` 等の fixture を test に注入 -- ``@pytest.mark.page_role(...)`` で accessibility / web vitals を autouse - -詳細は SKILL.md を参照。 -""" - -__version__ = "0.5.0" diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/accessibility.py b/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/accessibility.py deleted file mode 100644 index 588e3cd..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/accessibility.py +++ /dev/null @@ -1,90 +0,0 @@ -"""runner 内蔵の Web アクセシビリティ (axe-core) スキャンモジュール。 - -Web アクセシビリティ (旧称 a11y) は WCAG 準拠の機械検査を指し、本モジュールは -axe-core (axe-playwright-python) で WCAG 2.0/2.1/2.2 AA レベルの違反を検出する。 - -`scripts/run_a11y_scan.py` (CLI) はこのモジュールの薄いラッパで、 -runner からは `scan_page(page, ...)` を直接呼び出して `EvidenceCollectors` -の `axe_violations` に格納する。 - -page_role が `lp / list / form / dashboard / cart / checkout / settings / auth` -のとき runner が自動実行する (config.accessibility.auto_roles で上書き可能)。 -""" - -from __future__ import annotations - -from typing import Any - -from playwright.sync_api import Page - - -# WCAG 2.0/2.1/2.2 AA 準拠を最低基準として走査する。 -# WCAG 2.0 AAA は適合義務がない (一般的に過剰) ため除外。 -DEFAULT_TAGS: tuple[str, ...] = ("wcag2a", "wcag2aa", "wcag21aa", "wcag22aa") - -# page_role × a11y 自動実行のデフォルト対象。フォーム / 商取引 / 認証系は a11y 影響大。 -DEFAULT_AUTO_ROLES: frozenset[str] = frozenset({ - "lp", "list", "form", "dashboard", "cart", "checkout", "settings", "auth", -}) - - -def is_available() -> bool: - """axe-playwright-python がインストール済かを確認する (Maj-9: silent fail 対策)。""" - try: - import axe_playwright_python.sync_playwright # noqa: F401 - except ImportError: - return False - return True - - -def scan_page( - page: Page, - *, - tags: tuple[str, ...] | list[str] = DEFAULT_TAGS, -) -> list[dict[str, Any]]: - """既にロード済みの Page に対し axe-core を実行し violations の list を返す。 - - axe-playwright-python が未インストールなら空 list を返す。呼出側は事前に - `is_available()` で判定し、未インストールならスキップを明示すること。 - """ - try: - from axe_playwright_python.sync_playwright import Axe - except ImportError: - return [] - - axe = Axe() - try: - results = axe.run( - page, options={"runOnly": {"type": "tag", "values": list(tags)}}, - ) - except TypeError: - # axe-playwright-python の旧版は options 非対応 - results = axe.run(page) - - violations: list[dict[str, Any]] = [] - for v in results.response.get("violations", []): - violations.append({ - "id": v.get("id"), - "impact": v.get("impact"), - "tags": v.get("tags", []), - "help": v.get("help"), - "helpUrl": v.get("helpUrl"), - "nodes": [ - { - "html": n.get("html", "")[:200], - "target": n.get("target", []), - "failureSummary": n.get("failureSummary", "")[:300], - } - for n in v.get("nodes", []) - ], - }) - return violations - - -def should_auto_scan( - page_roles: list[str], - *, - auto_roles: frozenset[str] = DEFAULT_AUTO_ROLES, -) -> bool: - """testcase の page_role に基づき axe-core を自動実行すべきか判定する。""" - return any(r in auto_roles for r in page_roles) diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/body_check.py b/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/body_check.py deleted file mode 100644 index 37a8c4c..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/body_check.py +++ /dev/null @@ -1,118 +0,0 @@ -"""ページ本文の文字列マッチ検出 (PHP / SSR エラー検出, v0.4.0)。 - -JavaScript ランタイム由来の console.error / pageerror では拾えない、 -サーバ側で HTML 本文に直接出力された "Fatal error" / "Warning:" 等の -エラー文字列を、Playwright の ``page.on("response", ...)`` で拾った -HTML 本文に対して substring match で検出する純粋関数群。 - -旧 v0.2.x の自前 YAML runner にあった ``body_check`` 機能の再実装で、 -PHP プロジェクトのフロント漏れ ``Fatal error`` / ``STRICT:`` 等を -テスト失敗として捕捉する。 -""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any, Iterable - - -@dataclass(frozen=True) -class BodyViolation: - """1 件の body_check ヒット。""" - - url: str - category: str # "fatal" / "warning" / "not_found" - pattern: str - snippet: str - - def to_dict(self) -> dict[str, Any]: - return { - "url": self.url, - "category": self.category, - "pattern": self.pattern, - "snippet": self.snippet, - } - - -def _snippet(body: str, pattern: str, *, around: int = 60) -> str: - """body 中の pattern 周辺 ``around`` 文字を取り出して表示用に整形する。""" - idx = body.find(pattern) - if idx < 0: - return pattern - start = max(0, idx - around) - end = min(len(body), idx + len(pattern) + around) - snippet = body[start:end].replace("\n", " ").replace("\r", " ").replace("\t", " ") - prefix = "..." if start > 0 else "" - suffix = "..." if end < len(body) else "" - return prefix + snippet + suffix - - -def scan_body( - body: str, - url: str, - *, - fatal_patterns: Iterable[str] = (), - warning_patterns: Iterable[str] = (), - warning_head_chars: int = 300, - not_found_patterns: Iterable[str] = (), -) -> list[BodyViolation]: - """``body`` に該当パターンが出現すれば ``BodyViolation`` のリストで返す。 - - - ``fatal_patterns`` / ``not_found_patterns`` は body 全体を substring で走査 - - ``warning_patterns`` は ``body[:warning_head_chars]`` (= 先頭 N 文字 / - code points) のみを走査。本文中の説明文や入力例の "Notice:" 等を許容する - ための head 検索。bytes ではなく code points で切るのは、日本語ページで - 300 bytes ≒ 100 字相当となり実用にならないため。 - - パターン文字列は **substring 比較** (正規表現ではない)。空文字列は無視する。 - """ - violations: list[BodyViolation] = [] - - if not body: - return violations - - head_size = max(0, int(warning_head_chars)) - head = body[:head_size] if head_size else "" - - for pat in fatal_patterns: - if pat and pat in body: - violations.append( - BodyViolation( - url=url, - category="fatal", - pattern=pat, - snippet=_snippet(body, pat), - ) - ) - - if head: - for pat in warning_patterns: - if pat and pat in head: - violations.append( - BodyViolation( - url=url, - category="warning", - pattern=pat, - snippet=_snippet(head, pat), - ) - ) - - for pat in not_found_patterns: - if pat and pat in body: - violations.append( - BodyViolation( - url=url, - category="not_found", - pattern=pat, - snippet=_snippet(body, pat), - ) - ) - - return violations - - -def is_html_response(content_type: str | None) -> bool: - """``Content-Type`` ヘッダから HTML レスポンスか判定する。""" - if not content_type: - return False - return "html" in content_type.lower() diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/config.py b/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/config.py deleted file mode 100644 index 18436b4..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/config.py +++ /dev/null @@ -1,461 +0,0 @@ -"""共通設定 (config.yaml) のロードとデータクラス。 - -テストケース YAML ではなく、対象環境・ロール別ログイン・Playwright/Runner 設定、 -およびページ検査・スラッグ正規化・レポート生成のプロジェクト固有パラメータを保持する。 -""" - -from __future__ import annotations - -import os -import re -from dataclasses import dataclass, field -from pathlib import Path -from typing import Any, Literal - -import yaml - - -# --------------------------------------------------------------------------- -# 環境変数展開 (Codex Major 4) -# --------------------------------------------------------------------------- - -_ENV_RE = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}") - - -def _expand_env_in_str(s: str) -> str: - """文字列中の ${VAR} / ${VAR:-default} を環境変数で展開する。""" - def repl(m: re.Match) -> str: - name, default = m.group(1), m.group(2) - val = os.environ.get(name) - if val is None: - if default is None: - raise ValueError( - f"環境変数 ${{{name}}} が未定義です " - "(default 指定 ${VAR:-default} または env を設定してください)" - ) - return default - return val - return _ENV_RE.sub(repl, s) - - -def _expand_env(value: Any) -> Any: - """dict / list / str を再帰的に走査して ${VAR} を展開する。""" - if isinstance(value, str): - return _expand_env_in_str(value) - if isinstance(value, list): - return [_expand_env(v) for v in value] - if isinstance(value, dict): - return {k: _expand_env(v) for k, v in value.items()} - return value - - -# --- ブラウザ接続 --------------------------------------------------- - -BrowserMode = Literal["local", "cdp-remote"] -BROWSER_MODES: tuple[BrowserMode, ...] = ("local", "cdp-remote") - - -@dataclass -class BrowserConfig: - """ブラウザ接続設定。 - - cdp_endpoint が空文字列や空白のみの場合はデフォルト値 - ``http://localhost:9222`` にフォールバックする。 - """ - - mode: BrowserMode = "local" - cdp_endpoint: str = "http://localhost:9222" - - @classmethod - def from_raw(cls, raw: dict[str, Any]) -> "BrowserConfig": - base = cls() - mode_raw = str(raw.get("mode") or base.mode).lower() - if mode_raw not in BROWSER_MODES: - raise ValueError( - f"browser.mode は {BROWSER_MODES} のいずれかを指定してください " - f"(指定値: {mode_raw!r})" - ) - mode: BrowserMode = mode_raw # type: ignore[assignment] - # cdp_endpoint: 空文字列・空白のみの場合はデフォルト値にフォールバック - cdp_raw = raw.get("cdp_endpoint") - cdp_endpoint = str(cdp_raw).strip() if cdp_raw else "" - if not cdp_endpoint: - cdp_endpoint = base.cdp_endpoint - return cls( - mode=mode, - cdp_endpoint=cdp_endpoint, - ) - - -# --- 接続/認証 ------------------------------------------------------- - -@dataclass -class BasicAuth: - user: str - password: str - - -@dataclass -class Login: - path: str - requires_basic_auth: bool - fields: dict[str, str] - fail_if_url_contains: str - # ログイン送信ボタンを特定するためのプロジェクト固有セレクタ (CSS / role / text)。 - # auth fixture の _submit_login_form が「これ → role/type=submit フォールバック - # → Password で Enter」の順で試す。空のままでも汎用フォールバックで通常はログインできる。 - submit_selectors: list[str] = field(default_factory=list) - - -@dataclass -class Role: - id: str - label: str - login: Login - - -# --- レポート設定 --------------------------------------------------- - -@dataclass -class ReportConfig: - title: str = "シナリオ E2E テスト 実施報告書" - test_plan_link: str = "./test-plan.md" - phase_labels: dict[int, str] = field(default_factory=dict) - - -# --- Playwright / Runner ------------------------------------------- - -# Playwright ``record_har_mode`` に直接渡す値。"minimal" は request/response の -# メタデータのみ記録し、Basic 認証 + redirect が連続するページで navigation を -# abort させる race を回避する (Issue #62)。"full" は body も含めた完全な HAR、 -# "none" は HAR を出力しない (= ``record_har_path`` を inject しない)。 -HarMode = Literal["minimal", "full", "none"] -HAR_MODES: tuple[HarMode, ...] = ("minimal", "full", "none") - - -@dataclass -class PlaywrightConfig: - headless: bool = True - viewport_width: int = 1280 - viewport_height: int = 720 - slow_mo_ms: int = 0 - video_width: int = 1280 - video_height: int = 720 - navigation_timeout_ms: int = 30000 - # 各ステップ遷移後の表示維持時間 (動画でじっくり見せるため) - step_delay_ms: int = 1800 - # 動画にカーソル+字幕オーバーレイを焼き込む (true 推奨) - enable_overlay: bool = True - # Playwright Trace (trace.zip) を出力する。クリック箇所のハイライト・ - # DOM スナップショット・コンソール・ネットワークなどを `playwright show-trace` - # で対話的に確認できる。生成物が大きく (数MB〜) なるので必要時のみ。 - enable_trace: bool = True - # 録画後の動画フォーマット: "webm" (Playwright 既定) | "mp4" (H.264 変換) - # mp4 は Google Drive プレビュアで再生互換性が高い。 - video_format: str = "mp4" - # HAR 録画モード (Issue #62)。Playwright >= 1.30 で導入された - # ``record_har_mode`` に対応する。 - # - "minimal" (default): メタデータのみ記録。Basic 認証 + redirect が混在 - # するページで ``record_har_path`` 起因の ERR_ABORTED race を回避する。 - # - "full": Playwright 既定の full HAR (body + content)。 - # - "none": HAR を一切出力しない (= ``record_har_path`` を inject しない)。 - har_mode: HarMode = "minimal" - - @classmethod - def from_raw(cls, raw: dict[str, Any]) -> "PlaywrightConfig": - # dataclass の default を真実の源 (single source of truth) とする。 - # fallback 値を base = cls() から参照することで、dataclass default と - # from_raw() の fallback が乖離するバグを防ぐ (Codex Minor 6)。 - base = cls() - viewport = raw.get("viewport") or {} - video_size = raw.get("video_size") or {} - har_mode_raw = str(raw.get("har_mode", base.har_mode)).lower() - if har_mode_raw not in HAR_MODES: - raise ValueError( - f"playwright.har_mode は {HAR_MODES} のいずれかを指定してください " - f"(指定値: {har_mode_raw!r})" - ) - har_mode: HarMode = har_mode_raw # type: ignore[assignment] - return cls( - headless=bool(raw.get("headless", base.headless)), - viewport_width=int(viewport.get("width", base.viewport_width)), - viewport_height=int(viewport.get("height", base.viewport_height)), - slow_mo_ms=int(raw.get("slow_mo_ms", base.slow_mo_ms)), - video_width=int(video_size.get("width", base.video_width)), - video_height=int(video_size.get("height", base.video_height)), - navigation_timeout_ms=int(raw.get("navigation_timeout_ms", base.navigation_timeout_ms)), - step_delay_ms=int(raw.get("step_delay_ms", base.step_delay_ms)), - enable_overlay=bool(raw.get("enable_overlay", base.enable_overlay)), - enable_trace=bool(raw.get("enable_trace", base.enable_trace)), - video_format=str(raw.get("video_format", base.video_format)).lower(), - har_mode=har_mode, - ) - - @classmethod - def defaults(cls) -> "PlaywrightConfig": - """設定が完全に省略された場合の defaults。viewport=video_size=1280x720 で揃える。""" - return cls() - - -@dataclass -class RunnerConfig: - workers: int = 4 - testcases_dir: str = "./testcases" - - @classmethod - def from_raw(cls, raw: dict[str, Any]) -> "RunnerConfig": - return cls( - workers=int(raw.get("workers", 4)), - testcases_dir=str(raw.get("testcases_dir", "./testcases")), - ) - - -# --- accessibility / web vitals (v0.3.0) ----------------------------- - -@dataclass -class AccessibilityConfig: - """axe-core 自動スキャンの設定 (page_role に応じて runner が自動実行)。""" - enabled: bool = True - auto_roles: list[str] = field(default_factory=lambda: [ - "lp", "list", "form", "dashboard", "cart", "checkout", "settings", "auth", - ]) - tags: list[str] = field(default_factory=lambda: [ - "wcag2a", "wcag2aa", "wcag21aa", "wcag22aa", - ]) - # 検出した violations を testcase の FAIL 要因として扱うか (false なら情報出力のみ) - fail_on_violations: bool = True - - -@dataclass -class WebVitalsConfig: - """Core Web Vitals 自動計測の設定 (page_role に応じて runner が自動実行)。""" - enabled: bool = True - auto_roles: list[str] = field(default_factory=lambda: [ - "lp", "list", "dashboard", "search", - ]) - observe_ms: int = 5000 - # poor 判定が 1 件でもあれば testcase を FAIL とするか - fail_on_poor: bool = True - - -# --- body_check (PHP / SSR エラー検出, v0.4.0) ---------------------- - -@dataclass -class BodyCheckConfig: - """ページ本文の文字列マッチ検出 (PHP / SSR プロジェクト向け)。 - - JavaScript ランタイム由来の console.error / pageerror では拾えない、 - サーバ側で HTML 本文に直接出力された "Fatal error" / "Warning:" 等の - エラー文字列を、Playwright の ``page.on("response", ...)`` を介して - 検出する。 - - - ``fatal_patterns``: HTML 本文全体に対する substring match。1 つでも - 含まれれば violation。 - - ``warning_patterns``: 本文の **先頭 ``warning_head_chars`` 文字** に - 対する substring match。本文中の説明文に含まれる "Notice:" 等は許容し、 - ページ最上段への漏れだけを拾う。 - (PLAN18 の説明文は「先頭 300 文字」、code point ベース。日本語ページで - 300 bytes だと先頭 1〜2 行しか見えず実用にならないため、文字数を採用。) - - ``not_found_patterns``: 本文全体への substring match。 - - ``fail_on_match``: True なら violation 検出時に ``pytest.fail``。 - False なら情報収集のみ (report.md / body_check.jsonl には記録)。 - - default は ``enabled=True`` + PHP 系のフロント漏れ検出パターンを内蔵。 - config.yaml を書かなくてもまず PHP プロジェクトで素直に動く。 - """ - - enabled: bool = True - fatal_patterns: list[str] = field(default_factory=lambda: [ - "Fatal error", - "Uncaught", - "Parse error", - ]) - warning_patterns: list[str] = field(default_factory=lambda: [ - "STRICT:", - "Warning:", - "Notice:", - "Deprecated:", - ]) - # 文字数ベースの head 切り出し閾値 (code points)。PLAN18 のフィールド名は - # ``warning_head_bytes`` だったが、説明文は「先頭 300 文字」と書かれており - # 矛盾していた。実用上は文字数の方が日本語ページで安定するため採用。 - warning_head_chars: int = 300 - not_found_patterns: list[str] = field(default_factory=lambda: [ - "File not found", - ]) - fail_on_match: bool = True - - -# --- ルート --------------------------------------------------------- - -@dataclass -class Config: - base_url: str - basic_auth: BasicAuth - verify_tls: bool - roles: dict[str, Role] - playwright: PlaywrightConfig - runner: RunnerConfig - report: ReportConfig - config_path: Path # 設定ファイルの絶対パス(testcases_dir の解決基点) - browser: BrowserConfig = field(default_factory=BrowserConfig) - # docs/checklists/checklist-common.md C8/C9 の境界曖昧さに対応する「除外」設定。 - # console.error / pageerror の本文がいずれかの正規表現にマッチした場合は - # 集計から除外し FAIL を抑制する。3rd party の既知 warning などを許容するための - # 抜け穴。空 (デフォルト) なら従来どおり 1 件で FAIL。 - tolerated_console_errors: list[str] = field(default_factory=list) - tolerated_page_errors: list[str] = field(default_factory=list) - # accessibility / web_vitals 自動実行 (page_role に応じて runner が判定) - accessibility: AccessibilityConfig = field(default_factory=AccessibilityConfig) - web_vitals: WebVitalsConfig = field(default_factory=WebVitalsConfig) - # PHP / SSR ページ本文エラー検出 (v0.4.0, opt-in) - body_check: BodyCheckConfig = field(default_factory=BodyCheckConfig) - - @property - def testcases_dir(self) -> Path: - d = Path(self.runner.testcases_dir) - if not d.is_absolute(): - d = self.config_path.parent / d - return d.resolve() - - def role(self, role_id: str) -> Role: - if role_id not in self.roles: - raise KeyError(f"未定義のロール: {role_id}. roles 設定を確認してください。") - return self.roles[role_id] - - @classmethod - def load(cls, path: Path) -> "Config": - if not path.exists(): - raise FileNotFoundError( - f"設定ファイルが見つかりません: {path}\n" - "templates/scenario.config.yaml をコピーして作成してください。" - ) - with path.open("r", encoding="utf-8") as fp: - raw = yaml.safe_load(fp) - if not isinstance(raw, dict): - raise ValueError( - f"scenario.config.yaml の中身が空または辞書ではありません: {path}\n" - "templates/scenario.config.yaml をコピーして必要項目を埋めてください。" - ) - raw = _expand_env(raw) - return cls._from_dict(raw, config_path=path.resolve()) - - @classmethod - def _from_dict(cls, raw: dict[str, Any], *, config_path: Path) -> "Config": - target = raw["target"] - # basic_auth は省略可能 (サイトに Basic 認証が掛かっていない場合)。 - # 省略時は空 BasicAuth を使い、role 側で `requires_basic_auth: true` を - # 指定したテストケースだけが basic_auth ヘッダを要求する設計。 - ba_raw = target.get("basic_auth") or {} - basic_auth = BasicAuth( - user=str(ba_raw.get("user", "")), - password=str(ba_raw.get("password", "")), - ) - roles = {rid: _role_from_raw(rid, r) for rid, r in (raw.get("roles") or {}).items()} - - cfg = cls( - base_url=target["base_url"].rstrip("/"), - basic_auth=basic_auth, - verify_tls=bool(raw.get("verify_tls", False)), - roles=roles, - playwright=PlaywrightConfig.from_raw(raw.get("playwright") or {}), - runner=RunnerConfig.from_raw(raw.get("runner") or {}), - report=_report_from_raw(raw.get("report") or {}), - config_path=config_path, - browser=BrowserConfig.from_raw(raw.get("browser") or {}), - tolerated_console_errors=list(raw.get("tolerated_console_errors") or []), - tolerated_page_errors=list(raw.get("tolerated_page_errors") or []), - accessibility=_accessibility_from_raw(raw.get("accessibility") or {}), - web_vitals=_web_vitals_from_raw(raw.get("web_vitals") or {}), - body_check=_body_check_from_raw(raw.get("body_check") or {}), - ) - - # fail-fast: requires_basic_auth=True なロールが宣言されているのに - # basic_auth.user が空ならば実行時に HTTP 401 で必ず落ちる。先に検出して - # 設定不備として ValueError を投げる (Maj-4)。 - for role in cfg.roles.values(): - if role.login.requires_basic_auth and not basic_auth.user: - raise ValueError( - f"role '{role.id}' は requires_basic_auth=True だが、" - f"target.basic_auth.user が空 (config.yaml を確認してください)" - ) - - return cfg - - -def _role_from_raw(rid: str, raw: dict[str, Any]) -> Role: - login = raw["login"] - return Role( - id=rid, - label=str(raw.get("label", rid)), - login=Login( - path=login["path"], - requires_basic_auth=bool(login.get("requires_basic_auth", False)), - fields=dict(login["fields"]), - fail_if_url_contains=login["fail_if_url_contains"], - submit_selectors=list(login.get("submit_selectors") or []), - ), - ) - - -def _report_from_raw(raw: dict[str, Any]) -> ReportConfig: - labels_raw = raw.get("phase_labels") or {} - return ReportConfig( - title=str(raw.get("title", "シナリオ E2E テスト 実施報告書")), - test_plan_link=str(raw.get("test_plan_link", "./test-plan.md")), - phase_labels={int(k): str(v) for k, v in labels_raw.items()}, - ) - - -def _accessibility_from_raw(raw: dict[str, Any]) -> AccessibilityConfig: - base = AccessibilityConfig() - return AccessibilityConfig( - enabled=bool(raw.get("enabled", base.enabled)), - auto_roles=list(raw.get("auto_roles") or base.auto_roles), - tags=list(raw.get("tags") or base.tags), - fail_on_violations=bool(raw.get("fail_on_violations", base.fail_on_violations)), - ) - - -def _web_vitals_from_raw(raw: dict[str, Any]) -> WebVitalsConfig: - base = WebVitalsConfig() - return WebVitalsConfig( - enabled=bool(raw.get("enabled", base.enabled)), - auto_roles=list(raw.get("auto_roles") or base.auto_roles), - observe_ms=int(raw.get("observe_ms", base.observe_ms)), - fail_on_poor=bool(raw.get("fail_on_poor", base.fail_on_poor)), - ) - - -def _body_check_from_raw(raw: dict[str, Any]) -> BodyCheckConfig: - """``body_check`` セクションを ``BodyCheckConfig`` に変換する。 - - - キーが **省略** されている場合は dataclass の default 値を採用する - (config を書かなくても PHP 系のデフォルトパターンが効くようにするため)。 - - キーが **明示的に空リスト** で書かれている場合はそのまま空リストにする - (default を上書きしてカテゴリを無効化したい場合の挙動)。 - """ - base = BodyCheckConfig() - - def _patterns(key: str, default: list[str]) -> list[str]: - if key not in raw: - return list(default) - value = raw.get(key) - if value is None: - return list(default) - return [str(s) for s in value] - - # ``warning_head_chars`` を新フィールド名として採用。旧名 ``warning_head_bytes`` - # も alias として受理する (PLAN18 がフィールド名と説明文で矛盾していた経緯)。 - head_chars = raw.get("warning_head_chars") - if head_chars is None: - head_chars = raw.get("warning_head_bytes", base.warning_head_chars) - - return BodyCheckConfig( - enabled=bool(raw.get("enabled", base.enabled)), - fatal_patterns=_patterns("fatal_patterns", base.fatal_patterns), - warning_patterns=_patterns("warning_patterns", base.warning_patterns), - warning_head_chars=int(head_chars), - not_found_patterns=_patterns("not_found_patterns", base.not_found_patterns), - fail_on_match=bool(raw.get("fail_on_match", base.fail_on_match)), - ) diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/fixtures/__init__.py b/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/fixtures/__init__.py deleted file mode 100644 index b9eb500..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/fixtures/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -"""playwright-scenario-test pytest fixtures。 - -利用者は通常の pytest テストを書き、`pwk_config` / `pwk_role_` 等の -fixture をパラメタ宣言するだけで NDF の機能 (config / 認証 / evidence / -accessibility / web vitals / overlay / Drive) を享受できる。 - -各 fixture の実体はサブモジュールに分離する: -- ``auth`` : ``pwk_config`` / ``pwk_role_`` (login 済 storage_state) -- ``evidence`` : ``pwk_evidence`` (HAR / trace / console listeners) -- ``accessibility`` : autouse hook で page_role marker に応じ axe-core を実行 -- ``web_vitals`` : autouse hook で page_role marker に応じ Core Web Vitals 計測 - -pytest plugin (``playwright_kit.pytest_plugin``) から ``pytest_plugins`` で -読み込まれる想定。利用者プロジェクトの ``conftest.py`` で個別 import する必要は無い。 -""" - -from __future__ import annotations - -__all__ = ["auth"] diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/fixtures/accessibility.py b/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/fixtures/accessibility.py deleted file mode 100644 index eafc0a7..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/fixtures/accessibility.py +++ /dev/null @@ -1,143 +0,0 @@ -"""accessibility fixture: ``page_role`` marker に応じた axe-core 自動スキャン。 - -Web アクセシビリティ (WCAG 準拠) を ``axe-core`` で機械検査する fixture。 -``@pytest.mark.page_role("form")`` 等が付与された test 関数の終了直前に -axe-core を自動実行する。 - -利用方法: -- ``@pytest.mark.page_role("form")`` を test に付与すれば autouse 経由で - axe-core が走る (config.accessibility.auto_roles に該当する場合のみ) -- 違反があれば ``config.accessibility.fail_on_violations`` (default True) に従い - ``pytest.fail`` する -- 明示的に scan したい場合は ``pwk_accessibility_scan`` fixture を直接呼ぶ -""" - -from __future__ import annotations - -from collections import Counter -from typing import Iterator - -import pytest - -from playwright_kit import accessibility as accessibility_mod -from playwright_kit.config import Config -from playwright_kit.fixtures.evidence import PwkEvidence - - -def _page_roles_from_marker(item) -> list[str]: - """test item から ``page_role`` marker の引数 (役割名 list) を集約する。""" - roles: list[str] = [] - for marker in item.iter_markers(name="page_role"): - for arg in marker.args: - if isinstance(arg, str): - roles.append(arg) - elif isinstance(arg, (list, tuple)): - roles.extend(str(a) for a in arg) - return roles - - -@pytest.fixture() -def pwk_accessibility_scan(page, pwk_evidence: PwkEvidence, _pwk_config_optional): - """明示呼び出し用: ``violations = pwk_accessibility_scan()`` で 1 度スキャン。""" - config: Config | None = _pwk_config_optional - - def _scan(*, tags: tuple[str, ...] | None = None) -> list[dict]: - if not accessibility_mod.is_available(): - pwk_evidence.log_lines.append( - "[accessibility] axe-playwright-python 未インストール — SKIP " - "(`uv sync --extra a11y` で有効化)" - ) - return [] - actual_tags = ( - tuple(tags) - if tags is not None - else ( - tuple(config.accessibility.tags) - if config is not None - else accessibility_mod.DEFAULT_TAGS - ) - ) - violations = accessibility_mod.scan_page(page, tags=actual_tags) - pwk_evidence.axe_violations.extend(violations) - return violations - - return _scan - - -@pytest.fixture(autouse=True) -def _pwk_accessibility_autouse(request) -> Iterator[None]: - """``page_role`` marker が付いた test の終了直前に axe-core を実行する。 - - ``page`` fixture を **要求している test のみ** 対象。autouse fixture が - 無条件に ``page`` を要求すると、pytest-playwright が全 test を browser - parametrize してしまうため、ここでは ``request.fixturenames`` を見て - 必要な test だけ取得する。 - - Issue #60 fix: 旧版の ``"pwk_evidence" not in request.fixturenames`` ガードを - 廃止。test 引数に ``pwk_evidence`` を書いていなくても ``getfixturevalue`` - 経由で lazy 取得し、accessibility autouse が走るようにする。 - - teardown order 対策 (Issue #61): pytest fixture の teardown は LIFO のため、 - ``yield`` 後に ``getfixturevalue("pwk_evidence")`` を呼ぶと「既に解放済」 - AssertionError が発生する。setup phase で ``ev`` / ``page`` を取得して - closure に保持し、teardown phase はその参照のみを使う。 - """ - - # ``page`` を要求していない (= browser を使わない) test では何もしない。 - # これにより pure pytest test の挙動に影響を与えない。 - if "page" not in request.fixturenames: - yield - return - - config: Config | None = request.getfixturevalue("_pwk_config_optional") - if config is None or not config.accessibility.enabled: - yield - return - page_roles = _page_roles_from_marker(request.node) - if not page_roles: - yield - return - if not accessibility_mod.should_auto_scan( - page_roles, auto_roles=frozenset(config.accessibility.auto_roles) - ): - yield - return - - # setup phase: closure に必要なオブジェクトを束ねる。 - pwk_evidence: PwkEvidence = request.getfixturevalue("pwk_evidence") - page = request.getfixturevalue("page") - - yield - - # teardown phase: closure に保持した ev / page のみを参照する。 - if not accessibility_mod.is_available(): - pwk_evidence.log_lines.append( - "[accessibility autouse] axe-playwright-python 未インストール — SKIP" - ) - return - - try: - if page.is_closed(): - return - except Exception: - return - - violations = accessibility_mod.scan_page(page, tags=tuple(config.accessibility.tags)) - pwk_evidence.axe_violations.extend(violations) - if not violations: - return - - impacts = Counter(v.get("impact") or "unknown" for v in violations) - impact_summary = ", ".join(f"{k}={n}" for k, n in impacts.most_common()) - pwk_evidence.log_lines.append( - f"[accessibility autouse] {len(violations)} violations: {impact_summary}" - ) - - if config.accessibility.fail_on_violations: - pytest.fail( - f"[accessibility] {len(violations)} 件の axe-core 違反 " - f"[{impact_summary}]: " - + ", ".join( - f"{v.get('id')}({v.get('impact', '?')})" for v in violations[:5] - ) - ) diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/fixtures/auth.py b/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/fixtures/auth.py deleted file mode 100644 index 964f1e1..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/fixtures/auth.py +++ /dev/null @@ -1,318 +0,0 @@ -"""認証 fixture: ``pwk_config`` と動的に生成する ``pwk_role_``。 - -pytest-playwright が提供する ``page`` / ``context`` / ``browser_context_args`` -fixture と協調して動作する。 - -設計方針: -- ``pwk_config`` は session scope。``--pwk-config`` で指定された YAML を - 1 度だけ読み込む。利用者プロジェクトの ``conftest.py`` から override 可能。 -- 各 role に対し ``pwk_role_`` fixture を *動的* に生成する。 - 実体は ``_login_and_get_storage_state`` で session 内 1 回だけ login し、 - storage_state を session-scoped cache (`_StorageStateCache`) に保管。 - 以降の test では同じ role の cache を ``context.add_cookies`` 等で再利用する - ことで login の再実行を避ける。 -- function scope で ``page.context.storage_state(...)`` を inject し、 - ``page`` は既に該当 role でログイン済みの状態で test 関数に渡される。 - -fail_if_url_contains による失敗判定もここで行い、test 開始前に明示的に -``pytest.fail`` する。 -""" - -from __future__ import annotations - -import os -from dataclasses import dataclass -from pathlib import Path -from typing import Any, Callable -from urllib.parse import urlsplit - -import pytest - -from playwright_kit.config import Config, Login, Role - - -def _same_origin(origin_url: str, base_url: str) -> bool: - """``origin_url`` が ``base_url`` と同一 origin (scheme + host + port) かを返す。 - - storage_state には認証対象以外のサードパーティ origin (広告 / 計測タグ等) が - 含まれることがある。restore 時にそれらへ ``page.goto(url)`` するのは意図しない - 外部送信になるため、本関数で base_url の origin に厳格一致するもののみ許可する。 - """ - try: - a = urlsplit(origin_url) - b = urlsplit(base_url) - except ValueError: - return False - return ( - bool(a.scheme) - and bool(a.hostname) - and a.scheme == b.scheme - and a.hostname == b.hostname - and (a.port or None) == (b.port or None) - ) - - -# --------------------------------------------------------------------------- -# Internal helpers -# --------------------------------------------------------------------------- - - -@dataclass -class _StorageStateCache: - """session 内で role ごとの storage_state を 1 回だけ作る簡易 cache。""" - - states: dict[str, dict[str, Any]] - - @classmethod - def empty(cls) -> "_StorageStateCache": - return cls(states={}) - - def get(self, role_id: str) -> dict[str, Any] | None: - return self.states.get(role_id) - - def put(self, role_id: str, state: dict[str, Any]) -> None: - self.states[role_id] = state - - -def _submit_login_form(page, login: Login) -> None: - """ログインフォームの submit を行う。 - - 優先順位は ``login.submit_selectors`` → role/type=submit → password Enter。 - """ - for sel in login.submit_selectors: - try: - page.locator(sel).first.click(timeout=2000) - return - except Exception: - continue - for fallback in ( - "role=button[name=/login|sign.?in|ログイン/i]", - 'button[type="submit"]', - 'input[type="submit"]', - ): - try: - page.locator(fallback).first.click(timeout=2000) - return - except Exception: - continue - pw_field = next( - (n for n in login.fields if "pass" in n.lower() or "pwd" in n.lower()), - None, - ) - if pw_field: - page.locator(f'input[name="{pw_field}"]').press("Enter") - return - raise RuntimeError( - "ログイン送信ボタンが見つかりません (submit_selectors を設定してください)" - ) - - -def _login_and_get_storage_state( - *, - playwright, - base_url: str, - role: Role, - basic_auth_user: str, - basic_auth_password: str, - verify_tls: bool, - nav_timeout_ms: int = 30_000, -) -> dict[str, Any]: - """role の login flow を実行し storage_state を返す。 - - 1 度だけ呼ばれることを想定。失敗時は ``pytest.fail`` を投げる。 - - AQ Critical-2 完遂: 関数全体を browser try/finally で囲み、 - page.goto() / fill() / expect_navigation() / fail_if_url_contains で - pytest.fail() が発生した場合も含め、全ての failure path で - browser.close() が必ず呼ばれることを保証する。 - pytest.fail() は内部的に例外を raise するため finally は確実に動く。 - """ - browser = playwright.chromium.launch(headless=True) - try: - ctx_kwargs: dict[str, Any] = { - "ignore_https_errors": not verify_tls, - } - if role.login.requires_basic_auth: - ctx_kwargs["http_credentials"] = { - "username": basic_auth_user, - "password": basic_auth_password, - } - context = browser.new_context(**ctx_kwargs) - context.set_default_navigation_timeout(nav_timeout_ms) - context.set_default_timeout(nav_timeout_ms) - - try: - page = context.new_page() - url = f"{base_url}{role.login.path}" - try: - page.goto(url, wait_until="domcontentloaded", timeout=nav_timeout_ms) - except Exception as exc: # pragma: no cover - depends on remote target - pytest.fail( - f"[pwk_role_{role.id}] login page open failed: {url} ({exc})" - ) - - for name, value in role.login.fields.items(): - try: - page.locator(f'input[name="{name}"]').fill( - value, timeout=nav_timeout_ms - ) - except Exception as exc: # pragma: no cover - pytest.fail( - f"[pwk_role_{role.id}] fill {name!r} failed: {exc}" - ) - - try: - with page.expect_navigation( - wait_until="domcontentloaded", timeout=nav_timeout_ms - ): - _submit_login_form(page, role.login) - except Exception as exc: # pragma: no cover - pytest.fail( - f"[pwk_role_{role.id}] navigation 失敗: " - f"{type(exc).__name__}: {exc}" - ) - - final_url = page.url - # Amazon Q Critical-1: fail_if_url_contains が空文字列の場合、空文字列は - # あらゆる文字列に含まれるため常に True になり全 login が失敗する。 - # 空文字列 (= 未設定) の場合はチェックをスキップする。 - if role.login.fail_if_url_contains and role.login.fail_if_url_contains in final_url: - pytest.fail( - f"[pwk_role_{role.id}] login 失敗: " - f"final_url={final_url} に '{role.login.fail_if_url_contains}' を含む" - ) - - state = context.storage_state() - return state - finally: - try: - context.close() - except Exception: - pass - finally: - try: - browser.close() - except Exception: - pass - - -# --------------------------------------------------------------------------- -# Public fixtures -# --------------------------------------------------------------------------- - - -@pytest.fixture(scope="session") -def pwk_config(pytestconfig) -> Config: - """``--pwk-config`` で指定された YAML をロードして ``Config`` を返す。 - - 指定が無い場合は CWD 直下の ``scenario.config.yaml`` を試し、 - それも無ければ ``pytest.skip`` する (NDF 機能が要らない test と共存可能にする)。 - """ - raw_path: str | None = pytestconfig.getoption("pwk_config", default=None) - if not raw_path: - env = os.environ.get("PWK_CONFIG") - if env: - raw_path = env - if not raw_path: - candidate = Path.cwd() / "scenario.config.yaml" - if candidate.exists(): - raw_path = str(candidate) - if not raw_path: - pytest.skip( - "pwk_config 未指定: --pwk-config もしくは PWK_CONFIG env、" - "または ./scenario.config.yaml を用意してください。" - ) - - path = Path(raw_path).resolve() - return Config.load(path) - - -@pytest.fixture(scope="session") -def _pwk_storage_state_cache() -> _StorageStateCache: - return _StorageStateCache.empty() - - -def _make_role_fixture(role_id: str) -> Callable: - """role_id ごとに ``pwk_role_`` fixture の実装関数を生成する。""" - - def _fixture( - pwk_config: Config, - playwright, - context, - _pwk_storage_state_cache: _StorageStateCache, - ) -> Role: - """login 済の storage_state を ``context`` に注入し、Role を返す。 - - - ``playwright`` / ``context`` は ``pytest-playwright`` 提供 - - 既に同 role の storage_state が cache 済なら login をスキップ - """ - role = pwk_config.role(role_id) - - state = _pwk_storage_state_cache.get(role_id) - if state is None: - state = _login_and_get_storage_state( - playwright=playwright, - base_url=pwk_config.base_url, - role=role, - basic_auth_user=pwk_config.basic_auth.user, - basic_auth_password=pwk_config.basic_auth.password, - verify_tls=pwk_config.verify_tls, - nav_timeout_ms=pwk_config.playwright.navigation_timeout_ms, - ) - _pwk_storage_state_cache.put(role_id, state) - - # cookies / origins (localStorage 等) を新しい context に注入する。 - cookies = state.get("cookies") or [] - if cookies: - context.add_cookies(cookies) - # storage_state に含まれる広告/計測タグ等の third-party origin に対して - # 不用意に goto するのを避けるため、base_url と同一 origin のみ復元する。 - for origin in state.get("origins") or []: - url = origin.get("origin") - items = origin.get("localStorage") or [] - if not url or not items: - continue - if not _same_origin(url, pwk_config.base_url): - continue - try: - page = context.new_page() - page.goto(url, wait_until="domcontentloaded") - for it in items: - page.evaluate( - "([k, v]) => window.localStorage.setItem(k, v)", - [it.get("name"), it.get("value")], - ) - page.close() - except Exception: - # localStorage 注入失敗は致命的ではない (cookie ベースの認証なら OK)。 - pass - - return role - - _fixture.__name__ = f"pwk_role_{role_id}" - _fixture.__doc__ = ( - f"role={role_id!r} で login 済の storage_state を context に注入する。" - ) - return _fixture - - -def register_role_fixtures(plugin_module, config: Config) -> list[str]: - """plugin module に ``pwk_role_`` fixture を動的登録する。 - - ``pytest_configure`` から呼ばれる。pytest は modules の attribute を - fixture として discover するため、setattr で十分。 - - Returns: - 登録した fixture 名のリスト。 - """ - registered: list[str] = [] - for role_id in config.roles: - name = f"pwk_role_{role_id}" - if hasattr(plugin_module, name): - continue - impl = _make_role_fixture(role_id) - # function scope (default) で wrap してから plugin module に attach - wrapped = pytest.fixture(name=name)(impl) - setattr(plugin_module, name, wrapped) - registered.append(name) - return registered diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/fixtures/body_check.py b/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/fixtures/body_check.py deleted file mode 100644 index d6437c2..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/fixtures/body_check.py +++ /dev/null @@ -1,194 +0,0 @@ -"""body_check fixture: ``page.on("response", ...)`` でレスポンス本文を監視し、 -PHP / SSR が HTML 本文に出力したエラー文字列を検出する (v0.4.0)。 - -config.yaml の ``body_check.enabled`` の default は ``True`` (PHP 系パターン -内蔵)。``page`` fixture を要求している test に限り autouse で listener を -attach する (accessibility autouse と同じ guard 戦略)。 - -opt-out: -- 全体無効化: ``body_check.enabled: false`` を config.yaml に明示 -- カテゴリ単位: ``fatal_patterns: []`` などで明示空指定 -- 個別 test: ``@pytest.mark.no_body_check`` を付与 - -注意: -- 同じ page を ``pwk_body_check_scan`` helper で手動スキャンしつつ autouse - listener も走らせると、同一 violation を重複加算する。helper は autouse の - listener が拾えないタイミング (例: SPA 内のローカル DOM 更新後) でのみ使う。 -""" - -from __future__ import annotations - -import json -from typing import Any, Iterator - -import pytest - -from playwright_kit.body_check import ( - is_html_response, - scan_body, -) -from playwright_kit.config import BodyCheckConfig, Config -from playwright_kit.fixtures.evidence import PwkEvidence - - -def _build_response_handler(cfg: BodyCheckConfig, ev: PwkEvidence): - """``page.on("response", ...)`` 用の handler を closure として作る。 - - listener 内で発生する例外は test 失敗には伝播させず ``ev.log_lines`` に - 記録するに留める (accessibility / pageerror listener と同じ防御方針)。 - """ - fatal = tuple(cfg.fatal_patterns) - warn = tuple(cfg.warning_patterns) - not_found = tuple(cfg.not_found_patterns) - head_chars = int(cfg.warning_head_chars) - - def _on_response(response) -> None: - try: - headers = response.headers or {} - if not is_html_response(headers.get("content-type") or headers.get("Content-Type")): - return - try: - body = response.text() - except Exception: - # navigation 中の中断 / closed context などで text() が失敗するケース - return - violations = scan_body( - body, - response.url, - fatal_patterns=fatal, - warning_patterns=warn, - warning_head_chars=head_chars, - not_found_patterns=not_found, - ) - for v in violations: - ev.body_check_violations.append(v.to_dict()) - except Exception as exc: # pragma: no cover - listener 内で test を落とさない - ev.log_lines.append(f"[body_check listener] {exc}") - - return _on_response - - -def _write_jsonl(ev: PwkEvidence) -> None: - """1 violation = 1 行で ``case_dir/body_check.jsonl`` に書き出す。""" - if not ev.body_check_violations: - return - try: - path = ev.case_dir / "body_check.jsonl" - with path.open("w", encoding="utf-8") as fp: - for v in ev.body_check_violations: - fp.write(json.dumps(v, ensure_ascii=False) + "\n") - except Exception as exc: # pragma: no cover - ev.log_lines.append(f"[body_check] jsonl write failed: {exc}") - - -def _format_violation_summary(violations: list[dict[str, Any]], limit: int = 5) -> str: - """``pytest.fail`` メッセージ用の短いサマリ文字列。""" - parts: list[str] = [] - for v in violations[:limit]: - url = v.get("url", "?") - cat = v.get("category", "?") - pat = v.get("pattern", "?") - parts.append(f"{cat}:{pat!r}@{url}") - if len(violations) > limit: - parts.append(f"... (+{len(violations) - limit} more)") - return "; ".join(parts) - - -@pytest.fixture() -def pwk_body_check_scan(page, pwk_evidence: PwkEvidence, _pwk_config_optional): - """明示呼び出し用: ``violations = pwk_body_check_scan()`` で現在の page 本文を 1 度スキャン。 - - autouse 経路を使わず、特定タイミング (例: フォーム送信後の 200 応答) で - 本文を再評価したい場合の helper。 - """ - config: Config | None = _pwk_config_optional - - def _scan() -> list[dict[str, Any]]: - if config is None or not config.body_check.enabled: - return [] - try: - body = page.content() - except Exception as exc: - pwk_evidence.log_lines.append(f"[body_check] page.content() failed: {exc}") - return [] - violations = scan_body( - body, - page.url, - fatal_patterns=config.body_check.fatal_patterns, - warning_patterns=config.body_check.warning_patterns, - warning_head_chars=config.body_check.warning_head_chars, - not_found_patterns=config.body_check.not_found_patterns, - ) - as_dicts = [v.to_dict() for v in violations] - pwk_evidence.body_check_violations.extend(as_dicts) - return as_dicts - - return _scan - - -@pytest.fixture(autouse=True) -def _pwk_body_check_autouse(request) -> Iterator[None]: - """``page`` を要求する test に限り、HTML response への body_check を自動実行する。 - - ガード戦略: - - ``page`` を fixturename に持たない test は対象外 (browser を起動させない) - - config.body_check.enabled が False なら何もしない - - ``@pytest.mark.no_body_check`` が付いている test は skip - - ``pwk_evidence`` は ``getfixturevalue`` で setup phase に lazy resolve する。 - test 関数の引数に ``pwk_evidence`` を書いていなくても listener が attach される - (Issue #60)。 - - teardown 時に違反があれば ``case_dir/body_check.jsonl`` に書き出し、 - ``fail_on_match`` が True なら ``pytest.fail`` で test を失敗させる。 - """ - if "page" not in request.fixturenames: - yield - return - - config: Config | None = request.getfixturevalue("_pwk_config_optional") - if config is None or not config.body_check.enabled: - yield - return - - if request.node.get_closest_marker("no_body_check") is not None: - yield - return - - page = request.getfixturevalue("page") - # setup phase で fetch して closure に保持する (Issue #61 と同じ teardown - # order 問題を防ぐ)。 - ev: PwkEvidence = request.getfixturevalue("pwk_evidence") - handler = _build_response_handler(config.body_check, ev) - - try: - page.on("response", handler) - except Exception as exc: - ev.log_lines.append(f"[body_check] page.on attach failed: {exc}") - yield - return - - try: - yield - finally: - try: - page.remove_listener("response", handler) - except Exception: - # page が既に閉じられている場合などは黙殺 - pass - - if not ev.body_check_violations: - return - - _write_jsonl(ev) - - ev.log_lines.append( - f"[body_check] {len(ev.body_check_violations)} 件の違反: " - + _format_violation_summary(ev.body_check_violations) - ) - - if config.body_check.fail_on_match: - pytest.fail( - f"[body_check] {len(ev.body_check_violations)} 件の本文エラーを検出: " - + _format_violation_summary(ev.body_check_violations) - ) diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/fixtures/evidence.py b/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/fixtures/evidence.py deleted file mode 100644 index c5ab484..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/fixtures/evidence.py +++ /dev/null @@ -1,353 +0,0 @@ -"""evidence fixture: HAR / trace / console / pageerror の収集。 - -pytest-playwright が提供する ``browser_context_args`` / ``context`` / ``page`` -fixture と組み合わせて、test 関数ごとに以下を自動収集する: - -- HAR: ``browser_context_args`` に ``record_har_path`` を inject -- trace: ``context.tracing.start`` / ``stop`` (``--pwk-no-evidence`` で無効化) -- console.error / pageerror: page listener として attach - (``tolerated_console_errors`` / ``tolerated_page_errors`` でフィルタ) - -artifact の出力先は ``--pwk-out-dir`` (default: ``./reports//``)。 -test 関数 ID から sub-dir を作って 1 test = 1 dir で隔離する。 -""" - -from __future__ import annotations - -import datetime as _dt -import hashlib -import os -import re -from dataclasses import dataclass, field -from pathlib import Path -from typing import Any, Iterator - -import pytest - -from playwright_kit.config import Config, PlaywrightConfig - - -# --------------------------------------------------------------------------- -# Output directory resolver (session scope) -# --------------------------------------------------------------------------- - - -def _resolve_out_dir(pytestconfig) -> Path: - """``--pwk-out-dir`` が指定されればそれを、なければ ``reports//``。 - - run_id は session 開始時に 1 度だけ決定し、``pytestconfig._pwk_out_dir`` に - キャッシュする。これにより ``pwk_out_dir`` fixture と - ``pytest_terminal_summary`` が別々に ``datetime.now()`` を呼んで - 秒またぎでディレクトリがズレる問題を防ぐ (新規 Major 対応)。 - - ``--pwk-out-dir`` が明示指定されている場合はキャッシュ不要のため - 常にその値を返す(複数回呼ばれても同じ値)。 - """ - raw: str | None = pytestconfig.getoption("pwk_out_dir", default=None) - if raw: - return Path(raw).resolve() - - # --pwk-out-dir 未指定時のみキャッシュで run_id の秒またぎを防ぐ。 - # hasattr で厳密にチェックし、MagicMock 等が偽の属性を返さないようにする。 - if "_pwk_out_dir" in vars(pytestconfig): - return pytestconfig._pwk_out_dir # type: ignore[attr-defined] - - run_id = _dt.datetime.now().strftime("%Y%m%d-%H%M%S") - out = (Path.cwd() / "reports" / run_id).resolve() - # session-scoped キャッシュとして保存 - pytestconfig._pwk_out_dir = out # type: ignore[attr-defined] - return out - - -@pytest.fixture(scope="session") -def pwk_out_dir(pytestconfig) -> Path: - """session 全体で共有する成果物ルート。session 開始時に作成する。 - - ``_resolve_out_dir`` を通じて ``pytestconfig._pwk_out_dir`` にキャッシュし、 - ``pytest_terminal_summary`` と同じ out_dir を参照する。 - """ - out = _resolve_out_dir(pytestconfig) - out.mkdir(parents=True, exist_ok=True) - return out - - -# --------------------------------------------------------------------------- -# Per-test evidence (function scope) -# --------------------------------------------------------------------------- - - -_FILENAME_SAFE_RE = re.compile(r"[^\w\-]+") - - -def _safe_slug(name: str, fallback: str = "test") -> str: - """文字列から安全なファイル名スラグを生成する (後方互換維持)。""" - s = _FILENAME_SAFE_RE.sub("-", name).strip("-").lower() - return s[:80] or fallback - - -def _safe_case_slug(node: Any) -> str: - """nodeid + xdist worker + sha1[:6] suffix で衝突しない slug を生成する (Codex Major 2)。 - - - parametrize / 同名関数 / xdist 並列で trace.zip / request.har の上書きを防止。 - - 既存の _safe_slug(name, fallback) 仕様は変えず、evidence fixture 内のみ本関数を使う。 - """ - nodeid = getattr(node, "nodeid", getattr(node, "name", "test")) - worker = os.environ.get("PYTEST_XDIST_WORKER", "") - raw = f"{nodeid}@{worker}" if worker else nodeid - slug = _FILENAME_SAFE_RE.sub("-", raw).strip("-").lower() - digest = hashlib.sha1(raw.encode("utf-8")).hexdigest()[:6] - # 60 文字 + "-" + sha1[:6] = 最大 67 文字程度に圧縮 - return f"{slug[:60]}-{digest}".strip("-") or "test" - - -@dataclass -class PwkEvidence: - """1 test 関数分の証跡コレクタ。""" - - case_dir: Path - config: Config | None - enabled: bool - - har_path: Path | None = None - trace_path: Path | None = None - har_relpath: str | None = None - trace_relpath: str | None = None - - console_errors: list[str] = field(default_factory=list) - page_errors: list[str] = field(default_factory=list) - axe_violations: list[dict[str, Any]] = field(default_factory=list) - web_vitals_metrics: dict[str, float] = field(default_factory=dict) - web_vitals_passed: bool = True - # PHP / SSR ページ本文エラー (body_check) 違反 (v0.4.0)。1 件 = 1 dict - # ({url, category, pattern, snippet})。 - body_check_violations: list[dict[str, Any]] = field(default_factory=list) - - log_lines: list[str] = field(default_factory=list) - - _trace_started: bool = field(default=False, init=False, repr=False) - _tolerated_console_re: list[re.Pattern[str]] = field( - default_factory=list, init=False, repr=False - ) - _tolerated_page_re: list[re.Pattern[str]] = field( - default_factory=list, init=False, repr=False - ) - - def __post_init__(self) -> None: - if self.config is not None: - self._tolerated_console_re = [ - re.compile(p) for p in self.config.tolerated_console_errors - ] - self._tolerated_page_re = [ - re.compile(p) for p in self.config.tolerated_page_errors - ] - - # --- listener ------------------------------------------------------ - - def attach_listeners(self, page) -> None: - page.on("console", self._on_console) - page.on("pageerror", self._on_pageerror) - - def _on_console(self, msg) -> None: - try: - if msg.type != "error": - return - loc = getattr(msg, "location", None) or {} - text = msg.text[:500] - for rx in self._tolerated_console_re: - if rx.search(text): - return - self.console_errors.append(f"{loc.get('url', '?')}: {text}") - except Exception as exc: # pragma: no cover - self.log_lines.append(f"[console listener] {exc}") - - def _on_pageerror(self, exc) -> None: - try: - text = str(exc)[:1000] - for rx in self._tolerated_page_re: - if rx.search(text): - return - self.page_errors.append(text) - except Exception as listener_exc: # pragma: no cover - self.log_lines.append(f"[pageerror listener] {listener_exc}") - - # --- trace lifecycle (context scope) ------------------------------- - - def start_tracing(self, context) -> None: - if not self.enabled or self.trace_path is None: - return - try: - context.tracing.start( - name=self.case_dir.name, - title=self.case_dir.name, - snapshots=True, - screenshots=True, - sources=False, - ) - self._trace_started = True - except Exception as exc: # pragma: no cover - self.log_lines.append(f"[trace] start 失敗: {exc}") - - def stop_tracing(self, context) -> None: - if not self._trace_started or self.trace_path is None: - return - try: - context.tracing.stop(path=str(self.trace_path)) - self.trace_relpath = self.trace_path.name - except Exception as exc: # pragma: no cover - self.log_lines.append(f"[trace] stop 失敗: {exc}") - - def confirm_har(self) -> None: - if self.har_path and self.har_path.exists(): - self.har_relpath = self.har_path.name - - # --- summary ------------------------------------------------------- - - @property - def has_runtime_errors(self) -> bool: - return bool(self.console_errors or self.page_errors) - - def runtime_error_summary(self) -> str: - parts: list[str] = [] - if self.page_errors: - parts.append(f"pageerror {len(self.page_errors)} 件") - if self.console_errors: - parts.append(f"console.error {len(self.console_errors)} 件") - return "Runtime errors detected: " + ", ".join(parts) if parts else "" - - -# --------------------------------------------------------------------------- -# Fixtures -# --------------------------------------------------------------------------- - - -@pytest.fixture(scope="session") -def _pwk_config_optional(pytestconfig) -> Config | None: - """``pwk_config`` を session に 1 度だけ load する (失敗時は None)。 - - evidence fixture は pwk_config が無くても動くように optional にしてある。 - """ - cached = getattr(pytestconfig, "_pwk_config", None) - if cached is not None: - return cached # type: ignore[no-any-return] - return None - - -def _resolve_har_mode(pytestconfig, pwk_config) -> str: - """``--pwk-har-mode`` CLI > ``playwright.har_mode`` config > ``PlaywrightConfig`` default。 - - Issue #62 対策で default は ``minimal``。``--pwk-no-evidence`` が True の場合 - 呼び出し側で HAR を一切 inject しない (本関数の戻り値は使われない)。 - - default 値は ``PlaywrightConfig().har_mode`` から引くことで dataclass 側との - 二重管理を防ぐ。 - """ - cli = pytestconfig.getoption("pwk_har_mode", default=None) - if cli: - # ``pytest_plugin.py`` の argparse ``choices`` で値は担保済だが、 - # API 経由 (``--config`` 等) で大文字が来た場合に備える defensive normalize。 - return str(cli).lower() - if pwk_config is not None: - return str(pwk_config.playwright.har_mode).lower() - return PlaywrightConfig().har_mode - - -@pytest.fixture() -def browser_context_args( - browser_context_args, request, pytestconfig, pwk_out_dir, _pwk_config_optional -) -> dict[str, Any]: - """pytest-playwright の ``browser_context_args`` を function scope で override し、 - 1 test = 1 HAR を実現する (Codex Major 1)。 - - - scope を function に変更し、``request.node`` ごとに ``case_dir/request.har`` - を ``record_har_path`` に inject する。 - - session 共通 HAR (``session.har``) は廃止。これにより - ``PwkEvidence.confirm_har()`` が常に None を返す不整合を解消。 - - ``--pwk-no-evidence`` が True なら HAR 収集を OFF。 - - HAR mode (Issue #62): - - ``--pwk-har-mode none`` (または config ``playwright.har_mode: none``): - ``record_har_path`` を inject しない。 - - ``minimal`` (default): ``record_har_mode="minimal"`` でメタデータのみ - 記録。Basic 認証 + redirect 連続時の ``ERR_ABORTED`` race を回避する。 - - ``full``: Playwright 既定の full HAR (body + content) を記録。 - """ - no_evidence = bool(pytestconfig.getoption("pwk_no_evidence", default=False)) - args = dict(browser_context_args or {}) - if no_evidence: - return args - - har_mode = _resolve_har_mode(pytestconfig, _pwk_config_optional) - if har_mode == "none": - return args - - case_dir = pwk_out_dir / _safe_case_slug(request.node) - case_dir.mkdir(parents=True, exist_ok=True) - args.setdefault("record_har_path", str(case_dir / "request.har")) - if har_mode == "minimal": - # Playwright の record_har_mode="minimal" (Playwright >= 1.30) は - # request/response の主要メタデータのみ。content は記録されないので - # omit 指定は不要。 - args.setdefault("record_har_mode", "minimal") - else: # "full" - # 既存挙動 (body 含む) を維持しつつ content だけは省略する。 - args.setdefault("record_har_content", "omit") - return args - - -@pytest.fixture() -def pwk_evidence( - request, - pytestconfig, - pwk_out_dir: Path, - _pwk_config_optional, - context, - page, -) -> Iterator[PwkEvidence]: - """1 test 関数分の evidence collector を返す。 - - - ``--pwk-no-evidence`` が True なら trace/HAR を OFF にし、listener のみ動かす - - ``page`` fixture から console / pageerror listener を attach - - ``context.tracing.start/stop`` を裏で実行 (有効時) - - ``--pwk-overlay`` 指定時は ``overlay.OVERLAY_INIT_SCRIPT`` を ``context.add_init_script`` - で全 page に inject する - - ``pytest_runtest_makereport`` から FAIL 時に ``pwk_evidence`` の状態を確認可能 - """ - enabled = not bool(pytestconfig.getoption("pwk_no_evidence", default=False)) - overlay_enabled = bool(pytestconfig.getoption("pwk_overlay", default=False)) - # _safe_case_slug で nodeid + xdist worker + sha1[:6] の衝突しない slug を使用 (Codex Major 2) - case_dir = pwk_out_dir / _safe_case_slug(request.node) - case_dir.mkdir(parents=True, exist_ok=True) - - # HAR mode が "none" のときは ``request.har`` を期待しないようにする - # (browser_context_args で record_har_path 自体を inject していない: Issue #62)。 - har_mode = _resolve_har_mode(pytestconfig, _pwk_config_optional) - har_enabled = enabled and har_mode != "none" - - ev = PwkEvidence( - case_dir=case_dir, - config=_pwk_config_optional, - enabled=enabled, - har_path=(case_dir / "request.har") if har_enabled else None, - trace_path=(case_dir / "trace.zip") if enabled else None, - ) - ev.attach_listeners(page) - ev.start_tracing(context) - - # overlay (赤丸カーソル + 字幕、旧名 HUD) を init_script で inject。 - if overlay_enabled: - try: - from playwright_kit.overlay import OVERLAY_INIT_SCRIPT - - context.add_init_script(OVERLAY_INIT_SCRIPT) - except Exception as exc: # pragma: no cover - ev.log_lines.append(f"[overlay] add_init_script 失敗: {exc}") - - # request.node に ev を保持して makereport hook から参照可能にする - request.node._pwk_evidence = ev # type: ignore[attr-defined] - - try: - yield ev - finally: - try: - ev.stop_tracing(context) - finally: - ev.confirm_har() diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/fixtures/web_vitals.py b/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/fixtures/web_vitals.py deleted file mode 100644 index 924e382..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/fixtures/web_vitals.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Web Vitals fixture: ``page_role`` marker に応じた Core Web Vitals 自動計測。 - -``@pytest.mark.page_role("dashboard")`` 等が付与された test の終了直前に -LCP (Largest Contentful Paint) / CLS (Cumulative Layout Shift) / -TTFB (Time To First Byte) / longest_task (Long Tasks API) を計測する。 -""" - -from __future__ import annotations - -from typing import Iterator - -import pytest - -from playwright_kit import web_vitals as web_vitals_mod -from playwright_kit.config import Config -from playwright_kit.fixtures.evidence import PwkEvidence - - -def _page_roles_from_marker(item) -> list[str]: - roles: list[str] = [] - for marker in item.iter_markers(name="page_role"): - for arg in marker.args: - if isinstance(arg, str): - roles.append(arg) - elif isinstance(arg, (list, tuple)): - roles.extend(str(a) for a in arg) - return roles - - -@pytest.fixture() -def pwk_web_vitals_measure(page, pwk_evidence: PwkEvidence, _pwk_config_optional): - """明示呼び出し用: ``metrics = pwk_web_vitals_measure()`` で 1 度計測。""" - config: Config | None = _pwk_config_optional - - def _measure(*, observe_ms: int | None = None) -> dict[str, float]: - ms = ( - int(observe_ms) - if observe_ms is not None - else ( - int(config.web_vitals.observe_ms) - if config is not None - else 5000 - ) - ) - metrics = web_vitals_mod.measure_page(page, observe_ms=ms) - pwk_evidence.web_vitals_metrics.update(metrics) - pwk_evidence.web_vitals_passed = web_vitals_mod.passed(pwk_evidence.web_vitals_metrics) - return metrics - - return _measure - - -@pytest.fixture(autouse=True) -def _pwk_web_vitals_autouse(request) -> Iterator[None]: - """``page_role`` marker が付いた test の終了直前に Web Vitals 計測を行う。 - - accessibility autouse と同じく ``page`` fixture を要求している test のみ対象。 - - Issue #60 fix: 旧版の ``"pwk_evidence" not in request.fixturenames`` ガードを - 廃止。test 引数に ``pwk_evidence`` を書いていなくても ``getfixturevalue`` - 経由で lazy 取得し、Web Vitals autouse が走るようにする。 - - teardown order 対策 (Issue #61): ``yield`` 後に ``pwk_evidence`` を fetch - しようとすると LIFO 解放済の AssertionError になるため、setup phase で - ``ev`` / ``page`` を取得して closure に保持する。 - """ - - if "page" not in request.fixturenames: - yield - return - - config: Config | None = request.getfixturevalue("_pwk_config_optional") - if config is None or not config.web_vitals.enabled: - yield - return - page_roles = _page_roles_from_marker(request.node) - if not page_roles: - yield - return - if not web_vitals_mod.should_auto_measure( - page_roles, auto_roles=frozenset(config.web_vitals.auto_roles) - ): - yield - return - - # setup phase: closure に保持。 - pwk_evidence: PwkEvidence = request.getfixturevalue("pwk_evidence") - page = request.getfixturevalue("page") - - yield - - # teardown phase: closure 経由でアクセス。 - try: - if page.is_closed(): - return - except Exception: - return - - metrics = web_vitals_mod.measure_page(page, observe_ms=int(config.web_vitals.observe_ms)) - pwk_evidence.web_vitals_metrics.update(metrics) - pwk_evidence.web_vitals_passed = web_vitals_mod.passed(pwk_evidence.web_vitals_metrics) - - detail = ", ".join( - f"{k}={v:.1f}({web_vitals_mod.judge(k, v)})" for k, v in metrics.items() - ) or "no metrics collected" - pwk_evidence.log_lines.append(f"[web_vitals autouse] {detail}") - - if not pwk_evidence.web_vitals_passed and config.web_vitals.fail_on_poor: - pytest.fail(f"[web_vitals] poor metric を検出: {detail}") diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/overlay.py b/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/overlay.py deleted file mode 100644 index 21e9844..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/overlay.py +++ /dev/null @@ -1,220 +0,0 @@ -"""動画録画用 HUD オーバーレイ (赤丸カーソル + 2 行字幕) の制御。 - -ブラウザ標準のカーソルは録画に焼き付かないため、JS で `
` を 2 つ inject する: - - `#__hud_cursor` — mousemove/down/up を追う赤丸(クリックで黄色リップル) - - `#__hud_caption` — 画面上部の 2 行字幕(`直前 │ … / 次へ │ …`) - -OVERLAY_INIT_SCRIPT は context.add_init_script() に渡す。 -他の関数は Page を受け取り、HUD の状態を JS evaluate 越しに更新する。 -""" - -from __future__ import annotations - -from playwright.sync_api import Page - - -OVERLAY_INIT_SCRIPT = r""" -(() => { - if (window.__hudInited) return; - window.__hudInited = true; - function setup() { - if (!document.body) return false; - const cursor = document.createElement('div'); - cursor.id = '__hud_cursor'; - cursor.style.cssText = - 'position:fixed;width:24px;height:24px;border-radius:50%;' + - 'background:rgba(255,80,80,0.55);border:2px solid #f33;' + - 'pointer-events:none;z-index:2147483647;' + - 'transform:translate(-50%,-50%);' + - 'box-shadow:0 0 10px rgba(255,0,0,0.7);transition:background 0.1s;'; - document.documentElement.appendChild(cursor); - // 前のページから引き継いだ最終カーソル位置と表示状態を復元 - try { - const cx = sessionStorage.getItem('__hudCursorX'); - const cy = sessionStorage.getItem('__hudCursorY'); - const cv = sessionStorage.getItem('__hudCursorVisible'); - if (cx !== null) cursor.style.left = cx + 'px'; - if (cy !== null) cursor.style.top = cy + 'px'; - // 既定は「非表示」 (擬似クリック対象がないステップは消す方針) - if (cv === '1') { - cursor.style.opacity = '1'; - cursor.style.visibility = 'visible'; - } else { - cursor.style.opacity = '0'; - cursor.style.visibility = 'hidden'; - } - } catch (e) {} - document.addEventListener('mousemove', (e) => { - cursor.style.left = e.clientX + 'px'; - cursor.style.top = e.clientY + 'px'; - try { - sessionStorage.setItem('__hudCursorX', String(e.clientX)); - sessionStorage.setItem('__hudCursorY', String(e.clientY)); - } catch (err) {} - }, true); - // 任意座標でクリックリップルを発火させる外部 API - window.__hudFlash = function(x, y) { - cursor.style.left = x + 'px'; - cursor.style.top = y + 'px'; - cursor.style.opacity = '1'; - cursor.style.visibility = 'visible'; - try { sessionStorage.setItem('__hudCursorVisible', '1'); } catch (e) {} - spawnRipple(x, y); - cursor.style.background = 'rgba(0,255,200,0.95)'; - cursor.style.transform = 'translate(-50%,-50%) scale(1.6)'; - cursor.style.boxShadow = '0 0 22px rgba(255,224,0,0.95)'; - cursor.style.borderColor = '#ffe000'; - setTimeout(() => { - cursor.style.background = 'rgba(255,80,80,0.55)'; - cursor.style.transform = 'translate(-50%,-50%) scale(1)'; - cursor.style.boxShadow = '0 0 10px rgba(255,0,0,0.7)'; - cursor.style.borderColor = '#f33'; - }, 400); - }; - - // 擬似クリック対象が見つからないステップで非表示にする - window.__hudHideCursor = function() { - cursor.style.opacity = '0'; - cursor.style.visibility = 'hidden'; - try { sessionStorage.setItem('__hudCursorVisible', '0'); } catch (e) {} - }; - - function spawnRipple(x, y) { - // 3 重リングのリップルでクリック箇所を強調 - for (let i = 0; i < 3; i++) { - const ring = document.createElement('div'); - ring.style.cssText = - 'position:fixed;pointer-events:none;z-index:2147483645;' + - 'left:' + x + 'px;top:' + y + 'px;' + - 'width:0;height:0;border:4px solid #ffe000;' + - 'border-radius:50%;transform:translate(-50%,-50%);' + - 'box-shadow:0 0 12px rgba(255,224,0,0.8);'; - document.documentElement.appendChild(ring); - ring.animate( - [ - { width: '24px', height: '24px', opacity: 1, borderWidth: '5px', - borderColor: '#ffe000' }, - { width: '110px', height: '110px', opacity: 0, borderWidth: '2px', - borderColor: '#ff6600' } - ], - { duration: 800, delay: i * 130, easing: 'ease-out', - fill: 'forwards' } - ); - setTimeout(() => { try { ring.remove(); } catch(e) {} }, - 850 + i * 130); - } - const cross = document.createElement('div'); - cross.style.cssText = - 'position:fixed;pointer-events:none;z-index:2147483647;' + - 'left:' + x + 'px;top:' + y + 'px;' + - 'width:48px;height:48px;transform:translate(-50%,-50%);' + - 'background:radial-gradient(circle,rgba(255,224,0,0.55) 0%,rgba(255,224,0,0) 70%);'; - document.documentElement.appendChild(cross); - cross.animate( - [{ opacity: 1 }, { opacity: 0 }], - { duration: 600, easing: 'ease-out', fill: 'forwards' } - ); - setTimeout(() => { try { cross.remove(); } catch(e) {} }, 650); - } - - document.addEventListener('mousedown', (e) => { - cursor.style.background = 'rgba(0,255,200,0.95)'; - cursor.style.transform = 'translate(-50%,-50%) scale(1.6)'; - cursor.style.boxShadow = '0 0 22px rgba(255,224,0,0.95)'; - cursor.style.borderColor = '#ffe000'; - spawnRipple(e.clientX, e.clientY); - }, true); - document.addEventListener('mouseup', () => { - cursor.style.background = 'rgba(255,80,80,0.55)'; - cursor.style.transform = 'translate(-50%,-50%) scale(1)'; - cursor.style.boxShadow = '0 0 10px rgba(255,0,0,0.7)'; - cursor.style.borderColor = '#f33'; - }, true); - - const HUD_HEIGHT = 60; // 字幕 2 行分の固定高 - const cap = document.createElement('div'); - cap.id = '__hud_caption'; - cap.style.cssText = - 'position:fixed;top:0;left:0;right:0;' + - 'height:' + HUD_HEIGHT + 'px;' + - 'background:rgba(0,0,0,0.88);color:#fff;' + - 'font:14px/1.5 "Noto Sans CJK JP","Noto Sans JP",' + - '"Hiragino Sans","Yu Gothic","Meiryo",IPAGothic,sans-serif;' + - 'font-feature-settings:"palt";' + - 'padding:8px 16px;pointer-events:none;z-index:2147483646;' + - 'box-sizing:border-box;border-bottom:3px solid #fa0;' + - 'white-space:pre-wrap;word-break:break-all;'; - document.documentElement.appendChild(cap); - - // 本文が字幕で隠れないよう body を下にずらす (border-bottom 3px を含めて +3) - try { - const padTop = (HUD_HEIGHT + 3) + 'px'; - document.body.style.paddingTop = padTop; - document.documentElement.style.scrollPaddingTop = padTop; - } catch (e) {} - let stored = ''; - try { stored = sessionStorage.getItem('__hudCaption') || ''; } catch (e) {} - cap.textContent = window.__pendingCaption || stored || ''; - return true; - } - if (!setup()) { - document.addEventListener('DOMContentLoaded', setup); - } -})(); -""" - - -_SET_CAPTION_JS = """(text) => { - window.__pendingCaption = text; - try { sessionStorage.setItem('__hudCaption', text); } catch (e) {} - const cap = document.getElementById('__hud_caption'); - if (cap) cap.textContent = text; -}""" - - -def set_caption(page: Page, *, previous: str = "", next_action: str = "") -> None: - """現ページの HUD 字幕を「直前 / 次へ」の 2 行で更新する。 - - sessionStorage にも書き込むので、次の navigation 後に init script が拾い直す。 - """ - parts = [ - f"直前 │ {previous}" if previous else None, - f"次へ │ {next_action}" if next_action else None, - ] - text = "\n".join(p for p in parts if p) - try: - page.evaluate(_SET_CAPTION_JS, text) - except Exception: - pass - - -def flash_click(page: Page, x: int, y: int, *, settle_ms: int = 250) -> None: - """指定座標 (viewport 内) にカーソルを移動し、HUD リップルを発火させる。 - - DOM 要素はクリックしない (`page.mouse.click` は呼ばない)。HUD オーバーレイの - `__hudFlash(x, y)` を JS evaluate で呼び出すだけ。 - """ - try: - page.mouse.move(x, y, steps=10) - except Exception: - pass - try: - page.evaluate( - "(c) => { if (window.__hudFlash) window.__hudFlash(c[0], c[1]); }", - [x, y], - ) - except Exception: - return - if settle_ms > 0: - try: - page.wait_for_timeout(settle_ms) - except Exception: - pass - - -def hide_cursor(page: Page) -> None: - """擬似クリック対象が見つからないとき、HUD カーソルを非表示にする。""" - try: - page.evaluate("() => { if (window.__hudHideCursor) window.__hudHideCursor(); }") - except Exception: - pass diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/pytest_plugin.py b/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/pytest_plugin.py deleted file mode 100644 index 81d7dff..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/pytest_plugin.py +++ /dev/null @@ -1,491 +0,0 @@ -"""playwright-scenario-test の pytest plugin。 - -CLI options: -- ``--pwk-config ``: scenario.config.yaml を指定 -- ``--pwk-out-dir ``: 成果物 (HAR / trace / 動画 / report) の出力先 -- ``--pwk-no-evidence``: evidence 収集を OFF -- ``--pwk-overlay``: overlay (赤丸カーソル + 字幕、旧名 HUD) を ON -- ``--pwk-drive-folder ``: Drive 連携 - -markers: -- ``page_role(*roles)``: accessibility / web vitals autouse の判定材料 -- ``role(role_id)``: login する role を明示 (`pwk_role_` fixture と並用可) -- ``phase(num)``: report.md のフェーズ集計用 -- ``priority(level)``: report.md のソート用 -""" - -from __future__ import annotations - -import datetime as _dt -from pathlib import Path -from typing import Any - -import pytest - -from playwright_kit.pytest_report import PwkTestEntry, write_report - -# 配下の fixture モジュールを pytest_plugins として読み込む -# (こうすると entry-point 経由で plugin がロードされた瞬間に fixture が -# 全 test に対して discover される)。 -pytest_plugins = [ - "playwright_kit.fixtures.auth", - "playwright_kit.fixtures.evidence", - "playwright_kit.fixtures.accessibility", - "playwright_kit.fixtures.web_vitals", - "playwright_kit.fixtures.body_check", -] - - -# --------------------------------------------------------------------------- -# CLI options -# --------------------------------------------------------------------------- - - -def pytest_addoption(parser: pytest.Parser) -> None: - group = parser.getgroup("pwk", "playwright-scenario-test (playwright_kit)") - group.addoption( - "--pwk-config", - action="store", - default=None, - help="scenario.config.yaml へのパス (env PWK_CONFIG, または ./scenario.config.yaml も可)", - ) - group.addoption( - "--pwk-out-dir", - action="store", - default=None, - help="成果物出力先ディレクトリ (default: ./reports//)", - ) - group.addoption( - "--pwk-no-evidence", - action="store_true", - default=False, - help="HAR / trace / video の収集を OFF にする", - ) - group.addoption( - "--pwk-har-mode", - action="store", - default=None, - choices=["minimal", "full", "none"], - help=( - "HAR 録画モード (Issue #62)。" - "minimal=メタデータのみ (default; Basic 認証 + redirect race を回避), " - "full=Playwright 既定の full HAR, " - "none=HAR を出力しない。" - "config の playwright.har_mode より優先。" - ), - ) - group.addoption( - "--pwk-no-video", - action="store_true", - default=False, - help="動画収集を明示的に OFF にする (デフォルトは全テストで動画 ON)", - ) - group.addoption( - "--pwk-overlay", - action="store_true", - default=False, - help="overlay (赤丸カーソル + 字幕、旧名 HUD) を全 page に inject する", - ) - group.addoption( - "--pwk-drive-folder", - action="store", - default=None, - help=( - "Drive アップロード先フォルダ ID (terminal_summary 後に upload 実行)。" - "trace.zip / *.har / 動画には機微情報 (URL / Cookie / localStorage / 操作履歴) " - "が含まれる可能性があります。private folder + 信頼できる共有相手のみに限定してください。" - " (Codex Minor 8)" - ), - ) - - -# --------------------------------------------------------------------------- -# Markers / Config -# --------------------------------------------------------------------------- - - -_PWK_MARKERS: list[tuple[str, str]] = [ - ("page_role", "page_role(*roles): accessibility / web vitals autouse の判定 (例: form, list, dashboard)"), - ("role", "role(role_id): test がどの login role を要求するか (`pwk_role_` 経由でも可)"), - ("phase", "phase(num): report.md のフェーズ集計用 (1〜N の整数)"), - ("priority", "priority(level): report.md のソート用 (high/mid/low など任意文字列)"), - ("no_body_check", "no_body_check: body_check autouse をこの test では skip する"), -] - - -def pytest_configure(config: pytest.Config) -> None: - """marker 登録 + config の早期 load を試みる。 - - config 読み込みは ``pwk_config`` fixture でも遅延ロードされるが、 - ``pwk_role_`` fixture を *動的登録* するためには - ``pytest_configure`` で 1 度 Config をロードしておく必要がある。 - failure は警告にとどめ、利用者が playwright_kit 機能を使わない場合に test 全体を - 潰さないようにする。 - """ - for name, doc in _PWK_MARKERS: - config.addinivalue_line("markers", f"{name}: {doc}") - - # 動的 fixture 登録のため、可能なら Config を early load する。 - cfg = _try_load_config_silently(config) - if cfg is not None: - from playwright_kit.fixtures import auth as auth_module - - registered = auth_module.register_role_fixtures(auth_module, cfg) - if registered: - # plugin 自体にも公開しておく (ユーザが import 元を調整しなくて良いように)。 - import playwright_kit.pytest_plugin as plugin_self - - for name in registered: - fn = getattr(auth_module, name, None) - if fn is not None: - setattr(plugin_self, name, fn) - # session 中で再利用するためにキャッシュする。 - config._pwk_config = cfg # type: ignore[attr-defined] - - # 動画デフォルト ON (大原則: エビデンス動画を常に取得) - # ユーザーが --video を CLI で明示指定した場合はそちらを優先する。 - # --pwk-no-video 指定時は video='off' に設定する。 - # --pwk-no-evidence 指定時も video='off' に設定する (全エビデンス OFF)。 - # pytest-playwright の --video デフォルト値は 'off' であるため、 - # getoption() の返り値では明示指定の有無を判別できない。 - # invocation_params.args を走査して明示指定を検出する。 - try: - cli_args = list(config.invocation_params.args) - video_explicitly_set = any( - a == "--video" or a.startswith("--video=") for a in cli_args - ) - no_video = config.getoption("pwk_no_video", default=False) - no_evidence = config.getoption("pwk_no_evidence", default=False) - if not video_explicitly_set: - if no_video or no_evidence: - config.option.video = "off" - else: - config.option.video = "on" - except (ValueError, AttributeError): - pass - - -# --------------------------------------------------------------------------- -# Reports / hooks -# --------------------------------------------------------------------------- - - -@pytest.hookimpl(hookwrapper=True) -def pytest_runtest_makereport(item, call): - """test の各 phase 終了時に ``pwk_evidence`` の状態をレポートに紐付ける。 - - FAIL 時には evidence の trace/HAR path を log に追記し、 - 成果物 path / marker を rep.user_properties に保存して - ``pytest_terminal_summary`` で report.md に集約する。 - - HAR lifecycle 修正 (Codex Major-1 完遂 / 3回目): - Playwright は HAR を ``context.close()`` 時に flush する。 - ``pwk_evidence`` の finalizer は ``context`` の finalizer より先に動くため、 - call phase の時点では ``har_relpath`` / ``trace_relpath`` がまだ未確定の場合がある。 - - teardown phase の makereport は pytest-playwright の ``context`` finalizer が - teardown 中に完了した後に走るため、ここで再度 ``confirm_har()`` を呼んで - HAR の存在を確認し直し、確定した path を teardown report の user_properties に - 積む。``_collect_entries()`` が teardown report の pwk_har/pwk_trace を call - entry に merge することで、report.md に artifact path が反映される。 - """ - outcome = yield - rep = outcome.get_result() - - ev = getattr(item, "_pwk_evidence", None) - - # teardown phase: context.close() 後に HAR が flush されるため confirm_har() 再呼び出し。 - # 確定した har_relpath / trace_relpath を teardown report の user_properties に積む。 - # body_check_violations もこの phase で確定する (autouse fixture finalizer が - # pytest.fail 直前まで populate してから走る)。 - # _collect_entries() がこれらの値を call entry に merge する。 - if rep.when == "teardown" and ev is not None: - ev.confirm_har() - if ev.har_relpath: - rep.user_properties.append(("pwk_har", str(ev.case_dir / ev.har_relpath))) - if ev.trace_relpath: - rep.user_properties.append( - ("pwk_trace", str(ev.case_dir / ev.trace_relpath)) - ) - rep.user_properties.append( - ("pwk_body_check_violations", len(ev.body_check_violations)) - ) - if ev.body_check_violations: - rep.user_properties.append( - ("pwk_body_check_detail", list(ev.body_check_violations)) - ) - return - - if rep.when != "call": - return - - # pwk_evidence fixture が attach した状態を直接参照 - if ev is not None: - if ev.har_relpath: - rep.user_properties.append(("pwk_har", str(ev.case_dir / ev.har_relpath))) - if ev.trace_relpath: - rep.user_properties.append( - ("pwk_trace", str(ev.case_dir / ev.trace_relpath)) - ) - rep.user_properties.append(("pwk_console_errors", len(ev.console_errors))) - rep.user_properties.append(("pwk_page_errors", len(ev.page_errors))) - - # markers を user_properties に転写 - page_roles: list[str] = [] - for marker in item.iter_markers(name="page_role"): - for arg in marker.args: - if isinstance(arg, str): - page_roles.append(arg) - elif isinstance(arg, (list, tuple)): - page_roles.extend(str(a) for a in arg) - if page_roles: - rep.user_properties.append(("pwk_page_role", page_roles)) - - role_marker = item.get_closest_marker("role") - if role_marker is not None and role_marker.args: - rep.user_properties.append(("pwk_role", str(role_marker.args[0]))) - - phase_marker = item.get_closest_marker("phase") - if phase_marker is not None and phase_marker.args: - try: - rep.user_properties.append(("pwk_phase", int(phase_marker.args[0]))) - except (TypeError, ValueError): - pass - - priority_marker = item.get_closest_marker("priority") - if priority_marker is not None and priority_marker.args: - rep.user_properties.append(("pwk_priority", str(priority_marker.args[0]))) - - -# --------------------------------------------------------------------------- -# Terminal summary / session finish -# --------------------------------------------------------------------------- - - -def _collect_entries(terminalreporter) -> list[PwkTestEntry]: - """terminalreporter から ``PwkTestEntry`` のリストを構築する。 - - xfailed / xpassed も集約する (Codex Major 3)。 - pytest 内部では xfailed の rep は stats["xfailed"] に直接入るため、 - "xfailed" / "xpassed" キーを明示的に走査する。 - - artifact 伝搬 (Codex Major-1 / 3回目): - HAR は context.close() 時に flush されるため、call phase 時点では - har_relpath / trace_relpath が未確定の場合がある。 - teardown phase の makereport で確定した pwk_har / pwk_trace を - call entry に merge することで、report.md に artifact path を反映する。 - """ - # Step 1: call/setup phase の entry を nodeid でインデックス化 - call_entries: dict[str, PwkTestEntry] = {} - for outcome_key in ("passed", "failed", "skipped", "error", "xfailed", "xpassed"): - for rep in terminalreporter.stats.get(outcome_key, []): - if getattr(rep, "when", "call") not in ("call", "setup"): - continue - props = dict(rep.user_properties or []) - nodeid = getattr(rep, "nodeid", "?") - entry = PwkTestEntry( - nodeid=nodeid, - name=getattr(rep, "head_line", nodeid), - outcome=outcome_key, - duration_s=float(getattr(rep, "duration", 0.0) or 0.0), - page_role=list(props.get("pwk_page_role") or []), - role=props.get("pwk_role"), - phase=int(props.get("pwk_phase") or 0), - priority=props.get("pwk_priority"), - har_path=props.get("pwk_har"), - trace_path=props.get("pwk_trace"), - console_errors=int(props.get("pwk_console_errors") or 0), - page_errors=int(props.get("pwk_page_errors") or 0), - # Amazon Q Critical-3: skipped 時の longrepr は tuple 形式のため - # failed / error のときのみ str() 化する。他 outcome は None のまま。 - error_message=( - str(rep.longrepr) - if outcome_key in ("failed", "error") and rep.longrepr - else None - ), - ) - call_entries[nodeid] = entry - - # Step 2: teardown report の pwk_har / pwk_trace / body_check を call entry に merge する。 - # teardown 時点で context.close() 後の確定値や body_check の violation 集計が - # 積まれているため、call phase で未確定だった値をここで埋める。 - # pytest は setup/teardown の rep を stats[""] (空文字キー) に格納するため、 - # "" キーも含めて全キーを走査する。 - for outcome_key in terminalreporter.stats: - for rep in terminalreporter.stats[outcome_key]: - if getattr(rep, "when", None) != "teardown": - continue - nodeid = getattr(rep, "nodeid", "?") - if nodeid not in call_entries: - continue - entry = call_entries[nodeid] - props = dict(rep.user_properties or []) - if not entry.har_path and props.get("pwk_har"): - entry.har_path = props["pwk_har"] - if not entry.trace_path and props.get("pwk_trace"): - entry.trace_path = props["pwk_trace"] - if "pwk_body_check_violations" in props: - entry.body_check_violations = int( - props.get("pwk_body_check_violations") or 0 - ) - detail = props.get("pwk_body_check_detail") - if detail: - entry.body_check_detail = list(detail) - # body_check が teardown で pytest.fail を起こした場合、call phase - # は passed / xfailed / xpassed / skipped のまま teardown report - # のみ failed/error になる。call phase の本物の failure は上書き - # しないが、それ以外の outcome は teardown 失敗を反映させる - # (xfail テストでも teardown の body_check fail は実バグ扱い)。 - teardown_outcome = getattr(rep, "outcome", None) - if ( - teardown_outcome in ("failed", "error") - and entry.outcome not in ("failed", "error") - ): - entry.outcome = ( - "error" if teardown_outcome == "error" else "failed" - ) - if rep.longrepr and not entry.error_message: - entry.error_message = str(rep.longrepr) - - return list(call_entries.values()) - - -def pytest_terminal_summary(terminalreporter, exitstatus, config): - """``reports//report.md`` を生成する。 - - ``--pwk-out-dir`` 指定があればそこに、なければ ``pwk_out_dir`` fixture と - 同一の ``pytestconfig._pwk_out_dir`` キャッシュを参照する。 - キャッシュが無い場合 (pwk_out_dir fixture が一度も呼ばれていない) は - ``_resolve_out_dir`` 経由でセットする。これにより evidence と report.md の - 出力先が秒またぎでズレる問題を防ぐ (新規 Major 対応)。 - """ - # session 中で 1 件も test を回していない (collect-only など) は先に entries で判断。 - # xfailed / xpassed のみの session でも report を生成するため、 - # early return は _collect_entries() の結果で判断する (新規 Minor 対応)。 - entries = _collect_entries(terminalreporter) - if not entries: - return - - cached_cfg = getattr(config, "_pwk_config", None) - base_url = cached_cfg.base_url if cached_cfg is not None else None - title = ( - cached_cfg.report.title - if cached_cfg is not None - else "シナリオ E2E テスト 実施報告書" - ) - - # evidence.py の _resolve_out_dir と同一キャッシュ (_pwk_out_dir) を参照する。 - # これにより両者が独立に datetime.now() を呼んで別ディレクトリを作る問題を解消。 - from playwright_kit.fixtures.evidence import _resolve_out_dir - - out_dir = _resolve_out_dir(config) - out_dir.mkdir(parents=True, exist_ok=True) - - # Amazon Q Critical-4: xdist 並列実行時の session 開始時刻計算が不正確な問題を修正。 - # terminalreporter._sessionstarttime (pytest 内部 float) を優先利用し、 - # 無ければ従来の逐次実行前提の計算にフォールバックする。 - session_start_ts = getattr(terminalreporter, "_sessionstarttime", None) - if session_start_ts is not None: - started = _dt.datetime.fromtimestamp(session_start_ts) - else: - started = _dt.datetime.now() - _dt.timedelta( - seconds=sum(e.duration_s for e in entries) - ) - finished = _dt.datetime.now() - path = write_report( - entries, - out_dir=out_dir, - started_at=started, - finished_at=finished, - title=title, - base_url=base_url, - ) - terminalreporter.write_sep("-", "pwk report") - terminalreporter.write_line(f"report.md generated: {path}") - - # session 後の Drive アップロードに使うため pickle 不要な情報を保存 - config._pwk_report_path = path # type: ignore[attr-defined] - config._pwk_out_dir = out_dir # type: ignore[attr-defined] - - -def pytest_sessionfinish(session, exitstatus): - """``--pwk-drive-folder`` 指定時、生成済 report.md と evidence を Drive アップ。 - - ``upload_evidence.upload`` を直接呼ぶ。失敗時は警告のみで test 結果には影響しない。 - """ - folder_id: str | None = session.config.getoption( - "pwk_drive_folder", default=None - ) - if not folder_id: - return - - report_path: Path | None = getattr(session.config, "_pwk_report_path", None) - out_dir: Path | None = getattr(session.config, "_pwk_out_dir", None) - if report_path is None or out_dir is None: - return - - try: - # Amazon Q Critical-5: sys.path への動的 inject を廃止し、 - # playwright_kit.uploaders パッケージ経由で安全に import する。 - # scripts/upload_evidence.py は CLI スタンドアロン用途として残す。 - from playwright_kit.uploaders import upload, detect_kind - - # report.md は kind=any でアップ - if report_path.exists(): - upload( - report_path, kind="any", parent_folder_id=folder_id, public=False - ) - - # trace.zip / *.har / *.mp4 / body_check.jsonl を 1 階層下から拾い上げる - for sub in out_dir.iterdir(): - if not sub.is_dir(): - continue - for f in sub.iterdir(): - suffix = f.suffix - if suffix not in (".zip", ".har", ".mp4", ".webm", ".jsonl"): - continue - # detect_kind は body_check.jsonl 等の任意ファイルを未知の kind - # と扱うため、jsonl は ``any`` に固定する。 - kind = "any" if suffix == ".jsonl" else detect_kind(f) - upload( - f, kind=kind, parent_folder_id=folder_id, public=False - ) - except Exception as exc: # pragma: no cover - depends on Drive auth - import warnings - - warnings.warn( - f"[pwk] Drive upload 失敗 (session continues): {exc}", - stacklevel=1, - ) - - -def _try_load_config_silently(config: pytest.Config) -> Any | None: - """``--pwk-config`` 等から Config を試行ロードする。失敗時は None。""" - import os - from pathlib import Path - - raw_path: str | None = config.getoption("pwk_config", default=None) - if not raw_path: - env = os.environ.get("PWK_CONFIG") - if env: - raw_path = env - if not raw_path: - candidate = Path.cwd() / "scenario.config.yaml" - if candidate.exists(): - raw_path = str(candidate) - if not raw_path: - return None - - try: - from playwright_kit.config import Config - - return Config.load(Path(raw_path).resolve()) - except Exception as exc: # pragma: no cover - depends on user config - import warnings - - warnings.warn( - f"[pwk] config load 失敗 ({raw_path}): {exc}. " - "pwk_role_ fixture は動的登録されません。", - stacklevel=2, - ) - return None diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/pytest_report.py b/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/pytest_report.py deleted file mode 100644 index 2d67a89..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/pytest_report.py +++ /dev/null @@ -1,212 +0,0 @@ -"""pytest hook で集めた test result から Markdown レポートを生成する。 - -``pytest_terminal_summary`` から呼ばれ、``reports//report.md`` を生成する。 -``--pwk-drive-folder`` 指定時は Drive アップロードと URL 差し込みも担当。 -""" - -from __future__ import annotations - -import datetime as _dt -from dataclasses import dataclass, field -from pathlib import Path -from typing import Iterable - - -@dataclass -class PwkTestEntry: - """1 test 関数分のレポート用エントリ。 - - pytest の ``TestReport`` から要点だけを抽出して保持する - (``user_properties`` 経由で ``pwk_evidence`` の状態が紐付く)。 - """ - - nodeid: str - name: str - outcome: str # passed / failed / skipped / xfailed / xpassed / error - duration_s: float - page_role: list[str] = field(default_factory=list) - role: str | None = None - phase: int = 0 - priority: str | None = None - har_path: str | None = None - trace_path: str | None = None - console_errors: int = 0 - page_errors: int = 0 - error_message: str | None = None - # body_check (PHP / SSR エラー検出, v0.4.0) - body_check_violations: int = 0 - body_check_detail: list[dict] = field(default_factory=list) - - @property - def ok(self) -> bool: - return self.outcome in ("passed", "xfailed") - - @property - def status_label(self) -> str: - return { - "passed": "OK", - "failed": "FAIL", - "skipped": "SKIP", - "xfailed": "XFAIL", - "xpassed": "XPASS", - "error": "ERROR", - }.get(self.outcome, self.outcome.upper()) - - -def render_markdown( - entries: Iterable[PwkTestEntry], - *, - started_at: _dt.datetime, - finished_at: _dt.datetime, - title: str = "シナリオ E2E テスト 実施報告書", - base_url: str | None = None, -) -> str: - """test entries から Markdown 文字列を生成する。""" - entries_list = list(entries) - total = len(entries_list) - passed = sum(1 for e in entries_list if e.outcome == "passed") - failed = sum(1 for e in entries_list if e.outcome == "failed") - skipped = sum(1 for e in entries_list if e.outcome == "skipped") - errors = sum(1 for e in entries_list if e.outcome == "error") - xfailed = sum(1 for e in entries_list if e.outcome == "xfailed") - xpassed = sum(1 for e in entries_list if e.outcome == "xpassed") - duration = (finished_at - started_at).total_seconds() - # xfailed は期待通りの失敗なので OK 扱い (PwkTestEntry.ok と同じ方針) - # xpassed は意図せず pass したため注意喚起 (全PASS とはしない) - all_pass = total > 0 and (passed + xfailed) == total and xpassed == 0 - - lines: list[str] = [ - f"# {title}", - "", - f"- 実行開始: {started_at:%Y-%m-%d %H:%M:%S}", - f"- 実行終了: {finished_at:%Y-%m-%d %H:%M:%S}", - f"- 所要時間: {duration:.1f} 秒", - ] - if base_url: - lines.append(f"- 対象URL : {base_url}") - - # 集計サマリ行を構築 - extra_parts: list[str] = [] - if failed: - extra_parts.append(f"FAIL {failed}") - if skipped: - extra_parts.append(f"SKIP {skipped}") - if errors: - extra_parts.append(f"ERROR {errors}") - if xfailed: - extra_parts.append(f"XFAIL {xfailed}") - if xpassed: - extra_parts.append(f"XPASS {xpassed}") - # 全PASS でも xfailed / xpassed があれば内訳を明示する - if all_pass: - if extra_parts: - result_suffix = " (全PASS) / " + " / ".join(extra_parts) - else: - result_suffix = " (全PASS)" - else: - result_suffix = " / " + " / ".join(extra_parts) if extra_parts else "" - - lines.extend([ - f"- **結果: {passed}/{total} test PASS{result_suffix}**", - "", - "## サマリ", - "", - "| nodeid | role | page_role | status | duration | console.error | pageerror | body_check |", - "|---|---|---|---|---|---|---|---|", - ]) - - # phase / priority / nodeid の順でソート - sorted_entries = sorted( - entries_list, - key=lambda e: (e.phase, e.priority or "", e.nodeid), - ) - for e in sorted_entries: - page_role = ",".join(e.page_role) if e.page_role else "-" - lines.append( - f"| `{e.nodeid}` | {e.role or '-'} | {page_role} | " - f"{e.status_label} | {e.duration_s:.2f}s | " - f"{e.console_errors} | {e.page_errors} | " - f"{e.body_check_violations} |" - ) - - failures = [e for e in sorted_entries if e.outcome in ("failed", "error")] - if failures: - lines.extend(["", "## FAIL / ERROR の詳細", ""]) - for e in failures: - lines.append(f"### `{e.nodeid}` — {e.status_label}") - lines.append("") - if e.error_message: - lines.append("```") - lines.append(e.error_message[:2000]) - lines.append("```") - if e.trace_path: - lines.append(f"- trace: `{e.trace_path}`") - if e.har_path: - lines.append(f"- HAR: `{e.har_path}`") - lines.append("") - - body_check_hits = [e for e in sorted_entries if e.body_check_violations > 0] - if body_check_hits: - lines.extend(["", "## body_check 違反の詳細", ""]) - for e in body_check_hits: - lines.append( - f"### `{e.nodeid}` — body_check {e.body_check_violations} 件 " - f"({e.status_label})" - ) - lines.append("") - lines.append("| # | URL | category | pattern | snippet |") - lines.append("|---:|---|---|---|---|") - for i, v in enumerate(e.body_check_detail[:20], start=1): - url = _escape_table_cell(str(v.get("url", "?"))) - cat = _escape_table_cell(str(v.get("category", "?"))) - pat = _escape_table_cell(str(v.get("pattern", "?"))) - snippet = _escape_table_cell(str(v.get("snippet", ""))) - if len(snippet) > 200: - snippet = snippet[:200] + "..." - lines.append(f"| {i} | `{url}` | {cat} | `{pat}` | {snippet} |") - if len(e.body_check_detail) > 20: - lines.append( - f"\n_(表示は先頭 20 件のみ。詳細は ``body_check.jsonl`` を参照)_" - ) - lines.append("") - - return "\n".join(lines) + "\n" - - -def _escape_table_cell(text: str) -> str: - """Markdown 表のセル値を 1 行に潰してエスケープする。 - - 改行・タブが残ると行が分割されて表が崩れるので空白に置換する。 - ``|`` と backtick もエスケープして表構造とコード span を破壊しないようにする。 - """ - return ( - text.replace("\r\n", " ") - .replace("\r", " ") - .replace("\n", " ") - .replace("\t", " ") - .replace("|", "\\|") - .replace("`", "\\`") - ) - - -def write_report( - entries: Iterable[PwkTestEntry], - *, - out_dir: Path, - started_at: _dt.datetime, - finished_at: _dt.datetime, - title: str = "シナリオ E2E テスト 実施報告書", - base_url: str | None = None, -) -> Path: - """``out_dir/report.md`` を書き出してそのパスを返す。""" - out_dir.mkdir(parents=True, exist_ok=True) - md = render_markdown( - entries, - started_at=started_at, - finished_at=finished_at, - title=title, - base_url=base_url, - ) - path = out_dir / "report.md" - path.write_text(md, encoding="utf-8") - return path diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/uploaders/__init__.py b/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/uploaders/__init__.py deleted file mode 100644 index 283d2f0..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/uploaders/__init__.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Drive アップロード機能を playwright_kit パッケージから直接 import するためのラッパー。 - -scripts/upload_evidence.py の CLI スタンドアロン用途 (利用者が -``python upload_evidence.py ...`` で叩く) を壊さずに、pytest_sessionfinish から -安全に import できるようにする (Amazon Q Critical-5: sys.path 廃止)。 - -使い方 (pytest_plugin.py から): - from playwright_kit.uploaders import upload, detect_kind - -この module は google-auth スキルが存在しない環境でも import できる。 -実際のアップロード時のみ google-auth を必要とする (遅延 import)。 -""" - -from __future__ import annotations - -import os -import sys -from pathlib import Path -from urllib.parse import quote - - -_HERE = Path(__file__).resolve() -_CANDIDATES: tuple[Path, ...] = tuple( - Path(p).expanduser() - for p in ( - os.environ.get("GOOGLE_AUTH_SCRIPTS"), - "~/.claude/skills/google-auth/scripts", - "~/.codex/skills/google-auth/scripts", - str(_HERE.parent.parent.parent / "scripts"), - str(_HERE.parent.parent.parent.parent / "google-auth" / "scripts"), - ) - if p -) - - -def _ensure_google_auth_on_path() -> None: - for p in _CANDIDATES: - if p.is_dir(): - path = str(p) - if path not in sys.path: - sys.path.insert(0, path) - return - searched = "\n - ".join(str(p) for p in _CANDIDATES) - raise RuntimeError( - "Google Drive 連携には optional skill `google-auth` が必要です。\n" - "Codex 公開セットには同梱していないため、Drive 系コマンドを使う前に " - "`GOOGLE_AUTH_SCRIPTS` を google-auth/scripts へ設定してください。\n" - "例: export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf/skills/google-auth/scripts\n" - "検索した候補:\n - " - f"{searched}" - ) - - -# 拡張子 → kind の自動判定 -_EXT_KIND: dict[str, str] = { - ".zip": "trace", - ".har": "har", - ".mp4": "video", - ".webm": "video", -} - -_MIME_BY_KIND: dict[str, str] = { - "trace": "application/zip", - "har": "application/json", - "video": "video/mp4", - "any": "application/octet-stream", -} - -_MIME_BY_EXT: dict[str, str] = { - ".webm": "video/webm", - ".mp4": "video/mp4", - ".har": "application/json", - ".zip": "application/zip", -} - -ALLOWED_KINDS: frozenset[str] = frozenset(_MIME_BY_KIND) - - -def detect_kind(path: Path) -> str: - """拡張子から evidence kind を自動判定する。""" - return _EXT_KIND.get(path.suffix.lower(), "any") - - -def detect_mime(path: Path, kind: str) -> str: - """拡張子優先で MIME を決定し、未知拡張子は kind の既定値にフォールバック。""" - return _MIME_BY_EXT.get( - path.suffix.lower(), - _MIME_BY_KIND.get(kind, "application/octet-stream"), - ) - - -def upload( - file_path: Path, - *, - kind: str = "any", - parent_folder_id: str | None = None, - public: bool = False, -) -> dict: - """ファイルを Drive にアップして metadata + 補助 URL を返す。 - - ⚠️ trace.zip / HAR / video には DOM snapshot や入力痕跡・HTTP request body が含まれる。 - 既定では非公開アップロード。``public=True`` のときだけ anyone/read を付与する。 - ``parent_folder_id`` には **private folder** の ID を指定し、 - 共有相手を信頼できるメンバーに限定してください (Amazon Q Critical-5 / Codex Minor 8)。 - """ - if kind not in ALLOWED_KINDS: - raise ValueError( - f"未対応の kind: {kind!r} (allowed: {sorted(ALLOWED_KINDS)})" - ) - - _ensure_google_auth_on_path() - from google_auth import get_credentials # type: ignore # noqa: E402 - from googleapiclient.discovery import build # noqa: E402 - from googleapiclient.http import MediaFileUpload # noqa: E402 - - creds = get_credentials(["drive.file"]) - service = build("drive", "v3", credentials=creds) - - metadata: dict = {"name": file_path.name} - if parent_folder_id: - metadata["parents"] = [parent_folder_id] - media = MediaFileUpload( - str(file_path), mimetype=detect_mime(file_path, kind), - ) - f = service.files().create( - body=metadata, media_body=media, fields="id,webViewLink", - ).execute() - file_id = f["id"] - - if public: - service.permissions().create( - fileId=file_id, body={"type": "anyone", "role": "reader"}, - ).execute() - - direct_url: str | None = None - viewer_url: str | None = None - if public: - direct_url = f"https://drive.google.com/uc?export=download&id={file_id}" - if kind == "trace": - viewer_url = ( - f"https://trace.playwright.dev/?trace={quote(direct_url, safe='')}" - ) - - return { - "file_id": file_id, - "drive_view": f.get("webViewLink"), - "direct_download": direct_url, - "playwright_trace_viewer": viewer_url, - "is_public": public, - "kind": kind, - } diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/video.py b/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/video.py deleted file mode 100644 index e1a018e..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/video.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Playwright 録画 webm → Google Drive 互換 mp4 への変換。 - -Drive のストリーミング再生で「処理中」になりにくい設定: - - H.264 High profile / Level 4.0 / yuv420p / bt709 - - 30 fps CFR (Constant Frame Rate) - - 60 frames keyframe interval (2 sec at 30fps) - - AAC LC stereo 48kHz 128kbps (無音でも音声トラックは必須) - - +faststart で moov atom を先頭配置 → プログレッシブ再生 - -依存: imageio-ffmpeg (静的 ffmpeg バイナリ同梱の Python パッケージ) -""" - -from __future__ import annotations - -import subprocess -from pathlib import Path - - -def _ffmpeg_args(webm: Path, mp4: Path) -> list[str]: - """Drive 互換 mp4 への変換用 ffmpeg 引数を組み立てる。""" - return [ - "-y", - "-i", str(webm), - # 無音 AAC を映像と並行して生成 (一部プレイヤは音声トラック必須) - "-f", "lavfi", "-i", - "anullsrc=channel_layout=stereo:sample_rate=48000", - # --- 映像 --- - "-c:v", "libx264", - "-profile:v", "high", - "-level", "4.0", - "-pix_fmt", "yuv420p", - "-preset", "medium", - "-crf", "23", - "-r", "30", # 固定フレームレート - "-fps_mode", "cfr", # 旧 -vsync の後継 - "-g", "60", # キーフレーム間隔 2秒 - "-keyint_min", "30", - "-sc_threshold", "0", # シーン検出キーフレーム無効 - "-color_primaries", "bt709", - "-color_trc", "bt709", - "-colorspace", "bt709", - # --- 音声 (無音) --- - "-c:a", "aac", - "-b:a", "128k", - "-ar", "48000", - "-ac", "2", - "-shortest", # 映像終了で打ち切り - # --- ストリーミング向け --- - "-movflags", "+faststart", - str(mp4), - ] - - -def convert_webm_to_mp4(webm: Path, mp4: Path, *, timeout_sec: int = 300) -> Path | None: - """webm を Drive 互換 mp4 に変換。成功時は mp4 のパス、失敗時は None。""" - try: - import imageio_ffmpeg - ffmpeg = imageio_ffmpeg.get_ffmpeg_exe() - except Exception: - return None - - try: - subprocess.run( - [ffmpeg, *_ffmpeg_args(webm, mp4)], - check=True, capture_output=True, timeout=timeout_sec, - ) - except Exception: - return None - if not mp4.exists() or mp4.stat().st_size == 0: - return None - return mp4 diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/web_vitals.py b/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/web_vitals.py deleted file mode 100644 index 034247a..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/playwright_kit/web_vitals.py +++ /dev/null @@ -1,142 +0,0 @@ -"""runner 内蔵の Core Web Vitals (Web Vitals) 計測モジュール。 - -Core Web Vitals は Google が定義する「ユーザ体感パフォーマンス指標」群で、 -本モジュールは以下の 4 指標を計測する: - -- **LCP** (Largest Contentful Paint): 最大コンテンツ描画時間 (体感ロード速度) -- **CLS** (Cumulative Layout Shift): 累積レイアウトずれ量 (視覚的安定性) -- **TTFB** (Time To First Byte): 初バイト到達時間 (サーバ応答速さ) -- **longest_task** (Long Tasks API): 観測した最長タスクのミリ秒値 (応答性代理指標) - -`scripts/check_cwv.py` (CLI) はこのモジュールの薄いラッパで、runner からは -`measure_page(page)` を直接呼び出して `EvidenceCollectors.web_vitals_metrics` に -格納する。 - -page_role が `lp / list / dashboard` のとき runner が自動実行する -(config.web_vitals.auto_roles で上書き可能)。 - -注意: -- INP は実 user 入力ベースの指標であり Playwright で完全再現は不可能。 - `longest_task_ms` (50ms 超を 1 件以上検出) を「応答性低下の代理指標」として記録 - するが、INP の代わりにはならない。 -- 計測は page.evaluate で 5 秒間 PerformanceObserver を回す ため、testcase の - 最後 (全 step 実行後) に呼ぶこと。 -""" - -from __future__ import annotations - -from typing import Any - -from playwright.sync_api import Page - - -# web.dev 公式閾値 (75 percentile 基準) -THRESHOLDS: dict[str, dict[str, float]] = { - "lcp_ms": {"good": 2500, "poor": 4000}, - "cls": {"good": 0.1, "poor": 0.25}, - "ttfb_ms": {"good": 800, "poor": 1800}, - "longest_task_ms": {"good": 50, "poor": 200}, -} - -# page_role × Web Vitals 自動実行のデフォルト対象。インタラクション主体 (form / cart) は -# 過度な負荷になるため除外し、初回表示性能が UX に直結する role に限定する。 -DEFAULT_AUTO_ROLES: frozenset[str] = frozenset({"lp", "list", "dashboard", "search"}) - - -# Crit-1 (amazon-q-developer 指摘): JS 内の "5000" を `str.replace` する旧実装は -# JS 中に同じ数値が他出現すると壊れる fragile な置換だった。`page.evaluate` の -# arg 経由で `observeMs` を渡し、JS 関数引数として明示的に bind する。 -_PERF_JS = r""" -(observeMs) => new Promise((resolve) => { - const result = {lcp: null, cls: 0, longest_task: 0, ttfb: null}; - - try { - const lcpObs = new PerformanceObserver((list) => { - const entries = list.getEntries(); - const last = entries.at(-1); - if (last) result.lcp = last.startTime; - }); - lcpObs.observe({type: 'largest-contentful-paint', buffered: true}); - } catch (e) {} - - try { - const clsObs = new PerformanceObserver((list) => { - for (const entry of list.getEntries()) { - if (!entry.hadRecentInput) { - result.cls += entry.value; - } - } - }); - clsObs.observe({type: 'layout-shift', buffered: true}); - } catch (e) {} - - try { - const ltObs = new PerformanceObserver((list) => { - for (const entry of list.getEntries()) { - if (entry.duration > result.longest_task) { - result.longest_task = entry.duration; - } - } - }); - ltObs.observe({type: 'longtask', buffered: true}); - } catch (e) {} - - try { - const nav = performance.getEntriesByType('navigation')[0]; - if (nav) result.ttfb = nav.responseStart - nav.requestStart; - } catch (e) {} - - setTimeout(() => resolve(result), observeMs); -}); -""" - - -def measure_page(page: Page, *, observe_ms: int = 5000) -> dict[str, float]: - """既にロード済みの Page で Core Web Vitals を `observe_ms` ミリ秒観察し、metrics dict を返す。 - - Returns: `{"lcp_ms": float, "cls": float, "ttfb_ms": float, "longest_task_ms": float}` - 計測失敗した metric は dict から除外される。 - """ - try: - raw: dict[str, Any] = page.evaluate(_PERF_JS, int(observe_ms)) - except Exception: - return {} - - out: dict[str, float] = {} - if raw.get("lcp") is not None: - out["lcp_ms"] = float(raw["lcp"]) - if raw.get("cls") is not None: - out["cls"] = float(raw["cls"]) - if raw.get("ttfb") is not None: - out["ttfb_ms"] = float(raw["ttfb"]) - if raw.get("longest_task") is not None: - out["longest_task_ms"] = float(raw["longest_task"]) - return out - - -def judge(metric: str, value: float | None) -> str: - """値を `good` / `needs-improvement` / `poor` / `unknown` に分類する。""" - if value is None: - return "unknown" - th = THRESHOLDS.get(metric) - if not th: - return "unknown" - if value <= th["good"]: - return "good" - if value <= th["poor"]: - return "needs-improvement" - return "poor" - - -def passed(metrics: dict[str, float]) -> bool: - """すべての metric が good または needs-improvement なら True (poor が 1 件でも あれば False)。""" - return all(judge(k, v) != "poor" for k, v in metrics.items()) - - -def should_auto_measure( - page_roles: list[str], - *, - auto_roles: frozenset[str] = DEFAULT_AUTO_ROLES, -) -> bool: - """testcase の page_role に基づき Web Vitals を自動計測すべきか判定する。""" - return any(r in auto_roles for r in page_roles) diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/pyproject.toml b/plugins/ndf/skills-codex/playwright-kit-ops/pyproject.toml deleted file mode 100644 index 14e535b..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/pyproject.toml +++ /dev/null @@ -1,50 +0,0 @@ -[project] -name = "playwright_kit" -version = "0.5.0" -description = "pytest-playwright 上で動く Web シナリオ E2E テストの理論ベース実施フレームワーク (Claude Code skill)。v0.3.0: 自前 YAML DSL を全廃し、pytest plugin / fixture / marker / accessibility autouse / web vitals autouse / overlay / report.md / Google Drive 連携を提供。v0.4.0: PHP / SSR がフロント漏れさせる Fatal error / STRICT: 等を検出する body_check autouse を default 有効で復活 (PHP 系パターン内蔵)。v0.5.0: パッケージ名を playwright_kit に rename し、利用者プロジェクト直下で自己完結 (Skill 非依存) に動作させる構成へ移行。" -requires-python = ">=3.11" -dependencies = [ - "playwright>=1.50,<2.0", - "pyyaml>=6.0", - # webm → mp4 変換用。Playwright 録画は webm のみ生成するが、 - # Google Drive のプレビュアでは mp4 (H.264) の方が安定するため変換する。 - "imageio-ffmpeg>=0.4", - # v0.3.0: pure pytest-playwright 移行 - # pytest 本体と pytest-playwright fixture (page / context / browser_context_args)、 - # pytest-xdist による並列実行を main 依存として要求する。 - "pytest>=8.0", - "pytest-playwright>=0.5", - "pytest-xdist>=3.0", - # accessibility autouse は scenario.config.yaml で default 有効。 - # runtime テンプレート (templates/pyproject.toml.runtime) と揃えて main dep に含める。 - "axe-playwright-python>=0.1.4", -] - -[project.optional-dependencies] -# Drive 連携: scripts/trace_link.py / build_gdoc_with_drive_links.py 等が使用 -drive = [ - "google-api-python-client>=2.0", - "google-auth>=2.0", - "google-auth-oauthlib>=1.0", -] -# 開発用: pure function の単体テスト (uv run --extra dev pytest) -# pytest 本体は main deps に移行済 (v0.3.0)。dev には開発時のみの拡張を入れる余地。 -dev = [ - "pytest>=8.0", -] - -[project.entry-points."pytest11"] -# pytest が auto-discover する plugin entry-point。 -# ``uv pip install -e .`` 後は ``pytest`` 起動だけで playwright_kit plugin が有効になる。 -playwright-kit = "playwright_kit.pytest_plugin" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["playwright_kit"] - -[tool.pytest.ini_options] -testpaths = ["tests"] -addopts = "-q" diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/scripts/_drive_auth.py b/plugins/ndf/skills-codex/playwright-kit-ops/scripts/_drive_auth.py deleted file mode 100644 index 647ab52..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/scripts/_drive_auth.py +++ /dev/null @@ -1,57 +0,0 @@ -"""google-auth スキル経由で Drive API クレデンシャルを取得する共通ヘルパ。 - -3 つの uploader スクリプト (gdrive_upload_dir / build_gdoc_with_drive_links / -upload_md_as_gdoc) はいずれも同じ手順で `google_auth.get_credentials()` を -sys.path から発見する。本モジュールにロジックを集約する。 - -Drive 連携は optional dependency。`GOOGLE_AUTH_SCRIPTS` 環境変数が設定されて -いればそれを使い、それ以外は標準インストール先と sibling の google-auth -スキルを探す。Codex 公開セットには google-auth を含めないため、Codex で -Drive 系コマンドを使う場合は `GOOGLE_AUTH_SCRIPTS` を明示する。 -""" - -from __future__ import annotations - -import os -import sys -from pathlib import Path - - -_HERE = Path(__file__).resolve() -_CANDIDATES: tuple[Path, ...] = tuple( - Path(p).expanduser() - for p in ( - os.environ.get("GOOGLE_AUTH_SCRIPTS"), - "~/.claude/skills/google-auth/scripts", - "~/.codex/skills/google-auth/scripts", - str(_HERE.parent.parent.parent / "google-auth" / "scripts"), - ) - if p -) - - -def _ensure_google_auth_on_path() -> None: - """`from google_auth import get_credentials` できるよう sys.path を整える。""" - for p in _CANDIDATES: - if p.is_dir(): - path = str(p) - if path not in sys.path: - sys.path.insert(0, path) - return - searched = "\n - ".join(str(p) for p in _CANDIDATES) - raise RuntimeError( - "Google Drive 連携には optional skill `google-auth` が必要です。\n" - "Codex 公開セットには同梱していないため、Drive 系コマンドを使う前に " - "`GOOGLE_AUTH_SCRIPTS` を google-auth/scripts へ設定してください。\n" - "例: export GOOGLE_AUTH_SCRIPTS=/path/to/plugins/ndf/skills/google-auth/scripts\n" - "検索した候補:\n - " - f"{searched}" - ) - - -def drive_service(scopes: list[str]): - """認証済み Drive API v3 service を返す。""" - _ensure_google_auth_on_path() - from google_auth import get_credentials # type: ignore - from googleapiclient.discovery import build - return build("drive", "v3", credentials=get_credentials(scopes)) diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/scripts/build_gdoc_with_drive_links.py b/plugins/ndf/skills-codex/playwright-kit-ops/scripts/build_gdoc_with_drive_links.py deleted file mode 100644 index 47990c6..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/scripts/build_gdoc_with_drive_links.py +++ /dev/null @@ -1,126 +0,0 @@ -"""report.md の相対リンクを Google Drive URL に置換し、Google Docs として再アップロードする。 - -事前に対象ディレクトリを Drive にアップロード済みである前提。 -このスクリプトは: - 1. Drive 上の フォルダから {相対パス: file_id} mapping を構築 - 2. report.md 中の `(./TC-XX/foo.ext)` 形式リンクを Drive URL に書き換え - 3. text/markdown としてアップロードし mimeType=Google Docs 指定で自動変換 -""" - -from __future__ import annotations - -import argparse -import os -import re -import sys -from pathlib import Path - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _drive_auth import drive_service # noqa: E402 - -from googleapiclient.http import MediaFileUpload # noqa: E402 - -SCOPES = ["drive.file", "drive.readonly"] -FOLDER_MIME = "application/vnd.google-apps.folder" -DOC_MIME = "application/vnd.google-apps.document" -LINK_PATTERN = re.compile(r"\(\.?\/?(TC-[\w-]+/[^\)\s]+)\)") - - -def list_folder_files(service, folder_id: str, prefix: str = "") -> dict[str, str]: - """folder_id 配下のファイルを再帰的に列挙し、{相対パス: file_id} を返す。""" - out: dict[str, str] = {} - page_token: str | None = None - while True: - resp = service.files().list( - q=f"'{folder_id}' in parents and trashed=false", - fields="nextPageToken, files(id,name,mimeType)", - pageSize=200, pageToken=page_token, - supportsAllDrives=True, includeItemsFromAllDrives=True, - ).execute() - for f in resp.get("files", []): - rel = f"{prefix}/{f['name']}".lstrip("/") - if f["mimeType"] == FOLDER_MIME: - out.update(list_folder_files(service, f["id"], rel)) - else: - out[rel] = f["id"] - page_token = resp.get("nextPageToken") - if not page_token: - return out - - -def find_run_folder_id(service, parent_id: str, run_id: str) -> str: - """parent 配下の run_id 名フォルダの ID を返す。なければ例外。""" - files = service.files().list( - q=( - f"'{parent_id}' in parents and name='{run_id}' " - f"and mimeType='{FOLDER_MIME}' and trashed=false" - ), - fields="files(id,name)", - supportsAllDrives=True, includeItemsFromAllDrives=True, - ).execute().get("files", []) - if not files: - raise SystemExit(f"ERROR: run-id folder '{run_id}' not found under {parent_id}") - return files[0]["id"] - - -def _drive_url_for(rel: str, fid: str) -> str: - # PNG は uc?id (画像直接表示)、その他 (動画/zip/etc) は file/d//view - if rel.endswith(".png"): - return f"https://drive.google.com/uc?id={fid}" - return f"https://drive.google.com/file/d/{fid}/view" - - -def rewrite_links(md: str, mapping: dict[str, str]) -> tuple[str, int]: - """`(./TC-XX/foo.ext)` 形式リンクを Drive URL に置換し、(新md, 置換件数) を返す。""" - replaced = 0 - - def rep(m: re.Match[str]) -> str: - nonlocal replaced - rel = m.group(1) - fid = mapping.get(rel) - if fid is None: - return m.group(0) # 未マップは原文のまま - replaced += 1 - return f"({_drive_url_for(rel, fid)})" - - return LINK_PATTERN.sub(rep, md), replaced - - -def main() -> int: - p = argparse.ArgumentParser() - p.add_argument("--md", required=True, type=Path) - p.add_argument("--folder", required=True, - help="Drive folder containing the run-id subfolder") - p.add_argument("--run-id", required=True, - help="Run id subfolder name (= local report dir name)") - p.add_argument("--name", required=True) - args = p.parse_args() - - service = drive_service(SCOPES) - run_folder_id = find_run_folder_id(service, args.folder, args.run_id) - print(f"run folder: {run_folder_id}") - - mapping = list_folder_files(service, run_folder_id) - print(f"Indexed {len(mapping)} files") - - md_new, replaced = rewrite_links(args.md.read_text(encoding="utf-8"), mapping) - print(f"Replaced links: {replaced} matches") - - tmp_md = Path("/tmp/report_with_drive_links.md") - tmp_md.write_text(md_new, encoding="utf-8") - - media = MediaFileUpload(str(tmp_md), mimetype="text/markdown", resumable=True) - file = service.files().create( - body={"name": args.name, "mimeType": DOC_MIME, "parents": [args.folder]}, - media_body=media, - fields="id,name,webViewLink,mimeType", - supportsAllDrives=True, - ).execute() - print(f"OK: created {file['name']} ({file['mimeType']})") - print(f" id: {file['id']}") - print(f" url: {file['webViewLink']}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/scripts/check_cwv.py b/plugins/ndf/skills-codex/playwright-kit-ops/scripts/check_cwv.py deleted file mode 100644 index 679554d..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/scripts/check_cwv.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Core Web Vitals (LCP/CLS/TTFB/longest_task) を 1 URL に対して計測する CLI。 - -`playwright_kit.web_vitals` モジュールの薄いラッパ。runner は testcase 内蔵で同 module -を呼ぶため、本 CLI は外部 URL の単発計測専用。 - -Usage: - python check_cwv.py --url https://example.com - python check_cwv.py --url-list urls.txt --output web_vitals.json - python check_cwv.py --url https://example.com --device "Pixel 5" -""" - -from __future__ import annotations - -import argparse -import json -import sys -from pathlib import Path -from typing import Any - -from playwright.sync_api import sync_playwright - -_SKILL_ROOT = Path(__file__).resolve().parent.parent -if str(_SKILL_ROOT) not in sys.path: - sys.path.insert(0, str(_SKILL_ROOT)) - -from playwright_kit.web_vitals import THRESHOLDS, judge, measure_page # noqa: E402 - - -def measure( - url: str, - *, - storage_state: str | None = None, - device_name: str | None = None, - timeout_ms: int = 30_000, - headless: bool = True, - observe_ms: int = 5000, -) -> dict[str, Any]: - with sync_playwright() as p: - browser = p.chromium.launch(headless=headless) - ctx_kwargs: dict[str, Any] = {"ignore_https_errors": True} - if storage_state: - ctx_kwargs["storage_state"] = storage_state - if device_name: - device = p.devices.get(device_name) - if not device: - browser.close() - return {"url": url, "error": f"unknown device: {device_name}"} - ctx_kwargs.update(device) - - context = browser.new_context(**ctx_kwargs) - page = context.new_page() - try: - page.goto(url, timeout=timeout_ms, wait_until="domcontentloaded") - except Exception as exc: - browser.close() - return {"url": url, "error": str(exc)} - - metrics = measure_page(page, observe_ms=observe_ms) - browser.close() - return { - "url": url, - "device": device_name or "desktop", - "metrics": metrics, - "judgement": {k: judge(k, v) for k, v in metrics.items()}, - "thresholds": THRESHOLDS, - } - - -def main() -> int: - parser = argparse.ArgumentParser(description="Core Web Vitals を計測") - parser.add_argument("--url", help="計測対象 URL") - parser.add_argument("--url-list", type=Path, help="URL を 1 行 1 件で書いたファイル") - parser.add_argument("--storage-state", default=None) - parser.add_argument("--device", default=None, - help="Playwright device 名 (例: 'Pixel 5')") - parser.add_argument("--observe-ms", type=int, default=5000, - help="PerformanceObserver 観測時間 (ms)") - parser.add_argument("--output", type=Path, default=None) - parser.add_argument("--fail-on-poor", action="store_true", - help="poor 判定 1 件以上で exit 1") - parser.add_argument("--headed", action="store_true") - args = parser.parse_args() - - if not args.url and not args.url_list: - parser.error("--url または --url-list が必要です") - - urls = ( - [line.strip() for line in args.url_list.read_text().splitlines() if line.strip()] - if args.url_list else [args.url] - ) - - results = [ - measure(u, storage_state=args.storage_state, device_name=args.device, - headless=not args.headed, observe_ms=args.observe_ms) - for u in urls - ] - - text = json.dumps(results, indent=2, ensure_ascii=False) - if args.output: - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text(text, encoding="utf-8") - print(f"OK: web_vitals → {args.output}", file=sys.stderr) - else: - sys.stdout.write(text + "\n") - - has_poor = any( - any(v == "poor" for v in r.get("judgement", {}).values()) for r in results - ) - return 1 if args.fail_on_poor and has_poor else 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/plugins/ndf/skills-codex/playwright-kit-ops/scripts/classify_page_role.py b/plugins/ndf/skills-codex/playwright-kit-ops/scripts/classify_page_role.py deleted file mode 100644 index 2943843..0000000 --- a/plugins/ndf/skills-codex/playwright-kit-ops/scripts/classify_page_role.py +++ /dev/null @@ -1,238 +0,0 @@ -"""URL の page role を DOM の (implicit + explicit) role 集計から判定する。 - -実装はブラウザ DOM クエリで `