diff --git a/.github/workflows/runtime-plugin-authenticated-smoke.yml b/.github/workflows/runtime-plugin-authenticated-smoke.yml index 15e89f833..25acb945a 100644 --- a/.github/workflows/runtime-plugin-authenticated-smoke.yml +++ b/.github/workflows/runtime-plugin-authenticated-smoke.yml @@ -12,8 +12,6 @@ on: - all - claude - codex - - kiro - - agy jobs: authenticated-smoke: diff --git a/.github/workflows/runtime-plugin-smoke-latest.yml b/.github/workflows/runtime-plugin-smoke-latest.yml index 4495e9fe0..3b53df3e5 100644 --- a/.github/workflows/runtime-plugin-smoke-latest.yml +++ b/.github/workflows/runtime-plugin-smoke-latest.yml @@ -31,7 +31,7 @@ jobs: strategy: fail-fast: false matrix: - runtime: [claude, codex, kiro, agy] + runtime: [claude, codex] env: RUNTIME: ${{ matrix.runtime }} LATEST_VERSIONS: tmp/runtime-smoke-latest/cli-versions.env diff --git a/.github/workflows/runtime-plugin-smoke.yml b/.github/workflows/runtime-plugin-smoke.yml index ff951c3ad..46bdad413 100644 --- a/.github/workflows/runtime-plugin-smoke.yml +++ b/.github/workflows/runtime-plugin-smoke.yml @@ -52,7 +52,7 @@ jobs: strategy: fail-fast: false matrix: - runtime: [claude, codex, kiro, agy] + runtime: [claude, codex] env: SKIP_HEAVY: ${{ needs.ci-scope.outputs.skip == 'true' }} steps: diff --git a/.ndf/pace.json b/.ndf/pace.json index 06976c088..8751d905f 100644 --- a/.ndf/pace.json +++ b/.ndf/pace.json @@ -3,12 +3,12 @@ "fast": { "enabled": true, "modes": ["light", "standard", "legacy-refactor"], - "verify": "python3 plugins/ndf/scripts/release-verification-steps.py verify-install --ref develop --runtimes claude,codex,kiro" + "verify": "python3 plugins/ndf/scripts/release-verification-steps.py verify-install --ref develop --runtimes claude,codex" }, "auto": { "enabled": true, "modes": ["light", "standard", "legacy-refactor"], - "verify": "python3 plugins/ndf/scripts/release-verification-steps.py verify-install --ref develop --runtimes claude,codex,kiro" + "verify": "python3 plugins/ndf/scripts/release-verification-steps.py verify-install --ref develop --runtimes claude,codex" }, "areas": [ {"name": "駆動", "common": true, diff --git a/.ndf/project.json b/.ndf/project.json index 8e2327cb4..d27377eb6 100644 --- a/.ndf/project.json +++ b/.ndf/project.json @@ -192,10 +192,8 @@ "required-checks-check", "runtime-plugin-build-check", "runtime-plugin-validate", - "runtime-smoke (agy)", "runtime-smoke (claude)", "runtime-smoke (codex)", - "runtime-smoke (kiro)", "script-structure-check", "skill-frontmatter-check", "skill-repo-assumptions-check", @@ -209,7 +207,7 @@ }, "delivery": [ { - "target": "ndf プラグイン(Claude Code・Codex・Kiro)", + "target": "ndf プラグイン(Claude Code・Codex)", "kind": "manual", "trigger": "release の工程で版を上げ、main へマージしてタグ ndf--v<版> を打つ", "branch": "main", diff --git a/.ndf/supervise.json b/.ndf/supervise.json index f2ac08a8f..5a6789896 100644 --- a/.ndf/supervise.json +++ b/.ndf/supervise.json @@ -23,6 +23,6 @@ "release": { "form": "package-plugin", "plugin": "ndf", - "runtimes": ["claude", "codex", "kiro"] + "runtimes": ["claude", "codex"] } } diff --git a/AGENTS.md b/AGENTS.md index afdd75dfc..e4517f7c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## プロジェクト概要 -**Claude Code / Codex / Kiro CLI / agy 向けプラグインマーケットプレイス**の開発プロジェクトです。チーム全体で AI 開発ツールの導入を加速するための事前設定されたプラグインを提供します。 +**Claude Code / Codex 向けプラグインマーケットプレイス**の開発プロジェクトです。Kiro CLI と agy へは配らず、cross-review / cross-refactoring の相手役として起動するだけです。チーム全体で AI 開発ツールの導入を加速するための事前設定されたプラグインを提供します。 **リポジトリ**: https://github.com/devbasex/ai-plugins @@ -116,15 +116,15 @@ ai-plugins/ ├── .claude-plugin/ │ └── marketplace.json # マーケットプレイス定義(必須) ├── plugins/ -│ ├── ndf/ # NDF(4ランタイム共通の単一ディレクトリ) -│ ├── playwright-kit/ # playwright-kit(3ランタイム共通の単一ディレクトリ) +│ ├── ndf/ # NDF(Claude Code・Codex 共通の単一ディレクトリ) +│ ├── playwright-kit/ # playwright-kit(Claude Code・Codex 共通の単一ディレクトリ) │ └── mcp/ -│ └── mcp-*/ # MCPプラグイン10個(3ランタイム共通) +│ └── mcp-*/ # MCPプラグイン10個(Claude Code・Codex 共通) ├── docs/ # リポジトリ知識 ├── factory/ # Web サービス(Neural Development Factory)。プラグインと分け、自分の LICENSE を持つ ├── AGENTS.md # 共通エントリポイント ├── CLAUDE.md # Claude Code固有設定 -├── KIRO.md # Kiro CLI固有設定 +├── KIRO.md # 相手役として起動される Kiro CLI の説明 └── README.md # プロジェクト説明 ``` @@ -144,16 +144,15 @@ ai-plugins/ | [docs/articles/](docs/articles/README.md) | 社外ブログへ載せる記事の正本。**書いた日時点の記録で、以後の構成変更には追随しない** | | [docs/claude-code-skills-survey/](docs/claude-code-skills-survey/01-findings.md) | Claude Code Skills調査レポート(3 本) | | [docs/development-history/](docs/development-history/) | 2026-09-02 までの開発履歴と知見。**以降の振り返りは、起点の issue か Pull Request のコメントに残す**(`/ndf:retrospective`) | -| [plugins/ndf/README.md](plugins/ndf/README.md) | NDFプラグイン(4ランタイム共通) | +| [plugins/ndf/README.md](plugins/ndf/README.md) | NDFプラグイン(Claude Code・Codex 共通) | ## NDFプラグインについて -**NDFプラグイン**は、このマーケットプレイスの主要プラグインです。plugin 名は全ランタイムで `ndf` を維持し、配布物は `plugins/ndf/` の1ディレクトリにまとまっています。 -- Skill の実体は `plugins/ndf/skills/` の1箇所。配布先は `plugins/ndf/manifests/*-skills.txt` が決める +**NDFプラグイン**は、このマーケットプレイスの主要プラグインです。plugin 名は Claude Code・Codex のどちらでも `ndf` を維持し、配布物は `plugins/ndf/` の1ディレクトリにまとまっています。 +- Skill の実体は `plugins/ndf/skills/` の1箇所。配布先は Claude Code と Codex の 2 つで、配る Skill は `plugins/ndf/manifests/{claude,codex}-skills.txt` が決める - Claude Code版は専門サブエージェントと実行主体(supervisor・worker)の定義(`plugins/ndf/agents/`)、公開Skills、PreToolUse/SessionStart/Stopフックを提供 - Codex版は Codex向け公開Skillsと任意Slack通知hookを提供 -- Kiro版は `plugins/ndf/dev.kiro/install.sh` で `.kiro/skills/`、`.kiro/steering/ndf-policies.md`、`.kiro/agents/ndf.json` を生成 -- agy版は `plugins/ndf/dev.agy/` を `agy plugin install` で導入し、公開Skills・エージェント定義・PreToolUse/PreInvocationフックを提供 +- Kiro CLI と agy へは何も配らない。cross-review のレビュー担当、cross-refactoring の提案・実装の担当(相手役)として、ホスト(Claude Code・Codex)から CLI プロセスで起動する。相手役が読む手順書はホストの導入先から担当の worktree へ写す(起動の形の正本は `plugins/ndf/skills/external-ai/references/cli-kiro.md`・`cli-agy.md`) - 外部AI委譲は `/ndf:external-ai` skill と `corder` エージェント経由で Codex / agy を呼び出す 詳細は各 runtime README と `docs/ndf-plugin-reference.md` を参照。 diff --git a/KIRO.md b/KIRO.md index f15c72bb7..1faad537c 100644 --- a/KIRO.md +++ b/KIRO.md @@ -1,57 +1,17 @@ -# AI Plugins - Kiro CLI開発ガイドライン +# AI Plugins - Kiro CLI の扱い 共通の開発ガイドラインは **@AGENTS.md** を参照してください。 -このファイルにはKiro CLI固有の設定のみを記載します。 +このファイルには Kiro CLI に固有のことだけを書きます。 -## NDFプラグインのセットアップ +## 配布しない。相手役として起動する -### 前提条件 -- Kiro CLI がインストール済み -- Node.js(Slack通知を使う場合) -- Codex CLI(`/ndf:external-ai` skill で外部AI委譲を使う場合、または `--with-codex` で Kiro に Codex MCP サーバ設定を生成する場合): `npm install -g @openai/codex` +NDF とマーケットプレイスのプラグインは Kiro CLI へ配りません(配布先は Claude Code と Codex)。Kiro CLI は、cross-review のレビュー担当と cross-refactoring の提案・実装の担当(相手役)として、ホスト(Claude Code・Codex)から CLI プロセスで起動されるだけです。 -### インストール +- 起動の形(`kiro-cli chat --no-interactive --trust-all-tools …`)の正本は `plugins/ndf/skills/external-ai/references/cli-kiro.md` にあります +- 相手役に `--agent` は渡しません。相手役が読む手順書は、ホストの導入先から担当の worktree の `.kiro/skills/` へ写します(`plugins/ndf/skills/cross-refactoring/scripts/prepare-worktrees.sh`) +- 相手役に使えるかは `.ndf/runtimes.json` と `kiro-cli` の認証で決まります -```bash -# 基本(Skills + agentSpawn / userPromptSubmit フック) -bash plugins/ndf/dev.kiro/install.sh +## 既に Kiro CLI へ NDF を入れた利用者 -# Slack通知も有効化 -bash plugins/ndf/dev.kiro/install.sh --with-slack - -# 全部入り(Slack通知 + Kiro 側 Codex MCP 設定生成) -# 注: NDF の外部 AI 委譲は /ndf:external-ai skill が CLI を直接実行し、Codex MCP に依存しない。 -# --with-codex は Kiro セッションで `mcp__codex__*` を直接呼びたい場合だけ付ける -# (Codex MCP サーバの設定と .kiro/prompts/codex.md を追加する)。 -bash plugins/ndf/dev.kiro/install.sh --with-slack --with-codex -``` - -インストーラーは `plugins/ndf/skills/` から `.kiro/skills/` への symlink、`.kiro/steering/ndf-policies.md`、`.kiro/agents/ndf.json` を生成します。リンクする Skill は `plugins/ndf/manifests/kiro-skills.txt` が決めます。`ndf-policies` だけは steering の生成元として使い、`.kiro/skills/` へは symlink しません(Skill と steering の二重注入を避けるため)。 - -`.kiro/agents/ndf.json` の `hooks` には worktree 運用の 2 つが入ります。 - -| hook | 実行するもの | 起きること | -|---|---|---| -| `agentSpawn` | `scripts/worktree-session.sh` | メインディレクトリに残った未コミット変更の提示。ブランチの追従は既定では行わず、`.ndf/worktree.json` の `follow_branch: true` で有効にする | -| `userPromptSubmit` | `scripts/worktree-guard.sh` | worktree で作業する旨の案内 | - -Kiro CLI では tool 実行前の hook を使いません。この事象でモデルへ案内を渡す手段が終了コード 2 に限られ、それが tool の実行を拒否するためです。拒否しない方針のもとでは置けないため、パスを見ない案内をプロンプト送信時の hook が担います。 - -**どちらもリポジトリ側に `.ndf/worktree.json` があるときだけ動きます。** 無ければ何も出力せず終了コード 0 で終わります。書き方は `plugins/ndf/README.md` の「Hooks」節にあります。 - -### Slack通知の設定(オプション) - -`.env` に以下を設定: -``` -SLACK_CHANNEL_ID=C0123456789 -SLACK_BOT_TOKEN=xoxb-... -SLACK_USER_MENTION=<@U0123456789> -``` - -### 利用方法 - -- スキルはdescriptionのキーワードに基づいて自動参照されます -- ワークフロー系スキル(pr, fix, review等)は手動で指示してください - - 例: 「prスキルの手順に従ってPRを作成して」 -- `plugins/ndf/manifests/kiro-skills.txt` に skill が追加された場合、再度インストーラーを実行してください +配布を終えた版より前に `plugins/ndf/dev.kiro/install.sh` で入れた手元の残し方と消し方は、`plugins/ndf/README.md` の「agy・Kiro CLI への配布の終了」にあります。 diff --git a/README.md b/README.md index cd8bfd2a0..91029a4b3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # AI Plugins -Claude Code / Codex / Kiro CLI / agy で使うプラグインを配るマーケットプレイスです。 +Claude Code / Codex で使うプラグインを配るマーケットプレイスです。 入れると、Pull Request の作成・レビュー・修正といった開発の作業を、手順どおりに AI へ任せられます。 よく使う外部サービス(Notion・BigQuery・データベースなど)へつなぐ設定も入ります。 @@ -22,29 +22,10 @@ codex plugin marketplace add https://github.com/devbasex/ai-plugins codex plugin add ndf@ai-plugins ``` -### Kiro CLI +### Kiro CLI と agy -Kiro CLI はマーケットプレイスから入れられません。clone したディレクトリでインストーラーを実行します。 - -```bash -git clone https://github.com/devbasex/ai-plugins.git -cd ai-plugins -bash plugins/ndf/dev.kiro/install.sh -``` - -起動の方法は [plugins/ndf/README.md の「Kiro CLI」](./plugins/ndf/README.md#kiro-cli) にあります。 - -### agy - -agy もマーケットプレイスから入れられません。clone したディレクトリから導入します。 - -```bash -git clone https://github.com/devbasex/ai-plugins.git -cd ai-plugins -agy plugin install plugins/ndf/dev.agy -``` - -hook を効かせる手順は [plugins/ndf/README.md の「agy」](./plugins/ndf/README.md#agy) にあります。 +Kiro CLI と agy へはプラグインを配りません。cross-review と cross-refactoring が、別の AI の意見を得る相手役として CLI を起動するだけです(Claude Code か Codex に ndf を入れれば使えます)。 +前の版で入れた手元の扱いは [plugins/ndf/README.md の「agy・Kiro CLI への配布の終了」](./plugins/ndf/README.md#agykiro-cli-への配布の終了) にあります。 更新の方法と Slack 通知の設定は [plugins/ndf/README.md](./plugins/ndf/README.md) にあります。 @@ -67,10 +48,10 @@ Claude Code では `/ndf:` と打って呼びます。**Skill** は | プラグイン名 | バージョン | できること | 詳細 | | --- | --- | --- | --- | -| **ndf** | 10.27.4 | 開発の作業手順(Skill)と専門のサブエージェント。4 つの AI ツールへ同じ名前で入る | [README](./plugins/ndf/README.md) | +| **ndf** | 10.27.4 | 開発の作業手順(Skill)と専門のサブエージェント。Claude Code と Codex へ同じ名前で入る | [README](./plugins/ndf/README.md) | | **playwright-kit** | 3.0.2 | Playwright による E2E テストの計画・スクリプト作成・証跡のレポート | [README](./plugins/playwright-kit/README.md) | -外部サービスへつなぐ MCP サーバーの設定のプラグインが 10 個あります(Claude Code / Codex / Kiro CLI 向け)。 +外部サービスへつなぐ MCP サーバーの設定のプラグインが 10 個あります(Claude Code / Codex 向け)。 使うものだけを入れます(Claude Code なら `/plugin install <プラグイン名>@ai-plugins`)。 | プラグイン名 | つなぐ先 | @@ -89,7 +70,7 @@ Claude Code では `/ndf:` と打って呼びます。**Skill** は ### NDF プラグインの Skill の一覧 **NDFプラグイン v10.27.4** に入る Skill の数は、AI ツールごとに違います。 -Claude Code向け core 49個、Kiro向け core 46個、Codex向け core 45個、agy向け core 45個です。 +Claude Code向け core 49個、Codex向け core 45個です。 元Skills(49個)の内訳は次のとおりです。 - Pull Request とレビュー (7): pr, pr-tests, fix, pr-review, cherry-pick-pr, deploy, merged - 開発の進め方 (17): development-workflow, requirements-design, design, document-rebuild, document-sources, document-drafting, tdd-cycle, refactoring, quality-gates, release, release-verification, retrospective, out-of-scope, progress-tracking, backlog-refinement, layout-review, estimate @@ -105,7 +86,7 @@ Skill のほかに、次のものが入ります。 | 部品 | できること | | --- | --- | | サブエージェント(Claude Code) | 調査・データ分析・デバッグ・コードレビュー・CI/CD などを任せる専門のサブエージェント 8 個 | -| 外部 AI への委譲 | Codex や agy をバックグラウンドで動かし、別の AI の意見を得る | +| 外部 AI への委譲 | Codex・Kiro CLI・agy をバックグラウンドで動かし、別の AI の意見を得る | | hook | 会話の記録を 90 日以上残す。回答や承認を待つときに Slack へ知らせる。リポジトリに `.ndf/worktree.json` があるときは git worktree での作業を案内する | ## もっと知る diff --git a/docs/glossary.md b/docs/glossary.md index ac1985152..5f3a85aad 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -321,7 +321,7 @@ NDF が提供する工程・承認ゲート・モード・ステップの語。d | ステップの延長 | — | drive のステップの子孫(駆動と、駆動が起動した init)が自分の見込みの秒を延長のファイル(NDF_STEP_EXTEND_FILE の指すパス)へ書き、エンジンがその試行の締め切りを with_margin を足して後ろへずらすこと。前へはずらさない | — | — | — | | 過程の項目 | `process` | 結果 JSON の process。スクリプトが実行したこと(コマンド・操作)と、その結果の要旨を順に並べる | — | — | `docs/specifications/ndf-step-result-gaps-and-reading.md` | | 判断の項目 | `decisions` | 結果 JSON の decisions。スクリプトが決めたことと、その根拠を並べる | — | — | `docs/specifications/ndf-step-result-gaps-and-reading.md` | -| 最終工程の記録 | — | 最終工程を終えたことを示す、GitHub に残った記録。配布は配布の記録の `段階:`、リリース後テストはその課題の行、振り返りは課題の本文の `振り返り: <コメントの URL>` の行。sprint-close.py が閉じる前に確かめる | — | — | `plugins/ndf/skills/progress-tracking/SKILL.md` | +| 最終工程の記録 | — | 最終工程を終えたことを示す、GitHub に残った記録。配布は配布の記録の `段階:`、リリース後テストはその課題の行、振り返りは課題の本文の `振り返り: <コメントの URL>` の行のうち、指すコメントの識別子が記録の PR の最後の配布の記録のコメントの識別子以上のもの(その実行の振り返り)。sprint-close.py が閉じる前に確かめる | — | — | `plugins/ndf/skills/progress-tracking/SKILL.md` | | 閉じる予定の参照 | — | Pull Request の本文に `最終工程の後に閉じる: #<番号>` と書く形。GitHub は閉じず、sprint-close.py がスプリント課題として集める。宛先が既定ブランチの Pull Request では、NDF の手順が closing keywords をこの形へ置き換える | — | — | `plugins/ndf/skills/progress-tracking/SKILL.md` | ## NDF の Slack 通知(`ndf-notification`) diff --git a/docs/glossary/glossary.json b/docs/glossary/glossary.json index ef4d7f4a2..845c3ce4e 100644 --- a/docs/glossary/glossary.json +++ b/docs/glossary/glossary.json @@ -4708,7 +4708,7 @@ { "term": "最終工程の記録", "context": "ndf-workflow", - "meaning": "最終工程を終えたことを示す、GitHub に残った記録。配布は配布の記録の `段階:`、リリース後テストはその課題の行、振り返りは課題の本文の `振り返り: <コメントの URL>` の行。sprint-close.py が閉じる前に確かめる", + "meaning": "最終工程を終えたことを示す、GitHub に残った記録。配布は配布の記録の `段階:`、リリース後テストはその課題の行、振り返りは課題の本文の `振り返り: <コメントの URL>` の行のうち、指すコメントの識別子が記録の PR の最後の配布の記録のコメントの識別子以上のもの(その実行の振り返り)。sprint-close.py が閉じる前に確かめる", "source": "plugins/ndf/skills/progress-tracking/SKILL.md" }, { diff --git a/docs/ndf-plugin-reference.md b/docs/ndf-plugin-reference.md index 688f51637..cfb73f872 100644 --- a/docs/ndf-plugin-reference.md +++ b/docs/ndf-plugin-reference.md @@ -2,14 +2,12 @@ ## 概要 -NDF は Claude Code / Codex / Kiro CLI 向けの開発支援プラグイン群です。marketplace 上の plugin 名は全ランタイムで `ndf` を維持し、配布物は `plugins/ndf/` の 1 ディレクトリにまとまっています。 +NDF は Claude Code / Codex 向けの開発支援プラグイン群です。marketplace 上の plugin 名はどちらでも `ndf` を維持し、配布物は `plugins/ndf/` の 1 ディレクトリにまとまっています。配布先は Claude Code と Codex の 2 つで、Kiro CLI と agy へは配りません。この 2 つは cross-review / cross-refactoring の相手役として、ホスト(Claude Code・Codex)から CLI プロセスで起動されるだけです(「相手役のランタイム」)。 | ランタイム | 導入方法 | 公開 Skill | |---|---|---| | Claude Code | `.claude-plugin/marketplace.json` の `ndf` | 49 個 | | Codex | `.claude-plugin/marketplace.json` の `ndf` | 45 個 | -| Kiro CLI | `plugins/ndf/dev.kiro/install.sh` | 46 個 | -| agy | `plugins/ndf/dev.agy/` を `agy plugin install` | 45 個 | Skill の実体は `plugins/ndf/skills/` の 1 箇所だけです。ランタイム別の複製はありません。Skill や 共通スクリプトを変更する場合は `plugins/ndf/` を直接編集します。 @@ -25,11 +23,10 @@ plugins/ndf/ ├── agents/ # Claude Code のサブエージェント(専門 8 個と、3 層の定義 3 個(supervisor 2・worker 1)) ├── hooks/claude.json # Claude Code の SessionStart / Stop hook ├── hooks/codex.json # Codex の Stop hook -├── scripts/ # hook と Skill から呼ぶスクリプト -└── dev.kiro/ # Kiro CLI の installer・エージェント定義・プロンプト +└── scripts/ # hook と Skill から呼ぶスクリプト ``` -`dev.kiro` は Agent Plugins 1.0.0 §8.2 が定めるクライアント拡張ディレクトリです。生成物は +生成物は `skills/<名前>/agents/openai.yaml`(Codex の暗黙起動ポリシー)だけで、commit 対象です。利用者が plugin install 時に build を実行する必要はありません。 @@ -39,10 +36,8 @@ plugin install 時に build を実行する必要はありません。 |---|---| | Claude Code | `.claude-plugin/plugin.json`(`agents/` 11 個(専門 8 個と、3 層の定義 3 個(supervisor 2・worker 1))、`hooks/claude.json`、`skills` 配列 49 個)、`scripts/` | | Codex | `.codex-plugin/plugin.json`(`hooks/codex.json`、`skills` 配列 45 個)、`scripts/` | -| Kiro CLI | `dev.kiro/`(installer・agent config template・workflow prompts)、`manifests/kiro-skills.txt`、`skills/`、`scripts/` | -| agy | `dev.agy/`(マニフェスト・`hooks.json`・`skills/` の symlink・`model` を tier 名へ変えた `agents/` の写し・`scripts` への symlink) | -同じディレクトリを 4 ランタイムが共有しますが、読む対象はマニフェストと installer が決めるため、 +同じディレクトリを 2 ランタイムが共有しますが、読む対象はマニフェストが決めるため、 公開される Skill と hook はランタイムごとに異なります。Claude Code 専用の agents / statusline / transcript retention hook は Codex の `skills` 配列と `hooks/codex.json` には入りません。 @@ -54,7 +49,6 @@ NDF の Skill 実装は `plugins/ndf/skills/` にあります。公開セット |---|---| | `plugins/ndf/manifests/claude-skills.txt` | `.claude-plugin/plugin.json` の `skills` 配列 | | `plugins/ndf/manifests/codex-skills.txt` | `.codex-plugin/plugin.json` の `skills` 配列 | -| `plugins/ndf/manifests/kiro-skills.txt` | `dev.kiro/install.sh` が張る symlink | **`skills/` に置く Skill は、少なくとも 1 つの manifest へ載せます。** 配らない Skill の置き場所 (`optional-skills/`)は v10.5.0 で無くしました(#116)。 @@ -71,17 +65,17 @@ NDF の Skill 実装は `plugins/ndf/skills/` にあります。公開セット - quality / execution: `docker-container-access`, `qa-security-scan` (Playwright による E2E テストは v7.0.0 で `playwright-kit` プラグインへ分離) - external services: `external-ai`, `official-skills-autoloader`(Claude Code のみ) -- runtime 設定: `statusline`(Claude Code と Kiro CLI のみ)、`install-wrapper` / `restart`(カットポイントで claude を起動し直すラッパー。Claude Code のみ) +- runtime 設定: `statusline`(Claude Code のみ)、`install-wrapper` / `restart`(カットポイントで claude を起動し直すラッパー。Claude Code のみ) - policy: `ndf-policies`, `problem-solving`, `logging-guidelines` - 外部サービスと測定: `google-workspace`, `skill-stats`, `ml-model-structure`, - `notion-writing`(v10.5.0 で 4 ランタイムすべてへ配布へ回しました。#116 / #144) + `notion-writing`(v10.5.0 で配布へ回しました。#116 / #144) ## MCP Plugins -MCP plugin も `plugins/mcp//` の 1 ディレクトリにまとまっています。サーバ定義は `.mcp.json` の 1 箇所だけで、3 runtime が同じファイルを読みます。 +MCP plugin も `plugins/mcp//` の 1 ディレクトリにまとまっています。サーバ定義は `.mcp.json` の 1 箇所だけで、Claude Code と Codex が同じファイルを読みます(Codex だけ `.codex.mcp.json` を読ませる例外があります)。 -Claude Code と Codex は marketplace から同じ plugin 名で install します。Kiro CLI は `plugins/mcp//dev.kiro/install.sh` で対象プロジェクトの `.mcp.json` と必要な Kiro agent 設定を更新します。 +Claude Code と Codex は marketplace から同じ plugin 名で install します。 ## Build / Validation @@ -91,15 +85,13 @@ bash scripts/build-runtime-plugins.sh --check bash scripts/validate-runtime-plugins.sh claude plugin validate plugins/ndf python3 -m json.tool plugins/ndf/.codex-plugin/plugin.json >/dev/null -bash plugins/ndf/dev.kiro/install.sh --dry-run bash scripts/runtime-smoke-test.sh --runtime claude bash scripts/runtime-smoke-test.sh --runtime codex -bash scripts/runtime-smoke-test.sh --runtime kiro ``` -`--check` は生成物(`plugins/<プラグイン名>/skills//agents/openai.yaml` と `plugins/mcp/<プラグイン名>/dev.kiro/install.sh`)が最新であることを検証します。`validate-runtime-plugins.sh` は生成物同期、JSON / manifest、marketplace source、Kiro installer、Markdown ローカルリンクをまとめて確認します。 +`--check` は生成物(`plugins/<プラグイン名>/skills//agents/openai.yaml`)が最新であることを検証します。`validate-runtime-plugins.sh` は生成物同期、JSON / manifest、marketplace source、Markdown ローカルリンクをまとめて確認します。agy・Kiro CLI の導入物(`agy-skills.txt`・`kiro-skills.txt`・`dev.agy/`・`dev.kiro/`)が戻っていないことは `scripts/tests/test_distribution_runtimes.py` が確かめます。 -`runtime-smoke-test.sh` は Docker コンテナ内に repo copy、`/tmp/runtime-home`、`/tmp/runtime-project`、`/tmp/runtime-secrets` を分離して作成し、Claude / Codex / Kiro それぞれで `ndf` と `mcp-bigquery` 相当の実 install、Skill / MCP / hook / agent config の assertion、JUnit / log artifact 出力を確認します。PR CI は `--with-secrets=off` の非認証 smoke のみを実行し、認証付き smoke は `runtime-plugin-authenticated-smoke.yml` の protected workflow で実行します。 +`runtime-smoke-test.sh` は Docker コンテナ内に repo copy、`/tmp/runtime-home`、`/tmp/runtime-project`、`/tmp/runtime-secrets` を分離して作成し、Claude / Codex それぞれで `ndf` と `mcp-bigquery` 相当の実 install、Skill / MCP / hook の assertion、JUnit / log artifact 出力を確認します。PR CI は `--with-secrets=off` の非認証 smoke のみを実行し、認証付き smoke は `runtime-plugin-authenticated-smoke.yml` の protected workflow で実行します。 ローカル hook を使う場合は `bash scripts/install-dev-hooks.sh` で `.githooks/` を有効化します。 @@ -107,6 +99,10 @@ bash scripts/runtime-smoke-test.sh --runtime kiro Codex MCP サーバは廃止済みです。外部 AI 委譲は `/ndf:external-ai` Skill と Claude Code 版の `corder` エージェントから Codex / agy を直接呼び出す方式を標準とします。 +### 相手役のランタイム + +Kiro CLI と agy は cross-review のレビュー担当と cross-refactoring の提案・実装の担当(相手役)で、プラグインは配りません。ホストは `plugins/ndf/scripts/lib/launch-cli.sh` で CLI を起動し、`--agent` もプラグインの導入先も渡しません。cross-refactoring の担当が読む手順書(`refactoring`・`tdd-cycle`・`quality-gates`)は、ホストの導入先から担当の worktree(Kiro CLI は `.kiro/skills`、agy は `.agents/skills`)へ写します(`plugins/ndf/skills/cross-refactoring/scripts/prepare-worktrees.sh`)。起動の形の正本は `plugins/ndf/skills/external-ai/references/cli-kiro.md`・`cli-agy.md` です。 + ## 承認ゲートの外の問い合わせ `AskUserQuestion` は、問いの区分(入力の `metadata.source`)が人へ回してよい区分のときだけ利用者へ出ます。区分の無い・ @@ -131,8 +127,6 @@ Codex MCP サーバは廃止済みです。外部 AI 委譲は `/ndf:external-ai |---|---|---|---| | Claude Code 2.1.314 | `AskUserQuestion`(`metadata.source` を持つ) | 止められる(PreToolUse の `deny`) | 止める | | Codex CLI 0.164.0 | `request_user_input`(`codex features list` で `default_mode_request_user_input` が `under development`・`false`。exec では使えない) | 確かめていない(PreToolUse が発火するか、区分の欄があるか) | 対象外。Skill の手順の規則(`asking.md`)だけで守る | -| agy 1.4.7 | `ask_question` | 確かめていない(PreToolUse の `toolCall.name` と、拒否の理由がモデルへ届くか) | 対象外。Skill の手順の規則だけで守る | -| Kiro CLI 2.34.0 | 見つからない(組み込みの Tool に無く、MCP の elicitation だけ) | — | 対象外(止める対象が無い) | hook の無いランタイムでは、区分に当たる問いを本文か各ランタイムの問い合わせの Tool で出し、区分の名前を問いの先頭に書きます。 止めた問いでは Slack の待ち通知を送りません。 @@ -146,7 +140,6 @@ hook の無いランタイムでは、区分に当たる問いを本文か各ラ |---|---| | Claude Code | Stop・Notification(`permission_prompt` ほか)・PermissionRequest(`ExitPlanMode`)・PreToolUse(`AskUserQuestion`。問い合わせの guard が止めた問いを除く)hook で `wait-notify.py --runtime claude` を実行 | | Codex | `NDF_CODEX_SLACK_NOTIFY=true` の場合のみ Stop・PermissionRequest hook で `wait-notify.py --runtime codex` を実行 | -| Kiro CLI | `plugins/ndf/dev.kiro/install.sh --with-slack` で `stop` hook に `wait-notify.py --runtime kiro` を生成 | 機密情報は環境変数で管理し、リポジトリにはコミットしません。 @@ -154,7 +147,6 @@ hook の無いランタイムでは、区分に当たる問いを本文か各ラ - [Claude Code版 README](../plugins/ndf/README.md) - [Codex版 README](../plugins/ndf/README.md) -- [Kiro CLI版 README](../plugins/ndf/README.md) - [NDF プラグイン README](../plugins/ndf/README.md) - [開発ワークフローの用語集](../plugins/ndf/skills/development-workflow/references/glossary.md) — ステップ・パイプライン・セッション・承認ゲートなど、`development-workflow` とその周りで使う語 - [runtime plugin container smoke 仕様](specifications/runtime-plugin-container-smoke.md) diff --git a/docs/plugin-development-guide.md b/docs/plugin-development-guide.md index 205cfd2e4..21e2b0324 100644 --- a/docs/plugin-development-guide.md +++ b/docs/plugin-development-guide.md @@ -35,13 +35,9 @@ marketplace 定義はこの 1 つだけです。Codex は専用の定義を持 同じ `source` を読みます。`policy` / `category` / `interface` は Codex が要求する項目で、同じ entry へ 含めます(Claude Code はこれらを読み込み時に無視し、`claude plugin validate` は warning 付きで通ります)。 -Kiro CLI は marketplace を読まないため、installer で導入します。 - -```bash -bash plugins/ndf/dev.kiro/install.sh -bash plugins/playwright-kit/dev.kiro/install.sh -bash plugins/mcp/<プラグイン名>/dev.kiro/install.sh -``` +配布先は Claude Code と Codex の 2 つです。Kiro CLI と agy へはどのプラグインも配りません。この 2 つは +cross-review / cross-refactoring の相手役として CLI を起動するだけで、導入物を持ちません(`dev.kiro/`・`dev.agy/`・ +`kiro-skills.txt`・`agy-skills.txt` を足すと `scripts/tests/test_distribution_runtimes.py` が落とします)。 ## プラグイン構造 @@ -62,25 +58,18 @@ plugins/{plugin-name}/ │ └── {skill-name}/ │ └── SKILL.md ├── manifests/ # ランタイム別の配布 Skill 一覧(Skill を配る場合) -│ └── {claude,codex,kiro}-skills.txt +│ └── {claude,codex}-skills.txt ├── hooks/ # フック(オプション) │ ├── claude.json # Claude Code 用 │ └── codex.json # Codex 用 -├── dev.kiro/ # Kiro CLI 用(installer など) -│ └── install.sh -├── dev.agy/ # agy 用(マニフェスト・hooks.json・symlink) -│ ├── plugin.json -│ └── hooks.json └── README.md # プラグイン説明 ``` -4 ランタイムが同じディレクトリを読みます。読む対象はマニフェストと installer が決めるため、 -公開される Skill と hook はランタイムごとに異なります。`dev.kiro` と `dev.agy` は Agent Plugins -仕様 §8.2 が定めるクライアント拡張ディレクトリです。 +2 ランタイムが同じディレクトリを読みます。読む対象はマニフェストが決めるため、 +公開される Skill と hook はランタイムごとに異なります。 -**ルート直下の `plugin.json` は、agy へ配るためには置きません。** 置くと Codex がそちらを -優先して読み、`skills/` の実体を全件配ってしまいます(`plugins/ndf` で実測)。agy 向けの -定義は `dev.agy/plugin.json` へ置きます。 +**`.codex-plugin/` を持つプラグインはルート直下に `plugin.json` を置きません。** 置くと Codex がそちらを +優先して読み、`skills/` の実体を全件配ってしまいます(`plugins/ndf` で実測)。 ルートの `plugin.json`(Agent Plugins 形式)は、`skills/` を全件公開してよく hook も持たない プラグインにだけ置きます。判断の基準は @@ -134,11 +123,11 @@ plugins/{plugin-name}/ 2. **ディレクトリ構造を作成** ```bash - mkdir -p plugins/{plugin-name}/{.claude-plugin,.codex-plugin,commands,agents,skills,manifests,hooks,dev.kiro} + mkdir -p plugins/{plugin-name}/{.claude-plugin,.codex-plugin,commands,agents,skills,manifests,hooks} ``` 使わないディレクトリは作らなくて構いません。Skill を配らないなら `skills/` と - `manifests/`、hook が無いなら `hooks/`、Kiro CLI へ配らないなら `dev.kiro/` は不要です。 + `manifests/`、hook が無いなら `hooks/` は不要です。 3. **plugin.jsonを作成** - 必須フィールドをすべて含める @@ -185,25 +174,23 @@ frontmatter と命名の書き方は [AUTHORING.md](../plugins/ndf/skills/AUTHOR | 1 | `plugins/ndf/skills/<名前>/SKILL.md` | 実体 | `check-skill-frontmatter.py`(形だけ) | | 2 | `plugins/ndf/manifests/<ランタイム>-skills.txt` | 配るランタイムの分だけ名前を 1 行 | `validate-runtime-plugins.sh`(どの manifest にも無いときだけ。配る先は判断) | | 3 | `plugins/ndf/.claude-plugin/plugin.json` / `.codex-plugin/plugin.json` の `skills` 配列 | `./skills/<名前>`(manifest に載せたランタイムの分) | `validate-runtime-plugins.sh` | -| 4 | `bash scripts/build-runtime-plugins.sh` の生成物 | `dev.agy/skills/<名前>`(agy へ配るとき)、`skills/<名前>/agents/openai.yaml`(`disable-model-invocation: true` のとき)をコミット | `build-runtime-plugins.sh --check` | -| 5 | `README.md` の概要「公開Skills」とプラグイン一覧表の `ndf` 行(2 行) | `Claude Code向け core 45個` の形で 4 ランタイム | `check-doc-staleness.py` | +| 4 | `bash scripts/build-runtime-plugins.sh` の生成物 | `skills/<名前>/agents/openai.yaml`(`disable-model-invocation: true` のとき)をコミット | `build-runtime-plugins.sh --check` | +| 5 | `README.md` の概要「公開Skills」とプラグイン一覧表の `ndf` 行(2 行) | `Claude Code向け core 45個` の形で 2 ランタイム | `check-doc-staleness.py` | | 6 | `README.md` の「元Skills(N個)」 | 実体の数 | `check-doc-staleness.py` | | 7 | `README.md` のカテゴリ内訳 | `- 運用 (2): skill-stats, statusline` の形。名前と括弧の数 | `check-doc-staleness.py`(合計だけ。どのカテゴリに入れるかは判断) | -| 8 | `plugins/ndf/README.md` の配布先の表 4 行 | `\| Claude Code \| 45 個 \|` の形 | `check-doc-staleness.py` | +| 8 | `plugins/ndf/README.md` の配布先の表 2 行 | `\| Claude Code \| 45 個 \|` の形 | `check-doc-staleness.py` | | 9 | `plugins/ndf/README.md` のレイアウト図 | `唯一の実体(45 個)` | `check-doc-staleness.py` | -| 10 | `plugin.json` 3 本(claude・codex・`dev.agy`)の `description` | `45 focused NDF skills` の形 | `validate-runtime-plugins.sh` | +| 10 | `plugin.json` 2 本(claude・codex)の `description` | `45 focused NDF skills` の形 | `validate-runtime-plugins.sh` | | 11 | `.claude-plugin/marketplace.json` の `ndf` の `description` | 10 と同じ形(Claude Code の数) | `validate-runtime-plugins.sh` | -| 12 | `scripts/tests/test_agy_distribution.py` の `EXPECTED_COUNTS` | 4 ランタイムの数 | `pytest`(件数の assert だけで、直す場所を名指ししない) | -| 13 | `plugins/ndf/README.md` の agy の節「Skill N 個」 | agy の数 | **拾わない**(#611) | -| 14 | `plugins/ndf/README.md` の「Codex の暗黙起動抑止」の数と表 | `disable-model-invocation: true` のときだけ | **拾わない**(#611) | -| 15 | `docs/ndf-plugin-reference.md` の数 | 4 ランタイムの数と実体の数 | **拾わない**(#611) | +| 12 | `plugins/ndf/README.md` の「Codex の暗黙起動抑止」の数と表 | `disable-model-invocation: true` のときだけ | **拾わない** | +| 13 | `docs/ndf-plugin-reference.md` の数 | 2 ランタイムの数と実体の数 | **拾わない** | **数の形は 3 通りあります。** `README.md` は `core 45個`(空白なし)、`plugins/ndf/README.md` は `45 個`、`plugin.json` は `45 focused NDF skills` です。数だけを機械的に置き換えると、どれかに 当たりません。数はランタイムごとに manifest の行数から決めます。 ```bash -for r in claude codex kiro agy; do +for r in claude codex; do printf '%s ' "$r"; grep -cvE '^[[:space:]]*(#|$)' "plugins/ndf/manifests/$r-skills.txt" done ls -d plugins/ndf/skills/*/ | wc -l # 実体の数(6・9) @@ -245,12 +232,9 @@ uv run --with pytest --with pytest-xdist pytest scripts/tests -q -n auto 上書きされる**(`--scope local` を指定しても起きる)。続けて `marketplace remove` すると clone と導入記録まで消える。 -手元で確かめる手段は 2 つある。 +手元で確かめる手段は次のとおり。 ```bash -# Kiro CLI: installer が任意のディレクトリへ導入する。取得元を書き換えない -bash plugins/ndf/dev.kiro/install.sh --project <検証用ディレクトリ> --yes - # Claude Code: 1 つのプラグインをディレクトリから読み込む claude --plugin-dir plugins/ndf ``` diff --git a/docs/project-overview.md b/docs/project-overview.md index 4c7f059f5..7db4e20e8 100644 --- a/docs/project-overview.md +++ b/docs/project-overview.md @@ -23,17 +23,17 @@ Claude Code / Codex プラグインマーケットプレイス(内部用)と ``` ai-plugins/ ├── .claude-plugin/ -│ └── marketplace.json # マーケットプレイス定義(3ランタイム共通) +│ └── marketplace.json # マーケットプレイス定義(Claude Code・Codex 共通) ├── plugins/ -│ ├── ndf/ # NDF(3ランタイム共通の単一ディレクトリ) -│ ├── playwright-kit/ # playwright-kit(3ランタイム共通の単一ディレクトリ) +│ ├── ndf/ # NDF(Claude Code・Codex 共通の単一ディレクトリ) +│ ├── playwright-kit/ # playwright-kit(Claude Code・Codex 共通の単一ディレクトリ) │ └── mcp/ -│ └── mcp-*/ # MCPプラグイン10個(3ランタイム共通) +│ └── mcp-*/ # MCPプラグイン10個(Claude Code・Codex 共通) ├── docs/ # リポジトリ知識 ├── factory/ # Web サービス(Neural Development Factory)。プラグインと分け、自分の LICENSE を持つ ├── AGENTS.md # 共通エントリポイント ├── CLAUDE.md # Claude Code固有設定 -├── KIRO.md # Kiro CLI固有設定 +├── KIRO.md # 相手役として起動される Kiro CLI の説明 └── README.md # プロジェクトドキュメント ``` diff --git a/docs/specifications/ndf-backlog-refinement-stage.md b/docs/specifications/ndf-backlog-refinement-stage.md index c442d6236..4fd2befd6 100644 --- a/docs/specifications/ndf-backlog-refinement-stage.md +++ b/docs/specifications/ndf-backlog-refinement-stage.md @@ -100,8 +100,8 @@ conductor が打ち切った。今の形では次のように動く。 ### スプリントの終わりのプランの `refine` ステップ -`supervise.py new close` が組むプラン(`supervise_lib/sprint.py` の `close_plan`)は、`retro` の後に -`refine` を置く。`retro` の `next` は `refine` で、`retro` の `prompt` は棚卸しを含まない。 +`supervise.py new close` が組むプラン(`supervise_lib/sprint.py` の `close_plan`)は、`retro` の後に課題を閉じる `close` を、 +その後に `refine` を置く。`retro` の `next` は `close`、`close` の `next` は `refine` で、`retro` の `prompt` は棚卸しを含まない。 | キー | 値 | | --- | --- | @@ -131,7 +131,7 @@ worker の歯止めで、費用の上限は件数が持つ。プランの `上 | 不正な上限は候補を集めずに終了コード 2 | 同上の `test_candidates_reject_invalid_limit_before_collecting` | | 候補が 0 件の棚卸しは上限の導入で持ち越しにならない | 同上の `test_candidates_zero_is_ok_under_limit` | | 書き込み先・plan の `repo`・記録のどれかが食い違えば、1 件も書かずに終了コード 3 | 同上の `test_apply_refuses_*` | -| `new close` のプランで `retro` の次が `refine`、`refine` の `stage` が `棚卸し` で `timeout` を持ち、プロンプトが無人の扱いを渡す | `plugins/ndf/scripts/tests/test_supervise_pace.py`(`test_close_runs_spec_close_and_retro_once_each_in_order`) | +| `new close` のプランで `retro` の次が `close`、その次が `refine`、`refine` の `stage` が `棚卸し` で `timeout` を持ち、プロンプトが無人の扱いを渡す | `plugins/ndf/scripts/tests/test_supervise_pace.py`(`test_close_runs_spec_close_and_retro_once_each_in_order`) | | `STEP_PHASES` がフェーズの表と一致する | `plugins/ndf/scripts/tests/test_transcript_agents.py`(`test_the_step_names_match_the_phase_table`) | | `stage 棚卸し` が記録され、5 モードで必須として扱われる | `plugins/ndf/scripts/tests/test_progress_record_stages.py`(`test_the_refinement_stage_is_recorded_and_required_in_every_mode`) | | 最終工程を持つ 3 つの Skill(`retrospective` / `release-verification` / `release`)が棚卸しを呼ばない。順序の制約と規則 14 がそれぞれ 1 か所にある | 文書を `grep -n` で読んで確かめる(文言を照合するテストは書かない) | diff --git a/docs/specifications/ndf-cleanup-and-bundle-closing.md b/docs/specifications/ndf-cleanup-and-bundle-closing.md index 4e49ca1be..0674275d2 100644 --- a/docs/specifications/ndf-cleanup-and-bundle-closing.md +++ b/docs/specifications/ndf-cleanup-and-bundle-closing.md @@ -29,8 +29,8 @@ Request の head のリモートブランチは、同意を求めずに消す。 有無にかかわらず消さない。 **スプリント課題は、最終工程を出るときに閉じる。** 閉じる条件は「本番リリースまで済んで -いる、またはリリースを飛ばした」と「リリース後テストを通る経路では受け入れ条件がすべて `合格`」の -2 つで、課題ごとに見る。`merged` は課題を閉じない。 +いる、またはリリースを飛ばした」と「リリース後テストを通る経路では受け入れ条件がすべて `合格`」と +「その実行の最終工程の記録がある(`--final-stage`、省けば振り返り。#2228)」の 3 つで、課題ごとに見る。`merged` は課題を閉じない。 **リリース記録は Pull Request に置く。** `release` の出力物は会話に出るだけで別の実行からは 読めないため、書く側の形を 1 つに決め、読む側はその形だけを読む。 @@ -227,7 +227,7 @@ Pull Request を選ぶため、別のスプリント課題を閉じうる。 | `既に閉じていた` | 最初の読みが CLOSED(既定ブランチへのマージで GitHub が先に閉じた場合を含む) | | `失敗(理由)` | 最後の読みが CLOSED でない、または読み取りが 0 以外で終わった | | `開いたまま(理由)` | 閉じる条件に当たらない | -| `閉じているが条件を満たさない(理由)`(`closed_unverified`) | 最初の読みが CLOSED で、閉じる条件 2 を満たさない | +| `閉じているが条件を満たさない(理由)`(`closed_unverified`) | 最初の読みが CLOSED で、閉じる条件 2 か 3 を満たさない | **`失敗` が 1 件でもあれば、最終工程を完了と報告せず、棚卸しへ進まずに止まる。** `開いたまま` と `閉じているが条件を満たさない` は止める理由にせず、棚卸しへ進んでよい。 @@ -262,7 +262,7 @@ Pull Request を選ぶため、別のスプリント課題を閉じうる。 最後のブロックを読む(版数を上げない配布。[ndf-production-release-record.md](ndf-production-release-record.md) の 「リリース後テストの選び方」)。`配布なし` のときは、その記録より後に投稿された最後のブロックを読む。 表の `課題` の列が無いとき、または `課題` の列の頭が課題の参照(`#<番号>` / `<所有者>/<リポジトリ>#<番号>`)である -行が 1 行も無いときは、スプリント課題すべてを `開いたまま(条件と課題の対応が読めない)` にする。頭が課題の参照で +行が 1 行も無いときは、スプリント課題すべてを `開いたまま(条件と課題の対応が読めない)` にする(見出しが読める表で、すべての条件をマージ前に確かめたと本文に記録した課題は除く。#2144)。頭が課題の参照で ない行(`—`・`PR 1869` など)は飛ばし、ほかの課題の判定を止めない。 ### ボードの自動化 @@ -299,8 +299,10 @@ Pull Request を選ぶため、別のスプリント課題を閉じうる。 一時ディレクトリにあり、共通の git ディレクトリと別のファイルシステムのことがあるため、`mv` が コピーになる。 -**チャネルを分けないリポジトリでは、既定ブランチへのマージで GitHub が先に閉じる。** 「スプリントを -閉じる」は `既に閉じていた` を数えるだけで、二重に閉じようとしても結果は変わらない。 +**チャネルを分けないリポジトリでも、NDF の手順で書いた Pull Request の本文では GitHub が先に閉じない。** +既定ブランチ宛の本文の closing keywords は、`pr-steps.py` とプランの pr のステップが閉じる予定の参照 +(`最終工程の後に閉じる: #<番号>`)へ置き換える。手で書いた本文・コミットメッセージの closing keywords で +GitHub が先に閉じたときは、「スプリントを閉じる」は `既に閉じていた` を数えるだけで、二重に閉じようとしても結果は変わらない。 ## 関連リンク diff --git a/docs/specifications/ndf-sprint-close-acceptance-criteria.md b/docs/specifications/ndf-sprint-close-acceptance-criteria.md index 433775342..397398015 100644 --- a/docs/specifications/ndf-sprint-close-acceptance-criteria.md +++ b/docs/specifications/ndf-sprint-close-acceptance-criteria.md @@ -72,7 +72,7 @@ | # | 条件 | 破れたときの扱い | | --- | --- | --- | | I1 | `課題` の列が `^(<所有者>/<リポジトリ>)?#<番号>` に当たらない行は、どの課題の判定にも使わず、表全体の読みも止めない | — (読み飛ばす) | -| I2 | 課題を指す行が 1 行も無い表は、どの課題にも対応を持たない | 全件を `kept_open`(条件と課題の対応が読めない)。閉じた課題も `closed_unverified` にしない | +| I2 | 課題を指す行が 1 行も無い表は、どの課題にも対応を持たない | 全件を `kept_open`(条件と課題の対応が読めない)。閉じた課題も `closed_unverified` にしない。ただし本文の `## 受け入れ条件の確認` ですべての条件をマージ前に確かめたと記録した課題は、行が要らないため課題ごとの判定へ進める(#2144) | | I3 | 受け入れ条件の番号は、節のコードブロックの外にある行頭(字下げなし)のチェックボックスの行を上から数えた順(1 から)である。取り消し線で差し替えた行は数えない | — (数え方の定義) | | I4 | `closed_unverified` は課題ごとの判定(その課題の行が無い・合格でない行がある・合格の行が無い番号がある)にだけ使う。全体を止める経路(配布の記録が読めない・本番の前・対応が読めない)では閉じた課題も `kept_open` のままにする | — | | I5 | 印付けで変わるのは、節の中のチェックボックスの `[ ]` の空白 1 文字だけである(節の外・取り消し線の行・説明の行・改行の形は変わらない) | 書き込まず、その課題を `failed`(受け入れ条件の印を書けない) | diff --git a/docs/specifications/ndf-sprint-close-referenced-issues.md b/docs/specifications/ndf-sprint-close-referenced-issues.md index 3b073c0e7..c257845b9 100644 --- a/docs/specifications/ndf-sprint-close-referenced-issues.md +++ b/docs/specifications/ndf-sprint-close-referenced-issues.md @@ -21,6 +21,8 @@ 語の正本の行は `development-workflow` の [`references/glossary.md`](../../plugins/ndf/skills/development-workflow/references/glossary.md) にもある。 closing keywords は、Pull Request の本文に課題の番号と一緒に書くと、マージのときに GitHub がその課題を自動で閉じるキーワードである。 +閉じる予定の参照(`最終工程の後に閉じる: #12`。既定ブランチ宛の本文で NDF の手順が closing keywords を置き換えた形)も、 +`closing-issues.sh` が closing keywords と同じ指し先の規則で本文から集め、参照だけの課題にしない。 ## 背景 diff --git a/docs/specifications/ndf-sprint-misjudgments-unknown-record-and-manual-none.md b/docs/specifications/ndf-sprint-misjudgments-unknown-record-and-manual-none.md index 110729faf..34fafe161 100644 --- a/docs/specifications/ndf-sprint-misjudgments-unknown-record-and-manual-none.md +++ b/docs/specifications/ndf-sprint-misjudgments-unknown-record-and-manual-none.md @@ -51,9 +51,9 @@ | # | 条件 | 破れたときの扱い | | --- | --- | --- | | I1 | まとめのプランの記録の参照が `0`(`RECORD_NONE`)になるのは、リリースの経路がすべて届けない経路(`STAGE_NONE`)のときだけである | テストが落とす | -| I2 | 記録の参照が `{queue_pr:…}` でない(`has_record` が偽)とき、プランに `release-verify` を置かず、merge の次は `close` になる | テストが落とす | +| I2 | 記録の参照が `{queue_pr:…}` でない(`has_record` が偽)とき、プランに `release-verify` を置かず、merge の次は `retro`(その次が `close`。#2228)になる | テストが落とす | | I3 | `--record-pr unknown` の実行は、どの課題も閉じず、ボードも書かず、課題の本文も読まない(gh へ書き込まない) | テストが落とす | -| I4 | `--record-pr 0` の意味(記録を読まず、閉じる条件を見ずに `--issues` を閉じる。`--issues` が無ければ終了コード 2)は変わらない | 既存のテストが落とす | +| I4 | `--record-pr 0` の意味(記録を読まず、閉じる条件 1・2 を見ずに `--issues` を閉じる(条件 3 の振り返りの記録は見る)。`--issues` が無ければ終了コード 2)は変わらない | 既存のテストが落とす | | I5 | 要求の 2 列目が「無し」の形の行は `ManualRow` にならない。それ以外の空でない 2 列目は手動確認の行になる | テストが落とす | ### 入出力の契約 @@ -112,9 +112,9 @@ flowchart TD C -- いいえ --> D{"経路がすべて STAGE_NONE"} D -- はい --> Z["0"] D -- いいえ --> U["unknown"] - T --> V["merge → release-verify → close"] + T --> V["merge → release-verify → retro → close"] P --> V - Z --> W["merge → close(release-verify なし)"] + Z --> W["merge → retro → close(release-verify なし)"] U --> W ``` diff --git a/docs/specifications/ndf-workflow-development-loop.md b/docs/specifications/ndf-workflow-development-loop.md index 6eec23341..1c6044eb7 100644 --- a/docs/specifications/ndf-workflow-development-loop.md +++ b/docs/specifications/ndf-workflow-development-loop.md @@ -190,7 +190,7 @@ MVV のレッドラインで人へ戻すという要求がそれを決める。 | --- | --- | | `sprint-state.py init --name <名>- --pace auto --issue ... --dev V --prod V --goal <再開のコマンド> [--mvv mvv.md --milestone M]` | `--prod` を省けば開発版の接尾辞を外した版 | | `supervise.py new sprint --pace auto --issue <選んだ課題> --design <承認済みでない課題> [--designed ] ... [<-- の後の引数>]` | `loop answer --approved` で答えた課題は設計のプランを作らず、設計の結果のステージが設計 PR の設計の結果を読む | -| `supervise.py new close --pace auto --issue <選んだ課題> --state [--carry <前の周回の refine.deferred>] [--milestone M]` | まとめのステージ(確定仕様化 → Pull Request → 課題を閉じる → 振り返り → 棚卸し)だけを組む。最終の検査・開発版・本番は置かず、`fast` 節を読まない | +| `supervise.py new close --pace auto --issue <選んだ課題> --state [--carry <前の周回の refine.deferred>] [--milestone M]` | まとめのステージ(確定仕様化 → Pull Request → 振り返り → 課題を閉じる → 棚卸し。#2228)だけを組む。最終の検査・開発版・本番は置かず、`fast` 節を読まない | `pipeline` は、最初のステージの queue の最後に `--then <まとめのプラン>` をつなぎ、`supervise.py wait ` → `loop lap <名> --done ` を続けた 1 行である。手で行うステージか別の queue のステージが残るときは、それを終えてから diff --git a/docs/specifications/runtime-plugin-distribution.md b/docs/specifications/runtime-plugin-distribution.md index 4b6d6d0af..1719a1ef7 100644 --- a/docs/specifications/runtime-plugin-distribution.md +++ b/docs/specifications/runtime-plugin-distribution.md @@ -324,6 +324,5 @@ MCP plugin を変更する場合: - [NDF プラグイン README](../../plugins/ndf/README.md) - [playwright-kit プラグイン README](../../plugins/playwright-kit/README.md) - [Runtime Smoke の CLI の版](runtime-smoke-cli-versions.md) -- [`scripts/lib/agy_agents.py`](../../scripts/lib/agy_agents.py)(agy に配る定義の生成と model の対応表) - [Agent Plugins 仕様](https://github.com/agentplugins/agent-plugins-spec) - 課題 [#2087](https://github.com/devbasex/ai-plugins/issues/2087)(agy に配るエージェント定義) diff --git a/docs/versioning-and-distribution.md b/docs/versioning-and-distribution.md index 7ce87a6cf..d2a3fceb4 100644 --- a/docs/versioning-and-distribution.md +++ b/docs/versioning-and-distribution.md @@ -47,7 +47,7 @@ Codex は全ブランチを取得するが、登録した ref を基準にする **接尾辞は人が読むための目印である。** Claude Code の直接インストール経路は版数を **キャッシュキーとしての文字列一致**でしか見ず、`-dev` や `-rc` を prerelease として -扱わない。Codex と Kiro も同様で、Agent Plugins Specification には解釈の規定が無い。 +扱わない。Codex も同様で、Agent Plugins Specification には解釈の規定が無い。 semver の順序で除外されるのは、プラグイン間の依存解決(`dependencies`)の経路だけである。 それでも接尾辞を付けるのは、**入れたくない利用者が版数を見て判断できるようにする**ためである。 @@ -69,6 +69,10 @@ semver の順序で除外されるのは、プラグイン間の依存解決(` ## ランタイムごとの取得と導入 +**配布先は Claude Code と Codex の 2 つである。** Kiro CLI と agy へは配らず(#2231。最後に配ったのは +`10.27.4`)、cross-review / cross-refactoring の相手役として CLI を起動するだけである。前の版で入れた手元の扱いは +[plugins/ndf/README.md の「agy・Kiro CLI への配布の終了」](../plugins/ndf/README.md#agykiro-cli-への配布の終了)にある。 + **取得元の登録と、プラグインの導入は別の操作である。** 登録は取得元の一覧を書き換えるだけで、 導入済みの実体には触れない。実体は版ごとのディレクトリ(`plugins/cache/<取得元>/<名前>/<版>/`) に置かれ、導入の記録がそのうち 1 つを指す。**登録を切り替えただけでは、その記録は元の版を @@ -84,17 +88,6 @@ codex plugin marketplace add devbasex/ai-plugins codex plugin add ndf@ai-plugins ``` -**agy には入れ替えの操作が無い。** `agy plugin --help` の副コマンドは -`list` / `import` / `install` / `uninstall` / `enable` / `disable` / `validate` / `link` / -`help` で、導入済みを新しい版へ入れ替えるものが無い。他の 3 ランタイムは持つ -(Claude Code は `claude plugin update`、Codex は取得元を更新してから `codex plugin add`、 -Kiro CLI はインストーラの再実行)。 - -**`install` は既存の導入を拒まず、上書きする。ただし消えたファイルは残る**(agy 1.1.26 / -2026-09-05 に実測)。同じ名前で導入し直すと `plugin.json` の版数も Skill の中身も入れ替わるが、 -**取得元から取り除いたファイルは実体に残り続ける**。配布物が減る版へ上げると、消したはずの -Skill が利用者の手元で生き続ける。そのため案内は外してから入れ直す 2 手のままにする。 - **ローカルのディレクトリを同じ名前でマーケットプレイスとして追加しない。** 登録の鍵は取得元では なく `marketplace.json` の `name` で、**1 つの名前につき 1 つしか登録できない**(公式ドキュメントに "Each user can register only one marketplace per name" とある)。 @@ -111,17 +104,9 @@ Skill が利用者の手元で生き続ける。そのため案内は外して 定まらず検証の手段にならない。手元での確認は次のいずれかで行う。どれも取得元を書き換えない。 ```bash -bash plugins/ndf/dev.kiro/install.sh --project <検証用ディレクトリ> --yes # Kiro claude --plugin-dir plugins/ndf # Claude Code -agy plugin validate plugins/ndf/dev.agy # agy(読み込みの確認) ``` -**agy は導入したプラグインの `hooks.json` を読み込まない。** 読む先は -`~/.gemini/config/hooks.json` の 1 か所だけで、プラグイン配下とプロジェクト直下のどちらに -置いても `loaded 1 named hooks from 1 hooks.json file(s)` のまま変わらない(agy 1.1.26 で -実測)。導入した hook を効かせるには `bash plugins/ndf/dev.agy/install-hooks.sh` を実行して -その 1 か所へ差し込む。冪等で、他の項目には触れない。 - ## 開発版を試す ```bash @@ -143,16 +128,6 @@ ref の書き方は `owner/repo@ref` と `git-url#ref` の 2 つで、 `claude plugin marketplace add --help` には出ないが実機で動く。Codex は `--ref` を持ち、 `owner/repo@ref` も受け取る。`claude plugin install` に版を指定する手段は無い(これは確定)。 -**Kiro と agy はマーケットプレイスの経路を持たない。** clone した作業ディレクトリから -導入するため、ref にあたるのは clone の checkout である。clone 直後は既定ブランチ(`main`) -なので、正式版を使うだけなら追加の操作は要らない。 - -```bash -git -C checkout develop # 開発版を試すときだけ -bash plugins/ndf/dev.kiro/install.sh --project <ディレクトリ> --yes -agy plugin uninstall ndf && agy plugin install /plugins/ndf/dev.agy -``` - **サードパーティのマーケットプレイスは自動更新が既定で無効である。** `main` へ出した版が 即座に全利用者へ届くわけではなく、利用者が `marketplace update` を実行した時点で届く。 @@ -237,7 +212,7 @@ claude plugin tag plugins/ndf --push # ndf--v<版> を作って origin へ 落ちる。**記載を消してもチェックは通らない。** 位置を決める語が見つからなければ、読み取れない こととして落ちる。 -**定義ファイルが 7 箇所である。** 3 つの `plugin.json` はいずれも +**定義ファイルが 5 箇所である。** 2 つの `plugin.json` はいずれも `version` と `description` の両方に版数を持つため、片方だけ直すとチェックで止まる。 | 箇所 | 何を書くか | @@ -246,8 +221,6 @@ claude plugin tag plugins/ndf --push # ndf--v<版> を作って origin へ | `plugins/ndf/.claude-plugin/plugin.json` の `description` | `(vX.Y.Z)` の形で版数 | | `plugins/ndf/.codex-plugin/plugin.json` の `version` | 版数そのもの | | `plugins/ndf/.codex-plugin/plugin.json` の `description` | `(vX.Y.Z)` の形で版数 | -| `plugins/ndf/dev.agy/plugin.json` の `version` | 版数そのもの | -| `plugins/ndf/dev.agy/plugin.json` の `description` | `(vX.Y.Z)` の形で版数 | | `.claude-plugin/marketplace.json` の該当プラグインの `description` | 同上 | **plugin の README は版ごとの更新の節(`## vX.Y.Z へ更新するとき`)を持たない。** README は現行版の説明と、 @@ -255,7 +228,7 @@ CHANGELOG.md を参照する固定の「`## 更新情報`」の節だけを持 置く。配布の手順(`release-steps.py` の `bump`・`changelog`・`notes`)は README に触れない。節が戻ると `scripts/check-doc-staleness.py` が落とす(`plugins/*/README.md`・`plugins/mcp/*/README.md`)。 -**説明文書の本文が 6 箇所である。** いずれも利用者が読む入口にあり、古いまま残ると現行版を +**説明文書の本文が 5 箇所である。** いずれも利用者が読む入口にあり、古いまま残ると現行版を 誤って伝えるか、書かれたとおりに実行できない。 **指示書(`AGENTS.md` / `CLAUDE.md`)には版数を書かない。** 開発版の配布の `bump` が版数を @@ -267,7 +240,6 @@ CHANGELOG.md を参照する固定の「`## 更新情報`」の節だけを持 | `README.md` の概要 | `**NDFプラグイン vX.Y.Z**` | `ndf` の `plugin.json` | | `README.md` のプラグイン一覧表 | 版数の列 | 行ごとに `plugins/<名前>/.claude-plugin/plugin.json` | | `docs/versioning-and-distribution.md` の「版の付け方と開発版の配布」章 | 版数の例 | `ndf` の `plugin.json`(基底の比較) | -| `plugins/ndf/README.md` の Kiro の確認例 | 期待出力の版数 | `ndf` の `plugin.json` | | `plugins/ndf/README.md` の Codex のパス例 | `~/.codex/plugins/cache/.../ndf/X.Y.Z/`(2 箇所) | `ndf` の `plugin.json` | | `plugins/ndf/README.md` の `codex plugin list` の出力例 | 期待出力の版数 | `ndf` の `plugin.json` | @@ -305,9 +277,9 @@ CHANGELOG.md を参照する固定の「`## 更新情報`」の節だけを持 両方に版を持たせると `plugin.json` が無警告で優先され、食い違いに気づけなくなる。 `scripts/validate-runtime-plugins.sh` が突き合わせるのは、説明文書に書かれた Skill の数と、 -**版数を書いた箇所**です。定義ファイルが 7 箇所、説明文書の本文が -6 箇所あります(`README.md` の概要とプラグイン一覧表、正本に 1 箇所、 -`plugins/ndf/README.md` の Kiro と Codex の確認例 3 種類)。**記載を消してもチェックは通りません。** +**版数を書いた箇所**です。定義ファイルが 5 箇所、説明文書の本文が +5 箇所あります(`README.md` の概要とプラグイン一覧表、正本に 1 箇所、 +`plugins/ndf/README.md` の Codex の確認例 2 種類)。**記載を消してもチェックは通りません。** 一覧は上の 2 つの表にあります。 ## チェックに載らず手で直す箇所 @@ -340,12 +312,12 @@ CHANGELOG.md を参照する固定の「`## 更新情報`」の節だけを持 そのまま残す。現行版を指しているかは文脈で決まる。**チェックが突き合わせるのは、周囲の固定の語で 位置を決めた記載だけである。** 履歴と記録は最初から走査に入らない。 -**説明文書の本文に書かれた版数は取りこぼしやすい。** 上の 7 箇所を +**説明文書の本文に書かれた版数は取りこぼしやすい。** 上の本文の 5 箇所を チェックの対象へ入れ、この取りこぼしを機械が拾う。 ## 取得元の登録を確かめる -**取得元の登録そのものを確かめるときは、設定ディレクトリを隔離する。** 「ランタイムごとの取得と導入」の手元での確認の 3 つは配布物を +**取得元の登録そのものを確かめるときは、設定ディレクトリを隔離する。** 「ランタイムごとの取得と導入」の手元での確認(`claude --plugin-dir`)は配布物を 直接読み込む確認で、取得元の登録から導入までの経路は通らない。開発版チャネルの取得手順は その経路そのものであるため、別の設定ディレクトリを与えて確かめる。**利用者の登録には触れない** (claude 2.1.261 / codex-cli 0.153.2 で実測)。 diff --git a/issues/issue-2228-design.md b/issues/issue-2228-design.md index 111b34635..b5a9a4d75 100644 --- a/issues/issue-2228-design.md +++ b/issues/issue-2228-design.md @@ -104,7 +104,7 @@ | `scripts/pr-steps.py` | `upsert` で、宛先(新しく作るなら `--base` か起点、既存なら `baseRefName`)が `merge_closes_issues` に当たれば本文を `defer_closing` に通す。置き換えた指し先を `metrics.deferred_closing` に載せる | | `scripts/supervise_lib/pr.py` | `_publish` で同じ判定と置き換えを行ってから `gh pr create` / `gh pr edit` へ渡す | | `scripts/supervise_lib/sprint_close_steps.py`・`sprint.py`・`release_templates.py` | まとめのプランの並びを `merge →(release-verify)→ retro → close → refine` にする。`close` は `--final-stage 振り返り` を明示し、`retro` の指示に「課題を閉じない(次の close が閉じる)」を足す | -| `tests/fixtures/supervise-contract/plans/close/4-close.json` | まとめのプランの契約の写しを新しい並びに合わせる | +| `plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/close/4-close.json` | まとめのプランの契約の写しを新しい並びに合わせる | | `skills/progress-tracking/SKILL.md` の「スプリントを閉じる」 | 閉じる時点の正本。閉じる条件 3・`--final-stage` の値と呼ぶ Skill の対応・閉じる予定の参照・プランの並びを持つ | | `skills/pr/SKILL.md` の「closing keywords は本文だけに書く」 | 既定ブランチ宛では NDF の手順が置き換えることを書き、閉じる時点は正本を指す | | `skills/release-verification/SKILL.md`・`retrospective`・`release`・`merged` と `development-workflow` の references(`pace.md`・`loop.md`・`stage-completeness.md`・`glossary.md`) | 閉じる時点・順序・閉じる条件の写しを外し、正本を指す。順序の説明の文を新しい並びに直す | diff --git a/plugins/mcp/mcp-aws-docs/README.md b/plugins/mcp/mcp-aws-docs/README.md index e0c24c9ff..184063988 100644 --- a/plugins/mcp/mcp-aws-docs/README.md +++ b/plugins/mcp/mcp-aws-docs/README.md @@ -31,14 +31,6 @@ AWS公式ドキュメントにアクセスするためのMCPサーバープラ codex plugin add mcp-aws-docs@ai-plugins ``` -### Kiro CLI - -Kiro では repository clone 後、対象 plugin の installer を project root で実行します。 - -```bash -bash plugins/mcp/mcp-aws-docs/dev.kiro/install.sh -``` - ### 必要な環境変数 - 追加の環境変数は不要です。 diff --git a/plugins/mcp/mcp-aws-docs/dev.kiro/install.sh b/plugins/mcp/mcp-aws-docs/dev.kiro/install.sh deleted file mode 100755 index 9b57feca6..000000000 --- a/plugins/mcp/mcp-aws-docs/dev.kiro/install.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env bash -# MCP Plugin Installer for Kiro CLI -# Usage: bash plugins/mcp/<プラグイン名>/dev.kiro/install.sh [--project PATH] [--dry-run] -# -# scripts/build-runtime-plugins.sh が生成します。直接編集しないでください。 -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -PLUGIN_NAME="$(basename "$PLUGIN_DIR")" -PROJECT_ROOT="$(pwd)" -KIRO_DIR="$PROJECT_ROOT/.kiro" -KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" -KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" -KIRO_SKILLS_DIR="$KIRO_DIR/skills" -TARGET_MCP="$PROJECT_ROOT/.mcp.json" -SOURCE_MCP="$PLUGIN_DIR/.mcp.json" -SOURCE_HOOKS="$PLUGIN_DIR/hooks/hooks.json" -SOURCE_SKILLS="$PLUGIN_DIR/skills" -DRY_RUN=false - -usage() { - cat <<'USAGE' -Usage: bash install.sh [--project PATH] [--dry-run] - -Install this MCP plugin into the current project's Kiro settings. -USAGE -} - -while [ "$#" -gt 0 ]; do - case "$1" in - --project) - [ "$#" -ge 2 ] || { echo "ERROR: --project requires a path" >&2; exit 2; } - PROJECT_ROOT="$(cd "$2" && pwd)" - KIRO_DIR="$PROJECT_ROOT/.kiro" - KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" - KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" - KIRO_SKILLS_DIR="$KIRO_DIR/skills" - TARGET_MCP="$PROJECT_ROOT/.mcp.json" - shift - ;; - --dry-run) DRY_RUN=true ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; - esac - shift -done - -if [ ! -f "$SOURCE_MCP" ]; then - echo "ERROR: source .mcp.json not found: $SOURCE_MCP" >&2 - exit 1 -fi - -if [ "$DRY_RUN" = true ]; then - echo "Would merge $SOURCE_MCP into $TARGET_MCP" - echo "Would link plugin runtime into $KIRO_PLUGIN_LINK" - if [ -d "$SOURCE_SKILLS" ]; then - echo "Would link skills from $SOURCE_SKILLS into $KIRO_SKILLS_DIR" - fi - if [ -f "$SOURCE_HOOKS" ]; then - echo "Would merge hooks from $SOURCE_HOOKS into $KIRO_AGENT_FILE" - fi - exit 0 -fi - -mkdir -p "$KIRO_DIR/mcp_runtime" "$KIRO_SKILLS_DIR" "$(dirname "$KIRO_AGENT_FILE")" -if [ -e "$KIRO_PLUGIN_LINK" ] && [ ! -L "$KIRO_PLUGIN_LINK" ]; then - echo "ERROR: $KIRO_PLUGIN_LINK already exists and is not a symlink" >&2 - exit 1 -fi -ln -sfn "$PLUGIN_DIR" "$KIRO_PLUGIN_LINK" - -if [ -d "$SOURCE_SKILLS" ]; then - find "$SOURCE_SKILLS" -mindepth 1 -maxdepth 1 -type d | sort | while IFS= read -r skill_dir; do - skill_name="$(basename "$skill_dir")" - skill_link="$KIRO_SKILLS_DIR/$skill_name" - [ -f "$skill_dir/SKILL.md" ] || continue - if [ -e "$skill_link" ] && [ ! -L "$skill_link" ]; then - echo "ERROR: $skill_link already exists and is not a symlink" >&2 - exit 1 - fi - ln -sfn "$skill_dir" "$skill_link" - echo "Linked Kiro skill: $skill_name" - done -fi - -python3 - "$SOURCE_MCP" "$TARGET_MCP" "$SOURCE_HOOKS" "$KIRO_AGENT_FILE" "$KIRO_PLUGIN_LINK" <<'PY' -import json -import sys -from pathlib import Path - -source_path = Path(sys.argv[1]) -target_path = Path(sys.argv[2]) -hooks_path = Path(sys.argv[3]) -agent_path = Path(sys.argv[4]) -plugin_link = sys.argv[5] -source = json.loads(source_path.read_text()) -target = json.loads(target_path.read_text()) if target_path.exists() else {} -source_servers = source.get("mcpServers", {}) -target.setdefault("mcpServers", {}) -target["mcpServers"].update(source_servers) -target_path.write_text(json.dumps(target, indent=2, ensure_ascii=False) + "\n") - -if agent_path.exists(): - agent = json.loads(agent_path.read_text()) -else: - agent = { - "name": "default", - "resources": ["skill://.kiro/skills/**/SKILL.md"], - } - -# Kiro は tools に挙がったツールだけをモデルへ見せる。欄が無いと MCP のツールを -# 呼べない(#1873)。利用者が書いた tools は上書きしない。 -agent.setdefault("tools", ["*"]) -agent.setdefault("mcpServers", {}) -agent["mcpServers"].update(source_servers) - -if hooks_path.exists(): - source_hooks = json.loads(hooks_path.read_text()).get("hooks", {}) - target_hooks = agent.setdefault("hooks", {}) - event_map = { - "SessionStart": "agentSpawn", - "Stop": "stop", - } - # Kiro にはプラグインルートを示す環境変数が無い。hook の command が使う - # プレースホルダを、installer が張った symlink の絶対パスへ置き換える。 - placeholder = "${PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}" - for source_event, groups in source_hooks.items(): - target_event = event_map.get(source_event, source_event) - target_list = target_hooks.setdefault(target_event, []) - existing = { - entry.get("command") - for entry in target_list - if isinstance(entry, dict) - } - for group in groups: - for hook in group.get("hooks", []): - command = hook.get("command") - if not command: - continue - command = command.replace(placeholder, plugin_link) - if command in existing: - continue - entry = {"command": command} - if "timeout_ms" in hook: - entry["timeout_ms"] = hook["timeout_ms"] - target_list.append(entry) - existing.add(command) - -agent_path.write_text(json.dumps(agent, indent=2, ensure_ascii=False) + "\n") - -print(f"Installed MCP servers: {', '.join(sorted(source_servers))}") -print(f"Updated: {target_path}") -print(f"Updated: {agent_path}") -PY diff --git a/plugins/mcp/mcp-bigquery/README.md b/plugins/mcp/mcp-bigquery/README.md index 91931676f..0fc4bfcc9 100644 --- a/plugins/mcp/mcp-bigquery/README.md +++ b/plugins/mcp/mcp-bigquery/README.md @@ -1,7 +1,7 @@ # BigQuery MCP Google が運用するリモートの BigQuery MCP サーバー(`https://bigquery.googleapis.com/mcp`)を -Claude Code・Codex・Kiro CLI から使うためのプラグインです。 +Claude Code・Codex から使うためのプラグインです。 ## 概要 @@ -64,19 +64,17 @@ Claude Code・Codex・Kiro CLI から使うためのプラグインです。 |---|---|---| | Claude Code | `~/.claude/settings.json`(またはプロジェクトの `.claude/settings.json`) | `"permissions": {"deny": ["mcp__plugin_mcp-bigquery_bigquery__execute_sql", "mcp__bigquery__execute_sql"]}` | | Codex | `~/.codex/config.toml` | 下の例 | -| Kiro CLI | `.kiro/agents/default.json` の `mcpServers.bigquery` | `"disabledTools": ["execute_sql"]`(`install.sh` を流し直したら書き直す) | ```toml [plugins."mcp-bigquery@ai-plugins".mcp_servers.bigquery] disabled_tools = ["execute_sql"] ``` -Claude Code の 2 つ目の名前は、Kiro CLI の `install.sh` がプロジェクト直下の `.mcp.json` にも -`bigquery` を書くためです。同じプロジェクトで Claude Code を使うと、プロジェクトのサーバーとして +Claude Code の 2 つ目の名前は、前の版の Kiro CLI 向けの `install.sh` がプロジェクト直下の `.mcp.json` にも +`bigquery` を書いたためです。同じプロジェクトで Claude Code を使うと、プロジェクトのサーバーとして `mcp__bigquery__execute_sql` の名前で読まれ、1 つ目の名前の deny では止まりません。 -どれもツールの一覧から `execute_sql` が消えます(Claude Code 2.1.295・Codex 0.160.0・ -kiro-cli 2.24.1 で確認)。組織として止めるときは、IAM の deny ポリシーで `execute_sql` を禁じます。 +どれもツールの一覧から `execute_sql` が消えます(Claude Code 2.1.295・Codex 0.160.0 で確認)。組織として止めるときは、IAM の deny ポリシーで `execute_sql` を禁じます。 ## ランタイムごとの認証 @@ -86,13 +84,6 @@ kiro-cli 2.24.1 で確認)。組織として止めるときは、IAM の deny |---|---|---| | Claude Code | `headersHelper` | 接続のたびに `gcloud auth print-access-token` を実行する(401/403 でも取り直す) | | Codex | `http_headers_helper` | 同上 | -| Kiro CLI | `headers` の `${BIGQUERY_ACCESS_TOKEN}` | **自動では取り直さない**。起動前に環境変数へ入れる | - -Kiro CLI では、起動の前に次を実行してください。1 時間たったら入れ直して再起動します。 - -```bash -export BIGQUERY_ACCESS_TOKEN=$(gcloud auth print-access-token) -``` Codex は `http_headers_helper` を、`HOME`・`PATH` など限られた環境変数だけを渡して実行します。 `CLOUDSDK_CONFIG` で gcloud の設定の場所を変えていると、helper の中の gcloud は既定の場所 @@ -114,7 +105,7 @@ v2 までのローカルサーバー(`mcp-server-bigquery`)をやめ、リ |---|---|---| | サーバー | `uvx mcp-server-bigquery`(stdio) | `https://bigquery.googleapis.com/mcp`(HTTP) | | 認証 | サービスアカウントの鍵ファイル | gcloud にログインした利用者のトークン | -| 環境変数 | `BIGQUERY_PROJECT` `BIGQUERY_LOCATION` `BIGQUERY_DATASET` `BIGQUERY_KEY_FILE` | 不要(Kiro CLI のみ `BIGQUERY_ACCESS_TOKEN`) | +| 環境変数 | `BIGQUERY_PROJECT` `BIGQUERY_LOCATION` `BIGQUERY_DATASET` `BIGQUERY_KEY_FILE` | 不要 | | ツール名 | `execute-query` `list-tables` `describe-table` | `execute_sql_readonly` `list_table_ids` `get_table_info` など | v2 は依存する `mcp` パッケージの 2 系で起動できなくなっていました(`'Server' object has no @@ -134,14 +125,6 @@ attribute 'list_tools'`)。 codex plugin add mcp-bigquery@ai-plugins ``` -### Kiro CLI - -Kiro では repository clone 後、対象 plugin の installer を project root で実行します。 - -```bash -bash plugins/mcp/mcp-bigquery/dev.kiro/install.sh -``` - ## 更新情報 版ごとの変更と移行の手順は [CHANGELOG.md](https://github.com/devbasex/ai-plugins/blob/main/CHANGELOG.md) の `[mcp-bigquery <版>]` の節にあります。 diff --git a/plugins/mcp/mcp-bigquery/dev.kiro/install.sh b/plugins/mcp/mcp-bigquery/dev.kiro/install.sh deleted file mode 100755 index 9b57feca6..000000000 --- a/plugins/mcp/mcp-bigquery/dev.kiro/install.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env bash -# MCP Plugin Installer for Kiro CLI -# Usage: bash plugins/mcp/<プラグイン名>/dev.kiro/install.sh [--project PATH] [--dry-run] -# -# scripts/build-runtime-plugins.sh が生成します。直接編集しないでください。 -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -PLUGIN_NAME="$(basename "$PLUGIN_DIR")" -PROJECT_ROOT="$(pwd)" -KIRO_DIR="$PROJECT_ROOT/.kiro" -KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" -KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" -KIRO_SKILLS_DIR="$KIRO_DIR/skills" -TARGET_MCP="$PROJECT_ROOT/.mcp.json" -SOURCE_MCP="$PLUGIN_DIR/.mcp.json" -SOURCE_HOOKS="$PLUGIN_DIR/hooks/hooks.json" -SOURCE_SKILLS="$PLUGIN_DIR/skills" -DRY_RUN=false - -usage() { - cat <<'USAGE' -Usage: bash install.sh [--project PATH] [--dry-run] - -Install this MCP plugin into the current project's Kiro settings. -USAGE -} - -while [ "$#" -gt 0 ]; do - case "$1" in - --project) - [ "$#" -ge 2 ] || { echo "ERROR: --project requires a path" >&2; exit 2; } - PROJECT_ROOT="$(cd "$2" && pwd)" - KIRO_DIR="$PROJECT_ROOT/.kiro" - KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" - KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" - KIRO_SKILLS_DIR="$KIRO_DIR/skills" - TARGET_MCP="$PROJECT_ROOT/.mcp.json" - shift - ;; - --dry-run) DRY_RUN=true ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; - esac - shift -done - -if [ ! -f "$SOURCE_MCP" ]; then - echo "ERROR: source .mcp.json not found: $SOURCE_MCP" >&2 - exit 1 -fi - -if [ "$DRY_RUN" = true ]; then - echo "Would merge $SOURCE_MCP into $TARGET_MCP" - echo "Would link plugin runtime into $KIRO_PLUGIN_LINK" - if [ -d "$SOURCE_SKILLS" ]; then - echo "Would link skills from $SOURCE_SKILLS into $KIRO_SKILLS_DIR" - fi - if [ -f "$SOURCE_HOOKS" ]; then - echo "Would merge hooks from $SOURCE_HOOKS into $KIRO_AGENT_FILE" - fi - exit 0 -fi - -mkdir -p "$KIRO_DIR/mcp_runtime" "$KIRO_SKILLS_DIR" "$(dirname "$KIRO_AGENT_FILE")" -if [ -e "$KIRO_PLUGIN_LINK" ] && [ ! -L "$KIRO_PLUGIN_LINK" ]; then - echo "ERROR: $KIRO_PLUGIN_LINK already exists and is not a symlink" >&2 - exit 1 -fi -ln -sfn "$PLUGIN_DIR" "$KIRO_PLUGIN_LINK" - -if [ -d "$SOURCE_SKILLS" ]; then - find "$SOURCE_SKILLS" -mindepth 1 -maxdepth 1 -type d | sort | while IFS= read -r skill_dir; do - skill_name="$(basename "$skill_dir")" - skill_link="$KIRO_SKILLS_DIR/$skill_name" - [ -f "$skill_dir/SKILL.md" ] || continue - if [ -e "$skill_link" ] && [ ! -L "$skill_link" ]; then - echo "ERROR: $skill_link already exists and is not a symlink" >&2 - exit 1 - fi - ln -sfn "$skill_dir" "$skill_link" - echo "Linked Kiro skill: $skill_name" - done -fi - -python3 - "$SOURCE_MCP" "$TARGET_MCP" "$SOURCE_HOOKS" "$KIRO_AGENT_FILE" "$KIRO_PLUGIN_LINK" <<'PY' -import json -import sys -from pathlib import Path - -source_path = Path(sys.argv[1]) -target_path = Path(sys.argv[2]) -hooks_path = Path(sys.argv[3]) -agent_path = Path(sys.argv[4]) -plugin_link = sys.argv[5] -source = json.loads(source_path.read_text()) -target = json.loads(target_path.read_text()) if target_path.exists() else {} -source_servers = source.get("mcpServers", {}) -target.setdefault("mcpServers", {}) -target["mcpServers"].update(source_servers) -target_path.write_text(json.dumps(target, indent=2, ensure_ascii=False) + "\n") - -if agent_path.exists(): - agent = json.loads(agent_path.read_text()) -else: - agent = { - "name": "default", - "resources": ["skill://.kiro/skills/**/SKILL.md"], - } - -# Kiro は tools に挙がったツールだけをモデルへ見せる。欄が無いと MCP のツールを -# 呼べない(#1873)。利用者が書いた tools は上書きしない。 -agent.setdefault("tools", ["*"]) -agent.setdefault("mcpServers", {}) -agent["mcpServers"].update(source_servers) - -if hooks_path.exists(): - source_hooks = json.loads(hooks_path.read_text()).get("hooks", {}) - target_hooks = agent.setdefault("hooks", {}) - event_map = { - "SessionStart": "agentSpawn", - "Stop": "stop", - } - # Kiro にはプラグインルートを示す環境変数が無い。hook の command が使う - # プレースホルダを、installer が張った symlink の絶対パスへ置き換える。 - placeholder = "${PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}" - for source_event, groups in source_hooks.items(): - target_event = event_map.get(source_event, source_event) - target_list = target_hooks.setdefault(target_event, []) - existing = { - entry.get("command") - for entry in target_list - if isinstance(entry, dict) - } - for group in groups: - for hook in group.get("hooks", []): - command = hook.get("command") - if not command: - continue - command = command.replace(placeholder, plugin_link) - if command in existing: - continue - entry = {"command": command} - if "timeout_ms" in hook: - entry["timeout_ms"] = hook["timeout_ms"] - target_list.append(entry) - existing.add(command) - -agent_path.write_text(json.dumps(agent, indent=2, ensure_ascii=False) + "\n") - -print(f"Installed MCP servers: {', '.join(sorted(source_servers))}") -print(f"Updated: {target_path}") -print(f"Updated: {agent_path}") -PY diff --git a/plugins/mcp/mcp-chrome-devtools/README.md b/plugins/mcp/mcp-chrome-devtools/README.md index 872f67511..c77e14bac 100644 --- a/plugins/mcp/mcp-chrome-devtools/README.md +++ b/plugins/mcp/mcp-chrome-devtools/README.md @@ -33,14 +33,6 @@ Chrome DevTools MCPサーバーをClaude Codeで利用するためのプラグ codex plugin add mcp-chrome-devtools@ai-plugins ``` -### Kiro CLI - -Kiro では repository clone 後、対象 plugin の installer を project root で実行します。 - -```bash -bash plugins/mcp/mcp-chrome-devtools/dev.kiro/install.sh -``` - ### 必要な環境変数 - 追加の環境変数は不要です。 diff --git a/plugins/mcp/mcp-chrome-devtools/dev.kiro/install.sh b/plugins/mcp/mcp-chrome-devtools/dev.kiro/install.sh deleted file mode 100755 index 9b57feca6..000000000 --- a/plugins/mcp/mcp-chrome-devtools/dev.kiro/install.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env bash -# MCP Plugin Installer for Kiro CLI -# Usage: bash plugins/mcp/<プラグイン名>/dev.kiro/install.sh [--project PATH] [--dry-run] -# -# scripts/build-runtime-plugins.sh が生成します。直接編集しないでください。 -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -PLUGIN_NAME="$(basename "$PLUGIN_DIR")" -PROJECT_ROOT="$(pwd)" -KIRO_DIR="$PROJECT_ROOT/.kiro" -KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" -KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" -KIRO_SKILLS_DIR="$KIRO_DIR/skills" -TARGET_MCP="$PROJECT_ROOT/.mcp.json" -SOURCE_MCP="$PLUGIN_DIR/.mcp.json" -SOURCE_HOOKS="$PLUGIN_DIR/hooks/hooks.json" -SOURCE_SKILLS="$PLUGIN_DIR/skills" -DRY_RUN=false - -usage() { - cat <<'USAGE' -Usage: bash install.sh [--project PATH] [--dry-run] - -Install this MCP plugin into the current project's Kiro settings. -USAGE -} - -while [ "$#" -gt 0 ]; do - case "$1" in - --project) - [ "$#" -ge 2 ] || { echo "ERROR: --project requires a path" >&2; exit 2; } - PROJECT_ROOT="$(cd "$2" && pwd)" - KIRO_DIR="$PROJECT_ROOT/.kiro" - KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" - KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" - KIRO_SKILLS_DIR="$KIRO_DIR/skills" - TARGET_MCP="$PROJECT_ROOT/.mcp.json" - shift - ;; - --dry-run) DRY_RUN=true ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; - esac - shift -done - -if [ ! -f "$SOURCE_MCP" ]; then - echo "ERROR: source .mcp.json not found: $SOURCE_MCP" >&2 - exit 1 -fi - -if [ "$DRY_RUN" = true ]; then - echo "Would merge $SOURCE_MCP into $TARGET_MCP" - echo "Would link plugin runtime into $KIRO_PLUGIN_LINK" - if [ -d "$SOURCE_SKILLS" ]; then - echo "Would link skills from $SOURCE_SKILLS into $KIRO_SKILLS_DIR" - fi - if [ -f "$SOURCE_HOOKS" ]; then - echo "Would merge hooks from $SOURCE_HOOKS into $KIRO_AGENT_FILE" - fi - exit 0 -fi - -mkdir -p "$KIRO_DIR/mcp_runtime" "$KIRO_SKILLS_DIR" "$(dirname "$KIRO_AGENT_FILE")" -if [ -e "$KIRO_PLUGIN_LINK" ] && [ ! -L "$KIRO_PLUGIN_LINK" ]; then - echo "ERROR: $KIRO_PLUGIN_LINK already exists and is not a symlink" >&2 - exit 1 -fi -ln -sfn "$PLUGIN_DIR" "$KIRO_PLUGIN_LINK" - -if [ -d "$SOURCE_SKILLS" ]; then - find "$SOURCE_SKILLS" -mindepth 1 -maxdepth 1 -type d | sort | while IFS= read -r skill_dir; do - skill_name="$(basename "$skill_dir")" - skill_link="$KIRO_SKILLS_DIR/$skill_name" - [ -f "$skill_dir/SKILL.md" ] || continue - if [ -e "$skill_link" ] && [ ! -L "$skill_link" ]; then - echo "ERROR: $skill_link already exists and is not a symlink" >&2 - exit 1 - fi - ln -sfn "$skill_dir" "$skill_link" - echo "Linked Kiro skill: $skill_name" - done -fi - -python3 - "$SOURCE_MCP" "$TARGET_MCP" "$SOURCE_HOOKS" "$KIRO_AGENT_FILE" "$KIRO_PLUGIN_LINK" <<'PY' -import json -import sys -from pathlib import Path - -source_path = Path(sys.argv[1]) -target_path = Path(sys.argv[2]) -hooks_path = Path(sys.argv[3]) -agent_path = Path(sys.argv[4]) -plugin_link = sys.argv[5] -source = json.loads(source_path.read_text()) -target = json.loads(target_path.read_text()) if target_path.exists() else {} -source_servers = source.get("mcpServers", {}) -target.setdefault("mcpServers", {}) -target["mcpServers"].update(source_servers) -target_path.write_text(json.dumps(target, indent=2, ensure_ascii=False) + "\n") - -if agent_path.exists(): - agent = json.loads(agent_path.read_text()) -else: - agent = { - "name": "default", - "resources": ["skill://.kiro/skills/**/SKILL.md"], - } - -# Kiro は tools に挙がったツールだけをモデルへ見せる。欄が無いと MCP のツールを -# 呼べない(#1873)。利用者が書いた tools は上書きしない。 -agent.setdefault("tools", ["*"]) -agent.setdefault("mcpServers", {}) -agent["mcpServers"].update(source_servers) - -if hooks_path.exists(): - source_hooks = json.loads(hooks_path.read_text()).get("hooks", {}) - target_hooks = agent.setdefault("hooks", {}) - event_map = { - "SessionStart": "agentSpawn", - "Stop": "stop", - } - # Kiro にはプラグインルートを示す環境変数が無い。hook の command が使う - # プレースホルダを、installer が張った symlink の絶対パスへ置き換える。 - placeholder = "${PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}" - for source_event, groups in source_hooks.items(): - target_event = event_map.get(source_event, source_event) - target_list = target_hooks.setdefault(target_event, []) - existing = { - entry.get("command") - for entry in target_list - if isinstance(entry, dict) - } - for group in groups: - for hook in group.get("hooks", []): - command = hook.get("command") - if not command: - continue - command = command.replace(placeholder, plugin_link) - if command in existing: - continue - entry = {"command": command} - if "timeout_ms" in hook: - entry["timeout_ms"] = hook["timeout_ms"] - target_list.append(entry) - existing.add(command) - -agent_path.write_text(json.dumps(agent, indent=2, ensure_ascii=False) + "\n") - -print(f"Installed MCP servers: {', '.join(sorted(source_servers))}") -print(f"Updated: {target_path}") -print(f"Updated: {agent_path}") -PY diff --git a/plugins/mcp/mcp-dbhub/README.md b/plugins/mcp/mcp-dbhub/README.md index c2942ad70..0ac5bfddf 100644 --- a/plugins/mcp/mcp-dbhub/README.md +++ b/plugins/mcp/mcp-dbhub/README.md @@ -32,14 +32,6 @@ codex plugin add mcp-dbhub@ai-plugins ``` -### Kiro CLI - -Kiro では repository clone 後、対象 plugin の installer を project root で実行します。 - -```bash -bash plugins/mcp/mcp-dbhub/dev.kiro/install.sh -``` - ### 必要な環境変数 - 追加の環境変数は不要です。 @@ -54,9 +46,6 @@ bash plugins/mcp/mcp-dbhub/dev.kiro/install.sh # Codex codex plugin marketplace upgrade ai-plugins codex plugin add mcp-dbhub@ai-plugins - -# Kiro CLI -bash plugins/mcp/mcp-dbhub/dev.kiro/install.sh ``` ## 設定 diff --git a/plugins/mcp/mcp-dbhub/dev.kiro/install.sh b/plugins/mcp/mcp-dbhub/dev.kiro/install.sh deleted file mode 100755 index 9b57feca6..000000000 --- a/plugins/mcp/mcp-dbhub/dev.kiro/install.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env bash -# MCP Plugin Installer for Kiro CLI -# Usage: bash plugins/mcp/<プラグイン名>/dev.kiro/install.sh [--project PATH] [--dry-run] -# -# scripts/build-runtime-plugins.sh が生成します。直接編集しないでください。 -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -PLUGIN_NAME="$(basename "$PLUGIN_DIR")" -PROJECT_ROOT="$(pwd)" -KIRO_DIR="$PROJECT_ROOT/.kiro" -KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" -KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" -KIRO_SKILLS_DIR="$KIRO_DIR/skills" -TARGET_MCP="$PROJECT_ROOT/.mcp.json" -SOURCE_MCP="$PLUGIN_DIR/.mcp.json" -SOURCE_HOOKS="$PLUGIN_DIR/hooks/hooks.json" -SOURCE_SKILLS="$PLUGIN_DIR/skills" -DRY_RUN=false - -usage() { - cat <<'USAGE' -Usage: bash install.sh [--project PATH] [--dry-run] - -Install this MCP plugin into the current project's Kiro settings. -USAGE -} - -while [ "$#" -gt 0 ]; do - case "$1" in - --project) - [ "$#" -ge 2 ] || { echo "ERROR: --project requires a path" >&2; exit 2; } - PROJECT_ROOT="$(cd "$2" && pwd)" - KIRO_DIR="$PROJECT_ROOT/.kiro" - KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" - KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" - KIRO_SKILLS_DIR="$KIRO_DIR/skills" - TARGET_MCP="$PROJECT_ROOT/.mcp.json" - shift - ;; - --dry-run) DRY_RUN=true ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; - esac - shift -done - -if [ ! -f "$SOURCE_MCP" ]; then - echo "ERROR: source .mcp.json not found: $SOURCE_MCP" >&2 - exit 1 -fi - -if [ "$DRY_RUN" = true ]; then - echo "Would merge $SOURCE_MCP into $TARGET_MCP" - echo "Would link plugin runtime into $KIRO_PLUGIN_LINK" - if [ -d "$SOURCE_SKILLS" ]; then - echo "Would link skills from $SOURCE_SKILLS into $KIRO_SKILLS_DIR" - fi - if [ -f "$SOURCE_HOOKS" ]; then - echo "Would merge hooks from $SOURCE_HOOKS into $KIRO_AGENT_FILE" - fi - exit 0 -fi - -mkdir -p "$KIRO_DIR/mcp_runtime" "$KIRO_SKILLS_DIR" "$(dirname "$KIRO_AGENT_FILE")" -if [ -e "$KIRO_PLUGIN_LINK" ] && [ ! -L "$KIRO_PLUGIN_LINK" ]; then - echo "ERROR: $KIRO_PLUGIN_LINK already exists and is not a symlink" >&2 - exit 1 -fi -ln -sfn "$PLUGIN_DIR" "$KIRO_PLUGIN_LINK" - -if [ -d "$SOURCE_SKILLS" ]; then - find "$SOURCE_SKILLS" -mindepth 1 -maxdepth 1 -type d | sort | while IFS= read -r skill_dir; do - skill_name="$(basename "$skill_dir")" - skill_link="$KIRO_SKILLS_DIR/$skill_name" - [ -f "$skill_dir/SKILL.md" ] || continue - if [ -e "$skill_link" ] && [ ! -L "$skill_link" ]; then - echo "ERROR: $skill_link already exists and is not a symlink" >&2 - exit 1 - fi - ln -sfn "$skill_dir" "$skill_link" - echo "Linked Kiro skill: $skill_name" - done -fi - -python3 - "$SOURCE_MCP" "$TARGET_MCP" "$SOURCE_HOOKS" "$KIRO_AGENT_FILE" "$KIRO_PLUGIN_LINK" <<'PY' -import json -import sys -from pathlib import Path - -source_path = Path(sys.argv[1]) -target_path = Path(sys.argv[2]) -hooks_path = Path(sys.argv[3]) -agent_path = Path(sys.argv[4]) -plugin_link = sys.argv[5] -source = json.loads(source_path.read_text()) -target = json.loads(target_path.read_text()) if target_path.exists() else {} -source_servers = source.get("mcpServers", {}) -target.setdefault("mcpServers", {}) -target["mcpServers"].update(source_servers) -target_path.write_text(json.dumps(target, indent=2, ensure_ascii=False) + "\n") - -if agent_path.exists(): - agent = json.loads(agent_path.read_text()) -else: - agent = { - "name": "default", - "resources": ["skill://.kiro/skills/**/SKILL.md"], - } - -# Kiro は tools に挙がったツールだけをモデルへ見せる。欄が無いと MCP のツールを -# 呼べない(#1873)。利用者が書いた tools は上書きしない。 -agent.setdefault("tools", ["*"]) -agent.setdefault("mcpServers", {}) -agent["mcpServers"].update(source_servers) - -if hooks_path.exists(): - source_hooks = json.loads(hooks_path.read_text()).get("hooks", {}) - target_hooks = agent.setdefault("hooks", {}) - event_map = { - "SessionStart": "agentSpawn", - "Stop": "stop", - } - # Kiro にはプラグインルートを示す環境変数が無い。hook の command が使う - # プレースホルダを、installer が張った symlink の絶対パスへ置き換える。 - placeholder = "${PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}" - for source_event, groups in source_hooks.items(): - target_event = event_map.get(source_event, source_event) - target_list = target_hooks.setdefault(target_event, []) - existing = { - entry.get("command") - for entry in target_list - if isinstance(entry, dict) - } - for group in groups: - for hook in group.get("hooks", []): - command = hook.get("command") - if not command: - continue - command = command.replace(placeholder, plugin_link) - if command in existing: - continue - entry = {"command": command} - if "timeout_ms" in hook: - entry["timeout_ms"] = hook["timeout_ms"] - target_list.append(entry) - existing.add(command) - -agent_path.write_text(json.dumps(agent, indent=2, ensure_ascii=False) + "\n") - -print(f"Installed MCP servers: {', '.join(sorted(source_servers))}") -print(f"Updated: {target_path}") -print(f"Updated: {agent_path}") -PY diff --git a/plugins/mcp/mcp-devin/README.md b/plugins/mcp/mcp-devin/README.md index ff081d50e..e1a8d8965 100644 --- a/plugins/mcp/mcp-devin/README.md +++ b/plugins/mcp/mcp-devin/README.md @@ -43,14 +43,6 @@ DEVIN_API_KEY=your-devin-api-key codex plugin add mcp-devin@ai-plugins ``` -### Kiro CLI - -Kiro では repository clone 後、対象 plugin の installer を project root で実行します。 - -```bash -bash plugins/mcp/mcp-devin/dev.kiro/install.sh -``` - ### 必要な環境変数 - `DEVIN_API_KEY` diff --git a/plugins/mcp/mcp-devin/dev.kiro/install.sh b/plugins/mcp/mcp-devin/dev.kiro/install.sh deleted file mode 100755 index 9b57feca6..000000000 --- a/plugins/mcp/mcp-devin/dev.kiro/install.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env bash -# MCP Plugin Installer for Kiro CLI -# Usage: bash plugins/mcp/<プラグイン名>/dev.kiro/install.sh [--project PATH] [--dry-run] -# -# scripts/build-runtime-plugins.sh が生成します。直接編集しないでください。 -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -PLUGIN_NAME="$(basename "$PLUGIN_DIR")" -PROJECT_ROOT="$(pwd)" -KIRO_DIR="$PROJECT_ROOT/.kiro" -KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" -KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" -KIRO_SKILLS_DIR="$KIRO_DIR/skills" -TARGET_MCP="$PROJECT_ROOT/.mcp.json" -SOURCE_MCP="$PLUGIN_DIR/.mcp.json" -SOURCE_HOOKS="$PLUGIN_DIR/hooks/hooks.json" -SOURCE_SKILLS="$PLUGIN_DIR/skills" -DRY_RUN=false - -usage() { - cat <<'USAGE' -Usage: bash install.sh [--project PATH] [--dry-run] - -Install this MCP plugin into the current project's Kiro settings. -USAGE -} - -while [ "$#" -gt 0 ]; do - case "$1" in - --project) - [ "$#" -ge 2 ] || { echo "ERROR: --project requires a path" >&2; exit 2; } - PROJECT_ROOT="$(cd "$2" && pwd)" - KIRO_DIR="$PROJECT_ROOT/.kiro" - KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" - KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" - KIRO_SKILLS_DIR="$KIRO_DIR/skills" - TARGET_MCP="$PROJECT_ROOT/.mcp.json" - shift - ;; - --dry-run) DRY_RUN=true ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; - esac - shift -done - -if [ ! -f "$SOURCE_MCP" ]; then - echo "ERROR: source .mcp.json not found: $SOURCE_MCP" >&2 - exit 1 -fi - -if [ "$DRY_RUN" = true ]; then - echo "Would merge $SOURCE_MCP into $TARGET_MCP" - echo "Would link plugin runtime into $KIRO_PLUGIN_LINK" - if [ -d "$SOURCE_SKILLS" ]; then - echo "Would link skills from $SOURCE_SKILLS into $KIRO_SKILLS_DIR" - fi - if [ -f "$SOURCE_HOOKS" ]; then - echo "Would merge hooks from $SOURCE_HOOKS into $KIRO_AGENT_FILE" - fi - exit 0 -fi - -mkdir -p "$KIRO_DIR/mcp_runtime" "$KIRO_SKILLS_DIR" "$(dirname "$KIRO_AGENT_FILE")" -if [ -e "$KIRO_PLUGIN_LINK" ] && [ ! -L "$KIRO_PLUGIN_LINK" ]; then - echo "ERROR: $KIRO_PLUGIN_LINK already exists and is not a symlink" >&2 - exit 1 -fi -ln -sfn "$PLUGIN_DIR" "$KIRO_PLUGIN_LINK" - -if [ -d "$SOURCE_SKILLS" ]; then - find "$SOURCE_SKILLS" -mindepth 1 -maxdepth 1 -type d | sort | while IFS= read -r skill_dir; do - skill_name="$(basename "$skill_dir")" - skill_link="$KIRO_SKILLS_DIR/$skill_name" - [ -f "$skill_dir/SKILL.md" ] || continue - if [ -e "$skill_link" ] && [ ! -L "$skill_link" ]; then - echo "ERROR: $skill_link already exists and is not a symlink" >&2 - exit 1 - fi - ln -sfn "$skill_dir" "$skill_link" - echo "Linked Kiro skill: $skill_name" - done -fi - -python3 - "$SOURCE_MCP" "$TARGET_MCP" "$SOURCE_HOOKS" "$KIRO_AGENT_FILE" "$KIRO_PLUGIN_LINK" <<'PY' -import json -import sys -from pathlib import Path - -source_path = Path(sys.argv[1]) -target_path = Path(sys.argv[2]) -hooks_path = Path(sys.argv[3]) -agent_path = Path(sys.argv[4]) -plugin_link = sys.argv[5] -source = json.loads(source_path.read_text()) -target = json.loads(target_path.read_text()) if target_path.exists() else {} -source_servers = source.get("mcpServers", {}) -target.setdefault("mcpServers", {}) -target["mcpServers"].update(source_servers) -target_path.write_text(json.dumps(target, indent=2, ensure_ascii=False) + "\n") - -if agent_path.exists(): - agent = json.loads(agent_path.read_text()) -else: - agent = { - "name": "default", - "resources": ["skill://.kiro/skills/**/SKILL.md"], - } - -# Kiro は tools に挙がったツールだけをモデルへ見せる。欄が無いと MCP のツールを -# 呼べない(#1873)。利用者が書いた tools は上書きしない。 -agent.setdefault("tools", ["*"]) -agent.setdefault("mcpServers", {}) -agent["mcpServers"].update(source_servers) - -if hooks_path.exists(): - source_hooks = json.loads(hooks_path.read_text()).get("hooks", {}) - target_hooks = agent.setdefault("hooks", {}) - event_map = { - "SessionStart": "agentSpawn", - "Stop": "stop", - } - # Kiro にはプラグインルートを示す環境変数が無い。hook の command が使う - # プレースホルダを、installer が張った symlink の絶対パスへ置き換える。 - placeholder = "${PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}" - for source_event, groups in source_hooks.items(): - target_event = event_map.get(source_event, source_event) - target_list = target_hooks.setdefault(target_event, []) - existing = { - entry.get("command") - for entry in target_list - if isinstance(entry, dict) - } - for group in groups: - for hook in group.get("hooks", []): - command = hook.get("command") - if not command: - continue - command = command.replace(placeholder, plugin_link) - if command in existing: - continue - entry = {"command": command} - if "timeout_ms" in hook: - entry["timeout_ms"] = hook["timeout_ms"] - target_list.append(entry) - existing.add(command) - -agent_path.write_text(json.dumps(agent, indent=2, ensure_ascii=False) + "\n") - -print(f"Installed MCP servers: {', '.join(sorted(source_servers))}") -print(f"Updated: {target_path}") -print(f"Updated: {agent_path}") -PY diff --git a/plugins/mcp/mcp-markitdown/README.md b/plugins/mcp/mcp-markitdown/README.md index 219bfbacd..5387eb5df 100644 --- a/plugins/mcp/mcp-markitdown/README.md +++ b/plugins/mcp/mcp-markitdown/README.md @@ -30,14 +30,6 @@ codex plugin add mcp-markitdown@ai-plugins ``` -### Kiro CLI - -Kiro では repository clone 後、対象 plugin の installer を project root で実行します。 - -```bash -bash plugins/mcp/mcp-markitdown/dev.kiro/install.sh -``` - ### 必要な環境変数 - 追加の環境変数は不要です。 diff --git a/plugins/mcp/mcp-markitdown/dev.kiro/install.sh b/plugins/mcp/mcp-markitdown/dev.kiro/install.sh deleted file mode 100755 index 9b57feca6..000000000 --- a/plugins/mcp/mcp-markitdown/dev.kiro/install.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env bash -# MCP Plugin Installer for Kiro CLI -# Usage: bash plugins/mcp/<プラグイン名>/dev.kiro/install.sh [--project PATH] [--dry-run] -# -# scripts/build-runtime-plugins.sh が生成します。直接編集しないでください。 -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -PLUGIN_NAME="$(basename "$PLUGIN_DIR")" -PROJECT_ROOT="$(pwd)" -KIRO_DIR="$PROJECT_ROOT/.kiro" -KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" -KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" -KIRO_SKILLS_DIR="$KIRO_DIR/skills" -TARGET_MCP="$PROJECT_ROOT/.mcp.json" -SOURCE_MCP="$PLUGIN_DIR/.mcp.json" -SOURCE_HOOKS="$PLUGIN_DIR/hooks/hooks.json" -SOURCE_SKILLS="$PLUGIN_DIR/skills" -DRY_RUN=false - -usage() { - cat <<'USAGE' -Usage: bash install.sh [--project PATH] [--dry-run] - -Install this MCP plugin into the current project's Kiro settings. -USAGE -} - -while [ "$#" -gt 0 ]; do - case "$1" in - --project) - [ "$#" -ge 2 ] || { echo "ERROR: --project requires a path" >&2; exit 2; } - PROJECT_ROOT="$(cd "$2" && pwd)" - KIRO_DIR="$PROJECT_ROOT/.kiro" - KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" - KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" - KIRO_SKILLS_DIR="$KIRO_DIR/skills" - TARGET_MCP="$PROJECT_ROOT/.mcp.json" - shift - ;; - --dry-run) DRY_RUN=true ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; - esac - shift -done - -if [ ! -f "$SOURCE_MCP" ]; then - echo "ERROR: source .mcp.json not found: $SOURCE_MCP" >&2 - exit 1 -fi - -if [ "$DRY_RUN" = true ]; then - echo "Would merge $SOURCE_MCP into $TARGET_MCP" - echo "Would link plugin runtime into $KIRO_PLUGIN_LINK" - if [ -d "$SOURCE_SKILLS" ]; then - echo "Would link skills from $SOURCE_SKILLS into $KIRO_SKILLS_DIR" - fi - if [ -f "$SOURCE_HOOKS" ]; then - echo "Would merge hooks from $SOURCE_HOOKS into $KIRO_AGENT_FILE" - fi - exit 0 -fi - -mkdir -p "$KIRO_DIR/mcp_runtime" "$KIRO_SKILLS_DIR" "$(dirname "$KIRO_AGENT_FILE")" -if [ -e "$KIRO_PLUGIN_LINK" ] && [ ! -L "$KIRO_PLUGIN_LINK" ]; then - echo "ERROR: $KIRO_PLUGIN_LINK already exists and is not a symlink" >&2 - exit 1 -fi -ln -sfn "$PLUGIN_DIR" "$KIRO_PLUGIN_LINK" - -if [ -d "$SOURCE_SKILLS" ]; then - find "$SOURCE_SKILLS" -mindepth 1 -maxdepth 1 -type d | sort | while IFS= read -r skill_dir; do - skill_name="$(basename "$skill_dir")" - skill_link="$KIRO_SKILLS_DIR/$skill_name" - [ -f "$skill_dir/SKILL.md" ] || continue - if [ -e "$skill_link" ] && [ ! -L "$skill_link" ]; then - echo "ERROR: $skill_link already exists and is not a symlink" >&2 - exit 1 - fi - ln -sfn "$skill_dir" "$skill_link" - echo "Linked Kiro skill: $skill_name" - done -fi - -python3 - "$SOURCE_MCP" "$TARGET_MCP" "$SOURCE_HOOKS" "$KIRO_AGENT_FILE" "$KIRO_PLUGIN_LINK" <<'PY' -import json -import sys -from pathlib import Path - -source_path = Path(sys.argv[1]) -target_path = Path(sys.argv[2]) -hooks_path = Path(sys.argv[3]) -agent_path = Path(sys.argv[4]) -plugin_link = sys.argv[5] -source = json.loads(source_path.read_text()) -target = json.loads(target_path.read_text()) if target_path.exists() else {} -source_servers = source.get("mcpServers", {}) -target.setdefault("mcpServers", {}) -target["mcpServers"].update(source_servers) -target_path.write_text(json.dumps(target, indent=2, ensure_ascii=False) + "\n") - -if agent_path.exists(): - agent = json.loads(agent_path.read_text()) -else: - agent = { - "name": "default", - "resources": ["skill://.kiro/skills/**/SKILL.md"], - } - -# Kiro は tools に挙がったツールだけをモデルへ見せる。欄が無いと MCP のツールを -# 呼べない(#1873)。利用者が書いた tools は上書きしない。 -agent.setdefault("tools", ["*"]) -agent.setdefault("mcpServers", {}) -agent["mcpServers"].update(source_servers) - -if hooks_path.exists(): - source_hooks = json.loads(hooks_path.read_text()).get("hooks", {}) - target_hooks = agent.setdefault("hooks", {}) - event_map = { - "SessionStart": "agentSpawn", - "Stop": "stop", - } - # Kiro にはプラグインルートを示す環境変数が無い。hook の command が使う - # プレースホルダを、installer が張った symlink の絶対パスへ置き換える。 - placeholder = "${PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}" - for source_event, groups in source_hooks.items(): - target_event = event_map.get(source_event, source_event) - target_list = target_hooks.setdefault(target_event, []) - existing = { - entry.get("command") - for entry in target_list - if isinstance(entry, dict) - } - for group in groups: - for hook in group.get("hooks", []): - command = hook.get("command") - if not command: - continue - command = command.replace(placeholder, plugin_link) - if command in existing: - continue - entry = {"command": command} - if "timeout_ms" in hook: - entry["timeout_ms"] = hook["timeout_ms"] - target_list.append(entry) - existing.add(command) - -agent_path.write_text(json.dumps(agent, indent=2, ensure_ascii=False) + "\n") - -print(f"Installed MCP servers: {', '.join(sorted(source_servers))}") -print(f"Updated: {target_path}") -print(f"Updated: {agent_path}") -PY diff --git a/plugins/mcp/mcp-notion/README.md b/plugins/mcp/mcp-notion/README.md index 6f4dcf859..2e80e97d3 100644 --- a/plugins/mcp/mcp-notion/README.md +++ b/plugins/mcp/mcp-notion/README.md @@ -47,14 +47,6 @@ NOTION_TOKEN= codex plugin add mcp-notion@ai-plugins ``` -### Kiro CLI - -Kiro では repository clone 後、対象 plugin の installer を project root で実行します。 - -```bash -bash plugins/mcp/mcp-notion/dev.kiro/install.sh -``` - ### 必要な環境変数 - 追加の環境変数は不要です。 diff --git a/plugins/mcp/mcp-notion/dev.kiro/install.sh b/plugins/mcp/mcp-notion/dev.kiro/install.sh deleted file mode 100755 index 9b57feca6..000000000 --- a/plugins/mcp/mcp-notion/dev.kiro/install.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env bash -# MCP Plugin Installer for Kiro CLI -# Usage: bash plugins/mcp/<プラグイン名>/dev.kiro/install.sh [--project PATH] [--dry-run] -# -# scripts/build-runtime-plugins.sh が生成します。直接編集しないでください。 -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -PLUGIN_NAME="$(basename "$PLUGIN_DIR")" -PROJECT_ROOT="$(pwd)" -KIRO_DIR="$PROJECT_ROOT/.kiro" -KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" -KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" -KIRO_SKILLS_DIR="$KIRO_DIR/skills" -TARGET_MCP="$PROJECT_ROOT/.mcp.json" -SOURCE_MCP="$PLUGIN_DIR/.mcp.json" -SOURCE_HOOKS="$PLUGIN_DIR/hooks/hooks.json" -SOURCE_SKILLS="$PLUGIN_DIR/skills" -DRY_RUN=false - -usage() { - cat <<'USAGE' -Usage: bash install.sh [--project PATH] [--dry-run] - -Install this MCP plugin into the current project's Kiro settings. -USAGE -} - -while [ "$#" -gt 0 ]; do - case "$1" in - --project) - [ "$#" -ge 2 ] || { echo "ERROR: --project requires a path" >&2; exit 2; } - PROJECT_ROOT="$(cd "$2" && pwd)" - KIRO_DIR="$PROJECT_ROOT/.kiro" - KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" - KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" - KIRO_SKILLS_DIR="$KIRO_DIR/skills" - TARGET_MCP="$PROJECT_ROOT/.mcp.json" - shift - ;; - --dry-run) DRY_RUN=true ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; - esac - shift -done - -if [ ! -f "$SOURCE_MCP" ]; then - echo "ERROR: source .mcp.json not found: $SOURCE_MCP" >&2 - exit 1 -fi - -if [ "$DRY_RUN" = true ]; then - echo "Would merge $SOURCE_MCP into $TARGET_MCP" - echo "Would link plugin runtime into $KIRO_PLUGIN_LINK" - if [ -d "$SOURCE_SKILLS" ]; then - echo "Would link skills from $SOURCE_SKILLS into $KIRO_SKILLS_DIR" - fi - if [ -f "$SOURCE_HOOKS" ]; then - echo "Would merge hooks from $SOURCE_HOOKS into $KIRO_AGENT_FILE" - fi - exit 0 -fi - -mkdir -p "$KIRO_DIR/mcp_runtime" "$KIRO_SKILLS_DIR" "$(dirname "$KIRO_AGENT_FILE")" -if [ -e "$KIRO_PLUGIN_LINK" ] && [ ! -L "$KIRO_PLUGIN_LINK" ]; then - echo "ERROR: $KIRO_PLUGIN_LINK already exists and is not a symlink" >&2 - exit 1 -fi -ln -sfn "$PLUGIN_DIR" "$KIRO_PLUGIN_LINK" - -if [ -d "$SOURCE_SKILLS" ]; then - find "$SOURCE_SKILLS" -mindepth 1 -maxdepth 1 -type d | sort | while IFS= read -r skill_dir; do - skill_name="$(basename "$skill_dir")" - skill_link="$KIRO_SKILLS_DIR/$skill_name" - [ -f "$skill_dir/SKILL.md" ] || continue - if [ -e "$skill_link" ] && [ ! -L "$skill_link" ]; then - echo "ERROR: $skill_link already exists and is not a symlink" >&2 - exit 1 - fi - ln -sfn "$skill_dir" "$skill_link" - echo "Linked Kiro skill: $skill_name" - done -fi - -python3 - "$SOURCE_MCP" "$TARGET_MCP" "$SOURCE_HOOKS" "$KIRO_AGENT_FILE" "$KIRO_PLUGIN_LINK" <<'PY' -import json -import sys -from pathlib import Path - -source_path = Path(sys.argv[1]) -target_path = Path(sys.argv[2]) -hooks_path = Path(sys.argv[3]) -agent_path = Path(sys.argv[4]) -plugin_link = sys.argv[5] -source = json.loads(source_path.read_text()) -target = json.loads(target_path.read_text()) if target_path.exists() else {} -source_servers = source.get("mcpServers", {}) -target.setdefault("mcpServers", {}) -target["mcpServers"].update(source_servers) -target_path.write_text(json.dumps(target, indent=2, ensure_ascii=False) + "\n") - -if agent_path.exists(): - agent = json.loads(agent_path.read_text()) -else: - agent = { - "name": "default", - "resources": ["skill://.kiro/skills/**/SKILL.md"], - } - -# Kiro は tools に挙がったツールだけをモデルへ見せる。欄が無いと MCP のツールを -# 呼べない(#1873)。利用者が書いた tools は上書きしない。 -agent.setdefault("tools", ["*"]) -agent.setdefault("mcpServers", {}) -agent["mcpServers"].update(source_servers) - -if hooks_path.exists(): - source_hooks = json.loads(hooks_path.read_text()).get("hooks", {}) - target_hooks = agent.setdefault("hooks", {}) - event_map = { - "SessionStart": "agentSpawn", - "Stop": "stop", - } - # Kiro にはプラグインルートを示す環境変数が無い。hook の command が使う - # プレースホルダを、installer が張った symlink の絶対パスへ置き換える。 - placeholder = "${PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}" - for source_event, groups in source_hooks.items(): - target_event = event_map.get(source_event, source_event) - target_list = target_hooks.setdefault(target_event, []) - existing = { - entry.get("command") - for entry in target_list - if isinstance(entry, dict) - } - for group in groups: - for hook in group.get("hooks", []): - command = hook.get("command") - if not command: - continue - command = command.replace(placeholder, plugin_link) - if command in existing: - continue - entry = {"command": command} - if "timeout_ms" in hook: - entry["timeout_ms"] = hook["timeout_ms"] - target_list.append(entry) - existing.add(command) - -agent_path.write_text(json.dumps(agent, indent=2, ensure_ascii=False) + "\n") - -print(f"Installed MCP servers: {', '.join(sorted(source_servers))}") -print(f"Updated: {target_path}") -print(f"Updated: {agent_path}") -PY diff --git a/plugins/mcp/mcp-playwright/README.md b/plugins/mcp/mcp-playwright/README.md index 91b1832bc..435239178 100644 --- a/plugins/mcp/mcp-playwright/README.md +++ b/plugins/mcp/mcp-playwright/README.md @@ -31,14 +31,6 @@ Playwright MCPを自動的にセットアップし、ブラウザ自動化機能 codex plugin add mcp-playwright@ai-plugins ``` -### Kiro CLI - -Kiro では repository clone 後、対象 plugin の installer を project root で実行します。 - -```bash -bash plugins/mcp/mcp-playwright/dev.kiro/install.sh -``` - ### 必要な環境変数 - `HOME` diff --git a/plugins/mcp/mcp-playwright/dev.kiro/install.sh b/plugins/mcp/mcp-playwright/dev.kiro/install.sh deleted file mode 100755 index 9b57feca6..000000000 --- a/plugins/mcp/mcp-playwright/dev.kiro/install.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env bash -# MCP Plugin Installer for Kiro CLI -# Usage: bash plugins/mcp/<プラグイン名>/dev.kiro/install.sh [--project PATH] [--dry-run] -# -# scripts/build-runtime-plugins.sh が生成します。直接編集しないでください。 -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -PLUGIN_NAME="$(basename "$PLUGIN_DIR")" -PROJECT_ROOT="$(pwd)" -KIRO_DIR="$PROJECT_ROOT/.kiro" -KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" -KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" -KIRO_SKILLS_DIR="$KIRO_DIR/skills" -TARGET_MCP="$PROJECT_ROOT/.mcp.json" -SOURCE_MCP="$PLUGIN_DIR/.mcp.json" -SOURCE_HOOKS="$PLUGIN_DIR/hooks/hooks.json" -SOURCE_SKILLS="$PLUGIN_DIR/skills" -DRY_RUN=false - -usage() { - cat <<'USAGE' -Usage: bash install.sh [--project PATH] [--dry-run] - -Install this MCP plugin into the current project's Kiro settings. -USAGE -} - -while [ "$#" -gt 0 ]; do - case "$1" in - --project) - [ "$#" -ge 2 ] || { echo "ERROR: --project requires a path" >&2; exit 2; } - PROJECT_ROOT="$(cd "$2" && pwd)" - KIRO_DIR="$PROJECT_ROOT/.kiro" - KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" - KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" - KIRO_SKILLS_DIR="$KIRO_DIR/skills" - TARGET_MCP="$PROJECT_ROOT/.mcp.json" - shift - ;; - --dry-run) DRY_RUN=true ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; - esac - shift -done - -if [ ! -f "$SOURCE_MCP" ]; then - echo "ERROR: source .mcp.json not found: $SOURCE_MCP" >&2 - exit 1 -fi - -if [ "$DRY_RUN" = true ]; then - echo "Would merge $SOURCE_MCP into $TARGET_MCP" - echo "Would link plugin runtime into $KIRO_PLUGIN_LINK" - if [ -d "$SOURCE_SKILLS" ]; then - echo "Would link skills from $SOURCE_SKILLS into $KIRO_SKILLS_DIR" - fi - if [ -f "$SOURCE_HOOKS" ]; then - echo "Would merge hooks from $SOURCE_HOOKS into $KIRO_AGENT_FILE" - fi - exit 0 -fi - -mkdir -p "$KIRO_DIR/mcp_runtime" "$KIRO_SKILLS_DIR" "$(dirname "$KIRO_AGENT_FILE")" -if [ -e "$KIRO_PLUGIN_LINK" ] && [ ! -L "$KIRO_PLUGIN_LINK" ]; then - echo "ERROR: $KIRO_PLUGIN_LINK already exists and is not a symlink" >&2 - exit 1 -fi -ln -sfn "$PLUGIN_DIR" "$KIRO_PLUGIN_LINK" - -if [ -d "$SOURCE_SKILLS" ]; then - find "$SOURCE_SKILLS" -mindepth 1 -maxdepth 1 -type d | sort | while IFS= read -r skill_dir; do - skill_name="$(basename "$skill_dir")" - skill_link="$KIRO_SKILLS_DIR/$skill_name" - [ -f "$skill_dir/SKILL.md" ] || continue - if [ -e "$skill_link" ] && [ ! -L "$skill_link" ]; then - echo "ERROR: $skill_link already exists and is not a symlink" >&2 - exit 1 - fi - ln -sfn "$skill_dir" "$skill_link" - echo "Linked Kiro skill: $skill_name" - done -fi - -python3 - "$SOURCE_MCP" "$TARGET_MCP" "$SOURCE_HOOKS" "$KIRO_AGENT_FILE" "$KIRO_PLUGIN_LINK" <<'PY' -import json -import sys -from pathlib import Path - -source_path = Path(sys.argv[1]) -target_path = Path(sys.argv[2]) -hooks_path = Path(sys.argv[3]) -agent_path = Path(sys.argv[4]) -plugin_link = sys.argv[5] -source = json.loads(source_path.read_text()) -target = json.loads(target_path.read_text()) if target_path.exists() else {} -source_servers = source.get("mcpServers", {}) -target.setdefault("mcpServers", {}) -target["mcpServers"].update(source_servers) -target_path.write_text(json.dumps(target, indent=2, ensure_ascii=False) + "\n") - -if agent_path.exists(): - agent = json.loads(agent_path.read_text()) -else: - agent = { - "name": "default", - "resources": ["skill://.kiro/skills/**/SKILL.md"], - } - -# Kiro は tools に挙がったツールだけをモデルへ見せる。欄が無いと MCP のツールを -# 呼べない(#1873)。利用者が書いた tools は上書きしない。 -agent.setdefault("tools", ["*"]) -agent.setdefault("mcpServers", {}) -agent["mcpServers"].update(source_servers) - -if hooks_path.exists(): - source_hooks = json.loads(hooks_path.read_text()).get("hooks", {}) - target_hooks = agent.setdefault("hooks", {}) - event_map = { - "SessionStart": "agentSpawn", - "Stop": "stop", - } - # Kiro にはプラグインルートを示す環境変数が無い。hook の command が使う - # プレースホルダを、installer が張った symlink の絶対パスへ置き換える。 - placeholder = "${PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}" - for source_event, groups in source_hooks.items(): - target_event = event_map.get(source_event, source_event) - target_list = target_hooks.setdefault(target_event, []) - existing = { - entry.get("command") - for entry in target_list - if isinstance(entry, dict) - } - for group in groups: - for hook in group.get("hooks", []): - command = hook.get("command") - if not command: - continue - command = command.replace(placeholder, plugin_link) - if command in existing: - continue - entry = {"command": command} - if "timeout_ms" in hook: - entry["timeout_ms"] = hook["timeout_ms"] - target_list.append(entry) - existing.add(command) - -agent_path.write_text(json.dumps(agent, indent=2, ensure_ascii=False) + "\n") - -print(f"Installed MCP servers: {', '.join(sorted(source_servers))}") -print(f"Updated: {target_path}") -print(f"Updated: {agent_path}") -PY diff --git a/plugins/mcp/mcp-redash/README.md b/plugins/mcp/mcp-redash/README.md index 83b9d288e..044614759 100644 --- a/plugins/mcp/mcp-redash/README.md +++ b/plugins/mcp/mcp-redash/README.md @@ -33,14 +33,6 @@ Redash MCP サーバーをマルチ環境対応で利用するための Claude C codex plugin add mcp-redash@ai-plugins ``` -### Kiro CLI - -Kiro では repository clone 後、対象 plugin の installer を project root で実行します。 - -```bash -bash plugins/mcp/mcp-redash/dev.kiro/install.sh -``` - ### 必要な環境変数 - `REDASH_API_KEY` diff --git a/plugins/mcp/mcp-redash/dev.kiro/install.sh b/plugins/mcp/mcp-redash/dev.kiro/install.sh deleted file mode 100755 index 9b57feca6..000000000 --- a/plugins/mcp/mcp-redash/dev.kiro/install.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env bash -# MCP Plugin Installer for Kiro CLI -# Usage: bash plugins/mcp/<プラグイン名>/dev.kiro/install.sh [--project PATH] [--dry-run] -# -# scripts/build-runtime-plugins.sh が生成します。直接編集しないでください。 -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -PLUGIN_NAME="$(basename "$PLUGIN_DIR")" -PROJECT_ROOT="$(pwd)" -KIRO_DIR="$PROJECT_ROOT/.kiro" -KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" -KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" -KIRO_SKILLS_DIR="$KIRO_DIR/skills" -TARGET_MCP="$PROJECT_ROOT/.mcp.json" -SOURCE_MCP="$PLUGIN_DIR/.mcp.json" -SOURCE_HOOKS="$PLUGIN_DIR/hooks/hooks.json" -SOURCE_SKILLS="$PLUGIN_DIR/skills" -DRY_RUN=false - -usage() { - cat <<'USAGE' -Usage: bash install.sh [--project PATH] [--dry-run] - -Install this MCP plugin into the current project's Kiro settings. -USAGE -} - -while [ "$#" -gt 0 ]; do - case "$1" in - --project) - [ "$#" -ge 2 ] || { echo "ERROR: --project requires a path" >&2; exit 2; } - PROJECT_ROOT="$(cd "$2" && pwd)" - KIRO_DIR="$PROJECT_ROOT/.kiro" - KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" - KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" - KIRO_SKILLS_DIR="$KIRO_DIR/skills" - TARGET_MCP="$PROJECT_ROOT/.mcp.json" - shift - ;; - --dry-run) DRY_RUN=true ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; - esac - shift -done - -if [ ! -f "$SOURCE_MCP" ]; then - echo "ERROR: source .mcp.json not found: $SOURCE_MCP" >&2 - exit 1 -fi - -if [ "$DRY_RUN" = true ]; then - echo "Would merge $SOURCE_MCP into $TARGET_MCP" - echo "Would link plugin runtime into $KIRO_PLUGIN_LINK" - if [ -d "$SOURCE_SKILLS" ]; then - echo "Would link skills from $SOURCE_SKILLS into $KIRO_SKILLS_DIR" - fi - if [ -f "$SOURCE_HOOKS" ]; then - echo "Would merge hooks from $SOURCE_HOOKS into $KIRO_AGENT_FILE" - fi - exit 0 -fi - -mkdir -p "$KIRO_DIR/mcp_runtime" "$KIRO_SKILLS_DIR" "$(dirname "$KIRO_AGENT_FILE")" -if [ -e "$KIRO_PLUGIN_LINK" ] && [ ! -L "$KIRO_PLUGIN_LINK" ]; then - echo "ERROR: $KIRO_PLUGIN_LINK already exists and is not a symlink" >&2 - exit 1 -fi -ln -sfn "$PLUGIN_DIR" "$KIRO_PLUGIN_LINK" - -if [ -d "$SOURCE_SKILLS" ]; then - find "$SOURCE_SKILLS" -mindepth 1 -maxdepth 1 -type d | sort | while IFS= read -r skill_dir; do - skill_name="$(basename "$skill_dir")" - skill_link="$KIRO_SKILLS_DIR/$skill_name" - [ -f "$skill_dir/SKILL.md" ] || continue - if [ -e "$skill_link" ] && [ ! -L "$skill_link" ]; then - echo "ERROR: $skill_link already exists and is not a symlink" >&2 - exit 1 - fi - ln -sfn "$skill_dir" "$skill_link" - echo "Linked Kiro skill: $skill_name" - done -fi - -python3 - "$SOURCE_MCP" "$TARGET_MCP" "$SOURCE_HOOKS" "$KIRO_AGENT_FILE" "$KIRO_PLUGIN_LINK" <<'PY' -import json -import sys -from pathlib import Path - -source_path = Path(sys.argv[1]) -target_path = Path(sys.argv[2]) -hooks_path = Path(sys.argv[3]) -agent_path = Path(sys.argv[4]) -plugin_link = sys.argv[5] -source = json.loads(source_path.read_text()) -target = json.loads(target_path.read_text()) if target_path.exists() else {} -source_servers = source.get("mcpServers", {}) -target.setdefault("mcpServers", {}) -target["mcpServers"].update(source_servers) -target_path.write_text(json.dumps(target, indent=2, ensure_ascii=False) + "\n") - -if agent_path.exists(): - agent = json.loads(agent_path.read_text()) -else: - agent = { - "name": "default", - "resources": ["skill://.kiro/skills/**/SKILL.md"], - } - -# Kiro は tools に挙がったツールだけをモデルへ見せる。欄が無いと MCP のツールを -# 呼べない(#1873)。利用者が書いた tools は上書きしない。 -agent.setdefault("tools", ["*"]) -agent.setdefault("mcpServers", {}) -agent["mcpServers"].update(source_servers) - -if hooks_path.exists(): - source_hooks = json.loads(hooks_path.read_text()).get("hooks", {}) - target_hooks = agent.setdefault("hooks", {}) - event_map = { - "SessionStart": "agentSpawn", - "Stop": "stop", - } - # Kiro にはプラグインルートを示す環境変数が無い。hook の command が使う - # プレースホルダを、installer が張った symlink の絶対パスへ置き換える。 - placeholder = "${PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}" - for source_event, groups in source_hooks.items(): - target_event = event_map.get(source_event, source_event) - target_list = target_hooks.setdefault(target_event, []) - existing = { - entry.get("command") - for entry in target_list - if isinstance(entry, dict) - } - for group in groups: - for hook in group.get("hooks", []): - command = hook.get("command") - if not command: - continue - command = command.replace(placeholder, plugin_link) - if command in existing: - continue - entry = {"command": command} - if "timeout_ms" in hook: - entry["timeout_ms"] = hook["timeout_ms"] - target_list.append(entry) - existing.add(command) - -agent_path.write_text(json.dumps(agent, indent=2, ensure_ascii=False) + "\n") - -print(f"Installed MCP servers: {', '.join(sorted(source_servers))}") -print(f"Updated: {target_path}") -print(f"Updated: {agent_path}") -PY diff --git a/plugins/mcp/mcp-serena/README.md b/plugins/mcp/mcp-serena/README.md index 166c7df83..de628ae10 100644 --- a/plugins/mcp/mcp-serena/README.md +++ b/plugins/mcp/mcp-serena/README.md @@ -1,7 +1,7 @@ # mcp-serena Serena MCP サーバを提供するプラグインです。シンボル単位の読み書き(定義・参照・シンボル単位の -置換)と、言語サーバの診断を Claude Code・Codex・Kiro CLI で使えるようにします。 +置換)と、言語サーバの診断を Claude Code・Codex で使えるようにします。 ## 最初に: 言語サーバの設定はプロジェクトごとに行う @@ -51,14 +51,6 @@ Codex では `--context codex` で起動します(`.codex.mcp.json`)。 **Codex では、プラグインの hook は信頼するまで動きません**(Codex 0.156 で実測。何も表示されずに走らないだけになる)。対話の Codex で `/hooks` を開き、mcp-serena の hook を信頼してください。起動時に出る「Review hooks」の案内からも信頼できます。 -### Kiro CLI - -repository を clone した後、対象のプロジェクトの根で installer を実行します。 - -```bash -bash plugins/mcp/mcp-serena/dev.kiro/install.sh -``` - ### 前提 - `uv`(`uvx`)。Serena は `uvx --from serena-agent==1.7.0` で起動します @@ -75,9 +67,6 @@ bash plugins/mcp/mcp-serena/dev.kiro/install.sh # Codex codex plugin marketplace upgrade ai-plugins codex plugin add mcp-serena@ai-plugins - -# Kiro CLI -bash plugins/mcp/mcp-serena/dev.kiro/install.sh ``` ## 公式の `serena` プラグインと併用しない @@ -98,7 +87,7 @@ bash plugins/mcp/mcp-serena/dev.kiro/install.sh | ランタイム | 定義 | 文脈 | | --- | --- | --- | -| Claude Code / Kiro CLI | `.mcp.json` | `claude-code` | +| Claude Code | `.mcp.json` | `claude-code` | | Codex | `.codex.mcp.json` | `codex` | どちらも `SERENA_HOME=.serena`(起動したディレクトリからの相対)で動きます。worktree ごとに diff --git a/plugins/mcp/mcp-serena/dev.kiro/install.sh b/plugins/mcp/mcp-serena/dev.kiro/install.sh deleted file mode 100755 index 9b57feca6..000000000 --- a/plugins/mcp/mcp-serena/dev.kiro/install.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env bash -# MCP Plugin Installer for Kiro CLI -# Usage: bash plugins/mcp/<プラグイン名>/dev.kiro/install.sh [--project PATH] [--dry-run] -# -# scripts/build-runtime-plugins.sh が生成します。直接編集しないでください。 -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -PLUGIN_NAME="$(basename "$PLUGIN_DIR")" -PROJECT_ROOT="$(pwd)" -KIRO_DIR="$PROJECT_ROOT/.kiro" -KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" -KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" -KIRO_SKILLS_DIR="$KIRO_DIR/skills" -TARGET_MCP="$PROJECT_ROOT/.mcp.json" -SOURCE_MCP="$PLUGIN_DIR/.mcp.json" -SOURCE_HOOKS="$PLUGIN_DIR/hooks/hooks.json" -SOURCE_SKILLS="$PLUGIN_DIR/skills" -DRY_RUN=false - -usage() { - cat <<'USAGE' -Usage: bash install.sh [--project PATH] [--dry-run] - -Install this MCP plugin into the current project's Kiro settings. -USAGE -} - -while [ "$#" -gt 0 ]; do - case "$1" in - --project) - [ "$#" -ge 2 ] || { echo "ERROR: --project requires a path" >&2; exit 2; } - PROJECT_ROOT="$(cd "$2" && pwd)" - KIRO_DIR="$PROJECT_ROOT/.kiro" - KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" - KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" - KIRO_SKILLS_DIR="$KIRO_DIR/skills" - TARGET_MCP="$PROJECT_ROOT/.mcp.json" - shift - ;; - --dry-run) DRY_RUN=true ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; - esac - shift -done - -if [ ! -f "$SOURCE_MCP" ]; then - echo "ERROR: source .mcp.json not found: $SOURCE_MCP" >&2 - exit 1 -fi - -if [ "$DRY_RUN" = true ]; then - echo "Would merge $SOURCE_MCP into $TARGET_MCP" - echo "Would link plugin runtime into $KIRO_PLUGIN_LINK" - if [ -d "$SOURCE_SKILLS" ]; then - echo "Would link skills from $SOURCE_SKILLS into $KIRO_SKILLS_DIR" - fi - if [ -f "$SOURCE_HOOKS" ]; then - echo "Would merge hooks from $SOURCE_HOOKS into $KIRO_AGENT_FILE" - fi - exit 0 -fi - -mkdir -p "$KIRO_DIR/mcp_runtime" "$KIRO_SKILLS_DIR" "$(dirname "$KIRO_AGENT_FILE")" -if [ -e "$KIRO_PLUGIN_LINK" ] && [ ! -L "$KIRO_PLUGIN_LINK" ]; then - echo "ERROR: $KIRO_PLUGIN_LINK already exists and is not a symlink" >&2 - exit 1 -fi -ln -sfn "$PLUGIN_DIR" "$KIRO_PLUGIN_LINK" - -if [ -d "$SOURCE_SKILLS" ]; then - find "$SOURCE_SKILLS" -mindepth 1 -maxdepth 1 -type d | sort | while IFS= read -r skill_dir; do - skill_name="$(basename "$skill_dir")" - skill_link="$KIRO_SKILLS_DIR/$skill_name" - [ -f "$skill_dir/SKILL.md" ] || continue - if [ -e "$skill_link" ] && [ ! -L "$skill_link" ]; then - echo "ERROR: $skill_link already exists and is not a symlink" >&2 - exit 1 - fi - ln -sfn "$skill_dir" "$skill_link" - echo "Linked Kiro skill: $skill_name" - done -fi - -python3 - "$SOURCE_MCP" "$TARGET_MCP" "$SOURCE_HOOKS" "$KIRO_AGENT_FILE" "$KIRO_PLUGIN_LINK" <<'PY' -import json -import sys -from pathlib import Path - -source_path = Path(sys.argv[1]) -target_path = Path(sys.argv[2]) -hooks_path = Path(sys.argv[3]) -agent_path = Path(sys.argv[4]) -plugin_link = sys.argv[5] -source = json.loads(source_path.read_text()) -target = json.loads(target_path.read_text()) if target_path.exists() else {} -source_servers = source.get("mcpServers", {}) -target.setdefault("mcpServers", {}) -target["mcpServers"].update(source_servers) -target_path.write_text(json.dumps(target, indent=2, ensure_ascii=False) + "\n") - -if agent_path.exists(): - agent = json.loads(agent_path.read_text()) -else: - agent = { - "name": "default", - "resources": ["skill://.kiro/skills/**/SKILL.md"], - } - -# Kiro は tools に挙がったツールだけをモデルへ見せる。欄が無いと MCP のツールを -# 呼べない(#1873)。利用者が書いた tools は上書きしない。 -agent.setdefault("tools", ["*"]) -agent.setdefault("mcpServers", {}) -agent["mcpServers"].update(source_servers) - -if hooks_path.exists(): - source_hooks = json.loads(hooks_path.read_text()).get("hooks", {}) - target_hooks = agent.setdefault("hooks", {}) - event_map = { - "SessionStart": "agentSpawn", - "Stop": "stop", - } - # Kiro にはプラグインルートを示す環境変数が無い。hook の command が使う - # プレースホルダを、installer が張った symlink の絶対パスへ置き換える。 - placeholder = "${PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}" - for source_event, groups in source_hooks.items(): - target_event = event_map.get(source_event, source_event) - target_list = target_hooks.setdefault(target_event, []) - existing = { - entry.get("command") - for entry in target_list - if isinstance(entry, dict) - } - for group in groups: - for hook in group.get("hooks", []): - command = hook.get("command") - if not command: - continue - command = command.replace(placeholder, plugin_link) - if command in existing: - continue - entry = {"command": command} - if "timeout_ms" in hook: - entry["timeout_ms"] = hook["timeout_ms"] - target_list.append(entry) - existing.add(command) - -agent_path.write_text(json.dumps(agent, indent=2, ensure_ascii=False) + "\n") - -print(f"Installed MCP servers: {', '.join(sorted(source_servers))}") -print(f"Updated: {target_path}") -print(f"Updated: {agent_path}") -PY diff --git a/plugins/mcp/mcp-serena/tests/test_serena_lsp_kiro.py b/plugins/mcp/mcp-serena/tests/test_serena_lsp_kiro.py deleted file mode 100644 index c63c0eb97..000000000 --- a/plugins/mcp/mcp-serena/tests/test_serena_lsp_kiro.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Kiro の installer(AC29)。agentSpawn へ写った SessionStart は Kiro では何も出さない(決定 15)。""" - -import json -import os -import subprocess - -from serena_lsp_testlib import PLUGIN, files_of, make_repo - - -def test_kiro_agent_spawn_prints_nothing_without_claude_plugin_root(tmp_path): - project = make_repo(tmp_path / "p", files_of(py=30)) - env = {k: v for k, v in os.environ.items() if k not in ("CLAUDE_PLUGIN_ROOT", "PLUGIN_ROOT", "CODEX_PLUGIN_ROOT")} - subprocess.run(["bash", str(PLUGIN / "dev.kiro/install.sh"), "--project", str(project)], check=True, capture_output=True, env=env) - agent = json.loads((project / ".kiro/agents/default.json").read_text()) - commands = [h["command"] for h in agent["hooks"]["agentSpawn"] if "serena-lsp.py" in h["command"]] - assert commands - for command in commands: - proc = subprocess.run( - ["sh", "-c", command], input=json.dumps({"cwd": str(project)}), capture_output=True, text=True, env=env, cwd=project - ) - assert (proc.returncode, proc.stdout) == (0, "") diff --git a/plugins/ndf/README.md b/plugins/ndf/README.md index 9d2151853..f235051bf 100644 --- a/plugins/ndf/README.md +++ b/plugins/ndf/README.md @@ -4,14 +4,13 @@ PR 運用、レビュー、調査、実装計画、仕様書化、開発方法 完了判定)、Docker container access、statusline、外部 AI 委譲、Slack 通知を提供します。 配布物は `plugins/ndf/` の 1 ディレクトリにまとまっています。Skill の実体は `skills/` の -1 箇所だけで、どのランタイムへ配るかは `manifests/*-skills.txt` が決めます。 +1 箇所だけで、どのランタイムへ配るかは `manifests/*-skills.txt` が決めます。配布先は Claude Code と +Codex の 2 つです。Kiro CLI と agy へは配らず、cross-review / cross-refactoring の相手役として起動するだけです。 | ランタイム | 公開 Skill | マニフェスト | | --- | --- | --- | | Claude Code | 49 個 | `.claude-plugin/plugin.json` | | Codex | 45 個 | `.codex-plugin/plugin.json` | -| Kiro CLI | 46 個 | `dev.kiro/install.sh`(プラグイン機構が無いため installer で導入) | -| agy | 45 個 | `dev.agy/plugin.json`(取得元の登録が無いため clone から導入) | ## レイアウト @@ -26,23 +25,9 @@ plugins/ndf/ ├── hooks/claude.json # Claude Code の PreToolUse / SessionStart / Stop hook ├── hooks/codex.json # Codex の PreToolUse / SessionStart / Stop hook ├── scripts/ # hook と Skill から呼ぶスクリプト -├── dev.kiro/ # Kiro CLI の installer・エージェント定義・プロンプト -├── dev.agy/ # agy のマニフェスト・hook 定義・配布 Skill への symlink └── README.md ``` -`dev.kiro` と `dev.agy` は Agent Plugins 仕様 §8.2 が定めるクライアント拡張ディレクトリです。 - -`dev.agy/skills/` は `manifests/agy-skills.txt` から生成する symlink です -(`scripts/build-runtime-plugins.sh`)。`dev.agy/scripts` は `scripts/` を指し、実体を -4 ランタイムで共有します。**agy は配る Skill を絞る手段を利用者側の設定にしか持たない**ため、 -絞り込みはここへ何を並べるかで表します。 - -`dev.agy/agents/` は `agents/` から同じスクリプトが生成する写しです。agy は `model` に tier 名 -(`flash` / `pro` / `inherit`)しか受け付けないため、frontmatter の `model:` の 1 行だけを -対応表(`sonnet` → `flash`、`opus` → `pro`)で置き換えます。対応表は `scripts/lib/agy_agents.py` -が持ちます。写しは手で直さず、`agents/` を直してから生成し直します(`--check` が食い違いを落とします)。 - **`skills/` に置く Skill は、少なくとも 1 つの manifest へ載せます。** 配らない Skill の 置き場所(`optional-skills/`)は v10.5.0 で無くしました。どこからも起動できない Skill を 置き続ける理由が、公開数を一定に保つことだけだったためです。 @@ -67,62 +52,17 @@ codex plugin marketplace add https://github.com/devbasex/ai-plugins codex plugin add ndf@ai-plugins ``` -### Kiro CLI - -Kiro CLI にはプラグイン機構が無いため、installer が `.kiro/` へ配置します。リポジトリ root で -実行してください。 - -```bash -# 基本(Skills + steering + agentSpawn hook) -bash plugins/ndf/dev.kiro/install.sh - -# Slack 通知も有効化 -bash plugins/ndf/dev.kiro/install.sh --with-slack - -# Slack 通知 + Kiro 側 Codex MCP 設定も生成 -bash plugins/ndf/dev.kiro/install.sh --with-slack --with-codex - -# 書き込みを行わず内容だけ確認 -bash plugins/ndf/dev.kiro/install.sh --dry-run -``` - -導入後は `kiro-cli chat --agent ndf` で起動します。installer は `manifests/kiro-skills.txt` に -載る Skill だけを `.kiro/skills/` へ symlink します。版数は `.claude-plugin/plugin.json` から -読み取り、実行時に `NDF バージョン: <版数>` として表示します。 - -導入済みプロジェクトの版数は次で確認できます。 - -```bash -python3 -c "import json;print(json.load(open('.kiro/agents/ndf.json'))['description'])" -# => NDF統合開発エージェント(Kiro CLI用 / v10.27.4) -``` - -### agy +### agy・Kiro CLI への配布の終了 -agy にも取得元の登録がありません。clone した `dev.agy` を直接導入します。**更新の副コマンドが -無い**ため、新しい版へは入れ直します(`install` は上書きするが、消したファイルは実体に残る)。 +agy と Kiro CLI へは配りません。最後に配ったのは v10.27.4 です。cross-review と cross-refactoring は、 +Claude Code か Codex をホストにして agy・Kiro CLI を相手役として今までどおり起動します。相手役に配布物は要りません。 -```bash -agy plugin install plugins/ndf/dev.agy # 初回 -agy plugin uninstall ndf && agy plugin install plugins/ndf/dev.agy # 新しい版へ -``` - -導入すると `manifests/agy-skills.txt` に載る Skill 45 個と、エージェント 11 個(専門 8 個と、3 層の定義 3 個(supervisor 2・worker 1))、hook 1 個が -`~/.gemini/config/plugins/ndf/` へコピーされます。symlink は実体へ解決されてコピーされるため、 -clone を消しても導入した内容は残ります。 - -**hook はコピーされるだけで、agy はそれを読み込みません**(agy 1.1.26 で実測)。読む先は -`~/.gemini/config/hooks.json` の 1 か所だけです。次を実行して差し込みます。**冪等で、他の -項目には触れません**(`--dry-run` で内容を確認でき、`--uninstall` で外せます)。 +前の版で入れた手元は、こちらからは消しません。残すか消すかを選べます。 -```bash -bash plugins/ndf/dev.agy/install-hooks.sh -``` - -```bash -agy plugin list -# => {"imports":[{"name":"ndf","source":"antigravity","components":["skills","agents","hooks"]}]} -``` +| ランタイム | 残すとき | 消すとき | +| --- | --- | --- | +| agy | 導入の写し(`~/.gemini/config/plugins/ndf/`)だけで動き続ける。新しい版は届かない | 写しが残るうちに `bash ~/.gemini/config/plugins/ndf/install-hooks.sh --uninstall` で `~/.gemini/config/hooks.json` の項目を外し、`agy plugin uninstall ndf` を打つ | +| Kiro CLI | `.kiro/agents/ndf.json` の hook は clone の `plugins/ndf/scripts/` を呼ぶため、clone を進めても動く。Skill を固定したいときは clone を `ndf--v10.27.4` のタグへ戻す | 前の installer は `.kiro/agents/ndf.json` に利用者が足した `mcpServers`・hook・その他の鍵を引き継いでいたため、消す前に `cp .kiro/agents/ndf.json ndf.json.bak` で退避し、残したい項目を使い続けるエージェント(`.kiro/agents/` の別のファイル)へ写す。`ndf` を既定のエージェントにしたなら `kiro-cli agent set-default kiro_default` で戻し、`.kiro/agents/ndf.json`・`.kiro/steering/ndf-policies.md`・`.kiro/prompts/` の NDF の 5 本(`--with-codex` で入れたなら `codex.md` も)・`.kiro/skills/` の NDF への symlink(mcp-* を入れたなら `.kiro/mcp_runtime/` と `.kiro/agents/default.json` の該当の項目も)を消す | ## 更新情報 @@ -137,34 +77,22 @@ Skill 名は変わらないため `/playwright-` まで打てば候補に出ま ```bash /plugin install playwright-kit@ai-plugins # Claude Code codex plugin add playwright-kit@ai-plugins # Codex -bash plugins/playwright-kit/dev.kiro/install.sh # Kiro CLI ``` ## Hooks -### worktree 運用(4 ランタイム共通) +### worktree 運用(Claude Code と Codex) 開発の変更を `.worktrees/` の worktree の中で行い、リポジトリを clone したディレクトリ (メインディレクトリ)では行わない運用を支えます。**編集は止めません。** 案内が出ても操作は成立します。 -| 起きること | 担う hook | Claude Code | Codex | Kiro CLI | agy | -| --- | --- | --- | --- | --- | --- | -| メインディレクトリの保護対象パスを編集しようとすると案内が出る | tool 実行前 | `PreToolUse` | `PreToolUse` | — | `PreToolUse` | -| worktree で作業する旨の案内がプロンプトごとに出る | プロンプト送信時 | — | — | `userPromptSubmit` | — | -| メインディレクトリに残った未コミット変更が提示される | セッション開始時 | `SessionStart` | `SessionStart` | `agentSpawn` | `PreInvocation` | -| メインディレクトリのブランチが稼働中の worktree へ追従する(既定では動かさない。worktree の設定の `follow_branch: true` で有効にする) | セッション開始時 | `SessionStart` | `SessionStart` | `agentSpawn` | `PreInvocation` | - -Kiro CLI に tool 実行前の案内が無いのは、この事象でモデルへ案内を渡す手段が終了コード 2 に -限られ、それが tool の実行を拒否するためです。拒否しない方針のもとでは置けないため、パスを -見ない案内をプロンプト送信時の hook が担います。 - -**agy の 2 つは `install-hooks.sh` を実行するまで届きません。** agy はプラグインの -`hooks.json` を読み込まないためです(「インストール / agy」を参照)。 +| 起きること | 担う hook | Claude Code | Codex | +| --- | --- | --- | --- | +| メインディレクトリの保護対象パスを編集しようとすると案内が出る | tool 実行前 | `PreToolUse` | `PreToolUse` | +| メインディレクトリに残った未コミット変更が提示される | セッション開始時 | `SessionStart` | `SessionStart` | +| メインディレクトリのブランチが稼働中の worktree へ追従する(既定では動かさない。worktree の設定の `follow_branch: true` で有効にする) | セッション開始時 | `SessionStart` | `SessionStart` | -**agy は案内を作る時点と渡せる時点が離れています。** tool 実行前の hook がモデルへ文言を返す -口は拒否のときにしか働かないため、案内はセッションの記録へ積み、次のモデル呼び出しの前に -`injectSteps` で渡します。セッション開始時にあたる事象も持たないため、モデル呼び出しの通し番号が -0 のときを開始時として扱います。 +前の版で agy・Kiro CLI へ入れた手元の hook も、同じ `scripts/` の入口を呼び続けます(「agy・Kiro CLI への配布の終了」)。 **この仕組みはリポジトリ側の設定ファイル `.ndf/worktree.json` があるときだけ動きます。** 設定が無いリポジトリでは、いずれの hook も何も出力せず終了コード 0 で終わります。 @@ -206,8 +134,6 @@ bash <プラグインのパス>/scripts/worktree-setup.sh init | --- | --- | --- | | Claude Code | hook + 規約 | hook + 再開コマンド | | Codex | 規約だけ | 再開コマンドだけ | -| Kiro CLI | 規約だけ | 再開コマンドだけ | -| agy | 規約だけ | 再開コマンドだけ | 規約は `skills/development-workflow/references/waiting.md`(待ち方)と `skills/development-workflow/references/context-window.md`(会話を切る)にあります。 @@ -221,7 +147,7 @@ bash <プラグインのパス>/scripts/worktree-setup.sh init プロジェクトごとの働き方は `.ndf/ask.json` で変えます(無ければ `enforce`)。今の振る舞いへ戻すには `{"version": 1, "mode": "off"}` を書きます。 -区分の表と宣言の形は `skills/development-workflow/references/asking.md`、ランタイムごとの扱い(Codex・Kiro CLI・agy は対象外)は +区分の表と宣言の形は `skills/development-workflow/references/asking.md`、ランタイムごとの扱い(Codex は対象外)は [docs/ndf-plugin-reference.md の「承認ゲートの外の問い合わせ」](../../docs/ndf-plugin-reference.md#承認ゲートの外の問い合わせ) にあります。 ### 学習の設定を Off にする(Claude Code と Codex) @@ -300,8 +226,6 @@ Codex の Stop・PermissionRequest hook(`hooks/codex.json`)は `NDF_CODEX_SL ません。** `~/.codex/config.toml` の `[hooks.state]` に対象 hook の `enabled = true` が要ります。 `/hooks` で対象 hook を確認し、利用するプロジェクトで有効化してください。 -Kiro CLI では installer が `.kiro/agents/ndf.json` の `hooks` を生成します。 - ## Slack 通知 利用者の回答か承認が無いと進まない時点でだけ、Slack へ知らせます。応答が終わっても、待っていなければ @@ -379,9 +303,6 @@ NDF_CODEX_SLACK_NOTIFY=true 機密値は `.env` などで管理し、リポジトリへコミットしないでください。通知の記録(同じ待ちを 2 度送らないための 直前の 1 件)は `${XDG_STATE_HOME:-~/.local/state}/ndf/wait-notify/` にセッションごとに置き、7 日で消します。 -**Kiro は `install.sh --with-slack` を打ち直してください。** 通知の入口は `scripts/wait-notify.py` の 1 本です。 -`stop` hook が配布物に無い `scripts/slack-notify.js` を指す `.kiro/agents/ndf.json` では、通知が届きません。 - ## 外部 AI 委譲 `/ndf:external-ai` skill または `corder` エージェントから外部 AI 委譲を使う場合は、利用環境に @@ -479,12 +400,6 @@ codex plugin list | grep 'ndf@ai-plugins' 配布した Skill は抑止すると `$` も効きません**。この差は codex-cli 0.146.1 で実測した もので、公式ドキュメントには記載がありません。 -## Kiro CLI の詳細 - -installer の主なオプション・既定エージェントの切り替え・導入スコープ・旧バージョンからの -移行・再インストール時に保持される設定と、Kiro CLI 側の制限は -[docs/kiro-cli.md](docs/kiro-cli.md) にあります。 - ## 実機検証の記録 kiro-cli の実機検証と、Skill 数がコンテキスト量へ与える影響の実測は diff --git a/plugins/ndf/dev.agy/agents/code-reviewer.md b/plugins/ndf/dev.agy/agents/code-reviewer.md deleted file mode 100644 index 3ed2d6ff8..000000000 --- a/plugins/ndf/dev.agy/agents/code-reviewer.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -name: code-reviewer -model: flash -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/dev.agy/agents/corder.md b/plugins/ndf/dev.agy/agents/corder.md deleted file mode 100644 index d5ab5c020..000000000 --- a/plugins/ndf/dev.agy/agents/corder.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -name: corder -model: flash -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を活用して、最新のベストプラクティスに基づいた、保守性の高いコードを生成・レビューします。 - -## Codex の呼び方 - -`mcp__codex__*`(Codex MCP サーバ)は無い。Codex は `/ndf:external-ai` skill の `external-ai.py run codex` で呼ぶ(下の「Codex CLI」)。 - -## 専門領域 - -### 1. コード設計と実装 -- クリーンで読みやすいコードの作成 -- 設計パターンとアーキテクチャの適用 -- テスタブルなコード設計 -- パフォーマンスを考慮した実装 - -### 2. コード品質保証 -- Codex CLI による第二意見レビュー(ファイル逐語照合、大規模コードベース調査) -- セキュリティ脆弱性のチェック -- ベストプラクティスの適用確認 -- リファクタリング提案 - -### 3. コードベース理解 -- Serena MCPによるシンボル検索と分析 -- 既存コードの構造理解 -- 依存関係の把握 - -### 4. 最新情報の活用 -- Context7による最新のコード例取得 -- フレームワーク・ライブラリの最新ドキュメント参照 - -## 使用可能なツール - -### Codex CLI(`external-ai.py run` の 1 行) - -起動・上限つきの待ち・回収は `/ndf:external-ai` skill の `scripts/external-ai.py` が行う(手順は同 skill の「共通の実行手順」、Codex 固有の差分は `references/cli-codex.md`)。 - -```bash -# 1. プロンプトを一意の一時ファイルに書く(本文はファイル書き込みツールで。出力先に $FINAL を書く) -TMP=$(mktemp -d) -FINAL="$TMP/codex-result.md" - -# 2. 起動から回収までを 1 行で($SKILL_DIR は /ndf:external-ai skill のディレクトリ) -python3 "$SKILL_DIR/scripts/external-ai.py" run codex \ - --prompt-file "$TMP/prompt.md" --output-file "$FINAL" \ - --phase review --workdir "$PWD" -# => 最後の 1 行が JSON。{"status": "ok", ..., "metrics": {"outcome": "ok", "result": "...", ...}} -``` - -- Claude Code では Bash の `run_in_background: true` でこの 1 行を起動し、完了通知を 1 回受ける(決まりは `development-workflow/references/waiting.md`) -- `status` が `ok` なら `metrics.result` のファイルを読む。`stopped` なら `metrics.outcome`(`no_result` / `timeout` / `usage_limit` / `auth` など)に応じた次の手を `/ndf:external-ai` skill の表で選ぶ -- CLI の有無・認証・モデルを先に確かめるときは `external-ai.py check codex`。`metrics.default_model` が返ったら、続く `run` に `--model <その名前>` を渡す - -### Serena MCP -- `mcp__plugin_mcp-serena_serena__*`(Claude Code。Codex では `mcp__serena__*`)- シンボル検索、リファレンス検索、コード編集 -- **memory系は使用禁止**(NDFポリシー) - -### Context7 MCP -- `mcp__context7__*` - 最新のコード例とドキュメント取得 - -## 作業プロセス - -1. **要件理解**: 実装する機能の要件を明確化 -2. **コードベース調査**: Serenaで既存コード構造を理解 -3. **最新情報収集**: Context7で最新のベストプラクティスを確認 -4. **設計**: アーキテクチャと実装方針を決定 -5. **実装**: クリーンなコードを作成 -6. **レビュー**: `external-ai.py run codex`(`/ndf:external-ai` skill)で独立レビューを依頼 -7. **改善**: レビュー結果に基づいて修正 -8. **テスト**: 動作確認とテストコード作成 - -## コーディングスタイル - -- DRY(Don't Repeat Yourself)原則の遵守 -- SOLID原則の適用 -- 明確な変数名・関数名の使用 -- 適切なコメントとドキュメント -- エラーハンドリングの実装 -- セキュリティを考慮した実装 - -## ベストプラクティス - -- 実装前にSerenaで既存コードパターンを確認 -- Context7で最新のフレームワーク仕様を参照 -- 実装後は必ず Codex CLI (`/ndf:external-ai`) で第二意見レビュー -- テストコードも併せて作成 -- 破壊的変更は事前に影響範囲を確認 - -## サブエージェント呼び出しの制約 - -### 無限呼び出し防止ルール - -**重要:** サブエージェントの無限呼び出しを防ぐため、以下のルールを厳守してください。 - -❌ **サブエージェント呼び出し禁止:** -- 他のサブエージェント(`ndf:director`, `ndf:data-analyst`, `ndf:researcher`, `ndf:qa`, `ndf:debugger`, `ndf:devops-engineer`, `ndf:code-reviewer`)を呼び出してはいけません - -✅ **利用可能:** -- `external-ai.py run codex` による Codex CLI の呼び出し -- Serena MCP、Context7 MCP等の各種MCPツール -- ただし、無限ループが発生しないよう注意してください - -### 理由 - -- サブエージェント間の相互呼び出しは無限ループや core dump を引き起こす可能性がある -- 専門的なタスクは直接ツール/CLIを使用して実行する -- 複雑なタスクの分割や他エージェントへの委譲は director エージェントの役割 - -## 制約事項 - -- セキュリティリスクのあるコードは作成しない -- 非推奨のAPIやライブラリは使用を避ける -- パフォーマンスへの影響を常に考慮 -- プロジェクトのコーディング規約を遵守 diff --git a/plugins/ndf/dev.agy/agents/data-analyst.md b/plugins/ndf/dev.agy/agents/data-analyst.md deleted file mode 100644 index 633c7af91..000000000 --- a/plugins/ndf/dev.agy/agents/data-analyst.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -name: data-analyst -model: flash -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、Markdownテーブル形式でのデータエクスポート -- 結果データのファイル保存 -- レポート生成とデータ可視化の準備 -- データサマリーの作成 - -## SQL最適化パターン - -遅いクエリは EXPLAIN で実行計画を確認し、該当パターンを適用してから再度 EXPLAIN で改善を確認する。 - -| パターン | 問題 | 解決策 | -|---------|------|--------| -| N+1削減 | ループ内SQL | JOINで1回に統合 | -| インデックス | フルスキャン | WHERE/JOIN列にインデックス | -| JOIN最適化 | 不要な大規模JOIN | 必要な列のみ取得 | -| ウィンドウ関数 | 複雑なサブクエリ | ROW_NUMBER(), RANK()使用 | -| EXISTS vs IN | 遅いIN句 | EXISTSで書く | -| LIMIT活用 | 全件取得 | SQLでページング | - -| DO | DON'T | -|----|-------| -| EXPLAINで実行計画を確認 | 不要なDISTINCT | -| インデックスは選択的に作成 | 関数をWHERE句の列に適用 | -| 必要な列のみSELECT | 過剰なJOIN | -| 早期フィルタリング | サブクエリの多用 | - -## エクスポート形式の選択 - -| 形式 | 用途 | 注意点 | -|------|------|--------| -| CSV | 単純なデータ、他システム連携 | Excelで開くならUTF-8 BOMを付与 | -| JSON | API連携、構造化データ | 日付は ISO 8601 に統一 | -| Excel | 複雑なレポート、書式設定 | 1シート1,048,576行の上限を超えたら分割 | -| Markdown | ドキュメント埋め込み | パイプ文字のエスケープ | - -| DO | DON'T | -|----|-------| -| ヘッダー行を含める | 全データをメモリに展開 | -| 大きなデータはストリーミングで書き出す | 日付フォーマットの不統一 | -| 文字コードは UTF-8 に統一 | 特殊文字のエスケープ忘れ | - -## 使用可能なMCPツール - -### BigQuery MCP -- `mcp__plugin_mcp-bigquery_bigquery__*` - BigQueryクエリ実行(`execute_sql_readonly`)、データセット・テーブルの参照 - -### 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等)は慎重に扱い、ユーザーに確認する(`AskUserQuestion` の `metadata.source` は、利用者のデータを変えるなら `ndf-ask:redline:C3`、戻せない操作なら `ndf-ask:redline:C4`) -- 個人情報や機密データの取り扱いに注意 -- パフォーマンスに影響する重いクエリは事前に警告 diff --git a/plugins/ndf/dev.agy/agents/debugger.md b/plugins/ndf/dev.agy/agents/debugger.md deleted file mode 100644 index 70eb32e12..000000000 --- a/plugins/ndf/dev.agy/agents/debugger.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -name: debugger -model: flash -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)、関連シンボルの検索(Serena の `find_symbol` / `find_referencing_symbols`。使えなければ Grep) -3. **仮説立案**: 最も可能性の高い原因を3つまで列挙(確信度付き) -4. **検証**: 各仮説を確認する最小限の実験(ログ追加、短いテスト、境界値確認) -5. **根本原因特定**: 検証結果から原因を確定 -6. **修正提案**: 最小限の変更で直す案(副作用のリスクも明示) -7. **再発防止**: テスト追加、型強化、アサーション追加の提案 - -## 使用ツール - -### Claude Code built-in -- `Read` - コード、ログ、設定ファイルの確認 -- `Grep` - エラーメッセージ・シンボル検索 -- `Bash` - テスト再実行、環境確認、プロセス調査 -- `Edit` - 修正提案の適用(AI が判断して当て、根拠を報告に書く) - -### MCPツール(必要に応じて) -- Serena MCP(シンボル参照解析) -- Codex CLI(`/ndf:external-ai` 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/dev.agy/agents/devops-engineer.md b/plugins/ndf/dev.agy/agents/devops-engineer.md deleted file mode 100644 index 8f227feb4..000000000 --- a/plugins/ndf/dev.agy/agents/devops-engineer.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -name: devops-engineer -model: flash -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:external-ai` 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/dev.agy/agents/director.md b/plugins/ndf/dev.agy/agents/director.md deleted file mode 100644 index 37c529480..000000000 --- a/plugins/ndf/dev.agy/agents/director.md +++ /dev/null @@ -1,431 +0,0 @@ ---- -name: director -model: pro -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に移行 -- 設計案を作成しユーザー承認を得る(承認ゲート 1。`AskUserQuestion` の `metadata.source` は `ndf-gate:1`) -- **計画と調査結果をファイルに保存**(復帰可能にするため必須) - -### フェーズ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:external-ai` 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. **再開を AI が決める(問わない):** 保存した計画と進捗から続きを再開し、次を報告に書く。再開した判断は - `ask.py decide --question "続きから再開するか" --answer "再開する" --reason "<根拠>"` で記録する - ``` - 前回のセッションで{タスク名}を進めていました。 - 現在のステータス: フェーズ{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. ユーザー承認を取得(`ndf-gate:1`) -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 - コード構造理解、シンボル編集(memory のツールは使わない) -- 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/dev.agy/agents/qa.md b/plugins/ndf/dev.agy/agents/qa.md deleted file mode 100644 index 36394cb7d..000000000 --- a/plugins/ndf/dev.agy/agents/qa.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -name: qa -model: flash -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:external-ai` skill の `external-ai.py run codex` を 1 行で呼びます。 - -## 専門領域 - -### 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_mcp-serena_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/dev.agy/agents/researcher.md b/plugins/ndf/dev.agy/agents/researcher.md deleted file mode 100644 index 42d178756..000000000 --- a/plugins/ndf/dev.agy/agents/researcher.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -name: researcher -model: flash -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:external-ai` 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:external-ai` 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/dev.agy/agents/supervisor-waits.md b/plugins/ndf/dev.agy/agents/supervisor-waits.md deleted file mode 100644 index acffaa0bc..000000000 --- a/plugins/ndf/dev.agy/agents/supervisor-waits.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: supervisor-waits -description: NDF の 3 層の supervisor(収束ループから始める起動。キャッシュの寿命 1 時間)。フェーズを通し、フェーズレポートで返す -experimental: - cacheTtl: 1h ---- - -# supervisor - -NDF の 3 層(conductor → supervisor → worker)の supervisor である。conductor が起動し、 -1 つのフェーズを通して `## フェーズの報告` で返す。規約の正本は `development-workflow` の -`references/agent-layers.md` の「conductor → supervisor」にある。 - -**守る規則は起動指示の「守る規則」が渡す。** この定義は規則を写さない。起動指示に規則が -無いときは、正本の規則に従う。 - -**寿命の違う 2 つの定義があり、本文は同じである。** `supervisor` はキャッシュの寿命が 5 分で、 -収束ループの工程(リファクタリング・コードレビュー・ドキュメントレビュー)をフェーズの途中で始めると -hook が止める。止められたら `結果: スイッチポイント` で返す(規則 12)。`supervisor-waits` は寿命が -1 時間で、収束ループの工程から始める起動に使い、hook は止めない。 diff --git a/plugins/ndf/dev.agy/agents/supervisor.md b/plugins/ndf/dev.agy/agents/supervisor.md deleted file mode 100644 index a7d3b2a63..000000000 --- a/plugins/ndf/dev.agy/agents/supervisor.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: supervisor -description: NDF の 3 層の supervisor。1 つのフェーズ(連続する工程)を通し、フェーズレポートで返す ---- - -# supervisor - -NDF の 3 層(conductor → supervisor → worker)の supervisor である。conductor が起動し、 -1 つのフェーズを通して `## フェーズの報告` で返す。規約の正本は `development-workflow` の -`references/agent-layers.md` の「conductor → supervisor」にある。 - -**守る規則は起動指示の「守る規則」が渡す。** この定義は規則を写さない。起動指示に規則が -無いときは、正本の規則に従う。 - -**寿命の違う 2 つの定義があり、本文は同じである。** `supervisor` はキャッシュの寿命が 5 分で、 -収束ループの工程(リファクタリング・コードレビュー・ドキュメントレビュー)をフェーズの途中で始めると -hook が止める。止められたら `結果: スイッチポイント` で返す(規則 12)。`supervisor-waits` は寿命が -1 時間で、収束ループの工程から始める起動に使い、hook は止めない。 diff --git a/plugins/ndf/dev.agy/agents/worker.md b/plugins/ndf/dev.agy/agents/worker.md deleted file mode 100644 index 5a9e66658..000000000 --- a/plugins/ndf/dev.agy/agents/worker.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: worker -description: NDF の 3 層の worker。1 つの作業(調査・修正・検証・集計)を行い、作業の報告で返す -disallowedTools: Skill, Agent ---- - -# worker - -NDF の 3 層(conductor → supervisor → worker)の worker である。supervisor が起動し、 -1 つの作業を行って `## 作業の報告` で返す。規約の正本は `development-workflow` の -`references/agent-layers.md` の「supervisor → worker」にある。 - -**Skill と Agent のツールはこの定義で外してある。** 手順は起動指示の「手順」に従う。 -`SKILL.md` を読まない。手順が足りないときは `結果: 判断が要る` で返す。 - -守る規則: - -1. 人間へ問わない。別のサブエージェントを起動しない -2. 進行を記録しない(記録は supervisor が行う) -3. 収束の判定・設計の決定・受け入れ条件の書き換えを行わない。判断が要るときは - `結果: 判断が要る` で返す -4. 最後の応答の末尾に `## 作業の報告`(作業・結果・見つけたもの・置き場所・次にすること)を置く -5. 背景の処理を残したまま応答を終えない。待つときは `run_in_background` で起動して完了通知を待つ -6. Skill を起動しない。`SKILL.md` を読まない。手順は起動指示の「手順」に従う -7. 手段(コマンド・手順の順序・Tool)は状況に合わせて変えてよい。作業を妨げる不具合が作業場所の中で直せ、 - 即時修正の 4 条件(`development-workflow` の SKILL.md の「即時修正」)を満たすなら、直して続け「見つけたもの」に書く。 - 作業場所の状態が想定と違っても(detached HEAD など)作業場所の中で完遂する。置き場所(作業場所・ブランチ・ - 結果ファイルや出力のパス)と受け入れ条件・範囲は変えない。置き場所どうしが食い違ったら `結果: 判断が要る` で返す diff --git a/plugins/ndf/dev.agy/hooks.json b/plugins/ndf/dev.agy/hooks.json deleted file mode 100644 index 635d4304d..000000000 --- a/plugins/ndf/dev.agy/hooks.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "ndf-worktree": { - "PreToolUse": [ - { - "matcher": "Edit|MultiEdit|Write|NotebookEdit|fs_write|edit_file|write_file|str_replace_editor|replace|write_to_file|replace_file_content|apply_patch|Bash|shell|execute_bash|local_shell|run_command|run_shell_command", - "hooks": [ - { - "type": "command", - "command": "bash ./scripts/worktree-guard.sh", - "timeout": 5 - } - ] - } - ], - "PreInvocation": [ - { - "type": "command", - "command": "bash ./scripts/worktree-session.sh", - "timeout": 10 - } - ] - } -} diff --git a/plugins/ndf/dev.agy/install-hooks.sh b/plugins/ndf/dev.agy/install-hooks.sh deleted file mode 100755 index 4e1684150..000000000 --- a/plugins/ndf/dev.agy/install-hooks.sh +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env bash -# NDF hook installer for agy (Antigravity CLI) -# Usage: bash plugins/ndf/dev.agy/install-hooks.sh [--uninstall] [--dry-run] -# [--config PATH] [--plugin-dir PATH] -# -# `agy plugin install` は hooks.json を複製するが、**agy はそれを読み込まない**。読む先は -# 利用者の `~/.gemini/config/hooks.json` の 1 か所だけである(agy 1.1.26 で実測。プラグイン -# 配下とプロジェクト直下のどちらに置いても `loaded 1 named hooks from 1 hooks.json file(s)` -# のまま変わらない)。このスクリプトが、導入したプラグインの名前付き hook をその 1 か所へ -# 差し込む。 -# -# 冪等である。同じ名前の項目は置き換えるだけで、他の項目には触れない。 -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - -CONFIG_FILE="${HOME}/.gemini/config/hooks.json" -PLUGIN_DIR="${HOME}/.gemini/config/plugins/ndf" -UNINSTALL=false -DRY_RUN=false - -while [ "$#" -gt 0 ]; do - case "$1" in - --uninstall) UNINSTALL=true ;; - --dry-run) DRY_RUN=true ;; - --config) - [ "$#" -ge 2 ] || { echo "ERROR: --config requires a path" >&2; exit 2; } - CONFIG_FILE="$2"; shift ;; - --plugin-dir) - [ "$#" -ge 2 ] || { echo "ERROR: --plugin-dir requires a path" >&2; exit 2; } - PLUGIN_DIR="$2"; shift ;; - -h|--help) - sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//' - exit 0 ;; - *) echo "ERROR: unknown option: $1" >&2; exit 2 ;; - esac - shift -done - -# 差し込む定義の出どころは、導入した実体の hooks.json である。実体が無いときだけ、この -# リポジトリの dev.agy/hooks.json へ落ちる(導入前に中身を確かめられるようにするため)。 -# -# **--uninstall でも定義を読む。** 消す対象を「配布する定義が持つ名前」に限るため、 -# 名前の出どころが要る(#417 の 8)。 -SOURCE_HOOKS="$PLUGIN_DIR/hooks.json" -[ -f "$SOURCE_HOOKS" ] || SOURCE_HOOKS="$SCRIPT_DIR/hooks.json" - -if [ ! -f "$SOURCE_HOOKS" ]; then - echo "ERROR: hook の定義が見つからない: $SOURCE_HOOKS" >&2 - exit 1 -fi - -command -v python3 >/dev/null 2>&1 || { - echo "ERROR: python3 が要る(JSON の統合に使う)" >&2 - exit 1 -} - -python3 - "$CONFIG_FILE" "$SOURCE_HOOKS" "$PLUGIN_DIR" "$UNINSTALL" "$DRY_RUN" <<'PYEOF' -import json -import os -import pathlib -import re -import shlex -import shutil -import sys -import tempfile - -config_path = pathlib.Path(sys.argv[1]) -source_path = pathlib.Path(sys.argv[2]) -# **相対のまま保存しない。** agy はリポジトリの外でも起動するため、実行時の現在地は -# プラグインの位置と揃わない(#417 の 3)。案内は clone からの相対パスで書いてあり、 -# 書かれたとおりに実行すると踏む。 -plugin_dir = pathlib.Path(sys.argv[3]).expanduser().resolve() -uninstall = sys.argv[4] == "true" -dry_run = sys.argv[5] == "true" - -# 書き換えの対象にする command の形。`bash ./scripts/<名前>` だけを見る。 -RELATIVE_COMMAND = re.compile(r"^bash \./scripts/([A-Za-z0-9._-]+)$") - -unrewritten: list[str] = [] - - -def load(path: pathlib.Path) -> dict: - """JSON を読む。無ければ空、壊れていれば止まる(黙って上書きしない)。""" - if not path.exists(): - return {} - text = path.read_text(encoding="utf-8").strip() - if not text: - return {} - try: - value = json.loads(text) - except json.JSONDecodeError as exc: - sys.exit(f"ERROR: {path} を JSON として読めない: {exc}") - if not isinstance(value, dict): - sys.exit(f"ERROR: {path} の中身が名前付き hook の辞書ではない") - return value - - -def rewrite_command(command: str) -> str: - """`bash ./scripts/x.sh` を、導入先の絶対パスで組み立て直す。 - - **置換ではなく組み立て直しである。** 置換はシェルの語としての正しさを保証しない。 - 導入先に空白や特殊文字が含まれると、**正常終了するのに hook が効かない**状態に - なる(#417 の 4)。対象の形に当たらない command は書き換えず、書き換えなかった - ことを出力へ出す(黙って相対のまま保存しない)。 - """ - matched = RELATIVE_COMMAND.match(command) - if not matched: - if "./scripts/" in command: - unrewritten.append(command) - return command - target = plugin_dir / "scripts" / matched.group(1) - return f"bash {shlex.quote(str(target))}" - - -def absolutize(node: object) -> object: - """`command` の値だけを、導入先の絶対パスへ直す。""" - if isinstance(node, dict): - return { - key: ( - rewrite_command(value) - if key == "command" and isinstance(value, str) - else absolutize(value) - ) - for key, value in node.items() - } - if isinstance(node, list): - return [absolutize(value) for value in node] - return node - - -source = load(source_path) -config = load(config_path) -# **消すのは、配布する定義が持つ名前だけである。** 接頭辞で選ぶと、利用者が自分で -# 書いた `ndf-` 始まりの hook までまとめて消える(#417 の 8)。 -names = sorted(n for n in source if n in config) if uninstall else sorted(source) - -if uninstall: - updated = {k: v for k, v in config.items() if k not in names} -else: - updated = dict(config) - for name in names: - updated[name] = absolutize(source[name]) - -for command in unrewritten: - print(f"NOTE: 対象の形に当たらないため書き換えられなかった command: {command}") - -if updated == config: - print(f"変更なし: {config_path}") - print(f"対象の hook: {', '.join(names) if names else '(なし)'}") - sys.exit(0) - -action = "削除" if uninstall else "差し込み" -print(f"{action}: {', '.join(names) if names else '(なし)'}") -print(f"書き込み先: {config_path}") - -if dry_run: - print("--dry-run のため書き込まない。書き込む内容:") - print(json.dumps(updated, ensure_ascii=False, indent=2)) - sys.exit(0) - -config_path.parent.mkdir(parents=True, exist_ok=True) -if config_path.exists(): - backup = config_path.with_suffix(config_path.suffix + ".bak") - shutil.copy2(config_path, backup) - print(f"退避: {backup}") - -# **置き換えで書く。** 直接書くと長さを 0 へ戻してから書き直すため、中断すると利用者の -# hook が壊れた JSON のまま残る(#417 の 8)。同じディレクトリへ書いてから移す。 -handle = tempfile.NamedTemporaryFile( - "w", encoding="utf-8", dir=str(config_path.parent), - prefix=config_path.name + ".", suffix=".tmp", delete=False, -) -try: - with handle: - handle.write(json.dumps(updated, ensure_ascii=False, indent=2) + "\n") - handle.flush() - os.fsync(handle.fileno()) - os.replace(handle.name, config_path) -except BaseException: - pathlib.Path(handle.name).unlink(missing_ok=True) - raise -print("完了。agy を起動し直すと反映される") -PYEOF diff --git a/plugins/ndf/dev.agy/plugin.json b/plugins/ndf/dev.agy/plugin.json deleted file mode 100644 index f77458981..000000000 --- a/plugins/ndf/dev.agy/plugin.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "ndf", - "version": "10.27.4", - "description": "Antigravity CLI plugin (v10.27.4): 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation, and worktree guidance hooks." -} diff --git a/plugins/ndf/dev.agy/scripts b/plugins/ndf/dev.agy/scripts deleted file mode 120000 index a339954df..000000000 --- a/plugins/ndf/dev.agy/scripts +++ /dev/null @@ -1 +0,0 @@ -../scripts \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/backlog-refinement b/plugins/ndf/dev.agy/skills/backlog-refinement deleted file mode 120000 index 0163331d3..000000000 --- a/plugins/ndf/dev.agy/skills/backlog-refinement +++ /dev/null @@ -1 +0,0 @@ -../../skills/backlog-refinement \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/cherry-pick-pr b/plugins/ndf/dev.agy/skills/cherry-pick-pr deleted file mode 120000 index b1be2abb3..000000000 --- a/plugins/ndf/dev.agy/skills/cherry-pick-pr +++ /dev/null @@ -1 +0,0 @@ -../../skills/cherry-pick-pr \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/chrome-cdp b/plugins/ndf/dev.agy/skills/chrome-cdp deleted file mode 120000 index 3c83baec8..000000000 --- a/plugins/ndf/dev.agy/skills/chrome-cdp +++ /dev/null @@ -1 +0,0 @@ -../../skills/chrome-cdp \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/cross-refactoring b/plugins/ndf/dev.agy/skills/cross-refactoring deleted file mode 120000 index 2e33174a4..000000000 --- a/plugins/ndf/dev.agy/skills/cross-refactoring +++ /dev/null @@ -1 +0,0 @@ -../../skills/cross-refactoring \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/cross-review b/plugins/ndf/dev.agy/skills/cross-review deleted file mode 120000 index 5e19a3360..000000000 --- a/plugins/ndf/dev.agy/skills/cross-review +++ /dev/null @@ -1 +0,0 @@ -../../skills/cross-review \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/decision-request b/plugins/ndf/dev.agy/skills/decision-request deleted file mode 120000 index cc949c2d2..000000000 --- a/plugins/ndf/dev.agy/skills/decision-request +++ /dev/null @@ -1 +0,0 @@ -../../skills/decision-request \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/deploy b/plugins/ndf/dev.agy/skills/deploy deleted file mode 120000 index 9b4912f9b..000000000 --- a/plugins/ndf/dev.agy/skills/deploy +++ /dev/null @@ -1 +0,0 @@ -../../skills/deploy \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/design b/plugins/ndf/dev.agy/skills/design deleted file mode 120000 index 288cb4e14..000000000 --- a/plugins/ndf/dev.agy/skills/design +++ /dev/null @@ -1 +0,0 @@ -../../skills/design \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/development-workflow b/plugins/ndf/dev.agy/skills/development-workflow deleted file mode 120000 index 3b252c082..000000000 --- a/plugins/ndf/dev.agy/skills/development-workflow +++ /dev/null @@ -1 +0,0 @@ -../../skills/development-workflow \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/docker-container-access b/plugins/ndf/dev.agy/skills/docker-container-access deleted file mode 120000 index be3faec7f..000000000 --- a/plugins/ndf/dev.agy/skills/docker-container-access +++ /dev/null @@ -1 +0,0 @@ -../../skills/docker-container-access \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/document-drafting b/plugins/ndf/dev.agy/skills/document-drafting deleted file mode 120000 index c25e273aa..000000000 --- a/plugins/ndf/dev.agy/skills/document-drafting +++ /dev/null @@ -1 +0,0 @@ -../../skills/document-drafting \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/document-rebuild b/plugins/ndf/dev.agy/skills/document-rebuild deleted file mode 120000 index 2b1a27fb7..000000000 --- a/plugins/ndf/dev.agy/skills/document-rebuild +++ /dev/null @@ -1 +0,0 @@ -../../skills/document-rebuild \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/document-sources b/plugins/ndf/dev.agy/skills/document-sources deleted file mode 120000 index 4b46abf0c..000000000 --- a/plugins/ndf/dev.agy/skills/document-sources +++ /dev/null @@ -1 +0,0 @@ -../../skills/document-sources \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/document-style b/plugins/ndf/dev.agy/skills/document-style deleted file mode 120000 index c0d355f04..000000000 --- a/plugins/ndf/dev.agy/skills/document-style +++ /dev/null @@ -1 +0,0 @@ -../../skills/document-style \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/document-systems b/plugins/ndf/dev.agy/skills/document-systems deleted file mode 120000 index 417342832..000000000 --- a/plugins/ndf/dev.agy/skills/document-systems +++ /dev/null @@ -1 +0,0 @@ -../../skills/document-systems \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/estimate b/plugins/ndf/dev.agy/skills/estimate deleted file mode 120000 index d726b2193..000000000 --- a/plugins/ndf/dev.agy/skills/estimate +++ /dev/null @@ -1 +0,0 @@ -../../skills/estimate \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/external-ai b/plugins/ndf/dev.agy/skills/external-ai deleted file mode 120000 index dc45f6085..000000000 --- a/plugins/ndf/dev.agy/skills/external-ai +++ /dev/null @@ -1 +0,0 @@ -../../skills/external-ai \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/fix b/plugins/ndf/dev.agy/skills/fix deleted file mode 120000 index c9534c936..000000000 --- a/plugins/ndf/dev.agy/skills/fix +++ /dev/null @@ -1 +0,0 @@ -../../skills/fix \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/google-workspace b/plugins/ndf/dev.agy/skills/google-workspace deleted file mode 120000 index 3bceed204..000000000 --- a/plugins/ndf/dev.agy/skills/google-workspace +++ /dev/null @@ -1 +0,0 @@ -../../skills/google-workspace \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/implementation-plan b/plugins/ndf/dev.agy/skills/implementation-plan deleted file mode 120000 index 83547e127..000000000 --- a/plugins/ndf/dev.agy/skills/implementation-plan +++ /dev/null @@ -1 +0,0 @@ -../../skills/implementation-plan \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/investigation-rules b/plugins/ndf/dev.agy/skills/investigation-rules deleted file mode 120000 index 0b63b44fb..000000000 --- a/plugins/ndf/dev.agy/skills/investigation-rules +++ /dev/null @@ -1 +0,0 @@ -../../skills/investigation-rules \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/issue-plan-strategy b/plugins/ndf/dev.agy/skills/issue-plan-strategy deleted file mode 120000 index ce66014c9..000000000 --- a/plugins/ndf/dev.agy/skills/issue-plan-strategy +++ /dev/null @@ -1 +0,0 @@ -../../skills/issue-plan-strategy \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/layout-review b/plugins/ndf/dev.agy/skills/layout-review deleted file mode 120000 index 9b9b56a14..000000000 --- a/plugins/ndf/dev.agy/skills/layout-review +++ /dev/null @@ -1 +0,0 @@ -../../skills/layout-review \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/logging-guidelines b/plugins/ndf/dev.agy/skills/logging-guidelines deleted file mode 120000 index 6987f9ad6..000000000 --- a/plugins/ndf/dev.agy/skills/logging-guidelines +++ /dev/null @@ -1 +0,0 @@ -../../skills/logging-guidelines \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/merged b/plugins/ndf/dev.agy/skills/merged deleted file mode 120000 index dc91bf9bf..000000000 --- a/plugins/ndf/dev.agy/skills/merged +++ /dev/null @@ -1 +0,0 @@ -../../skills/merged \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/ml-model-structure b/plugins/ndf/dev.agy/skills/ml-model-structure deleted file mode 120000 index abf7c0518..000000000 --- a/plugins/ndf/dev.agy/skills/ml-model-structure +++ /dev/null @@ -1 +0,0 @@ -../../skills/ml-model-structure \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/ndf-policies b/plugins/ndf/dev.agy/skills/ndf-policies deleted file mode 120000 index fe50a72fb..000000000 --- a/plugins/ndf/dev.agy/skills/ndf-policies +++ /dev/null @@ -1 +0,0 @@ -../../skills/ndf-policies \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/notion-writing b/plugins/ndf/dev.agy/skills/notion-writing deleted file mode 120000 index 4eb4605b1..000000000 --- a/plugins/ndf/dev.agy/skills/notion-writing +++ /dev/null @@ -1 +0,0 @@ -../../skills/notion-writing \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/out-of-scope b/plugins/ndf/dev.agy/skills/out-of-scope deleted file mode 120000 index 801078dd8..000000000 --- a/plugins/ndf/dev.agy/skills/out-of-scope +++ /dev/null @@ -1 +0,0 @@ -../../skills/out-of-scope \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/plan-to-spec b/plugins/ndf/dev.agy/skills/plan-to-spec deleted file mode 120000 index 0d3b1bf7f..000000000 --- a/plugins/ndf/dev.agy/skills/plan-to-spec +++ /dev/null @@ -1 +0,0 @@ -../../skills/plan-to-spec \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/pr b/plugins/ndf/dev.agy/skills/pr deleted file mode 120000 index 5167ba85c..000000000 --- a/plugins/ndf/dev.agy/skills/pr +++ /dev/null @@ -1 +0,0 @@ -../../skills/pr \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/pr-review b/plugins/ndf/dev.agy/skills/pr-review deleted file mode 120000 index 0daced676..000000000 --- a/plugins/ndf/dev.agy/skills/pr-review +++ /dev/null @@ -1 +0,0 @@ -../../skills/pr-review \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/pr-tests b/plugins/ndf/dev.agy/skills/pr-tests deleted file mode 120000 index e6b0e830e..000000000 --- a/plugins/ndf/dev.agy/skills/pr-tests +++ /dev/null @@ -1 +0,0 @@ -../../skills/pr-tests \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/problem-solving b/plugins/ndf/dev.agy/skills/problem-solving deleted file mode 120000 index 2cfb3d9cc..000000000 --- a/plugins/ndf/dev.agy/skills/problem-solving +++ /dev/null @@ -1 +0,0 @@ -../../skills/problem-solving \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/progress-tracking b/plugins/ndf/dev.agy/skills/progress-tracking deleted file mode 120000 index 0b6c6c7b8..000000000 --- a/plugins/ndf/dev.agy/skills/progress-tracking +++ /dev/null @@ -1 +0,0 @@ -../../skills/progress-tracking \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/qa-security-scan b/plugins/ndf/dev.agy/skills/qa-security-scan deleted file mode 120000 index a3089de12..000000000 --- a/plugins/ndf/dev.agy/skills/qa-security-scan +++ /dev/null @@ -1 +0,0 @@ -../../skills/qa-security-scan \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/quality-gates b/plugins/ndf/dev.agy/skills/quality-gates deleted file mode 120000 index 66afc786f..000000000 --- a/plugins/ndf/dev.agy/skills/quality-gates +++ /dev/null @@ -1 +0,0 @@ -../../skills/quality-gates \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/refactoring b/plugins/ndf/dev.agy/skills/refactoring deleted file mode 120000 index 70a03362b..000000000 --- a/plugins/ndf/dev.agy/skills/refactoring +++ /dev/null @@ -1 +0,0 @@ -../../skills/refactoring \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/release b/plugins/ndf/dev.agy/skills/release deleted file mode 120000 index 8d3c1a425..000000000 --- a/plugins/ndf/dev.agy/skills/release +++ /dev/null @@ -1 +0,0 @@ -../../skills/release \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/release-verification b/plugins/ndf/dev.agy/skills/release-verification deleted file mode 120000 index a2745ba4b..000000000 --- a/plugins/ndf/dev.agy/skills/release-verification +++ /dev/null @@ -1 +0,0 @@ -../../skills/release-verification \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/requirements-design b/plugins/ndf/dev.agy/skills/requirements-design deleted file mode 120000 index 9c666662f..000000000 --- a/plugins/ndf/dev.agy/skills/requirements-design +++ /dev/null @@ -1 +0,0 @@ -../../skills/requirements-design \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/retrospective b/plugins/ndf/dev.agy/skills/retrospective deleted file mode 120000 index 837cc05d8..000000000 --- a/plugins/ndf/dev.agy/skills/retrospective +++ /dev/null @@ -1 +0,0 @@ -../../skills/retrospective \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/skill-stats b/plugins/ndf/dev.agy/skills/skill-stats deleted file mode 120000 index 3dc2587e0..000000000 --- a/plugins/ndf/dev.agy/skills/skill-stats +++ /dev/null @@ -1 +0,0 @@ -../../skills/skill-stats \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/tdd-cycle b/plugins/ndf/dev.agy/skills/tdd-cycle deleted file mode 120000 index ffa0c5d76..000000000 --- a/plugins/ndf/dev.agy/skills/tdd-cycle +++ /dev/null @@ -1 +0,0 @@ -../../skills/tdd-cycle \ No newline at end of file diff --git a/plugins/ndf/dev.agy/skills/worktree b/plugins/ndf/dev.agy/skills/worktree deleted file mode 120000 index 28d0b89b0..000000000 --- a/plugins/ndf/dev.agy/skills/worktree +++ /dev/null @@ -1 +0,0 @@ -../../skills/worktree \ No newline at end of file diff --git a/plugins/ndf/dev.kiro/agents/ndf.json.template b/plugins/ndf/dev.kiro/agents/ndf.json.template deleted file mode 100644 index b82fd3bad..000000000 --- a/plugins/ndf/dev.kiro/agents/ndf.json.template +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "ndf", - "description": "NDF統合開発エージェント(Kiro CLI用 / v{{VERSION}})", - "tools": [ - "*" - ], - "resources": [ - "file://AGENTS.md", - "file://README.md" - ], - "hooks": { - "agentSpawn": [ - { - "command": "if [ -f \"${PWD}/CLAUDE.ndf.md\" ] || [ -f \"$HOME/.claude/CLAUDE.ndf.md\" ]; then echo \"[NDF] CLAUDE.ndf.md が検出されました。廃止済みです。cleanup を実行して削除してください。\"; fi" - } - ] - } -} diff --git a/plugins/ndf/dev.kiro/install.sh b/plugins/ndf/dev.kiro/install.sh deleted file mode 100755 index 22493894b..000000000 --- a/plugins/ndf/dev.kiro/install.sh +++ /dev/null @@ -1,561 +0,0 @@ -#!/usr/bin/env bash -# NDF Plugin Installer for Kiro CLI -# Usage: bash plugins/ndf/dev.kiro/install.sh [--project PATH] [--scope workspace|global] -# [--set-default] [--yes] -# [--with-slack] [--with-codex] [--dry-run] -# -# このスクリプトは Agent Plugins 仕様 §8.2 のクライアント拡張ディレクトリ(dev.kiro/)に -# 置く。プラグインルートは 1 つ上の階層で、Skill と scripts はそちらから読む。Kiro CLI 固有の -# エージェント定義とプロンプトだけがこのディレクトリにある。 -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -AGENT_NAME="ndf" -TEMPLATE_FILE="$SCRIPT_DIR/agents/$AGENT_NAME.json.template" -PLUGIN_SKILLS_DIR="$PLUGIN_DIR/skills" -PLUGIN_PROMPTS_DIR="$SCRIPT_DIR/prompts" -POLICY_SKILL_FILE="$PLUGIN_SKILLS_DIR/ndf-policies/SKILL.md" -SKILL_MANIFEST="$PLUGIN_DIR/manifests/kiro-skills.txt" -MANIFEST_FILE="$PLUGIN_DIR/.claude-plugin/plugin.json" -# 版数はルートのマニフェストだけが持つ。Kiro 向けに複製した VERSION ファイルは置かない -# (同じ値を 2 箇所に置くと、片方だけが古くなる)。 -# Skill の統合・改名により配布を終えた prompt。過去のインストールで .kiro/prompts/ に残った分を除去する -# review.md は v6.0.0 で pr-review.md へ改名した -DEPRECATED_PROMPTS="clean.md review.md" - -# Parse options -PROJECT_ROOT="$(pwd)" -PROJECT_GIVEN=false -SCOPE="workspace" -SET_DEFAULT=false -ASSUME_YES=false -WITH_SLACK=false -WITH_CODEX=false -DRY_RUN=false -while [ "$#" -gt 0 ]; do - case "$1" in - --project) - [ "$#" -ge 2 ] || { echo "ERROR: --project requires a path" >&2; exit 2; } - # cd の裸のエラーで終わらせず、何を直せばよいかを案内して止める(#415)。 - # 作り方は存在しないパスにだけ添える(ファイルに mkdir -p は失敗する)。 - # 案内のパスは printf '%q' で bash の語 1 つとして読める形へ整える。 - [ -e "$2" ] || { - echo "ERROR: --project points at a path that does not exist: $2" >&2 - printf 'HINT: mkdir -p %q\n' "$2" >&2 - exit 2 - } - [ -d "$2" ] || { echo "ERROR: --project points at a path that is not a directory: $2" >&2; exit 2; } - PROJECT_ROOT="$(cd "$2" && pwd)" - PROJECT_GIVEN=true - shift - ;; - --scope) - [ "$#" -ge 2 ] || { echo "ERROR: --scope requires workspace or global" >&2; exit 2; } - SCOPE="$2" - shift - ;; - --set-default) SET_DEFAULT=true ;; - --yes|-y) ASSUME_YES=true ;; - --with-slack) WITH_SLACK=true ;; - --with-codex) WITH_CODEX=true ;; - --dry-run) DRY_RUN=true ;; - --help|-h) - echo "Usage: bash plugins/ndf/dev.kiro/install.sh [OPTIONS]" - echo "" - echo "Options:" - echo " --project PATH install into PATH instead of current directory (--scope workspace のみ)" - echo " --scope SCOPE workspace(既定, プロジェクトの .kiro/)または global(~/.kiro/)" - echo " --set-default kiro-cli の既定エージェントを ndf に切り替える(オプトイン)" - echo " -y, --yes --set-default の確認プロンプトを省略する" - echo " --with-slack stopフックに回答・承認待ちのSlack通知を追加" - echo " --with-codex Codex MCPサーバ設定と直接実行用プロンプトを追加" - echo " --dry-run 書き込みを行わず実行内容を表示" - echo " -h, --help このヘルプを表示" - exit 0 - ;; - *) - echo "ERROR: unknown option: $1" >&2 - exit 2 - ;; - esac - shift -done - -case "$SCOPE" in - workspace) - KIRO_DIR="$PROJECT_ROOT/.kiro" - ;; - global) - [ -n "${HOME:-}" ] || { echo "ERROR: --scope global には HOME が必要です" >&2; exit 2; } - KIRO_DIR="$HOME/.kiro" - if [ "$PROJECT_GIVEN" = true ]; then - echo "WARN: --scope global では --project は使用されません" >&2 - fi - ;; - *) - echo "ERROR: invalid --scope: $SCOPE (workspace|global)" >&2 - exit 2 - ;; -esac - -SKILLS_DIR="$KIRO_DIR/skills" -PROMPTS_DIR="$KIRO_DIR/prompts" -STEERING_FILE="$KIRO_DIR/steering/ndf-policies.md" -AGENT_FILE="$KIRO_DIR/agents/$AGENT_NAME.json" -LEGACY_AGENT_FILE="$KIRO_DIR/agents/default.json" - -require_path() { - local test_flag="$1" path="$2" - test "$test_flag" "$path" || { - echo "ERROR: $path が見つかりません" >&2 - exit 1 - } -} - -# 掃除するのは現在の checkout($PLUGIN_SKILLS_DIR 配下)を指す既存リンクだけに限る。 -# 別の場所を指す利用者のリンクは残す。旧 installer が別 checkout から張ったリンクは -# ここでは掛からず、後段の migrate_policy_skill_entry などが名前単位で扱う。 -remove_managed_skill_links() { - mkdir -p "$SKILLS_DIR" - local skill_link target target_abs plugin_skills_abs - plugin_skills_abs="$(realpath -m "$PLUGIN_SKILLS_DIR")" - while IFS= read -r skill_link; do - target="$(readlink "$skill_link")" - case "$target" in - /*) target_abs="$target" ;; - *) target_abs="$(realpath -m "$(dirname "$skill_link")/$target")" ;; - esac - case "$target_abs" in - "$plugin_skills_abs"/*) rm -f "$skill_link" ;; - esac - done < <(find "$SKILLS_DIR" -mindepth 1 -maxdepth 1 -type l | sort) -} - -# ndf-policies は Step 3 で steering として展開する。Skill としてもリンクすると -# Kiro 組み込みルールの Skill 読み込みと steering 読み込みで文脈へ二重注入されるため、 -# ここではリンクしない。manifest には残す(steering の生成元として必要なため)。 -# 旧 installer が別 checkout から張ったリンクは remove_managed_skill_links の掃除 -# (現在の $PLUGIN_SKILLS_DIR 配下を指すものだけ削除)に掛からないため、ここで -# リンク先に関係なく既存のエントリを取り除いてから skip する。 -migrate_policy_skill_entry() { - local skill_name="$1" - # 削除するのは旧 installer が張ったシンボリックリンクだけに限る。実体 - # ディレクトリや通常ファイルは利用者が置いたものの可能性があるため、 - # 消さずに案内して手動対応に委ねる。 - if [ -L "$SKILLS_DIR/$skill_name" ]; then - if [ "$DRY_RUN" = false ]; then - rm -f "$SKILLS_DIR/$skill_name" - fi - echo " REMOVED: $skill_name (steering へ移行済みのため .kiro/skills のリンクを削除)" - elif [ -e "$SKILLS_DIR/$skill_name" ]; then - echo " WARN: $SKILLS_DIR/$skill_name はシンボリックリンクではありません。" >&2 - echo " steering (.kiro/steering/ndf-policies.md) と二重に読み込まれるため、" >&2 - echo " 内容を確認のうえ手動で退避または削除してください。" >&2 - fi - echo " SKIP: $skill_name (steering として配置)" -} - -# manifest 掲載の Skill を .kiro/skills/ へリンクする。張った本数はグローバルの -# SKILL_COUNT へ明示して返し(標準出力は " linked:" 等の案内だけに保つ)、呼び出し側は -# その変数を読む。ndf-policies は migrate_policy_skill_entry に委ね、リンクは張らない。 -install_manifest_skills() { - SKILL_COUNT=0 - local src_dir skill_name - while IFS= read -r src_dir; do - skill_name="$(basename "$src_dir")" - - if [ ! -f "$src_dir/SKILL.md" ]; then - echo " SKIP: $skill_name (SKILL.mdなし)" - continue - fi - - if [ "$skill_name" = "ndf-policies" ]; then - migrate_policy_skill_entry "$skill_name" - continue - fi - - if [ "$DRY_RUN" = false ]; then - ln -sfn "$PLUGIN_SKILLS_DIR/$skill_name" "$SKILLS_DIR/$skill_name" - fi - echo " linked: $skill_name" - SKILL_COUNT=$((SKILL_COUNT + 1)) - # 配る Skill は manifests/kiro-skills.txt が決める。skills/ にはどの runtime へも配る - # Skill が並んでおり、Kiro 向けはその一部である。ディレクトリを列挙すると、他の runtime - # だけへ配る Skill まで張ってしまう。 - done < <(sed 's/#.*//' "$SKILL_MANIFEST" | sed 's/[[:space:]]*$//' \ - | grep -v '^$' | sort | sed "s#^#$PLUGIN_SKILLS_DIR/#") -} - -# 再インストールで消すのは installer が配った prompt だけに限る。--with-codex を -# 外したときの codex.md と、配布を終えた $DEPRECATED_PROMPTS を除き、利用者の prompt は残す。 -cleanup_managed_prompts() { - mkdir -p "$PROMPTS_DIR" - if [ "$WITH_CODEX" = false ]; then - rm -f "$PROMPTS_DIR/codex.md" - fi - local deprecated_prompt - for deprecated_prompt in $DEPRECATED_PROMPTS; do - if [ -f "$PROMPTS_DIR/$deprecated_prompt" ]; then - rm -f "$PROMPTS_DIR/$deprecated_prompt" - echo " removed (deprecated): ${deprecated_prompt%.md}" - fi - done -} - -# 配布元の prompt を .kiro/prompts/ へコピーする。codex.md の有無のチェックはコピーと -# 案内の出力を終えた後に行う(チェックより前の標準出力と配置を変えないため)。 -install_prompts() { - local prompt_file prompt_name - while IFS= read -r prompt_file; do - prompt_name="$(basename "$prompt_file")" - [ "$prompt_name" = "codex.md" ] && [ "$WITH_CODEX" = false ] && continue - if [ "$DRY_RUN" = false ]; then - cp "$prompt_file" "$PROMPTS_DIR/$prompt_name" - fi - echo " prompt: ${prompt_name%.md}" - done < <(find "$PLUGIN_PROMPTS_DIR" -maxdepth 1 -type f -name '*.md' | sort) - - if [ "$WITH_CODEX" = true ]; then - require_path -f "$PLUGIN_PROMPTS_DIR/codex.md" - fi -} - -echo "=== NDF Plugin Installer for Kiro CLI ===" -echo " スコープ: $SCOPE ($KIRO_DIR)" - -require_path -d "$PLUGIN_SKILLS_DIR" -require_path -f "$SKILL_MANIFEST" -require_path -f "$MANIFEST_FILE" -NDF_VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$MANIFEST_FILE")" -if [ -z "$NDF_VERSION" ]; then - echo "ERROR: $MANIFEST_FILE から版数を読み取れません" >&2 - exit 1 -fi - -require_path -f "$TEMPLATE_FILE" -require_path -f "$POLICY_SKILL_FILE" - -# --- Step 1: Create symlinks in /skills/ --- -echo "Skills シンボリックリンクを作成中..." -if [ "$DRY_RUN" = false ]; then - remove_managed_skill_links -fi -install_manifest_skills - -# --- Step 2: Create prompts in /prompts/ for workflow skills --- -echo "ワークフロープロンプトを作成中..." -if [ "$DRY_RUN" = false ]; then - cleanup_managed_prompts -fi -install_prompts - -if [ "$WITH_SLACK" = true ]; then echo "Slack通知: 有効"; else echo "Slack通知: 無効 (--with-slack で有効化)"; fi -if [ "$WITH_CODEX" = true ]; then echo "Codex CLI連携: 有効"; else echo "Codex CLI連携: 無効 (--with-codex で有効化)"; fi - -if [ "$DRY_RUN" = true ]; then - echo "" - echo "DRY RUN: 書き込みは行いませんでした" - if [ -f "$LEGACY_AGENT_FILE" ]; then - echo " 旧設定 $LEGACY_AGENT_FILE を検出(実行時に移行可否を判定します)" - fi - echo " NDF バージョン: $NDF_VERSION" - echo " エージェント設定: $AGENT_FILE" - echo " 常時指示: $STEERING_FILE" - echo " Skills数: $SKILL_COUNT" - exit 0 -fi - -# --- Step 3: Generate steering (always-on instructions) --- -# steering はエージェント選択に依存せず読み込まれるため、常時指示はここへ置く。 -mkdir -p "$(dirname "$STEERING_FILE")" -python3 - "$POLICY_SKILL_FILE" "$STEERING_FILE" <<'PY' -import sys -from pathlib import Path - -source, dest = (Path(p) for p in sys.argv[1:3]) -text = source.read_text(encoding="utf-8") -if text.startswith("---\n"): - end = text.find("\n---\n", 3) - if end != -1: - text = text[end + len("\n---\n"):] -body = text.strip("\n") -header = ( - "\n" - "\n" -) -dest.write_text(f"{header}\n{body}\n", encoding="utf-8") -PY -echo "常時指示を生成: $STEERING_FILE" - -# --- Step 4: Migrate legacy default agent --- -MIGRATED_FROM_LEGACY=false -if [ -f "$LEGACY_AGENT_FILE" ]; then - cp "$LEGACY_AGENT_FILE" "${LEGACY_AGENT_FILE}.bak" - echo "" - echo "WARN: 旧エージェント設定 $LEGACY_AGENT_FILE を検出しました。" - echo " バックアップ: ${LEGACY_AGENT_FILE}.bak" - if python3 -c ' -import json, sys -try: - config = json.load(open(sys.argv[1], encoding="utf-8")) - # JSON がオブジェクト以外(配列など)でも AttributeError にせず、壊れた JSON と - # 同じ「NDF 生成物ではない」扱いへ倒す。 - # 旧 installer が生成したものだけを識別する。description に NDF が含まれる - # だけでは、利用者が NDF 用に自作した default エージェントまで移行対象に - # なり、テンプレートで上書きしてしまう。旧テンプレート固有の値との一致を - # 求める。 - LEGACY_DESCRIPTION = "NDF統合開発エージェント(Kiro CLI用)" - LEGACY_RESOURCE = "skill://.kiro/skills/**/SKILL.md" - LEGACY_HOOK_MARK = "CLAUDE.ndf.md" - - def has_legacy_hook(cfg): - hooks = cfg.get("hooks") - if not isinstance(hooks, dict): - return False - spawn = hooks.get("agentSpawn") - if not isinstance(spawn, list): - return False - return any( - isinstance(h, dict) and LEGACY_HOOK_MARK in str(h.get("command") or "") - for h in spawn - ) - - resources = config.get("resources") if isinstance(config, dict) else None - matched = ( - isinstance(config, dict) - and config.get("name") == "default" - and config.get("description") == LEGACY_DESCRIPTION - and ( - (isinstance(resources, list) and LEGACY_RESOURCE in resources) - or has_legacy_hook(config) - ) - ) -except Exception: - sys.exit(1) -sys.exit(0 if matched else 1) -' "$LEGACY_AGENT_FILE"; then - echo " これは旧版 NDF installer の生成物です。" - if [ -f "$AGENT_FILE" ]; then - # 移行先が既にある場合に上書きすると、そちらの利用者設定を失う。手動判断へ回す。 - echo " ただし $AGENT_FILE が既に存在するため自動移行しません。" - echo " 移行手順:" - echo " 1. 必要な設定が ${LEGACY_AGENT_FILE}.bak にだけ残っていないか確認する" - echo " 2. 不要になったら rm $LEGACY_AGENT_FILE ${LEGACY_AGENT_FILE}.bak" - else - # 旧設定を移行先へ置いてから Step 5 に進める。Step 5 は既存ファイルから - # installer 管理外のキーを引き継ぐため、これだけで利用者設定の移行と - # テンプレート由来キー(エージェント名など)の最新化が両方完了する。 - mv "$LEGACY_AGENT_FILE" "$AGENT_FILE" - MIGRATED_FROM_LEGACY=true - echo " $AGENT_FILE へ自動移行しました(利用者が追記した設定は下で引き継ぎます)。" - echo " 不要になったら: rm ${LEGACY_AGENT_FILE}.bak" - fi - else - echo " NDF 以外が管理している設定です。移行手順:" - echo " 1. 必要な mcpServers / hooks を $AGENT_FILE へ写す" - echo " 2. 不要になったら rm $LEGACY_AGENT_FILE ${LEGACY_AGENT_FILE}.bak" - echo " Kiro 用 MCP プラグインの installer は default.json を更新するため、" - echo " MCP を併用する場合は上記の写し替えが必要です。" - echo " 写した設定は本 installer を再実行しても保持されます。" - fi - echo "" -fi - -# --- Step 5: Generate agent config --- -mkdir -p "$KIRO_DIR/agents" - -# Step 4 で移行した直後は ${LEGACY_AGENT_FILE}.bak が同じ内容のバックアップなので取らない。 -if [ -f "$AGENT_FILE" ] && [ "$MIGRATED_FROM_LEGACY" = false ]; then - cp "$AGENT_FILE" "${AGENT_FILE}.bak" - echo "既存設定をバックアップ: ${AGENT_FILE}.bak" -fi - -# installer が管理するのはテンプレート由来のキー(name / description / tools / -# resources / hooks.agentSpawn)と、フラグで切り替える hooks.stop / mcpServers.codex -# だけ。それ以外(利用者が足した mcpServers エントリ、独自フック、独自キー)は -# 既存の $AGENT_FILE から引き継ぐ。再インストールで写し替えた設定が消えないようにする。 -python3 - "$TEMPLATE_FILE" "$WITH_SLACK" "$WITH_CODEX" "$AGENT_FILE" "$PLUGIN_DIR" "$NDF_VERSION" <<'PY' -import json -import shlex -import sys -from pathlib import Path - -template_file, with_slack, with_codex, agent_file, plugin_dir, ndf_version = sys.argv[1:7] -with open(template_file, encoding="utf-8") as f: - config = json.load(f) - -# 導入後にも版数を確認できるよう description へ埋める。Kiro には plugin.json が -# ないため、これと VERSION ファイルが版数を示す唯一の手段になる。 -config["description"] = config["description"].replace("{{VERSION}}", ndf_version) - -# installer が上書きする範囲 -managed_keys = set(config) | {"mcpServers"} -managed_hooks = set(config.get("hooks") or {}) | {"stop", "userPromptSubmit"} -managed_servers = {"codex"} - -hooks = config.setdefault("hooks", {}) - -# 作業ツリー運用の hook。判定は 3 ランタイム共通のスクリプトが持つ。 -# Kiro CLI は tool 実行前の hook からモデルへ案内を渡す手段が終了コード 2 に -# 限られ、これは tool の実行を拒否する。拒否しない方針のもとでは置けないため、 -# パスを見ない案内をプロンプト送信時の hook が担う(詳細設計 06 の決定 5)。 -guard_script = Path(plugin_dir) / "scripts" / "worktree-guard.sh" -session_script = Path(plugin_dir) / "scripts" / "worktree-session.sh" -hooks.setdefault("agentSpawn", []).append( - { - "command": f"bash {shlex.quote(str(session_script))}", - "timeout_ms": 10000, - } -) -hooks["userPromptSubmit"] = [ - { - "command": f"bash {shlex.quote(str(guard_script))}", - "timeout_ms": 5000, - } -] - -if with_slack == "true": - notify_script = Path(plugin_dir) / "scripts" / "wait-notify.py" - hooks["stop"] = [ - { - "command": f"python3 {shlex.quote(str(notify_script))} --runtime kiro", - "timeout_ms": 15000, - } - ] -else: - hooks.pop("stop", None) - -servers = {} -if with_codex == "true": - servers["codex"] = { - "command": "codex", - "args": ["mcp-server"], - "env": {}, - } - -existing = {} -agent_path = Path(agent_file) -if agent_path.is_file(): - try: - loaded = json.loads(agent_path.read_text(encoding="utf-8")) - except (json.JSONDecodeError, UnicodeDecodeError) as exc: - print(f" WARN: 既存の {agent_file} を読めないため引き継ぎません: {exc}") - else: - if isinstance(loaded, dict): - existing = loaded - else: - print(f" WARN: 既存の {agent_file} が JSON オブジェクトではないため引き継ぎません") - -kept = [] -for key, value in existing.items(): - if key not in managed_keys: - config[key] = value - kept.append(key) -# hooks / mcpServers が dict 以外(配列や文字列)だと .items() で落ちるため、 -# 壊れた JSON と同じく警告して引継ぎ対象から外す。 -for section, target, managed in ( - ("hooks", hooks, managed_hooks), - ("mcpServers", servers, managed_servers), -): - value = existing.get(section) - if value is None: - continue - if not isinstance(value, dict): - print(f" WARN: 既存の {agent_file} の {section} が JSON オブジェクトではないため引き継ぎません") - continue - for key, item in value.items(): - if key not in managed: - target[key] = item - kept.append(f"{section}.{key}") - -if not hooks: - config.pop("hooks", None) -if servers: - config["mcpServers"] = servers -else: - config.pop("mcpServers", None) - -with open(agent_file, "w", encoding="utf-8") as f: - json.dump(config, f, indent=2, ensure_ascii=False) - f.write("\n") - -if kept: - print(" 利用者管理の設定を引き継ぎました: " + ", ".join(sorted(kept))) -PY - -# --- Step 6: Optionally switch the default agent --- -if [ "$SET_DEFAULT" = true ]; then - echo "" - if ! command -v kiro-cli >/dev/null 2>&1; then - echo "ERROR: kiro-cli が見つからないため既定エージェントを変更できません" >&2 - exit 1 - fi - # kiro-cli は workspace エージェントを cwd 配下の .kiro/agents からのみ検出する。 - # 呼び出し元 cwd のままだと、--project で別ディレクトリへ導入したエージェントを - # 見つけられない(または同名の別エージェントを既定にしてしまう)。生成した - # $AGENT_FILE を確実に指すディレクトリで kiro-cli を実行する。 - # workspace: 導入先プロジェクトルート ($PROJECT_ROOT/.kiro/agents) - # global: $HOME ($HOME/.kiro/agents = 生成先。global エージェントはどこからでも - # 解決できるが、cwd 側の同名 workspace エージェントに隠されないようにする) - case "$SCOPE" in - workspace) KIRO_CWD="$PROJECT_ROOT" ;; - *) KIRO_CWD="$HOME" ;; - esac - esc="$(printf '\033')" - # kiro-cli 2.16.1 の agent list は一覧を標準エラー出力へ書く - kiro_default_agent() { - (cd "$KIRO_CWD" && kiro-cli agent list 2>&1) \ - | sed -e "s/${esc}\\[[0-9;]*m//g" \ - | awk '/^\*/ { print $2; exit }' - } - # 表示用の取得は失敗しても続行する(未ログイン等でも set-default の結果は後段で検証する) - current_default="$(kiro_default_agent || true)" - echo "既定エージェントの操作ディレクトリ: $KIRO_CWD" - echo "現在の既定エージェント: ${current_default:-不明}" - echo "変更後の既定エージェント: $AGENT_NAME" - proceed=true - if [ "$ASSUME_YES" = false ]; then - if [ -t 0 ]; then - printf '既定エージェントを %s に変更しますか? [y/N]: ' "$AGENT_NAME" - # EOF (Ctrl+D) で read が非ゼロ終了しても set -e で落とさず、既定の N へ倒す - read -r answer || answer="" - case "$answer" in - [yY]|[yY][eE][sS]) ;; - *) proceed=false ;; - esac - else - echo "確認入力を取得できないため、--set-default の指定を承認とみなして続行します" - fi - fi - if [ "$proceed" = true ]; then - (cd "$KIRO_CWD" && kiro-cli agent set-default "$AGENT_NAME") - # kiro-cli 2.16.1 の set-default はエージェント未検出でも終了コード 0 を返すため、 - # 反映結果を agent list で検証する。 - if [ "$(kiro_default_agent || true)" != "$AGENT_NAME" ]; then - echo "ERROR: 既定エージェントを $AGENT_NAME に変更できませんでした($KIRO_CWD で検出できず)" >&2 - exit 1 - fi - echo "既定エージェントを $AGENT_NAME に変更しました(元に戻す: kiro-cli agent set-default ${current_default:-kiro_default})" - else - echo "既定エージェントは変更しませんでした" - fi -fi - -echo "" -echo "=== インストール完了 ===" -echo " NDF バージョン: $NDF_VERSION" -echo " エージェント設定: $AGENT_FILE" -echo " 常時指示: $STEERING_FILE" -echo " Skills数: $SKILL_COUNT (シンボリックリンク: $SKILLS_DIR)" -echo "" -echo "前提: NDF は 1M コンテキストのモデルを対象に配布 Skill を組んでいます。" -echo " 起動時に読み込む文脈は $SKILL_COUNT 個の SKILL.md と AGENTS.md / README.md / 常時指示です。" -echo " 既定の auto はこの条件を満たします。モデルを固定する場合も 1M のものを選んでください。" -echo "" -echo "Kiro CLIを起動して動作確認してください:" -echo " kiro-cli chat --agent $AGENT_NAME" -if [ "$SET_DEFAULT" = false ]; then - echo "" - echo "既定エージェントとして起動したい場合は --set-default を付けて再実行してください。" -fi diff --git a/plugins/ndf/dev.kiro/prompts/codex.md b/plugins/ndf/dev.kiro/prompts/codex.md deleted file mode 100644 index ddb3b52d0..000000000 --- a/plugins/ndf/dev.kiro/prompts/codex.md +++ /dev/null @@ -1,4 +0,0 @@ -Codex CLIにコード生成・レビュー・調査を委譲してください。 - -external-aiスキルの手順に従って実行してください。Codex CLI 固有の呼び出し方法は -`external-ai` スキルの `references/cli-codex.md`(Kiro では `.kiro/skills/external-ai/references/cli-codex.md`)を参照します。引数があればそのまま使用します。 diff --git a/plugins/ndf/dev.kiro/prompts/fix.md b/plugins/ndf/dev.kiro/prompts/fix.md deleted file mode 100644 index 468dacabd..000000000 --- a/plugins/ndf/dev.kiro/prompts/fix.md +++ /dev/null @@ -1,3 +0,0 @@ -PRのレビューコメントを確認し、修正対応を実行してください。 - -fixスキルの手順に従って実行してください。引数があればそのまま使用します。 diff --git a/plugins/ndf/dev.kiro/prompts/merged.md b/plugins/ndf/dev.kiro/prompts/merged.md deleted file mode 100644 index f0a158330..000000000 --- a/plugins/ndf/dev.kiro/prompts/merged.md +++ /dev/null @@ -1,3 +0,0 @@ -PRマージ後のクリーンアップ(main更新、worktree/ブランチ削除)、または作業中ブランチへの main 取り込みを実行してください。 - -mergedスキルの「用途の切り分け」で依頼の意図を判定し、該当する節の手順に従って実行してください。引数があればそのまま使用します。 diff --git a/plugins/ndf/dev.kiro/prompts/pr-review.md b/plugins/ndf/dev.kiro/prompts/pr-review.md deleted file mode 100644 index 1b4479df1..000000000 --- a/plugins/ndf/dev.kiro/prompts/pr-review.md +++ /dev/null @@ -1,3 +0,0 @@ -PRを専門家としてレビューし、Approve/Request Changesを判定してください。 - -pr-reviewスキルの手順に従って実行してください。引数があればそのまま使用します。 diff --git a/plugins/ndf/dev.kiro/prompts/pr-tests.md b/plugins/ndf/dev.kiro/prompts/pr-tests.md deleted file mode 100644 index 85fb20dab..000000000 --- a/plugins/ndf/dev.kiro/prompts/pr-tests.md +++ /dev/null @@ -1,3 +0,0 @@ -PRのTest Planを自動実行し、結果をPRコメントに反映してください。 - -pr-testsスキルの手順に従って実行してください。引数があればそのまま使用します。 diff --git a/plugins/ndf/dev.kiro/prompts/pr.md b/plugins/ndf/dev.kiro/prompts/pr.md deleted file mode 100644 index 43ca66bab..000000000 --- a/plugins/ndf/dev.kiro/prompts/pr.md +++ /dev/null @@ -1,3 +0,0 @@ -commit, push, PR作成を一括実行してください。 - -prスキルの手順に従って実行してください。引数があればそのまま使用します。 diff --git a/plugins/ndf/docs/kiro-cli.md b/plugins/ndf/docs/kiro-cli.md deleted file mode 100644 index 2981b9557..000000000 --- a/plugins/ndf/docs/kiro-cli.md +++ /dev/null @@ -1,104 +0,0 @@ -# Kiro CLI の運用と制限 - -Kiro CLI 固有の詳細です。導入の手順そのものは -[README の「Kiro CLI」](../README.md#kiro-cli)にあります。 - -## Kiro CLI の運用 - -### 主なオプション - -| オプション | 内容 | -| --- | --- | -| `--project PATH` | 現在のディレクトリではなく PATH へ導入する(`--scope workspace` のみ有効) | -| `--scope workspace\|global` | `workspace`(既定)はプロジェクトの `.kiro/`、`global` は `~/.kiro/` へ導入する | -| `--set-default` | `kiro-cli` の既定エージェントを `ndf` に切り替える(オプトイン) | -| `-y`, `--yes` | `--set-default` の確認プロンプトを省略する | -| `--with-slack` | stop フックに回答・承認待ちの Slack 通知(`scripts/wait-notify.py --runtime kiro`)を追加する | -| `--with-codex` | Codex MCP サーバ設定(`ndf.json` の `mcpServers.codex`)と、Codex CLI 直接実行用プロンプトを追加する | -| `--dry-run` | 書き込みを行わず実行内容を表示する | - -### 既定エージェントの切り替え - -Kiro の既定エージェントは組み込みの `kiro_default` です。`kiro-cli chat` を素で起動する限り、NDF のフック・外部 AI 連携・`resources` は読み込まれません。既定として使いたい場合は `--set-default` を付けます。 - -```bash -bash plugins/ndf/dev.kiro/install.sh --set-default -``` - -利用者の既存設定を無断で奪わないよう、`--set-default` は明示指定したときだけ動作します。実行前に現在の既定エージェントを表示し、対話端末では確認を取ります。元に戻す場合は次のとおりです。 - -```bash -kiro-cli agent set-default kiro_default -``` - -`--scope workspace`(既定)で導入したエージェントはそのプロジェクトでしか見つかりません。既定を `ndf` にしたまま別のディレクトリで `kiro-cli chat` を起動すると `user defined default ndf not found. Falling back to in-memory default` になります。どこでも既定として使いたい場合は `--scope global` と併用してください。 - -### 導入スコープ - -| スコープ | Skills | 常時指示 | エージェント定義 | -| --- | --- | --- | --- | -| `workspace`(既定) | `.kiro/skills/` | `.kiro/steering/ndf-policies.md` | `.kiro/agents/ndf.json` | -| `global` | `~/.kiro/skills/` | `~/.kiro/steering/ndf-policies.md` | `~/.kiro/agents/ndf.json` | - -常時適用したい指示は steering へ置きます。steering はエージェント選択に依存せず読み込まれるため、既定エージェントを書き換えない運用でも効きます。`.kiro/steering/ndf-policies.md` は `plugins/ndf/skills/ndf-policies/SKILL.md` から生成されるため、直接編集しないでください。 - -`ndf-policies` は steering の生成元としてのみ使うため、`.kiro/skills/` へは symlink しません。Kiro は `.kiro/skills/*/SKILL.md` と `.kiro/steering/**/*.md` の両方を文脈へ読み込むため、両方に置くと同じ内容が 2 回注入されます。`plugins/ndf/manifests/kiro-skills.txt` には引き続き載せます(`plugins/ndf/skills/ndf-policies/SKILL.md` が steering の生成元だからです)。 - -`--project` で別ディレクトリへ導入する場合も `--set-default` は正しく動きます。`kiro-cli` は workspace エージェントを cwd 配下の `.kiro/agents/` からのみ検出するため、installer は `kiro-cli` を導入先(`--scope workspace` なら `--project` のパス、`--scope global` なら `$HOME`)で実行します。`kiro-cli agent set-default` はエージェントが見つからなくても終了コード 0 を返すので、installer は実行後に `agent list` で反映を検証し、切り替わっていなければ失敗させます。 - -### 旧バージョンからの移行 - -v4 系の installer は `.kiro/agents/default.json` を生成していました。この設定は Kiro の既定エージェントにならず、フックも `resources` も無効のままでした。エージェント名を `ndf` に変えたため、再インストールが必要です。 - -旧 installer が張った `.kiro/skills/ndf-policies` の symlink は、リンク先が現在のプラグイン配下でなくても再インストール時に削除します(削除するのはリンク自体だけで、リンク先の実体には触れません)。`.kiro/skills/ndf-policies` が symlink ではなく実体のディレクトリやファイルだった場合は、利用者が置いたものの可能性があるため installer は削除せず警告を出します。二重注入を避けるため、内容を確認のうえ手動で退避または削除してください。別の checkout パスから導入した環境でも、steering との二重注入が再インストール 1 回で解消されます。 - -```bash -# 1. 再インストール(旧 default.json は自動でバックアップ・移行されます) -bash plugins/ndf/dev.kiro/install.sh --with-slack - -# 2. 必要なら既定エージェントを切り替える -bash plugins/ndf/dev.kiro/install.sh --set-default - -# 3. 移行を確認したらバックアップを削除する -rm .kiro/agents/default.json.bak -``` - -`.kiro/agents/default.json` は再インストール時に必ず `.kiro/agents/default.json.bak` へバックアップされます。そのうえで installer は次のように振る舞います。 - -| 旧 `default.json` | `ndf.json` | 振る舞い | -| --- | --- | --- | -| 旧版 NDF installer の生成物(`name` が `default`、`description` が旧テンプレートと完全一致、かつ旧 `resources` の `skill://.kiro/skills/**/SKILL.md` または `agentSpawn` フックの `CLAUDE.ndf.md` チェックを持つ) | なし | `ndf.json` へ自動移行する(`default.json` は残らない)。独自に追記した `mcpServers` / フック / 独自キーは下表のマージで保持される | -| 同上 | あり | 自動移行しない(`ndf.json` の設定を失わないため)。`default.json` は残るので、必要な設定を写したうえで削除する | -| NDF 以外が管理している(利用者が作成したものなど) | 問わない | 自動移行しない。勝手に移行すると利用者の設定を壊すため、バックアップと移行手順の案内のみを行う | - -`--dry-run` では上記の移行を含め一切の書き込みを行いません(旧設定を検出したことだけ表示します)。 - -Kiro 用 MCP プラグインの installer(`plugins/mcp/<プラグイン名>/dev.kiro/install.sh`)は `.kiro/agents/default.json` を更新します。自動移行後に MCP installer を実行すると `default.json` が再び作られるため、`mcpServers` を `.kiro/agents/ndf.json` へ写してください。写し替えは一度だけで済みます。`install.sh` を再実行しても、写した `mcpServers` は保持されます。 - -### 再インストール時に保持される設定 - -`install.sh` は `.kiro/agents/ndf.json` を毎回テンプレートから再生成しますが、上書きするのは installer が管理するキーだけです。既存ファイルにある利用者管理の設定は読み取ってマージし直します。 - -| 区分 | キー | 再実行時の扱い | -| --- | --- | --- | -| installer 管理 | `name` / `description` / `tools` / `resources` / `hooks.agentSpawn` | テンプレートから再生成する(上書き) | -| installer 管理 | `hooks.stop` | `--with-slack` の有無で生成・削除する | -| installer 管理 | `mcpServers.codex` | `--with-codex` の有無で生成・削除する | -| 利用者管理 | 上記以外の `mcpServers` エントリ、`hooks` の項目、トップレベルキー | そのまま引き継ぐ | - -引き継いだ項目は実行ログに `利用者管理の設定を引き継ぎました: mcpServers.bigquery` のように表示します。再生成の前に `.kiro/agents/ndf.json.bak` へバックアップも取るため、意図しない結果になった場合は差し戻せます。 - -`mcpServers.codex` だけは installer 管理です。`--with-codex` を付けずに再実行すると削除されるため、Codex MCP を使う場合は `--with-codex` を付けたまま運用してください。 - -## Kiro CLI の制限 - -### `allowed-tools` は事前承認にならない - -Skill frontmatter の `allowed-tools` は、プロジェクト配置(`.kiro/skills/`)では事前承認として機能しません([kirodotdev/Kiro#6055](https://github.com/kirodotdev/Kiro/issues/6055))。`allowed-tools: execute_bash` を持つ Skill でも `Command execute_bash is rejected because it matches one or more rules on the denied list` になります。 - -Kiro では、Skill の実行時にツール利用の確認が入る前提で操作してください。NDF の Skill 本文は「無確認で実行される」前提を持ちません。 - -### プラグイン機構がない - -Kiro CLI には Skill・フック・外部連携・常時指示をまとめて配布する仕組みがありません([kirodotdev/Kiro#8578](https://github.com/kirodotdev/Kiro/issues/8578))。Kiro IDE の Powers は CLI では使えないため、`install.sh` による導入を継続します。 - diff --git a/plugins/ndf/manifests/agy-skills.txt b/plugins/ndf/manifests/agy-skills.txt deleted file mode 100644 index 4b6339464..000000000 --- a/plugins/ndf/manifests/agy-skills.txt +++ /dev/null @@ -1,45 +0,0 @@ -cherry-pick-pr -cross-refactoring -cross-review -deploy -development-workflow -docker-container-access -chrome-cdp -external-ai -fix -implementation-plan -layout-review -investigation-rules -decision-request -issue-plan-strategy -logging-guidelines -document-style -notion-writing -merged -ndf-policies -out-of-scope -backlog-refinement -estimate -progress-tracking -plan-to-spec -pr -pr-review -pr-tests -problem-solving -qa-security-scan -quality-gates -refactoring -release-verification -requirements-design -design -document-sources -document-drafting -document-rebuild -document-systems -retrospective -tdd-cycle -worktree -release -google-workspace -ml-model-structure -skill-stats diff --git a/plugins/ndf/manifests/kiro-skills.txt b/plugins/ndf/manifests/kiro-skills.txt deleted file mode 100644 index 483b9a15f..000000000 --- a/plugins/ndf/manifests/kiro-skills.txt +++ /dev/null @@ -1,46 +0,0 @@ -pr -pr-tests -fix -pr-review -cross-review -cross-refactoring -merged -ndf-policies -document-style -notion-writing -docker-container-access -chrome-cdp -implementation-plan -layout-review -investigation-rules -decision-request -problem-solving -logging-guidelines -cherry-pick-pr -deploy -external-ai -statusline -issue-plan-strategy -plan-to-spec -qa-security-scan -development-workflow -requirements-design -design -document-sources -document-drafting -document-rebuild -document-systems -tdd-cycle -refactoring -quality-gates -worktree -out-of-scope -backlog-refinement -estimate -progress-tracking -release-verification -retrospective -release -google-workspace -ml-model-structure -skill-stats diff --git a/plugins/ndf/scripts/acceptance-check.py b/plugins/ndf/scripts/acceptance-check.py new file mode 100755 index 000000000..ba2802796 --- /dev/null +++ b/plugins/ndf/scripts/acceptance-check.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""受け入れ条件の確認の区分を課題の本文へ記録する・読む(#2144。完了判定 `quality-gates` が呼ぶ)。 + + python3 acceptance-check.py record --issue <番号> [--repo <所有者>/<リポジトリ>] + [--pre-merge '<条件の番号>=<手段>']... [--post-release <条件の番号>]... + python3 acceptance-check.py show --issue <番号> [--repo <所有者>/<リポジトリ>] + +記録の置き場は課題の本文の `## 受け入れ条件の確認` の節の表(`| 条件 | 区分 | 手段 |`)である。区分は +`マージ前`(マージ前に確かめた。手段にテスト名か実行したコマンドを書く)か `リリース後`(リリースの後でしか +確かめられない)のどちらか。record は今の表へ渡した番号の行を足すか置き換え、課題ごとの錠の中で本文を読んでから +書く。番号は本文の `## 受け入れ条件` にある番号だけを受ける(規則は lib/acceptance.py)。 + +この記録を読むのは、承認資料の「未実施のリリース後テスト」(release_lib/materials.read_unverified。マージ前の +条件を載せない)と、sprint-close.py --with-verification の閉じる条件 2(マージ前の条件に合格の行を求めない)である。 + +結果は lib/step_result.py の形の 1 行の JSON(`tool` は `acceptance-check`)。items は条件ごとに +{kind:"criterion", name:<番号>, result:"マージ前"|"リリース後", means?}。終了コード: 0 = 済んだ(変更なしを含む)/ +1 = 本文を書けない / 2 = 本文を読めない・引数の誤り(受け入れ条件に無い番号・手段の無いマージ前・同じ番号を 2 度)/ +3 = リポジトリを決められない・本文に受け入れ条件が無い。 +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent / "lib")) +import deps # noqa: E402 + +deps.require("md", "mdtable", "locks", "durable", "waits") +import acceptance # noqa: E402 +import gh_call # noqa: E402 +import gh_parts # noqa: E402 +import gh_rest # noqa: E402 +import gh_sections # noqa: E402 +import mdtable # noqa: E402 +from step_result import EXIT_PRECONDITION, EXIT_UNREADABLE, EXIT_VIOLATION, StepError, emit, main_with, result # noqa: E402 + +TOOL = "acceptance-check" + + +def parse_pre_merge(values) -> dict[int, str]: + """`<番号>=<手段>` の並びを {番号: 手段} にする。形が違えば StepError(2)。""" + out: dict[int, str] = {} + for v in values or (): + num, sep, means = v.partition("=") + if not sep or not num.strip().isdigit() or not means.strip(): + raise StepError(f"--pre-merge は '<条件の番号>=<手段>' で渡す: {v!r}", EXIT_UNREADABLE) + out[int(num)] = means.strip() + return out + + +def merged_checks(body: str, pre_merge: dict[int, str], post_release) -> dict[int, acceptance.Check]: + """本文の今の記録へ渡した区分を重ねた記録。受け入れ条件に無い番号・両方に出る番号は StepError。""" + numbers = {c.number for c in acceptance.criteria(body)} + if not numbers: + raise StepError("本文に受け入れ条件(`## 受け入れ条件` のチェックボックス)が無い", EXIT_PRECONDITION) + post = set(post_release or ()) + both = sorted(post & set(pre_merge)) + if both: + raise StepError(f"マージ前とリリース後の両方に渡した番号: {both}", EXIT_UNREADABLE) + unknown = sorted((post | set(pre_merge)) - numbers) + if unknown: + raise StepError(f"受け入れ条件に無い番号: {unknown}(本文の条件は 1〜{max(numbers)})", EXIT_UNREADABLE) + checks = dict(acceptance.read_check_kinds(body)) + checks.update({n: acceptance.Check(acceptance.PRE_MERGE, m) for n, m in pre_merge.items()}) + checks.update({n: acceptance.Check(acceptance.POST_RELEASE, "") for n in post}) + return checks + + +def checks_table(checks: dict[int, acceptance.Check]) -> str: + """`## 受け入れ条件の確認` の節の中身(番号の順の表)。""" + rows = [(str(n), c.kind, c.means if c.kind == acceptance.PRE_MERGE else acceptance.NO_MEANS) for n, c in sorted(checks.items())] + return mdtable.table_markdown(acceptance.CHECKS_HEAD, rows, align=("left", "left", "left")) + + +def recorded_body(body: str, pre_merge: dict[int, str], post_release) -> str: + """記録を書いた本文。節の外は変えない。""" + table = checks_table(merged_checks(body, pre_merge, post_release)) + return gh_sections.replace_section(body, acceptance.CHECKS_HEADING, table) + + +def _criterion_items(checks: dict[int, acceptance.Check]) -> list[dict]: + return [ + {"kind": "criterion", "name": str(n), "result": c.kind, **({"means": c.means} if c.means else {})} + for n, c in sorted(checks.items()) + ] + + +def _repo_slug(a) -> str: + slug = gh_call.resolve_repo(a.repo) + if not slug: + raise StepError("リポジトリを決められない(--repo を渡す)", EXIT_PRECONDITION) + return slug + + +def _fetch_issue_body(slug: str, n: int) -> str: + body = gh_rest.fetch_body(slug, n) + if body is None: + raise StepError(f"{slug}#{n} の本文を読めない", EXIT_UNREADABLE) + return body + + +def cmd_record(a): + pre = parse_pre_merge(a.pre_merge) + if not pre and not a.post_release: + raise StepError("--pre-merge か --post-release を 1 つ以上渡す", EXIT_UNREADABLE) + slug = _repo_slug(a) + with gh_parts.issue_lock(slug, a.issue): # 読みから書きまでを課題ごとの錠の中で行う + body = _fetch_issue_body(slug, a.issue) + new = recorded_body(body, pre, a.post_release) + if new != body and not gh_rest.update_body(slug, a.issue, new): + emit(result(TOOL, "stopped", f"{slug}#{a.issue} の本文を書けない"), EXIT_VIOLATION) + checks = acceptance.read_check_kinds(new) + pre_n = sum(c.kind == acceptance.PRE_MERGE for c in checks.values()) + state = "変更なし" if new == body else "記録した" + emit( + result(TOOL, "ok", f"#{a.issue}: {state}(マージ前 {pre_n} 件・リリース後 {len(checks) - pre_n} 件)", _criterion_items(checks)), 0 + ) + + +def cmd_show(a): + slug = _repo_slug(a) + checks = acceptance.read_check_kinds(_fetch_issue_body(slug, a.issue)) + emit(result(TOOL, "ok", f"#{a.issue}: 記録 {len(checks)} 件", _criterion_items(checks)), 0) + + +def build_parser(): + ap = argparse.ArgumentParser(prog="acceptance-check.py", description=__doc__.splitlines()[0]) + sub = ap.add_subparsers(dest="cmd", required=True) + p = sub.add_parser("record", help="条件ごとの区分(マージ前 / リリース後)と手段を本文へ書く") + p.add_argument("--issue", type=int, required=True) + p.add_argument("--repo") + p.add_argument("--pre-merge", action="append", default=[], help="'<条件の番号>=<手段(テスト名・実行したコマンド)>'") + p.add_argument("--post-release", action="append", type=int, default=[], help="リリースの後でしか確かめられない条件の番号") + p.set_defaults(func=cmd_record) + p = sub.add_parser("show", help="本文の記録を読む") + p.add_argument("--issue", type=int, required=True) + p.add_argument("--repo") + p.set_defaults(func=cmd_show) + return ap + + +def main(argv=None): + return main_with(build_parser(), lambda a: TOOL, argv) + + +if __name__ == "__main__": + main() diff --git a/plugins/ndf/scripts/hook_lib/worktree.py b/plugins/ndf/scripts/hook_lib/worktree.py index ec4a2ad9a..76ee5279e 100644 --- a/plugins/ndf/scripts/hook_lib/worktree.py +++ b/plugins/ndf/scripts/hook_lib/worktree.py @@ -1,6 +1,7 @@ """メインディレクトリを編集しようとしたときに、worktree で作業する旨を伝える guard(#1142 の決定 20)。 -結線先は 3 つある(`hooks/*.json`・`dev.kiro/install.sh`・`dev.agy/hooks.json`)。 +結線先は `hooks/*.json`(Claude Code・Codex)。Kiro CLI と agy の分岐は、配布を終える前(#2231)に導入した手元の hook が +この入口を呼び続けるために残す。 - PreToolUse(Claude Code / Codex CLI): 編集先のパスを見た案内を `additionalContext` でモデルへ渡す - プロンプト送信時(Kiro CLI): パスを見ない案内を標準出力へ書く - PreToolUse(agy): 案内をセッションの状態ファイルの `pending` へ積み、次のモデル呼び出しの前に diff --git a/plugins/ndf/scripts/lib/acceptance.py b/plugins/ndf/scripts/lib/acceptance.py index a262a8d6d..7ce52bf54 100644 --- a/plugins/ndf/scripts/lib/acceptance.py +++ b/plugins/ndf/scripts/lib/acceptance.py @@ -80,6 +80,56 @@ def criteria_lines(body: str) -> list[tuple[Criterion, str]]: return out +# --- 受け入れ条件の確認の区分(#2144。完了判定が書き、承認資料と閉じる判定が読む) + + +CHECKS_HEADING = "## 受け入れ条件の確認" +PRE_MERGE = "マージ前" # マージ前に確かめた。手段(テスト名・実行したコマンド)を必ず持つ +POST_RELEASE = "リリース後" # リリースの後でしか確かめられない +KINDS = (PRE_MERGE, POST_RELEASE) +CHECKS_HEAD = ("条件", "区分", "手段") +NO_MEANS = "—" + + +class Check(NamedTuple): + """1 つの受け入れ条件の確認の区分。means はマージ前に確かめた手段(リリース後なら空)。""" + + kind: str + means: str + + +def read_check_kinds(body: str) -> dict[int, Check]: + """本文の `## 受け入れ条件の確認` の表を {条件の番号: Check} で返す。節・表が無ければ空(今の課題)。 + + `条件` の列の頭の数字を番号として読む。区分が `マージ前` / `リリース後` のどちらでもない行と、手段の無い + `マージ前` の行は読まない(確かめた手段を書けない条件は確認済みと言えない)。使う側は `deps.require("md")` を先に呼ぶ。""" + import md + + section = gh_sections.get_section(body or "", CHECKS_HEADING) + found = md.tables(section) if section else [] + if not found: + return {} + head = [c.strip() for c in found[0].header] + if any(h not in head for h in CHECKS_HEAD): + return {} + ni, ki, mi = (head.index(h) for h in CHECKS_HEAD) + out: dict[int, Check] = {} + for row in found[0].rows: + cs = [c.strip() for c in row] + num, kind, means = re.match(r"^(\d+)", cs[ni]), cs[ki], cs[mi].strip("`").strip() + if not num or kind not in KINDS: + continue + if kind == PRE_MERGE and (not means or means == NO_MEANS): + continue + out[int(num.group(1))] = Check(kind, "" if kind == POST_RELEASE else cs[mi]) + return out + + +def pre_merge_numbers(body: str) -> frozenset: + """マージ前に確かめたと記録された受け入れ条件の番号。""" + return frozenset(n for n, c in read_check_kinds(body).items() if c.kind == PRE_MERGE) + + # --- リリース後テストの表(sprint-close.py の閉じる判定と release-steps.py の承認の材料が同じ規則で読む。#1885 の決定 8) @@ -105,44 +155,58 @@ def verification_verdicts(block, record_repo): found = md.tables(block) if not found: return None - head = [c.strip() for c in found[0].header] - if "課題" not in head or "結果" not in head: + cols = _column_indices(found[0].header) + if cols is None: return None - ci, ri = head.index("課題"), head.index("結果") - ai = head.index("受け入れ条件") if "受け入れ条件" in head else None acc = {} for row in found[0].rows: - cs = [c.strip() for c in row] # md は行のセルを見出しの数へ揃える - m = _ISSUE_CELL.match(cs[ci]) - if not m: + parsed = _parse_verdict_row(row, cols, record_repo) + if parsed is None: continue - key = (m.group(1) or record_repo, int(m.group(2))) - ok = cs[ri].startswith("合格") + key, ok, num = parsed all_pass, passed, failed = acc.get(key, (True, set(), set())) - num = _CRITERION_CELL.match(cs[ai]) if ai is not None else None - if num: - (passed if ok else failed).add(int(num.group(1))) + if num is not None: + (passed if ok else failed).add(num) acc[key] = (all_pass and ok, passed, failed) if not acc: return None return {k: IssueRows(a, frozenset(p - f)) for k, (a, p, f) in acc.items()} -_VERSION = re.compile(r"v?(\d+\.\d+\.\d+)") +def verdict_table_readable(block): + """表があり、見出しに課題と結果の列があるか。verification_verdicts の None が「課題を指す行が無い」だけかを分ける。""" + import md + + found = md.tables(block) + return bool(found) and _column_indices(found[0].header) is not None + + +def _column_indices(header): + """見出しから (課題, 結果, 受け入れ条件) の列の位置を返す。受け入れ条件の列が無ければ 3 つ目は None。 + 課題か結果の列が無ければ None。""" + head = [c.strip() for c in header] + if "課題" not in head or "結果" not in head: + return None + ai = head.index("受け入れ条件") if "受け入れ条件" in head else None + return head.index("課題"), head.index("結果"), ai + + +def _parse_verdict_row(row, cols, record_repo): + """1 行を (key, 合格か, 受け入れ条件の番号または None) に解く。課題を指さない行は None。""" + ci, ri, ai = cols + cs = [c.strip() for c in row] # md は行のセルを見出しの数へ揃える + m = _ISSUE_CELL.match(cs[ci]) + if not m: + return None + key = (m.group(1) or record_repo, int(m.group(2))) + num = _CRITERION_CELL.match(cs[ai]) if ai is not None else None + return key, cs[ri].startswith("合格"), int(num.group(1)) if num else None def version_verdicts(text, version, record_repo): """PR の本文とコメント(text)の `## リリース後テスト` のブロックのうち、`対象の版:` の基底が version の基底と 一致する最後のブロックの表を読む({(repo, number): IssueRows})。無い・読めなければ空。""" import dist_record - import md - want = _VERSION.match(str(version)) - lines = text.splitlines() - sections = [s for s in md.md_sections(text) if lines[s.heading.line].startswith("#")] - blocks = [ - b - for b in dist_record._verify_blocks(lines, sections) - if want and (m := _VERSION.match(b["ver"] or "")) and m.group(1) == want.group(1) - ] - return (verification_verdicts("\n".join(blocks[-1]["lines"]), record_repo) or {}) if blocks else {} + block = dist_record.verify_block_for_base(text, version) + return (verification_verdicts(block, record_repo) or {}) if block is not None else {} diff --git a/plugins/ndf/scripts/lib/closing-issues.sh b/plugins/ndf/scripts/lib/closing-issues.sh index a675da40f..d00974c79 100755 --- a/plugins/ndf/scripts/lib/closing-issues.sh +++ b/plugins/ndf/scripts/lib/closing-issues.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# NDF plugin: Pull Request の本文から、閉じる語が指す issue を取り出す。 +# NDF plugin: Pull Request の本文から、閉じる語と閉じる予定の参照が指す issue を取り出す。 # # closing-issues.sh [--repo <所有者>/<リポジトリ>] [本文のファイル] # 引数が無ければ標準入力から読む。`<所有者>/<リポジトリ>` と `<番号>` をタブで @@ -16,6 +16,9 @@ # # 起点ブランチが既定ブランチでないリポジトリでは、マージしても自動クローズが働かない。 # 後片付けの工程がこの一覧を読み、まだ OPEN のものだけを閉じる。 +# +# **閉じる予定の参照(`最終工程の後に閉じる: #12`)も同じ指し先の規則で取り出す**(#2228)。 +# 既定ブランチ宛の本文では NDF の手順が閉じる語をこの形へ置き換える。形の正本は lib/closing.py の DEFERRED。 set -uo pipefail DEFAULT_REPO="" @@ -46,7 +49,7 @@ body=$(cat -- "${1:--}") # **Pull Request の URL は拾わない。** 閉じる対象は issue である。 NAME='[A-Za-z0-9._-]+' REF="(https?://github\.com/$NAME/$NAME/issues/[0-9]+|$NAME/$NAME#[0-9]+|#[0-9]+)" -KEYWORD='\b(close[sd]?|fix(e[sd])?|resolve[sd]?)[[:space:]]*:?[[:space:]]*' +KEYWORD='(\b(close[sd]?|fix(e[sd])?|resolve[sd]?)[[:space:]]*:?[[:space:]]*|最終工程の後に閉じる[[:space:]]*:[[:space:]]*)' # **1 件も見つからないことは正常な結果である。** `grep` の終了コードをそのまま返すと、 # 閉じる語の無い Pull Request で失敗したように見える。 diff --git a/plugins/ndf/scripts/lib/closing.py b/plugins/ndf/scripts/lib/closing.py index 2aa5e1ace..5903502b1 100644 --- a/plugins/ndf/scripts/lib/closing.py +++ b/plugins/ndf/scripts/lib/closing.py @@ -1,16 +1,34 @@ -"""本文の課題を閉じる closing keywords(GitHub が課題を自動で閉じるキーワード `Closes #n` など)と、課題の参照の読み取り。 -`pr-steps.py`(closing keywords の検査)と `release-steps.py`(版に含む PR から課題へたどる。宛先が既定ブランチでない PR は -GitHub の `closingIssuesReferences` が空になる)、`sprint-close.py`(参照だけの課題)が使う。""" +"""本文の課題を閉じる closing keywords(GitHub が課題を自動で閉じるキーワード `Closes #n` など)と、閉じる予定の参照 +(`最終工程の後に閉じる: #n`。GitHub は閉じず、`sprint-close.py` がスプリント課題として集める)と、課題の参照の読み取り。 +`pr-steps.py`(closing keywords の検査と、既定ブランチ宛の本文の置き換え)と `supervise_lib/pr.py`(同じ置き換え)、 +`release-steps.py`(版に含む PR から課題へたどる。宛先が既定ブランチでない PR は GitHub の `closingIssuesReferences` が +空になる)、`sprint-close.py`(参照だけの課題)が使う。閉じる指定の形はこのモジュールだけが持ち、 +`closing-issues.sh` は同じ形を読む(#2228)。""" from __future__ import annotations import re -CLOSING = re.compile( - r"\b(close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?\s*" - r"(https?://github\.com/[\w.-]+/[\w.-]+/issues/\d+|[\w.-]+/[\w.-]+#\d+|#\d+)", - re.I, -) +_TARGET = r"(https?://github\.com/[\w.-]+/[\w.-]+/issues/\d+|[\w.-]+/[\w.-]+#\d+|#\d+)" +_KEYWORD = r"\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?\s*" +DEFERRED = "最終工程の後に閉じる" # 閉じる予定の参照の語。GitHub の closing keywords を含まない(#2228 の I9) +_DEFERRED_LEAD = DEFERRED + r"\s*:\s*" + +CLOSING = re.compile(_KEYWORD + _TARGET, re.I) +_ANY_CLOSING = re.compile(r"(?:" + _KEYWORD + r"|" + _DEFERRED_LEAD + r")" + _TARGET, re.I) + + +def defer_closing(text): + """closing keywords を閉じる予定の参照へ置き換える。(置き換えた本文, 置き換えた指し先の字面の並び) を返す。 + + 指し先の字面(`#12`・`owner/repo#12`・課題の URL)は変えない。宛先が既定ブランチの Pull Request の本文だけに使う。""" + targets: list[str] = [] + + def repl(m): + targets.append(m.group(1)) + return f"{DEFERRED}: {m.group(1)}" + + return CLOSING.sub(repl, text or ""), targets def closing_words(text): @@ -19,12 +37,12 @@ def closing_words(text): def closing_issues(text): - """closing keywords が指す課題の字面(`#12`・`owner/repo#12`・課題の URL)。""" - return [m.group(2) for m in CLOSING.finditer(text or "")] + """closing keywords と閉じる予定の参照が指す課題の字面(`#12`・`owner/repo#12`・課題の URL)を現れた順で返す。""" + return [m.group(1) for m in _ANY_CLOSING.finditer(text or "")] def closing_numbers(text) -> list[int]: - """closing keywords が指す課題の番号を、重ねずに現れた順で返す。""" + """closing keywords と閉じる予定の参照が指す課題の番号を、重ねずに現れた順で返す。""" out: list[int] = [] for ref in closing_issues(text): n = int(re.search(r"(\d+)$", ref).group(1)) @@ -45,7 +63,7 @@ def issue_ref(ref, repo): def closing_refs(text, repo) -> list[tuple[str, int]]: - """closing keywords が指す課題を (owner/name, 番号) で、重ねずに現れた順で返す。`#12` は `repo` の課題。""" + """closing keywords と閉じる予定の参照が指す課題を (owner/name, 番号) で、重ねずに現れた順で返す。`#12` は `repo` の課題。""" out: list[tuple[str, int]] = [] for ref in closing_issues(text): got = issue_ref(ref, repo) diff --git a/plugins/ndf/scripts/lib/dist_record.py b/plugins/ndf/scripts/lib/dist_record.py index 070851680..fd1777f67 100644 --- a/plugins/ndf/scripts/lib/dist_record.py +++ b/plugins/ndf/scripts/lib/dist_record.py @@ -59,6 +59,11 @@ def _read_dist_block(block): return out +def _heading_sections(text, lines): + """本文の節のうち、見出しの行が `#` で始まるもの(ブロックの切り出しと配布の記録の読み取りが共有する)。""" + return [s for s in md.md_sections(text) if lines[s.heading.line].startswith("#")] + + def _verify_blocks(lines, sections): """リリース後テストのブロックを切り出す。各ブロックは {"start": 開始行, "ver": 対象の版, "lines": 行の並び}。""" blocks = [] @@ -80,6 +85,20 @@ def _verify_blocks(lines, sections): _EXPLICIT_VERSION = re.compile(r"v?\d+\.\d+\.\d+") +_BASE_VERSION = re.compile(r"v?(\d+\.\d+\.\d+)") + + +def verify_block_for_base(text, version): + """本文(text)のリリース後テストのブロックのうち、`対象の版:` の基底が version の基底と一致する最後のブロックの + 本文。無ければ None。parse_record の完全な版の一致とは別の規則で、受け入れ条件の版ごとの判定が使う。""" + want = _BASE_VERSION.match(str(version)) + lines = text.splitlines() + blocks = [ + b + for b in _verify_blocks(lines, _heading_sections(text, lines)) + if want and (m := _BASE_VERSION.match(b["ver"] or "")) and m.group(1) == want.group(1) + ] + return "\n".join(blocks[-1]["lines"]) if blocks else None def _is_explicit_version(ver): @@ -103,22 +122,41 @@ def _pick_verify_block(blocks, stage, version, dist_start): return "\n".join(after[-1]["lines"]) if after else None -def parse_record(text): +def _dist_comment_id(parts, dist_line): + """最後の配布の記録の見出しの行を含む部分のコメントの識別子(振り返りの行を比べる基準。#2228 の決定 10)。 + + parts は (先頭の行の番号, コメントの識別子) の並び(本文は識別子が None)。記録が本文にあれば、記録の PR の + 最初のコメントの識別子を使い、コメントが 1 件も無ければ None。parts が無ければ None。""" + if not parts: + return None + hit = None + for start, cid in parts: + if start <= dist_line: + hit = cid + if hit is not None: + return hit + return next((cid for _, cid in parts if cid is not None), None) + + +def parse_record(text, parts=None): """最後の配布の記録と、使うリリース後テストのブロックを読む。 - 戻り値: {"found", "stage", "version", "sprint_prs", "verify_block"}。 + parts は text を組んだ部分ごとの (先頭の行の番号, コメントの識別子) の並び(sprint-close.py の read_record が返す)。 + 戻り値: {"found", "stage", "version", "sprint_prs", "verify_block", "dist_comment_id"}。 verify_block は本番なら対象の版が一致する最後のブロック(無ければ最後の配布の記録より後で、対象の版が版数でない 最後のブロック)、 配布なしなら最後の配布の記録より後の最後のブロック。無ければ None。 + dist_comment_id は最後の配布の記録のコメントの識別子(_dist_comment_id。記録が無ければ None)。 """ lines = text.splitlines() - sections = [s for s in md.md_sections(text) if lines[s.heading.line].startswith("#")] + sections = _heading_sections(text, lines) dists = [s for s in sections if lines[s.heading.line].startswith(DIST)] dist_start = dists[-1].heading.line if dists else None - out = {"found": dist_start is not None, "stage": None, "version": None, "sprint_prs": [], "verify_block": None} + out = {"found": dist_start is not None, "stage": None, "version": None, "sprint_prs": [], "verify_block": None, "dist_comment_id": None} if dist_start is None: return out out.update(_read_dist_block(lines[dists[-1].start : dists[-1].end])) blocks = _verify_blocks(lines, sections) out["verify_block"] = _pick_verify_block(blocks, out["stage"] or "", out["version"], dist_start) + out["dist_comment_id"] = _dist_comment_id(parts, dist_start) return out diff --git a/plugins/ndf/scripts/lib/repo.py b/plugins/ndf/scripts/lib/repo.py index a60a2de9e..fd3c30115 100644 --- a/plugins/ndf/scripts/lib/repo.py +++ b/plugins/ndf/scripts/lib/repo.py @@ -12,6 +12,7 @@ from pathlib import Path import proc +from closing import defer_closing ORIGIN_URL = re.compile(r"[:/](?P[^/:]+)/(?P[^/]+?)(?:\.git)?/?$") WORKTREE_DECL = Path(".ndf") / "worktree.json" @@ -85,6 +86,21 @@ def default_branch(root) -> str | None: return None +def merge_closes_issues(root, base) -> bool: + """宛先 `base` の Pull Request をマージすると、GitHub が本文の closing keywords で課題を閉じるか。 + + GitHub が閉じるのは既定ブランチへのマージだけである。既定ブランチを決められない・宛先が分からないときも真を返す + (置き換えても閉じる予定の参照として NDF の手順で閉じられ、失うものが無い。#2228 の決定 9)。""" + d = default_branch(root) + return d is None or not base or str(base).removeprefix("origin/") == d + + +def defer_for_base(root, base, text): + """宛先 base の Pull Request の本文を、マージで GitHub が課題を閉じるとき(merge_closes_issues)だけ + closing.defer_closing に通す。(本文, 置き換えた指し先の字面の並び) を返す。`pr-steps.py` と `supervise_lib/pr.py` が使う。""" + return defer_closing(text) if merge_closes_issues(root, base) else (text or "", []) + + def base_branch(root) -> str | None: """開発の起点。`.ndf/worktree.json` の `base_branch` → 既定ブランチ(`default_branch`)の順。決まらなければ `None`。""" return declared_base(root) or default_branch(root) diff --git a/plugins/ndf/scripts/pr-steps.py b/plugins/ndf/scripts/pr-steps.py index 02872eea2..364e67edb 100644 --- a/plugins/ndf/scripts/pr-steps.py +++ b/plugins/ndf/scripts/pr-steps.py @@ -17,7 +17,6 @@ from __future__ import annotations import argparse -import json import re import sys import tempfile @@ -40,13 +39,16 @@ result, run, ) -import gh_parts # noqa: E402 from closing import closing_issues, closing_words # noqa: E402 import repo # noqa: E402 +from repo import defer_for_base # noqa: E402 import tool_paths # noqa: E402 from design_diff import DESIGN_DIFF_HEADING # noqa: E402 from pr_mode import needs_review, pr_target, split_stages, with_mode_line # noqa: E402 +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from pr_steps_lib.gh_pr import existing_pr, pr_for_report, send_body, target_pr # noqa: E402 PR の読み書き + TOOL = "pr" SCRIPTS = Path(__file__).resolve().parent REVIEW_MARK = "" @@ -96,51 +98,6 @@ def compare_ref(root, base): raise StepError(f"起点 {base} が無い(git fetch origin {base} を先に打つ)", EXIT_PRECONDITION) -def repo_owner_name(root): - p = gh_parts.gh(["repo", "view", "--json", "owner,name"], cwd=root) - if p.returncode == 0: - try: - d = json.loads(p.stdout) - return d["owner"]["login"], d["name"] - except (ValueError, KeyError, TypeError): - pass - slug = repo.owner_repo(root) - if not slug: - raise StepError("リポジトリの所有者と名前を決められない", EXIT_UNREADABLE) - owner, _, name = slug.partition("/") - return owner, name - - -def existing_pr(root, branch): - """head が branch の OPEN の PR({number, url, isDraft, baseRefName})。無ければ None。上限なら REST。""" - p = gh_parts.gh(["pr", "list", "--head", branch, "--state", "open", "--json", "number,url,isDraft,baseRefName"], cwd=root) - if p.returncode == 0: - try: - prs = json.loads(p.stdout or "[]") - except ValueError: - raise StepError("gh pr list の出力を読めない", EXIT_UNREADABLE) - return prs[0] if prs else None - if not gh_parts.is_rate_limited(p.stderr): - raise StepError(f"gh pr list が失敗: {p.stderr.strip()[:300]}", EXIT_PRECONDITION) - owner, name = repo_owner_name(root) - q = gh_parts.gh(["api", f"repos/{owner}/{name}/pulls?head={owner}:{branch}&state=open"], cwd=root) - if q.returncode != 0: - raise StepError(f"REST でも既存の PR を引けない: {q.stderr.strip()[:300]}") - try: - prs = json.loads(q.stdout or "[]") - except ValueError: - raise StepError("REST の出力を読めない", EXIT_UNREADABLE) - if not prs: - return None - pr = prs[0] - return { - "number": pr["number"], - "url": pr["html_url"], - "isDraft": pr.get("draft", False), - "baseRefName": pr.get("base", {}).get("ref", ""), - } - - def diff_numbers(root, ref): # 3 点(分岐点からの差分)。2 点だと起点が進んでいたとき他 PR の変更まで数える last = (git(root, "diff", "--shortstat", f"{ref}...HEAD").stdout.strip().splitlines() or [""])[-1] @@ -333,64 +290,17 @@ def decisions_sync(root, number): return run(["bash", str(script), "sync", str(number)], cwd=root, check=False).returncode -def rest_create(root, branch, base, title, body, draft): - owner, name = repo_owner_name(root) - payload = {"title": title, "head": branch, "base": base, "body": body, "draft": bool(draft)} - with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as f: - json.dump(payload, f, ensure_ascii=False) - tmp = f.name - try: - p = gh_parts.gh(["api", f"repos/{owner}/{name}/pulls", "--input", tmp], cwd=root) - finally: - Path(tmp).unlink(missing_ok=True) # 本文を /tmp に残さない - if p.returncode != 0: - raise StepError(f"REST でも作成が失敗: {p.stderr.strip()[:300]}") - d = json.loads(p.stdout) - return d["number"], d["html_url"] - - -def send_body(root, branch, body, body_path, a, must_exist): - """gh へ本文ファイルを渡して PR を作る・更新する。(番号, URL, action) を返す。""" - if getattr(a, "pr", None): - pr = {"number": a.pr, "url": f"#{a.pr}"} - else: - pr = existing_pr(root, branch) - if pr: - args = ["pr", "edit", str(pr["number"]), "--body-file", body_path] - if a.title: - args += ["--title", a.title] - p = gh_parts.gh(args, cwd=root) - if p.returncode != 0: - raise StepError(f"gh pr edit {pr['number']} が失敗: {p.stderr.strip()[:300]}") - return pr["number"], pr["url"], "updated" - if must_exist: - raise StepError(f"{branch} の OPEN の PR が無い(create を使う)", EXIT_PRECONDITION) - if not a.title: - raise StepError("--title が要る", EXIT_UNREADABLE) - base = a.base or repo.base_branch(root) or "main" - args = ["pr", "create", "--base", base, "--title", a.title, "--body-file", body_path] - if a.draft: - args.append("--draft") - p = gh_parts.gh(args, cwd=root) - if p.returncode == 0: - url = (p.stdout.strip().splitlines() or [""])[-1] - m = re.search(r"/pull/(\d+)", url) - return (int(m.group(1)) if m else None), url, "created" - if gh_parts.is_rate_limited(p.stderr): - number, url = rest_create(root, branch, base, a.title, body, a.draft) - return number, url, "created_rest" - raise StepError(f"gh pr create が失敗: {p.stderr.strip()[:300]}") - - def upsert(a, must_exist): root = git_root(a.root) branch = current_branch(root) body = with_mode_line(body_with_mark(a.body_file), getattr(a, "mode", None), split_stages(getattr(a, "stages", None))) + pr, base = target_pr(root, branch, a, must_exist) + body, deferred = defer_for_base(root, base, body) # 既定ブランチ宛はマージで閉じないよう置き換える(#2228 の I7) with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8") as f: f.write(body) body_path = f.name try: - number, url, action = send_body(root, branch, body, body_path, a, must_exist) + number, url, action = send_body(root, branch, body, body_path, a, pr, base) finally: Path(body_path).unlink(missing_ok=True) # 本文を /tmp に残さない sync_exit = decisions_sync(root, number) if number else None @@ -401,9 +311,11 @@ def upsert(a, must_exist): "branch": branch, "draft": bool(getattr(a, "draft", False)), "closing_issues_in_body": closing_issues(body), + "deferred_closing": deferred, "decisions_sync_exit": sync_exit, } items = [{"kind": "pr", "name": url, "result": action}] + items += [{"kind": "closing", "name": t, "result": "deferred"} for t in deferred] if sync_exit is not None: items.append({"kind": "decisions", "name": "pr-body-decisions.sh sync", "result": f"exit={sync_exit}"}) nxt = None @@ -459,32 +371,6 @@ def cmd_update(a): # --- report ----------------------------------------------------------------------- -def pr_for_report(root, number): - fields = "number,title,url,isDraft,baseRefName,headRefName,body" - p = gh_parts.gh(["pr", "view", str(number), "--json", fields], cwd=root) - if p.returncode == 0: - try: - return json.loads(p.stdout) - except ValueError: - raise StepError("gh pr view の出力を読めない", EXIT_UNREADABLE) - if not gh_parts.is_rate_limited(p.stderr): - raise StepError(f"gh pr view {number} が失敗: {p.stderr.strip()[:300]}") - owner, name = repo_owner_name(root) - q = gh_parts.gh(["api", f"repos/{owner}/{name}/pulls/{number}"], cwd=root) - if q.returncode != 0: - raise StepError(f"REST でも PR を引けない: {q.stderr.strip()[:300]}") - d = json.loads(q.stdout) - return { - "number": d["number"], - "title": d["title"], - "url": d["html_url"], - "isDraft": d.get("draft", False), - "baseRefName": d["base"]["ref"], - "headRefName": d["head"]["ref"], - "body": d.get("body") or "", - } - - def section(body, heading): """見出しが `heading` で始まる(大文字と小文字を区別しない)`## ` の節の行(子の節を含む)。""" return [line for title, lines in h2_sections(body) if title.lower().startswith(heading.lower()) for line in lines] diff --git a/plugins/ndf/scripts/pr_steps_lib/__init__.py b/plugins/ndf/scripts/pr_steps_lib/__init__.py new file mode 100644 index 000000000..35ec6cdcf --- /dev/null +++ b/plugins/ndf/scripts/pr_steps_lib/__init__.py @@ -0,0 +1 @@ +"""pr-steps.py から分けたモジュール。""" diff --git a/plugins/ndf/scripts/pr_steps_lib/gh_pr.py b/plugins/ndf/scripts/pr_steps_lib/gh_pr.py new file mode 100644 index 000000000..08ef8c9b1 --- /dev/null +++ b/plugins/ndf/scripts/pr_steps_lib/gh_pr.py @@ -0,0 +1,162 @@ +"""PR を gh で読み書きする部品(`pr-steps.py` から分けた)。上限(rate limit)なら REST へ退避する。 + +書く先の PR と宛先を決める `target_pr`、本文ファイルで作る・更新する `send_body`、既存の PR と report の読み取りを持つ。 +出す(emit)のは呼び手である。 +""" + +from __future__ import annotations + +import json +import re +import tempfile +from pathlib import Path + +import gh_parts +import repo +from step_result import EXIT_PRECONDITION, EXIT_UNREADABLE, StepError + + +def repo_owner_name(root): + p = gh_parts.gh(["repo", "view", "--json", "owner,name"], cwd=root) + if p.returncode == 0: + try: + d = json.loads(p.stdout) + return d["owner"]["login"], d["name"] + except (ValueError, KeyError, TypeError): + pass + slug = repo.owner_repo(root) + if not slug: + raise StepError("リポジトリの所有者と名前を決められない", EXIT_UNREADABLE) + owner, _, name = slug.partition("/") + return owner, name + + +def _gh_or_rest(root, args, rest_path, parse_rest, *, label, fail, rest_fail, fail_code=1, empty=""): + """gh を呼び、成功なら JSON を返す。上限なら REST(rest_path(owner, name))の出力を parse_rest で読む。""" + p = gh_parts.gh(args, cwd=root) + if p.returncode == 0: + try: + return json.loads(p.stdout or empty) + except ValueError: + raise StepError(f"{label} の出力を読めない", EXIT_UNREADABLE) + if not gh_parts.is_rate_limited(p.stderr): + raise StepError(f"{fail}: {p.stderr.strip()[:300]}", fail_code) + owner, name = repo_owner_name(root) + q = gh_parts.gh(["api", rest_path(owner, name)], cwd=root) + if q.returncode != 0: + raise StepError(f"{rest_fail}: {q.stderr.strip()[:300]}") + return parse_rest(q.stdout) + + +def existing_pr(root, branch): + """head が branch の OPEN の PR({number, url, isDraft, baseRefName})。無ければ None。上限なら REST。""" + + def parse_rest(out): + try: + prs = json.loads(out or "[]") + except ValueError: + raise StepError("REST の出力を読めない", EXIT_UNREADABLE) + if not prs: + return [] + pr = prs[0] + return [ + { + "number": pr["number"], + "url": pr["html_url"], + "isDraft": pr.get("draft", False), + "baseRefName": pr.get("base", {}).get("ref", ""), + } + ] + + prs = _gh_or_rest( + root, + ["pr", "list", "--head", branch, "--state", "open", "--json", "number,url,isDraft,baseRefName"], + lambda owner, name: f"repos/{owner}/{name}/pulls?head={owner}:{branch}&state=open", + parse_rest, + label="gh pr list", + fail="gh pr list が失敗", + rest_fail="REST でも既存の PR を引けない", + fail_code=EXIT_PRECONDITION, + empty="[]", + ) + return prs[0] if prs else None + + +def pr_for_report(root, number): + def parse_rest(out): + d = json.loads(out) + return { + "number": d["number"], + "title": d["title"], + "url": d["html_url"], + "isDraft": d.get("draft", False), + "baseRefName": d["base"]["ref"], + "headRefName": d["head"]["ref"], + "body": d.get("body") or "", + } + + return _gh_or_rest( + root, + ["pr", "view", str(number), "--json", "number,title,url,isDraft,baseRefName,headRefName,body"], + lambda owner, name: f"repos/{owner}/{name}/pulls/{number}", + parse_rest, + label="gh pr view", + fail=f"gh pr view {number} が失敗", + rest_fail="REST でも PR を引けない", + ) + + +def target_pr(root, branch, a, must_exist): + """書く先の PR と宛先。(既存の PR か None, 宛先のブランチか None) を返す。 + + 宛先は、既存の PR なら baseRefName、新しく作るなら --base か起点(#2228 の決定 9)。""" + if getattr(a, "pr", None): + return {"number": a.pr, "url": f"#{a.pr}"}, pr_for_report(root, a.pr).get("baseRefName") or None + pr = existing_pr(root, branch) + if pr: + return pr, pr.get("baseRefName") or None + if must_exist: + raise StepError(f"{branch} の OPEN の PR が無い(create を使う)", EXIT_PRECONDITION) + if not a.title: + raise StepError("--title が要る", EXIT_UNREADABLE) + return None, a.base or repo.base_branch(root) or "main" + + +def send_body(root, branch, body, body_path, a, pr, base): + """gh へ本文ファイルを渡して PR を作る・更新する。(番号, URL, action) を返す。""" + if pr: + args = ["pr", "edit", str(pr["number"]), "--body-file", body_path] + if a.title: + args += ["--title", a.title] + p = gh_parts.gh(args, cwd=root) + if p.returncode != 0: + raise StepError(f"gh pr edit {pr['number']} が失敗: {p.stderr.strip()[:300]}") + return pr["number"], pr["url"], "updated" + args = ["pr", "create", "--base", base, "--title", a.title, "--body-file", body_path] + if a.draft: + args.append("--draft") + p = gh_parts.gh(args, cwd=root) + if p.returncode == 0: + url = (p.stdout.strip().splitlines() or [""])[-1] + m = re.search(r"/pull/(\d+)", url) + return (int(m.group(1)) if m else None), url, "created" + if gh_parts.is_rate_limited(p.stderr): + number, url = rest_create(root, branch, base, a.title, body, a.draft) + return number, url, "created_rest" + raise StepError(f"gh pr create が失敗: {p.stderr.strip()[:300]}") + + +def rest_create(root, branch, base, title, body, draft): + owner, name = repo_owner_name(root) + payload = {"title": title, "head": branch, "base": base, "body": body, "draft": bool(draft)} + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as f: + json.dump(payload, f, ensure_ascii=False) + tmp = f.name + try: + p = gh_parts.gh(["api", f"repos/{owner}/{name}/pulls", "--input", tmp], cwd=root) + finally: + Path(tmp).unlink(missing_ok=True) # 本文を /tmp に残さない + if p.returncode != 0: + raise StepError(f"REST でも作成が失敗: {p.stderr.strip()[:300]}") + d = json.loads(p.stdout) + return d["number"], d["html_url"] diff --git a/plugins/ndf/scripts/progress-record.sh b/plugins/ndf/scripts/progress-record.sh index f64e9271a..8e5b52564 100644 --- a/plugins/ndf/scripts/progress-record.sh +++ b/plugins/ndf/scripts/progress-record.sh @@ -43,8 +43,8 @@ TOOL=progress-record . "$SCRIPT_DIR/lib/projects-common.sh" 2>/dev/null \ || sr_emit --tool "$TOOL" --status stopped --summary "lib/projects-common.sh を読めない" --code 3 # 工程名・モード・進め方の一覧と通過記録は development-workflow のライブラリが持つ(#725 の決定 2・3)。 -# **同じプラグインの根の相対で辿る。** `cd -P` で実体へ解決しない。agy の配置(dev.agy/scripts と -# dev.agy/skills/development-workflow がどちらも symlink)でも、`scripts/` の 1 つ上に `skills/` がある。 +# **同じプラグインの根の相対で辿る。** `cd -P` で実体へ解決しない。`scripts/` が symlink で置かれた配置でも、 +# `scripts/` の 1 つ上に `skills/` がある。 # shellcheck source=../skills/development-workflow/scripts/lib/workflow-common.sh . "$SCRIPT_DIR/../skills/development-workflow/scripts/lib/workflow-common.sh" 2>/dev/null \ || sr_emit --tool "$TOOL" --status stopped --summary "development-workflow の workflow-common.sh を読めない" --code 3 diff --git a/plugins/ndf/scripts/release-steps.py b/plugins/ndf/scripts/release-steps.py index 7976a0b25..e8451713e 100755 --- a/plugins/ndf/scripts/release-steps.py +++ b/plugins/ndf/scripts/release-steps.py @@ -35,7 +35,7 @@ python3 release-steps.py approve --approval <承認資料> --facts-from <開発版の状態> [--answer-from <状態>] --by user python3 release-steps.py approval-facts --version <版> --prs ... [--prev-tag <タグ>] [--plugin <名前>] [--root ] python3 release-steps.py notes --version <版> --prs ... [--approval <提示物>] - [--verified claude,codex,kiro] [--ref <ブランチ>] [--plugin <名前>] [--root ] + [--verified claude,codex] [--ref <ブランチ>] [--plugin <名前>] [--root ] release --channel prod は承認したコミットを受け取り、配布の PR をマージした後の `origin/<ベースブランチ>` の先端と比べ、 承認の外の変更があれば本番チャネルの PR もタグも作らずに承認ゲート(10)で止まる(release_lib/approval.py。#815)。 @@ -330,15 +330,11 @@ def base_version(root, pdir, ref): return None -def plan_bump(root, pdir, plugin, old, new): - """`plugin` の版を `old` から `new` へ上げる箇所を集めた `BumpPlan` を返す(まだ書き換えない)。""" - ed = bump.BumpPlan(root, old, new) - desc_re = r'^\s*"description"\s*:.*\(v' + re.escape(old) + r"\)" - +def _bump_manifests(ed, pdir, old, desc_re): + """plugin.json 群の version と description の版を書き換える箇所を `ed` へ足す。""" for rel, _ in ( (".claude-plugin/plugin.json", True), (".codex-plugin/plugin.json", False), - ("dev.agy/plugin.json", False), ("plugin.json", False), ): f = pdir / rel @@ -348,6 +344,9 @@ def plan_bump(root, pdir, plugin, old, new): if f"(v{old})" in f.read_text(encoding="utf-8"): ed.sub(f, desc_re, f"{rel} の description") + +def _bump_marketplace_and_readme(ed, root, plugin, old, desc_re): + """marketplace.json の description と README.md のプラグイン一覧表の版を書き換える箇所を `ed` へ足す。""" mp = root / ".claude-plugin" / "marketplace.json" if mp.is_file(): s, e = bump.marketplace_range(ed.lines(mp), plugin) @@ -362,14 +361,25 @@ def plan_bump(root, pdir, plugin, old, new): ed.sub(readme, r"^\| \*\*" + re.escape(plugin) + r"\*\* \|", "README.md のプラグイン一覧表") elif plugin in ("ndf", "playwright-kit"): ed.manual.append(f"README.md のプラグイン一覧表に {plugin} の行が無い") - if plugin == "ndf": - ed.sub(readme, r"\*\*NDFプラグイン v", "README.md の概要の版") - nr = pdir / "README.md" - ed.sub(nr, r"(Kiro CLI用 / v", "plugins/ndf/README.md の Kiro の確認例") - ed.sub(nr, r"/plugins/cache/ai-plugins/ndf/", "plugins/ndf/README.md の Codex のパス例", count=2) - ed.sub(nr, r"ndf@ai-plugins\s+installed", "plugins/ndf/README.md の codex plugin list の出力例") - bump.versioning_doc(ed) + +def _bump_ndf_docs(ed, root, pdir): + """ndf だけが持つ版の記載(README 概要・Codex のパス例・list の出力例・版の文書)を `ed` へ足す。""" + ed.sub(root / "README.md", r"\*\*NDFプラグイン v", "README.md の概要の版") + nr = pdir / "README.md" + ed.sub(nr, r"/plugins/cache/ai-plugins/ndf/", "plugins/ndf/README.md の Codex のパス例", count=2) + ed.sub(nr, r"ndf@ai-plugins\s+installed", "plugins/ndf/README.md の codex plugin list の出力例") + bump.versioning_doc(ed) + + +def plan_bump(root, pdir, plugin, old, new): + """`plugin` の版を `old` から `new` へ上げる箇所を集めた `BumpPlan` を返す(まだ書き換えない)。""" + ed = bump.BumpPlan(root, old, new) + desc_re = r'^\s*"description"\s*:.*\(v' + re.escape(old) + r"\)" + _bump_manifests(ed, pdir, old, desc_re) + _bump_marketplace_and_readme(ed, root, plugin, old, desc_re) + if plugin == "ndf": + _bump_ndf_docs(ed, root, pdir) return ed @@ -861,6 +871,36 @@ def kept_sections(root, approval): raise StepError(f"前の承認資料 {approval} を読めない(書き換えずに止める): {e}", 2) +def included_pr_facts(prs, data, checks, repo): + """含む PR ごとの事実から、機械向けの items と承認資料の「含む PR」の行を作る。 + + data は PR 番号 → 取得済みの PR、checks は PR 番号 → CI の結果(pr_check_buckets の戻り)。""" + items, rows = [], [] + for p in prs: + n, d = p.number, data.get(p.number) or {} + oid = (d.get("mergeCommit") or {}).get("oid") or "" + pr_checks = checks[n] + passed = sum(1 for c in pr_checks if c.get("bucket") == "pass") + url = d.get("url") or f"https://github.com/{repo}/pull/{n}" + items.append( + { + "kind": "pr", + "name": f"#{n}", + "result": str(d.get("state", "")).lower() or "unknown", + "url": url, + "title": d.get("title", ""), + "merge_commit": oid, + "checks_passed": passed, + "checks": len(pr_checks), + "via": p.via, + "no_change": included.no_change(d), + } + ) + facts = [str(d.get("state", "")), oid[:8] if oid else "—", f"CI {passed}/{len(pr_checks)}"] + rows.append(included.included_row(n, d.get("title", ""), facts, p.via, included.no_change(d))) + return items, rows + + def cmd_approval_facts(a): root = git_root(a.root) repo = repo_lib.owner_repo(root) @@ -883,30 +923,8 @@ def cmd_approval_facts(a): ins = int(m.group(1)) if (m := re.search(r"(\d+) insertions?", stat)) else 0 dels = int(m.group(1)) if (m := re.search(r"(\d+) deletions?", stat)) else 0 - items, rows = [], [] - for p in prs: - n, d = p.number, data.get(p.number) or {} - oid = (d.get("mergeCommit") or {}).get("oid") or "" - checks = pr_check_buckets(root, n) - passed = sum(1 for c in checks if c.get("bucket") == "pass") - url = d.get("url") or f"https://github.com/{repo}/pull/{n}" - items.append( - { - "kind": "pr", - "name": f"#{n}", - "result": str(d.get("state", "")).lower() or "unknown", - "url": url, - "title": d.get("title", ""), - "merge_commit": oid, - "checks_passed": passed, - "checks": len(checks), - "via": p.via, - "no_change": included.no_change(d), - } - ) - facts = [str(d.get("state", "")), oid[:8] if oid else "—", f"CI {passed}/{len(checks)}"] - rows.append(included.included_row(n, d.get("title", ""), facts, p.via, included.no_change(d))) - + checks = {p.number: pr_check_buckets(root, p.number) for p in prs} + items, rows = included_pr_facts(prs, data, checks, repo) compare = f"https://github.com/{repo}/compare/{prev}...{dev}" path = approval_present( TOOL, @@ -1049,37 +1067,7 @@ def cmd_notes(a): migration = [f"- {i}" for *_, ms in notes for i in ms] fallback = sum(1 for *_, by_title, _ in notes if by_title) if path: - verified = [r for r in (a.verified or "").split(",") if r] - ref = a.ref or release_decl(root, a)[0] - decl = (release_decl(root, a)[0], repo_lib.owner_repo(root) or "", a.version, a.prs) - m = materials.approval_materials( - root, - decl, - getattr(a, "check_state", None), - prs, - data, - lambda r, n: issue_body(root, n) if materials.same_repo(r, decl[1]) else issue_body(root, n, repo=r), - ) - results, machine = materials.check_lines(m), materials.risk_lines(m, bool(risks), materials.now_jst()) - items = materials.write_approval(path, a.version, bullets, risks, verified, ref, migration, results, machine) - items += materials.items_of(m) + unmerged_items(skipped) + rep_items - emit( - result( - TOOL, - "ok", - f"提示物の欄を {len(notes)} 件の PR から書いた{unmerged_note(skipped)}", - items, - { - "version": a.version, - "prs": len(notes), - "lines": len(bullets), - "migration": len(migration), - "approval": str(path), - "unmerged": skipped, - "replaced": gone, - }, - ) - ) + _emit_approval_notes(root, a, path, prs, data, notes, bullets, risks, migration, skipped, rep_items, gone) return items = write_notes(root, a.version, plugin_of(root, a), bullets, migration) + unmerged_items(skipped) + rep_items emit( @@ -1088,15 +1076,48 @@ def cmd_notes(a): "ok", f"{len(notes)} 件の PR の利用者向けの変化を書いた{unmerged_note(skipped)}", items, - { - "version": a.version, - "prs": len(notes), - "lines": len(bullets), - "migration": len(migration), - "fallback": fallback, - "unmerged": skipped, - "replaced": gone, - }, + _notes_metrics(a, notes, bullets, migration, skipped, gone, fallback=fallback), + ) + ) + + +def _notes_metrics(a, notes, bullets, migration, skipped, gone, **extra): + """notes の 2 つの経路に共通の metrics。経路ごとの鍵(approval / fallback)は extra で migration の後へ入れる。""" + return { + "version": a.version, + "prs": len(notes), + "lines": len(bullets), + "migration": len(migration), + **extra, + "unmerged": skipped, + "replaced": gone, + } + + +def _emit_approval_notes(root, a, path, prs, data, notes, bullets, risks, migration, skipped, rep_items, gone): + """承認の資料(--approval)の経路: 資料を集めて提示物の欄を書き、結果を出す。""" + verified = [r for r in (a.verified or "").split(",") if r] + base = release_decl(root, a)[0] + ref = a.ref or base + decl = (base, repo_lib.owner_repo(root) or "", a.version, a.prs) + m = materials.approval_materials( + root, + decl, + getattr(a, "check_state", None), + prs, + data, + lambda r, n: issue_body(root, n) if materials.same_repo(r, decl[1]) else issue_body(root, n, repo=r), + ) + results, machine = materials.check_lines(m), materials.risk_lines(m, bool(risks), materials.now_jst()) + items = materials.write_approval(path, a.version, bullets, risks, verified, ref, migration, results, machine) + items += materials.items_of(m) + unmerged_items(skipped) + rep_items + emit( + result( + TOOL, + "ok", + f"提示物の欄を {len(notes)} 件の PR から書いた{unmerged_note(skipped)}", + items, + _notes_metrics(a, notes, bullets, migration, skipped, gone, approval=str(path)), ) ) @@ -1192,7 +1213,7 @@ def build_parser() -> argparse.ArgumentParser: ) p.add_argument("--plugin", help="配るプラグイン(既定は宣言の release.plugin)") p.add_argument("--approval", help="本番承認の提示物。渡すと「配る中身」「検証への配布で確かめたこと」の欄を書く") - p.add_argument("--verified", default="", help="--approval: 導入を確かめた経路(カンマ区切り。例 claude,codex,kiro)") + p.add_argument("--verified", default="", help="--approval: 導入を確かめた経路(カンマ区切り。例 claude,codex)") p.add_argument("--ref", help="--approval: 検証への配布で導入した ref(既定は宣言のベースブランチ)") p.add_argument( "--check-state", diff --git a/plugins/ndf/scripts/release-verification-steps.py b/plugins/ndf/scripts/release-verification-steps.py index a88fa97bd..eb206e5b5 100755 --- a/plugins/ndf/scripts/release-verification-steps.py +++ b/plugins/ndf/scripts/release-verification-steps.py @@ -2,7 +2,7 @@ """release-verification-steps.py: リリース後テストの導入確認(#862。試作は #827 の phase-steps.py)。 python3 release-verification-steps.py verify-install --ref <ベースブランチ|本番チャネル> --expect <版> - [--plugins <名前>,...] [--runtimes claude,codex,kiro] [--root ] + [--plugins <名前>,...] [--runtimes claude,codex] [--root ] 隔離した HOME で ref から導入し、導入された版と、前の正式版のタグから変わったファイルの中身が ref と一致するかを確かめる。利用者の HOME の設定が変わっていないことも確かめる。 @@ -26,6 +26,7 @@ import sys import tempfile from pathlib import Path +from typing import NamedTuple sys.path.insert(0, str(Path(__file__).resolve().parent / "lib")) from step_result import ( @@ -138,7 +139,7 @@ def same_entry(s, t): """s と t の中身が同じか。symlink はリンク先の文字列か、辿った先の中身で比べる。 導入先は symlink をそのまま残すことも、辿った先を複製することもあるため、どちらでも一致とする。 - ディレクトリ(例: dev.agy/skills/<名前> → ../../skills/<名前>)は配下のファイルをすべて比べる。 + ディレクトリは配下のファイルをすべて比べる。 """ s, t = Path(s), Path(t) if s.is_symlink() and t.is_symlink() and os.readlink(s) == os.readlink(t): @@ -164,7 +165,7 @@ def compare_files(src_root, dst, rel_plugin, files, label, keeps_symlinks=True): if not s.exists(): continue # ref で消えたファイル if not keeps_symlinks and s.is_symlink(): - continue # 導入先に入らない(例: dev.agy/skills/<名前> → ../../skills/<名前>) + continue # 導入先に入らない if not same_entry(s, t): out.append(f"{label}: {f}") return out @@ -220,83 +221,6 @@ def verify_codex(env, ref, plugins, expect, where): return res, dirs -def verify_kiro(env, src, tmp, expect): - proj = Path(tmp) / "kiro-project" - proj.mkdir() - res = {"exit": 0, "version": None} - code, out = run_env_i(["bash", str(src / "plugins/ndf/dev.kiro/install.sh"), "--project", str(proj), "--yes"], env, cwd=str(src)) - res["exit"] = code - if code != 0: - res["error"] = out.strip()[-500:] - return res, None - agent = proj / ".kiro" / "agents" / "ndf.json" - if agent.is_file(): - try: - desc = json.loads(agent.read_text(encoding="utf-8")).get("description", "") - except (OSError, ValueError): - desc = "" - m = re.search(r"v(\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)", desc) - res["version"] = m.group(1) if m else None - return res, proj - - -KIRO_PLUGIN = "plugins/ndf" -KIRO_POLICY = "ndf-policies" - - -def kiro_manifest_skills(src): - """manifests/kiro-skills.txt に載る Skill 名(コメント・空行を除く)。""" - text = (Path(src) / KIRO_PLUGIN / "manifests" / "kiro-skills.txt").read_text(encoding="utf-8") - return sorted({line.split("#", 1)[0].strip() for line in text.splitlines()} - {""}) - - -def kiro_steering_body(policy_file): - """install.sh が steering へ写す ndf-policies の本文(frontmatter を除いた部分)。""" - text = Path(policy_file).read_text(encoding="utf-8") - if text.startswith("---\n"): - end = text.find("\n---\n", 3) - if end != -1: - text = text[end + len("\n---\n") :] - return text.strip("\n") - - -def compare_kiro(src, proj): - """Kiro の導入先(/.kiro)を ref の展開物と比べ、一致しないものを返す。 - - manifest の Skill は .kiro/skills/<名前> のリンク、dev.kiro/prompts/*.md は .kiro/prompts/ の複製、 - ndf-policies は .kiro/steering/ndf-policies.md の本文、agents/ndf.json は JSON として読めることを見る。 - """ - src, kiro = Path(src), Path(proj) / ".kiro" - plugin = src / KIRO_PLUGIN - out = [] - for name in kiro_manifest_skills(src): - s, t = plugin / "skills" / name, kiro / "skills" / name - if not (s / "SKILL.md").is_file(): - continue # installer も SKIP する - if name == KIRO_POLICY: - if t.exists() or t.is_symlink(): - out.append(f"kiro: {t.relative_to(proj)} が残っている(steering へ移した Skill)") - continue - if not same_entry(s, t): - out.append(f"kiro: {t.relative_to(proj)}") - for f in sorted((plugin / "dev.kiro" / "prompts").glob("*.md")): - if f.name == "codex.md": - continue # --with-codex のときだけ配る - t = kiro / "prompts" / f.name - if not (t.is_file() and filecmp.cmp(f, t, shallow=False)): - out.append(f"kiro: {t.relative_to(proj)}") - steering = kiro / "steering" / f"{KIRO_POLICY}.md" - body = kiro_steering_body(plugin / "skills" / KIRO_POLICY / "SKILL.md") - if not (steering.is_file() and steering.read_text(encoding="utf-8").endswith(f"\n{body}\n")): - out.append(f"kiro: {steering.relative_to(proj)}") - agent = kiro / "agents" / "ndf.json" - try: - json.loads(agent.read_text(encoding="utf-8")) - except (OSError, ValueError): - out.append(f"kiro: {agent.relative_to(proj)}") - return out - - def install_source(root, a): """導入元(owner/repo・マーケットプレイスの名前・本番チャネル)と、確かめるプラグイン。値は origin の URL と宣言から読む (#1336 の決定 8)。--ref は宣言のベースブランチか本番チャネルに限る。""" @@ -327,64 +251,86 @@ def _extract_source(root, ref_rev, tmp) -> Path: return src -def _verify_runtimes(a, root, src, tmp, runtimes, plugins, rel, prev, ref_rev, where): +class VerifyContext(NamedTuple): + """verify-install の 1 回で組む値の束(ref・版・前のタグ・対象のプラグインと導入元)。""" + + ref: str + expect: str + root: Path + ref_rev: str + prev: str | None + runtimes: list[str] + plugins: list[str] + rel: dict[str, str] + where: object + + +def _verify_runtimes(ctx: VerifyContext, src, tmp): """ランタイムごとに導入して確かめる。(ランタイムごとの結果, 中身の不一致) を返す。""" runtimes_res, mismatch = {}, [] changed = { - p: ([f for f in git(root, "diff", "--name-only", prev, ref_rev, "--", r).stdout.split() if f] if prev else []) - for p, r in rel.items() + p: ([f for f in git(ctx.root, "diff", "--name-only", ctx.prev, ctx.ref_rev, "--", r).stdout.split() if f] if ctx.prev else []) + for p, r in ctx.rel.items() } env = isolated_env(tmp) for name, fn in (("claude", verify_claude), ("codex", verify_codex)): - if name not in runtimes: + if name not in ctx.runtimes: continue - res, dirs = fn(env, a.ref, plugins, a.expect, where) + res, dirs = fn(env, ctx.ref, ctx.plugins, ctx.expect, ctx.where) runtimes_res[name] = res for p, d in dirs.items(): if d is None: mismatch.append(f"{name}: {p} の導入先が無い") else: - mismatch += compare_files(src, d, rel[p], changed[p], name, keeps_symlinks=(name != "codex")) - if "kiro" in runtimes: - runtimes_res["kiro"], proj = verify_kiro(env, src, tmp, a.expect) - if proj is not None: - mismatch += compare_kiro(src, proj) + mismatch += compare_files(src, d, ctx.rel[p], changed[p], name, keeps_symlinks=(name != "codex")) return runtimes_res, mismatch -def _build_result(a, ref_rev, prev, runtimes_res, mismatch, before, after) -> dict: +def _runtime_result(r, expect) -> str: + """ランタイム 1 件の判定: 導入が失敗 → failed、版が --expect と違う → version_mismatch、それ以外 ok。""" + v = r.get("version") + # mcp-serena などは別の版を持つので、ndf があれば ndf だけを --expect と比べる + vs = ([v.get("ndf")] if "ndf" in v else list(v.values())) if isinstance(v, dict) else [v] + if r.get("exit") != 0: + return "failed" + if any(x != expect for x in vs): + return "version_mismatch" + return "ok" + + +def _env_items(before, after) -> list[dict]: + """利用者の環境の差分を、変わった鍵ごとの項目にする。""" + if before == after: + return [] + return [ + {"kind": "user_env", "name": k, "result": "changed", "before": before[k], "after": after[k]} + for k in before + if before[k] != after[k] + ] + + +def _build_result(ctx: VerifyContext, runtimes_res, mismatch, before, after) -> dict: """ランタイムごとの結果・中身の不一致・利用者の環境の差分から結果をまとめる。""" - items = [] env_same = before == after - if not env_same: - for k in before: - if before[k] != after[k]: - items.append({"kind": "user_env", "name": k, "result": "changed", "before": before[k], "after": after[k]}) + items = _env_items(before, after) ok = env_same and not mismatch for name, r in runtimes_res.items(): - v = r.get("version") - # mcp-serena などは別の版を持つので、ndf があれば ndf だけを --expect と比べる - vs = ([v.get("ndf")] if "ndf" in v else list(v.values())) if isinstance(v, dict) else [v] - res = "ok" - if r.get("exit") != 0: - res = "failed" - elif any(x != a.expect for x in vs): - res = "version_mismatch" + res = _runtime_result(r, ctx.expect) if res != "ok": ok = False items.append({"kind": "runtime", "name": name, "result": res, **r}) items += [{"kind": "file", "name": m, "result": "mismatch"} for m in mismatch] metrics = { - "ref": a.ref, - "rev": ref_rev[:8], - "prev_tag": prev, - "expect": a.expect, + "ref": ctx.ref, + "rev": ctx.ref_rev[:8], + "prev_tag": ctx.prev, + "expect": ctx.expect, "user_env_unchanged": env_same, "mismatch": len(mismatch), } bad_rt = [i["name"] for i in items if i["kind"] == "runtime" and i["result"] != "ok"] summary = ( - f"{a.ref}({ref_rev[:8]})から {', '.join(runtimes_res)} へ導入し v{a.expect} を確かめた" + f"{ctx.ref}({ctx.ref_rev[:8]})から {', '.join(runtimes_res)} へ導入し v{ctx.expect} を確かめた" if ok else f"導入の確認が通らない(runtime: {', '.join(bad_rt) or 'なし'} / 中身の不一致 {len(mismatch)} 件" f" / 利用者の環境が{'変わらない' if env_same else '変わった'})" @@ -396,7 +342,7 @@ def cmd_verify_install(a): root = git_root(a.root) where, plugins = install_source(root, a) runtimes = [r.strip() for r in a.runtimes.split(",") if r.strip()] - bad = [r for r in runtimes if r not in ("claude", "codex", "kiro")] + bad = [r for r in runtimes if r not in ("claude", "codex")] if bad: raise StepError(f"未知の runtime: {','.join(bad)}", EXIT_UNREADABLE) rel = {p: plugin_dir(root, p).relative_to(root).as_posix() for p in plugins} @@ -407,17 +353,18 @@ def cmd_verify_install(a): tags = git(root, "tag", "--list", f"{prefix}*", "--sort=-v:refname").stdout.split() cur = f"{prefix}{a.expect}" prev = next((t for t in tags if t != cur and "-" not in t[len(prefix) :]), None) + ctx = VerifyContext(a.ref, a.expect, root, ref_rev, prev, runtimes, plugins, rel, where) before = user_env_snapshot() tmp = tempfile.mkdtemp(prefix="ndf-verify-install-") try: src = _extract_source(root, ref_rev, tmp) - runtimes_res, mismatch = _verify_runtimes(a, root, src, tmp, runtimes, plugins, rel, prev, ref_rev, where) + runtimes_res, mismatch = _verify_runtimes(ctx, src, tmp) finally: shutil.rmtree(tmp, ignore_errors=True) after = user_env_snapshot() - emit(_build_result(a, ref_rev, prev, runtimes_res, mismatch, before, after)) + emit(_build_result(ctx, runtimes_res, mismatch, before, after)) def build_parser(): @@ -430,7 +377,7 @@ def build_parser(): p.add_argument("--ref", required=True, help="導入する ref(宣言のベースブランチか本番チャネル)") p.add_argument("--expect", required=True, type=version_arg) p.add_argument("--plugins", help="カンマ区切り(例 ndf,mcp-serena。既定は宣言の release.plugin)") - p.add_argument("--runtimes", default="claude,codex,kiro", help="カンマ区切り(claude,codex,kiro)") + p.add_argument("--runtimes", default="claude,codex", help="カンマ区切り(claude,codex)") p.set_defaults(func=cmd_verify_install) return ap diff --git a/plugins/ndf/scripts/release_lib/materials.py b/plugins/ndf/scripts/release_lib/materials.py index 8cedacbb6..4969b8271 100644 --- a/plugins/ndf/scripts/release_lib/materials.py +++ b/plugins/ndf/scripts/release_lib/materials.py @@ -270,7 +270,8 @@ def read_unconfirmed(root: Path, base: str, issues) -> tuple[Unconfirmed, ...] | def read_unverified(bodies: dict[tuple[str, int], str], verdicts: dict, repo: str) -> tuple[Unverified, ...] | None: """閉じる課題((owner/name, 番号))の受け入れ条件のうち、印が無く、リリース後テストの表(verdicts)にその - リポジトリの課題として合格の行が無いもの。どの課題にも受け入れ条件が無ければ None。""" + リポジトリの課題として合格の行が無いもの。本文の `## 受け入れ条件の確認` でマージ前に確かめたと記録された + 条件は載せない(#2144。記録の無い課題は今までどおり)。どの課題にも受け入れ条件が無ければ None。""" out, found = [], False for (owner, n), body in bodies.items(): rows = acceptance.criteria_lines(body or "") @@ -282,7 +283,8 @@ def read_unverified(bodies: dict[tuple[str, int], str], verdicts: dict, repo: st continue passed = v.passed if v is not None else frozenset() other = None if same_repo(owner, repo) else owner - out += [Unverified(n, c.number, text, other) for c, text in rows if not c.checked and c.number not in passed] + done = passed | acceptance.pre_merge_numbers(body or "") + out += [Unverified(n, c.number, text, other) for c, text in rows if not c.checked and c.number not in done] return tuple(out) if found else None diff --git a/plugins/ndf/scripts/sprint-close.py b/plugins/ndf/scripts/sprint-close.py index 601b07573..5a23794ad 100755 --- a/plugins/ndf/scripts/sprint-close.py +++ b/plugins/ndf/scripts/sprint-close.py @@ -1,19 +1,25 @@ #!/usr/bin/env python3 """sprint-close.py: スプリントを閉じる(progress-tracking の「スプリントを閉じる」の手順)。 - python3 sprint-close.py --record-pr [--prs 1,2] [--issues 3,4] [--with-verification] + python3 sprint-close.py --record-pr [--final-stage 配布|リリース後テスト|振り返り] + [--prs 1,2] [--issues 3,4] [--with-verification] [--label "<マイルストーン>の<工程名>"] [--dry-run] [--root ] 1. 記録の PR の本文とコメントを読み、最後の「## 配布の記録」ブロックを取る 2. スプリントの PR の一覧(--prs、無ければブロックの `スプリント:` の行)の本文から、closing keywords - (GitHub が課題を自動で閉じるキーワード Closes / Fixes / Resolves)が指す課題を集める + (GitHub が課題を自動で閉じるキーワード Closes / Fixes / Resolves)と閉じる予定の参照 + (`最終工程の後に閉じる: #N`。既定ブランチ宛の本文で closing keywords を置き換えた形)が指す課題を集める (lib/closing-issues.sh)。--issues の課題(記録のリポジトリ)を足す(`pace: fast` の実装 PR は closing keywords を持たない)。PR を読んだのに集めた課題が 0 件なら、何も書かずに止まる(終了コード 2) -3. 閉じる条件 1(本番への配布まで済んだ、または配布なし)と、--with-verification のときは +3. 閉じる条件 1(本番への配布まで済んだ、または配布なし)と、--with-verification か最終工程がリリース後テストのときは 条件 2(その課題のリリース後テストの行がすべて合格で、本文の受け入れ条件の番号のすべてに合格の行がある) を課題ごとに判定する。`課題` の列が課題を指さない行(`—`・`PR 1869` など)は飛ばす。条件 2 は課題ごとの錠 - (lib/gh_parts.py の issue_lock)の中で状態と本文を 1 回で読んで判定し、満たした記録のリポジトリの課題は - 本文の受け入れ条件に印(`[x]`)を付けてから閉じる(規則は lib/acceptance.py。#1683) + (lib/gh_parts.py の issue_lock)の中で状態と本文を 1 回で読んで判定し(本文の `## 受け入れ条件の確認` で + マージ前に確かめたと記録された条件には合格の行を求めない。#2144)、満たした記録のリポジトリの課題は + 本文の受け入れ条件に印(`[x]`)を付けてから閉じる(規則は lib/acceptance.py。#1683)。 + 最後に条件 3(最終工程の記録。--final-stage、省けば振り返り)を判定する。振り返りは課題の本文の + `振り返り: <コメントの URL>` の行のうち、コメントの識別子が記録の PR の最後の配布の記録のコメントの識別子以上の + ものがあること(final_stage_gap。#2228)。条件 2 と同じ 1 回の本文の読みを使い、満たさなければ印も書かない 4. 条件を満たす課題ごとに (a) 状態を読む → (b) 記録のリポジトリの課題ならボードを Done → (c) 読み直して OPEN なら閉じる → (d) 読み直して CLOSED を確かめる 5. スプリントの PR の題と本文が番号で参照し、2 で集めた課題に入らない開いた課題(参照だけの課題、 @@ -24,9 +30,9 @@ closed / already_closed / failed / kept_open / closed_unverified(既に閉じていたが条件 2 を満たさない。 開き直さない)(--dry-run では would_close)。checked は印を付けた受け入れ条件の番号、would_check は --dry-run で付ける予定の番号。ボードへ Done を書けなかった(`projects-sync.sh` の結果 JSON が ok でない)ときは -{kind:"board_note"} の項目に理由が載り、課題を閉じる処理は続ける。 +{kind:"board_note"} の項目に理由が載り、課題を閉じる処理は続ける。metrics.final_stage は判定に使った最終工程。 `--record-pr 0` は「本番の記録なし」(最終の検査で変更が無く本番を飛ばした)。配布の記録を読まず、 -閉じる条件も見ずに --issues の課題を閉じる。--issues と一緒のときだけ受ける。 +閉じる条件 1・2 を見ずに --issues の課題を閉じる(条件 3 の振り返りは、比べる配布の記録が無いため行があれば満たす)。--issues と一緒のときだけ受ける。 `--record-pr unknown` は「記録の置き場が分からない」(リリースの経路が雛形でも昇格でもなく、まとめのプランが 記録の PR を決められない)。配布の記録を読まず、どの課題も閉じず、ボードも書かずに --issues の課題を理由つきの kept_open で返す(終了コード 0)。--issues と一緒のときだけ受ける。 @@ -39,7 +45,6 @@ import argparse import json import shlex -import subprocess import sys from contextlib import nullcontext from pathlib import Path @@ -65,32 +70,27 @@ import gh_rest # noqa: E402 import locks # noqa: E402 import step_read # noqa: E402 projects-sync.sh の結果 JSON を読む(#2227) -from closing import referenced_numbers # noqa: E402 from dist_record import parse_record # noqa: E402 記録の形は lib/dist_record.py が持つ(#1273) +sys.path.insert(0, str(HERE)) +from sprint_close_lib.collect import read_record, referenced_items, sprint_issues # noqa: E402 +from sprint_close_lib.final_stage import ( # noqa: E402 閉じる条件 3(#2228) + FINAL_GAP_LEAD, + FINAL_STAGES, + NO_BASE, + STAGE_RETRO, + STAGE_VERIFY, + final_stage_gap, + gap_item, +) + TOOL = "sprint-close" RECORD_UNKNOWN = "unknown" # --record-pr の「記録の置き場が分からない」(supervise_lib/sprint_routes.py が渡す。#1875) UNKNOWN_REASON = "配布の記録の置き場が分からない(リリースの経路が雛形でも昇格でもない。リリース記録を確かめて手で閉じる)" LOCK_TIMEOUT = 30 # 課題ごとの錠の待ち(body-lock の既定と同じ) -# --- 記録の読み取り ------------------------------------------------------------- - - -def read_record(root, repo, n): - """記録の PR の本文とコメントを投稿の順に 1 つの文字列にする。""" - p = gh_call.gh(["pr", "view", str(n), "--repo", repo, "--json", "body,comments"], cwd=root) - if p.returncode != 0: - raise StepError(f"記録の PR #{n} を読めない: {p.stderr.strip()[:300]}", EXIT_UNREADABLE) - try: - d = json.loads(p.stdout) - except ValueError: - raise StepError(f"記録の PR #{n} の出力を読めない", EXIT_UNREADABLE) - parts = [d.get("body") or ""] + [(c or {}).get("body") or "" for c in d.get("comments") or []] - return "\n".join(parts) - - IssueRows = acceptance.IssueRows # リリース後テストの表は承認の材料と同じ規則で読む(lib/acceptance.py。#1885 の決定 8) verification_verdicts = acceptance.verification_verdicts @@ -98,59 +98,27 @@ def read_record(root, repo, n): NO_ROWS = "リリース後テストの行が無い" NOT_PASSED = "リリース後テストに合格でない条件がある(不合格・保留)" UNVERIFIED = "閉じているが閉じる条件 2 を満たさない: " +UNREADABLE_BODY = "本文を読めない(最終工程の記録を確かめられない)" +NO_MAPPING = "条件と課題の対応が読めない" -def judge_acceptance(rows, criteria): +def judge_acceptance(rows, criteria, pre_merge=frozenset()): """閉じる条件 2 の判定。(満たすか, 満たさない理由, 印を付ける番号) を返す。 - 受け入れ条件が 0 個なら表の行の合否だけで決める。""" + 受け入れ条件が 0 個なら表の行の合否だけで決める。pre_merge(本文の `## 受け入れ条件の確認` でマージ前に + 確かめたと記録された番号。#2144)の条件には合格の行を求めない。すべての条件がそれなら行が無くても満たす。""" + if rows is None and criteria and all(c.number in pre_merge for c in criteria): + return True, None, [c.number for c in criteria if not c.checked] if rows is None: return False, NO_ROWS, [] if not rows.all_pass: return False, NOT_PASSED, [] - missing = [c.number for c in criteria if c.number not in rows.passed] + missing = [c.number for c in criteria if c.number not in rows.passed and c.number not in pre_merge] if missing: return False, "合格の行が無い受け入れ条件: " + ", ".join(map(str, missing)), [] return True, None, [c.number for c in criteria if not c.checked] -# --- 課題の収集 ----------------------------------------------------------------- - - -def _read_sprint_pr(root, repo, n): - """スプリントの PR の題と本文を 1 回で読む。""" - p = gh_call.gh(["pr", "view", str(n), "--repo", repo, "--json", "title,body"], cwd=root) - if p.returncode != 0: - raise StepError(f"スプリントの PR #{n} を読めない: {p.stderr.strip()[:300]}", EXIT_UNREADABLE) - try: - d = json.loads(p.stdout) - except ValueError: - raise StepError(f"スプリントの PR #{n} の出力を読めない", EXIT_UNREADABLE) - return d.get("title") or "", d.get("body") or "" - - -def sprint_issues(root, repo, prs): - """(closing keywords が指す課題, 題と本文が参照する記録のリポジトリの番号) を返す。 - - closing keywords は本文だけから読む(GitHub は題の closing keywords で課題を閉じない)。""" - seen, out, refs = set(), [], [] - for n in prs: - title, body = _read_sprint_pr(root, repo, n) - refs += [r for r in referenced_numbers(title + "\n" + body, repo) if r not in refs] - c = subprocess.run( - ["bash", str(HERE / "lib" / "closing-issues.sh"), "--repo", repo], cwd=root, input=body, capture_output=True, text=True - ) - for ln in c.stdout.splitlines(): - if "\t" not in ln: - continue - r, num = ln.split("\t", 1) - key = (r.strip(), int(num.strip())) - if key not in seen: - seen.add(key) - out.append(key) - return out, refs - - # --- 閉じる --------------------------------------------------------------------- @@ -160,17 +128,34 @@ def issue_state(root, repo, n): class CloseContext: - """全課題で一定の閉じる処理の材料。notes は課題ごとのボードの記録の失敗を溜める。retry_cmd は今の引数で組んだやり直しの cmd。""" + """全課題で一定の閉じる処理の材料。notes は課題ごとのボードの記録の失敗を溜める。retry_cmd は今の引数で組んだやり直しの cmd。 + final_stage と since は条件 3(final_stage_gap)の材料。""" - def __init__(self, root, record_repo, comment, argv=()): + def __init__(self, root, record_repo, comment, argv=(), final_stage=STAGE_RETRO, since=None): self.root, self.record_repo, self.comment = root, record_repo, comment + self.final_stage, self.since = final_stage, since # 条件 3 の最終工程と、振り返りの行を比べる基準 self.notes = [] self.retry_cmd = shlex.join(["python3", str(Path(__file__).resolve()), *argv]) +def _issue_base(repo, n): + """課題 1 件の結果の項目の共通部分。""" + return {"kind": "issue", "repo": repo, "number": n} + + +def _failed(base, reason, ctx): + """やり直しの cmd を添えた failed の項目。""" + return {**base, "result": "failed", "reason": reason, "cmd": ctx.retry_cmd} + + +def _dry_run_result(state): + """試行(dry_run)の結果: CLOSED なら already_closed、それ以外は would_close。""" + return "already_closed" if state == "CLOSED" else "would_close" + + def close_one(ctx, repo, n): root, record_repo, comment, notes = ctx.root, ctx.record_repo, ctx.comment, ctx.notes - it = {"kind": "issue", "repo": repo, "number": n} + it = _issue_base(repo, n) before = issue_state(root, repo, n) if before is None: return {**it, "result": "failed", "reason": "before を読めない", "cmd": f"gh issue close {n} --repo {repo}"} @@ -223,7 +208,7 @@ def _read_sprint_record(a, root, record_repo): return {"found": True, "stage": "配布なし(本番の記録なし)", "sprint_prs": [], "verify_block": None}, [] if a.record_pr == RECORD_UNKNOWN: return {"found": False, "stage": "記録の置き場が分からない", "sprint_prs": [], "verify_block": None}, [] - rec = parse_record(read_record(root, record_repo, a.record_pr)) + rec = parse_record(*read_record(root, record_repo, a.record_pr)) return rec, a.prs or rec["sprint_prs"] @@ -239,7 +224,8 @@ def _closing_blocker(a, rec, record_repo): elif not (stage.startswith("本番") or stage.startswith("配布なし")): kept_all = "本番への配布の前(本番への配布の後に閉じる)" verdicts = None - if kept_all is None and a.with_verification and a.record_pr != 0: + verify = a.with_verification or a.final_stage == STAGE_VERIFY # 最終工程がリリース後テストなら必ず見る(I4) + if kept_all is None and verify and a.record_pr != 0: blk = rec["verify_block"] if blk is None: kept_all = ( @@ -248,7 +234,9 @@ def _closing_blocker(a, rec, record_repo): else: verdicts = verification_verdicts(blk, record_repo) if verdicts is None: - kept_all = "条件と課題の対応が読めない" + kept_all = NO_MAPPING + if acceptance.verdict_table_readable(blk): + verdicts = {} # 課題を指す行が無い表: すべての条件をマージ前に確かめた課題だけ判定へ進める(#2144) return kept_all, verdicts @@ -264,38 +252,46 @@ def _read_state_body(root, repo, n): return d["state"], d.get("body") or "" -def _judge_and_mark(a, ctx, repo, n, rows): - """錠の中で状態と本文を読み、条件 2 を判定し、満たせば印を書く。(結果の項目か None, 印を付けた番号) を返す。""" - base = {"kind": "issue", "repo": repo, "number": n} +def _judge_and_mark(a, ctx, repo, n, rows, kept=None): + """錠の中で状態と本文を読み、条件 2 と条件 3 を判定し、満たせば印を書く。(結果の項目か None, 印を付けた番号) を返す。 + + kept は課題を指す行の無い表の理由(NO_MAPPING)。そのときはすべての条件がマージ前の課題だけを判定へ進め、 + 本文を読めない課題とそれ以外の課題は kept の kept_open にする(同じ 1 回の本文の読みで決める。#2144)。""" + base = _issue_base(repo, n) got = _read_state_body(ctx.root, repo, n) if got is None: - return {**base, "result": "failed", "reason": "本文を読めない", "cmd": ctx.retry_cmd}, [] + return ({**base, "result": "kept_open", "reason": kept} if kept else _failed(base, UNREADABLE_BODY, ctx)), [] state, body = got - ok, why, to_check = judge_acceptance(rows, acceptance.criteria(body)) + ok, why, to_check = judge_acceptance(rows, acceptance.criteria(body), acceptance.pre_merge_numbers(body)) own = repo == ctx.record_repo # 他のリポジトリの課題の本文は書かない(I7) + if not ok and kept: + return {**base, "result": "kept_open", "reason": kept}, [] if not ok: if state == "CLOSED": return {**base, "result": "closed_unverified", "reason": UNVERIFIED + why}, [] return {**base, "result": "kept_open", "reason": why}, [] + gap = final_stage_gap(ctx.final_stage, body, ctx.since) + if gap: # 条件 3 を満たさない課題の本文は書かない + return gap_item(base, state, gap), [] if a.dry_run: - out = {**base, "result": "already_closed" if state == "CLOSED" else "would_close"} + out = {**base, "result": _dry_run_result(state)} return ({**out, "would_check": to_check} if own and to_check else out), [] if not (own and to_check): return None, [] new = acceptance.check_criteria(body, to_check) if not acceptance.only_marks(body, new) or not gh_rest.update_body(repo, n, new): - return {**base, "result": "failed", "reason": "受け入れ条件の印を書けない", "cmd": ctx.retry_cmd}, [] + return _failed(base, "受け入れ条件の印を書けない", ctx), [] return None, to_check -def _close_verified(a, ctx, repo, n, rows): +def _close_verified(a, ctx, repo, n, rows, kept=None): """--with-verification の 1 課題。本文の読みから書きまでを課題ごとの錠の中で行い、閉じるのは錠の外で行う。""" lock = gh_parts.issue_lock(repo, n, LOCK_TIMEOUT) if repo == ctx.record_repo and not a.dry_run else nullcontext() try: with lock: - done, checked = _judge_and_mark(a, ctx, repo, n, rows) + done, checked = _judge_and_mark(a, ctx, repo, n, rows, kept) except locks.LockTimeout: - return {"kind": "issue", "repo": repo, "number": n, "result": "failed", "reason": "本文の錠を取れない", "cmd": ctx.retry_cmd} + return _failed(_issue_base(repo, n), "本文の錠を取れない", ctx) if done is not None: return done out = close_one(ctx, repo, n) @@ -303,40 +299,33 @@ def _close_verified(a, ctx, repo, n, rows): def _close_item(a, ctx, repo, n, kept_all, verdicts): - base = {"kind": "issue", "repo": repo, "number": n} + base = _issue_base(repo, n) + if kept_all == NO_MAPPING and verdicts == {}: # 課題を指す行の無い表: 判定は錠の中の 1 回の本文の読みで行う + return _close_verified(a, ctx, repo, n, None, kept_all) if kept_all: return {**base, "result": "kept_open", "reason": kept_all} if verdicts is not None: return _close_verified(a, ctx, repo, n, verdicts.get((repo, n))) + if ctx.final_stage == STAGE_RETRO: + got = _read_state_body(ctx.root, repo, n) + if got is None: # 記録の置き場を読めないときは記録があるとは読まない(I2) + return _failed(base, UNREADABLE_BODY, ctx) + state, body = got + gap = final_stage_gap(ctx.final_stage, body, ctx.since) + if gap: + return gap_item(base, state, gap) + if a.dry_run: + return {**base, "result": _dry_run_result(state)} if a.dry_run: st = issue_state(ctx.root, repo, n) return { **base, - "result": "already_closed" if st == "CLOSED" else "would_close", + "result": _dry_run_result(st), **({"reason": "状態を読めない"} if st is None else {}), } return close_one(ctx, repo, n) -NOT_GIVEN = "closing keywords(GitHub が課題を自動で閉じるキーワード Closes / Fixes / Resolves)も --issues も無い参照" - - -def _referenced_items(root, repo, refs, issues): - """参照だけの課題の行。1 番号につき状態と URL を 1 回読み、Pull Request と閉じた課題は行にしない。""" - items = [] - for n in refs: - if (repo, n) in issues: - continue - p = gh_call.gh(["issue", "view", str(n), "--repo", repo, "--json", "state,url", "-q", "[.state, .url] | @tsv"], cwd=root) - state, _, url = p.stdout.strip().partition("\t") - base = {"kind": "issue", "repo": repo, "number": n, "result": "kept_open"} - if p.returncode != 0 or not state: - items.append({**base, "reason": f"状態を読めない({NOT_GIVEN})"}) - elif "/pull/" not in url and state != "CLOSED": - items.append({**base, "reason": NOT_GIVEN}) - return items - - def _close_summary(items, prs, dry_run, referenced): kinds = ("closed", "already_closed", "failed", "closed_unverified", "kept_open", "would_close") count = {k: sum(1 for i in items if i["result"] == k) for k in kinds} @@ -354,74 +343,85 @@ def _close_summary(items, prs, dry_run, referenced): def _stop_no_issues(prs, refs): """スプリントの PR を読んだのに閉じる課題が 0 件。課題もボードも書かずに止まる(参照の状態は読まない)。""" seen = f"(PR が参照した番号: {' '.join(f'#{n}' for n in refs)})" if refs else "" + _stop_close( + f"閉じる課題が 0 件(スプリントの PR {len(prs)} 本に closing keywords(GitHub が課題を自動で閉じるキーワード" + " Closes / Fixes / Resolves)が無く、--issues も無い)", + f"閉じる課題を --issues で渡して打ち直す{seen}", + {"issues": 0, "prs": len(prs)}, + ) + + +def _stop_close(summary, nxt, metrics=None): + """閉じる前に止まる(items は空・終了コードは EXIT_UNREADABLE)。""" emit( - result( - TOOL, - "stopped", - f"閉じる課題が 0 件(スプリントの PR {len(prs)} 本に closing keywords(GitHub が課題を自動で閉じるキーワード" - " Closes / Fixes / Resolves)が無く、--issues も無い)", - [], - {"issues": 0, "prs": len(prs)}, - next=f"閉じる課題を --issues で渡して打ち直す{seen}", - ), + result(TOOL, "stopped", summary, [], {"issues": 0} if metrics is None else metrics, next=nxt), EXIT_UNREADABLE, ) -def cmd_close(a): - if a.record_pr in (0, RECORD_UNKNOWN) and not a.issues: - what = "0(本番の記録なし)" if a.record_pr == 0 else f"{RECORD_UNKNOWN}(記録の置き場が分からない)" +def _collect_close_issues(a, root, record_repo, prs): + """閉じる課題(スプリントの PR が閉じる課題と --issues)と、PR が参照しただけの番号を集める。0 件なら止まる。""" + given = [(record_repo, n) for n in a.issues or []] + if not prs and not given: + _stop_close("スプリントの PR の一覧が取れない。推測せず運用者に一覧を聞く", "運用者に一覧を聞き、--prs で渡して打ち直す") + issues, refs = sprint_issues(root, record_repo, prs) if prs else ([], []) + issues += [k for k in given if k not in issues] + if not issues: + _stop_no_issues(prs, refs) + return issues, refs + + +def _emit_close_result(a, items, notes, metrics, summary, count, decisions): + """結果を出す: 失敗があれば止める → 記録の置き場が分からなければ閉じない旨 → 通常(最終工程の記録の欠けを案内)。""" + if count["failed"]: emit( result( TOOL, "stopped", - f"--record-pr {what} は --issues と一緒に渡す", - [], - {"issues": 0}, - next="閉じる課題を --issues で渡して打ち直す", - ), - EXIT_UNREADABLE, + summary, + items + notes, + metrics, + next="失敗した課題の cmd でやり直す。棚卸しへ進まない", + decisions=decisions, + ) ) + if a.record_pr == RECORD_UNKNOWN: + summary = f"記録の置き場が分からないため閉じない(リリースの経路が雛形でも昇格でもない): {summary}" + nxt = "リリース記録とリリース後テストの記録を確かめ、受け入れ条件を満たした課題を手で閉じる" + emit(result(TOOL, "ok", summary, items + notes, metrics, next=nxt, decisions=decisions)) + nxt = None + if any(FINAL_GAP_LEAD in (i.get("reason") or "") for i in items): + nxt = "最終工程の記録が無い課題は、その工程(振り返りなど)を通してから打ち直す。前の版で工程を通った課題は確かめて手で閉じる" + emit(result(TOOL, "ok", summary, items + notes, metrics, next=nxt, decisions=decisions)) + + +def cmd_close(a): + if a.record_pr in (0, RECORD_UNKNOWN) and not a.issues: + what = "0(本番の記録なし)" if a.record_pr == 0 else f"{RECORD_UNKNOWN}(記録の置き場が分からない)" + _stop_close(f"--record-pr {what} は --issues と一緒に渡す", "閉じる課題を --issues で渡して打ち直す") root = git_root(a.root) record_repo = _record_repo(a, root) - given = [(record_repo, n) for n in a.issues or []] rec, prs = _read_sprint_record(a, root, record_repo) - if not prs and not given: - emit( - result( - TOOL, - "stopped", - "スプリントの PR の一覧が取れない。推測せず運用者に一覧を聞く", - [], - {"issues": 0}, - next="運用者に一覧を聞き、--prs で渡して打ち直す", - ), - EXIT_UNREADABLE, - ) - issues, refs = sprint_issues(root, record_repo, prs) if prs else ([], []) - issues += [k for k in given if k not in issues] - if not issues: - _stop_no_issues(prs, refs) + issues, refs = _collect_close_issues(a, root, record_repo, prs) kept_all, verdicts = _closing_blocker(a, rec, record_repo) items = [] comment = f"スプリント({a.label})を通りました" if a.label else "スプリントの終わりの工程を通りました" - ctx = CloseContext(root, record_repo, comment, a.argv) + since = NO_BASE if a.record_pr == 0 else rec.get("dist_comment_id") + ctx = CloseContext(root, record_repo, comment, a.argv, a.final_stage, since) notes = ctx.notes for repo, n in issues: items.append(_close_item(a, ctx, repo, n, kept_all, verdicts)) - refs_items = _referenced_items(root, record_repo, refs, issues) + refs_items = referenced_items(root, record_repo, refs, issues) items += refs_items count, metrics, summary = _close_summary(items, prs, a.dry_run, referenced=len(refs_items)) - if count["failed"]: - emit(result(TOOL, "stopped", summary, items + notes, metrics, next="失敗した課題の cmd でやり直す。棚卸しへ進まない")) - if a.record_pr == RECORD_UNKNOWN: - summary = f"記録の置き場が分からないため閉じない(リリースの経路が雛形でも昇格でもない): {summary}" - nxt = "リリース記録とリリース後テストの記録を確かめ、受け入れ条件を満たした課題を手で閉じる" - emit(result(TOOL, "ok", summary, items + notes, metrics, next=nxt)) - emit(result(TOOL, "ok", summary, items + notes, metrics)) + metrics["final_stage"] = a.final_stage + decisions = [ + {"decision": f"最終工程を {a.final_stage} として判定した", "basis": f"--final-stage {a.final_stage}(省略時は{STAGE_RETRO})"} + ] + _emit_close_result(a, items, notes, metrics, summary, count, decisions) class Parser(argparse.ArgumentParser): @@ -467,6 +467,12 @@ def build_parser(): ) ap.add_argument("--repo", help="記録のリポジトリ(owner/name)。省けば gh repo view で決める") ap.add_argument("--with-verification", action="store_true", help="リリース後テストを通る経路(閉じる条件 2 を見る)") + ap.add_argument( + "--final-stage", + choices=FINAL_STAGES, + default=STAGE_RETRO, + help="その実行の最終工程(閉じる条件 3)。省けば振り返り。リリース後テストは条件 2 を必ず見る", + ) ap.add_argument("--label", help="閉じるときのコメントに入れる「<マイルストーン>の<工程名>」") ap.add_argument("--dry-run", action="store_true", help="状態を読むだけで、ボードも課題も書かない") ap.add_argument("--root", help="対象のリポジトリの根(既定はカレントの git の根)") diff --git a/plugins/ndf/scripts/sprint_close_lib/__init__.py b/plugins/ndf/scripts/sprint_close_lib/__init__.py new file mode 100644 index 000000000..73217df0e --- /dev/null +++ b/plugins/ndf/scripts/sprint_close_lib/__init__.py @@ -0,0 +1 @@ +"""sprint-close.py から分けたモジュール。""" diff --git a/plugins/ndf/scripts/sprint_close_lib/collect.py b/plugins/ndf/scripts/sprint_close_lib/collect.py new file mode 100644 index 000000000..845f96125 --- /dev/null +++ b/plugins/ndf/scripts/sprint_close_lib/collect.py @@ -0,0 +1,86 @@ +"""記録の PR とスプリントの PR の読み取りと、閉じる課題・参照だけの課題の集め方(`sprint-close.py` から分けた)。 + +記録の形は `lib/dist_record.py`、closing keywords の読み取りは `lib/closing.py` が持つ。 +出す(emit)のは呼び手である。 +""" + +from __future__ import annotations + +import json +import re + +import gh_call +from closing import closing_refs, referenced_numbers +from step_result import EXIT_UNREADABLE, StepError + +_COMMENT_ID = re.compile(r"#issuecomment-(\d+)$") + + +def read_record(root, repo, n): + """記録の PR の本文とコメントを投稿の順に 1 つの文字列にする。 + + (連結した文字列, 部分ごとの (先頭の行の番号, コメントの識別子) の並び) を返す。本文の識別子は None、コメントの + 識別子は `comments[].url` の `#issuecomment-<識別子>` の数字(読めなければ None)。""" + p = gh_call.gh(["pr", "view", str(n), "--repo", repo, "--json", "body,comments"], cwd=root) + if p.returncode != 0: + raise StepError(f"記録の PR #{n} を読めない: {p.stderr.strip()[:300]}", EXIT_UNREADABLE) + try: + d = json.loads(p.stdout) + except ValueError: + raise StepError(f"記録の PR #{n} の出力を読めない", EXIT_UNREADABLE) + comments = [c or {} for c in d.get("comments") or []] + parts = [(d.get("body") or "", None)] + for c in comments: + m = _COMMENT_ID.search(c.get("url") or "") + parts.append((c.get("body") or "", int(m.group(1)) if m else None)) + starts, line = [], 0 + for text, cid in parts: + starts.append((line, cid)) + line += len(text.split("\n")) + return "\n".join(t for t, _ in parts), starts + + +def _read_sprint_pr(root, repo, n): + """スプリントの PR の題と本文を 1 回で読む。""" + p = gh_call.gh(["pr", "view", str(n), "--repo", repo, "--json", "title,body"], cwd=root) + if p.returncode != 0: + raise StepError(f"スプリントの PR #{n} を読めない: {p.stderr.strip()[:300]}", EXIT_UNREADABLE) + try: + d = json.loads(p.stdout) + except ValueError: + raise StepError(f"スプリントの PR #{n} の出力を読めない", EXIT_UNREADABLE) + return d.get("title") or "", d.get("body") or "" + + +def sprint_issues(root, repo, prs): + """(closing keywords が指す課題, 題と本文が参照する記録のリポジトリの番号) を返す。 + + closing keywords は本文だけから読む(GitHub は題の closing keywords で課題を閉じない)。""" + seen, out, refs = set(), [], [] + for n in prs: + title, body = _read_sprint_pr(root, repo, n) + refs += [r for r in referenced_numbers(title + "\n" + body, repo) if r not in refs] + for key in closing_refs(body, repo): + if key not in seen: + seen.add(key) + out.append(key) + return out, refs + + +NOT_GIVEN = "closing keywords(GitHub が課題を自動で閉じるキーワード Closes / Fixes / Resolves)も --issues も無い参照" + + +def referenced_items(root, repo, refs, issues): + """参照だけの課題の行。1 番号につき状態と URL を 1 回読み、Pull Request と閉じた課題は行にしない。""" + items = [] + for n in refs: + if (repo, n) in issues: + continue + p = gh_call.gh(["issue", "view", str(n), "--repo", repo, "--json", "state,url", "-q", "[.state, .url] | @tsv"], cwd=root) + state, _, url = p.stdout.strip().partition("\t") + base = {"kind": "issue", "repo": repo, "number": n, "result": "kept_open"} + if p.returncode != 0 or not state: + items.append({**base, "reason": f"状態を読めない({NOT_GIVEN})"}) + elif "/pull/" not in url and state != "CLOSED": + items.append({**base, "reason": NOT_GIVEN}) + return items diff --git a/plugins/ndf/scripts/sprint_close_lib/final_stage.py b/plugins/ndf/scripts/sprint_close_lib/final_stage.py new file mode 100644 index 000000000..e5e274684 --- /dev/null +++ b/plugins/ndf/scripts/sprint_close_lib/final_stage.py @@ -0,0 +1,41 @@ +"""閉じる条件 3(最終工程の記録。#2228)の判定(`sprint-close.py` から分けた)。 + +配布とリリース後テストの記録は条件 1・2 が見るため、ここは振り返りの記録だけを判定する。出す(emit)のは呼び手である。 +""" + +from __future__ import annotations + +import re + +UNVERIFIED_FINAL = "閉じているが閉じる条件 3 を満たさない: " +STAGE_DIST, STAGE_VERIFY, STAGE_RETRO = "配布", "リリース後テスト", "振り返り" +FINAL_STAGES = (STAGE_DIST, STAGE_VERIFY, STAGE_RETRO) +NO_BASE = 0 # --record-pr 0 の比べる基準(配布の記録が無いため、どの識別子も数える) +_RETRO_LINE = re.compile(r"^振り返り:\s*https://github\.com/[\w.-]+/[\w.-]+/(?:issues|pull)/\d+#issuecomment-(\d+)\s*$", re.M) +FINAL_GAP = "最終工程({stage})の記録が無い: {what}" +FINAL_GAP_LEAD = "最終工程(" + + +def final_stage_gap(stage, body, since): + """最終工程の記録が無ければ、何が無いかを含む理由を返す。あれば None。 + + 配布とリリース後テストの記録は条件 1・2 が見るため、ここでは振り返りだけを判定する。振り返りは本文の + `振り返り: <コメントの URL>` の行のうち、識別子が since(記録の PR の最後の配布の記録のコメントの識別子)以上の + ものを数える。since が None なら比べる基準が無いため満たさない。""" + if stage != STAGE_RETRO: + return None + if since is None: + return FINAL_GAP.format(stage=stage, what="比べる配布の記録のコメントが無い") + ids = [int(x) for x in _RETRO_LINE.findall(body or "")] + if not ids: + return FINAL_GAP.format(stage=stage, what="課題の本文に振り返りの記録の行が無い") + if max(ids) < since: + return FINAL_GAP.format(stage=stage, what="課題の本文の振り返りの行が今回の配布の記録より前のもの") + return None + + +def gap_item(base, state, gap): + """条件 3 を満たさない課題の項目。閉じていれば開き直さない。""" + if state == "CLOSED": + return {**base, "result": "closed_unverified", "reason": UNVERIFIED_FINAL + gap} + return {**base, "result": "kept_open", "reason": gap} diff --git a/plugins/ndf/scripts/supervise_lib/pr.py b/plugins/ndf/scripts/supervise_lib/pr.py index 41a6d6f2f..6e8140ad6 100644 --- a/plugins/ndf/scripts/supervise_lib/pr.py +++ b/plugins/ndf/scripts/supervise_lib/pr.py @@ -13,6 +13,7 @@ import mdtable from pr_mode import with_mode_line from release_lib.others import MIGRATION_HEADING +from repo import defer_for_base from supervise_lib.claude import TAIL from supervise_lib.paths import DECISIONS_SH from supervise_lib.pr_materials import CHANGES_HEADING, Materials, gather_materials @@ -317,10 +318,32 @@ def _llm_body(self, ctx, step: dict, body: str, changes: str, issues: str, mats: return body def _publish(self, ctx, base: str, branch: str, title: str, body: str, retitle: bool = False) -> tuple[bool, str]: - """既存の PR があれば本文を書き直し(retitle なら題も。#1289 の決定 6)、無ければ Draft で作る。""" - found = gh_call.gh( - ["pr", "list", "--head", branch, "--state", "open", "--json", "url", "--jq", ".[0].url"], cwd=ctx.cwd - ).stdout.rstrip() + """既存の PR があれば本文を書き直し(retitle なら題も。#1289 の決定 6)、無ければ Draft で作る。 + + 宛先(既存の PR なら baseRefName、無ければ base)が既定ブランチなら、本文の closing keywords を閉じる予定の参照へ + 置き換えてから送る(マージで課題を閉じない。#2228 の I7)。""" + found, _, found_base = ( + gh_call.gh( + [ + "pr", + "list", + "--head", + branch, + "--state", + "open", + "--json", + "url,baseRefName", + "--jq", + ".[0] | [.url, .baseRefName] | @tsv", + ], + cwd=ctx.cwd, + ) + .stdout.strip() + .partition("\t") + ) + body, deferred = defer_for_base(ctx.cwd, (found_base or None) if found else base, body) + if deferred: + ctx.state.cur["deferred_closing"] = deferred if found: p = gh_call.gh(["pr", "edit", found, *(["--title", title] if retitle else []), "--body", body], cwd=ctx.cwd) url = found diff --git a/plugins/ndf/scripts/supervise_lib/release_templates.py b/plugins/ndf/scripts/supervise_lib/release_templates.py index ed22eba17..464b9fd2e 100644 --- a/plugins/ndf/scripts/supervise_lib/release_templates.py +++ b/plugins/ndf/scripts/supervise_lib/release_templates.py @@ -236,7 +236,7 @@ def judge_record_step() -> dict: def release_verify_step(sprint: str, refs: str, record_pr: str) -> dict: - """リリース後テスト(/ndf:release-verification)。記録を配布の記録の PR へ置き、まとめの close がそれを読む(#1683)。""" + """リリース後テスト(/ndf:release-verification)。記録を配布の記録の PR へ置き、振り返りの後のまとめの close がそれを読む(#1683)。""" return { "id": "release-verify", "type": "work", @@ -245,9 +245,11 @@ def release_verify_step(sprint: str, refs: str, record_pr: str) -> dict: "stage": "リリース後テスト", "timeout": 3600, "prompt": f"/ndf:release-verification スプリント {sprint}({refs})。配布の記録は Pull Request #{record_pr} にある。" - "リリース後テストの記録をその Pull Request へコメントで書く。課題は閉じない(次の close が閉じる)。" + "受け入れ条件ごとに確かめ、スプリントの Pull Request の「手動確認」でリリース後テストへ回した項目と、" + "マージ前に未検証として残した項目も行として載せて、リリース後テストの記録をその Pull Request へコメントで書く。" + "課題は閉じない(次の close が閉じる)。" "番号が 0 なら本番へ出ていないため、何もせずに終える。", - "next": "close", + "next": "retro", } @@ -259,7 +261,9 @@ def _release_verify_steps(a, v: str, dev: bool, repo: str | None, ref: str, rts: { "id": "verify", "type": "run", - "stage": "配布" if dev else "リリース後テスト", + # 導入の確認だけを流すため、本番でも「リリース後テスト」と記録しない。受け入れ条件ごとの確認と + # 配布の記録の PR への記録は、まとめの release-verify(release_verify_step)が担う(#2288) + "stage": "配布", "timeout": 1500, "cwd": repo, "cmd": f"sh -c 'git pull -q --ff-only origin {a.base} && {VERIFY_PY} verify-install --ref {ref} --expect {v} --runtimes {rts}'", diff --git a/plugins/ndf/scripts/supervise_lib/sprint.py b/plugins/ndf/scripts/supervise_lib/sprint.py index c9ef44671..6fa66f4de 100644 --- a/plugins/ndf/scripts/supervise_lib/sprint.py +++ b/plugins/ndf/scripts/supervise_lib/sprint.py @@ -48,7 +48,7 @@ route_waves, ) from supervise_lib.verify_steps import merge_steps -from supervise_lib.sprint_close_steps import _close_step, _retro_refine_steps, _spec_pr_steps +from supervise_lib.sprint_close_steps import _close_step, _refine_step, _retro_step, _spec_pr_steps def prod_version(version: str) -> str: @@ -139,7 +139,7 @@ def auto_sprint_plans(a) -> list[dict]: def close_plan(a, repo: str) -> dict: - """まとめ: 確定仕様化 → Pull Request →(配布の記録があれば)リリース後テスト → 課題を閉じる → 振り返り → 棚卸し。""" + """まとめ: 確定仕様化 → Pull Request →(配布の記録があれば)リリース後テスト → 振り返り → 課題を閉じる → 棚卸し。""" issues = ",".join(map(str, a.issue)) refs = " ".join(f"#{i}" for i in a.issue) branch = f"spec/{a.name}" @@ -158,10 +158,11 @@ def close_plan(a, repo: str) -> dict: "進め方": "auto" if getattr(a, "pace", None) == "auto" else "fast", "steps": [ *_spec_pr_steps(a, refs), - *merge_steps(a, next="release-verify" if has_record(record_pr) else "close"), + *merge_steps(a, next="release-verify" if has_record(record_pr) else "retro"), *([release_verify_step(a.name, refs, record_pr)] if has_record(record_pr) else []), + _retro_step(a, refs, stats), _close_step(a, repo, record_pr, issues), - *_retro_refine_steps(a, refs, stats), + _refine_step(a, refs), ], }, a, diff --git a/plugins/ndf/scripts/supervise_lib/sprint_close_steps.py b/plugins/ndf/scripts/supervise_lib/sprint_close_steps.py index 591c14435..b744baa38 100644 --- a/plugins/ndf/scripts/supervise_lib/sprint_close_steps.py +++ b/plugins/ndf/scripts/supervise_lib/sprint_close_steps.py @@ -1,4 +1,7 @@ -"""new close のまとめのステップ(確定仕様化 → Pull Request・課題を閉じる・振り返りと棚卸し)。`sprint.close_plan` が組む。""" +"""new close のまとめのステップ(確定仕様化 → Pull Request・振り返り → 課題を閉じる → 棚卸し)。`sprint.close_plan` が組む。 + +閉じる時点の決まりの正本は `skills/progress-tracking/SKILL.md` の「スプリントを閉じる」。閉じてよいかは sprint-close.py が +判定し、プランは並び(振り返りの後に close)と `--final-stage` の値だけを持つ(#2228 の I6)。""" from __future__ import annotations @@ -36,7 +39,7 @@ def _spec_pr_steps(a, refs: str) -> list[dict]: def _close_step(a, repo: str, record_pr: str, issues: str) -> dict: - """課題を閉じる(sprint-close.py)。""" + """課題を閉じる(sprint-close.py)。プランはどのモードでも retro を通すため、最終工程は振り返りを明示して渡す。""" return { "id": "close", "type": "run", @@ -44,8 +47,8 @@ def _close_step(a, repo: str, record_pr: str, issues: str) -> dict: "cwd": repo, "timeout": 900, "cmd": f"python3 {HERE / 'sprint-close.py'} --record-pr {record_pr} --issues {issues} " - f"--with-verification --label {shlex.quote(f'スプリント {a.name}の後片付け')}", - "next": "retro", + f"--final-stage 振り返り --with-verification --label {shlex.quote(f'スプリント {a.name}の後片付け')}", + "next": "refine", } @@ -58,34 +61,36 @@ def carry_text(a) -> str: return f"前の周回の持ち越し {nums} を `upkeep.py` の `--add` で候補へ渡す({' '.join(map(str, carry))})。" -def _retro_refine_steps(a, refs: str, stats: str) -> list[dict]: - """振り返りと棚卸し。""" - return [ - { - "id": "retro", - "type": "work", - "full": True, - "kind": "振り返り", - "stage": "振り返り", - "timeout": 3600, - "prompt": f"/ndf:retrospective スプリント {a.name}({refs})。材料に検査の記録の集計(`{stats}` の出力: " - "トリガーが立った回数・検査ごとの指摘の件数・検査の後に逃げた不具合の件数)を使い、閾値の" - "見直しが要るかを書く。", - "next": "refine", - }, - { - "id": "refine", - "type": "work", - "full": True, - "kind": "棚卸し", - "stage": "棚卸し", - "timeout": 3600, - "prompt": f"/ndf:backlog-refinement スプリント {a.name}({refs})。棚卸しの工程として無人で通す。{carry_text(a)}" - "候補が上限を超えて持ち越し(`metrics.deferred`)が出ても、`--limit` を上げて打ち直さず、番号と件数を" - "報告に載せる。`upkeep.py apply` が終了コード 10 を返したら、承認を求めず、承認資料のパス" - "(`presentation_path`)と対象の番号を報告に載せて終える。`--repo` を渡さない(記録のリポジトリの外へ" - "書かない)。親 issue の起票と子 issue の結び付けは行わず、クラスタと提案する親 issue の内容(題・子の" - "番号)を報告の「人の判断待ち」に載せて終える。", - "next": "end", - }, - ] +def _retro_step(a, refs: str, stats: str) -> dict: + """振り返り。課題は閉じない(次の close が閉じる)。""" + return { + "id": "retro", + "type": "work", + "full": True, + "kind": "振り返り", + "stage": "振り返り", + "timeout": 3600, + "prompt": f"/ndf:retrospective スプリント {a.name}({refs})。材料に検査の記録の集計(`{stats}` の出力: " + "トリガーが立った回数・検査ごとの指摘の件数・検査の後に逃げた不具合の件数)を使い、閾値の" + "見直しが要るかを書く。課題は閉じない(次の close が閉じる)。", + "next": "close", + } + + +def _refine_step(a, refs: str) -> dict: + """棚卸し。""" + return { + "id": "refine", + "type": "work", + "full": True, + "kind": "棚卸し", + "stage": "棚卸し", + "timeout": 3600, + "prompt": f"/ndf:backlog-refinement スプリント {a.name}({refs})。棚卸しの工程として無人で通す。{carry_text(a)}" + "候補が上限を超えて持ち越し(`metrics.deferred`)が出ても、`--limit` を上げて打ち直さず、番号と件数を" + "報告に載せる。`upkeep.py apply` が終了コード 10 を返したら、承認を求めず、承認資料のパス" + "(`presentation_path`)と対象の番号を報告に載せて終える。`--repo` を渡さない(記録のリポジトリの外へ" + "書かない)。親 issue の起票と子 issue の結び付けは行わず、クラスタと提案する親 issue の内容(題・子の" + "番号)を報告の「人の判断待ち」に載せて終える。", + "next": "end", + } diff --git a/plugins/ndf/scripts/supervise_lib/sprint_routes.py b/plugins/ndf/scripts/supervise_lib/sprint_routes.py index b809e4bc0..374fe6896 100644 --- a/plugins/ndf/scripts/supervise_lib/sprint_routes.py +++ b/plugins/ndf/scripts/supervise_lib/sprint_routes.py @@ -57,7 +57,7 @@ def has_release_template(a) -> bool: return any(r.stage == delivery.STAGE_TEMPLATE for r in routes_of(a)) -RECORD_NONE = "0" # 本番の記録なし(配布なし。sprint-close.py は閉じる条件を見ずに閉じる。#1336) +RECORD_NONE = "0" # 本番の記録なし(配布なし。sprint-close.py は閉じる条件 1・2 を見ない。#1336) RECORD_UNKNOWN = "unknown" # 記録の置き場が分からない(sprint-close.py は課題を開いたまま残す。#1875) diff --git a/plugins/ndf/scripts/supervise_lib/templates.py b/plugins/ndf/scripts/supervise_lib/templates.py index 195d6c034..1cd9e6960 100644 --- a/plugins/ndf/scripts/supervise_lib/templates.py +++ b/plugins/ndf/scripts/supervise_lib/templates.py @@ -268,9 +268,8 @@ def _inspect_steps(a, pr_ref: str, scope: str) -> list[dict]: "stage": "実装レビュー", "timeout": 3600, "args": f"{pr_ref} --max-rounds 4", + # 収束しなかった終わり(終了コード 6 と gaps)も落ちたとして止まり、後続の検査へ進めない(#2143) "on_fail": "abort", - # 収束しなかった終わり(終了コード 6 と gaps)は見送りとして材料へ載せて進む(#2227 の決定 1) - "on_exit": {"6": "base-sync"}, "next": "base-sync", # 全体テストの前にベースブランチを取り込む(#1917) }, ] diff --git a/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/check.json b/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/check.json index 5aa0713a2..634954216 100644 --- a/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/check.json +++ b/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/check.json @@ -41,9 +41,6 @@ "timeout": 3600, "args": "5 --max-rounds 4", "on_fail": "abort", - "on_exit": { - "6": "base-sync" - }, "next": "base-sync" }, { diff --git a/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/close/1-check.json b/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/close/1-check.json index 6588bdf4e..d2a81c28c 100644 --- a/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/close/1-check.json +++ b/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/close/1-check.json @@ -68,9 +68,6 @@ "timeout": 3600, "args": "{pr} --max-rounds 4", "on_fail": "abort", - "on_exit": { - "6": "base-sync" - }, "next": "base-sync" }, { diff --git a/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/close/3-release-prod.json b/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/close/3-release-prod.json index e4a59670a..b817949ba 100644 --- a/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/close/3-release-prod.json +++ b/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/close/3-release-prod.json @@ -95,7 +95,7 @@ { "id": "verify", "type": "run", - "stage": "リリース後テスト", + "stage": "配布", "timeout": 1500, "cwd": "/repo", "cmd": "sh -c 'git pull -q --ff-only origin main && python3 /scripts/release-verification-steps.py verify-install --ref main --expect 1.2.0 --runtimes claude'", diff --git a/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/close/4-close.json b/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/close/4-close.json index ef1922b9a..21fc67a68 100644 --- a/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/close/4-close.json +++ b/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/close/4-close.json @@ -79,16 +79,7 @@ "kind": "リリース後テスト", "stage": "リリース後テスト", "timeout": 3600, - "prompt": "/ndf:release-verification スプリント m9(#15)。配布の記録は Pull Request #{queue_pr:release-prod} にある。リリース後テストの記録をその Pull Request へコメントで書く。課題は閉じない(次の close が閉じる)。番号が 0 なら本番へ出ていないため、何もせずに終える。", - "next": "close" - }, - { - "id": "close", - "type": "run", - "stage": "後片付け", - "cwd": "/repo", - "timeout": 900, - "cmd": "python3 /scripts/sprint-close.py --record-pr {queue_pr:release-prod} --issues 15 --with-verification --label 'スプリント m9の後片付け'", + "prompt": "/ndf:release-verification スプリント m9(#15)。配布の記録は Pull Request #{queue_pr:release-prod} にある。受け入れ条件ごとに確かめ、スプリントの Pull Request の「手動確認」でリリース後テストへ回した項目と、マージ前に未検証として残した項目も行として載せて、リリース後テストの記録をその Pull Request へコメントで書く。課題は閉じない(次の close が閉じる)。番号が 0 なら本番へ出ていないため、何もせずに終える。", "next": "retro" }, { @@ -98,7 +89,16 @@ "kind": "振り返り", "stage": "振り返り", "timeout": 3600, - "prompt": "/ndf:retrospective スプリント m9(#15)。材料に検査の記録の集計(`python3 /scripts/check-trigger.py stats --root /repo` の出力: トリガーが立った回数・検査ごとの指摘の件数・検査の後に逃げた不具合の件数)を使い、閾値の見直しが要るかを書く。", + "prompt": "/ndf:retrospective スプリント m9(#15)。材料に検査の記録の集計(`python3 /scripts/check-trigger.py stats --root /repo` の出力: トリガーが立った回数・検査ごとの指摘の件数・検査の後に逃げた不具合の件数)を使い、閾値の見直しが要るかを書く。課題は閉じない(次の close が閉じる)。", + "next": "close" + }, + { + "id": "close", + "type": "run", + "stage": "後片付け", + "cwd": "/repo", + "timeout": 900, + "cmd": "python3 /scripts/sprint-close.py --record-pr {queue_pr:release-prod} --issues 15 --final-stage 振り返り --with-verification --label 'スプリント m9の後片付け'", "next": "refine" }, { diff --git a/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/sprint/10-release-prod.json b/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/sprint/10-release-prod.json index 09c8fa976..801c31119 100644 --- a/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/sprint/10-release-prod.json +++ b/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/sprint/10-release-prod.json @@ -90,7 +90,7 @@ { "id": "verify", "type": "run", - "stage": "リリース後テスト", + "stage": "配布", "timeout": 1500, "cwd": "/repo", "cmd": "sh -c 'git pull -q --ff-only origin main && python3 /scripts/release-verification-steps.py verify-install --ref main --expect 1.2.0 --runtimes claude'", diff --git a/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/sprint/7-check.json b/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/sprint/7-check.json index 4205d0846..7ee5b040c 100644 --- a/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/sprint/7-check.json +++ b/plugins/ndf/scripts/tests/fixtures/supervise-contract/plans/sprint/7-check.json @@ -69,9 +69,6 @@ "timeout": 3600, "args": "{pr} --max-rounds 4", "on_fail": "abort", - "on_exit": { - "6": "base-sync" - }, "next": "base-sync" }, { diff --git a/plugins/ndf/scripts/tests/sprint_args.py b/plugins/ndf/scripts/tests/sprint_args.py new file mode 100644 index 000000000..2c155d5c0 --- /dev/null +++ b/plugins/ndf/scripts/tests/sprint_args.py @@ -0,0 +1,25 @@ +"""supervise.py の `new sprint` を起動する引数列の共通の土台。 + +既定の名前・課題・版数を持ち、テストごとの差分(名前・課題・設計・版数と、末尾に足すフラグ)だけを受ける。 +""" + +from __future__ import annotations + +from pathlib import Path + + +def new_sprint_args( + worktree: Path | str, + out: Path | str, + *extra: str, + name: str = "m", + issue: tuple[str, ...] = ("1",), + design: tuple[str, ...] = (), + version: str | None = "10.18.0-dev.1", +) -> list[str]: + args = ["new", "sprint", "--name", name, "--worktree", str(worktree), "--issue", *issue] + if design: + args += ["--design", *design] + if version is not None: + args += ["--version", version] + return [*args, "--out", str(out), *extra] diff --git a/plugins/ndf/scripts/tests/test_acceptance_checks.py b/plugins/ndf/scripts/tests/test_acceptance_checks.py new file mode 100644 index 000000000..8831fdbef --- /dev/null +++ b/plugins/ndf/scripts/tests/test_acceptance_checks.py @@ -0,0 +1,216 @@ +"""受け入れ条件の確認の区分(#2144)。完了判定が `acceptance-check.py record` で本文へ書き、承認資料 +(release_lib/materials.read_unverified)と sprint-close.py の閉じる条件 2 が同じ記録を読む。 + +本文の読み書き(gh_rest.fetch_body / update_body)は代役に差し替え、課題ごとの錠は一時の置き場で取る。 +""" + +from __future__ import annotations + +import importlib.util +import json +import sys +from pathlib import Path + +import pytest + +HERE = Path(__file__).resolve().parent +SCRIPTS = HERE.parent +sys.path.insert(0, str(SCRIPTS / "lib")) +sys.path.insert(0, str(SCRIPTS)) +import deps # noqa: E402 + +deps.require("md", "mdtable", "locks") +import acceptance # noqa: E402 +from release_lib import materials # noqa: E402 + + +def _load(name: str, file: str): + spec = importlib.util.spec_from_file_location(name, SCRIPTS / file) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +check = _load("acceptance_check", "acceptance-check.py") +sprint_close = _load("sprint_close_for_checks", "sprint-close.py") + +BODY = "# 課題\n\n## 受け入れ条件\n\n- [ ] 1. 一\n- [ ] 2. 二\n- [x] 3. 三\n- [ ] 4. 四\n\n## 進行\n\nモード: standard\n" + + +@pytest.fixture +def issue(monkeypatch, tmp_path): + """課題 o/r#7 の本文の代役。bodies["o/r#7"] を読み書きする。""" + monkeypatch.setenv("NDF_LOCK_DIR", str(tmp_path / "locks")) + store = {"o/r#7": BODY, "writes": 0} + + def fetch(repo, n): + return store.get(f"{repo}#{n}") + + def update(repo, n, body): + store[f"{repo}#{n}"] = body + store["writes"] += 1 + return True + + monkeypatch.setattr(check.gh_rest, "fetch_body", fetch) + monkeypatch.setattr(check.gh_rest, "update_body", update) + return store + + +def run(capsys, *argv): + with pytest.raises(SystemExit) as e: + check.main(list(argv)) + return e.value.code, json.loads(capsys.readouterr().out.strip().splitlines()[-1]) + + +# --- 記録を書く・読む(受け入れ条件 1) ------------------------------------------- + + +def test_record_writes_a_table_that_read_check_kinds_reads(issue, capsys): + code, out = run( + capsys, "record", "--issue", "7", "--repo", "o/r", "--pre-merge", "1=pytest tests/test_x.py::test_a", "--post-release", "4" + ) + assert code == 0 and out["status"] == "ok" + body = issue["o/r#7"] + assert acceptance.read_check_kinds(body) == { + 1: acceptance.Check("マージ前", "pytest tests/test_x.py::test_a"), + 4: acceptance.Check("リリース後", ""), + } + assert [(i["name"], i["result"]) for i in out["items"]] == [("1", "マージ前"), ("4", "リリース後")] + assert acceptance.criteria(body) == acceptance.criteria(BODY) # 受け入れ条件の節は変えない + + +def test_record_again_overrides_and_keeps_the_rest(issue, capsys): + run(capsys, "record", "--issue", "7", "--repo", "o/r", "--pre-merge", "1=a", "--post-release", "2") + code, _ = run(capsys, "record", "--issue", "7", "--repo", "o/r", "--pre-merge", "2=b") + assert code == 0 + assert acceptance.read_check_kinds(issue["o/r#7"]) == {1: acceptance.Check("マージ前", "a"), 2: acceptance.Check("マージ前", "b")} + writes = issue["writes"] + code, out = run(capsys, "record", "--issue", "7", "--repo", "o/r", "--pre-merge", "2=b") + assert code == 0 and "変更なし" in out["summary"] and issue["writes"] == writes + + +@pytest.mark.parametrize( + "args", + [ + ["--pre-merge", "9=a"], # 受け入れ条件に無い番号 + ["--pre-merge", "1"], # 手段が無い + ["--pre-merge", "1=a", "--post-release", "1"], # 両方 + [], # 何も渡さない + ], +) +def test_record_rejects_bad_arguments_without_writing(issue, capsys, args): + code, out = run(capsys, "record", "--issue", "7", "--repo", "o/r", *args) + assert code == 2 and out["status"] == "stopped" + assert issue["o/r#7"] == BODY + + +def test_record_without_criteria_stops(issue, capsys): + issue["o/r#7"] = "# 課題\n\n本文だけ\n" + code, _ = run(capsys, "record", "--issue", "7", "--repo", "o/r", "--post-release", "1") + assert code == 3 + + +def test_show_reads_the_record(issue, capsys): + run(capsys, "record", "--issue", "7", "--repo", "o/r", "--pre-merge", "2=`ruff check .`") + code, out = run(capsys, "show", "--issue", "7", "--repo", "o/r") + assert code == 0 and out["items"] == [{"kind": "criterion", "name": "2", "result": "マージ前", "means": "`ruff check .`"}] + + +def test_read_check_kinds_skips_rows_without_means_or_kind(): + body = ( + BODY + + "\n## 受け入れ条件の確認\n\n| 条件 | 区分 | 手段 |\n| --- | --- | --- |\n| 1 | マージ前 | — |\n| 2 | 済み | x |\n| 4 | リリース後 | — |\n" + ) + assert acceptance.read_check_kinds(body) == {4: acceptance.Check("リリース後", "")} + assert acceptance.read_check_kinds(BODY) == {} + + +# --- 承認資料(受け入れ条件 2・3) --------------------------------------------------- + + +def _recorded(pre, post): + return check.recorded_body(BODY, pre, post) + + +def test_unverified_leaves_out_pre_merge_and_keeps_post_release(): + body = _recorded({1: "pytest -k a", 2: "pytest -k b"}, [4]) + got = materials.read_unverified({("o/r", 7): body}, {}, "o/r") + assert [u.number for u in got] == [4] + + +def test_unverified_without_a_record_lists_every_unchecked_criterion(): + got = materials.read_unverified({("o/r", 7): BODY}, {}, "o/r") + assert [u.number for u in got] == [1, 2, 4] + + +# --- 閉じる条件 2(受け入れ条件 4) -------------------------------------------------- + + +def _judge(body, rows): + return sprint_close.judge_acceptance(rows, acceptance.criteria(body), acceptance.pre_merge_numbers(body)) + + +def test_pre_merge_criteria_need_no_verification_row(): + body = _recorded({1: "pytest -k a", 2: "pytest -k b"}, [4]) + ok, why, to_check = _judge(body, acceptance.IssueRows(True, frozenset({3, 4}))) + assert ok and why is None and to_check == [1, 2, 4] + + +def test_post_release_criteria_still_need_a_row(): + body = _recorded({1: "pytest -k a"}, [2, 4]) + ok, why, _ = _judge(body, acceptance.IssueRows(True, frozenset({4}))) + assert not ok and "2" in why + + +def test_all_pre_merge_closes_without_rows_and_unrecorded_still_needs_rows(): + body = _recorded({1: "a", 2: "b", 3: "c", 4: "d"}, []) + assert _judge(body, None)[0] + assert _judge(BODY, None) == (False, sprint_close.NO_ROWS, []) + + +# --- 版ごとのリリース後テストの表(version_verdicts の現状固定) ------------------------ + + +def _verify(ver, rows, verdict="合格"): + table = "| 課題 | 受け入れ条件 | 結果 |\n| --- | --- | --- |\n" + "".join(f"| {i} | {c} | {r} |\n" for i, c, r in rows) + return f"## リリース後テスト\n\n対象の版: {ver}\n\n{table}\n合否: {verdict}\n" + + +def test_version_verdicts_reads_the_block_whose_base_version_matches(): + text = _verify("1.0.1-dev.1", [("#7", "2. 二", "合格"), ("x/y#8", "1. 一", "不合格")]) + assert acceptance.version_verdicts(text, "v1.0.1", "o/r") == { + ("o/r", 7): acceptance.IssueRows(True, frozenset({2})), + ("x/y", 8): acceptance.IssueRows(False, frozenset()), + } + + +def test_version_verdicts_uses_the_last_matching_block_and_ignores_other_versions(): + text = "\n".join( + [ + _verify("1.0.1", [("#7", "1. 一", "不合格")]), + _verify("1.0.1(注記)", [("#7", "1. 一", "合格")]), + _verify("1.0.2", [("#9", "1. 一", "合格")]), + ] + ) + assert acceptance.version_verdicts(text, "1.0.1", "o/r") == {("o/r", 7): acceptance.IssueRows(True, frozenset({1}))} + + +@pytest.mark.parametrize( + "text, version", + [ + ("", "1.0.1"), + ("本文だけ\n", "1.0.1"), + (_verify("1.0.2", [("#7", "1. 一", "合格")]), "1.0.1"), + (_verify("main c427284", [("#7", "1. 一", "合格")]), "1.0.1"), + (_verify("1.0.1", [("#7", "1. 一", "合格")]), "main"), + (_verify("1.0.1", [("—", "導入", "合格")]), "1.0.1"), + ("## リリース後テスト\n\n対象の版: 1.0.1\n\n| 課題 | 結果 |\n| --- | --- |\n| #7 | 合格 |\n", "1.0.1"), + ], +) +def test_version_verdicts_returns_empty_when_nothing_matches(text, version): + assert acceptance.version_verdicts(text, version, "o/r") == {} + + +def test_version_verdicts_skips_a_block_without_a_version_line(): + text = "## リリース後テスト\n\n| 課題 | 結果 |\n| --- | --- |\n| #7 | 合格 |\n\n合否: 合格\n" + assert acceptance.version_verdicts(text, "1.0.1", "o/r") == {} diff --git a/plugins/ndf/scripts/tests/test_closing.py b/plugins/ndf/scripts/tests/test_closing.py index 5e8a517cb..3b769c9fb 100644 --- a/plugins/ndf/scripts/tests/test_closing.py +++ b/plugins/ndf/scripts/tests/test_closing.py @@ -34,3 +34,49 @@ def test_reference_forms(text, expected): def test_order_of_appearance_without_duplicates(): assert referenced_numbers("#3 https://github.com/o/r/issues/1 #3 Fixes #2 #1", "o/r") == [3, 1, 2] + + +# --- 閉じる予定の参照(#2228) -------------------------------------------------------- + +import os # noqa: E402 +import subprocess # noqa: E402 + +from closing import DEFERRED, closing_issues, closing_numbers, closing_refs, closing_words, defer_closing # noqa: E402 + +SH = Path(__file__).resolve().parents[1] / "lib" / "closing-issues.sh" +BODY = ( + "## 閉じる課題\n\nCloses #12\nfixes: o/r#3, #4\nResolves https://github.com/o/r/issues/9\n" + "最終工程の後に閉じる:#20\nRefs #30\nclosed #12\n" +) + + +def test_defer_closing_keeps_the_targets_and_leaves_no_closing_keywords(): + new, targets = defer_closing(BODY) + assert targets == ["#12", "o/r#3", "https://github.com/o/r/issues/9", "#12"] + assert closing_words(new) == [] + assert closing_issues(new) == closing_issues(BODY) + assert f"{DEFERRED}: o/r#3" in new and "Refs #30" in new + + +def test_the_deferred_form_is_read_as_a_closing_target_in_order(): + text = "最終工程の後に閉じる: #20\nCloses #12\n最終工程の後に閉じる : https://github.com/o/r/issues/9" + assert closing_issues(text) == ["#20", "#12", "https://github.com/o/r/issues/9"] + assert closing_numbers(text) == [20, 12, 9] + assert closing_refs(text, "a/b") == [("a/b", 20), ("a/b", 12), ("o/r", 9)] + assert closing_words(text) == ["Closes #12"] + + +def test_the_deferred_form_needs_the_colon(): + assert closing_issues("最終工程の後に閉じる #20") == [] + + +@pytest.mark.parametrize("locale", ["C", "C.UTF-8"]) +def test_closing_issues_sh_reads_the_same_targets_as_closing_refs(locale): + """2 つの読み手(closing-issues.sh と closing.closing_refs)が同じ本文から同じ指し先を返す。""" + for text in (BODY, defer_closing(BODY)[0]): + p = subprocess.run( + ["bash", str(SH), "--repo", "a/b"], input=text, capture_output=True, text=True, env={**os.environ, "LC_ALL": locale} + ) + assert p.returncode == 0, p.stderr + got = [(r, int(n)) for r, n in (ln.split("\t") for ln in p.stdout.splitlines() if ln)] + assert got == closing_refs(text, "a/b") diff --git a/plugins/ndf/scripts/tests/test_hook_deadline.py b/plugins/ndf/scripts/tests/test_hook_deadline.py index 72a83155f..877532aaf 100644 --- a/plugins/ndf/scripts/tests/test_hook_deadline.py +++ b/plugins/ndf/scripts/tests/test_hook_deadline.py @@ -9,7 +9,6 @@ import json import os -import re import subprocess import sys import time @@ -130,15 +129,8 @@ def walk(node, under_pre: bool) -> None: return found -@pytest.mark.parametrize("rel", ["hooks/claude.json", "hooks/codex.json", "dev.agy/hooks.json"], ids=["claude", "codex", "agy"]) +@pytest.mark.parametrize("rel", ["hooks/claude.json", "hooks/codex.json"], ids=["claude", "codex"]) def test_the_hook_limit_leaves_the_margin(rel): limits = _hook_timeouts(PLUGIN / rel) assert limits assert all(deadline.DEADLINE_SECONDS + deadline.MARGIN_SECONDS <= t for t in limits) - - -def test_the_kiro_prompt_hook_limit_leaves_the_margin(): - text = (PLUGIN / "dev.kiro" / "install.sh").read_text(encoding="utf-8") - m = re.search(r'hooks\["userPromptSubmit"\]\s*=\s*\[.*?guard_script.*?"timeout_ms":\s*(\d+)', text, re.S) - assert m - assert deadline.DEADLINE_SECONDS + deadline.MARGIN_SECONDS <= int(m.group(1)) / 1000 diff --git a/plugins/ndf/scripts/tests/test_pr_steps.py b/plugins/ndf/scripts/tests/test_pr_steps.py index eca922d8c..0e68ccb7d 100644 --- a/plugins/ndf/scripts/tests/test_pr_steps.py +++ b/plugins/ndf/scripts/tests/test_pr_steps.py @@ -514,3 +514,71 @@ def test_plan_sprint_base_summary_names_the_sprint_review(repo, env): code, out, err = call(["plan", "--base", "sprint/m1"], env, repo) assert code == 0, err assert out["summary"].endswith("新しい PR を作る(スプリントブランチ宛て。実装レビューはスプリントの PR で通す)") + + +# --- 既定ブランチ宛の本文の closing keywords(#2228・#2082) ------------------------- + + +def set_default(repo, name): + """origin の HEAD(既定ブランチ)を name にする。name が origin に無ければ develop から作る。""" + if name != "develop": + git(repo, "push", "-q", "origin", f"develop:{name}") + git(repo, "fetch", "-q", "origin") + git(repo, "remote", "set-head", "origin", name) + + +def sent_body(env): + return Path(env["FAKE_GH_LOG"] + ".body").read_text(encoding="utf-8") + + +def test_create_to_the_default_branch_defers_the_closing_keywords(repo, env, tmp_path): + sys.path.insert(0, str(SCRIPTS / "lib")) + from closing import closing_words + + set_default(repo, "develop") # 起点と既定ブランチが同じ構成 + text = "## Summary\n\nCloses #858\nfixes: o/r#3\nResolves https://github.com/o/r/issues/9\n" + code, out, err = call(["create", "--title", "題", "--body-file", str(body_file(tmp_path, text))], env, repo) + assert code == 0, err + sent = sent_body(env) + assert closing_words(sent) == [] + assert "最終工程の後に閉じる: #858" in sent and "最終工程の後に閉じる: o/r#3" in sent + m = out["metrics"] + assert m["deferred_closing"] == ["#858", "o/r#3", "https://github.com/o/r/issues/9"] + assert m["closing_issues_in_body"] == m["deferred_closing"] + assert [i["name"] for i in out["items"] if i["kind"] == "closing" and i["result"] == "deferred"] == m["deferred_closing"] + assert out["status"] == "ok" + + +def test_update_of_a_pr_to_the_default_branch_defers_the_closing_keywords(repo, env, tmp_path): + set_default(repo, "main") + env["FAKE_GH_PRS"] = json.dumps( + {"feature/x": [{"number": 7, "url": "https://github.com/o/r/pull/7", "isDraft": False, "baseRefName": "main"}]} + ) + code, out, err = call(["update", "--body-file", str(body_file(tmp_path))], env, repo) + assert code == 0, err + assert "Closes" not in sent_body(env) and out["metrics"]["deferred_closing"] == ["#858"] + + +def test_update_by_number_reads_the_base_of_that_pr(repo, env, tmp_path): + set_default(repo, "main") + env["FAKE_GH_VIEW"] = json.dumps({"7": {"baseRefName": "main"}}) + code, out, err = call(["update", "--pr", "7", "--body-file", str(body_file(tmp_path))], env, repo) + assert code == 0, err + assert out["metrics"]["deferred_closing"] == ["#858"] + + +def test_create_without_a_known_default_branch_defers_the_closing_keywords(repo, env, tmp_path): + code, out, err = call(["create", "--title", "題", "--body-file", str(body_file(tmp_path))], env, repo) + assert code == 0, err + assert "Closes #858" not in sent_body(env) and out["metrics"]["deferred_closing"] == ["#858"] + + +@pytest.mark.parametrize("existing", [False, True]) +def test_a_pr_to_another_branch_keeps_the_closing_keywords(repo, env, tmp_path, existing): + set_default(repo, "main") # 起点 develop ≠ 既定ブランチ main(このリポジトリの構成) + if existing: + env["FAKE_GH_PRS"] = json.dumps({"feature/x": [{"number": 7, "url": "u", "isDraft": False, "baseRefName": "develop"}]}) + code, out, err = call(["create", "--title", "題", "--body-file", str(body_file(tmp_path))], env, repo) + assert code == 0, err + assert "Closes #858" in sent_body(env) + assert out["metrics"]["deferred_closing"] == [] and out["metrics"]["closing_issues_in_body"] == ["#858"] diff --git a/plugins/ndf/scripts/tests/test_progress_record_stages.py b/plugins/ndf/scripts/tests/test_progress_record_stages.py index 0c90eb47c..ba0daf7ee 100644 --- a/plugins/ndf/scripts/tests/test_progress_record_stages.py +++ b/plugins/ndf/scripts/tests/test_progress_record_stages.py @@ -23,7 +23,6 @@ SCRIPTS = Path(__file__).resolve().parents[1] SYNC = SCRIPTS / "projects-sync.sh" RECORD = SCRIPTS / "progress-record.sh" -AGY_RECORD = SCRIPTS.parent / "dev.agy" / "scripts" / "progress-record.sh" WF_LIB = SCRIPTS.parent / "skills" / "development-workflow" / "scripts" / "lib" / "workflow-common.sh" STAGE_CHECK = SCRIPTS.parent / "skills" / "development-workflow" / "scripts" / "stage-check.sh" GUARD = SCRIPTS.parent / "skills" / "development-workflow" / "scripts" / "workflow-guard.sh" @@ -340,14 +339,6 @@ def test_the_body_lists_the_stages_of_the_one_stage_table(env: Env) -> None: assert name not in common -def test_the_agy_layout_records_through_the_symlink(env: Env) -> None: - """agy の配置(dev.agy/scripts は symlink)から呼んでも積まれる。""" - assert AGY_RECORD.exists() - out = env.run(str(AGY_RECORD), "42", "設計") - assert out.returncode == 0, out.stderr - assert env.stages(42) == ["設計"] - - @pytest.mark.parametrize("verb", ["view", "edit"]) def test_a_failing_gh_still_records_the_stage_once(env: Env, verb: str) -> None: """gh が失敗しても通過記録は 1 回だけ積まれ、結果は ok 以外(#671)。""" diff --git a/plugins/ndf/scripts/tests/test_release_record.py b/plugins/ndf/scripts/tests/test_release_record.py index c7d1847d0..aa6836cff 100644 --- a/plugins/ndf/scripts/tests/test_release_record.py +++ b/plugins/ndf/scripts/tests/test_release_record.py @@ -212,7 +212,9 @@ def test_sprint_close_closes_with_the_record_the_step_wrote(repo, gh, tmp_path): encoding="utf-8", ) env = {**gh.env, "PATH": f"{bindir}{os.pathsep}{os.environ['PATH']}", "FAKE_GH_STATE": str(state)} - code, out, err = harness.call("sprint-close.py", ["--record-pr", "40", "--repo", "o/r", "--issues", "5", "--dry-run"], env, repo) + code, out, err = harness.call( + "sprint-close.py", ["--final-stage", "配布", "--record-pr", "40", "--repo", "o/r", "--issues", "5", "--dry-run"], env, repo + ) assert code == 0, (out, err) assert harness.issues_of(out)["o/r#5"]["result"] == "would_close" diff --git a/plugins/ndf/scripts/tests/test_sprint_close_acceptance.py b/plugins/ndf/scripts/tests/test_sprint_close_acceptance.py index 8ffb86cd4..2d0ed67fe 100644 --- a/plugins/ndf/scripts/tests/test_sprint_close_acceptance.py +++ b/plugins/ndf/scripts/tests/test_sprint_close_acceptance.py @@ -8,6 +8,7 @@ from __future__ import annotations import importlib.util +import shlex import sys from pathlib import Path @@ -23,7 +24,15 @@ sys.path.insert(0, str(HERE.parent / "lib")) import acceptance # noqa: E402 -CLOSE = ["--record-pr", "20", "--repo", "o/r", "--with-verification"] +CLOSE = [ + "--record-pr", + "20", + "--repo", + "o/r", + "--with-verification", + "--final-stage", + "配布", +] # 条件 2 を見る(条件 3 は test_sprint_close_final_stage.py) THREE = "# 課題\n\n## 受け入れ条件\n\n- [ ] 1. 一\n- [ ] 2. 二\n- [ ] 3. 三\n\n## 進行\n\nモード: standard\n" @@ -96,6 +105,21 @@ def test_a_table_without_issue_rows_keeps_every_issue_open(repo, gh): assert not calls(gh, ["issue", "close"]) and not calls(gh, ["api"]) +def test_a_table_without_issue_rows_closes_an_issue_verified_before_merge(repo, gh): + """導入の確認だけの表でも、すべての条件をマージ前に確かめた課題は閉じる。他は対応の読めない kept_open(#2144)。""" + record = "\n## 受け入れ条件の確認\n\n| 条件 | 区分 | 手段 |\n| --- | --- | --- |\n" + "".join( + f"| {i} | マージ前 | `pytest` |\n" for i in (1, 2, 3) + ) + setup(gh, [("—", "導入の確認", "合格")], {"o/r#1": THREE + record, "o/r#2": THREE}, prs="Fixes #1, fixes #2") + code, out, err = call("sprint-close.py", CLOSE, gh.env, repo) + assert code == 0, (out, err) + got = issues_of(out) + assert got["o/r#1"]["result"] == "closed" and got["o/r#1"]["checked"] == [1, 2, 3] + assert (got["o/r#2"]["result"], got["o/r#2"]["reason"]) == ("kept_open", "条件と課題の対応が読めない") + reads = [c for c in calls(gh, ["issue", "view"]) if "state,body" in c] + assert sorted(c[2] for c in reads) == ["1", "2"] # 本文は課題ごとに 1 回だけ読む + + # --- 閉じる条件 2 と印(受け入れ条件 3〜11) ------------------------------------------ @@ -109,6 +133,16 @@ def test_a_criterion_without_a_passing_row_keeps_the_issue_open(repo, gh): assert gh.get()["issue_bodies"]["o/r#1"] == THREE +def test_a_pre_merge_criterion_needs_no_passing_row(repo, gh): + """本文の `## 受け入れ条件の確認` でマージ前に確かめたと記録した条件 3 は、合格の行が無くても閉じる(#2144)。""" + record = "\n## 受け入れ条件の確認\n\n| 条件 | 区分 | 手段 |\n| --- | --- | --- |\n| 3 | マージ前 | `pytest -k three` |\n" + setup(gh, [("#1", "1. 一", "合格"), ("#1", "2. 二", "合格")], {"o/r#1": THREE + record}) + code, out, err = call("sprint-close.py", CLOSE, gh.env, repo) + it = issues_of(out)["o/r#1"] + assert code == 0, (out, err) + assert it["result"] == "closed" and it["checked"] == [1, 2, 3] + + def test_all_criteria_passed_closes_and_marks_before_closing(repo, gh): """1〜3 がそろうと閉じ、本文の 3 個のチェックボックスに印が付く。書くのは閉じる前(受け入れ条件 4)。""" setup(gh, [("#1", f"{i}. c", "合格") for i in (1, 2, 3)], {"o/r#1": THREE}) @@ -182,7 +216,7 @@ def test_a_failed_write_does_not_close(repo, gh): code, out, err = call("sprint-close.py", CLOSE, gh.env, repo) it = issues_of(out)["o/r#1"] assert code == 1 and it["result"] == "failed" and it["reason"] == "受け入れ条件の印を書けない" - assert it["cmd"].endswith("sprint-close.py " + " ".join(CLOSE)) + assert it["cmd"].endswith("sprint-close.py " + shlex.join(CLOSE)) assert not calls(gh, ["issue", "close"]) @@ -195,7 +229,13 @@ def test_issue_without_checkboxes_is_judged_by_the_rows_only(repo, gh, body): assert not calls(gh, ["api"]) -@pytest.mark.parametrize("args", [["--record-pr", "20", "--repo", "o/r"], ["--record-pr", "0", "--repo", "o/r", "--issues", "1"]]) +@pytest.mark.parametrize( + "args", + [ + ["--record-pr", "20", "--repo", "o/r", "--final-stage", "配布"], + ["--record-pr", "0", "--repo", "o/r", "--issues", "1", "--final-stage", "配布"], + ], +) def test_paths_without_verification_do_not_read_the_body(repo, gh, args): """--with-verification なし・--record-pr 0 は本文を読まず書かない(受け入れ条件 12)。""" setup(gh, [("#1", "1. c", "合格")], {"o/r#1": THREE}) @@ -226,7 +266,7 @@ def test_body_is_read_inside_the_issue_lock_and_a_held_lock_fails(tmp_path, monk """本文の読み書きは課題ごとの錠の中で行う。錠を取れなければ failed(非機能の条件)。""" monkeypatch.setenv("NDF_LOCK_DIR", str(tmp_path)) mod = load_sprint_close() - ctx = mod.CloseContext(str(tmp_path), "o/r", "c") + ctx = mod.CloseContext(str(tmp_path), "o/r", "c", final_stage="配布") seen = [] def read(root, repo_, n): @@ -256,7 +296,9 @@ def test_a_check_that_changes_more_than_the_marks_is_not_written(tmp_path, monke monkeypatch.setattr(mod.acceptance, "check_criteria", lambda body, nums: body.replace("# 課題", "# 変えた")) wrote = [] monkeypatch.setattr(mod.gh_rest, "update_body", lambda *a: wrote.append(a) or True) - it = mod._close_verified(_A(), mod.CloseContext(str(tmp_path), "o/r", "c"), "o/r", 1, mod.IssueRows(True, frozenset({1, 2, 3}))) + it = mod._close_verified( + _A(), mod.CloseContext(str(tmp_path), "o/r", "c", final_stage="配布"), "o/r", 1, mod.IssueRows(True, frozenset({1, 2, 3})) + ) assert it["result"] == "failed" and not wrote diff --git a/plugins/ndf/scripts/tests/test_sprint_close_final_stage.py b/plugins/ndf/scripts/tests/test_sprint_close_final_stage.py new file mode 100644 index 000000000..9f6b5cfcb --- /dev/null +++ b/plugins/ndf/scripts/tests/test_sprint_close_final_stage.py @@ -0,0 +1,166 @@ +"""sprint-close.py の閉じる条件 3(最終工程の記録)と閉じる予定の参照(#2228・#2119・#2082)。 + +gh の偽物は test_sprint_close_merge_green.py のものを使う。記録の PR のコメントは url に `#issuecomment-<識別子>` を持つ。 +""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + +import pytest + +HERE = Path(__file__).resolve().parent +_spec = importlib.util.spec_from_file_location("sprint_close_harness_final", HERE / "test_sprint_close_merge_green.py") +harness = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(harness) +call, issues_of, DIST_PROD, verify_block = harness.call, harness.issues_of, harness.DIST_PROD, harness.verify_block +repo, gh = harness.repo, harness.gh # pytest の fixture + +URL = "https://github.com/o/r/pull/20#issuecomment-{}" + + +def retro(cid): + return f"# 課題\n\n本文\n\n振り返り: https://github.com/o/r/pull/21#issuecomment-{cid}\n" + + +def comment(body, cid): + return {"body": body, "url": URL.format(cid)} + + +def setup(gh, issue_body, *, dist_in_comment=False, comments=None, state="OPEN", prs="Fixes #1"): + """記録の PR 20 と、PR 11 が閉じる課題 #1。dist_in_comment なら配布の記録を識別子 200 のコメントに置く。""" + if comments is None: + comments = [comment(DIST_PROD, 200)] if dist_in_comment else [comment("メモ", 100)] + gh.set( + records={"20": {"body": "記録" if dist_in_comment else DIST_PROD, "comments": comments}}, + bodies={"11": prs, "12": ""}, + issues={"o/r#1": [state]}, + issue_bodies={"o/r#1": issue_body}, + ) + + +def wrote(gh): + st = gh.get() + return "closed" in st or "written" in st or any(c[:2] == ["project", "item-edit"] for c in st["calls"]) + + +@pytest.mark.parametrize("verify", [False, True]) +def test_an_issue_without_the_retro_record_is_kept_open(repo, gh, verify): + comments = [comment("メモ", 100)] + ([comment(verify_block("1.1.0", [("#1", "合格")]), 101)] if verify else []) + setup(gh, "# 課題\n\n本文\n", comments=comments) + args = ["--record-pr", "20", "--repo", "o/r"] + (["--with-verification"] if verify else []) + code, out, err = call("sprint-close.py", args, gh.env, repo) + it = issues_of(out)["o/r#1"] + assert code == 0 and it["result"] == "kept_open" and "振り返り" in it["reason"], err + assert not wrote(gh) + assert out["metrics"]["final_stage"] == "振り返り" + assert out["decisions"][0]["decision"] == "最終工程を 振り返り として判定した" + assert "振り返り" in (out.get("next") or "") + + +def test_a_retro_line_of_a_previous_run_does_not_count(repo, gh): + setup(gh, retro(150), dist_in_comment=True) + code, out, _ = call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r"], gh.env, repo) + it = issues_of(out)["o/r#1"] + assert it["result"] == "kept_open" and "振り返り" in it["reason"] and "前のもの" in it["reason"] + setup(gh, retro(150) + retro(250), dist_in_comment=True) + code, out, _ = call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r"], gh.env, repo) + assert code == 0 and issues_of(out)["o/r#1"]["result"] == "closed" + assert sum(1 for c in gh.get()["calls"] if c[:3] == ["pr", "view", "20"]) == 1 + + +def test_a_record_in_the_body_compares_with_the_first_comment(repo, gh): + setup(gh, retro(100), comments=[comment("振り返りの記録", 100), comment("x", 300)]) + code, out, _ = call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r"], gh.env, repo) + assert code == 0 and issues_of(out)["o/r#1"]["result"] == "closed" + + +def test_a_record_without_comments_has_no_base_and_keeps_open(repo, gh): + setup(gh, retro(100), comments=[]) + code, out, _ = call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r"], gh.env, repo) + it = issues_of(out)["o/r#1"] + assert it["result"] == "kept_open" and "比べる配布の記録のコメントが無い" in it["reason"] + + +def test_a_closed_issue_without_the_record_is_not_reopened(repo, gh): + setup(gh, "本文", state="CLOSED") + code, out, _ = call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r"], gh.env, repo) + it = issues_of(out)["o/r#1"] + assert it["result"] == "closed_unverified" and "振り返り" in it["reason"] + assert not any(c[:2] == ["issue", "reopen"] for c in gh.get()["calls"]) + + +@pytest.mark.parametrize("verify", [False, True]) +def test_an_unreadable_body_fails_and_stops(repo, gh, verify): + comments = [comment("メモ", 100)] + ([comment(verify_block("1.1.0", [("#1", "合格")]), 101)] if verify else []) + setup(gh, retro(100), comments=comments, state=None) + args = ["--record-pr", "20", "--repo", "o/r"] + (["--with-verification"] if verify else []) + code, out, _ = call("sprint-close.py", args, gh.env, repo) + it = issues_of(out)["o/r#1"] + assert code == 1 and out["status"] == "stopped" and it["result"] == "failed" and "最終工程" in it["reason"] + assert it["cmd"].endswith(" ".join(args)) and not wrote(gh) + + +def test_the_retro_record_and_passing_rows_close_and_mark(repo, gh): + body = "# 課題\n\n## 受け入れ条件\n\n- [ ] 1. 一\n\n振り返り: https://github.com/o/r/pull/21#issuecomment-500\n" + rows = "| 課題 | 受け入れ条件 | 実行したこと | 実行時刻 | 結果 |\n| --- | --- | --- | --- | --- |\n| #1 | 1. 一 | x | t | 合格 |\n" + setup(gh, body, comments=[comment(f"## リリース後テスト\n対象の版: 1.1.0(2026-09-24)\n\n{rows}\n合否: 済\n", 101)]) + code, out, err = call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r", "--with-verification"], gh.env, repo) + it = issues_of(out)["o/r#1"] + assert code == 0 and it["result"] == "closed" and it["checked"] == [1], it + assert gh.get()["issue_bodies"]["o/r#1"].count("- [x] 1.") == 1 + + +def test_record_zero_closes_with_a_retro_line_and_keeps_without(repo, gh): + setup(gh, retro(5)) + code, out, _ = call("sprint-close.py", ["--record-pr", "0", "--repo", "o/r", "--issues", "1"], gh.env, repo) + assert code == 0 and issues_of(out)["o/r#1"]["result"] == "closed" + setup(gh, "本文") + code, out, _ = call("sprint-close.py", ["--record-pr", "0", "--repo", "o/r", "--issues", "1"], gh.env, repo) + assert issues_of(out)["o/r#1"]["result"] == "kept_open" + + +def test_record_unknown_does_not_read_the_body(repo, gh): + setup(gh, retro(5)) + code, out, _ = call("sprint-close.py", ["--record-pr", "unknown", "--repo", "o/r", "--issues", "1"], gh.env, repo) + assert code == 0 and issues_of(out)["o/r#1"]["result"] == "kept_open" + assert not any(c[:2] == ["issue", "view"] for c in gh.get().get("calls", [])) + + +def test_the_verification_stage_always_reads_the_rows(repo, gh): + setup(gh, "本文", comments=[comment(verify_block("1.1.0", [("#1", "不合格")]), 101)]) + args = ["--record-pr", "20", "--repo", "o/r", "--final-stage", "リリース後テスト"] + code, out, _ = call("sprint-close.py", args, gh.env, repo) + it = issues_of(out)["o/r#1"] + assert code == 0 and it["result"] == "kept_open" and "合格でない" in it["reason"] + assert out["metrics"]["final_stage"] == "リリース後テスト" + + +def test_the_distribution_stage_closes_without_a_retro_line(repo, gh): + setup(gh, "本文") + code, out, _ = call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r", "--final-stage", "配布"], gh.env, repo) + assert code == 0 and issues_of(out)["o/r#1"]["result"] == "closed" + + +def test_an_unknown_final_stage_is_a_call_error(repo, gh): + setup(gh, "本文") + code, _, _ = call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r", "--final-stage", "棚卸し"], gh.env, repo) + assert code == 3 + + +def test_a_pr_with_only_deferred_references_collects_its_issue(repo, gh): + """既定ブランチ宛で置き換えた本文だけのスプリントでも、課題が閉じる対象になる(#2082 の受け入れ条件 3)。""" + setup(gh, retro(100), prs="## 閉じる課題\n\n最終工程の後に閉じる: #1\n") + code, out, err = call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r", "--prs", "11", "--dry-run"], gh.env, repo) + assert code == 0, err + assert issues_of(out)["o/r#1"]["result"] == "would_close" and out["metrics"]["referenced_only"] == 0 + assert not wrote(gh) + + +def test_final_stage_gap_counts_only_lines_from_the_start(): + mod = harness.load_sprint_close() + body = "- 振り返り: https://github.com/o/r/pull/2#issuecomment-9\n" + assert "行が無い" in mod.final_stage_gap("振り返り", body, 1) + assert mod.final_stage_gap("振り返り", body.lstrip("- "), 9) is None + assert mod.final_stage_gap("配布", "", None) is None diff --git a/plugins/ndf/scripts/tests/test_sprint_close_issues.py b/plugins/ndf/scripts/tests/test_sprint_close_issues.py index 15f59f9ca..9a5014a16 100644 --- a/plugins/ndf/scripts/tests/test_sprint_close_issues.py +++ b/plugins/ndf/scripts/tests/test_sprint_close_issues.py @@ -65,7 +65,12 @@ def get(self): def test_record_zero_closes_the_given_issues_without_reading_a_record(repo, gh): gh.set(issues={"o/r#1": ["OPEN"], "o/r#2": ["CLOSED"]}) - code, out, err = call("sprint-close.py", ["--record-pr", "0", "--issues", "1,2", "--repo", "o/r", "--with-verification"], gh.env, repo) + code, out, err = call( + "sprint-close.py", + ["--final-stage", "配布", "--record-pr", "0", "--issues", "1,2", "--repo", "o/r", "--with-verification"], + gh.env, + repo, + ) assert code == 0, (out, err) res = issues_of(out) assert res["o/r#1"]["result"] == "closed" and res["o/r#2"]["result"] == "already_closed" @@ -73,7 +78,7 @@ def test_record_zero_closes_the_given_issues_without_reading_a_record(repo, gh): def test_record_zero_alone_returns_two(repo, gh): - code, out, err = call("sprint-close.py", ["--record-pr", "0", "--repo", "o/r"], gh.env, repo) + code, out, err = call("sprint-close.py", ["--final-stage", "配布", "--record-pr", "0", "--repo", "o/r"], gh.env, repo) assert code == 2 and out["status"] == "stopped" assert gh.get().get("calls", []) == [] @@ -90,19 +95,19 @@ def test_record_unknown_keeps_the_given_issues_open_with_a_reason(repo, gh): assert gh.get().get("calls", []) == [] # 記録も状態も読まず、閉じもボードも書かない assert out["summary"].startswith("記録の置き場が分からない") and out["metrics"]["closed"] == 0 gh.set(issues={"o/r#1": ["OPEN"], "o/r#2": ["OPEN"]}) - _, zero, _ = call("sprint-close.py", ["--record-pr", "0", "--issues", "1,2", "--repo", "o/r"], gh.env, repo) + _, zero, _ = call("sprint-close.py", ["--final-stage", "配布", "--record-pr", "0", "--issues", "1,2", "--repo", "o/r"], gh.env, repo) assert zero["summary"] != out["summary"] and "置き場" not in zero["summary"] def test_record_unknown_alone_returns_two(repo, gh): - code, out, err = call("sprint-close.py", ["--record-pr", "unknown", "--repo", "o/r"], gh.env, repo) + code, out, err = call("sprint-close.py", ["--final-stage", "配布", "--record-pr", "unknown", "--repo", "o/r"], gh.env, repo) assert code == 2 and out["status"] == "stopped" and "unknown" in out["summary"] assert gh.get().get("calls", []) == [] @pytest.mark.parametrize("value", ["-1", "none"]) def test_record_pr_rejects_other_values(repo, gh, value): - code, _, _ = call("sprint-close.py", ["--record-pr", value, "--issues", "1", "--repo", "o/r"], gh.env, repo) + code, _, _ = call("sprint-close.py", ["--final-stage", "配布", "--record-pr", value, "--issues", "1", "--repo", "o/r"], gh.env, repo) assert code == 3 @@ -112,7 +117,9 @@ def test_issues_are_added_to_the_closing_words_of_the_prs(repo, gh): bodies={"11": "Fixes #1", "12": ""}, issues={"o/r#1": ["OPEN"], "o/r#3": ["OPEN"]}, ) - code, out, err = call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r", "--issues", "3,1"], gh.env, repo) + code, out, err = call( + "sprint-close.py", ["--final-stage", "配布", "--record-pr", "20", "--repo", "o/r", "--issues", "3,1"], gh.env, repo + ) assert code == 0, (out, err) assert sorted(issues_of(out)) == ["o/r#1", "o/r#3"] assert all(i["result"] == "closed" for i in issues_of(out).values()) @@ -123,6 +130,6 @@ def test_issues_alone_do_not_need_the_sprint_prs(repo, gh): records={"20": {"body": "## 配布の記録\n段階: 本番(2026-09-25 承認)\n版: 1.0.0 → 1.1.0\n", "comments": []}}, issues={"o/r#5": ["OPEN"]}, ) - code, out, err = call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r", "--issues", "5"], gh.env, repo) + code, out, err = call("sprint-close.py", ["--final-stage", "配布", "--record-pr", "20", "--repo", "o/r", "--issues", "5"], gh.env, repo) assert code == 0, (out, err) assert issues_of(out)["o/r#5"]["result"] == "closed" diff --git a/plugins/ndf/scripts/tests/test_sprint_close_merge_green.py b/plugins/ndf/scripts/tests/test_sprint_close_merge_green.py index 3e9992a14..dd6ff4e51 100644 --- a/plugins/ndf/scripts/tests/test_sprint_close_merge_green.py +++ b/plugins/ndf/scripts/tests/test_sprint_close_merge_green.py @@ -209,7 +209,9 @@ def test_sprint_close_four_outcomes(repo, gh): bodies={"11": "Fixes #1, closes #2", "12": "Resolves #3 and fixes other/x#4"}, issues={"o/r#1": ["OPEN"], "o/r#2": ["CLOSED"], "o/r#3": ["OPEN"], "other/x#4": [None]}, ) - code, out, err = call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r", "--label", "M26の振り返り"], gh.env, repo) + code, out, err = call( + "sprint-close.py", ["--final-stage", "配布", "--record-pr", "20", "--repo", "o/r", "--label", "M26の振り返り"], gh.env, repo + ) res = issues_of(out) assert res["o/r#1"]["result"] == "closed" and res["o/r#1"]["cmd"] == "gh issue reopen 1 --repo o/r" assert res["o/r#2"]["result"] == "already_closed" @@ -226,7 +228,7 @@ def test_sprint_close_failed_when_close_does_not_take(repo, gh): issues={"o/r#1": ["OPEN"]}, close_works=False, ) - code, out, err = call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r"], gh.env, repo) + code, out, err = call("sprint-close.py", ["--final-stage", "配布", "--record-pr", "20", "--repo", "o/r"], gh.env, repo) it = issues_of(out)["o/r#1"] assert code == 1 and it["result"] == "failed" and it["cmd"] == "gh issue close 1 --repo o/r" @@ -237,7 +239,7 @@ def test_sprint_close_kept_open_before_production(repo, gh): bodies={"11": "Fixes #1"}, issues={"o/r#1": ["OPEN"]}, ) - code, out, err = call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r"], gh.env, repo) + code, out, err = call("sprint-close.py", ["--final-stage", "配布", "--record-pr", "20", "--repo", "o/r"], gh.env, repo) assert code == 0 and out["status"] == "ok" assert issues_of(out)["o/r#1"]["result"] == "kept_open" assert "closed" not in gh.get() @@ -252,7 +254,9 @@ def test_sprint_close_verification_per_issue(repo, gh): bodies={"11": "Fixes #1", "12": "Fixes #2 fixes #3"}, issues={"o/r#1": ["OPEN"], "o/r#2": ["OPEN"], "o/r#3": ["OPEN"]}, ) - code, out, err = call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r", "--with-verification"], gh.env, repo) + code, out, err = call( + "sprint-close.py", ["--final-stage", "配布", "--record-pr", "20", "--repo", "o/r", "--with-verification"], gh.env, repo + ) res = issues_of(out) assert res["o/r#1"]["result"] == "closed" assert res["o/r#2"]["result"] == "kept_open" and "合格でない" in res["o/r#2"]["reason"] @@ -262,13 +266,15 @@ def test_sprint_close_verification_per_issue(repo, gh): def test_sprint_close_no_verification_record_keeps_all(repo, gh): gh.set(records={"20": {"body": DIST_PROD, "comments": []}}, bodies={"11": "Fixes #1", "12": ""}, issues={"o/r#1": ["OPEN"]}) - code, out, err = call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r", "--with-verification"], gh.env, repo) + code, out, err = call( + "sprint-close.py", ["--final-stage", "配布", "--record-pr", "20", "--repo", "o/r", "--with-verification"], gh.env, repo + ) assert code == 0 and issues_of(out)["o/r#1"]["reason"] == "本番の版のリリース後テストの記録が無い" def test_sprint_close_empty_list_is_2(repo, gh): gh.set(records={"20": {"body": "## 配布の記録\n段階: 配布なし(文書だけ)\n", "comments": []}}) - code, out, err = call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r"], gh.env, repo) + code, out, err = call("sprint-close.py", ["--final-stage", "配布", "--record-pr", "20", "--repo", "o/r"], gh.env, repo) assert code == 2 and out["status"] == "stopped" @@ -278,7 +284,9 @@ def test_sprint_close_dry_run_writes_nothing(repo, gh): bodies={"11": "Fixes #1"}, issues={"o/r#1": ["OPEN"]}, ) - code, out, err = call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r", "--prs", "11", "--dry-run"], gh.env, repo) + code, out, err = call( + "sprint-close.py", ["--final-stage", "配布", "--record-pr", "20", "--repo", "o/r", "--prs", "11", "--dry-run"], gh.env, repo + ) assert code == 0 and issues_of(out)["o/r#1"]["result"] == "would_close" assert "closed" not in gh.get() diff --git a/plugins/ndf/scripts/tests/test_sprint_close_references.py b/plugins/ndf/scripts/tests/test_sprint_close_references.py index 5a77e2154..9ff3ddfd4 100644 --- a/plugins/ndf/scripts/tests/test_sprint_close_references.py +++ b/plugins/ndf/scripts/tests/test_sprint_close_references.py @@ -22,7 +22,7 @@ def run(gh, repo, *extra): - return call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r", *extra], gh.env, repo) + return call("sprint-close.py", ["--record-pr", "20", "--repo", "o/r", "--final-stage", "配布", *extra], gh.env, repo) def issue_calls(gh, verb, n=None): diff --git a/plugins/ndf/scripts/tests/test_step_scripts.py b/plugins/ndf/scripts/tests/test_step_scripts.py index 7edb4e4b4..e6d564125 100644 --- a/plugins/ndf/scripts/tests/test_step_scripts.py +++ b/plugins/ndf/scripts/tests/test_step_scripts.py @@ -438,7 +438,7 @@ def load_verification(): @pytest.mark.parametrize("installed", ["symlink", "copy", "differs", "missing"]) def test_compare_files_follows_a_symlink_to_a_directory(tmp_path, installed): - """dev.agy/skills/<名前> → ../../skills/<名前> のようなディレクトリへの symlink を比べられる。""" + """<配置>/<名前> → ../skills/<名前> のようなディレクトリへの symlink を比べられる。""" mod = load_verification() src = tmp_path / "src" / "p" (src / "skills" / "a").mkdir(parents=True) @@ -474,49 +474,6 @@ def test_compare_files_skips_symlinks_for_a_runtime_that_drops_them(tmp_path): assert out == ["codex: p/skills/a/SKILL.md"] -def kiro_project(tmp_path): - """この checkout の dev.kiro/install.sh で /proj へ導入し、(src, proj) を返す。""" - src = SCRIPTS.parents[2] - proj = tmp_path / "proj" - proj.mkdir() - p = subprocess.run( - ["bash", str(src / "plugins/ndf/dev.kiro/install.sh"), "--project", str(proj), "--yes"], - capture_output=True, - text=True, - cwd=str(src), - ) - assert p.returncode == 0, p.stdout + p.stderr - return src, proj - - -def test_compare_kiro_accepts_what_the_installer_made(tmp_path): - src, proj = kiro_project(tmp_path) - assert load_verification().compare_kiro(src, proj) == [] - - -@pytest.mark.parametrize("broken", ["skill_link", "prompt", "steering", "agent", "policy_link"]) -def test_compare_kiro_reports_missing_or_changed_outputs(tmp_path, broken): - """Kiro の生成物(skills / prompts / steering / agents)の欠落・内容違いを不一致にする。""" - src, proj = kiro_project(tmp_path) - kiro = proj / ".kiro" - if broken == "skill_link": - (kiro / "skills" / "pr").unlink() - want = "kiro: .kiro/skills/pr" - elif broken == "prompt": - (kiro / "prompts" / "pr.md").write_text("changed\n", encoding="utf-8") - want = "kiro: .kiro/prompts/pr.md" - elif broken == "steering": - (kiro / "steering" / "ndf-policies.md").write_text("stale\n", encoding="utf-8") - want = "kiro: .kiro/steering/ndf-policies.md" - elif broken == "agent": - (kiro / "agents" / "ndf.json").write_text("{", encoding="utf-8") - want = "kiro: .kiro/agents/ndf.json" - else: - (kiro / "skills" / "ndf-policies").symlink_to(src / "plugins/ndf/skills/ndf-policies") - want = "kiro: .kiro/skills/ndf-policies が残っている(steering へ移した Skill)" - assert load_verification().compare_kiro(src, proj) == [want] - - # --- phase-steps.py(互換の入口) -------------------------------------------- diff --git a/plugins/ndf/scripts/tests/test_supervise.py b/plugins/ndf/scripts/tests/test_supervise.py index 6e7b408a2..2b27cdc23 100644 --- a/plugins/ndf/scripts/tests/test_supervise.py +++ b/plugins/ndf/scripts/tests/test_supervise.py @@ -32,6 +32,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent)) from account_fake import FakeAnthropic, accounts, window # noqa: E402,F401 +from sprint_args import new_sprint_args # noqa: E402 FAKE_GH = """#!{py} import json, sys @@ -328,6 +329,15 @@ def test_an_unconverged_review_stops_the_plan(tmp_path, monkeypatch, final, ran) steps[0]["on_exit"] = {"6": "after"} s, text = run_plan(tmp_path, steps) assert [e["id"] for e in s.state.log] == ["review", "after"], text + # 検査のプランの review(templates の雛形そのもの)も、収束しなければ後続の base-sync へ進まない + from types import SimpleNamespace + + from supervise_lib.templates import _inspect_steps + + review = next(s for s in _inspect_steps(SimpleNamespace(), "1", "x") if s["id"] == "review") + review = {**review, "drive": "fake", "args": "", "next": "after"} + s, text = run_plan(tmp_path, [review, *steps[1:]]) + assert [e["id"] for e in s.state.log] == ran, text def _skipping_drive(tmp_path, monkeypatch): @@ -723,22 +733,7 @@ def test_new_check_skips_refactor_for_modes_without_it(tmp_path, mode): def test_new_sprint_check_for_light_goes_from_pr_to_review(tmp_path): out = tmp_path / "m" - p = cli( - "new", - "sprint", - "--name", - "m", - "--worktree", - str(tmp_path), - "--issue", - "1", - "--mode", - "light", - "--version", - "10.18.0-dev.1", - "--out", - str(out), - ) + p = cli(*new_sprint_args(tmp_path, out, "--mode", "light")) assert p.returncode == 0, p.stderr waves = {w["name"]: w for w in json.loads((out / "sprint.json").read_text())["ステージ"]} steps = {s["id"]: s for s in json.loads(Path(waves["検査"]["plans"][0]).read_text())["steps"]} @@ -1519,6 +1514,7 @@ def test_new_release_dev_and_prod(tmp_path): assert st["facts"]["presentation_to"] == "issues/approval-ndf-v10.17.11.md" assert st["facts"]["gate_next"] == "explain" and st["verify"]["cwd"] == "/r" assert "--ref develop" in st["verify"]["cmd"] and "--channel dev" in st["release"]["cmd"] + assert st["verify"]["stage"] == "配布" # 開発版の導入の確認は「配布」(#2288 の AC2) p = cli( "new", @@ -1552,7 +1548,9 @@ def test_new_release_dev_and_prod(tmp_path): "fix", "judge-record", ] - assert "--ref main" in st["verify"]["cmd"] and st["verify"]["stage"] == "リリース後テスト" + # 導入の確認だけのステップは「リリース後テスト」と記録しない。それは仕上げの release-verify が担う(#2288 の AC1) + assert "--ref main" in st["verify"]["cmd"] and st["verify"]["stage"] == "配布" + assert not [s for s in plan["steps"] if s.get("stage") == "リリース後テスト"] assert st["verify"]["next"] == "cleanup" and "facts" not in st # 先頭の approve が開発版の facts の出力から承認したコミットを読む(#1426 の AC9) assert "approve --approval issues/approval-ndf-v10.17.11.md --facts-from '{queue_state?:release}' --by user" in st["approve"]["cmd"] @@ -1580,23 +1578,7 @@ def test_new_release_requires_version(): def test_new_sprint_writes_waves_in_order(tmp_path): out = tmp_path / "m" - p = cli( - "new", - "sprint", - "--name", - "v10-18", - "--worktree", - str(tmp_path), - "--issue", - "11", - "12", - "--design", - "11", - "--version", - "10.18.0-dev.1", - "--out", - str(out), - ) + p = cli(*new_sprint_args(tmp_path, out, name="v10-18", issue=("11", "12"), design=("11",))) assert p.returncode == 0, p.stderr res = json.loads(p.stdout) assert res["status"] == "ok" @@ -1686,7 +1668,7 @@ def test_new_sprint_writes_waves_in_order(tmp_path): def test_new_sprint_without_design_skips_gate(tmp_path): out = tmp_path / "m" - p = cli("new", "sprint", "--name", "m", "--worktree", str(tmp_path), "--issue", "1", "--version", "10.18.0-dev.1", "--out", str(out)) + p = cli(*new_sprint_args(tmp_path, out)) assert p.returncode == 0, p.stderr names = [w["name"] for w in json.loads((out / "sprint.json").read_text())["ステージ"]] assert names == ["スプリントブランチ", "実装", "検査", "配布", "関門 2", "本番"] @@ -2531,10 +2513,7 @@ def test_new_check_builds_the_refactor_scope_from_the_pr_files(tmp_path): def test_new_sprint_check_hands_the_declared_tests_and_scope_to_the_same_builder(tmp_path): out = tmp_path / "m" - p = cli( - "new", "sprint", "--name", "m", "--worktree", str(tmp_path), "--issue", "1", "--version", "10.18.0-dev.1", - "--tests", "tests/cli", "--scope", "lib/a", "--out", str(out), - ) # fmt: skip + p = cli(*new_sprint_args(tmp_path, out, "--tests", "tests/cli", "--scope", "lib/a")) assert p.returncode == 0, p.stderr check = next(out.glob("*-check.json")) refactor = next(s for s in json.loads(check.read_text())["steps"] if s["id"] == "refactor") @@ -2561,7 +2540,7 @@ def test_refactor_step_stops_with_the_scope_exit_code_before_starting_the_drive( def test_new_sprint_check_and_release_run_without_a_whole_skill(tmp_path): out = tmp_path / "m" - p = cli("new", "sprint", "--name", "m", "--worktree", str(tmp_path), "--issue", "1", "--version", "10.18.0-dev.1", "--out", str(out)) + p = cli(*new_sprint_args(tmp_path, out)) assert p.returncode == 0, p.stderr waves = {w["name"]: w for w in json.loads((out / "sprint.json").read_text())["ステージ"]} for name in ("検査", "配布"): @@ -2578,7 +2557,7 @@ def test_new_sprint_check_and_release_run_without_a_whole_skill(tmp_path): def test_normal_sprint_puts_gate_2_and_prod_after_the_dev_release(tmp_path): """normal の雛形の経路は配布の後に関門 2 と本番を置き、本番は承認の後に単独で流す(#1914)。""" out = tmp_path / "m" - p = cli("new", "sprint", "--name", "m", "--worktree", str(tmp_path), "--issue", "1", "--version", "10.18.0-dev.1", "--out", str(out)) + p = cli(*new_sprint_args(tmp_path, out)) assert p.returncode == 0, p.stderr index = json.loads((out / "sprint.json").read_text())["ステージ"] assert [w["name"] for w in index][-3:] == ["配布", "関門 2", "本番"] @@ -2601,7 +2580,7 @@ def test_prod_plan_queued_alone_takes_the_check_pr(tmp_path): from supervise_lib import paths, queue out = tmp_path / "m" - p = cli("new", "sprint", "--name", "m", "--worktree", str(tmp_path), "--issue", "1", "--version", "10.18.0-dev.1", "--out", str(out)) + p = cli(*new_sprint_args(tmp_path, out)) assert p.returncode == 0, p.stderr waves = {w["name"]: w for w in json.loads((out / "sprint.json").read_text())["ステージ"]} check, prod = waves["検査"]["plans"][0], waves["本番"]["plans"][0] diff --git a/plugins/ndf/scripts/tests/test_supervise_new.py b/plugins/ndf/scripts/tests/test_supervise_new.py index 108b3317b..8fd5da3ed 100644 --- a/plugins/ndf/scripts/tests/test_supervise_new.py +++ b/plugins/ndf/scripts/tests/test_supervise_new.py @@ -20,6 +20,9 @@ sys.path.insert(0, str(SCRIPTS)) from supervise_lib import commands, engine, pr as pr_step # noqa: E402 +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from sprint_args import new_sprint_args # noqa: E402 + # new sprint は --design に無い課題の本文を gh で読む(#1767)。見本の本文で答える pytestmark = pytest.mark.usefixtures("issue_bodies") @@ -46,25 +49,7 @@ def plain_repo(tmp_path, release=None): def new_sprint(root, out, *extra): - return cli( - "new", - "sprint", - "--name", - "m6", - "--worktree", - str(root), - "--issue", - "216", - "247", - "--design", - "216", - "--version", - "3.8.0-dev.1", - "--out", - str(out), - *extra, - cwd=root, - ) + return cli(*new_sprint_args(root, out, *extra, name="m6", issue=("216", "247"), design=("216",), version="3.8.0-dev.1"), cwd=root) # --- #1192: リリースの雛形の無い形 ------------------------------------------------- @@ -148,7 +133,7 @@ def test_new_close_without_release_form_puts_manual_stage(tmp_path, delivery): def test_new_sprint_and_close_with_package_plugin_still_need_versions(tmp_path): """#1336 の I6: 雛形で組む経路だけが版数を要する。""" root = plain_repo(tmp_path, {"form": "package-plugin", "plugin": "foo", "runtimes": ["claude"]}) - p = cli("new", "sprint", "--name", "m6", "--worktree", str(root), "--issue", "216", "--out", str(tmp_path / "m"), cwd=root) + p = cli(*new_sprint_args(root, tmp_path / "m", name="m6", issue=("216",), version=None), cwd=root) assert p.returncode == 2 and "--version" in p.stderr p = close_cli(root, tmp_path / "c", "--version", "3.8.0-dev.1") assert p.returncode == 2 and "--prod" in p.stderr and "--version" not in p.stderr.split("無い:")[-1] @@ -202,7 +187,7 @@ def test_samples_start_and_close_a_sprint_without_versions(tmp_path, name): (root / ".ndf" / "project.json").write_text(json.dumps({"delivery": rows}, ensure_ascii=False)) (root / ".ndf" / "pace.json").write_text(json.dumps({"version": 1, "fast": {"enabled": True, "verify": "true"}})) out = tmp_path / "m" - p = cli("new", "sprint", "--name", "m6", "--worktree", str(root), "--issue", "216", "--out", str(out), cwd=root) + p = cli(*new_sprint_args(root, out, name="m6", issue=("216",), version=None), cwd=root) assert p.returncode == 0, p.stderr manifest = json.loads((out / "sprint.json").read_text()) assert [(r["route"], r["stage"]) for r in manifest["リリースの経路"]] == expected diff --git a/plugins/ndf/scripts/tests/test_supervise_pace.py b/plugins/ndf/scripts/tests/test_supervise_pace.py index e27bc1f90..f6fec9272 100644 --- a/plugins/ndf/scripts/tests/test_supervise_pace.py +++ b/plugins/ndf/scripts/tests/test_supervise_pace.py @@ -25,6 +25,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent)) from pace_state import sprint_state # noqa: E402 +from sprint_args import new_sprint_args # noqa: E402 # new sprint は --design に無い課題の本文を gh で読む(#1767)。見本の本文で答える @@ -57,26 +58,7 @@ def assert_transitions_exist(plan: dict) -> None: def new_fast_sprint(tmp_path, state, *extra): out = tmp_path / "m" p = cli( - "new", - "sprint", - "--name", - "m26", - "--worktree", - str(tmp_path), - "--issue", - "11", - "12", - "--design", - "11", - "--version", - "10.18.0-dev.1", - "--pace", - "fast", - "--state", - str(state), - "--out", - str(out), - *extra, + *new_sprint_args(tmp_path, out, "--pace", "fast", "--state", str(state), *extra, name="m26", issue=("11", "12"), design=("11",)) ) return p, out @@ -167,25 +149,7 @@ def test_fast_sprint_is_refused_without_the_declaration(tmp_path): (repo / ".ndf" / "worktree.json").write_text('{"version": 1, "base_branch": "develop", "production_branch": "main"}') (repo / ".ndf" / "supervise.json").write_text((REPO / ".ndf" / "supervise.json").read_text()) out = tmp_path / "m" - p = cli( - "new", - "sprint", - "--name", - "m", - "--worktree", - str(repo), - "--issue", - "1", - "--version", - "1.0.0-dev.1", - "--pace", - "fast", - "--state", - str(sprint_state(tmp_path, "fast")), - "--out", - str(out), - cwd=repo, - ) + p = cli(*new_sprint_args(repo, out, "--pace", "fast", "--state", str(sprint_state(tmp_path, "fast")), version="1.0.0-dev.1"), cwd=repo) assert p.returncode == 1 and "pace.json" in json.loads(p.stdout)["summary"] @@ -291,7 +255,7 @@ def test_check_since_last_with_pr_is_a_usage_error(tmp_path): # ---------- new close ---------- -def test_close_runs_spec_close_and_retro_once_each_in_order(tmp_path): +def test_close_runs_spec_retro_and_close_once_each_in_order(tmp_path): out = tmp_path / "close" p = cli( "new", @@ -325,19 +289,21 @@ def test_close_runs_spec_close_and_retro_once_each_in_order(tmp_path): stages = [s.get("stage") for s in close["steps"]] for stage in ("確定仕様化", "後片付け", "振り返り"): assert stages.count(stage) == 1 - assert stages.index("確定仕様化") < stages.index("後片付け") < stages.index("振り返り") + assert stages.index("確定仕様化") < stages.index("振り返り") < stages.index("後片付け") # 振り返りの後に閉じる(#2228) cl = steps_of(close)["close"]["cmd"] assert "--record-pr {queue_pr:release-prod}" in cl and "--issues 11,12" in cl and "--with-verification" in cl + assert "--final-stage 振り返り" in cl assert close["課題"] == [11, 12] and close.get("記録") cmd = waves["最終の検査"]["command"] assert cmd.count("--then") == 3 assert_transitions_exist(close) # 棚卸しは振り返りの後の別のステップ(#842)。振り返りのプロンプトは棚卸しを含まない steps = steps_of(close) - assert steps["retro"]["next"] == "refine" and "backlog-refinement" not in steps["retro"]["prompt"] + assert steps["retro"]["next"] == "close" and steps["close"]["next"] == "refine" + assert "backlog-refinement" not in steps["retro"]["prompt"] refine = steps["refine"] assert refine["stage"] == "棚卸し" and refine["type"] == "work" and refine["timeout"] > 0 - assert refine["next"] == "end" and stages.index("振り返り") < stages.index("棚卸し") + assert refine["next"] == "end" and stages.index("後片付け") < stages.index("棚卸し") # 無人で通す: 上限を上げて打ち直さない・終了コード 10 で承認を求めない・記録のリポジトリの外へ書かない for words in ("--limit` を上げて打ち直さず", "承認を求めず", "`--repo` を渡さない", "親 issue の起票"): assert words in refine["prompt"] @@ -504,7 +470,7 @@ def test_queue_pr_placeholder_reads_a_sibling_plan_when_queued_alone(tmp_path): @pytest.mark.parametrize("alone", [True, False]) def test_normal_sprint_release_fills_the_check_pr_alone_or_after_check(tmp_path, monkeypatch, alone): out = tmp_path / "m" - p = cli("new", "sprint", "--name", "m", "--worktree", str(tmp_path), "--issue", "1", "--version", "10.18.0-dev.1", "--out", str(out)) + p = cli(*new_sprint_args(tmp_path, out)) assert p.returncode == 0, p.stderr waves = {w["name"]: w for w in load(out / "sprint.json")["ステージ"]} check, release = waves["検査"]["plans"][0], waves["配布"]["plans"][0] diff --git a/plugins/ndf/scripts/tests/test_supervise_pace_verify.py b/plugins/ndf/scripts/tests/test_supervise_pace_verify.py index 678f7cc7a..05c15e83d 100644 --- a/plugins/ndf/scripts/tests/test_supervise_pace_verify.py +++ b/plugins/ndf/scripts/tests/test_supervise_pace_verify.py @@ -301,9 +301,11 @@ def test_close_of_the_promote_route_reads_the_promote_pr(tmp_path): ids = [s["id"] for s in steps] close = next(s for s in steps if s["id"] == "close") assert "--record-pr {queue_pr:promote} " in close["cmd"] - assert ids.index("release-verify") == ids.index("close") - 1 + assert ids.index("release-verify") < ids.index("retro") < ids.index("close") rv = steps[ids.index("release-verify")] assert rv["type"] == "work" and rv["prompt"].startswith("/ndf:release-verification") and "{queue_pr:promote}" in rv["prompt"] + # 「リリース後テスト」と記録するのは、受け入れ条件ごとに確かめて記録を書くこのステップだけ(#2288 の AC1) + assert [s["id"] for s in steps if s.get("stage") == "リリース後テスト"] == ["release-verify"] PRD_ON_BASE = {**PRD, "trigger": "develop へのマージ", "branch": "develop"} # ベースブランチへのマージで本番へ届く @@ -312,7 +314,7 @@ def test_close_of_the_promote_route_reads_the_promote_pr(tmp_path): @pytest.mark.parametrize("rows", [[STG], [PRD_ON_BASE], [API], [STG, API]], ids=["stg-only", "merged", "manual", "stg-manual"]) def test_close_without_a_known_record_keeps_the_issues_open(tmp_path, rows): """#1875 1875-1・1875-2: 雛形も昇格も無い届ける経路(merge・手で届ける・検証の環境だけ)は --record-pr unknown で、 - リリース後テストを置かず merge の次が close。""" + リリース後テストを置かず merge の次が振り返り、その次が close。""" steps = close_steps(tmp_path, rows) close = next(s for s in steps if s["id"] == "close") assert "--record-pr unknown " in close["cmd"] and "release-verify" not in [s["id"] for s in steps] @@ -324,3 +326,47 @@ def test_close_of_no_delivery_keeps_record_pr_zero(tmp_path): steps = close_steps(tmp_path, []) close = next(s for s in steps if s["id"] == "close") assert "--record-pr 0 " in close["cmd"] and "release-verify" not in [s["id"] for s in steps] + + +def chain(steps) -> list[str]: + """merge から `next` をたどった id の並び(end まで)。""" + by = {s["id"]: s for s in steps} + out, cur = [], "merge" + while cur in by and cur not in out: + out.append(cur) + cur = by[cur].get("next") + return out + + +@pytest.mark.parametrize( + "rows, want", + [ + ([STG, PRD], ["merge", "release-verify", "retro", "close", "refine"]), + ([], ["merge", "retro", "close", "refine"]), + ([STG], ["merge", "retro", "close", "refine"]), + ], + ids=["record", "no-delivery", "unknown"], +) +def test_close_comes_after_the_retro_and_before_the_refine(tmp_path, rows, want): + """#2228 の受け入れ条件 5・6(I6): next のつながりで リリース後テスト → 振り返り → 課題を閉じる → 棚卸し。 + 振り返りとリリース後テストの指示は課題を閉じない。close は最終工程を振り返りとして渡す。""" + steps = close_steps(tmp_path, rows) + assert chain(steps)[-len(want) :] == want and chain(steps)[0] == "merge" + by = {s["id"]: s for s in steps} + assert "--final-stage 振り返り" in by["close"]["cmd"] + for sid in ("retro", "release-verify"): + if sid in by: + assert "課題は閉じない" in by[sid]["prompt"] + + +@pytest.mark.parametrize("mode", ["light", "operation", "documentation", "legacy-refactor"]) +def test_close_comes_after_the_retro_in_every_mode(tmp_path, mode): + """#2228 の受け入れ条件 6: どのモードで組んでも close は振り返りの後、棚卸しの前にある。""" + repo = make_repo(tmp_path, []) + out = tmp_path / "c" + args = ["new", "close", "--name", "m2", "--worktree", str(repo), "--issue", "11", "--mode", mode] + args += ["--state", str(sprint_state(tmp_path, "auto")), "--out", str(out)] + p = subprocess.run([PY, str(SUPERVISE), *args], capture_output=True, text=True, cwd=repo) + assert p.returncode == 0, p.stdout + p.stderr + close = next(load(f) for f in sorted(out.glob("*.json")) if load(f).get("フェーズ") == "まとめ") + assert chain(close["steps"])[-3:] == ["retro", "close", "refine"] diff --git a/plugins/ndf/scripts/tests/test_supervise_pr_closing.py b/plugins/ndf/scripts/tests/test_supervise_pr_closing.py new file mode 100644 index 000000000..c3db43b60 --- /dev/null +++ b/plugins/ndf/scripts/tests/test_supervise_pr_closing.py @@ -0,0 +1,73 @@ +"""supervise_lib/pr.py の _publish: 既定ブランチ宛の本文の closing keywords を閉じる予定の参照へ置き換える(#2228 の I7・I8)。""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest + +SCRIPTS = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SCRIPTS / "lib")) +sys.path.insert(0, str(SCRIPTS)) + +import gh_call # noqa: E402 +from closing import closing_words # noqa: E402 +from supervise_lib.pr import PrStep # noqa: E402 + +BODY = "## 閉じる課題\n\nCloses #12\nCloses #13\n" + + +def git(cwd, *args): + subprocess.run(["git", "-C", str(cwd), *args], capture_output=True, text=True, check=True) + + +@pytest.fixture +def repo(tmp_path): + """origin の HEAD が main の作業場所(develop も origin にある)。""" + origin = tmp_path / "origin.git" + subprocess.run(["git", "init", "-q", "--bare", "-b", "main", str(origin)], check=True) + root = tmp_path / "wt" + root.mkdir() + git(root, "init", "-q", "-b", "main") + git(root, "-c", "user.email=t@example.com", "-c", "user.name=t", "commit", "-q", "--allow-empty", "-m", "init") + git(root, "remote", "add", "origin", str(origin)) + git(root, "push", "-q", "origin", "main", "main:develop") + git(root, "fetch", "-q", "origin") + git(root, "remote", "set-head", "origin", "main") + return root + + +def publish(monkeypatch, repo, base, found=""): + calls = [] + + def fake(args, stdin=None, cwd=None): + calls.append(args) + out = found if args[:2] == ["pr", "list"] else ("https://github.com/o/r/pull/5\n" if args[:2] == ["pr", "create"] else "") + return gh_call.GhResult(0, out, "") + + monkeypatch.setattr(gh_call, "gh", fake) + ctx = SimpleNamespace(cwd=str(repo), plan={}, state=SimpleNamespace(cur={})) + ok, _ = PrStep()._publish(ctx, base, "feat/x", "題", BODY) + assert ok + sent = next(c for c in calls if c[:2] in (["pr", "create"], ["pr", "edit"])) + return sent[sent.index("--body") + 1], ctx.state.cur + + +def test_a_new_pr_to_the_default_branch_gets_deferred_references(monkeypatch, repo): + body, cur = publish(monkeypatch, repo, "main") + assert closing_words(body) == [] and "最終工程の後に閉じる: #13" in body + assert cur["deferred_closing"] == ["#12", "#13"] + + +def test_an_existing_pr_is_judged_by_its_own_base(monkeypatch, repo): + body, _ = publish(monkeypatch, repo, "develop", found="https://github.com/o/r/pull/5\tmain\n") + assert closing_words(body) == [] + + +@pytest.mark.parametrize("base, found", [("develop", ""), ("main", "https://github.com/o/r/pull/5\tdevelop\n")]) +def test_a_pr_to_another_branch_keeps_the_closing_keywords(monkeypatch, repo, base, found): + body, cur = publish(monkeypatch, repo, base, found=found) + assert closing_words(body) == ["Closes #12", "Closes #13"] and "deferred_closing" not in cur diff --git a/plugins/ndf/skills/AUTHORING.md b/plugins/ndf/skills/AUTHORING.md index b95157cd7..271b520ab 100644 --- a/plugins/ndf/skills/AUTHORING.md +++ b/plugins/ndf/skills/AUTHORING.md @@ -1,7 +1,8 @@ # Skill 執筆規約 -`plugins/ndf/skills/` は 4 ランタイム(Claude Code / Codex / Kiro / agy)へ配布する Skill の -編集元である。ここでは frontmatter の書き方を規約として定める。本文の書き方は各 `SKILL.md` +`plugins/ndf/skills/` は Claude Code と Codex へ配布する Skill の編集元である。Kiro と agy へは配らないが、 +cross-refactoring が担当の worktree へ写す手順書(`refactoring` など)を相手役の Kiro・agy が Skill として読むため、 +下の表の Kiro・agy の列(可搬性の規則)は残す。ここでは frontmatter の書き方を規約として定める。本文の書き方は各 `SKILL.md` に委ねるが、**本文のうち、実行先のリポジトリに依存する部分は規約が扱う**(「対象リポジトリを 仮定しない」)。手順の中身が正しいかと、その手順が ai-plugins でしか成立しないかは別の問題で、 後者は Skill を書く人ごとに判断が分かれるためである。 @@ -33,7 +34,7 @@ python3 scripts/check-skill-repo-assumptions.py --report # 走査の規模と ## `description` を単一の真実とする -4 ランタイムに共通する土台は Agent Skills 仕様の 6 項目(`name` / `description` / `license` / +読む 4 ランタイムに共通する土台は Agent Skills 仕様の 6 項目(`name` / `description` / `license` / `compatibility` / `metadata` / `allowed-tools`)である。ただし共通に効く度合いは項目ごとに異なる。 | 項目 | 4 ランタイムでの扱い | @@ -139,11 +140,9 @@ description: "Delete merged branches and worktrees, stopping only where git refu ([棚卸の計画](../../../issues/old/ndf-development-skills/07-tasks.md) の Task 0-8) - 「常時注入のみ」に相当する機能は Codex と Kiro と agy にない。3 つとも `user-invocable: false` を解釈せず、この分類の Skill も通常の Skill として扱う。唯一の対象で - ある `ndf-policies` は 4 ランタイムすべてへ配布している(`plugins/ndf/manifests/`)ため、 - Codex と agy では暗黙起動されうる。したがってこの分類の Skill は `description` に**「知識として参照する。手順として - 実行しない」旨を明記する**。Kiro は Skill として配らず `.kiro/steering/` へ常時指示として - 置き換えることで回避する([棚卸の計画](../../../issues/old/ndf-development-skills/07-tasks.md) - の Task 0-9)。`description` の書き換えは同計画の Task 0-10 で行う + ある `ndf-policies` は Codex へも配布している(`plugins/ndf/manifests/`)ため、 + Codex では暗黙起動されうる。したがってこの分類の Skill は `description` に**「知識として参照する。手順として + 実行しない」旨を明記する**([棚卸の計画](../../../issues/old/ndf-development-skills/07-tasks.md) の Task 0-10) - **Claude Code では** `disable-model-invocation: true` の Skill は `description` がコンテキスト へ載らず、`user-invocable: false` は載る。Codex と Kiro と agy にはこのキーがなく `description` は常に読まれるため、明示指示専用にする Skill は `description` 自体へ「利用者が明示的に指示した @@ -284,9 +283,7 @@ Skill 名の部分だからである。ただし配布先に何が入ってい | `SKILL.md` 行数 | 500 行。超えるものは補助ファイルへ分割 | 仕様の推奨、コンパクション対策 | | `SKILL.md` 本文 | 5,000 トークン | 仕様の推奨 | | Claude Code の初期 Skill 一覧の合計 | **27,900 文字**(10,000 トークン = コンテキストの 1%)。1 項目は `description` + `when_to_use` を合わせて 1,536 文字で切り詰め | Claude Code 公式ドキュメント。既定モデル Opus 5 のコンテキスト 1,000,000 から算出。文字数は較正した換算比による | -| Codex の初期 Skill 一覧の合計 | **15,177 文字**(5,440 トークン = コンテキストの 2%) | Codex 公式ドキュメント。既定モデル gpt-5.6-sol のコンテキスト 272,000 から算出。**4 ランタイムで最も厳しく、実質の制約になる** | -| Kiro の初期 Skill 一覧の合計 | **27,900 文字**(10,000 トークン = コンテキストの 1%) | 公式ドキュメントに規定が無いため、コンテキスト長が同じ Claude Code の 1% を借りる。既定モデル auto のコンテキスト 1,000,000 から算出 | -| agy の初期 Skill 一覧の合計 | **27,900 文字**(10,000 トークン = コンテキストの 1%) | 公式ドキュメントに規定が無いため、Kiro と同じく Claude Code の 1% を借りる。既定モデルは Gemini 3.x Flash 系で、コンテキスト長を出す手段が見当たらないため 1,000,000 を前提に置く | +| Codex の初期 Skill 一覧の合計 | **15,177 文字**(5,440 トークン = コンテキストの 2%) | Codex 公式ドキュメント。既定モデル gpt-5.6-sol のコンテキスト 272,000 から算出。**配布先の 2 ランタイムで厳しい側で、実質の制約になる** | | 全 Skill の frontmatter 合計 | 20,000 文字(**警告のみ**) | リポジトリ固有の目安。ランタイムが課す制約ではないため、超過しても警告にとどめる。**plugin family をまたいだ合計**。値は Codex の予算から逆算する([目安の決め方](#frontmatter-合計の目安は-codex-の予算から逆算する)) | ### 予算はトークンで効く @@ -333,7 +330,6 @@ Skill を増減したときや、既定モデルが変わったときに再実 | --- | --- | ---: | --- | ---: | ---: | | Claude Code | Opus 5 | 1,000,000 | 1% | 10,000 | 27,900 | | Codex | gpt-5.6-sol | 272,000 | 2% | 5,440 | 15,177 | -| Kiro | auto | 1,000,000 | 1%(Claude Code から借りる) | 10,000 | 27,900 | 文字換算は較正値(2026-09-01 実測で 2.79 文字/トークン)による。`--calibrate` を実行すると この値は更新される。 @@ -379,19 +375,6 @@ Skill の一覧 7.4k は、他のプラグインと組み込みを含む全量 コンテキストの内訳を測り、上の表を差し替えたうえで値を決める。据え置いたまま Skill を足すと 警告が出続け、目安として働かなくなる。 -**Kiro の規定は Claude Code から借りている。** 公式ドキュメント(kiro.dev/docs/skills)に一覧 -予算の記述が無いため、既定モデル `auto` のコンテキスト長が同じ 1,000,000 である Claude Code の -1% を当てる。憶測で独自の値を置くより、同じ土俵の実在する規定へ揃えるほうが根拠が残る。 -コンテキスト 1,000,000 は過去の実測の記録で、kiro-cli 2.20.1 では同じ手段(`--list-models`)を -取れない。値を更新するときは、その時点で一覧を取れる手段から確かめ直す。 -**NDF は Kiro でも 1M コンテキストのモデルだけを対象とする**(`dev.kiro/install.sh` が導入時に -この前提を出力する。モデルの一覧を取る手段が kiro-cli の版によって変わるため、機械でのチェックは -置いていない)。 - -起動時に読み込むコンテキスト量(Kiro が全 `SKILL.md` を読む合計)に**上限は置かない**。 -コンテキスト長が同じ Claude Code に上限を置かないため、Kiro だけを縛る根拠が無い。数は smoke test のログへ残るため、増え方は -後から追える。 - **コンテキストが最も長い Claude Code ではなく、Codex が全体の制約になる。** 比率が 2 倍でも コンテキストが 1/3.7 のため、予算は Claude Code の約半分にしかならない。Skill を増やすときは Codex の 5,440 文字を基準に考える。 @@ -405,7 +388,6 @@ Claude Code 側は `skillListingBudgetFraction` 設定または `SLASH_COMMAND_T | --- | :-: | :-: | :-: | :-: | | Claude Code | ○ | ○ | ○(`description` と合算して 1,536 文字で切り詰め) | **×** | | Codex | ○ | ○ | × | **○** | -| Kiro | ○ | ○ | × | 規定なし(多い側で見積もる) | パスを載せるのは Codex だけである(*"In Codex, the initial list also includes each skill's file path."*)。Claude Code の公式記述は *"a listing of skill names and descriptions"* で @@ -429,9 +411,9 @@ Codex は起動時に全 Skill の `name` と `description` とファイルパ 超過分を逃がす。 逃がし先は配布先で選ぶ。Claude Code だけに配布する Skill は `when_to_use` へ移してよい。 -一方 `when_to_use` は Codex と Kiro と agy では読まれないため、この 3 ランタイムへも配布する -Skill のトリガ語を `when_to_use` へ移すと、そのランタイムでは一覧に載らず暗黙起動に効かなくなる。 -4 ランタイムへ配布する Skill はトリガ語を `description` に残したまま要約し、手順の説明を +一方 `when_to_use` は Codex では読まれないため、Codex へも配布する +Skill のトリガ語を `when_to_use` へ移すと、Codex では一覧に載らず暗黙起動に効かなくなる。 +Codex へも配布する Skill はトリガ語を `description` に残したまま要約し、手順の説明を 本文へ逃がす。 Claude Code のコンパクション後は、呼び出し済み Skill の先頭 5,000 トークンのみが再添付され、 diff --git a/plugins/ndf/skills/backlog-refinement/tests/test_issue_upkeep_layout.py b/plugins/ndf/skills/backlog-refinement/tests/test_issue_upkeep_layout.py index bf833e8f9..7289e8d61 100644 --- a/plugins/ndf/skills/backlog-refinement/tests/test_issue_upkeep_layout.py +++ b/plugins/ndf/skills/backlog-refinement/tests/test_issue_upkeep_layout.py @@ -36,9 +36,9 @@ def section(text: str, heading: str) -> str: return heading + (rest[: ends[0]] if ends else rest) -@pytest.mark.parametrize("runtime", ["claude", "codex", "kiro", "agy"]) +@pytest.mark.parametrize("runtime", ["claude", "codex"]) def test_the_skill_is_distributed(runtime: str) -> None: - """4 つの manifest すべてに載る。""" + """配布先の 2 つの manifest に載る。""" manifest = ROOT / "plugins" / "ndf" / "manifests" / f"{runtime}-skills.txt" names = [line.split("#", 1)[0].strip() for line in manifest.read_text(encoding="utf-8").splitlines()] assert "backlog-refinement" in names diff --git a/plugins/ndf/skills/development-workflow/references/glossary.md b/plugins/ndf/skills/development-workflow/references/glossary.md index 28b2e8ff0..862d47007 100644 --- a/plugins/ndf/skills/development-workflow/references/glossary.md +++ b/plugins/ndf/skills/development-workflow/references/glossary.md @@ -60,7 +60,7 @@ flowchart TB | --- | --- | --- | --- | | マイルストーン | 着手の順序を表す単位。スプリントはこの中から切り出す | GitHub の milestone。open は `<2 桁の連番> <主題>`、closed は `v<版数>` | [parallel-work.md](parallel-work.md) の「用語」 | | スプリント | 1 回のリリースとして出す課題と Pull Request のセット。版数を持つプロジェクトでは 1 つの版になる。区切りは 1 回のリリースとして出す中身で決める。工程はスプリント単位で 1 回ずつ通し、モードもスプリントで 1 つにする | ブランチ `sprint/<名前>`、目録 `sprint.json`、状態のファイル(例 `sprint-state.json`) | [parallel-work.md](parallel-work.md) の「用語」 | -| スプリント課題 | 「スプリントを閉じる」が閉じる対象にする課題。スプリントに含まれる Pull Request の本文が GitHub の closing keywords(PR 本文に `Closes #123` のように書くと、マージ時にその課題を自動で閉じる機能)で指す課題と、`--issues` で渡した課題の和 | `sprint-close.py` が閉じる | [progress-tracking](../../progress-tracking/SKILL.md) | +| スプリント課題 | 「スプリントを閉じる」が閉じる対象にする課題。スプリントに含まれる Pull Request の本文が GitHub の closing keywords(PR 本文に `Closes #123` のように書くと、マージ時にその課題を自動で閉じる機能)か閉じる予定の参照(`最終工程の後に閉じる: #N`)で指す課題と、`--issues` で渡した課題の和 | `sprint-close.py` が閉じる | [progress-tracking](../../progress-tracking/SKILL.md) | | 工程 | 工程表(モードごとに起動する Skill の表)の 1 行。要求と受け入れ条件・設計・実装・リリースなど | 進捗記録の `stage` | [../SKILL.md](../SKILL.md) の「モードごとに起動する Skill」 | | フェーズ | supervisor 1 つ(またはプラン 1 本)が通す、連続する工程のグループ。設計・実装・検査・取り込み・仕上げ・リリースの 6 つ | プランの `"フェーズ"`、Agent の `description` の先頭語 | [agent-layers.md](agent-layers.md) の「フェーズ」 | | プラン | `supervise.py` が流す 1 本の JSON。フェーズの手順をステップの列として持つ | `plan.json`、`supervise.py new impl` / `check` / `release` / `sprint` / `close` | [supervise.py](../../../scripts/supervise.py) の docstring | diff --git a/plugins/ndf/skills/development-workflow/references/loop.md b/plugins/ndf/skills/development-workflow/references/loop.md index 6f0eee90d..6fdde0e7f 100644 --- a/plugins/ndf/skills/development-workflow/references/loop.md +++ b/plugins/ndf/skills/development-workflow/references/loop.md @@ -12,7 +12,7 @@ 2. `loop select l20261025` が `upkeep.py rank` を打ち、#11 と #12 を選ぶ。#13 は「順位(切り出しの境界の外)」で外す 3. conductor がモードを `standard`、版を `10.20.0-dev.1` と決め、`loop sprint l20261025 --mode standard --dev 10.20.0-dev.1` を打つ 4. 返った `pipeline` をそのまま流す。設計 → 承認ゲート 1(MVV 判定)→ 実装 → 検査 → 開発版 → 本番(承認ゲート 2)→ - まとめ(確定仕様化・課題を閉じる・振り返り・棚卸し)→ `loop lap` + まとめ(確定仕様化・振り返り・課題を閉じる・棚卸し)→ `loop lap` 5. #12 の設計が承認ゲート 1 で「判定できない」だった。`loop lap`(または `loop gate`)が #12 を人の判断待ちへ移し、 課題の本文に `## 人の判断待ち(開発ループ l20261025)` を書き、#11 だけで続きの `pipeline` を返す 6. `loop lap` が周回を締め、止まる条件に当たらなければ `ndf-next` を返す。ラッパーが次のセッションを起動し、手順 1 から続ける diff --git a/plugins/ndf/skills/development-workflow/references/pace.md b/plugins/ndf/skills/development-workflow/references/pace.md index a0e567703..683fbf468 100644 --- a/plugins/ndf/skills/development-workflow/references/pace.md +++ b/plugins/ndf/skills/development-workflow/references/pace.md @@ -71,7 +71,7 @@ graph LR | --- | --- | --- | | その場で通す | 作業場所の用意 / 計画 / 実装 / 完了判定 / Pull Request / 後片付け / 配布 / リリース後テスト | 課題ごと。実装のプランが範囲テスト → Draft の Pull Request(CI と並べる)→ 全体テスト → doc-lint → マージまでを通す。リリースは開発版と verify-install まで。本番は下のゲート 2 に従う | | トリガーで通す | リファクタリング / コードレビュー | 検査のトリガーが立ったとき、次の開発版の前に 1 回。範囲は前回の検査からの差分 | -| スプリントの終わりにまとめる | 確定仕様化 / 振り返り(受け入れ条件の確認と課題を閉じる作業を含む)/ 棚卸し | スプリントの終わりに 1 回ずつ | +| スプリントの終わりにまとめる | 確定仕様化 / 振り返り / 課題を閉じる(受け入れ条件の確認を含む。`progress-tracking` の「スプリントを閉じる」)/ 棚卸し | スプリントの終わりに 1 回ずつ | | 省かない | 要求と受け入れ条件 / 設計 / ドキュメント再構成 / ドキュメントレビュー | モードの定めどおり。設計の承認が要る変更は設計 Pull Request を出す | | 承認ゲート | `normal` | `auto` | `fast` | @@ -202,9 +202,9 @@ glob の `**` は区切りをまたぎ、`*` と `?` はまたがない。どの **スプリントの終わり**は `supervise.py new close --name M --worktree <根> --issue N... --version <開発版> --prod <正式版> --state <状態>` が組む。最終の検査(実行条件 `eval --final`)→ 開発版と本番(実行条件 `changed --id -final`。 -最終の検査で変更があったときだけ)→ まとめ(`spec` 確定仕様化 → Pull Request → `close` 後片付け → `retro` 振り返り → `refine` 棚卸し) +最終の検査で変更があったときだけ)→ まとめ(`spec` 確定仕様化 → Pull Request →(配布の記録があれば `release-verify` リリース後テスト →)`retro` 振り返り → `close` 後片付け → `refine` 棚卸し) を `--then` のステージで流す。`close` のステップは `sprint-close.py --record-pr {queue_pr:release-prod} --issues <課題> ---with-verification` で、本番が飛ばされたときは `--record-pr 0`(本番の記録なし)になる。`refine` は `close` が 0 で終わったときだけ届き(スプリントを閉じてから棚卸しへ進む)、棚卸しを無人で通す(承認の要る反映は +--final-stage 振り返り --with-verification` で、本番が飛ばされたときは `--record-pr 0`(本番の記録なし)になる。`refine` は `close` が 0 で終わったときだけ届き(スプリントを閉じてから棚卸しへ進む)、棚卸しを無人で通す(承認の要る反映は 行わず、承認資料のパスと番号を報告に載せて終える)。まとめのプランは課題すべてへ 工程を記録するため、通過記録の報告が `まとめる:` から `記録あり:` へ移る。 リリースの経路がテンプレートで組むものでなければ `--version`・`--prod` は要らず、開発版と本番の代わりに下の経路のステージが入り、 diff --git a/plugins/ndf/skills/development-workflow/references/stage-completeness.md b/plugins/ndf/skills/development-workflow/references/stage-completeness.md index 161fd5c0c..50dde00a5 100644 --- a/plugins/ndf/skills/development-workflow/references/stage-completeness.md +++ b/plugins/ndf/skills/development-workflow/references/stage-completeness.md @@ -192,7 +192,7 @@ bash "$SCRIPTS/../skills/development-workflow/scripts/stage-check.sh" report < ## Pull Request の作成の時点で見るもの -**リリース(`配布`)の記録まで待たない。** `gh pr create` を観測し、本文の GitHub の closing keywords(PR 本文に `Closes #123` のように書くと、マージ時にその課題を自動で閉じる機能)が指す課題の通過記録を +**リリース(`配布`)の記録まで待たない。** `gh pr create` を観測し、本文の GitHub の closing keywords(PR 本文に `Closes #123` のように書くと、マージ時にその課題を自動で閉じる機能)と閉じる予定の参照(`最終工程の後に閉じる: #N`)が指す課題の通過記録を 読んで、記録の無い必須の工程を案内する(#424)。 | 何を見るか | 出すもの | diff --git a/plugins/ndf/skills/development-workflow/tests/test_document_rebuild_stage.py b/plugins/ndf/skills/development-workflow/tests/test_document_rebuild_stage.py index 26012a089..149e13184 100644 --- a/plugins/ndf/skills/development-workflow/tests/test_document_rebuild_stage.py +++ b/plugins/ndf/skills/development-workflow/tests/test_document_rebuild_stage.py @@ -26,7 +26,7 @@ NEW_STAGE = "ドキュメント再構成" RENAMED = {"設計レビュー": "ドキュメントレビュー", "レビュー": "実装レビュー"} -RUNTIMES = ("claude", "codex", "kiro", "agy") +RUNTIMES = ("claude", "codex") def lib_body() -> str: diff --git a/plugins/ndf/skills/merged/SKILL.md b/plugins/ndf/skills/merged/SKILL.md index de44ab8b6..738fbd247 100644 --- a/plugins/ndf/skills/merged/SKILL.md +++ b/plugins/ndf/skills/merged/SKILL.md @@ -198,14 +198,14 @@ python3 "$SCRIPTS/merged-steps.py" probe (--pr | --head <ブランチ # **gate も同じ実体を読む**ため、Skill の下にコピーは無い。 CLOSING="$SCRIPTS/lib/closing-issues.sh" -# 1. 本文から closing keywords が指す先を取り出す(<所有者>/<リポジトリ> と <番号> をタブ区切りで出す) +# 1. 本文から closing keywords と閉じる予定の参照が指す先を取り出す(<所有者>/<リポジトリ> と <番号> をタブ区切りで出す) gh pr view --json body -q .body | bash "$CLOSING" # 2. まだ OPEN のものだけに絞り、報告へ載せる gh issue view <番号> --repo <所有者>/<リポジトリ> --json state -q .state # OPEN / CLOSED ``` -- GitHub の closing keywords(PR 本文に `Closes #123` のように書くと、マージ時にその課題を自動で閉じる機能)が 1 つも無ければ何もしない。スクリプトは何も出さずに終了コード 0 で終わる +- GitHub の closing keywords(PR 本文に `Closes #123` のように書くと、マージ時にその課題を自動で閉じる機能)も閉じる予定の参照(`最終工程の後に閉じる: #N`。`progress-tracking` の「スプリントを閉じる」)も 1 つも無ければ何もしない。スクリプトは何も出さずに終了コード 0 で終わる - **この一覧は、後片付けの進捗記録を書く先でもある**(`progress-tracking` の「工程の単位と 記録する課題」。後片付けは Pull Request 単位で、マージした側が本文の closing keywords(`Closes #N` などの記法)が指す課題 すべてへ書く) @@ -278,7 +278,7 @@ bash "$SCRIPTS/../skills/development-workflow/scripts/stage-check.sh" report ..origin/<名前>`)を添える | | 止まった対象 | 「止まる条件」の「止まったときの一覧」と、同意の結果 | | 未完了 | 退避が失敗した worktreeのパス、退避先・退避済みのパス・失敗したパスと `mv` の出力(退避済みのものは退避先に残す。戻すなら同じ `mv` の逆)、「原因を取り除いた後に `/ndf:merged ` をもう一度実行する」 | -| スプリント課題 | このマージの closing keywords(`Closes #N` などの記法)が指す課題のうち OPEN のもの。「閉じるのはスプリントの最終工程(`progress-tracking` の「スプリントを閉じる」)」と添える | +| スプリント課題 | このマージの closing keywords(`Closes #N` などの記法)と閉じる予定の参照が指す課題のうち OPEN のもの。「閉じるのはスプリントの最終工程(`progress-tracking` の「スプリントを閉じる」)」と添える | | スプリントの最後か | 最後 / 残りあり / **判断できない** | あわせて次も載せる。 diff --git a/plugins/ndf/skills/pr/SKILL.md b/plugins/ndf/skills/pr/SKILL.md index e050ccc2c..2e326fa7b 100644 --- a/plugins/ndf/skills/pr/SKILL.md +++ b/plugins/ndf/skills/pr/SKILL.md @@ -152,6 +152,8 @@ URL は最終行に生のまま置く(Markdown リンクにすると利用者 課題の Pull Request は `--base sprint/<名前>` で出す。スプリントの Pull Request は、 スプリントブランチの worktree から `--base` を省いて出し、本文にスプリントが閉じる課題を番号ごとに書く。 +開発の起点(`base_branch`)が既定ブランチと同じリポジトリでは、上の develop の行の Pull Request が既定ブランチ宛になる。 +そのときの本文の扱いは次の節にある。 ## closing keywords は本文だけに書く @@ -159,6 +161,11 @@ URL は最終行に生のまま置く(Markdown リンクにすると利用者 メッセージに書くと、マージでそのコミットが指す課題だけが先に閉じ、スプリントが割れる。**本文の closing keywords は 外さない**(`stage-completeness.md` と `progress-tracking` の「スプリントを閉じる」が本文を入力にする)。 +**宛先が既定ブランチの Pull Request では、`pr-steps.py create` / `update` が本文の closing keywords を閉じる予定の参照 +(`最終工程の後に閉じる: #<番号>`)へ置き換えてから送る。** 書く側は closing keywords で書いてよい。置き換えた +指し先は結果の `metrics.deferred_closing` に載る。課題を閉じる時点と閉じる予定の参照の形は `progress-tracking` の +「スプリントを閉じる」が正本である。 + ## 設計 Pull Request の本文 **head のブランチ名が `design/` で始まる Pull Request の本文は、決定の中身を持たない**(決定は設計文書だけが持つ)。 diff --git a/plugins/ndf/skills/progress-tracking/SKILL.md b/plugins/ndf/skills/progress-tracking/SKILL.md index 8a242d800..c4ffd3e84 100644 --- a/plugins/ndf/skills/progress-tracking/SKILL.md +++ b/plugins/ndf/skills/progress-tracking/SKILL.md @@ -142,7 +142,7 @@ worker の起動指示へはこれをコピーし、本文を読ませない。 | 工程の単位(`parallel-work.md` の表) | 記録する側 | 記録する課題 | | --- | --- | --- | | 課題 | その課題を扱う supervisor | その課題 | -| Pull Request(後片付けを含む) | その工程を行った側。後片付けはマージした側 | その Pull Request の本文の GitHub の closing keywords(PR 本文に `Closes #123` のように書くと、マージ時にその課題を自動で閉じる機能)が指す課題すべて | +| Pull Request(後片付けを含む) | その工程を行った側。後片付けはマージした側 | その Pull Request の本文の GitHub の closing keywords(PR 本文に `Closes #123` のように書くと、マージ時にその課題を自動で閉じる機能)と閉じる予定の参照(下の「閉じる予定の参照」)が指す課題すべて | | スプリント(確定仕様化・リリース・体裁レビュー・リリース後テスト・振り返り) | その工程を行った側。リリース以降はオーケストレーター | スプリント課題すべて | **記録は 1 回の実行に課題を並べてよい。** どの記録も記録のスクリプトが自分で通過記録へ積むため、 @@ -153,20 +153,28 @@ worker の起動指示へはこれをコピーし、本文を読ませない。 ## スプリントを閉じる -**契機は最終工程を出るときである。** この手順はそこで **1 度だけ**行う。 +**閉じる時点の決まりの正本はこの節だけである。** 呼ぶ側(Skill・まとめのプラン・`pr`)は閉じる時点と順序と +閉じる条件を写さず、この節を指す。worker は呼ばない(supervisor か conductor が呼ぶ)。 + +**課題を閉じてよいのは、その課題がその実行の最終工程を終えたときだけである。** NDF のどの経路 +(まとめのプランの `close`・最終工程を担う Skill・conductor の手打ち)で打っても、`sprint-close.py` が課題ごとに +最終工程の記録(閉じる条件 3)を確かめ、無ければ閉じない。呼ぶ側の手順の並びに頼らない。 **最終工程とは、棚卸しの前にその実行で最後に通る工程である**(工程表は `development-workflow` の 「モードごとに起動する Skill」。`operation` のリリース後テストと振り返りはそれぞれ条件付き)。 検証リリースで止めた実行も、その実行の最終工程でこの手順を行う。課題は閉じる条件に 当たらず OPEN のまま残り、後で本番へのリリースを行った実行の最終工程が改めて行う。 -**正本はここだけである。** 呼ぶ側は手順をコピーせず、この節を指す。worker は呼ばない -(supervisor か conductor が呼ぶ)。 +| 呼ぶ側 | 呼ぶ時点 | 呼ぶ条件 | `--final-stage` | +| --- | --- | --- | --- | +| まとめのプラン(`supervise.py new close`)の `close` のステップ | 振り返りの後、棚卸しの前 | プランで仕上げる実行(どのモードも `retro` を通す) | `振り返り` | +| `retrospective` | 記録を投稿し、課題の本文に辿る経路の行を足した後 | 振り返りを通る変更 | `振り返り` | +| `release-verification` | 検証結果を記録した後 | 後に振り返りが続かない変更(振り返りを通らない `operation`) | `リリース後テスト` | +| `release` | 配布を終えた後 | 後にリリース後テストも振り返りも続かない変更(`light`、どちらも通らない `operation`) | `配布` | -| 呼ぶ Skill | 呼ぶ時点 | 呼ぶ条件 | -| --- | --- | --- | -| `retrospective` | 記録を投稿した後 | 振り返りを通る変更 | -| `release-verification` | 検証結果を記録した後 | 後に振り返りが続かない変更(振り返りを通らない `operation`) | -| `release` | 配布を終えた後 | 後にリリース後テストも振り返りも続かない変更(`light`、どちらも通らない `operation`) | +まとめのプランの並びは `merge →(配布の記録があれば)release-verify → retro → close → refine` である。 +`release-verify` と `retro` の指示は課題を閉じない。プランの中では最終工程が振り返りになるため、`close` が閉じる。 +プランの外で Skill を直接通すときは、上の表の Skill がこの手順を行う。両方が打っても、早すぎる時点では +閉じる条件 3 で閉じず、2 度目は `already_closed` になる。 **閉じた後は、工程表の次の工程 `棚卸し`(`backlog-refinement`)へ進む。「スプリントを閉じる → 棚卸し」の 順序の正本はここである。** 逆にすると、`backlog-refinement` の手順 1 が読む「このスプリントで閉じた課題」が @@ -181,20 +189,25 @@ worker の起動指示へはこれをコピーし、本文を読ませない。 (別の実行なら `retrospective` の「Pull Request の番号を特定する」で引く)。 ```bash -python3 "$SCRIPTS/sprint-close.py" --record-pr <記録のPR番号> \ - [--with-verification] [--label "<マイルストーン>の<工程名>"] [--prs <番号,番号>] [--dry-run] +python3 "$SCRIPTS/sprint-close.py" --record-pr <記録のPR番号> [--final-stage <配布|リリース後テスト|振り返り>] \ + [--with-verification] [--label "<マイルストーン>の<工程名>"] [--prs <番号,番号>] [--issues <番号,番号>] [--dry-run] ``` -- `--with-verification`: リリース後テストを通る経路で付ける(閉じる条件 2 を見る) +- `--final-stage`: その実行の最終工程(上の表の値)。省けば `振り返り`。省いても閉じる条件は緩まない +- `--with-verification`: リリース後テストを通る経路で付ける(閉じる条件 2 を見る)。`--final-stage リリース後テスト` + では付けなくても条件 2 を見る - `--prs`: 同じ実行の中で `release` が承認の提示に並べた一覧を渡すとき。省けば最後の 「## 配布の記録」の `スプリント:` の行を使う +- `--issues`: スプリントの PR の本文が指さない課題を足すとき(`pace: fast` の実装 PR など) - スクリプトが行うこと: 配布の記録の読み取り → スプリント課題(スプリントの PR の本文の closing keywords - (GitHub が課題を自動で閉じるキーワード。`Closes #N` などの記法)が指す課題と `--issues` の課題)の収集 → - 閉じる条件の判定 → 課題ごとに「状態を読む → 記録のリポジトリの課題ならボードを Done → 読み直して OPEN なら閉じる → + (GitHub が課題を自動で閉じるキーワード。`Closes #N` などの記法)と閉じる予定の参照が指す課題と `--issues` の課題)の収集 → + 閉じる条件 1〜3 の判定 → 課題ごとに「状態を読む → 記録のリポジトリの課題ならボードを Done → 読み直して OPEN なら閉じる → 読み直して CLOSED を確かめる」→ 参照だけの課題の報告。他のリポジトリの課題にはボードを書かない - `--with-verification` の経路では、課題ごとの錠の中で状態と本文を 1 回で読んで閉じる条件 2 を判定し、満たした 記録のリポジトリの課題は、本文の `## 受け入れ条件` のチェックボックスに印(`[x]`)を付けてから閉じる。変えるのは - チェックボックスの空白 1 文字だけで、節の外と説明の行は変えない。他のリポジトリの課題の本文は書かない + チェックボックスの空白 1 文字だけで、節の外と説明の行は変えない。他のリポジトリの課題の本文は書かない。 + 閉じる条件 3 を満たさない課題には印を付けない +- 閉じる条件 3 は条件 2 と同じ 1 回の本文の読みを使う(`--final-stage 振り返り` で条件 2 を見ないときも本文を 1 回読む) - 参照だけの課題: スプリントの PR の題か本文が `#<番号>` か課題の URL で参照し、スプリント課題に入らない 記録のリポジトリの開いた課題。閉じずボードも書かず、`kept_open` で載せる。件数は `metrics.referenced_only` にある @@ -204,20 +217,21 @@ python3 "$SCRIPTS/sprint-close.py" --record-pr <記録のPR番号> \ | --- | --- | | 0 | `items` の表を報告に貼り、棚卸しの工程へ進む | | 1 | 失敗した課題がある。`items[].reason` と `cmd`(やり直すコマンド)を報告し、最終工程を完了と報告せず、棚卸しへ進まずに止まる | -| 2 | スプリントの PR の一覧が取れない。推測せず運用者に一覧を聞き、`--prs` で打ち直す。または、PR を読んだのにスプリント課題が 0 件(PR の本文に closing keywords(GitHub が課題を自動で閉じるキーワード Closes / Fixes / Resolves)が無く、`--issues` も無い)。`next` に並ぶ PR が参照した番号から閉じる課題を選び、`--issues` で渡して打ち直す | +| 2 | スプリントの PR の一覧が取れない。推測せず運用者に一覧を聞き、`--prs` で打ち直す。または、PR を読んだのにスプリント課題が 0 件(PR の本文に closing keywords(GitHub が課題を自動で閉じるキーワード Closes / Fixes / Resolves)も閉じる予定の参照も無く、`--issues` も無い)。`next` に並ぶ PR が参照した番号から閉じる課題を選び、`--issues` で渡して打ち直す | | 3 | 呼び出しの誤り。引数を直して打ち直す | `items[]` の `kind: "issue"` の `result` は次の 5 つのどれかになる(`--dry-run` では `would_close` も出る)。`kind: "board_note"` はボードへ Done を書けなかった理由(`projects-sync.sh` の結果 JSON が `ok` でない)で、報告にそのまま載せる。課題を閉じる処理は止めない。 印を付けた受け入れ条件の番号は `checked`、`--dry-run` で付ける予定の番号は `would_check` に載る。 +判定に使った最終工程は `metrics.final_stage` と `decisions` に載る。 | `result` | 意味 | 報告に載せるもの | | --- | --- | --- | | `closed` | 閉じた(ボードの自動化が閉じた場合を含む) | 番号と `cmd`(戻し方 `gh issue reopen`) | | `already_closed` | 手順の前から閉じていた(既定ブランチへのマージで GitHub が閉じた場合を含む) | 番号 | -| `failed` | 状態を読めない、または閉じた後も CLOSED でない。`--with-verification` では、本文の錠を取れない・本文を読めない・受け入れ条件の印を書けない(閉じない) | 番号・`reason`・`cmd`(やり直す `gh issue close`。本文の失敗では同じ `sprint-close.py` の呼び出し) | -| `closed_unverified` | 既に閉じていたが閉じる条件 2 を満たさない。開き直さない | 番号と `reason`(満たさない理由と番号)。受け入れ条件を確かめて印を付けるか、開き直すかを人が決める | -| `kept_open` | 閉じる条件に当たらない。または参照だけの課題(`reason` が「closing keywords(…)も --issues も無い参照」。状態を読めなければ先頭に「状態を読めない」) | 番号と `reason`(いつ閉じるか。参照だけの課題は、閉じるなら `--issues` で渡す) | +| `failed` | 状態を読めない、または閉じた後も CLOSED でない。本文を読めない(最終工程の記録を確かめられない。閉じない)。`--with-verification` では、本文の錠を取れない・受け入れ条件の印を書けない(閉じない) | 番号・`reason`・`cmd`(やり直す `gh issue close`。本文の失敗では同じ `sprint-close.py` の呼び出し) | +| `closed_unverified` | 既に閉じていたが閉じる条件 2 か 3 を満たさない。開き直さない | 番号と `reason`(満たさない理由と番号)。受け入れ条件を確かめて印を付けるか、開き直すかを人が決める | +| `kept_open` | 閉じる条件に当たらない(条件 3 では `reason` が「最終工程(<工程>)の記録が無い: <何が無いか>」)。または参照だけの課題(`reason` が「closing keywords(…)も --issues も無い参照」。状態を読めなければ先頭に「状態を読めない」) | 番号と `reason`(いつ閉じるか。参照だけの課題は、閉じるなら `--issues` で渡す) | **`kept_open` と `closed_unverified` は止める理由にしない。** 棚卸しへ進んでよい。 @@ -226,11 +240,31 @@ python3 "$SCRIPTS/sprint-close.py" --record-pr <記録のPR番号> \ | # | 条件 | 満たさないとき | | --- | --- | --- | | 1 | 最後の「## 配布の記録」の `段階:` が `本番` または `配布なし` で始まる | スプリント課題すべてを OPEN のまま残す | -| 2 | リリース後テストを通る経路では、その課題の行がすべて `合格` で、課題の本文の `## 受け入れ条件` のチェックボックスの番号(上から数えた順、1 から)のすべてに合格の行がある。同じ番号に合格でない行があればその番号は合格でない。チェックボックスが無い課題は行の合否だけで見る | その課題を OPEN のまま残す(`reason` に合格の行が無い番号)。既に閉じていれば `closed_unverified` | +| 2 | リリース後テストを通る経路(`--with-verification` か `--final-stage リリース後テスト`)では、その課題の行がすべて `合格` で、課題の本文の `## 受け入れ条件` のチェックボックスの番号(上から数えた順、1 から)のすべてに合格の行がある。同じ番号に合格でない行があればその番号は合格でない。チェックボックスが無い課題は行の合否だけで見る | その課題を OPEN のまま残す(`reason` に合格の行が無い番号)。既に閉じていれば `closed_unverified` | +| 3 | その課題の最終工程の記録がある。`配布` は条件 1、`リリース後テスト` は条件 2 がそれに当たる。`振り返り` は課題の本文に行頭から `振り返り: https://github.com/<所有者>/<リポジトリ>/(issues\|pull)/<番号>#issuecomment-<識別子>` の行があり、そのうち 1 つ以上の識別子が、記録の PR の最後の配布の記録を含むコメントの識別子以上である(配布の記録が本文にあれば記録の PR の最初のコメントの識別子と比べ、コメントが無ければ満たさない。`--record-pr 0` は行があれば満たす)。前の実行の振り返りの行は数えない | その課題を OPEN のまま残す(`reason` に通っていない工程)。既に閉じていれば `closed_unverified`。本文を読めなければ `failed` | **検証リリースは閉じる条件を満たさない。** 利用者が常用する版にまだ届いていない段階で、 確かめていないことが終わったように見える。 +`--record-pr 0`(本番の記録なし)は条件 1・2 を見ない。`--record-pr unknown`(記録の置き場が分からない)は +本文を読まずにすべて `kept_open` で返す。 + +### 閉じる予定の参照 + +**既定ブランチ宛の Pull Request の本文には closing keywords を残さない。** GitHub は既定ブランチへのマージの時点に +closing keywords の課題を閉じ、最終工程より先に閉じてしまう。NDF の手順(`pr-steps.py create` / `update` と +まとめのプランの pr のステップ)は、宛先が既定ブランチ(既定ブランチを決められないときを含む)なら、本文の +closing keywords を閉じる予定の参照へ置き換えてから送る。宛先が既定ブランチでない Pull Request の本文は書き換えない。 + +```text +最終工程の後に閉じる: <指し先> +``` + +`<指し先>` は closing keywords と同じ 3 つ(`#<番号>`・`<所有者>/<リポジトリ>#<番号>`・課題の URL)で、1 つの指し先に +1 つ書く。GitHub はこの形では閉じず、`sprint-close.py`(`lib/closing-issues.sh`)がスプリント課題として集める。 +形の正本は `$SCRIPTS/lib/closing.py` の `DEFERRED` である。置き換えた指し先は `pr-steps.py` の結果の +`metrics.deferred_closing` に載る。 + ### 配布の記録 **配布の記録は Pull Request に置く。** 書く側はこの形で書き、`sprint-close.py` はこの形だけを読む。 @@ -244,7 +278,7 @@ python3 "$SCRIPTS/sprint-close.py" --record-pr <記録のPR番号> \ `対象の版:` が本番の版と一致する最後のブロック(無ければ、版数を上げない配布として、最後の配布の記録より後で `対象の版:` が版数でない最後のブロック。`配布なし` ならその記録より後の最後のブロック)を使う。 表の `課題` の列の頭が課題の参照でない行は飛ばす。課題を指す行が 1 行も無い表は、スプリント課題すべてを -「条件と課題の対応が読めない」で OPEN のまま残す。 +「条件と課題の対応が読めない」で OPEN のまま残す(すべての条件をマージ前に確かめたと本文に記録した課題は除く)。 ## 関連 diff --git a/plugins/ndf/skills/quality-gates/SKILL.md b/plugins/ndf/skills/quality-gates/SKILL.md index e663a3c7e..005d89934 100644 --- a/plugins/ndf/skills/quality-gates/SKILL.md +++ b/plugins/ndf/skills/quality-gates/SKILL.md @@ -129,6 +129,21 @@ flowchart LR 条件ごとに検証手段を書く。手段を書けない条件は満たしたと言えない。 +**条件ごとに、マージ前に確かめたか、リリースの後でしか確かめられないかを課題の本文へ記録する。** +承認資料の「未実施のリリース後テスト」は、この記録で `マージ前` とした条件を載せない。記録の無い条件は、 +マージ前に確かめていても載る。閉じる判定(`sprint-close.py --with-verification`)も、`マージ前` の条件には +リリース後テストの合格の行を求めない。 + +```bash +python3 "$SCRIPTS/acceptance-check.py" record --issue 2144 \ + --pre-merge '1=pytest tests/test_acceptance_checks.py' --pre-merge '4=pytest tests/test_sprint_close_acceptance.py' \ + --post-release 5 --post-release 6 +``` + +本文の `## 受け入れ条件の確認` の節に `| 条件 | 区分 | 手段 |` の表として残り、打ち直すと渡した番号の行だけが +置き換わる。`マージ前` には手段(テスト名か実行したコマンド)が要る。打っても確かめられない条件(本番の利用者の +環境・次のスプリントで見ることなど)だけを `リリース後` にする。記録は `acceptance-check.py show --issue <番号>` で読める。 + **「人手の確認」として残す前に、機械で打てないかを確かめる。** 端末・画面・操作を伴う条件でも、 擬似端末(`script` / `expect` / pty)に本物のクライアントをつなぎ、その端末へ入力を打てば、振る舞いは マージ前に確かめられる。使い捨ての環境(実装で建てたイメージ・コンテナ)があれば、その中で打つ。 diff --git a/plugins/ndf/skills/release-verification/SKILL.md b/plugins/ndf/skills/release-verification/SKILL.md index 2e100dd8f..8cf42b0f2 100644 --- a/plugins/ndf/skills/release-verification/SKILL.md +++ b/plugins/ndf/skills/release-verification/SKILL.md @@ -66,7 +66,7 @@ allowed-tools: ```bash python3 "$SCRIPTS/release-verification-steps.py" verify-install --ref --expect <版> \ - [--plugins ndf,...] [--runtimes claude,codex,kiro] --root . + [--plugins ndf,...] [--runtimes claude,codex] --root . ``` 隔離した HOME で ref から導入し、導入された版と、前の正式版のタグから変わったファイルの中身が @@ -153,11 +153,9 @@ ref と一致するか、利用者の HOME の設定が変わっていないか **後に振り返りが続かない変更(振り返りを通らない `operation`)では、この工程がその実行の 最終工程である。** 検証結果を記録した後に、`progress-tracking` の「スプリントを閉じる」を -行う。**手順はそこが正本で、ここにはコピーしない。** 振り返りを通る変更では、そちらが行うため -ここでは行わない。閉じた後の順序(棚卸しへ進む)は、その正本にある。 - -**不合格・保留の条件を持つ課題と、合格の行が無い受け入れ条件を持つ課題は閉じない。** その課題は `開いたまま` になり、判定と理由が -報告に載る。確かめていないことが終わったように見えないためである。 +`--final-stage リリース後テスト` で行う。**手順・閉じる条件・閉じた後の順序はそこが正本で、ここにはコピーしない。** +振り返りを通る変更と、まとめのプランの `release-verify` のステップでは、課題を閉じない(振り返りの後に閉じる)。 +閉じなかった課題は `開いたまま` になり、判定と理由が報告に載る。 ## 関連 diff --git a/plugins/ndf/skills/release/SKILL.md b/plugins/ndf/skills/release/SKILL.md index 2cf867d9f..84ac62b4d 100644 --- a/plugins/ndf/skills/release/SKILL.md +++ b/plugins/ndf/skills/release/SKILL.md @@ -330,8 +330,8 @@ Pull Request へコメントで投稿する)。 そのことと根拠のコマンドを残す。書かずに省くと、リリースし忘れたのか、リリースが不要だったのかを 後から区別できない。**飛ばしたときも同じブロックを残す。** `段階: 配布なし(<飛ばした理由>)` と `スプリント:` の 2 行だけを書き(`版:` は書かない)、置き場所は Pull Request を作らない形と同じにする。 -**この記録が無いと、スプリント課題が閉じられない**(閉じる条件は「本番リリースまで済んだ、または -リリースを飛ばした」。読み方は `progress-tracking` の「スプリントを閉じる」が持つ)。 +**この記録が無いと、スプリント課題が閉じられない**(閉じる条件と読み方は `progress-tracking` の +「スプリントを閉じる」が持つ)。 ## モードごとの要否 @@ -346,7 +346,7 @@ Pull Request へコメントで投稿する)。 **後にリリース後テストも振り返りも続かない変更(`light`、どちらも通らない `operation`)では、 この工程がその実行の最終工程である。** 配布を終えた後に、 -`progress-tracking` の「スプリントを閉じる」を行う。**手順と、閉じた後の順序(棚卸しへ進む)はそこが正本で、ここにはコピーしない。** +`progress-tracking` の「スプリントを閉じる」を `--final-stage 配布` で行う。**手順と、閉じた後の順序(棚卸しへ進む)はそこが正本で、ここにはコピーしない。** 続く工程があるなら呼ばない(`release-verification` か `retrospective` が行う)。 **検証リリースで止めた実行でも呼ぶ。** 課題は閉じる条件に当たらず `開いたまま` になり、 閉じるのは後で本番リリースを行った実行の最終工程である。 diff --git a/plugins/ndf/skills/requirements-design/references/acceptance-criteria.md b/plugins/ndf/skills/requirements-design/references/acceptance-criteria.md index 4577e4c05..aba3d8b5e 100644 --- a/plugins/ndf/skills/requirements-design/references/acceptance-criteria.md +++ b/plugins/ndf/skills/requirements-design/references/acceptance-criteria.md @@ -84,6 +84,18 @@ - [ ] 個人情報がログへ出力されない ``` +**退行しない条件は、前提にした今の振る舞いを書く前に 1 度実行して確かめる。** 「今と同じく〜する」は、 +今の振る舞いが本当にそうであることを前提にしている。前提が事実と違うと、条件はリリース後テストで初めて崩れる +(例: 「収束しないときは止まる」と書いたが、`final` が `max_rounds` のときは変更前から止まらなかった)。 +確かめた手段(実行したコマンドかテストの名前)とその結果を、条件のそばか課題の本文に書く。 + +```markdown +- [ ] 区分の記録が無い課題では、今と同じく印の無い条件がすべて載る(退行しない) + 前提の確認: `pytest tests/test_release_materials.py -k unverified` → 印の無い 3 件が載る(10.27.3) +``` + +前提が事実と違ったら、条件を今の振る舞いに合わせて書き直すか、違いを別の課題にする。 + ## 受け入れ条件とテストの対応 受け入れ条件は、そのままテストの名前になる粒度で書く。1 条件に複数の判定を詰め込むと、 diff --git a/plugins/ndf/skills/retrospective/SKILL.md b/plugins/ndf/skills/retrospective/SKILL.md index 602b35204..bf43cfa23 100644 --- a/plugins/ndf/skills/retrospective/SKILL.md +++ b/plugins/ndf/skills/retrospective/SKILL.md @@ -352,9 +352,11 @@ gh issue edit --repo "$RECORD_REPO" --body-file /tmp/issue-body.md ## スプリントを閉じる -**振り返りを通る変更では、この工程がその実行の最終工程である。** 記録を投稿した後に、 -`progress-tracking` の「スプリントを閉じる」を行う。**手順はそこが正本で、ここには写さない。** -ボードの `Status` を `Done` にするのも、課題を閉じるのも、その手順の中で行う。 +**振り返りを通る変更では、この工程がその実行の最終工程である。** 記録を投稿し、課題の本文に辿る経路の行を +足した後に、`progress-tracking` の「スプリントを閉じる」を `--final-stage 振り返り` で行う。辿る経路の行は、 +その手順が確かめる最終工程の記録である。**手順と閉じる条件はそこが正本で、ここには写さない。** +ボードの `Status` を `Done` にするのも、課題を閉じるのも、その手順の中で行う。まとめのプランの `retro` の +ステップでは課題を閉じない(次の `close` のステップが閉じる)。 ## 振り返りの範囲 diff --git a/plugins/ndf/skills/worktree/tests/test_agy_hooks.py b/plugins/ndf/skills/worktree/tests/test_agy_hooks.py deleted file mode 100644 index 6515e4c69..000000000 --- a/plugins/ndf/skills/worktree/tests/test_agy_hooks.py +++ /dev/null @@ -1,276 +0,0 @@ -"""agy の hook の経路を検証する(#215 の受け入れ条件 A4〜A6)。 - -agy は案内を作る時点と渡せる時点が離れている。**tool 実行前の hook がモデルへ文言を返す口 -(`reason`)は、拒否のときにしか働かない**(設計の決定 4 の実測)。NDF の誘導は操作を止め -ないため、案内はセッションの記録へ積み、次のモデル呼び出しの前に `PreInvocation` が -`injectSteps[].userMessage` で渡す。 - -セッション開始時にあたる事象も無いため、モデル呼び出しの通し番号(`invocationNum`)が 0 の -ときを開始時として扱う(決定 5)。 - -入力の形は agy 1.1.25 で実測したものである。tool の名前は `toolCall.name`、編集先は -`toolCall.args.TargetFile`、作業ディレクトリは `workspacePaths[0]`、セッションの識別子は -`conversationId` にある。**`write_file` は権限の名前であって tool の名前ではない。** -""" - -from __future__ import annotations - -import json -import os -import subprocess -from pathlib import Path - -import pytest - -from hook_lib import payload -from worktree_helpers import GUARD, SCRIPTS_DIR, SESSION, write_declaration - -HOOKS_JSON = SCRIPTS_DIR.parent / "dev.agy" / "hooks.json" -CONVERSATION = "c-0001" - - -def run_hook(script: Path, payload: dict, cwd: Path, tmpdir: Path) -> dict: - env = os.environ.copy() - env["LC_ALL"] = "C" - env["TMPDIR"] = str(tmpdir) - proc = subprocess.run( - ["bash", str(script)], - input=json.dumps(payload), - cwd=str(cwd), - env=env, - capture_output=True, - text=True, - ) - return {"rc": proc.returncode, "out": proc.stdout, "err": proc.stderr} - - -def edit_call(path: Path, cwd: Path, tool: str = "write_to_file") -> dict: - """agy の `PreToolUse` の入力。編集先は絶対パスで届く。""" - return { - "conversationId": CONVERSATION, - "stepIdx": 3, - "workspacePaths": [str(cwd)], - "modelName": "gemini-3.8-flash-high", - "toolCall": {"name": tool, "args": {"TargetFile": str(path), "CodeContent": "x"}}, - } - - -def command_call(command: str, cwd: Path) -> dict: - """`run_command` は実行ディレクトリを `Cwd` で渡す。""" - return { - "conversationId": CONVERSATION, - "stepIdx": 4, - "workspacePaths": [str(cwd)], - "toolCall": {"name": "run_command", "args": {"CommandLine": command, "Cwd": str(cwd)}}, - } - - -def invocation(cwd: Path, number: int = 1) -> dict: - """agy の `PreInvocation` の入力。""" - return { - "conversationId": CONVERSATION, - "invocationNum": number, - "initialNumSteps": 4, - "workspacePaths": [str(cwd)], - } - - -def declared(main_repo: Path) -> Path: - write_declaration(main_repo, json.dumps({"version": 1})) - return main_repo - - -def state_file(tmpdir: Path) -> Path: - return tmpdir / f"ndf-worktree-{CONVERSATION}.json" - - -def pending_of(tmpdir: Path) -> list[str]: - path = state_file(tmpdir) - if not path.is_file(): - return [] - return json.loads(path.read_text(encoding="utf-8")).get("pending", []) - - -def injected(result: dict) -> str: - """`injectSteps` の `userMessage` を連結して返す。無ければ空文字。""" - if not result["out"].strip(): - return "" - steps = json.loads(result["out"]).get("injectSteps", []) - return "\n".join(step.get("userMessage", "") for step in steps) - - -# --- A4: 誘導がモデルへ届く ------------------------------------------------- - - -def test_protected_edit_is_queued_and_allowed(main_repo: Path, tmp_path: Path) -> None: - """主ディレクトリの編集は記録へ積まれ、tool の実行は止まらない。""" - declared(main_repo) - tmpdir = tmp_path / "state" - tmpdir.mkdir() - target = main_repo / "plugins" / "ndf" / "README.md" - - result = run_hook(GUARD, edit_call(target, main_repo), cwd=main_repo, tmpdir=tmpdir) - - assert result["rc"] == 0, result["err"] - assert json.loads(result["out"]) == {"decision": "allow"} - queued = pending_of(tmpdir) - assert len(queued) == 1 - assert "plugins/ndf/README.md" in queued[0] - - -def test_pending_reaches_the_next_invocation(main_repo: Path, tmp_path: Path) -> None: - """積まれた案内は、次のモデル呼び出しの前に `userMessage` として渡される。""" - declared(main_repo) - tmpdir = tmp_path / "state" - tmpdir.mkdir() - target = main_repo / "plugins" / "ndf" / "README.md" - run_hook(GUARD, edit_call(target, main_repo), cwd=main_repo, tmpdir=tmpdir) - - result = run_hook(SESSION, invocation(main_repo), cwd=main_repo, tmpdir=tmpdir) - - assert result["rc"] == 0, result["err"] - assert "plugins/ndf/README.md" in injected(result) - - -def test_pending_is_delivered_once(main_repo: Path, tmp_path: Path) -> None: - """取り出した案内は記録から消える。同じ案内を毎回渡さない。""" - declared(main_repo) - tmpdir = tmp_path / "state" - tmpdir.mkdir() - run_hook( - GUARD, - edit_call(main_repo / "plugins" / "ndf" / "README.md", main_repo), - cwd=main_repo, - tmpdir=tmpdir, - ) - run_hook(SESSION, invocation(main_repo), cwd=main_repo, tmpdir=tmpdir) - - assert pending_of(tmpdir) == [] - again = run_hook(SESSION, invocation(main_repo, number=2), cwd=main_repo, tmpdir=tmpdir) - assert injected(again) == "" - - -@pytest.mark.parametrize("tool", ["write_to_file", "replace_file_content"]) -def test_edit_tools_are_covered(main_repo: Path, tmp_path: Path, tool: str) -> None: - """編集を伴う tool は 2 つとも誘導の対象になる。 - - `write_file` は権限の名前であり、`toolCall.name` には届かない(実測)。 - """ - declared(main_repo) - tmpdir = tmp_path / "state" - tmpdir.mkdir() - target = main_repo / "src" / "app.py" - - run_hook(GUARD, edit_call(target, main_repo, tool=tool), cwd=main_repo, tmpdir=tmpdir) - - assert any("src/app.py" in item for item in pending_of(tmpdir)) - - -def test_shell_write_is_covered(main_repo: Path, tmp_path: Path) -> None: - """`run_command` は書き込みの形から編集先を推定する。起点は `Cwd` である。""" - declared(main_repo) - tmpdir = tmp_path / "state" - tmpdir.mkdir() - - run_hook(GUARD, command_call("echo x > src/app.py", main_repo), cwd=main_repo, tmpdir=tmpdir) - - assert any("src/app.py" in item for item in pending_of(tmpdir)) - - -def test_first_invocation_reports_stray_changes(main_repo: Path, tmp_path: Path) -> None: - """通し番号が 0 のとき、主ディレクトリの逸脱を提示する(開始時にあたる)。""" - declared(main_repo) - (main_repo / "README.md").write_text("# changed\n", encoding="utf-8") - tmpdir = tmp_path / "state" - tmpdir.mkdir() - - result = run_hook(SESSION, invocation(main_repo, number=0), cwd=main_repo, tmpdir=tmpdir) - - assert "未コミット変更" in injected(result) - - -def test_later_invocation_does_not_repeat_the_report(main_repo: Path, tmp_path: Path) -> None: - """通し番号が 0 でないときは逸脱の提示を行わない。毎回の呼び出しで繰り返さない。""" - declared(main_repo) - (main_repo / "README.md").write_text("# changed\n", encoding="utf-8") - tmpdir = tmp_path / "state" - tmpdir.mkdir() - - result = run_hook(SESSION, invocation(main_repo, number=3), cwd=main_repo, tmpdir=tmpdir) - - assert injected(result) == "" - assert json.loads(result["out"]) == {} - - -# --- A5: 作業ツリーの中では出さない ----------------------------------------- - - -def test_edit_inside_the_worktree_queues_nothing(main_repo: Path, worktree: Path, tmp_path: Path) -> None: - """作業ツリーの中の編集は記録へ積まれない。""" - declared(main_repo) - tmpdir = tmp_path / "state" - tmpdir.mkdir() - target = worktree / "plugins" / "ndf" / "README.md" - - result = run_hook(GUARD, edit_call(target, worktree), cwd=worktree, tmpdir=tmpdir) - - assert result["rc"] == 0, result["err"] - assert pending_of(tmpdir) == [] - - -def test_allowed_path_queues_nothing(main_repo: Path, tmp_path: Path) -> None: - """許可されたパスの編集は記録へ積まれない。""" - declared(main_repo) - tmpdir = tmp_path / "state" - tmpdir.mkdir() - - run_hook(GUARD, edit_call(main_repo / "docs" / "note.md", main_repo), cwd=main_repo, tmpdir=tmpdir) - - assert pending_of(tmpdir) == [] - - -# --- A6: 宣言が無いリポジトリでは何もしない --------------------------------- - - -def test_no_declaration_produces_no_output(main_repo: Path, tmp_path: Path) -> None: - """`.ndf/worktree.json` が無ければ、どちらの hook も何も出力しない。""" - tmpdir = tmp_path / "state" - tmpdir.mkdir() - target = main_repo / "plugins" / "ndf" / "README.md" - - guard = run_hook(GUARD, edit_call(target, main_repo), cwd=main_repo, tmpdir=tmpdir) - session = run_hook(SESSION, invocation(main_repo, number=0), cwd=main_repo, tmpdir=tmpdir) - - assert guard["rc"] == 0 and guard["out"].strip() == "" - assert session["rc"] == 0 and session["out"].strip() == "" - - -# --- hook の結線 ------------------------------------------------------------- - - -def test_hooks_json_matcher_matches_the_library() -> None: - """`dev.agy/hooks.json` の `matcher` は共通ライブラリの一覧から作る。 - - tool 名の一覧を 2 箇所に持つと、判定を足しても hook が起動しない状態になる。 - """ - config = json.loads(HOOKS_JSON.read_text(encoding="utf-8")) - matchers = {entry.get("matcher") for hook in config.values() for entry in hook.get("PreToolUse", [])} - assert payload.tool_matcher() in matchers, matchers - - -def test_hooks_json_wires_both_scripts() -> None: - """`PreToolUse` は誘導を、`PreInvocation` は開始時と案内の受け渡しを担う。 - - `PreInvocation` は matcher を取らず、handler の配列を直に持つ(agy の書式)。 - """ - config = json.loads(HOOKS_JSON.read_text(encoding="utf-8")) - pre_tool = [hook["command"] for definition in config.values() for entry in definition.get("PreToolUse", []) for hook in entry["hooks"]] - pre_invocation = [handler["command"] for definition in config.values() for handler in definition.get("PreInvocation", [])] - assert any("worktree-guard.sh" in c for c in pre_tool), pre_tool - assert any("worktree-session.sh" in c for c in pre_invocation), pre_invocation - - -@pytest.mark.parametrize("tool", ["write_to_file", "replace_file_content"]) -def test_agy_tool_names_are_in_the_library(tool: str) -> None: - """agy の tool 名は共通ライブラリの一覧に載っている。""" - assert tool in payload.EDIT_TOOLS, payload.EDIT_TOOLS diff --git a/plugins/ndf/skills/worktree/tests/test_hook_wiring.py b/plugins/ndf/skills/worktree/tests/test_hook_wiring.py index 49390b2ca..c801697bb 100644 --- a/plugins/ndf/skills/worktree/tests/test_hook_wiring.py +++ b/plugins/ndf/skills/worktree/tests/test_hook_wiring.py @@ -16,7 +16,6 @@ from worktree_helpers import SCRIPTS_DIR HOOKS_DIR = SCRIPTS_DIR.parent / "hooks" -INSTALL_SH = SCRIPTS_DIR.parent / "dev.kiro" / "install.sh" def guard_entries(runtime: str) -> list[dict]: @@ -43,12 +42,3 @@ def test_sessionstart_runs_the_session_script(runtime: str) -> None: config = json.loads((HOOKS_DIR / f"{runtime}.json").read_text(encoding="utf-8")) commands = [hook["command"] for entry in config["hooks"]["SessionStart"] for hook in entry["hooks"]] assert any("worktree-session.sh" in c for c in commands), commands - - -def test_kiro_installer_wires_both_hooks() -> None: - """Kiro CLI は導入スクリプトがエージェント定義へ書き込む。""" - body = INSTALL_SH.read_text(encoding="utf-8") - assert "worktree-session.sh" in body - assert "worktree-guard.sh" in body - assert '"userPromptSubmit"' in body - assert 'hooks.setdefault("agentSpawn"' in body diff --git a/plugins/playwright-kit/README.md b/plugins/playwright-kit/README.md index b83d17fcf..15c024d66 100644 --- a/plugins/playwright-kit/README.md +++ b/plugins/playwright-kit/README.md @@ -1,9 +1,8 @@ # playwright-kit Plugin -Playwright による E2E テストの計画・実装・証跡管理を提供するプラグインです。3 ランタイム分の +Playwright による E2E テストの計画・実装・証跡管理を提供するプラグインです。Claude Code と Codex の 配布物を `plugins/playwright-kit/` の 1 ディレクトリにまとめており、Skill の実体は `skills/` の -1 箇所だけです。ランタイム固有のファイルは `.claude-plugin/`(Claude Code)と `dev.kiro/` -(Kiro CLI)に分かれ、Codex はルートの `plugin.json`(Agent Plugins 形式)を読みます。 +1 箇所だけです。Claude Code は `.claude-plugin/` を、Codex はルートの `plugin.json`(Agent Plugins 形式)を読みます。 NDF プラグイン v6.1.0 までは NDF に同梱していました。**Skill 名は変えていない**ため、 `/playwright-` まで打てば従来どおり 4 個が候補に出ます。 @@ -36,24 +35,6 @@ codex plugin marketplace add devbasex/ai-plugins codex plugin add playwright-kit@ai-plugins ``` -### Kiro CLI - -Kiro にはプラグイン機構がないため、installer が `.kiro/skills/` へ symlink を張ります。 - -```bash -# プロジェクトへ導入(既定) -bash plugins/playwright-kit/dev.kiro/install.sh - -# ホーム配下へ導入 -bash plugins/playwright-kit/dev.kiro/install.sh --scope global - -# 書き込みを行わず内容だけ確認 -bash plugins/playwright-kit/dev.kiro/install.sh --dry-run -``` - -NDF の installer と違い、エージェント定義・常時指示・プロンプト・フックは扱いません -(このプラグインは Skill だけを配ります)。 - ### 更新 ```bash @@ -64,9 +45,6 @@ NDF の installer と違い、エージェント定義・常時指示・プロ # Codex codex plugin marketplace upgrade ai-plugins codex plugin add playwright-kit@ai-plugins - -# Kiro CLI -bash plugins/playwright-kit/dev.kiro/install.sh ``` ## 更新情報 diff --git a/plugins/playwright-kit/dev.kiro/install.sh b/plugins/playwright-kit/dev.kiro/install.sh deleted file mode 100755 index 06d2dbd82..000000000 --- a/plugins/playwright-kit/dev.kiro/install.sh +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env bash -# playwright-kit Plugin Installer for Kiro CLI -# Usage: bash plugins/playwright-kit/dev.kiro/install.sh [--project PATH] [--scope workspace|global] [--dry-run] -# -# Kiro CLI にはプラグイン機構がないため、プラグインの skills/ を導入先の skills/ へ symlink する。 -# NDF の installer と違い、エージェント定義・常時指示・プロンプト・フックは扱わない -# (この plugin は Skill だけを配る)。 -# -# このスクリプトは Agent Plugins 仕様 §8.2 のクライアント拡張ディレクトリ(dev.kiro/)に -# 置く。プラグインルートは 1 つ上の階層になる。 -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -PLUGIN_SKILLS_DIR="$PLUGIN_DIR/skills" -MANIFEST_FILE="$PLUGIN_DIR/plugin.json" - -PROJECT_ROOT="$(pwd)" -PROJECT_GIVEN=false -SCOPE="workspace" -DRY_RUN=false - -while [ "$#" -gt 0 ]; do - case "$1" in - --project) - [ "$#" -ge 2 ] || { echo "ERROR: --project requires a path" >&2; exit 2; } - PROJECT_ROOT="$(cd "$2" && pwd)" - PROJECT_GIVEN=true - shift - ;; - --scope) - [ "$#" -ge 2 ] || { echo "ERROR: --scope requires workspace or global" >&2; exit 2; } - SCOPE="$2" - shift - ;; - --dry-run) DRY_RUN=true ;; - --help|-h) - echo "Usage: bash plugins/playwright-kit/dev.kiro/install.sh [OPTIONS]" - echo "" - echo "Options:" - echo " --project PATH install into PATH instead of current directory (--scope workspace のみ)" - echo " --scope SCOPE workspace(既定, プロジェクトの .kiro/)または global(~/.kiro/)" - echo " --dry-run 書き込みを行わず実行内容を表示" - echo " -h, --help このヘルプを表示" - exit 0 - ;; - *) echo "ERROR: unknown option: $1" >&2; exit 2 ;; - esac - shift -done - -case "$SCOPE" in - workspace) - KIRO_DIR="$PROJECT_ROOT/.kiro" - ;; - global) - [ -n "${HOME:-}" ] || { echo "ERROR: --scope global には HOME が必要です" >&2; exit 2; } - KIRO_DIR="$HOME/.kiro" - if [ "$PROJECT_GIVEN" = true ]; then - echo "WARN: --scope global では --project は使用されません" >&2 - fi - ;; - *) - echo "ERROR: invalid --scope: $SCOPE (workspace|global)" >&2 - exit 2 - ;; -esac - -SKILLS_DIR="$KIRO_DIR/skills" - -echo "=== playwright-kit Plugin Installer for Kiro CLI ===" -echo " スコープ: $SCOPE ($KIRO_DIR)" - -if [ ! -d "$PLUGIN_SKILLS_DIR" ]; then - echo "ERROR: $PLUGIN_SKILLS_DIR が見つかりません" >&2 - exit 1 -fi -# 版数はルートマニフェストだけが持つ。Kiro 向けに複製した VERSION ファイルは置かない -# (同じ値を 2 箇所に置くと、片方だけが古くなる)。 -if [ ! -f "$MANIFEST_FILE" ]; then - echo "ERROR: $MANIFEST_FILE が見つかりません" >&2 - exit 1 -fi -PLUGIN_VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$MANIFEST_FILE")" -if [ -z "$PLUGIN_VERSION" ]; then - echo "ERROR: $MANIFEST_FILE から版数を読み取れません" >&2 - exit 1 -fi - -# 既存リンクの掃除は、このプラグイン配下を指すものだけに限る。 -# 利用者が置いた実体ディレクトリや他プラグインのリンクには触れない。 -if [ "$DRY_RUN" = false ]; then - mkdir -p "$SKILLS_DIR" - plugin_skills_abs="$(realpath -m "$PLUGIN_SKILLS_DIR")" - while IFS= read -r skill_link; do - target="$(readlink "$skill_link")" - case "$target" in - /*) target_abs="$target" ;; - *) target_abs="$(realpath -m "$(dirname "$skill_link")/$target")" ;; - esac - case "$target_abs" in - "$plugin_skills_abs"/*) rm -f "$skill_link" ;; - esac - done < <(find "$SKILLS_DIR" -mindepth 1 -maxdepth 1 -type l | sort) -fi - -SKILL_COUNT=0 -while IFS= read -r src_dir; do - skill_name="$(basename "$src_dir")" - if [ ! -f "$src_dir/SKILL.md" ]; then - echo " SKIP: $skill_name (SKILL.mdなし)" - continue - fi - if [ -e "$SKILLS_DIR/$skill_name" ] && [ ! -L "$SKILLS_DIR/$skill_name" ]; then - echo " WARN: $SKILLS_DIR/$skill_name はシンボリックリンクではありません。" >&2 - echo " 内容を確認のうえ手動で退避または削除してください。" >&2 - continue - fi - if [ "$DRY_RUN" = false ]; then - ln -sfn "$PLUGIN_SKILLS_DIR/$skill_name" "$SKILLS_DIR/$skill_name" - fi - echo " linked: $skill_name" - SKILL_COUNT=$((SKILL_COUNT + 1)) -done < <(find "$PLUGIN_SKILLS_DIR" -mindepth 1 -maxdepth 1 -type d | sort) - -if [ "$DRY_RUN" = true ]; then - echo "" - echo "DRY RUN: 書き込みは行いませんでした" - echo " playwright-kit バージョン: $PLUGIN_VERSION" - echo " Skills数: $SKILL_COUNT (配置先: $SKILLS_DIR)" - exit 0 -fi - -echo "" -echo "=== インストール完了 ===" -echo " playwright-kit バージョン: $PLUGIN_VERSION" -echo " Skills数: $SKILL_COUNT (シンボリックリンク: $SKILLS_DIR)" -echo "" -echo "playwright_kit の実行環境は playwright-kit-ops skill の手順で用意してください。" diff --git a/plugins/playwright-kit/manifests/kiro-skills.txt b/plugins/playwright-kit/manifests/kiro-skills.txt deleted file mode 100644 index c6e9e5661..000000000 --- a/plugins/playwright-kit/manifests/kiro-skills.txt +++ /dev/null @@ -1,4 +0,0 @@ -playwright-planning -playwright-authoring -playwright-evidence -playwright-kit-ops diff --git a/scripts/build-runtime-plugins.sh b/scripts/build-runtime-plugins.sh index 3344d3f76..ed4498d69 100755 --- a/scripts/build-runtime-plugins.sh +++ b/scripts/build-runtime-plugins.sh @@ -9,8 +9,7 @@ usage() { Usage: bash scripts/build-runtime-plugins.sh [--check] Generate the tracked build outputs: Codex implicit-invocation policies for -single-directory plugins, the agy skill links and agent definitions under -dev.agy/, and the runtime copies of MCP plugins. +single-directory plugins. Options: --check Compare generated output with the working tree and fail on drift. @@ -38,287 +37,15 @@ sync_codex_skill_policies() { python3 "$ROOT_DIR/scripts/lib/codex_skill_policies.py" "$plugin_dir/skills" "$manifest" "$CHECK" } -# agy は配る Skill を絞る手段を利用者側の設定にしか持たない(`plugins.json` の絞り込みは -# プラグインのディレクトリ名に掛かり、同梱の Skill には届かない)。そこで絞り込みは -# dev.agy/skills/ へ何を並べるかで表す。並べる内容は manifests/agy-skills.txt が決める。 -# hook の実体は 4 ランタイムで共有するため、同じ位置へ symlink を張る。エージェント定義は -# agy の受け付ける `model` へ変えた写しを置く(sync_agy_agents)。 -# Agent Plugins 仕様 §8.2 のクライアント拡張ディレクトリに当たる。 -sync_agy_skill_links() { - local family="$1" - local plugin_dir="$ROOT_DIR/plugins/$family" - - [ -f "$plugin_dir/manifests/agy-skills.txt" ] || return 0 - python3 - "$plugin_dir" "$CHECK" <<'PY' -import os -import sys -from pathlib import Path - -plugin_dir, check = Path(sys.argv[1]), sys.argv[2] == "true" -dev_dir = plugin_dir / "dev.agy" -links_dir = dev_dir / "skills" - -published = [ - line.split("#", 1)[0].strip() - for line in (plugin_dir / "manifests/agy-skills.txt").read_text(encoding="utf-8").splitlines() - if line.split("#", 1)[0].strip() -] - -# 生成する symlink と、その向き先。skills/ は基準が決め、scripts は固定である。 -expected = {links_dir / name: f"../../skills/{name}" for name in published} -expected.update({dev_dir / "scripts": "../scripts"}) - -actual = set() -if links_dir.is_dir(): - actual |= set(links_dir.iterdir()) -if (dev_dir / "scripts").is_symlink(): - actual.add(dev_dir / "scripts") -stale = sorted(actual - set(expected)) - -failed = False - - -def report(message: str) -> None: - global failed - print(message, file=sys.stderr) - failed = True - - -for path, target in sorted(expected.items()): - if check: - if not path.is_symlink(): - report(f"Generated link missing: {path}") - elif os.readlink(path) != target: - report(f"Generated link points elsewhere: {path} -> {os.readlink(path)}") - continue - path.parent.mkdir(parents=True, exist_ok=True) - if path.is_symlink() or path.exists(): - path.unlink() - path.symlink_to(target) - -for path in stale: - if check: - report(f"Generated link is stale: {path}") - else: - path.unlink() - -raise SystemExit(1 if failed else 0) -PY -} - -# Kiro CLI にはプラグイン機構が無いため、MCP プラグインごとに installer を置く。内容は -# プラグイン名に依存しないので、雛形を 1 つ持って各プラグインの dev.kiro/ へ書き出す。 -# Agent Plugins 仕様 §8.2 のクライアント拡張ディレクトリに当たる。 -write_kiro_mcp_installer() { - local plugin_dir="$1" - local installer="$plugin_dir/dev.kiro/install.sh" - local tmp - - tmp="$(mktemp)" - cat > "$tmp" <<'EOF' -#!/usr/bin/env bash -# MCP Plugin Installer for Kiro CLI -# Usage: bash plugins/mcp/<プラグイン名>/dev.kiro/install.sh [--project PATH] [--dry-run] -# -# scripts/build-runtime-plugins.sh が生成します。直接編集しないでください。 -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -PLUGIN_NAME="$(basename "$PLUGIN_DIR")" -PROJECT_ROOT="$(pwd)" -KIRO_DIR="$PROJECT_ROOT/.kiro" -KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" -KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" -KIRO_SKILLS_DIR="$KIRO_DIR/skills" -TARGET_MCP="$PROJECT_ROOT/.mcp.json" -SOURCE_MCP="$PLUGIN_DIR/.mcp.json" -SOURCE_HOOKS="$PLUGIN_DIR/hooks/hooks.json" -SOURCE_SKILLS="$PLUGIN_DIR/skills" -DRY_RUN=false - -usage() { - cat <<'USAGE' -Usage: bash install.sh [--project PATH] [--dry-run] - -Install this MCP plugin into the current project's Kiro settings. -USAGE -} - -while [ "$#" -gt 0 ]; do - case "$1" in - --project) - [ "$#" -ge 2 ] || { echo "ERROR: --project requires a path" >&2; exit 2; } - PROJECT_ROOT="$(cd "$2" && pwd)" - KIRO_DIR="$PROJECT_ROOT/.kiro" - KIRO_AGENT_FILE="$KIRO_DIR/agents/default.json" - KIRO_PLUGIN_LINK="$KIRO_DIR/mcp_runtime/$PLUGIN_NAME" - KIRO_SKILLS_DIR="$KIRO_DIR/skills" - TARGET_MCP="$PROJECT_ROOT/.mcp.json" - shift - ;; - --dry-run) DRY_RUN=true ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; - esac - shift -done - -if [ ! -f "$SOURCE_MCP" ]; then - echo "ERROR: source .mcp.json not found: $SOURCE_MCP" >&2 - exit 1 -fi - -if [ "$DRY_RUN" = true ]; then - echo "Would merge $SOURCE_MCP into $TARGET_MCP" - echo "Would link plugin runtime into $KIRO_PLUGIN_LINK" - if [ -d "$SOURCE_SKILLS" ]; then - echo "Would link skills from $SOURCE_SKILLS into $KIRO_SKILLS_DIR" - fi - if [ -f "$SOURCE_HOOKS" ]; then - echo "Would merge hooks from $SOURCE_HOOKS into $KIRO_AGENT_FILE" - fi - exit 0 -fi - -mkdir -p "$KIRO_DIR/mcp_runtime" "$KIRO_SKILLS_DIR" "$(dirname "$KIRO_AGENT_FILE")" -if [ -e "$KIRO_PLUGIN_LINK" ] && [ ! -L "$KIRO_PLUGIN_LINK" ]; then - echo "ERROR: $KIRO_PLUGIN_LINK already exists and is not a symlink" >&2 - exit 1 -fi -ln -sfn "$PLUGIN_DIR" "$KIRO_PLUGIN_LINK" - -if [ -d "$SOURCE_SKILLS" ]; then - find "$SOURCE_SKILLS" -mindepth 1 -maxdepth 1 -type d | sort | while IFS= read -r skill_dir; do - skill_name="$(basename "$skill_dir")" - skill_link="$KIRO_SKILLS_DIR/$skill_name" - [ -f "$skill_dir/SKILL.md" ] || continue - if [ -e "$skill_link" ] && [ ! -L "$skill_link" ]; then - echo "ERROR: $skill_link already exists and is not a symlink" >&2 - exit 1 - fi - ln -sfn "$skill_dir" "$skill_link" - echo "Linked Kiro skill: $skill_name" - done -fi - -python3 - "$SOURCE_MCP" "$TARGET_MCP" "$SOURCE_HOOKS" "$KIRO_AGENT_FILE" "$KIRO_PLUGIN_LINK" <<'PY' -import json -import sys -from pathlib import Path - -source_path = Path(sys.argv[1]) -target_path = Path(sys.argv[2]) -hooks_path = Path(sys.argv[3]) -agent_path = Path(sys.argv[4]) -plugin_link = sys.argv[5] -source = json.loads(source_path.read_text()) -target = json.loads(target_path.read_text()) if target_path.exists() else {} -source_servers = source.get("mcpServers", {}) -target.setdefault("mcpServers", {}) -target["mcpServers"].update(source_servers) -target_path.write_text(json.dumps(target, indent=2, ensure_ascii=False) + "\n") - -if agent_path.exists(): - agent = json.loads(agent_path.read_text()) -else: - agent = { - "name": "default", - "resources": ["skill://.kiro/skills/**/SKILL.md"], - } - -# Kiro は tools に挙がったツールだけをモデルへ見せる。欄が無いと MCP のツールを -# 呼べない(#1873)。利用者が書いた tools は上書きしない。 -agent.setdefault("tools", ["*"]) -agent.setdefault("mcpServers", {}) -agent["mcpServers"].update(source_servers) - -if hooks_path.exists(): - source_hooks = json.loads(hooks_path.read_text()).get("hooks", {}) - target_hooks = agent.setdefault("hooks", {}) - event_map = { - "SessionStart": "agentSpawn", - "Stop": "stop", - } - # Kiro にはプラグインルートを示す環境変数が無い。hook の command が使う - # プレースホルダを、installer が張った symlink の絶対パスへ置き換える。 - placeholder = "${PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}" - for source_event, groups in source_hooks.items(): - target_event = event_map.get(source_event, source_event) - target_list = target_hooks.setdefault(target_event, []) - existing = { - entry.get("command") - for entry in target_list - if isinstance(entry, dict) - } - for group in groups: - for hook in group.get("hooks", []): - command = hook.get("command") - if not command: - continue - command = command.replace(placeholder, plugin_link) - if command in existing: - continue - entry = {"command": command} - if "timeout_ms" in hook: - entry["timeout_ms"] = hook["timeout_ms"] - target_list.append(entry) - existing.add(command) - -agent_path.write_text(json.dumps(agent, indent=2, ensure_ascii=False) + "\n") - -print(f"Installed MCP servers: {', '.join(sorted(source_servers))}") -print(f"Updated: {target_path}") -print(f"Updated: {agent_path}") -PY -EOF - - if [ "$CHECK" = true ]; then - if [ ! -f "$installer" ] || ! diff -q "$tmp" "$installer" >/dev/null; then - echo "Generated file is out of date: ${installer#$ROOT_DIR/}" >&2 - rm -f "$tmp" - return 1 - fi - rm -f "$tmp" - return 0 - fi - mkdir -p "$(dirname "$installer")" - mv "$tmp" "$installer" - chmod +x "$installer" -} - -# agy 1.4 系は `model` に tier 名しか受け付けないため、エージェント定義は symlink で共有せず、 -# `model` だけを tier 名へ変えた写しを dev.agy/agents/ へ置く(#2087)。 -sync_agy_agents() { - local family="$1" - local plugin_dir="$ROOT_DIR/plugins/$family" - - [ -d "$plugin_dir/dev.agy" ] || return 0 - python3 "$ROOT_DIR/scripts/lib/agy_agents.py" "$plugin_dir" "$CHECK" -} - -sync_mcp_plugins() { - local plugin_dir - - for plugin_dir in "$ROOT_DIR"/plugins/mcp/*; do - [ -f "$plugin_dir/.mcp.json" ] || continue - write_kiro_mcp_installer "$plugin_dir" - done -} - for dir in "$ROOT_DIR"/plugins/*; do [ -d "$dir/manifests" ] || continue family="$(basename "$dir")" case "$family" in - *-shared|*-claude|*-codex|*-kiro) continue ;; + *-shared|*-claude|*-codex) continue ;; esac sync_codex_skill_policies "$family" - sync_agy_skill_links "$family" - sync_agy_agents "$family" done -sync_mcp_plugins - if [ "$CHECK" = true ]; then echo "runtime plugin generated files are up to date" else diff --git a/scripts/check-cross-skill-refs.py b/scripts/check-cross-skill-refs.py index 5ec706235..9f03bce8d 100755 --- a/scripts/check-cross-skill-refs.py +++ b/scripts/check-cross-skill-refs.py @@ -2,7 +2,7 @@ """Skill の境界をまたぐ**配布に依存する参照**を数え、例外の一覧に無いものを失敗として返す。 Skill は配布の基準(`plugins/ndf/manifests/*-skills.txt`)で 1 つずつ配るかを決める。 -**配る Skill を絞る配布先(agy)では、基準に無い Skill が配布した先から消える。** +**配る Skill を絞る配布先(今は Codex。Claude Code より 4 本少ない)では、基準に無い Skill が配布した先から消える。** 別の Skill の `scripts/` や `references/` を読み込む・起動する・`sys.path` へ入れる参照は、 その相手を配らない配布先で解決できない。**実行するか読むかは、相手が欠けたときの結果を 変えない。** diff --git a/scripts/check-doc-staleness.py b/scripts/check-doc-staleness.py index 3ee0729f3..e9036a8fd 100644 --- a/scripts/check-doc-staleness.py +++ b/scripts/check-doc-staleness.py @@ -59,17 +59,10 @@ def plugin_json_path(name: str) -> str: return f"plugins/{name}/.claude-plugin/plugin.json" -# `README.md` はランタイムを Claude Code / Kiro / Codex / agy の順、`plugins/ndf/README.md` は -# Claude Code / Codex / Kiro CLI / agy の順で書いている。位置ではなく名前で対応づける。表記も -# 2 本で違う(`Kiro` と `Kiro CLI`)ため、対応表を文書ごとに持つ。**agy はどちらも同じ表記で -# ある。** コマンド名がそのまま配布先の名前であり、製品名で言い換えると読み手が結び付けられない。 -ROOT_README_RUNTIMES = {"Claude Code": "claude", "Kiro": "kiro", "Codex": "codex", "agy": "agy"} -PLUGIN_README_RUNTIMES = { - "Claude Code": "claude", - "Codex": "codex", - "Kiro CLI": "kiro", - "agy": "agy", -} +# 配布先は Claude Code と Codex の 2 つで、`README.md`・`plugins/ndf/README.md` のどちらも +# この順で書いている。位置ではなく名前で対応づけ、表記が分かれたときに備えて対応表を文書ごとに持つ。 +ROOT_README_RUNTIMES = {"Claude Code": "claude", "Codex": "codex"} +PLUGIN_README_RUNTIMES = {"Claude Code": "claude", "Codex": "codex"} def runtime_alternation(labels: dict[str, str]) -> str: @@ -232,7 +225,7 @@ class RepositoryMetrics: source: str -# 点で照合する版数記載(G・K・L・M)の一覧。点の照合を足すときはここへ 1 行足す。 +# 点で照合する版数記載(G・L・M)の一覧。点の照合を足すときはここへ 1 行足す。 # 同じ文書の中では並びの順に報告する。 POINT_VERSION_SPECS: list[PointVersionSpec] = [ PointVersionSpec( @@ -241,12 +234,6 @@ class RepositoryMetrics: "**NDFプラグイン v<版>**", re.compile(r"\*\*NDFプラグイン v" + VERSION + r"\*\*"), ), # G - PointVersionSpec( - PLUGIN_README, - "Kiro の確認例の版数", - "(Kiro CLI用 / v<版>)", - re.compile(r"Kiro CLI用 / v" + VERSION + r")"), - ), # K PointVersionSpec( PLUGIN_README, "Codex のキャッシュパスの例の版数", @@ -782,7 +769,7 @@ def main() -> int: root = Path(args.root).resolve() report = Report() - counts = {runtime: manifest_skill_count(root, runtime, report) for runtime in ("claude", "codex", "kiro", "agy")} + counts = {runtime: manifest_skill_count(root, runtime, report) for runtime in ("claude", "codex")} skills = skill_dir_count(root, SKILLS_DIR, report) version = plugin_version(root, report) diff --git a/scripts/check-skill-frontmatter.py b/scripts/check-skill-frontmatter.py index d8aba6cd5..9f1dee701 100644 --- a/scripts/check-skill-frontmatter.py +++ b/scripts/check-skill-frontmatter.py @@ -7,7 +7,7 @@ チェックは 3 種類に分かれる。 - **individual** — Skill 単位。仕様準拠・安全性・可搬性・運用 -- **aggregate** — 配布先ごとの初期一覧予算(Claude Code / Codex / Kiro CLI / agy)と +- **aggregate** — 配布先ごとの初期一覧予算(Claude Code / Codex)と frontmatter 総量。 予算はチェック対象の plugin family すべての合計に対して判定する - **cross** — Skill 間。トリガ語の重複と、既知の外部 Skill 名との衝突 @@ -95,36 +95,15 @@ # Codex(公式ドキュメント "Build skills"): # "at most 2% of the model's context window, or 8,000 characters when the # context window is unknown" -# 既定モデル gpt-5.6-sol のコンテキストは 272,000(kiro-cli --list-models で実測)。 +# 既定モデル gpt-5.6-sol のコンテキストは 272,000(kiro-cli --list-models で実測した過去の記録)。 # 2% = 5,440 トークン。コンテキスト長が判明しているため 8,000 のフォールバックは使わない。 # 比率は 2 倍でもコンテキストが 1/3.7 のため、**予算は Claude Code の約半分**にしかならず、 # 実質ここが全体の制約になる。 -# -# Kiro: 公式ドキュメント(kiro.dev/docs/skills)に一覧予算の規定が無い。 -# 既定モデル auto のコンテキストは 1,000,000。**この値は過去の実測の記録である。** -# 当時は `kiro-cli --list-models` で一覧を取れたが、2.20.1 にそのオプションは無く、 -# 同じ手段では再検証できない(`--list` は認証を要求する)。値を更新するときは、 -# その時点で一覧を取れる手段を確かめ、この行の出典も書き直すこと。Claude Code の -# Opus 5 と同じである。**NDF は Kiro でも 1M コンテキストのモデルだけを対象とする** -# (`plugins/ndf/dev.kiro/install.sh` が導入時にこの前提を出力する。モデルの一覧を -# 取る手段が kiro-cli の版によって変わるため、機械でのチェックは置いていない)。 -# 規定が無い以上どこかから基準を借りるほかなく、コンテキスト長が同じ Claude Code の -# 1% を当てる。憶測で独自の値を置くより、同じ土俵の実在する規定へ揃えるほうが根拠が残る。 -# -# agy(Antigravity CLI): 公式ドキュメント(antigravity.google/docs/skills)に一覧予算の -# 規定が無い。既定のモデルは Gemini 3.x Flash 系(`agy models` の先頭が -# `gemini-3.8-flash-high`。1.1.25 で実測)で、コンテキスト長を出す手段は見当たらない -# (`agy models` は名前と表示名だけを返し、`/context` にあたるコマンドも無い)。 -# **この 2 つの値は Claude Code から借りたものである。** Gemini 3.x Flash の -# コンテキスト長 1,000,000 は Claude Code の Opus 5 と同じであり、規定が無い以上 -# どこかから基準を借りるほかない。憶測で独自の値を置くより、同じコンテキスト長を持つ -# 実在する規定へ揃えるほうが根拠が残る(Kiro CLI と同じ扱い)。 # 一覧に何が載るかはランタイムごとに違う。**公式に記述があるのは Claude Code と Codex # だけ**である。 # Claude Code: "loads a listing of skill names and descriptions into context" # Codex: "In Codex, the initial list also includes each skill's file path." -# Kiro CLI と agy は一覧の構成も公式に記述が無いため、多い側(パスを含む)で見積もる。 class Listing(NamedTuple): @@ -136,12 +115,9 @@ class Listing(NamedTuple): includes_path: bool # 一覧に Skill のファイルパスが載るか -# Kiro と agy の割合は Claude Code から借りる(上の Kiro・agy のコメント) LISTINGS = { "claude": Listing(1_000_000, 0.01, "error", False), "codex": Listing(272_000, 0.02, "error", True), - "kiro": Listing(1_000_000, 0.01, "error", True), - "agy": Listing(1_000_000, 0.01, "error", True), } # 全 Skill の frontmatter 合計。**plugin family をまたいで合計する**(利用者の環境では @@ -550,7 +526,7 @@ def load_manifests(skills_dir: pathlib.Path) -> dict[str, set[str]]: """ man_dir = skills_dir.parent / "manifests" out: dict[str, set[str]] = {} - for runtime in ("claude", "codex", "kiro", "agy"): + for runtime in ("claude", "codex"): f = man_dir / f"{runtime}-skills.txt" if not f.exists(): continue @@ -788,7 +764,7 @@ def _discover_skills_dirs() -> list[pathlib.Path]: for d in sorted(pathlib.Path("plugins").glob("*")): if not (d / "manifests").is_dir() or not (d / "skills").is_dir(): continue - if d.name.endswith(("-claude", "-codex", "-kiro")): + if d.name.endswith(("-claude", "-codex")): continue found.append(d / "skills") # どの manifest にも載せない Skill も規約のチェックは受ける。配らないだけで、 diff --git a/scripts/check-skill-repo-assumptions.py b/scripts/check-skill-repo-assumptions.py index 2f3887337..649c553f6 100755 --- a/scripts/check-skill-repo-assumptions.py +++ b/scripts/check-skill-repo-assumptions.py @@ -111,7 +111,7 @@ "plugins/ndf/scripts/project_lib/model.py": "NDF 自身の宣言の形(project.schema.json)の URL。対象リポジトリを指していない", } -RUNTIMES = ("claude", "codex", "kiro", "agy") +RUNTIMES = ("claude", "codex") def canon(path: pathlib.Path | str) -> str: diff --git a/scripts/ci-heavy-skip.py b/scripts/ci-heavy-skip.py index f98e5af81..7c3a61e3b 100644 --- a/scripts/ci-heavy-skip.py +++ b/scripts/ci-heavy-skip.py @@ -52,7 +52,7 @@ ) MANIFEST_RE = re.compile( r"^(\.claude-plugin/marketplace\.json" - r"|plugins/.+/(\.claude-plugin/|\.codex-plugin/|dev\.agy/)?plugin\.json)$" + r"|plugins/.+/(\.claude-plugin/|\.codex-plugin/)?plugin\.json)$" ) MANIFEST_LINE_RE = re.compile(r'^\s*"(version|description)"\s*:') ANCESTOR_LIMIT = 20 diff --git a/scripts/lib/agy_agents.py b/scripts/lib/agy_agents.py deleted file mode 100644 index ce1e36be0..000000000 --- a/scripts/lib/agy_agents.py +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env python3 -"""agy に配るエージェント定義(`dev.agy/agents/*.md`)を生成・確認する(`scripts/build-runtime-plugins.sh` の一部)。 - -agy 1.4 系はエージェント定義の `model` に tier 名(`flash` / `pro` / `inherit`)しか受け付けず、 -Claude Code の型の名前(`sonnet` / `opus`)を持つ定義を読み飛ばす(#2087)。そこで `agents/*.md` を写し、 -frontmatter の `model:` の 1 行だけを対応表で tier 名へ置き換える。YAML として読み直さないのは、 -写しと元の差を `model:` の 1 行に限ってバイト列で確かめるためである(設計の決定 3)。 - - python3 scripts/lib/agy_agents.py - -2 つ目が `true` なら書かずに確かめ、食い違いを標準エラーに出して終了コード 1 で終わる。 -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -# agy 1.4 系が `model` に受け付ける tier 名。欄が無いときは agy の既定(`inherit`)になる。 -AGY_TIERS = frozenset({"flash", "pro", "inherit"}) - -# Claude Code の型の名前 → agy の tier 名。安い型は安い tier へ、高い型は高い tier へ写す(設計の決定 2)。 -# ここに無い値は生成を止める(設計の決定 4)。足すときは 1 行足す。 -MODEL_MAP = { - "sonnet": "flash", - "opus": "pro", - "haiku": "flash", - "inherit": "inherit", -} - - -class UnknownModel(Exception): - """対応表に無い `model` の値。""" - - -def model_value(line: str) -> str: - """`model: <値>` の行から値を取り出す(引用符は外す)。""" - value = line.split(":", 1)[1].strip() - if len(value) >= 2 and value[0] == value[-1] and value[0] in "'\"": - value = value[1:-1] - return value - - -def _model_line_index(lines: list[str]) -> int | None: - """frontmatter の `model:` の行の添字。frontmatter か欄が無ければ None(行末の改行の有無は問わない)。""" - if not lines or lines[0].strip() != "---": - return None - for index, line in enumerate(lines[1:], start=1): - if line.strip() == "---": - return None - if line.startswith("model:"): - return index - return None - - -def frontmatter_model(text: str) -> str | None: - """frontmatter の `model` の値。欄が無ければ None。""" - lines = text.splitlines() - index = _model_line_index(lines) - return None if index is None else model_value(lines[index]) - - -def agy_copy(text: str, model_map: dict[str, str]) -> str: - """frontmatter の `model:` の 1 行だけを tier 名へ置き換えた写し。欄が無ければそのまま返す。""" - lines = text.splitlines(keepends=True) - index = _model_line_index(lines) - if index is None: - return text - line = lines[index] - value = model_value(line) - if value not in model_map: - raise UnknownModel(value) - ending = line[len(line.rstrip("\r\n")) :] - lines[index] = f"model: {model_map[value]}{ending}" - return "".join(lines) - - -def map_errors(model_map: dict[str, str]) -> list[str]: - """対応表の値のうち tier 名でないもの。""" - return [f"Model map value is not an agy tier: {key}: {value}" for key, value in sorted(model_map.items()) if value not in AGY_TIERS] - - -def sync(plugin_dir: Path, check: bool, model_map: dict[str, str] | None = None) -> bool: - """生成物を書く(`check` なら確かめる)。食い違いがあれば偽。""" - model_map = MODEL_MAP if model_map is None else model_map - dev_dir = plugin_dir / "dev.agy" - source_dir = plugin_dir / "agents" - if not dev_dir.is_dir(): - return True - root = plugin_dir.resolve().parents[1] - - def rel(path: Path) -> str: - try: - return str(path.relative_to(root)) - except ValueError: - return str(path) - - errors = map_errors(model_map) - for message in errors: - print(message, file=sys.stderr) - if errors: - return False - - out_dir = dev_dir / "agents" - expected = _expected_copies(source_dir, out_dir, model_map, rel) - if expected is None: - return False - if check: - return _check_copies(out_dir, expected, rel) - _write_copies(out_dir, expected) - return True - - -def _expected_copies(source_dir: Path, out_dir: Path, model_map: dict[str, str], rel) -> dict[Path, str] | None: - """写しの置き場 → 中身。対応表に無い `model` があれば標準エラーに出して None。""" - sources = sorted(source_dir.glob("*.md")) if source_dir.is_dir() else [] - expected: dict[Path, str] = {} - ok = True - for source in sources: - text = source.read_text(encoding="utf-8") - try: - expected[out_dir / source.name] = agy_copy(text, model_map) - except UnknownModel as exc: - print(f"Unknown model for agy: {rel(source)}: {exc}", file=sys.stderr) - ok = False - return expected if ok else None - - -def _check_copies(out_dir: Path, expected: dict[Path, str], rel) -> bool: - """書かずに確かめる。食い違いを標準エラーに出し、あれば偽。""" - if out_dir.is_symlink(): - print(f"Generated agents must be files, not a symlink: {rel(out_dir)}", file=sys.stderr) - return False - ok = True - actual = {p for p in out_dir.iterdir()} if out_dir.is_dir() else set() - for path, content in sorted(expected.items()): - if not path.is_file(): - print(f"Generated agent missing: {rel(path)}", file=sys.stderr) - ok = False - elif path.read_text(encoding="utf-8") != content: - print(f"Generated agent is out of date: {rel(path)}", file=sys.stderr) - ok = False - for path in sorted(actual - set(expected)): - print(f"Generated agent is stale: {rel(path)}", file=sys.stderr) - ok = False - return ok - - -def _write_copies(out_dir: Path, expected: dict[Path, str]) -> None: - """写しを書き、元に無い写しを消す。""" - # 古い symlink(`../agents`)を外してから書く。外さずに書くと元の定義を書き換える。 - if out_dir.is_symlink(): - out_dir.unlink() - out_dir.mkdir(parents=True, exist_ok=True) - for path, content in expected.items(): - path.write_text(content, encoding="utf-8") - for path in sorted(set(out_dir.iterdir()) - set(expected)): - path.unlink() - - -def main(argv: list[str]) -> int: - plugin_dir, check = Path(argv[0]), argv[1] == "true" - return 0 if sync(plugin_dir, check) else 1 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/scripts/lib/validate_manifests.py b/scripts/lib/validate_manifests.py index 9df978c46..8e864ce52 100644 --- a/scripts/lib/validate_manifests.py +++ b/scripts/lib/validate_manifests.py @@ -51,59 +51,11 @@ def shaped(model: type[_Open], data: Any) -> _Open | None: return None -root = Path(sys.argv[1]) -# 検出済みの plugin family は呼び出し側から受け取る(検出を 2 箇所に持つと、 -# 一方だけが新しい family を拾ってチェック範囲が食い違う)。 -families = sys.argv[2:] - - -def plugin_dir_of(family: str) -> Path: - """プラグインの配布ディレクトリ。3 ランタイムとも同じ場所を読む。""" - return root / f"plugins/{family}" - - def read_json(path: Path): with path.open(encoding="utf-8") as fh: return json.load(fh) -errors: list[str] = [] - -claude_marketplace = read_json(root / ".claude-plugin/marketplace.json") -for plugin in claude_marketplace.get("plugins", []): - entry = shaped(Sourced, plugin) - if entry is None: - errors.append(f".claude-plugin marketplace plugin {plugin.get('name')} has invalid source") - continue - source = entry.source - plugin_dir = (root / source).resolve() - if not plugin_dir.is_dir(): - errors.append(f"Claude marketplace source missing: {source}") - continue - if not (plugin_dir / ".claude-plugin/plugin.json").is_file(): - errors.append(f"Claude plugin manifest missing under {source}") - -# Codex は専用のマーケットプレイス定義が無ければ .claude-plugin/marketplace.json へ -# フォールバックする。定義は 1 つに統合したので、同じエントリを Codex 側の要件でもチェックする。 -# Codex は plugin.json(Agent Plugins 形式)> .codex-plugin > .claude-plugin の順で採るため、 -# ルートマニフェストか Codex 用マニフェストのどちらかがあれば良い。 -for plugin in claude_marketplace.get("plugins", []): - entry = shaped(Sourced, plugin) - if entry is None: - continue - source = entry.source - plugin_dir = (root / source).resolve() - if not plugin_dir.is_dir(): - continue - if not ((plugin_dir / ".codex-plugin/plugin.json").is_file() or (plugin_dir / "plugin.json").is_file()): - errors.append(f"Codex plugin manifest missing under {source}(.codex-plugin/plugin.json かルートの plugin.json のどちらかが要る)") - # Codex は policy / category / interface を要求する。統合した定義から欠けると - # Codex 側の一覧に出ない。 - for key in ("policy", "category", "interface"): - if key not in plugin: - errors.append(f"marketplace entry {plugin.get('name')} に {key} がない(Codex が要求する)") - - # 版数と Skill 数は plugin.json と marketplace の description に重複して書かれている。 # `.claude-plugin/marketplace.json` と Codex 版 plugin.json は build-runtime-plugins.sh の # 生成対象ではなく、古い値が残っても JSON としては妥当なため他のチェックに掛からない。 @@ -121,271 +73,343 @@ def _version_in_description(lib: Path): return module.VERSION_IN_DESCRIPTION -try: - VERSION_IN_DESCRIPTION = _version_in_description(root / "scripts" / "lib") -except (ImportError, AttributeError, OSError) as exc: - raise SystemExit(f"版数の書式を読み込めない(scripts/lib/version_pattern.py): {exc}") # `<数> ... skills` の形で書く規約。版数(8.0.0)や製品名(E2E)の数字を拾わないよう前後が # 英数字・ドットでない整数だけを見て、さらに `skills` との間に挟める語を 3 語までに絞る。 # こうしないと離れた位置にある無関係な数(`8 specialized agents` など)を Skill 数と誤認する。 DESCRIBED_SKILL_COUNT = re.compile(r"(? None: - described_entry = shaped(Described, manifest) - if described_entry is None: - return - description = described_entry.description - found = VERSION_IN_DESCRIPTION.search(description) - if not found: - errors.append(f"{label} の description に `(vX.Y.Z)` 形式の版数がない") - elif found.group(1) != version: - errors.append(f"{label} の description の版数が古い(description: v{found.group(1)} / Claude 版 plugin.json: v{version})") - if expected is None: - return - # 抽出できないこと自体をエラーにする。素通りさせると、Skill 数の記述を消すか書式を変える - # だけでこのチェックを無効化できてしまう。 - described = described_skill_count(description) - if described is None: - errors.append(f"{label} の description から Skill 数を読み取れない(`<数> ... skills` の形で書く。{source}: {expected})") - elif described != expected: - errors.append(f"{label} の description の Skill 数が食い違う(description: {described} / {source}: {expected})") - - -for family in families: - claude_dir = plugin_dir_of(family) - claude_plugin_path = claude_dir / ".claude-plugin/plugin.json" - if not claude_plugin_path.is_file(): - continue - claude_plugin = read_json(claude_plugin_path) - versioned = shaped(Versioned, claude_plugin) - if versioned is None: - errors.append(f"{family} の claude plugin.json に version がない") - continue - version = versioned.version - check_description( - str(claude_plugin_path.relative_to(root)), - claude_plugin, - version, - manifest_skill_count(family, "claude"), - "claude-skills.txt", - ) - codex_plugin_path = plugin_dir_of(family) / ".codex-plugin/plugin.json" - if codex_plugin_path.is_file(): - codex_plugin = read_json(codex_plugin_path) - if codex_plugin.get("version") != version: - errors.append( - f"{codex_plugin_path.relative_to(root)} の version が claude 版と" - f"食い違う(codex: {codex_plugin.get('version')} / claude: {version})" - ) - check_description( - str(codex_plugin_path.relative_to(root)), - codex_plugin, - version, - manifest_skill_count(family, "codex"), - "codex-skills.txt", - ) - # ルートマニフェスト(Agent Plugins 形式)は絞り込みを持たず `skills/` を全件公開する。 - # description の Skill 数は manifest ではなく実体の数と突き合わせる。 - # agy 向けの定義はクライアント拡張ディレクトリ(Agent Plugins 1.0.0 §8.2)へ置く。 - # ルート直下へ置くと Codex がそちらを優先して読み、配布 Skill が codex-skills.txt では - # なく skills/ の実体になる。agy には取得元の登録が無く `agy plugin list` も版数を出さない - # ため、利用者が版を判断できる手がかりは clone した中身の版数だけである。 - agy_manifest_path = plugin_dir_of(family) / "dev.agy/plugin.json" - if agy_manifest_path.is_file(): - agy_manifest = read_json(agy_manifest_path) - if agy_manifest.get("version") != version: - errors.append( - f"{agy_manifest_path.relative_to(root)} の version が claude 版と" - f"食い違う(agy: {agy_manifest.get('version')} / claude: {version})" - ) - check_description( - str(agy_manifest_path.relative_to(root)), - agy_manifest, - version, - manifest_skill_count(family, "agy"), - "agy-skills.txt", - ) - root_manifest_path = plugin_dir_of(family) / "plugin.json" - if root_manifest_path.is_file(): - root_manifest = read_json(root_manifest_path) - if root_manifest.get("version") != version: - errors.append( - f"{root_manifest_path.relative_to(root)} の version が claude 版と" - f"食い違う(root: {root_manifest.get('version')} / claude: {version})" - ) - check_description( - str(root_manifest_path.relative_to(root)), - root_manifest, - version, - published_skill_count(family), - "skills/ の実体", - ) - expected_source = "./" + claude_dir.relative_to(root).as_posix() - for plugin in claude_marketplace.get("plugins", []): - if plugin.get("source") != expected_source: - continue - check_description( - f".claude-plugin/marketplace.json の {plugin.get('name')}", - plugin, - version, - manifest_skill_count(family, "claude"), - "claude-skills.txt", - ) - -for family in families: - source = plugin_dir_of(family) - for manifest in sorted((source / "manifests").glob("*-skills.txt")): - runtime = manifest.name.removesuffix("-skills.txt") - runtime_skills = plugin_dir_of(family) / "skills" - if not runtime_skills.is_dir(): - errors.append(f"runtime skills directory missing: {runtime_skills.relative_to(root)}") - continue - for raw in manifest.read_text(encoding="utf-8").splitlines(): - skill = raw.split("#", 1)[0].strip() - if not skill: +class VersionPatternUnavailable(Exception): + """版数の書式(`scripts/lib/version_pattern.py`)を読み込めない。""" + + +class ManifestChecker: + """1 つの根の下の定義ファイルを突き合わせ、見つけた食い違いを `errors` へためる。 + + 検出済みの plugin family は呼び出し側から受け取る(検出を 2 箇所に持つと、 + 一方だけが新しい family を拾ってチェック範囲が食い違う)。 + """ + + def __init__(self, root: Path, families: list[str]) -> None: + self.root = root + self.families = families + self.errors: list[str] = [] + self.claude_marketplace: dict = {} + self.version_in_description: re.Pattern[str] | None = None + + def plugin_dir_of(self, family: str) -> Path: + """プラグインの配布ディレクトリ。3 ランタイムとも同じ場所を読む。""" + return self.root / f"plugins/{family}" + + def run(self) -> list[str]: + """すべての検査を決まった順序で走らせ、食い違いの一覧を返す。""" + self.claude_marketplace = read_json(self.root / ".claude-plugin/marketplace.json") + self.check_marketplace() + self.load_version_pattern() + self.check_versions() + self.check_manifest_skills() + self.check_root_manifest_skills() + self.check_unlisted_skills() + self.check_skill_arrays() + self.check_mcp() + return self.errors + + def check_marketplace(self) -> None: + for plugin in self.claude_marketplace.get("plugins", []): + entry = shaped(Sourced, plugin) + if entry is None: + self.errors.append(f".claude-plugin marketplace plugin {plugin.get('name')} has invalid source") continue - if "/" in skill or ".." in skill: - errors.append(f"invalid skill name in {manifest.relative_to(root)}: {skill}") + source = entry.source + plugin_dir = (self.root / source).resolve() + if not plugin_dir.is_dir(): + self.errors.append(f"Claude marketplace source missing: {source}") continue - if not (source / "skills" / skill / "SKILL.md").is_file(): - errors.append(f"{family} source skill missing: {skill}") - if not (runtime_skills / skill / "SKILL.md").is_file(): - errors.append(f"{family} {runtime} runtime skill missing: {skill}") - -# ルートマニフェストを置く family は `skills/` を全件公開する。Codex はこのマニフェストを -# 優先して読むため、codex-skills.txt に載らない Skill を `skills/` へ置くと配布先が増える。 -# 実体と codex 用 manifest が一致していることを確かめる。 -for family in families: - if not (plugin_dir_of(family) / "plugin.json").is_file(): - continue - source = plugin_dir_of(family) - manifest = source / "manifests/codex-skills.txt" - if not manifest.is_file(): - continue - listed = {line.split("#", 1)[0].strip() for line in manifest.read_text(encoding="utf-8").splitlines() if line.split("#", 1)[0].strip()} - present = {d.name for d in (source / "skills").iterdir() if (d / "SKILL.md").is_file()} - for extra in sorted(present - listed): - errors.append( - f"{family} の skills/ にあるが codex-skills.txt に無い: {extra}" - "(ルートマニフェストは skills/ を全件公開するため Codex へ配られる)" - ) - -# 単一ディレクトリ構成では skills/ に全 runtime 分の実体が並ぶ。どの manifest にも -# 載らない Skill をここへ置くと、ルートマニフェストや将来の絞り込み漏れで配られる。 -# skills/ に置く Skill は少なくとも 1 つの manifest へ載せる規約なので、その違反を -# 検出する(配らない置き場所は v10.5.0 で無くなった。#116)。 -for family in families: - source = plugin_dir_of(family) - listed: set[str] = set() - for manifest in sorted((source / "manifests").glob("*-skills.txt")): - listed |= { - line.split("#", 1)[0].strip() for line in manifest.read_text(encoding="utf-8").splitlines() if line.split("#", 1)[0].strip() - } - for skill_dir in sorted((source / "skills").iterdir()): - if not (skill_dir / "SKILL.md").is_file(): - continue - if skill_dir.name not in listed: - errors.append( - f"{family} の skills/ にあるがどの manifest にも載っていない: {skill_dir.name}" - "(skills/ に置く Skill は少なくとも 1 つの manifest へ載せる)" - ) - -# マニフェストの skills は配列で明示する。配列に載っていない Skill はランタイムから -# 読み込まれないため、manifest と一致していないと配布漏れになる。ディレクトリの中身を -# 見る上のチェックでは検出できないので、ここで突き合わせる。 -for family in families: - for runtime, manifest_key in (("claude", ".claude-plugin"), ("codex", ".codex-plugin")): - skills_manifest = plugin_dir_of(family) / f"manifests/{runtime}-skills.txt" - plugin_json = plugin_dir_of(family) / f"{manifest_key}/plugin.json" - if not (skills_manifest.is_file() and plugin_json.is_file()): - continue - expected = [line.split("#", 1)[0].strip() for line in skills_manifest.read_text(encoding="utf-8").splitlines()] - expected_set = {name for name in expected if name} - manifest_data = json.loads(plugin_json.read_text(encoding="utf-8")) - declared = manifest_data.get("skills") + if not (plugin_dir / ".claude-plugin/plugin.json").is_file(): + self.errors.append(f"Claude plugin manifest missing under {source}") + + # Codex は専用のマーケットプレイス定義が無ければ .claude-plugin/marketplace.json へ + # フォールバックする。定義は 1 つに統合したので、同じエントリを Codex 側の要件でもチェックする。 + # Codex は plugin.json(Agent Plugins 形式)> .codex-plugin > .claude-plugin の順で採るため、 + # ルートマニフェストか Codex 用マニフェストのどちらかがあれば良い。 + for plugin in self.claude_marketplace.get("plugins", []): + entry = shaped(Sourced, plugin) + if entry is None: + continue + source = entry.source + plugin_dir = (self.root / source).resolve() + if not plugin_dir.is_dir(): + continue + if not ((plugin_dir / ".codex-plugin/plugin.json").is_file() or (plugin_dir / "plugin.json").is_file()): + self.errors.append( + f"Codex plugin manifest missing under {source}(.codex-plugin/plugin.json かルートの plugin.json のどちらかが要る)" + ) + # Codex は policy / category / interface を要求する。統合した定義から欠けると + # Codex 側の一覧に出ない。 + for key in ("policy", "category", "interface"): + if key not in plugin: + self.errors.append(f"marketplace entry {plugin.get('name')} に {key} がない(Codex が要求する)") + + def load_version_pattern(self) -> None: + """読み込めないときは素通りさせずに止める(`VersionPatternUnavailable`)。""" try: - schema.load_shape(SkillPaths, manifest_data) - bad: set[str] = set() - except schema.ShapeError as exc: - bad = {where for where, _ in exc.problems} - if "skills" in bad: - # 配列以外(ディレクトリ指定・欠落)を許すと、この突き合わせが黙って skip され - # 配布漏れの再発を検出できなくなる。配列で明示する形式に固定する。 - errors.append( - f"{family} の {runtime} plugin.json の skills が配列ではない" - f"(実際: {type(declared).__name__})。manifest との突き合わせができない" + self.version_in_description = _version_in_description(self.root / "scripts" / "lib") + except (ImportError, AttributeError, OSError) as exc: + raise VersionPatternUnavailable(str(exc)) from exc + + def manifest_skill_count(self, family: str, runtime: str): + manifest = self.plugin_dir_of(family) / f"manifests/{runtime}-skills.txt" + if not manifest.is_file(): + return None + return sum(1 for line in manifest.read_text(encoding="utf-8").splitlines() if line.split("#", 1)[0].strip()) + + def published_skill_count(self, family: str): + """ルートマニフェストが公開する Skill 数。Agent Plugins 1.0.0 §6.1 が + `skills/` を固定位置と定めており、絞り込みを持たないため実体の数と一致する。""" + skills_dir = self.plugin_dir_of(family) / "skills" + if not skills_dir.is_dir(): + return None + return sum(1 for d in skills_dir.iterdir() if (d / "SKILL.md").is_file()) + + def check_description(self, label: str, manifest: dict, version: str, expected, source: str) -> None: + described_entry = shaped(Described, manifest) + if described_entry is None: + return + description = described_entry.description + found = self.version_in_description.search(description) + if not found: + self.errors.append(f"{label} の description に `(vX.Y.Z)` 形式の版数がない") + elif found.group(1) != version: + self.errors.append(f"{label} の description の版数が古い(description: v{found.group(1)} / Claude 版 plugin.json: v{version})") + if expected is None: + return + # 抽出できないこと自体をエラーにする。素通りさせると、Skill 数の記述を消すか書式を変える + # だけでこのチェックを無効化できてしまう。 + described = described_skill_count(description) + if described is None: + self.errors.append(f"{label} の description から Skill 数を読み取れない(`<数> ... skills` の形で書く。{source}: {expected})") + elif described != expected: + self.errors.append(f"{label} の description の Skill 数が食い違う(description: {described} / {source}: {expected})") + + def check_versions(self) -> None: + for family in self.families: + claude_dir = self.plugin_dir_of(family) + claude_plugin_path = claude_dir / ".claude-plugin/plugin.json" + if not claude_plugin_path.is_file(): + continue + claude_plugin = read_json(claude_plugin_path) + versioned = shaped(Versioned, claude_plugin) + if versioned is None: + self.errors.append(f"{family} の claude plugin.json に version がない") + continue + version = versioned.version + self.check_description( + str(claude_plugin_path.relative_to(self.root)), + claude_plugin, + version, + self.manifest_skill_count(family, "claude"), + "claude-skills.txt", ) - else: - # 比較はパス全体で行う。basename だけを見ると `./wrong/pr` のように - # 実在しない場所を指す項目を通してしまう(claude CLI が無い環境では - # 後段の `claude plugin validate` も skip されるため気づけない)。 - expected_entries = {f"./skills/{name}" for name in expected_set} - declared_entries = set() - for index, entry in enumerate(declared): - if f"skills[{index}]" in bad: - errors.append(f"{runtime} plugin.json の skills 配列に文字列以外の項目がある({type(entry).__name__})") + codex_plugin_path = self.plugin_dir_of(family) / ".codex-plugin/plugin.json" + if codex_plugin_path.is_file(): + codex_plugin = read_json(codex_plugin_path) + if codex_plugin.get("version") != version: + self.errors.append( + f"{codex_plugin_path.relative_to(self.root)} の version が claude 版と" + f"食い違う(codex: {codex_plugin.get('version')} / claude: {version})" + ) + self.check_description( + str(codex_plugin_path.relative_to(self.root)), + codex_plugin, + version, + self.manifest_skill_count(family, "codex"), + "codex-skills.txt", + ) + # ルートマニフェスト(Agent Plugins 形式)は絞り込みを持たず `skills/` を全件公開する。 + # description の Skill 数は manifest ではなく実体の数と突き合わせる。 + root_manifest_path = self.plugin_dir_of(family) / "plugin.json" + if root_manifest_path.is_file(): + root_manifest = read_json(root_manifest_path) + if root_manifest.get("version") != version: + self.errors.append( + f"{root_manifest_path.relative_to(self.root)} の version が claude 版と" + f"食い違う(root: {root_manifest.get('version')} / claude: {version})" + ) + self.check_description( + str(root_manifest_path.relative_to(self.root)), + root_manifest, + version, + self.published_skill_count(family), + "skills/ の実体", + ) + expected_source = "./" + claude_dir.relative_to(self.root).as_posix() + for plugin in self.claude_marketplace.get("plugins", []): + if plugin.get("source") != expected_source: continue - declared_entries.add(entry) - for missing in sorted(expected_entries - declared_entries): - errors.append(f"{family} の {runtime} plugin.json の skills 配列に載っていない: {missing}(manifest には登録済み)") - for extra in sorted(declared_entries - expected_entries): - errors.append( - f"{family} の {runtime} plugin.json の skills 配列に余分な項目: {extra}" - "(manifest に無い、またはパスが `./skills/` の形式でない)" + self.check_description( + f".claude-plugin/marketplace.json の {plugin.get('name')}", + plugin, + version, + self.manifest_skill_count(family, "claude"), + "claude-skills.txt", ) -# MCP プラグインも 1 ディレクトリにまとめた。runtime ごとの配布物は無く、 -# 3 runtime が同じ .mcp.json を読む(Codex だけ .codex.mcp.json を読ませる例外がある)。 -for mcp in sorted((root / "plugins/mcp").iterdir()): - if not mcp.is_dir(): - continue - if not (mcp / ".mcp.json").is_file(): - errors.append(f"MCP config missing: plugins/mcp/{mcp.name}/.mcp.json") - continue - if not (mcp / ".claude-plugin/plugin.json").is_file(): - errors.append(f"Claude plugin manifest missing: plugins/mcp/{mcp.name}/.claude-plugin/plugin.json") - if not (mcp / ".codex-plugin/plugin.json").is_file(): - errors.append(f"Codex plugin manifest missing: plugins/mcp/{mcp.name}/.codex-plugin/plugin.json") - if not (mcp / "dev.kiro/install.sh").is_file(): - errors.append(f"Kiro MCP installer missing: plugins/mcp/{mcp.name}/dev.kiro/install.sh") - # Codex は .mcp.json を manifest の mcpServers から読む。指定が無いと - # サーバが 1 つも登録されない。Codex だけ別の起動定義を読ませるときは - # `./.codex.mcp.json` を指す(mcp-serena の `--context codex`。#818)。 - codex_manifest = mcp / ".codex-plugin/plugin.json" - if codex_manifest.is_file(): - declared = read_json(codex_manifest).get("mcpServers") - allowed = ("./.mcp.json", "./.codex.mcp.json") - if declared not in allowed or not (mcp / declared).is_file(): - errors.append( - f"plugins/mcp/{mcp.name}/.codex-plugin/plugin.json の mcpServers が " - f"実在する `./.mcp.json` か `./.codex.mcp.json` でない(実際: {declared!r})" - ) - -if errors: - for error in errors: - print(f"ERROR: {error}", file=sys.stderr) - raise SystemExit(1) + def check_manifest_skills(self) -> None: + for family in self.families: + source = self.plugin_dir_of(family) + for manifest in sorted((source / "manifests").glob("*-skills.txt")): + runtime = manifest.name.removesuffix("-skills.txt") + runtime_skills = self.plugin_dir_of(family) / "skills" + if not runtime_skills.is_dir(): + self.errors.append(f"runtime skills directory missing: {runtime_skills.relative_to(self.root)}") + continue + for raw in manifest.read_text(encoding="utf-8").splitlines(): + skill = raw.split("#", 1)[0].strip() + if not skill: + continue + if "/" in skill or ".." in skill: + self.errors.append(f"invalid skill name in {manifest.relative_to(self.root)}: {skill}") + continue + if not (source / "skills" / skill / "SKILL.md").is_file(): + self.errors.append(f"{family} source skill missing: {skill}") + if not (runtime_skills / skill / "SKILL.md").is_file(): + self.errors.append(f"{family} {runtime} runtime skill missing: {skill}") + + def check_root_manifest_skills(self) -> None: + # ルートマニフェストを置く family は `skills/` を全件公開する。Codex はこのマニフェストを + # 優先して読むため、codex-skills.txt に載らない Skill を `skills/` へ置くと配布先が増える。 + # 実体と codex 用 manifest が一致していることを確かめる。 + for family in self.families: + if not (self.plugin_dir_of(family) / "plugin.json").is_file(): + continue + source = self.plugin_dir_of(family) + manifest = source / "manifests/codex-skills.txt" + if not manifest.is_file(): + continue + listed = { + line.split("#", 1)[0].strip() for line in manifest.read_text(encoding="utf-8").splitlines() if line.split("#", 1)[0].strip() + } + present = {d.name for d in (source / "skills").iterdir() if (d / "SKILL.md").is_file()} + for extra in sorted(present - listed): + self.errors.append( + f"{family} の skills/ にあるが codex-skills.txt に無い: {extra}" + "(ルートマニフェストは skills/ を全件公開するため Codex へ配られる)" + ) -print("runtime plugin manifests and generated paths are valid") + def check_unlisted_skills(self) -> None: + # 単一ディレクトリ構成では skills/ に全 runtime 分の実体が並ぶ。どの manifest にも + # 載らない Skill をここへ置くと、ルートマニフェストや将来の絞り込み漏れで配られる。 + # skills/ に置く Skill は少なくとも 1 つの manifest へ載せる規約なので、その違反を + # 検出する(配らない置き場所は v10.5.0 で無くなった。#116)。 + for family in self.families: + source = self.plugin_dir_of(family) + listed: set[str] = set() + for manifest in sorted((source / "manifests").glob("*-skills.txt")): + listed |= { + line.split("#", 1)[0].strip() + for line in manifest.read_text(encoding="utf-8").splitlines() + if line.split("#", 1)[0].strip() + } + for skill_dir in sorted((source / "skills").iterdir()): + if not (skill_dir / "SKILL.md").is_file(): + continue + if skill_dir.name not in listed: + self.errors.append( + f"{family} の skills/ にあるがどの manifest にも載っていない: {skill_dir.name}" + "(skills/ に置く Skill は少なくとも 1 つの manifest へ載せる)" + ) + + def check_skill_arrays(self) -> None: + # マニフェストの skills は配列で明示する。配列に載っていない Skill はランタイムから + # 読み込まれないため、manifest と一致していないと配布漏れになる。ディレクトリの中身を + # 見る上のチェックでは検出できないので、ここで突き合わせる。 + for family in self.families: + for runtime, manifest_key in (("claude", ".claude-plugin"), ("codex", ".codex-plugin")): + skills_manifest = self.plugin_dir_of(family) / f"manifests/{runtime}-skills.txt" + plugin_json = self.plugin_dir_of(family) / f"{manifest_key}/plugin.json" + if not (skills_manifest.is_file() and plugin_json.is_file()): + continue + expected = [line.split("#", 1)[0].strip() for line in skills_manifest.read_text(encoding="utf-8").splitlines()] + expected_set = {name for name in expected if name} + manifest_data = json.loads(plugin_json.read_text(encoding="utf-8")) + declared = manifest_data.get("skills") + try: + schema.load_shape(SkillPaths, manifest_data) + bad: set[str] = set() + except schema.ShapeError as exc: + bad = {where for where, _ in exc.problems} + if "skills" in bad: + # 配列以外(ディレクトリ指定・欠落)を許すと、この突き合わせが黙って skip され + # 配布漏れの再発を検出できなくなる。配列で明示する形式に固定する。 + self.errors.append( + f"{family} の {runtime} plugin.json の skills が配列ではない" + f"(実際: {type(declared).__name__})。manifest との突き合わせができない" + ) + else: + # 比較はパス全体で行う。basename だけを見ると `./wrong/pr` のように + # 実在しない場所を指す項目を通してしまう(claude CLI が無い環境では + # 後段の `claude plugin validate` も skip されるため気づけない)。 + expected_entries = {f"./skills/{name}" for name in expected_set} + declared_entries = set() + for index, entry in enumerate(declared): + if f"skills[{index}]" in bad: + self.errors.append(f"{runtime} plugin.json の skills 配列に文字列以外の項目がある({type(entry).__name__})") + continue + declared_entries.add(entry) + for missing in sorted(expected_entries - declared_entries): + self.errors.append( + f"{family} の {runtime} plugin.json の skills 配列に載っていない: {missing}(manifest には登録済み)" + ) + for extra in sorted(declared_entries - expected_entries): + self.errors.append( + f"{family} の {runtime} plugin.json の skills 配列に余分な項目: {extra}" + "(manifest に無い、またはパスが `./skills/` の形式でない)" + ) + + def check_mcp(self) -> None: + # MCP プラグインも 1 ディレクトリにまとめた。runtime ごとの配布物は無く、 + # 2 runtime が同じ .mcp.json を読む(Codex だけ .codex.mcp.json を読ませる例外がある)。 + for mcp in sorted((self.root / "plugins/mcp").iterdir()): + if not mcp.is_dir(): + continue + if not (mcp / ".mcp.json").is_file(): + self.errors.append(f"MCP config missing: plugins/mcp/{mcp.name}/.mcp.json") + continue + if not (mcp / ".claude-plugin/plugin.json").is_file(): + self.errors.append(f"Claude plugin manifest missing: plugins/mcp/{mcp.name}/.claude-plugin/plugin.json") + if not (mcp / ".codex-plugin/plugin.json").is_file(): + self.errors.append(f"Codex plugin manifest missing: plugins/mcp/{mcp.name}/.codex-plugin/plugin.json") + # Codex は .mcp.json を manifest の mcpServers から読む。指定が無いと + # サーバが 1 つも登録されない。Codex だけ別の起動定義を読ませるときは + # `./.codex.mcp.json` を指す(mcp-serena の `--context codex`。#818)。 + codex_manifest = mcp / ".codex-plugin/plugin.json" + if codex_manifest.is_file(): + declared = read_json(codex_manifest).get("mcpServers") + allowed = ("./.mcp.json", "./.codex.mcp.json") + if declared not in allowed or not (mcp / declared).is_file(): + self.errors.append( + f"plugins/mcp/{mcp.name}/.codex-plugin/plugin.json の mcpServers が " + f"実在する `./.mcp.json` か `./.codex.mcp.json` でない(実際: {declared!r})" + ) + + +def main(argv: list[str]) -> None: + checker = ManifestChecker(Path(argv[1]), argv[2:]) + try: + errors = checker.run() + except VersionPatternUnavailable as exc: + raise SystemExit(f"版数の書式を読み込めない(scripts/lib/version_pattern.py): {exc}") + if errors: + for error in errors: + print(f"ERROR: {error}", file=sys.stderr) + raise SystemExit(1) + print("runtime plugin manifests and generated paths are valid") + + +if __name__ == "__main__": + main(sys.argv) diff --git a/scripts/runtime-smoke-latest.py b/scripts/runtime-smoke-latest.py index e08fe704b..b7775e5eb 100755 --- a/scripts/runtime-smoke-latest.py +++ b/scripts/runtime-smoke-latest.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """最新の版のスモークの手順のうち、判断の要らない 2 つを持つ(#1982)。 -- `resolve --runtime `: 取得元の最新の版を、CLI の版の一覧 +- `resolve --runtime `: 取得元の最新の版を、CLI の版の一覧 (`tests/runtime-smoke/cli-versions.env`)と同じ `KEY=VALUE` の形で標準出力へ出す。 取得元に届かない・形が読めないときは終了コード 1 で、標準エラーへ取得元の URL を書く - `report --runtime <名前> --run-url [--versions <パス>] [--repo ]`: 題が @@ -22,16 +22,10 @@ import urllib.request from pathlib import Path -RUNTIMES = ("claude", "codex", "kiro", "agy") +RUNTIMES = ("claude", "codex") VERSION_RE = re.compile(r"^[0-9]+(\.[0-9]+)+$") -HEX_RE = re.compile(r"^[0-9a-f]+$") NPM_DIST_TAGS = "https://registry.npmjs.org/-/package/{package}/dist-tags" -KIRO_MANIFEST = "https://prod.download.cli.kiro.dev/stable/latest/manifest.json" -AGY_MANIFEST = "https://antigravity-cli-auto-updater-974169037036.us-central1.run.app/manifests/{platform}.json" -# arch の名前は `uname -m` の値に揃える(一覧のキーの末尾)。agy の manifest は別の名前で引く -AGY_PLATFORMS = {"X86_64": "linux_amd64", "AARCH64": "linux_arm64"} -KIRO_ARCHES = {"X86_64": "x86_64", "AARCH64": "aarch64"} TITLE = "最新の版のスモークが落ちた: {runtime}" @@ -59,12 +53,6 @@ def _version(url: str, value: object) -> str: return value -def _hex(url: str, value: object, length: int) -> str: - if not isinstance(value, str) or len(value) != length or not HEX_RE.match(value): - raise ResolveError(url, f"checksum が読めない: {value!r}") - return value - - def _npm_latest(package: str) -> str: url = NPM_DIST_TAGS.format(package=package) data = fetch_json(url) @@ -80,45 +68,7 @@ def resolve_codex() -> list[tuple[str, str]]: return [("CODEX_VERSION", _npm_latest("@openai/codex"))] -def resolve_kiro() -> list[tuple[str, str]]: - url = KIRO_MANIFEST - data = fetch_json(url) - if not isinstance(data, dict) or not isinstance(data.get("packages"), list): - raise ResolveError(url, "packages が無い") - version = _version(url, data.get("version")) - pairs = [("KIRO_CLI_VERSION", version)] - for key, arch in KIRO_ARCHES.items(): - download = f"{version}/kirocli-{arch}-linux-musl.tar.gz" - found = [p for p in data["packages"] if isinstance(p, dict) and p.get("download") == download] - if len(found) != 1: - raise ResolveError(url, f"{download} が manifest に 1 つだけ載っていない") - pairs.append((f"KIRO_CLI_SHA256_{key}", _hex(url, found[0].get("sha256"), 64))) - return pairs - - -def resolve_agy() -> list[tuple[str, str]]: - versions: dict[str, str] = {} - urls: list[tuple[str, str]] = [] - sums: list[tuple[str, str]] = [] - for key, platform in AGY_PLATFORMS.items(): - url = AGY_MANIFEST.format(platform=platform) - data = fetch_json(url) - if not isinstance(data, dict): - raise ResolveError(url, "形が読めない") - version = _version(url, data.get("version")) - dl = data.get("url") - # 版つきの取得先だけを受ける(版を含まない url は固定にならない) - if not isinstance(dl, str) or not dl.startswith("https://") or f"/{version}-" not in dl: - raise ResolveError(url, f"版つきの url が読めない: {dl!r}") - versions[platform] = version - urls.append((f"AGY_URL_{key}", dl)) - sums.append((f"AGY_SHA512_{key}", _hex(url, data.get("sha512"), 128))) - if len(set(versions.values())) != 1: - raise ResolveError(AGY_MANIFEST.format(platform="*"), f"arch ごとの版が違う: {versions}") - return [("AGY_VERSION", next(iter(versions.values()))), *urls, *sums] - - -RESOLVERS = {"claude": resolve_claude, "codex": resolve_codex, "kiro": resolve_kiro, "agy": resolve_agy} +RESOLVERS = {"claude": resolve_claude, "codex": resolve_codex} def resolve(runtime: str) -> list[tuple[str, str]]: diff --git a/scripts/runtime-smoke-test.sh b/scripts/runtime-smoke-test.sh index db5ac38de..b018ebb7d 100755 --- a/scripts/runtime-smoke-test.sh +++ b/scripts/runtime-smoke-test.sh @@ -13,7 +13,7 @@ usage() { Usage: bash scripts/runtime-smoke-test.sh [OPTIONS] Options: - --runtime claude|codex|kiro|agy|all runtime to test (default: all) + --runtime claude|codex|all runtime to test (default: all) --with-secrets off|auto|required secret handling mode (default: off) --secret-file KEY=PATH inject an allowlisted file secret --keep-container keep the container for local debugging @@ -44,7 +44,7 @@ while [ "$#" -gt 0 ]; do shift done -case "$RUNTIME" in claude|codex|kiro|agy|all) ;; *) echo "ERROR: invalid --runtime: $RUNTIME" >&2; exit 2 ;; esac +case "$RUNTIME" in claude|codex|all) ;; *) echo "ERROR: invalid --runtime: $RUNTIME" >&2; exit 2 ;; esac case "$WITH_SECRETS" in off|auto|required) ;; *) echo "ERROR: invalid --with-secrets: $WITH_SECRETS" >&2; exit 2 ;; esac if [ "$KEEP_CONTAINER" = true ] && [ "$WITH_SECRETS" != off ]; then echo "ERROR: --keep-container cannot be used with --with-secrets=$WITH_SECRETS" >&2 @@ -57,7 +57,7 @@ mkdir -p "$ARTIFACT_DIR" ARTIFACT_DIR="$(cd "$ARTIFACT_DIR" && pwd)" RUNTIMES=("$RUNTIME") -[ "$RUNTIME" = all ] && RUNTIMES=(claude codex kiro agy) +[ "$RUNTIME" = all ] && RUNTIMES=(claude codex) RAW_SECRET_NAMES=( ANTHROPIC_API_KEY diff --git a/scripts/script-structure-allow/plugins__ndf__dev.kiro__install.sh--lines.json b/scripts/script-structure-allow/plugins__ndf__dev.kiro__install.sh--lines.json deleted file mode 100644 index 3148cc866..000000000 --- a/scripts/script-structure-allow/plugins__ndf__dev.kiro__install.sh--lines.json +++ /dev/null @@ -1 +0,0 @@ -{"path": "plugins/ndf/dev.kiro/install.sh", "name": "", "kind": "lines", "reason": "L0 の時点で 561 行。移行の範囲の外。触って 500 行以下にしたら消す", "lines": 561} diff --git a/scripts/script-structure-allow/plugins__ndf__scripts__pr-steps.py--lines.json b/scripts/script-structure-allow/plugins__ndf__scripts__pr-steps.py--lines.json deleted file mode 100644 index 0ecfbea43..000000000 --- a/scripts/script-structure-allow/plugins__ndf__scripts__pr-steps.py--lines.json +++ /dev/null @@ -1 +0,0 @@ -{"path": "plugins/ndf/scripts/pr-steps.py", "name": "", "kind": "lines", "reason": "一括の整形(#1323)で物理行が増え、ツールのパスの分類と除外(#1436)で 6 行増えた。500 行以下にしたら消す", "lines": 599} diff --git a/scripts/tests/agy_build_helpers.py b/scripts/tests/agy_build_helpers.py deleted file mode 100644 index fe6661e70..000000000 --- a/scripts/tests/agy_build_helpers.py +++ /dev/null @@ -1,82 +0,0 @@ -"""生成物の同期(`scripts/build-runtime-plugins.sh`)のテスト補助。 - -生成の本体はシェルスクリプトで、自分の位置からリポジトリの根を決める。実物のリポジトリで -動かすと `dev.agy/skills/` を書き換えてしまうため、一時ディレクトリへ同じ配置の木を作り、 -スクリプトを複製してそちらで動かす。**複製ではなく実物を複製する**(複製を置くと、生成の -規則が 2 つに分かれる)。 - -Skill 名は実物(`cherry-pick-pr` など)と重ならない値にしてある。テストが実物の manifest へ -依存していないことを、値そのもので示すためである。 -""" - -from __future__ import annotations - -import json -import subprocess -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[2] -BUILD = REPO_ROOT / "scripts/build-runtime-plugins.sh" -AGY_AGENTS = REPO_ROOT / "scripts/lib/agy_agents.py" - -FAMILY = "ndf" -VERSION = "9.3.0" -AGY_SKILLS = ["alpha", "bravo", "charlie"] - - -def build_tree(base: Path, skills: list[str] | None = None) -> Path: - """agy の生成に要るものだけを備えた木を作り、その根を返す。""" - skills = AGY_SKILLS if skills is None else skills - root = base / "repo" - plugin = root / f"plugins/{FAMILY}" - - (root / "scripts").mkdir(parents=True) - (root / "scripts/build-runtime-plugins.sh").write_text(BUILD.read_text(encoding="utf-8"), encoding="utf-8") - (root / "scripts/lib").mkdir() - (root / "scripts/lib/agy_agents.py").write_text(AGY_AGENTS.read_text(encoding="utf-8"), encoding="utf-8") - # MCP プラグインの同期はディレクトリを走査する。空でも存在していないと読めない。 - (root / "plugins/mcp").mkdir(parents=True) - - (plugin / "manifests").mkdir(parents=True) - (plugin / "manifests/agy-skills.txt").write_text( - "# コメント行と空行は数えない\n\n" + "".join(f"{name}\n" for name in skills), - encoding="utf-8", - ) - for name in skills: - (plugin / "skills" / name).mkdir(parents=True) - (plugin / "skills" / name / "SKILL.md").write_text(f"# {name}\n", encoding="utf-8") - (plugin / "agents").mkdir(parents=True) - (plugin / "scripts").mkdir(parents=True) - - (plugin / "dev.agy").mkdir(parents=True) - (plugin / "dev.agy/plugin.json").write_text( - json.dumps({"name": FAMILY, "version": VERSION}, ensure_ascii=False, indent=2) + "\n", - encoding="utf-8", - ) - return root - - -def run_build(root: Path, check: bool = False) -> subprocess.CompletedProcess[str]: - """複製したスクリプトを木の中で実行し、終了コードと出力を観測する。""" - args = ["bash", str(root / "scripts/build-runtime-plugins.sh")] - if check: - args.append("--check") - return subprocess.run(args, capture_output=True, text=True, check=False) - - -def output_of(result: subprocess.CompletedProcess[str]) -> str: - return result.stdout + result.stderr - - -def links_dir(root: Path) -> Path: - return root / f"plugins/{FAMILY}/dev.agy/skills" - - -def agents_dir(root: Path) -> Path: - """エージェント定義の正(Claude Code の定義)。""" - return root / f"plugins/{FAMILY}/agents" - - -def agy_agents_dir(root: Path) -> Path: - """agy に配る定義(生成した写し)。""" - return root / f"plugins/{FAMILY}/dev.agy/agents" diff --git a/scripts/tests/doc_staleness_helpers.py b/scripts/tests/doc_staleness_helpers.py index 297a41d3c..4712e9f35 100644 --- a/scripts/tests/doc_staleness_helpers.py +++ b/scripts/tests/doc_staleness_helpers.py @@ -28,12 +28,10 @@ # v10.5.0 で無くなった。#116)。 SKILL_DIRS = ["alpha", "bravo", "charlie", "delta", "echo"] -# ランタイムごとの配布 Skill 数。4 つとも別の値にして、取り違えを検出できるようにする。 +# ランタイムごとの配布 Skill 数。2 つを別の値にして、取り違えを検出できるようにする。 MANIFESTS = { "claude": ["alpha", "bravo", "charlie", "delta", "echo"], "codex": ["alpha", "bravo", "charlie"], - "kiro": ["alpha", "bravo", "charlie", "delta"], - "agy": ["alpha", "bravo"], } # 記号(G〜M)は `issues/parallel-batch-03/04-issue-209.md` の「チェックする記載」に対応する。 @@ -42,7 +40,7 @@ **NDFプラグイン v9.3.0** のチェック用の最小構成です。 -- **公開Skills**: Claude Code向け core 5個、Kiro向け core 4個、Codex向け core 3個、agy向け core 2個に分離。 +- **公開Skills**: Claude Code向け core 5個、Codex向け core 3個に分離。 - **元Skills(5個)**: - 第1群 (4): alpha, bravo, charlie, delta - 第2群 (1): echo @@ -111,7 +109,7 @@ 最初のタグは `ndf--v8.5.4` である。それより前の版(`8.4.0` 以前)はタグでは戻せない。 """ -# K: Kiro の確認例 / L: Codex のキャッシュパスの例(2 箇所) / M: `codex plugin list` の出力例 +# L: Codex のキャッシュパスの例(2 箇所) / M: `codex plugin list` の出力例 PLUGIN_README = """# NDF Plugin 配布物は 1 ディレクトリにまとまっています。 @@ -120,8 +118,6 @@ | --- | --- | --- | | Claude Code | 5 個 | `.claude-plugin/plugin.json` | | Codex | 3 個 | `.codex-plugin/plugin.json` | -| Kiro CLI | 4 個 | `dev.kiro/install.sh` | -| agy | 2 個 | `dev.agy/plugin.json` | ## レイアウト @@ -135,13 +131,6 @@ 版ごとの変更と移行の手順は CHANGELOG.md の `[ndf <版>]` の節にあります。 -## Kiro CLI で確かめる - -```bash -kiro agent list -# => NDF統合開発エージェント(Kiro CLI用 / v9.3.0) -``` - ## Codex で確かめる ```text @@ -259,7 +248,6 @@ def retarget_version(root: Path, version: str) -> None: edit(versioning, f"`{old_next}-rc.1`", f"`{new_next}-rc.1`") plugin_readme = root / "plugins/ndf/README.md" - edit(plugin_readme, f"Kiro CLI用 / v{VERSION})", f"Kiro CLI用 / v{version})") edit_all(plugin_readme, f"ndf/{VERSION}/skills/", f"ndf/{version}/skills/", 2) edit(plugin_readme, f"enabled {VERSION} ", f"enabled {version} ") diff --git a/scripts/tests/test_agy_agents.py b/scripts/tests/test_agy_agents.py deleted file mode 100644 index 1ae1b7507..000000000 --- a/scripts/tests/test_agy_agents.py +++ /dev/null @@ -1,192 +0,0 @@ -"""agy に配るエージェント定義の生成(scripts/lib/agy_agents.py。#2087)。 - -agy 1.4 系は `model` に tier 名(`flash` / `pro` / `inherit`)しか受け付けない。`dev.agy/agents/` には -`plugins/ndf/agents/` の写しを置き、`model:` の 1 行だけを tier 名へ変える。agy の無い環境で、 -写しの追従し忘れ・手直し・余り・足りなさと、受け付けられない `model` を見つける。 -""" - -from __future__ import annotations - -import importlib.util -import os -import shutil -from pathlib import Path - -from agy_build_helpers import agents_dir, agy_agents_dir, build_tree, output_of, run_build - -REPO_ROOT = Path(__file__).resolve().parents[2] -PLUGIN_DIR = REPO_ROOT / "plugins/ndf" - -_spec = importlib.util.spec_from_file_location("agy_agents", REPO_ROOT / "scripts/lib/agy_agents.py") -assert _spec and _spec.loader -agy_agents = importlib.util.module_from_spec(_spec) -_spec.loader.exec_module(agy_agents) - - -def _agent(name: str, model: str | None, body: str = "本文") -> str: - model_line = f"model: {model}\n" if model is not None else "" - return f"---\nname: {name}\n{model_line}description: |\n {name} の説明\n---\n\n{body}\n" - - -def _tree(tmp_path: Path, agents: dict[str, str | None]) -> Path: - root = build_tree(tmp_path) - for name, model in agents.items(): - (agents_dir(root) / f"{name}.md").write_text(_agent(name, model), encoding="utf-8") - return root - - -def _generated(root: Path) -> Path: - result = run_build(root) - assert result.returncode == 0, output_of(result) - return agy_agents_dir(root) - - -def test_repo_copies_use_agy_tiers() -> None: - """配る写しの `model` は tier 名か欄なし(受け入れ条件 7・I3)。""" - copies = sorted((PLUGIN_DIR / "dev.agy/agents").glob("*.md")) - assert copies - for path in copies: - model = agy_agents.frontmatter_model(path.read_text(encoding="utf-8")) - assert model is None or model in agy_agents.AGY_TIERS, f"{path}: {model}" - - -def test_repo_copies_follow_every_definition() -> None: - """写しのファイル名の集合は、エージェント定義の集合と等しい(I1)。""" - assert not (PLUGIN_DIR / "dev.agy/agents").is_symlink() - sources = sorted(p.name for p in (PLUGIN_DIR / "agents").glob("*.md")) - copies = sorted(p.name for p in (PLUGIN_DIR / "dev.agy/agents").iterdir()) - assert copies == sources - - -def test_model_map_values_are_tiers() -> None: - """対応表の値はすべて tier 名(I4)。""" - assert set(agy_agents.MODEL_MAP.values()) <= agy_agents.AGY_TIERS - - -def test_build_converts_model_and_keeps_the_rest(tmp_path: Path) -> None: - """`model:` の 1 行だけが変わり、ほかは元とバイト列で一致する(受け入れ条件 6・7、I2)。""" - root = _tree(tmp_path, {"cheap": "sonnet", "strong": "opus", "plain": None}) - out = _generated(root) - for name, tier in (("cheap", "flash"), ("strong", "pro"), ("plain", None)): - source = (agents_dir(root) / f"{name}.md").read_text(encoding="utf-8") - copy = (out / f"{name}.md").read_text(encoding="utf-8") - assert agy_agents.frontmatter_model(copy) == tier - assert [line for line in source.splitlines() if not line.startswith("model:")] == [ - line for line in copy.splitlines() if not line.startswith("model:") - ] - assert run_build(root, check=True).returncode == 0 - - -def test_build_does_not_touch_the_definitions(tmp_path: Path) -> None: - """古い symlink のままでも、生成は元の定義を書き換えない。`--check` は symlink を落とす(I6)。""" - root = _tree(tmp_path, {"cheap": "sonnet"}) - before = (agents_dir(root) / "cheap.md").read_bytes() - shutil.rmtree(_generated(root)) - os.symlink("../agents", agy_agents_dir(root)) - - result = run_build(root, check=True) - assert result.returncode == 1 - assert "Generated agents must be files, not a symlink" in output_of(result) - - out = _generated(root) - assert not out.is_symlink() and out.is_dir() - assert (agents_dir(root) / "cheap.md").read_bytes() == before - assert run_build(root, check=True).returncode == 0 - - -def test_check_detects_missing_copy(tmp_path: Path) -> None: - """定義を足して生成し直さないと `--check` が落ち、足りない写しを示す(I1)。""" - root = _tree(tmp_path, {"cheap": "sonnet"}) - _generated(root) - (agents_dir(root) / "added.md").write_text(_agent("added", "sonnet"), encoding="utf-8") - result = run_build(root, check=True) - assert result.returncode == 1 - assert "Generated agent missing: plugins/ndf/dev.agy/agents/added.md" in output_of(result) - assert (_generated(root) / "added.md").is_file() - - -def test_check_detects_stale_copy(tmp_path: Path) -> None: - """定義を消して生成し直さないと `--check` が落ち、余った写しを示す。生成し直すと消える(I1)。""" - root = _tree(tmp_path, {"cheap": "sonnet", "gone": "opus"}) - _generated(root) - (agents_dir(root) / "gone.md").unlink() - result = run_build(root, check=True) - assert result.returncode == 1 - assert "Generated agent is stale: plugins/ndf/dev.agy/agents/gone.md" in output_of(result) - assert not (_generated(root) / "gone.md").exists() - - -def test_check_detects_body_drift_and_hand_edits(tmp_path: Path) -> None: - """定義の本文を変えて生成し直さない・写しを手で直す、のどちらも `--check` が落ちる(I2)。""" - root = _tree(tmp_path, {"cheap": "sonnet"}) - out = _generated(root) - source = agents_dir(root) / "cheap.md" - source.write_text(source.read_text(encoding="utf-8").replace("本文", "本文!"), encoding="utf-8") - result = run_build(root, check=True) - assert result.returncode == 1 - assert "Generated agent is out of date: plugins/ndf/dev.agy/agents/cheap.md" in output_of(result) - - _generated(root) - copy = out / "cheap.md" - copy.write_text(copy.read_text(encoding="utf-8").replace("model: flash", "model: sonnet"), encoding="utf-8") - assert run_build(root, check=True).returncode == 1 - - -def test_unknown_model_stops_build_and_check(tmp_path: Path) -> None: - """対応表に無い値は、生成も `--check` も止め、パスと値を示す(I5)。""" - root = _tree(tmp_path, {"odd": "claude-opus-5-5"}) - for check in (False, True): - result = run_build(root, check=check) - assert result.returncode == 1 - assert "Unknown model for agy: plugins/ndf/agents/odd.md: claude-opus-5-5" in output_of(result) - assert not agy_agents_dir(root).exists() - - -def test_broken_model_map_stops_build(tmp_path: Path) -> None: - """対応表の値が tier 名でないと、生成を始めずに止まる(I4)。""" - root = _tree(tmp_path, {"cheap": "sonnet"}) - plugin_dir = root / "plugins/ndf" - assert agy_agents.sync(plugin_dir, check=False, model_map={"sonnet": "sonnet"}) is False - assert not agy_agents_dir(root).exists() - - -# 現状固定テスト: agy_copy と frontmatter_model は frontmatter の走査を別々に持つ。 -# 走査をまとめても、次の入力での出力が変わらないことを固定する。 -_FRONTMATTER_CASES = [ - # (入力, frontmatter_model の値, agy_copy の出力) - ("", None, ""), - ("model: sonnet\n", None, "model: sonnet\n"), - ("本文\n---\nmodel: sonnet\n---\n", None, "本文\n---\nmodel: sonnet\n---\n"), - ("---\nname: a\n---\n\nmodel: sonnet\n", None, "---\nname: a\n---\n\nmodel: sonnet\n"), - ("---\nname: a\n---\n", None, "---\nname: a\n---\n"), - ("---\nname: a\nmodel: sonnet\n---\n", "sonnet", "---\nname: a\nmodel: flash\n---\n"), - ("--- \nmodel: opus\n ---\n", "opus", "--- \nmodel: pro\n ---\n"), - ("---\nmodel: 'opus'\n---\n", "opus", "---\nmodel: pro\n---\n"), - ('---\nmodel: "haiku"\n---\n', "haiku", "---\nmodel: flash\n---\n"), - ("---\nmodel: inherit \n---\n", "inherit", "---\nmodel: inherit\n---\n"), - ("---\r\nmodel: sonnet\r\n---\r\n", "sonnet", "---\r\nmodel: flash\r\n---\r\n"), - ("---\nmodel: sonnet", "sonnet", "---\nmodel: flash"), - ("---\nname: a\nmodel: sonnet\n", "sonnet", "---\nname: a\nmodel: flash\n"), - ("---\n model: sonnet\n---\n", None, "---\n model: sonnet\n---\n"), - ("---\nmodel: sonnet\nmodel: opus\n---\n", "sonnet", "---\nmodel: flash\nmodel: opus\n---\n"), -] - - -def test_characterize_frontmatter_model_and_agy_copy() -> None: - """frontmatter の走査の境界(無い・閉じない・欄なし・引用符・CRLF・字下げ・2 つ目の欄)を固定する。""" - for text, model, copy in _FRONTMATTER_CASES: - assert agy_agents.frontmatter_model(text) == model, repr(text) - assert agy_agents.agy_copy(text, agy_agents.MODEL_MAP) == copy, repr(text) - - -def test_characterize_agy_copy_unknown_model() -> None: - """対応表に無い値は UnknownModel で止まり、値(引用符を外したもの)を持つ。frontmatter の外の値は見ない。""" - for text, value in (("---\nmodel: gpt\n---\n", "gpt"), ("---\nmodel: 'x y'\n---\n", "x y"), ("---\nmodel:\n---\n", "")): - try: - agy_agents.agy_copy(text, agy_agents.MODEL_MAP) - except agy_agents.UnknownModel as exc: - assert exc.args == (value,) - else: - raise AssertionError(repr(text)) - text = "---\nname: a\n---\nmodel: gpt\n" - assert agy_agents.agy_copy(text, agy_agents.MODEL_MAP) == text diff --git a/scripts/tests/test_agy_distribution.py b/scripts/tests/test_agy_distribution.py deleted file mode 100644 index d69cfcb04..000000000 --- a/scripts/tests/test_agy_distribution.py +++ /dev/null @@ -1,185 +0,0 @@ -"""配布先ランタイム agy の配置を固定する(#215 の受け入れ条件 A8 / A12)。 - -agy はプラグインのディレクトリ直下の `plugin.json` で対象を判別する。**その位置へ置くと -Codex の配布 Skill が `.codex-plugin/plugin.json` の `skills` 配列ではなく `skills/` の実体に -変わる**(設計の決定 1 の実測)。増える 2 個は Claude Code だけで動くものであるため、 -ルート直下へ置かないこと自体を固定する。 - -配る Skill の基準は `manifests/agy-skills.txt` だけが持ち、`dev.agy/skills/` の symlink は -`scripts/build-runtime-plugins.sh` が生成する。基準と生成物が食い違ったまま配られないよう、 -`--check` が不足・余分・向き先の違いを検出することを確かめる。 -""" - -from __future__ import annotations - -import os -import subprocess -import sys -from pathlib import Path - -import pytest - -from agy_build_helpers import ( - AGY_SKILLS, - FAMILY, - build_tree, - links_dir, - output_of, - run_build, -) - -REPO_ROOT = Path(__file__).resolve().parents[2] -PLUGIN_DIR = REPO_ROOT / f"plugins/{FAMILY}" -MANIFEST_DIR = PLUGIN_DIR / "manifests" - -# 配布数は課題ごとに変わりうるが、**agy を足したことで他の 3 つが変わらない**ことは -# #215 の受け入れ条件(A12)である。値を書いて固定する。v10.5.0 で 5 個 -# (`optional-skills/` の 4 個と `notion-writing`)を 4 ランタイムすべてへ足したため、 -# 4 つとも同じ 5 だけ増えている(#116 / #144)。Google の 2 つの Skill を `google-workspace` -# の 1 つへまとめたため、4 つとも同じ 1 だけ減っている(#744)。見積もりの `estimate` を -# 4 つとも足したため、同じ 1 だけ増えている(#1856)。 -EXPECTED_COUNTS = {"claude": 49, "codex": 45, "kiro": 46, "agy": 45} - - -def manifest_names(runtime: str) -> list[str]: - path = MANIFEST_DIR / f"{runtime}-skills.txt" - assert path.is_file(), f"{path} が無い" - return [line.split("#", 1)[0].strip() for line in path.read_text(encoding="utf-8").splitlines() if line.split("#", 1)[0].strip()] - - -def test_root_plugin_json_is_absent() -> None: - """ルート直下の `plugin.json` を置かない。 - - 置くと Codex がそちらを優先して読み、`skills/` の実体を全件配る。agy 向けの定義は - `dev.agy/plugin.json` にある(Agent Plugins 1.0.0 §8.2 のクライアント拡張ディレクトリ)。 - """ - assert not (PLUGIN_DIR / "plugin.json").exists(), ( - f"plugins/{FAMILY}/plugin.json があると Codex の配布 Skill が manifest ではなく" - " skills/ の実体になる(agy 向けの定義は dev.agy/plugin.json へ置く)" - ) - - -@pytest.mark.parametrize("runtime,expected", sorted(EXPECTED_COUNTS.items())) -def test_manifest_counts_do_not_change(runtime: str, expected: int) -> None: - """4 ランタイムの配布数。agy を足しても既存の 3 つは動かない。""" - assert len(manifest_names(runtime)) == expected - - -def test_agy_manifest_starts_from_the_codex_set() -> None: - """agy へ配る集合は Codex と同じ。 - - Claude Code だけで動く 2 個(`official-skills-autoloader` / `statusline`)を外した - 集合である(設計の決定 3)。 - """ - assert manifest_names("agy") == manifest_names("codex") - - -def test_dev_agy_links_follow_the_manifest() -> None: - """`dev.agy/skills/` は基準のとおりに並び、実体の Skill を指す。""" - links = PLUGIN_DIR / "dev.agy/skills" - assert sorted(p.name for p in links.iterdir()) == sorted(manifest_names("agy")) - for name in manifest_names("agy"): - link = links / name - assert link.is_symlink(), f"{link} が symlink でない" - assert os.readlink(link) == f"../../skills/{name}" - assert (link / "SKILL.md").is_file() - - -def test_dev_agy_shares_the_implementation() -> None: - """hook の実体は 4 ランタイムで共有する(設計の決定 6)。エージェント定義は写しを置く(#2087)。""" - link = PLUGIN_DIR / "dev.agy" / "scripts" - assert link.is_symlink(), f"{link} が symlink でない" - assert os.readlink(link) == "../scripts" - - -def test_build_creates_the_links(tmp_path: Path) -> None: - """基準から生成できる。生成した直後は `--check` が通る。""" - root = build_tree(tmp_path) - result = run_build(root) - assert result.returncode == 0, output_of(result) - assert sorted(p.name for p in links_dir(root).iterdir()) == sorted(AGY_SKILLS) - assert run_build(root, check=True).returncode == 0 - - -def test_check_detects_a_missing_link(tmp_path: Path) -> None: - """symlink を 1 本消すと `--check` が落ちる。""" - root = build_tree(tmp_path) - run_build(root) - (links_dir(root) / AGY_SKILLS[0]).unlink() - - result = run_build(root, check=True) - assert result.returncode != 0 - assert AGY_SKILLS[0] in output_of(result) - - -def test_check_detects_an_extra_link(tmp_path: Path) -> None: - """基準に無い名前を足すと `--check` が落ちる。""" - root = build_tree(tmp_path) - run_build(root) - (links_dir(root) / "delta").symlink_to("../../skills/delta") - - result = run_build(root, check=True) - assert result.returncode != 0 - assert "delta" in output_of(result) - - -def test_check_detects_a_wrong_target(tmp_path: Path) -> None: - """向き先が違う symlink も落ちる。実体を指していなければ配られない。""" - root = build_tree(tmp_path) - run_build(root) - link = links_dir(root) / AGY_SKILLS[0] - link.unlink() - link.symlink_to(f"../../skills/{AGY_SKILLS[1]}") - - result = run_build(root, check=True) - assert result.returncode != 0 - assert AGY_SKILLS[0] in output_of(result) - - -def test_build_removes_a_stale_link(tmp_path: Path) -> None: - """基準から外した Skill の symlink は、生成し直すと消える。""" - root = build_tree(tmp_path) - run_build(root) - (links_dir(root) / "delta").symlink_to("../../skills/delta") - - assert run_build(root).returncode == 0 - assert not (links_dir(root) / "delta").is_symlink() - - -def test_build_skips_a_family_without_the_manifest(tmp_path: Path) -> None: - """`agy-skills.txt` を持たない family では何も作らない。 - - agy へ配らないプラグイン(`playwright-kit`)を巻き込まないことを固定する。 - """ - root = build_tree(tmp_path) - (root / f"plugins/{FAMILY}/manifests/agy-skills.txt").unlink() - - assert run_build(root).returncode == 0 - assert not links_dir(root).exists() - - -def test_generated_links_are_not_collected_twice() -> None: - """`dev.agy/skills/` の symlink を、テストの収集がたどらない。 - - symlink の先は `skills/` の実体であり、そこには Skill ごとのテストがある。両方を - たどると同じテストが 2 度数えられる(agy を配布先へ加えた時点で 1569 件が 2974 件へ - 増えた)。件数は退行の判定に使うため、二重に数えられる状態を残さない。 - """ - result = subprocess.run( - [ - sys.executable, - "-m", - "pytest", - "--collect-only", - "-q", - "-p", - "no:cacheprovider", - f"plugins/{FAMILY}", - ], - cwd=REPO_ROOT, - capture_output=True, - text=True, - check=False, - ) - assert result.returncode == 0, result.stdout + result.stderr - assert "dev.agy" not in result.stdout, [line for line in result.stdout.splitlines() if "dev.agy" in line] diff --git a/scripts/tests/test_agy_install_hooks.py b/scripts/tests/test_agy_install_hooks.py deleted file mode 100644 index a294bdf1a..000000000 --- a/scripts/tests/test_agy_install_hooks.py +++ /dev/null @@ -1,263 +0,0 @@ -"""agy へ hook を差し込む導入スクリプトの振る舞いを固定する(#305)。 - -**`agy plugin install` は `hooks.json` を複製するが、agy はそれを読み込まない。** -読む先は利用者の `~/.gemini/config/hooks.json` の 1 か所だけである(agy 1.1.26 で実測。 -プラグイン配下とプロジェクト直下のどちらに置いても -`loaded 1 named hooks from 1 hooks.json file(s)` のまま変わらない)。 -`dev.agy/install-hooks.sh` がその 1 か所へ差し込む。 - -ここで見るのは、**他の項目に触れないこと**・**冪等であること**・**相対の指定を -導入先の絶対パスへ直すこと**・**壊れた設定を黙って上書きしないこと**の 4 つである。 -""" - -from __future__ import annotations - -import json -import subprocess -from pathlib import Path - -import pytest - -ROOT = Path(__file__).resolve().parents[2] -INSTALLER = ROOT / "plugins" / "ndf" / "dev.agy" / "install-hooks.sh" - -# 利用者が既に持っている項目。差し込みで消えないことを見る。 -OTHER = {"other-tool": {"PreInvocation": [{"type": "command", "command": "true"}]}} - - -def run(*args: str) -> subprocess.CompletedProcess: - return subprocess.run(["bash", str(INSTALLER), *args], capture_output=True, text=True) - - -@pytest.fixture() -def plugin_dir(tmp_path: Path) -> Path: - """導入済みの実体を模す。`hooks.json` と、指し先のスクリプトを置く。""" - target = tmp_path / "plugins" / "ndf" - (target / "scripts").mkdir(parents=True) - (target / "scripts" / "worktree-guard.sh").write_text("", encoding="utf-8") - (target / "hooks.json").write_text( - json.dumps( - { - "ndf-worktree": { - "PreToolUse": [ - { - "matcher": "Write|write_to_file", - "hooks": [ - { - "type": "command", - "command": "bash ./scripts/worktree-guard.sh", - "timeout": 5, - } - ], - } - ] - } - } - ), - encoding="utf-8", - ) - return target - - -@pytest.fixture() -def config(tmp_path: Path) -> Path: - path = tmp_path / "config" / "hooks.json" - path.parent.mkdir(parents=True) - path.write_text(json.dumps(OTHER), encoding="utf-8") - return path - - -def test_it_adds_the_plugin_hook_without_touching_other_entries(config: Path, plugin_dir: Path) -> None: - result = run("--config", str(config), "--plugin-dir", str(plugin_dir)) - - assert result.returncode == 0, result.stderr - written = json.loads(config.read_text(encoding="utf-8")) - assert sorted(written) == ["ndf-worktree", "other-tool"] - assert written["other-tool"] == OTHER["other-tool"] - - -def test_it_rewrites_relative_commands_to_the_installed_path(config: Path, plugin_dir: Path) -> None: - """利用者の設定へ差し込むと、実行時の現在地はプラグインの位置と揃わない。""" - run("--config", str(config), "--plugin-dir", str(plugin_dir)) - - written = json.loads(config.read_text(encoding="utf-8")) - command = written["ndf-worktree"]["PreToolUse"][0]["hooks"][0]["command"] - assert command == f"bash {plugin_dir}/scripts/worktree-guard.sh" - assert "./scripts/" not in command - - -def test_running_it_twice_changes_nothing(config: Path, plugin_dir: Path) -> None: - run("--config", str(config), "--plugin-dir", str(plugin_dir)) - first = config.read_text(encoding="utf-8") - - result = run("--config", str(config), "--plugin-dir", str(plugin_dir)) - - assert result.returncode == 0 - assert "変更なし" in result.stdout - assert config.read_text(encoding="utf-8") == first - - -def test_dry_run_does_not_write(config: Path, plugin_dir: Path) -> None: - before = config.read_text(encoding="utf-8") - - result = run("--config", str(config), "--plugin-dir", str(plugin_dir), "--dry-run") - - assert result.returncode == 0 - assert config.read_text(encoding="utf-8") == before - - -def test_uninstall_removes_only_the_plugin_entries(config: Path, plugin_dir: Path) -> None: - run("--config", str(config), "--plugin-dir", str(plugin_dir)) - - result = run("--config", str(config), "--plugin-dir", str(plugin_dir), "--uninstall") - - assert result.returncode == 0, result.stderr - written = json.loads(config.read_text(encoding="utf-8")) - assert sorted(written) == ["other-tool"] - - -def test_it_creates_the_config_when_absent(tmp_path: Path, plugin_dir: Path) -> None: - missing = tmp_path / "fresh" / "hooks.json" - - result = run("--config", str(missing), "--plugin-dir", str(plugin_dir)) - - assert result.returncode == 0, result.stderr - assert sorted(json.loads(missing.read_text(encoding="utf-8"))) == ["ndf-worktree"] - - -def test_it_stops_on_a_broken_config(tmp_path: Path, plugin_dir: Path) -> None: - """読めない設定を黙って上書きしない。利用者の hook がまとめて消える。""" - broken = tmp_path / "broken.json" - broken.write_text("{ this is not json", encoding="utf-8") - - result = run("--config", str(broken), "--plugin-dir", str(plugin_dir)) - - assert result.returncode != 0 - assert broken.read_text(encoding="utf-8") == "{ this is not json" - - -def test_it_falls_back_to_the_repository_definition(config: Path, tmp_path: Path) -> None: - """導入前でも中身を確かめられる。実体が無ければリポジトリの定義を読む。""" - result = run( - "--config", - str(config), - "--plugin-dir", - str(tmp_path / "not-installed"), - "--dry-run", - ) - - assert result.returncode == 0, result.stderr - assert "ndf-worktree" in result.stdout - - -def test_the_repository_definition_is_the_one_that_is_distributed() -> None: - """差し込む定義は、配布する `hooks.json` と同じものである。""" - source = json.loads((INSTALLER.parent / "hooks.json").read_text(encoding="utf-8")) - assert sorted(source) == ["ndf-worktree"] - assert sorted(source["ndf-worktree"]) == ["PreInvocation", "PreToolUse"] - - -def test_a_relative_plugin_dir_is_resolved_to_an_absolute_path(config: Path, plugin_dir: Path, tmp_path: Path) -> None: - """agy はリポジトリの外でも起動する。相対のまま保存すると hook が落ちる(#417 の 3)。 - - `plugins/ndf/README.md` の案内は clone からの相対パスで書いてあるため、書かれた - とおりに実行すると踏む。 - """ - result = subprocess.run( - [ - "bash", - str(INSTALLER), - "--config", - str(config), - "--plugin-dir", - str(plugin_dir.relative_to(tmp_path)), - ], - cwd=str(tmp_path), - capture_output=True, - text=True, - ) - - assert result.returncode == 0, result.stderr - written = json.loads(config.read_text(encoding="utf-8")) - command = written["ndf-worktree"]["PreToolUse"][0]["hooks"][0]["command"] - assert command == f"bash {plugin_dir}/scripts/worktree-guard.sh" - - -@pytest.fixture() -def spaced_plugin_dir(tmp_path: Path) -> Path: - """空白とシェルの特殊文字を含む導入先。""" - target = tmp_path / "plugin space" / "ndf$x" - (target / "scripts").mkdir(parents=True) - (target / "scripts" / "worktree-guard.sh").write_text("#!/usr/bin/env bash\nexit 0\n", encoding="utf-8") - (target / "hooks.json").write_text( - json.dumps( - { - "ndf-worktree": { - "PreInvocation": [ - { - "type": "command", - "command": "bash ./scripts/worktree-guard.sh", - "timeout": 5, - } - ] - } - } - ), - encoding="utf-8", - ) - return target - - -def test_a_command_with_a_spaced_path_is_runnable(config: Path, spaced_plugin_dir: Path) -> None: - """文字列の一致だけでは、シェルの語として壊れていることを捕まえられない(#417 の 4)。 - - **正常終了するのに hook が効かない**形になるため、利用者から見て失敗が見えない。 - """ - result = run("--config", str(config), "--plugin-dir", str(spaced_plugin_dir)) - assert result.returncode == 0, result.stderr - - written = json.loads(config.read_text(encoding="utf-8")) - command = written["ndf-worktree"]["PreInvocation"][0]["command"] - executed = subprocess.run(["bash", "-c", command], capture_output=True, text=True) - assert executed.returncode == 0, f"{command}\n{executed.stderr}" - - -def test_uninstall_keeps_entries_the_plugin_does_not_distribute(config: Path, plugin_dir: Path) -> None: - """`ndf-` で始まる利用者の項目まで消さない(#417 の 8)。""" - mine = {"PreInvocation": [{"type": "command", "command": "true"}]} - current = json.loads(config.read_text(encoding="utf-8")) - current["ndf-mine"] = mine - config.write_text(json.dumps(current), encoding="utf-8") - run("--config", str(config), "--plugin-dir", str(plugin_dir)) - - result = run("--config", str(config), "--plugin-dir", str(plugin_dir), "--uninstall") - - assert result.returncode == 0, result.stderr - written = json.loads(config.read_text(encoding="utf-8")) - assert sorted(written) == ["ndf-mine", "other-tool"] - assert written["ndf-mine"] == mine - - -def test_the_write_leaves_no_temporary_file_behind(config: Path, plugin_dir: Path) -> None: - """置き換えで書く。中断で壊れた設定を残さない(#417 の 8)。""" - result = run("--config", str(config), "--plugin-dir", str(plugin_dir)) - - assert result.returncode == 0, result.stderr - leftovers = sorted(p.name for p in config.parent.iterdir() if p.name not in {"hooks.json", "hooks.json.bak"}) - assert leftovers == [] - assert sorted(json.loads(config.read_text(encoding="utf-8"))) == ["ndf-worktree", "other-tool"] - - -def test_it_reports_a_command_it_could_not_rewrite(config: Path, tmp_path: Path) -> None: - """黙って相対パスのまま保存すると、正常終了して効かない状態になる(決定 6)。""" - target = tmp_path / "odd" - (target / "scripts").mkdir(parents=True) - (target / "hooks.json").write_text( - json.dumps({"ndf-odd": {"PreInvocation": [{"type": "command", "command": "sh -c './scripts/x.sh'"}]}}), - encoding="utf-8", - ) - - result = run("--config", str(config), "--plugin-dir", str(target)) - - assert result.returncode == 0, result.stderr - assert "書き換えられなかった" in result.stdout + result.stderr diff --git a/scripts/tests/test_agy_listing_budget.py b/scripts/tests/test_agy_listing_budget.py deleted file mode 100644 index 6c64805a4..000000000 --- a/scripts/tests/test_agy_listing_budget.py +++ /dev/null @@ -1,114 +0,0 @@ -"""agy の初期一覧を予算の判定へ入れる(#215 の受け入れ条件 A10)。 - -配布先が 1 つ増えても、`manifests/agy-skills.txt` を読む経路と予算を突き合わせる経路が -無ければ、agy の一覧だけが判定されないまま増え続ける。**予算そのものは Claude Code から -借りた値である**(agy に規定が無いため。`scripts/check-skill-frontmatter.py` の注記)。 -借りた事実は注記に残し、ここでは「借りた値で判定が働くこと」を固定する。 - -一時ディレクトリへ作った木に対して実行する。実物の Skill は読むだけで、書き換えない。 -""" - -from __future__ import annotations - -import subprocess -import sys -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[2] -CHECKER = REPO_ROOT / "scripts/check-skill-frontmatter.py" - -FAMILY = "ndf" - - -def write_skill(skills_dir: Path, name: str, description: str) -> None: - (skills_dir / name).mkdir(parents=True, exist_ok=True) - (skills_dir / name / "SKILL.md").write_text( - f'---\nname: {name}\ndescription: "{description}"\n---\n\n# {name}\n', - encoding="utf-8", - ) - - -def build_tree(base: Path, count: int, description: str) -> Path: - """agy の manifest を持つ木を作り、その `skills/` を返す。""" - plugin = base / f"plugins/{FAMILY}" - skills_dir = plugin / "skills" - names = [f"probe-{index:03d}" for index in range(count)] - for name in names: - write_skill(skills_dir, name, description) - (plugin / "manifests").mkdir(parents=True, exist_ok=True) - for runtime in ("claude", "codex", "kiro", "agy"): - (plugin / f"manifests/{runtime}-skills.txt").write_text("".join(f"{name}\n" for name in names), encoding="utf-8") - return skills_dir - - -def run_checker(skills_dir: Path, *args: str) -> subprocess.CompletedProcess[str]: - return subprocess.run( - [sys.executable, str(CHECKER), "--skills-dir", str(skills_dir), *args], - capture_output=True, - text=True, - check=False, - ) - - -def report_row(output: str, runtime: str) -> list[str]: - for line in output.splitlines(): - columns = line.split() - if columns and columns[0] == runtime: - return columns - raise AssertionError(f"{runtime} の行が出力に無い\n{output}") - - -def test_report_lists_agy_with_a_budget(tmp_path: Path) -> None: - """報告に agy の行が出て、上限を持つ。""" - skills_dir = build_tree(tmp_path, count=3, description="Probe skill for the listing budget.") - result = run_checker(skills_dir, "--report") - - assert result.returncode == 0, result.stdout + result.stderr - row = report_row(result.stdout, "agy") - assert row[1].isdigit() and int(row[1]) > 0, row - assert row[2].isdigit() and int(row[2]) > 0, row - - -def test_agy_borrows_the_claude_budget(tmp_path: Path) -> None: - """上限は Claude Code と同じ値である(同じコンテキスト長の規定を借りている)。""" - skills_dir = build_tree(tmp_path, count=3, description="Probe skill for the listing budget.") - result = run_checker(skills_dir, "--report") - - assert report_row(result.stdout, "agy")[2] == report_row(result.stdout, "claude")[2] - - -def test_agy_listing_counts_the_path(tmp_path: Path) -> None: - """agy の一覧はパスを含む側で見積もる。Claude Code の合計より大きくなる。 - - 一覧の構成に公式の記述が無いため、多い側で見積もる(Kiro CLI と同じ扱い)。 - """ - skills_dir = build_tree(tmp_path, count=3, description="Probe skill for the listing budget.") - result = run_checker(skills_dir, "--report") - - agy = int(report_row(result.stdout, "agy")[1]) - claude = int(report_row(result.stdout, "claude")[1]) - assert agy > claude, result.stdout - - -def test_agy_budget_overflow_is_reported(tmp_path: Path) -> None: - """予算を超えると agy の指摘が出る。読み取るだけで判定していない状態を弾く。""" - skills_dir = build_tree(tmp_path, count=60, description="x" * 900) - result = run_checker(skills_dir) - - output = result.stdout + result.stderr - assert "ops/agy-listing" in output, output - assert result.returncode != 0 - - -def test_no_agy_manifest_means_no_agy_row(tmp_path: Path) -> None: - """`agy-skills.txt` を持たない木では agy の行が出ない。 - - manifest が唯一の配布の基準であることを、無い側でも確かめる。 - """ - skills_dir = build_tree(tmp_path, count=3, description="Probe skill for the listing budget.") - (skills_dir.parent / "manifests/agy-skills.txt").unlink() - result = run_checker(skills_dir, "--report") - - assert result.returncode == 0, result.stdout + result.stderr - with_agy = [line for line in result.stdout.splitlines() if line.split()[:1] == ["agy"]] - assert not with_agy, result.stdout diff --git a/scripts/tests/test_check_required_checks.py b/scripts/tests/test_check_required_checks.py index 611d7dbef..7b46124a5 100644 --- a/scripts/tests/test_check_required_checks.py +++ b/scripts/tests/test_check_required_checks.py @@ -21,14 +21,12 @@ assert spec.loader is not None spec.loader.exec_module(crc) -# 2026-10-05 の develop の必須のチェック 12 個 -REQUIRED_12 = [ +# 2026-10-05 の develop の必須のチェック 12 個から、配布をやめた runtime-smoke の kiro・agy を除いた 10 個(#2231) +REQUIRED_10 = [ "guard", "pytest", "runtime-smoke (claude)", "runtime-smoke (codex)", - "runtime-smoke (kiro)", - "runtime-smoke (agy)", "runtime-plugin-build-check", "skill-frontmatter-check", "skill-repo-assumptions-check", @@ -36,7 +34,7 @@ "markdown-link-check", "instruction-files-check", ] -# 承認ゲート 2 で ruleset へ足した 7 つ(#653)。足した後の必須は 19 個 +# 承認ゲート 2 で ruleset へ足した 7 つ(#653)。足した後の必須は 17 個 ADDED_7 = [ "doc-line-limit-check", "glossary-check", @@ -46,7 +44,7 @@ "script-structure-check", "skill-shell-vars-check", ] -REQUIRED_19 = REQUIRED_12 + ADDED_7 +REQUIRED_17 = REQUIRED_10 + ADDED_7 def rules(*contexts: str) -> list[dict]: @@ -117,12 +115,12 @@ def fake_run(cmd, **kw): def test_this_repository_has_only_the_awaiting_six_and_itself(tmp_path, capsys): - """AC6: 12 個の必須と必須にしないジョブの宣言だけで、差は 6 つと突き合わせのチェック自身だけ。""" + """AC6: 10 個の必須と必須にしないジョブの宣言だけで、差は 6 つと突き合わせのチェック自身だけ。""" allow = json.loads((REPO / "scripts" / "required-checks-allow.json").read_text(encoding="utf-8")) (tmp_path / "allow.json").write_text( json.dumps({"version": 1, "not_required": allow["not_required"], "awaiting": allow["awaiting"]}), encoding="utf-8" ) - (tmp_path / "rules.json").write_text(json.dumps(rules(*REQUIRED_12)), encoding="utf-8") + (tmp_path / "rules.json").write_text(json.dumps(rules(*REQUIRED_10)), encoding="utf-8") code = crc.main( [ "--root", @@ -404,7 +402,7 @@ def test_the_job_runs_only_on_pull_requests(): def test_cli_runs_as_a_script(tmp_path): rules_file = tmp_path / "rules.json" - rules_file.write_text(json.dumps(rules(*REQUIRED_19)), encoding="utf-8") + rules_file.write_text(json.dumps(rules(*REQUIRED_17)), encoding="utf-8") p = subprocess.run( [sys.executable, str(CHECK), "--root", str(REPO), "--repo", "o/r", "--branch", "develop", "--rules-file", str(rules_file)], capture_output=True, diff --git a/scripts/tests/test_cross_review_codistribution.py b/scripts/tests/test_cross_review_codistribution.py index 445cd67a0..70b3d2a35 100644 --- a/scripts/tests/test_cross_review_codistribution.py +++ b/scripts/tests/test_cross_review_codistribution.py @@ -20,7 +20,7 @@ def test_the_manifests_are_found() -> None: """配る基準そのものが見つかること。**空の一覧で通さない。**""" - assert len(MANIFESTS) >= 4, [p.name for p in MANIFESTS] + assert len(MANIFESTS) >= 2, [p.name for p in MANIFESTS] @pytest.mark.parametrize("manifest", MANIFESTS, ids=lambda p: p.stem) diff --git a/scripts/tests/test_distribution_runtimes.py b/scripts/tests/test_distribution_runtimes.py new file mode 100644 index 000000000..7b9a4d2b6 --- /dev/null +++ b/scripts/tests/test_distribution_runtimes.py @@ -0,0 +1,56 @@ +"""配布先は Claude Code と Codex だけである(#2231 の I1)。 + +agy と Kiro CLI へは何も配らず、cross-review / cross-refactoring の相手役として起動するだけにした。 +その導入物(`agy-skills.txt`・`kiro-skills.txt`・`dev.agy/`・`dev.kiro/`)がどのプラグインにも戻らないこと、 +リリースの対象ランタイム(`.ndf/supervise.json` の `release.runtimes`)が `claude`・`codex` の中に +収まることを確かめる。生成の手順は agy・kiro の導入物を作らないため、手で足したものはここでしか見つからない。 +""" + +from __future__ import annotations + +import json +from pathlib import Path + +REPO = Path(__file__).resolve().parents[2] +DISTRIBUTED = {"claude", "codex"} +RETIRED_FILES = ("agy-skills.txt", "kiro-skills.txt") +RETIRED_DIRS = ("dev.agy", "dev.kiro") + + +def retired_paths(root: Path) -> list[str]: + """`plugins/` の下にある agy・kiro の導入物を、根からの相対パスで返す。""" + found = [p for name in RETIRED_FILES for p in root.glob(f"plugins/**/manifests/{name}")] + found += [p for name in RETIRED_DIRS for p in root.glob(f"plugins/**/{name}") if p.is_dir()] + return sorted(p.relative_to(root).as_posix() for p in found) + + +def release_runtimes(root: Path) -> set[str]: + data = json.loads((root / ".ndf/supervise.json").read_text(encoding="utf-8")) + return set(data["release"]["runtimes"]) + + +def test_no_plugin_ships_agy_or_kiro_files() -> None: + assert retired_paths(REPO) == [] + + +def test_release_runtimes_are_claude_and_codex_only() -> None: + runtimes = release_runtimes(REPO) + assert runtimes + assert runtimes <= DISTRIBUTED, runtimes - DISTRIBUTED + + +def test_a_restored_kiro_installer_is_found(tmp_path: Path) -> None: + """mcp-* の `dev.kiro/install.sh` を 1 つ戻すと見つかる(検査が空振りしていないこと)。""" + installer = tmp_path / "plugins/mcp/mcp-notion/dev.kiro/install.sh" + installer.parent.mkdir(parents=True) + installer.write_text("#!/bin/sh\n", encoding="utf-8") + manifest = tmp_path / "plugins/ndf/manifests/agy-skills.txt" + manifest.parent.mkdir(parents=True) + manifest.write_text("pr\n", encoding="utf-8") + assert retired_paths(tmp_path) == ["plugins/mcp/mcp-notion/dev.kiro", "plugins/ndf/manifests/agy-skills.txt"] + + +def test_a_kiro_release_runtime_is_found(tmp_path: Path) -> None: + (tmp_path / ".ndf").mkdir() + (tmp_path / ".ndf/supervise.json").write_text(json.dumps({"release": {"runtimes": ["claude", "codex", "kiro"]}}), encoding="utf-8") + assert release_runtimes(tmp_path) - DISTRIBUTED == {"kiro"} diff --git a/scripts/tests/test_doc_staleness.py b/scripts/tests/test_doc_staleness.py index 0d2cad35b..1648fdb0b 100644 --- a/scripts/tests/test_doc_staleness.py +++ b/scripts/tests/test_doc_staleness.py @@ -74,9 +74,7 @@ def test_root_defaults_to_current_directory() -> None: ("before", "after", "expected"), [ ("Claude Code向け core 5個", "Claude Code向け core 4個", "5"), - ("Kiro向け core 4個", "Kiro向け core 3個", "4"), ("Codex向け core 3個", "Codex向け core 2個", "3"), - ("agy向け core 2個", "agy向け core 1個", "2"), ], ) def test_runtime_skill_count_mismatch_fails(tree: Path, before: str, after: str, expected: str) -> None: @@ -92,9 +90,7 @@ def test_runtime_skill_count_mismatch_fails(tree: Path, before: str, after: str, "fragment", [ "Claude Code向け core 5個、", - "Kiro向け core 4個、", - "Codex向け core 3個、", - "agy向け core 2個", + "Codex向け core 3個", ], ) def test_runtime_skill_count_removed_fails(tree: Path, fragment: str) -> None: @@ -167,8 +163,6 @@ def test_category_breakdown_removed_fails(tree: Path) -> None: [ ("| Claude Code | 5 個 |", "| Claude Code | 4 個 |", "5"), ("| Codex | 3 個 |", "| Codex | 2 個 |", "3"), - ("| Kiro CLI | 4 個 |", "| Kiro CLI | 9 個 |", "4"), - ("| agy | 2 個 |", "| agy | 7 個 |", "2"), ], ) def test_distribution_table_mismatch_fails(tree: Path, before: str, after: str, expected: str) -> None: @@ -185,8 +179,6 @@ def test_distribution_table_mismatch_fails(tree: Path, before: str, after: str, [ "| Claude Code | 5 個 | `.claude-plugin/plugin.json` |\n", "| Codex | 3 個 | `.codex-plugin/plugin.json` |\n", - "| Kiro CLI | 4 個 | `dev.kiro/install.sh` |\n", - "| agy | 2 個 | `dev.agy/plugin.json` |\n", ], ) def test_distribution_table_row_removed_fails(tree: Path, row: str) -> None: @@ -265,10 +257,10 @@ def test_failure_output_names_file_label_and_both_values(tree: Path) -> None: def test_missing_manifest_fails(tree: Path) -> None: """数える相手が無いことを、読み取れた値と一致しているとみなさない。""" - (tree / "plugins/ndf/manifests/kiro-skills.txt").unlink() + (tree / "plugins/ndf/manifests/codex-skills.txt").unlink() result = run_check(tree) assert result.returncode != 0 - assert "kiro-skills.txt" in output_of(result) + assert "codex-skills.txt" in output_of(result) def test_missing_plugin_json_fails(tree: Path) -> None: @@ -315,7 +307,6 @@ def agents_md(tree: Path) -> Path: ("mark", "document", "before", "after"), [ ("G", "README.md", "**NDFプラグイン v9.3.0**", "**NDFプラグイン v9.2.1**"), - ("K", "plugins/ndf/README.md", "Kiro CLI用 / v9.3.0)", "Kiro CLI用 / v9.2.1)"), ( "M", "plugins/ndf/README.md", @@ -352,7 +343,6 @@ def test_codex_cache_path_partial_stale_fails(tree: Path) -> None: ("mark", "document", "fragment", "occurrences"), [ ("G", "README.md", "**NDFプラグイン v9.3.0** のチェック用の最小構成です。\n", 1), - ("K", "plugins/ndf/README.md", "# => NDF統合開発エージェント(Kiro CLI用 / v9.3.0)\n", 1), ("L", "plugins/ndf/README.md", "~/.codex/plugins/cache/ai-plugins/ndf/9.3.0/skills/deploy/SKILL.md", 2), ("M", "plugins/ndf/README.md", "# => ndf@ai-plugins installed, enabled 9.3.0 \n", 1), ], @@ -804,7 +794,7 @@ def test_versions_outside_the_section_do_not_fail(tree: Path) -> None: def test_bumping_only_the_plugin_version_reports_every_body_claim(tree: Path) -> None: - """この課題が起きた経路そのもの。説明文書を直さずに版だけ上げると 6 種類が挙がる。""" + """この課題が起きた経路そのもの。説明文書を直さずに版だけ上げると 5 種類が挙がる。""" bump_plugin_version(tree, "9.4.0") result = run_check(tree) assert result.returncode != 0 @@ -813,7 +803,6 @@ def test_bumping_only_the_plugin_version_reports_every_body_claim(tree: Path) -> "概要の版数", "プラグイン一覧表の ndf の版数", "版の付け方の節の版数", - "Kiro の確認例の版数", "Codex のキャッシュパスの例の版数", "`codex plugin list` の出力例の版数", ): @@ -907,12 +896,12 @@ def test_manifest_skill_count_with_only_blank_and_comment_lines_returns_zero(tmp `None` と区別されている経路である。 """ module = _load_checker() - manifest = tmp_path / "plugins/ndf/manifests/kiro-skills.txt" + manifest = tmp_path / "plugins/ndf/manifests/codex-skills.txt" manifest.parent.mkdir(parents=True) manifest.write_text("# コメント行\n\n \n# 別のコメント\n", encoding="utf-8") report = module.Report() - assert module.manifest_skill_count(tmp_path, "kiro", report) == 0 + assert module.manifest_skill_count(tmp_path, "codex", report) == 0 assert report.errors == [] diff --git a/scripts/tests/test_external_ai_codistribution.py b/scripts/tests/test_external_ai_codistribution.py index 478120c9b..4780bbdfb 100644 --- a/scripts/tests/test_external_ai_codistribution.py +++ b/scripts/tests/test_external_ai_codistribution.py @@ -50,7 +50,7 @@ def test_some_skill_points_at_external_ai() -> None: def test_the_manifests_are_found() -> None: - assert len(MANIFESTS) == 4, [m.name for m in MANIFESTS] + assert len(MANIFESTS) == 2, [m.name for m in MANIFESTS] @pytest.mark.parametrize("manifest", MANIFESTS, ids=lambda p: p.name) diff --git a/scripts/tests/test_kiro_installer_project.py b/scripts/tests/test_kiro_installer_project.py deleted file mode 100644 index 0dd8627ae..000000000 --- a/scripts/tests/test_kiro_installer_project.py +++ /dev/null @@ -1,485 +0,0 @@ -"""Kiro のインストーラが `--project` の誤った値を案内つきで止めることを固定する(#415)。 - -変更前は `cd` の裸のエラーと終了コード 1 で終わり、何を直せばよいかが読み取れなかった。 -ここで見るのは、**誤りの種類ごとに文言が分かれること**・**存在しないパスにだけ -`mkdir -p` の案内が付くこと**・**案内のパスが bash の語 1 つとして読み戻せること**・ -**正しい値では振る舞いが変わらないこと**の 4 つである。 - -終了コードはどちらの誤りも 2 であるため、文言まで見ないと 2 つの誤りを区別できない。 -正しい値の確認は `--dry-run` で行い、利用者の環境へ書き込まない。 -""" - -from __future__ import annotations - -import json -import os -import shutil -import subprocess -from pathlib import Path - -import pytest - -ROOT = Path(__file__).resolve().parents[2] -INSTALLER = ROOT / "plugins" / "ndf" / "dev.kiro" / "install.sh" - - -def run(*args: str, home: Path, cwd: Path | None = None) -> subprocess.CompletedProcess: - # `--scope global` は HOME の下を導入先にする。誤って書き込んでも利用者の HOME に - # 届かないよう、一時ディレクトリを HOME として渡す。 - env = {**os.environ, "HOME": str(home)} - return subprocess.run( - ["bash", str(INSTALLER), *args], - capture_output=True, - text=True, - env=env, - cwd=cwd, - ) - - -def hint_words(stderr: str) -> list[str]: - """`HINT: mkdir -p ` に続く部分を bash で読み戻し、語の並びを返す。 - - `shlex.split` は `$'...'` の形を解さないため使わない(タブを含むパスで `$` が残る)。 - """ - prefix = "HINT: mkdir -p " - lines = [line for line in stderr.splitlines() if line.startswith(prefix)] - assert len(lines) == 1, stderr - quoted = lines[0][len(prefix) :] - out = subprocess.run( - ["bash", "-c", 'eval "set -- $1"; printf "%s\\0" "$@"', "_", quoted], - capture_output=True, - text=True, - check=True, - ).stdout - return out.split("\0")[:-1] - - -def assert_no_bare_cd_error(proc: subprocess.CompletedProcess) -> None: - assert "cd:" not in proc.stdout + proc.stderr - - -@pytest.mark.parametrize( - ("missing_path", "extra_args"), - [ - (Path("skills"), ()), - (Path("manifests/kiro-skills.txt"), ()), - (Path("dev.kiro/prompts/codex.md"), ("--with-codex",)), - ], -) -def test_missing_prerequisite_stops_with_path_error(tmp_path: Path, missing_path: Path, extra_args: tuple[str, ...]) -> None: - # 現状固定: 必須パスの種類やチェック位置によらず、同じ形式のエラー 1 行と - # 終了コード 1 で停止する。 - plugin_dir = tmp_path / "ndf" - shutil.copytree(INSTALLER.parents[1], plugin_dir) - missing = plugin_dir / missing_path - if missing.is_dir(): - shutil.rmtree(missing) - else: - missing.unlink() - - project = tmp_path / "project" - project.mkdir() - env = {**os.environ, "HOME": str(tmp_path)} - proc = subprocess.run( - [ - "bash", - str(plugin_dir / "dev.kiro" / "install.sh"), - "--project", - str(project), - "--dry-run", - "--yes", - *extra_args, - ], - capture_output=True, - text=True, - env=env, - ) - - assert proc.returncode == 1 - assert proc.stderr.splitlines() == [f"ERROR: {missing} が見つかりません"] - - -def test_project_without_path_stops_with_error(tmp_path: Path) -> None: - proc = run("--project", home=tmp_path) - - assert proc.returncode == 2 - assert proc.stderr.splitlines() == ["ERROR: --project requires a path"] - assert_no_bare_cd_error(proc) - - -@pytest.mark.parametrize("name", ["missing", "my project", "tab\tx"]) -def test_missing_path_stops_with_error_and_hint(tmp_path: Path, name: str) -> None: - target = tmp_path / name - proc = run("--project", str(target), "--yes", home=tmp_path) - - assert proc.returncode == 2 - lines = proc.stderr.splitlines() - assert lines[0] == f"ERROR: --project points at a path that does not exist: {target}" - assert lines[1].startswith("HINT: mkdir -p ") - assert len(lines) == 2 - # 案内のパスは、空白やタブを含んでも語 1 つとして渡したパスへ戻る - assert hint_words(proc.stderr) == [str(target)] - assert not target.exists() - assert_no_bare_cd_error(proc) - - -def test_empty_path_stops_with_error_and_hint(tmp_path: Path) -> None: - proc = run("--project", "", "--yes", home=tmp_path) - - assert proc.returncode == 2 - lines = proc.stderr.splitlines() - assert lines[0] == "ERROR: --project points at a path that does not exist: " - assert lines[1].startswith("HINT: mkdir -p ") - assert len(lines) == 2 - assert hint_words(proc.stderr) == [""] - assert list(tmp_path.iterdir()) == [] - assert_no_bare_cd_error(proc) - - -def test_file_path_stops_without_hint(tmp_path: Path) -> None: - target = tmp_path / "afile" - target.write_text("", encoding="utf-8") - proc = run("--project", str(target), "--yes", home=tmp_path) - - assert proc.returncode == 2 - assert proc.stderr.splitlines() == [f"ERROR: --project points at a path that is not a directory: {target}"] - assert "HINT:" not in proc.stderr - assert_no_bare_cd_error(proc) - - -def test_global_scope_with_missing_path_stops_the_same_way(tmp_path: Path) -> None: - target = tmp_path / "missing" - proc = run("--scope", "global", "--project", str(target), "--yes", home=tmp_path) - - assert proc.returncode == 2 - lines = proc.stderr.splitlines() - assert lines[0] == f"ERROR: --project points at a path that does not exist: {target}" - assert hint_words(proc.stderr) == [str(target)] - assert len(lines) == 2 - assert "WARN:" not in proc.stderr - assert_no_bare_cd_error(proc) - - -def test_global_scope_with_existing_path_warns_and_succeeds(tmp_path: Path) -> None: - # --scope global と存在するディレクトリの併用では、PROJECT_GIVEN が真になって - # WARN を出しつつ正常終了する。誤りではないため終了コードは 0 で、案内 (HINT) も - # 出ない。存在しないパス併用時のエラー停止(上のテスト)とは別の経路である。 - project = tmp_path / "project" - project.mkdir() - proc = run( - "--scope", - "global", - "--project", - str(project), - "--dry-run", - "--yes", - home=tmp_path, - ) - - assert proc.returncode == 0, proc.stderr - assert "WARN: --scope global では --project は使用されません" in proc.stderr - assert "ERROR:" not in proc.stderr - assert "HINT:" not in proc.stderr - assert_no_bare_cd_error(proc) - - -def test_existing_directory_is_unchanged(tmp_path: Path) -> None: - project = tmp_path / "project" - project.mkdir() - proc = run("--project", str(project), "--dry-run", "--yes", home=tmp_path) - - assert proc.returncode == 0, proc.stderr - assert "ERROR:" not in proc.stderr - assert_no_bare_cd_error(proc) - # --dry-run は導入先へ書き込まない - assert list(project.iterdir()) == [] - - -def test_reinstall_preserves_user_managed_agent_config(tmp_path: Path) -> None: - project = tmp_path / "project" - project.mkdir() - first = run("--project", str(project), "--yes", home=tmp_path) - assert first.returncode == 0, first.stderr - - agent_file = project / ".kiro" / "agents" / "ndf.json" - config = json.loads(agent_file.read_text(encoding="utf-8")) - config["customKey"] = {"enabled": True} - config.setdefault("mcpServers", {})["myserver"] = { - "command": "myserver", - "args": ["serve"], - } - config.setdefault("hooks", {})["myhook"] = [{"command": "echo custom", "timeout_ms": 1000}] - agent_file.write_text(json.dumps(config, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - - second = run("--project", str(project), "--yes", home=tmp_path) - assert second.returncode == 0, second.stderr - - reinstalled = json.loads(agent_file.read_text(encoding="utf-8")) - assert reinstalled["customKey"] == {"enabled": True} - assert reinstalled["mcpServers"]["myserver"] == { - "command": "myserver", - "args": ["serve"], - } - assert reinstalled["hooks"]["myhook"] == [{"command": "echo custom", "timeout_ms": 1000}] - assert reinstalled["hooks"]["agentSpawn"] - assert reinstalled["hooks"]["userPromptSubmit"] - - -def test_reinstall_removes_optional_features_when_flags_omitted(tmp_path: Path) -> None: - project = tmp_path / "project" - project.mkdir() - first = run( - "--project", - str(project), - "--with-slack", - "--with-codex", - "--yes", - home=tmp_path, - ) - assert first.returncode == 0, first.stderr - - agent_file = project / ".kiro" / "agents" / "ndf.json" - config = json.loads(agent_file.read_text(encoding="utf-8")) - assert "stop" in config["hooks"] - assert "codex" in config["mcpServers"] - - config["customKey"] = {"enabled": True} - config["mcpServers"]["myserver"] = { - "command": "myserver", - "args": ["serve"], - } - config["hooks"]["myhook"] = [{"command": "echo custom", "timeout_ms": 1000}] - agent_file.write_text(json.dumps(config, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - - second = run("--project", str(project), "--yes", home=tmp_path) - assert second.returncode == 0, second.stderr - - reinstalled = json.loads(agent_file.read_text(encoding="utf-8")) - assert "stop" not in reinstalled["hooks"] - assert "codex" not in reinstalled.get("mcpServers", {}) - assert reinstalled["mcpServers"]["myserver"] == { - "command": "myserver", - "args": ["serve"], - } - assert reinstalled["hooks"]["myhook"] == [{"command": "echo custom", "timeout_ms": 1000}] - assert reinstalled["customKey"] == {"enabled": True} - assert reinstalled["hooks"]["agentSpawn"] - assert reinstalled["hooks"]["userPromptSubmit"] - - -def test_reinstall_removes_deprecated_prompts_and_keeps_user_prompt(tmp_path: Path) -> None: - # 配布を終えた clean.md と review.md は再インストールで除去し、利用者が置いた - # 無関係な prompt は残す。削除範囲が広がる退行を検出するための現状固定である。 - project = tmp_path / "project" - prompts_dir = project / ".kiro" / "prompts" - prompts_dir.mkdir(parents=True) - (prompts_dir / "clean.md").write_text("old clean\n", encoding="utf-8") - (prompts_dir / "review.md").write_text("old review\n", encoding="utf-8") - custom_text = "利用者が置いた prompt\n" - (prompts_dir / "custom.md").write_text(custom_text, encoding="utf-8") - - proc = run("--project", str(project), "--yes", home=tmp_path) - - assert proc.returncode == 0, proc.stderr - assert " removed (deprecated): clean" in proc.stdout.splitlines() - assert " removed (deprecated): review" in proc.stdout.splitlines() - assert not (prompts_dir / "clean.md").exists() - assert not (prompts_dir / "review.md").exists() - assert (prompts_dir / "pr.md").is_file() - assert (prompts_dir / "custom.md").read_text(encoding="utf-8") == custom_text - - # --with-codex を付けないため codex.md は配布されない - distributed = {p.name for p in (INSTALLER.parent / "prompts").glob("*.md") if p.name != "codex.md"} - assert distributed - assert {p.name for p in prompts_dir.iterdir()} == distributed | {"custom.md"} - - -@pytest.mark.parametrize( - ("existing", "warn_marker"), - [ - # 不正 JSON: 読み込みが例外になり、読めない旨で引継ぎを断念する。 - ("{ not json", "を読めないため引き継ぎません"), - # JSON オブジェクト以外(配列): dict でないため引継ぎを断念する。 - ("[]", "が JSON オブジェクトではないため引き継ぎません"), - # hooks がオブジェクト以外(配列): その節だけ引継ぎを断念する。 - ('{"hooks": []}', "の hooks が JSON オブジェクトではないため引き継ぎません"), - # mcpServers がオブジェクト以外(文字列): その節だけ引継ぎを断念する。 - ('{"mcpServers": "x"}', "の mcpServers が JSON オブジェクトではないため引き継ぎません"), - ], -) -def test_reinstall_with_malformed_agent_config_warns_and_continues(tmp_path: Path, existing: str, warn_marker: str) -> None: - # 既存 ndf.json が不正 JSON・JSON オブジェクト以外・hooks / mcpServers が - # オブジェクト以外のとき、引継ぎを断念して警告を出しつつ導入を続ける経路を固定する。 - # 終了コードは 0 で、出力された ndf.json は JSON オブジェクトとして読め、 - # テンプレート由来の agentSpawn と userPromptSubmit を必ず持つ。 - project = tmp_path / "project" - (project / ".kiro" / "agents").mkdir(parents=True) - agent_file = project / ".kiro" / "agents" / "ndf.json" - agent_file.write_text(existing, encoding="utf-8") - - proc = run("--project", str(project), "--yes", home=tmp_path) - - assert proc.returncode == 0, proc.stderr - # 引継ぎ断念の警告は Python 側の print で標準出力へ出る。 - assert f"WARN: 既存の {agent_file} {warn_marker}" in proc.stdout - assert_no_bare_cd_error(proc) - - reinstalled = json.loads(agent_file.read_text(encoding="utf-8")) - assert isinstance(reinstalled, dict) - assert reinstalled["hooks"]["agentSpawn"] - assert reinstalled["hooks"]["userPromptSubmit"] - - -@pytest.mark.parametrize( - ("cwd_part", "arg", "resolved_part"), - [(".", "project/sub", "project/sub"), ("project", ".", "project")], -) -def test_relative_path_is_resolved_against_cwd(tmp_path: Path, cwd_part: str, arg: str, resolved_part: str) -> None: - # 相対パスは実行時のカレントディレクトリを起点に `cd` と `pwd` で絶対パスへ解決され、 - # スコープの表示にはその絶対パスが出る。 - base = tmp_path.resolve() - (base / "project" / "sub").mkdir(parents=True) - proc = run("--project", arg, "--dry-run", "--yes", home=base, cwd=base / cwd_part) - - assert proc.returncode == 0, proc.stderr - assert f"スコープ: workspace ({base / resolved_part}/.kiro)" in proc.stdout - assert "ERROR:" not in proc.stderr - assert_no_bare_cd_error(proc) - - -@pytest.mark.parametrize("case", ["symlink", "file", "directory"]) -def test_ndf_policies_skill_migration(tmp_path: Path, case: str) -> None: - # ndf-policies の旧シンボリックリンクは削除する一方、同名の通常ファイルまたは - # 実体ディレクトリは保持する分岐と、どちらでも steering を生成する結果を固定する。 - project = tmp_path / f"project_{case}" - skills_dir = project / ".kiro" / "skills" - skills_dir.mkdir(parents=True) - target = skills_dir / "ndf-policies" - - if case == "symlink": - dummy = tmp_path / "legacy_target" - dummy.mkdir() - target.symlink_to(dummy) - elif case == "file": - target.write_text("user managed ndf-policies file\n", encoding="utf-8") - elif case == "directory": - target.mkdir() - (target / "custom.md").write_text("user managed content\n", encoding="utf-8") - - proc = run("--project", str(project), "--yes", home=tmp_path) - - assert proc.returncode == 0, proc.stderr - assert_no_bare_cd_error(proc) - - if case == "symlink": - assert not target.exists() - assert not target.is_symlink() - assert " REMOVED: ndf-policies (steering へ移行済みのため .kiro/skills のリンクを削除)" in proc.stdout - assert f"WARN: {target} はシンボリックリンクではありません。" not in proc.stderr - elif case == "file": - assert target.is_file() - assert not target.is_symlink() - assert target.read_text(encoding="utf-8") == "user managed ndf-policies file\n" - assert f"WARN: {target} はシンボリックリンクではありません。" in proc.stderr - assert " REMOVED: ndf-policies" not in proc.stdout - elif case == "directory": - assert target.is_dir() - assert not target.is_symlink() - assert (target / "custom.md").read_text(encoding="utf-8") == "user managed content\n" - assert f"WARN: {target} はシンボリックリンクではありません。" in proc.stderr - assert " REMOVED: ndf-policies" not in proc.stdout - - steering_file = project / ".kiro" / "steering" / "ndf-policies.md" - assert steering_file.is_file() - steering_content = steering_file.read_text(encoding="utf-8") - assert "" in steering_content - assert "" in steering_content - assert "# NDFポリシー" in steering_content - assert "## ブランチ運用の原則" in steering_content - - source_skill = ROOT / "plugins" / "ndf" / "skills" / "ndf-policies" / "SKILL.md" - source_body = source_skill.read_text(encoding="utf-8").split("\n---\n", 1)[1].strip("\n") - assert source_body in steering_content - - -def _kiro_manifest_skills() -> set[str]: - """manifests/kiro-skills.txt が配布対象として列挙する Skill 名の集合。 - - install.sh の Step 1 と同じ整形(コメント除去・末尾空白除去・空行除去)で読む。 - """ - manifest = INSTALLER.parents[1] / "manifests" / "kiro-skills.txt" - names: set[str] = set() - for raw in manifest.read_text(encoding="utf-8").splitlines(): - line = raw.split("#", 1)[0].rstrip() - if line: - names.add(line) - return names - - -def test_manifest_skills_are_linked_with_count_and_output(tmp_path: Path) -> None: - # 現状固定: Step 1 の Skill 配布パイプラインは manifest 掲載 Skill のうち - # ndf-policies を除いた分だけ .kiro/skills/ へ symlink を張り、その本数を - # 「Skills数」として出力し、各 Skill に " linked: <名前>" を出す。リンク先は - # プラグインの skills/<名前> を指す。構造改善で本数・リンク先・出力が動かないことを守る。 - project = tmp_path / "project" - project.mkdir() - proc = run("--project", str(project), "--yes", home=tmp_path) - assert proc.returncode == 0, proc.stderr - - skills_dir = project / ".kiro" / "skills" - plugin_skills = INSTALLER.parents[1] / "skills" - manifest_skills = _kiro_manifest_skills() - expected_linked = manifest_skills - {"ndf-policies"} - - # 実際に張られた symlink とそのリンク先を確認する。 - linked = {p.name for p in skills_dir.iterdir() if p.is_symlink()} - assert linked == expected_linked - for name in expected_linked: - assert (skills_dir / name).resolve() == (plugin_skills / name).resolve() - - # ndf-policies は steering へ回すためリンクしない。 - assert not (skills_dir / "ndf-policies").is_symlink() - - # 出力の「linked」行と「Skills数」が本数と一致する。 - linked_lines = {line[len(" linked: ") :] for line in proc.stdout.splitlines() if line.startswith(" linked: ")} - assert linked_lines == expected_linked - assert f" Skills数: {len(expected_linked)} (シンボリックリンク: {skills_dir})" in proc.stdout - - -def test_reinstall_removes_only_managed_skill_links(tmp_path: Path) -> None: - # 現状固定: 掃除が消すのは現在の checkout(プラグインの skills/)配下を指す - # 既存リンクだけである。別の場所を指す利用者のリンクは残す。再インストールでも - # 掃除→再リンクで最終状態が manifest 掲載分と一致する。 - project = tmp_path / "project" - project.mkdir() - first = run("--project", str(project), "--yes", home=tmp_path) - assert first.returncode == 0, first.stderr - - skills_dir = project / ".kiro" / "skills" - plugin_skills = INSTALLER.parents[1] / "skills" - - # 利用者が別の場所を指して張ったリンク(掃除対象外)。 - foreign_target = tmp_path / "foreign_skill" - foreign_target.mkdir() - (skills_dir / "user-external").symlink_to(foreign_target) - - # 現在の checkout を指すが manifest に無い名前のリンク(掃除対象)。 - stale = skills_dir / "stale-managed" - stale.symlink_to(plugin_skills / "pr") - - second = run("--project", str(project), "--yes", home=tmp_path) - assert second.returncode == 0, second.stderr - - manifest_skills = _kiro_manifest_skills() - expected_managed = manifest_skills - {"ndf-policies"} - - # 現在の checkout 配下を指す managed リンクは manifest 掲載分だけが残る。 - assert not (skills_dir / "stale-managed").exists() - # 別の場所を指す利用者のリンクは残る。 - assert (skills_dir / "user-external").is_symlink() - assert (skills_dir / "user-external").resolve() == foreign_target.resolve() - - managed_now = { - p.name for p in skills_dir.iterdir() if p.is_symlink() and str(p.resolve()).startswith(str(plugin_skills.resolve()) + os.sep) - } - assert managed_now == expected_managed diff --git a/scripts/tests/test_kiro_mcp_installer_tools.py b/scripts/tests/test_kiro_mcp_installer_tools.py deleted file mode 100644 index 9efa8833c..000000000 --- a/scripts/tests/test_kiro_mcp_installer_tools.py +++ /dev/null @@ -1,71 +0,0 @@ -"""MCP プラグインの Kiro 用 installer が default.json に `tools` を持たせることを固定する(#1873)。 - -Kiro CLI はエージェント定義の `tools` に挙がったツールだけをモデルへ見せる。欄が無いと -MCP サーバーへの接続は成功しても、MCP のツールを呼べない。 - -見るのは 3 つである。**新しく作る default.json に `tools: ["*"]` が入ること**・ -**既存の default.json に `tools` が無ければ足すこと**・**利用者が書いた `tools` は -上書きしないこと**。installer は `scripts/build-runtime-plugins.sh` が全 MCP プラグインへ -同じ内容で書き出すため、全プラグインの installer を通す。 -""" - -from __future__ import annotations - -import json -import os -import subprocess -from pathlib import Path - -import pytest - -ROOT = Path(__file__).resolve().parents[2] -INSTALLERS = sorted(ROOT.glob("plugins/mcp/mcp-*/dev.kiro/install.sh")) - - -def install(installer: Path, project: Path, home: Path) -> dict: - env = {**os.environ, "HOME": str(home)} - subprocess.run( - ["bash", str(installer), "--project", str(project)], - check=True, - capture_output=True, - text=True, - env=env, - ) - return json.loads((project / ".kiro/agents/default.json").read_text(encoding="utf-8")) - - -def test_installers_exist() -> None: - assert INSTALLERS - - -@pytest.mark.parametrize("installer", INSTALLERS, ids=lambda p: p.parents[1].name) -def test_new_agent_file_allows_all_tools(tmp_path: Path, installer: Path) -> None: - project = tmp_path / "project" - project.mkdir() - - agent = install(installer, project, tmp_path) - - assert agent["tools"] == ["*"] - - -def test_existing_agent_file_without_tools_gets_all_tools(tmp_path: Path) -> None: - project = tmp_path / "project" - agent_file = project / ".kiro/agents/default.json" - agent_file.parent.mkdir(parents=True) - agent_file.write_text(json.dumps({"name": "default", "mine": 1}), encoding="utf-8") - - agent = install(INSTALLERS[0], project, tmp_path) - - assert agent["tools"] == ["*"] - assert agent["mine"] == 1 - - -def test_user_tools_are_kept(tmp_path: Path) -> None: - project = tmp_path / "project" - agent_file = project / ".kiro/agents/default.json" - agent_file.parent.mkdir(parents=True) - agent_file.write_text(json.dumps({"name": "default", "tools": ["fs_read"]}), encoding="utf-8") - - agent = install(INSTALLERS[0], project, tmp_path) - - assert agent["tools"] == ["fs_read"] diff --git a/scripts/tests/test_refactoring_codistribution.py b/scripts/tests/test_refactoring_codistribution.py index dd06e10a1..03f538f70 100644 --- a/scripts/tests/test_refactoring_codistribution.py +++ b/scripts/tests/test_refactoring_codistribution.py @@ -18,7 +18,7 @@ def test_the_manifests_are_found() -> None: """配る基準そのものが見つかること。**空の一覧で通さない。**""" - assert len(MANIFESTS) >= 4, [p.name for p in MANIFESTS] + assert len(MANIFESTS) >= 2, [p.name for p in MANIFESTS] @pytest.mark.parametrize("manifest", MANIFESTS, ids=lambda p: p.stem) diff --git a/scripts/tests/test_runtime_smoke_latest.py b/scripts/tests/test_runtime_smoke_latest.py index 0dbaaeb90..243781bd0 100644 --- a/scripts/tests/test_runtime_smoke_latest.py +++ b/scripts/tests/test_runtime_smoke_latest.py @@ -15,8 +15,6 @@ REPO = Path(__file__).resolve().parents[2] SCRIPT = REPO / "scripts" / "runtime-smoke-latest.py" -SHA256 = {"x86_64": "a" * 64, "aarch64": "b" * 64} -SHA512 = {"linux_amd64": "c" * 128, "linux_arm64": "d" * 128} @pytest.fixture @@ -28,34 +26,10 @@ def mod(monkeypatch): return m -def sources(claude="2.1.300", codex="0.163.0", kiro="2.32.0", agy="1.3.5", agy_arm=None): - agy_arm = agy_arm or agy - - def kiro_pkg(arch, ext="tar.gz", sha=None): - return {"download": f"{kiro}/kirocli-{arch}-linux-musl.{ext}", "sha256": sha or SHA256[arch]} - +def sources(claude="2.1.300", codex="0.163.0"): return { "https://registry.npmjs.org/-/package/@anthropic-ai/claude-code/dist-tags": {"latest": claude, "next": "9.9.9"}, "https://registry.npmjs.org/-/package/@openai/codex/dist-tags": {"latest": codex, "alpha": "0.200.0-alpha.1"}, - "https://prod.download.cli.kiro.dev/stable/latest/manifest.json": { - "version": kiro, - "packages": [ - kiro_pkg("x86_64", "tar.xz", "e" * 64), - kiro_pkg("x86_64"), - kiro_pkg("aarch64"), - {"download": f"{kiro}/kirocli-x86_64-linux.tar.gz", "sha256": "f" * 64}, - ], - }, - "https://antigravity-cli-auto-updater-974169037036.us-central1.run.app/manifests/linux_amd64.json": { - "version": agy, - "url": f"https://example.invalid/antigravity-cli/{agy}-1/linux-x64/cli_linux_x64.tar.gz", - "sha512": SHA512["linux_amd64"], - }, - "https://antigravity-cli-auto-updater-974169037036.us-central1.run.app/manifests/linux_arm64.json": { - "version": agy_arm, - "url": f"https://example.invalid/antigravity-cli/{agy_arm}-1/linux-arm/cli_linux_arm64.tar.gz", - "sha512": SHA512["linux_arm64"], - }, } @@ -80,14 +54,6 @@ def test_resolve_all_emits_versions_list_shape(mod, monkeypatch, capsys): assert pairs == { "CLAUDE_CODE_VERSION": "2.1.300", "CODEX_VERSION": "0.163.0", - "KIRO_CLI_VERSION": "2.32.0", - "KIRO_CLI_SHA256_X86_64": SHA256["x86_64"], - "KIRO_CLI_SHA256_AARCH64": SHA256["aarch64"], - "AGY_VERSION": "1.3.5", - "AGY_URL_X86_64": "https://example.invalid/antigravity-cli/1.3.5-1/linux-x64/cli_linux_x64.tar.gz", - "AGY_URL_AARCH64": "https://example.invalid/antigravity-cli/1.3.5-1/linux-arm/cli_linux_arm64.tar.gz", - "AGY_SHA512_X86_64": SHA512["linux_amd64"], - "AGY_SHA512_AARCH64": SHA512["linux_arm64"], } # 一覧と同じキーの集合を出す(版を上げる手順で写せる) listed = REPO / "tests" / "runtime-smoke" / "cli-versions.env" @@ -105,11 +71,9 @@ def test_resolve_one_runtime_emits_only_its_keys(mod, monkeypatch, capsys): "runtime, table", [ ("claude", sources(claude="latest")), - ("kiro", {**sources(), "https://prod.download.cli.kiro.dev/stable/latest/manifest.json": {"version": "2.32.0", "packages": []}}), - ("agy", sources(agy_arm="1.3.6")), - ("agy", {k: v for k, v in sources().items() if "linux_arm64" not in k}), + ("codex", {k: v for k, v in sources().items() if "@openai/codex" not in k}), ], - ids=["bad-version", "kiro-no-musl", "agy-arch-mismatch", "unreachable"], + ids=["bad-version", "unreachable"], ) def test_resolve_fails_with_source_url(mod, monkeypatch, capsys, runtime, table): use_sources(mod, monkeypatch, table) @@ -146,25 +110,25 @@ def test_report_creates_issue_with_runtime_versions_and_run_url(mod, monkeypatch gh = FakeGh([{"number": 5, "title": "最新の版のスモークが落ちた: codex"}]) monkeypatch.setattr(mod, "run_gh", gh) versions = tmp_path / "v.env" - versions.write_text("AGY_VERSION=1.3.5\n", encoding="utf-8") - assert run(mod, "report", "--runtime", "agy", "--run-url", RUN_URL, "--versions", str(versions), "--repo", "o/r") == 0 + versions.write_text("CLAUDE_CODE_VERSION=2.1.300\n", encoding="utf-8") + assert run(mod, "report", "--runtime", "claude", "--run-url", RUN_URL, "--versions", str(versions), "--repo", "o/r") == 0 assert gh.kinds() == [["issue", "list"], ["issue", "create"]] args, body = gh.calls[1] - assert args[args.index("--title") + 1] == "最新の版のスモークが落ちた: agy" + assert args[args.index("--title") + 1] == "最新の版のスモークが落ちた: claude" assert args[args.index("--repo") + 1] == "o/r" - assert "agy" in body and "AGY_VERSION=1.3.5" in body and RUN_URL in body + assert "claude" in body and "CLAUDE_CODE_VERSION=2.1.300" in body and RUN_URL in body assert not any(word in body for word in ("Closes", "Fixes", "Resolves")) def test_report_comments_on_open_issue_instead_of_creating(mod, monkeypatch, tmp_path): gh = FakeGh( [ - {"number": 7, "title": "最新の版のスモークが落ちた: agy"}, - {"number": 8, "title": "最新の版のスモークが落ちた: agy (old)"}, + {"number": 7, "title": "最新の版のスモークが落ちた: codex"}, + {"number": 8, "title": "最新の版のスモークが落ちた: codex (old)"}, ] ) monkeypatch.setattr(mod, "run_gh", gh) - assert run(mod, "report", "--runtime", "agy", "--run-url", RUN_URL) == 0 + assert run(mod, "report", "--runtime", "codex", "--run-url", RUN_URL) == 0 assert gh.kinds() == [["issue", "list"], ["issue", "comment"]] args, body = gh.calls[1] assert args[2] == "7" @@ -173,5 +137,5 @@ def test_report_comments_on_open_issue_instead_of_creating(mod, monkeypatch, tmp def test_report_fails_when_gh_fails(mod, monkeypatch, capsys): monkeypatch.setattr(mod, "run_gh", FakeGh([], fail=True)) - assert run(mod, "report", "--runtime", "kiro", "--run-url", RUN_URL) == 1 + assert run(mod, "report", "--runtime", "codex", "--run-url", RUN_URL) == 1 assert "gh" in capsys.readouterr().err diff --git a/scripts/tests/test_runtime_smoke_versions.py b/scripts/tests/test_runtime_smoke_versions.py index 87c6874d6..f572a3d85 100644 --- a/scripts/tests/test_runtime_smoke_versions.py +++ b/scripts/tests/test_runtime_smoke_versions.py @@ -1,6 +1,6 @@ """スモークが入れる CLI の版の固定と、最新の版のスモークの workflow の形(#1982 の I2・I3・I5)。 -- I2: CLI の版の一覧(`tests/runtime-smoke/cli-versions.env`)のキーの集合が、4 つの Containerfile が +- I2: CLI の版の一覧(`tests/runtime-smoke/cli-versions.env`)のキーの集合が、2 つの Containerfile が 宣言する `ARG` の名前の和集合と一致し、各 `ARG` は接頭辞のランタイムの Containerfile にだけ現れ、 値は空でなく、`ARG` は既定値を持たない - I3: 配布の経路の取得先は版数を含む取得先だけ(`latest`・公式のインストーラを使わない) @@ -20,7 +20,7 @@ SMOKE = REPO / "tests" / "runtime-smoke" VERSIONS = SMOKE / "cli-versions.env" LATEST_WORKFLOW = REPO / ".github" / "workflows" / "runtime-plugin-smoke-latest.yml" -PREFIXES = {"claude": "CLAUDE_CODE_", "codex": "CODEX_", "kiro": "KIRO_CLI_", "agy": "AGY_"} +PREFIXES = {"claude": "CLAUDE_CODE_", "codex": "CODEX_"} ARG_RE = re.compile(r"^ARG\s+([A-Za-z_][A-Za-z0-9_]*)(=.*)?\s*$", re.MULTILINE) @@ -64,10 +64,8 @@ def test_versions_match_containerfile_args(): def test_versions_are_concrete(): versions = read_versions(VERSIONS) - for key in ("CLAUDE_CODE_VERSION", "CODEX_VERSION", "KIRO_CLI_VERSION", "AGY_VERSION"): + for key in ("CLAUDE_CODE_VERSION", "CODEX_VERSION"): assert re.fullmatch(r"[0-9]+(\.[0-9]+)+", versions[key]), f"{key} が版数でない: {versions[key]}" - for key in ("AGY_URL_X86_64", "AGY_URL_AARCH64"): - assert f"/{versions['AGY_VERSION']}-" in versions[key], f"{key} が AGY_VERSION の版を含まない" @pytest.mark.parametrize("runtime", sorted(PREFIXES)) diff --git a/scripts/tests/test_skill_frontmatter_listing.py b/scripts/tests/test_skill_frontmatter_listing.py index 76865bc73..60a12ddd5 100644 --- a/scripts/tests/test_skill_frontmatter_listing.py +++ b/scripts/tests/test_skill_frontmatter_listing.py @@ -1,7 +1,7 @@ """check-skill-frontmatter.py の measure_aggregate が数える初期一覧の文字数を固定する。 配布先ごとの数え方(Claude Code は description と when_to_use を連結して切り詰め、 -パスを載せない。Codex / Kiro / agy は description だけでパスを載せる)と、 +パスを載せない。Codex は description だけでパスを載せる)と、 manifest に無い Skill を数えないこと、frontmatter 総量を固定する。 """ @@ -37,7 +37,7 @@ def _skill(dir_name: str, block: str, **fm) -> dict: def test_counts_name_description_and_path_per_runtime(tmp_path): m = _load() - skills_dir = _tree(tmp_path, {"claude": ["alpha"], "codex": ["alpha"], "kiro": ["alpha"], "agy": ["alpha"]}) + skills_dir = _tree(tmp_path, {"claude": ["alpha"], "codex": ["alpha"]}) skills = [_skill("alpha", "x" * 10, name="alpha", description="desc", when_to_use="wtu")] rel = len("skills/alpha/SKILL.md") got = m.measure_aggregate(skills, skills_dir) @@ -45,8 +45,6 @@ def test_counts_name_description_and_path_per_runtime(tmp_path): "listings": { "claude": len("alpha") + len("descwtu"), "codex": len("alpha") + len("desc") + rel, - "kiro": len("alpha") + len("desc") + rel, - "agy": len("alpha") + len("desc") + rel, }, "frontmatter_total": 10, } @@ -83,7 +81,7 @@ def test_missing_frontmatter_falls_back_to_dir_name(tmp_path): def test_manifest_comments_are_ignored(tmp_path): m = _load() skills_dir = _tree(tmp_path, {}) - (skills_dir.parent / "manifests/kiro-skills.txt").write_text("# header\nalpha # note\n\n", encoding="utf-8") + (skills_dir.parent / "manifests/codex-skills.txt").write_text("# header\nalpha # note\n\n", encoding="utf-8") skills = [_skill("alpha", "", name="alpha", description="dd")] got = m.measure_aggregate(skills, skills_dir) - assert got["listings"] == {"kiro": len("alpha") + 2 + len("skills/alpha/SKILL.md")} + assert got["listings"] == {"codex": len("alpha") + 2 + len("skills/alpha/SKILL.md")} diff --git a/scripts/tests/test_validate_agy_manifest.py b/scripts/tests/test_validate_agy_manifest.py deleted file mode 100644 index 33965d8fc..000000000 --- a/scripts/tests/test_validate_agy_manifest.py +++ /dev/null @@ -1,98 +0,0 @@ -"""agy の定義ファイルをチェックの対象へ入れる(#215 の受け入れ条件 A9)。 - -版数を持つ箇所は 13 から 15 へ増える(`dev.agy/plugin.json` の `version` と `description`)。 -agy には取得元の登録が無く、`agy plugin list` も版数を出さないため、利用者が版を判断できる -手がかりは clone した中身の版数だけである。古いまま残らないよう、Claude 版 `plugin.json` を -基準に突き合わせる。 - -**記載を消してチェックを通せない形にする。** Skill 数を読み取れないこと自体も失敗として扱う。 -""" - -from __future__ import annotations - -import json -from pathlib import Path - -from validate_manifests_helpers import ( - MANIFESTS, - VERSION, - build_tree, - description, - output_of, - run_check, -) - -AGY_PLUGIN_JSON = "plugins/ndf/dev.agy/plugin.json" - - -def agy_manifest(root: Path) -> Path: - return root / AGY_PLUGIN_JSON - - -def test_matching_agy_manifest_passes(tmp_path: Path) -> None: - """版数と Skill 数が揃っていれば通る。""" - result = run_check(build_tree(tmp_path), tmp_path) - assert result.returncode == 0, output_of(result) - - -def test_old_version_fails(tmp_path: Path) -> None: - """`version` が Claude 版と食い違うと落ちる。""" - root = build_tree(tmp_path) - path = agy_manifest(root) - payload = json.loads(path.read_text(encoding="utf-8")) - payload["version"] = "9.2.0" - path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") - - result = run_check(root, tmp_path) - assert result.returncode == 1 - assert f"{AGY_PLUGIN_JSON} の version が claude 版と食い違う" in output_of(result) - - -def test_old_version_in_description_fails(tmp_path: Path) -> None: - """`description` の版数だけが古い場合も落ちる。""" - root = build_tree(tmp_path) - path = agy_manifest(root) - payload = json.loads(path.read_text(encoding="utf-8")) - payload["description"] = description("9.2.0", len(MANIFESTS["agy"])) - path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") - - result = run_check(root, tmp_path) - assert result.returncode == 1 - assert f"{AGY_PLUGIN_JSON} の description の版数が古い" in output_of(result) - - -def test_wrong_skill_count_fails(tmp_path: Path) -> None: - """`description` の Skill 数は `agy-skills.txt` の行数と突き合わせる。""" - root = build_tree(tmp_path) - path = agy_manifest(root) - payload = json.loads(path.read_text(encoding="utf-8")) - payload["description"] = description(VERSION, len(MANIFESTS["agy"]) + 1) - path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") - - result = run_check(root, tmp_path) - assert result.returncode == 1 - output = output_of(result) - assert f"{AGY_PLUGIN_JSON} の description の Skill 数が食い違う" in output - assert "agy-skills.txt" in output - - -def test_removed_skill_count_fails(tmp_path: Path) -> None: - """Skill 数の記載を消してもチェックは通らない。""" - root = build_tree(tmp_path) - path = agy_manifest(root) - payload = json.loads(path.read_text(encoding="utf-8")) - payload["description"] = f"Fixture plugin (v{VERSION}): nothing countable here." - path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") - - result = run_check(root, tmp_path) - assert result.returncode == 1 - assert f"{AGY_PLUGIN_JSON} の description から Skill 数を読み取れない" in output_of(result) - - -def test_family_without_dev_agy_is_skipped(tmp_path: Path) -> None: - """`dev.agy/` を持たない family は素通りする。agy へ配らないプラグインを巻き込まない。""" - root = build_tree(tmp_path) - agy_manifest(root).unlink() - - result = run_check(root, tmp_path) - assert result.returncode == 0, output_of(result) diff --git a/scripts/tests/test_validate_manifests_version.py b/scripts/tests/test_validate_manifests_version.py index f7b383082..72759ae54 100644 --- a/scripts/tests/test_validate_manifests_version.py +++ b/scripts/tests/test_validate_manifests_version.py @@ -3,16 +3,11 @@ 開発版の版数には接尾辞(`-dev.<連番>` / `-rc.<連番>`)が付く(`docs/versioning-and-distribution.md` の「版の付け方と開発版の配布」)。数字 3 つの直後に閉じ括弧を求める書式では `(v9.7.0-dev.1)` に一致せず、開発版を出すたびにこのチェックで止まっていた(#254)。 - -あわせて、agy の定義ファイル(`plugins/ndf/dev.agy/plugin.json`)の版数が -Claude 版と突き合わされることを固定する(#215 の受け入れ条件 A11)。 """ from __future__ import annotations -import json from pathlib import Path -from typing import Any, Callable import pytest @@ -50,7 +45,6 @@ def test_prerelease_version_dropped_in_description_fails(tmp_path: Path) -> None for label in ( "plugins/ndf/.claude-plugin/plugin.json", "plugins/ndf/.codex-plugin/plugin.json", - "plugins/ndf/dev.agy/plugin.json", ".claude-plugin/marketplace.json の ndf", ): assert f"{label} の description の版数が古い" in output, output @@ -96,95 +90,3 @@ def test_skill_count_is_read_beside_the_version(tmp_path: Path, version: str) -> "plugins/ndf/.claude-plugin/plugin.json の description の Skill 数が食い違う" "(description: 4 / claude-skills.txt: 5)" in output_of(result) ) - - -# --- agy の定義ファイルの版数(#215 の受け入れ条件 A11) --- -# -# `plugins/ndf/dev.agy/plugin.json` は `version` と `description` の 2 箇所に版数を持つ。 -# どちらも Claude 版の `plugin.json` と突き合わせる。**片方だけ直した状態で配れないこと**を、 -# 古い値と記載の欠落の両方で確かめる。 - -AGY_MANIFEST = "plugins/ndf/dev.agy/plugin.json" - - -def rewrite_agy_manifest(root: Path, mutate: Callable[[dict[str, Any]], None]) -> None: - """agy の定義ファイルだけを崩す。他のランタイムの定義には触らない。""" - path = root / AGY_MANIFEST - manifest = json.loads(path.read_text(encoding="utf-8")) - mutate(manifest) - path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") - - -def test_agy_consistent_manifest_passes(tmp_path: Path) -> None: - """崩していない木では、agy の定義ファイルも含めて通る。""" - root = build_tree(tmp_path) - assert (root / AGY_MANIFEST).is_file() - result = run_check(root, tmp_path) - assert result.returncode == 0, output_of(result) - - -def test_agy_stale_version_fails(tmp_path: Path) -> None: - """`version` が前の版のままなら落ちる。""" - root = build_tree(tmp_path) - rewrite_agy_manifest(root, lambda m: m.update(version="9.2.1")) - - result = run_check(root, tmp_path) - - assert result.returncode == 1 - out = output_of(result) - assert f"{AGY_MANIFEST} の version が claude 版と食い違う" in out, out - assert "9.2.1" in out and helpers.VERSION in out, out - - -def test_agy_missing_version_fails(tmp_path: Path) -> None: - """`version` の記載を消しても通らない。読み取れないこと自体を失敗として扱う。""" - root = build_tree(tmp_path) - rewrite_agy_manifest(root, lambda m: m.pop("version")) - - result = run_check(root, tmp_path) - - assert result.returncode == 1 - assert f"{AGY_MANIFEST} の version が claude 版と食い違う" in output_of(result) - - -def test_agy_stale_version_in_description_fails(tmp_path: Path) -> None: - """`description` の版数だけが前の版でも落ちる。""" - root = build_tree(tmp_path) - rewrite_agy_manifest( - root, - lambda m: m.update(description=m["description"].replace(f"(v{helpers.VERSION})", "(v9.2.1)")), - ) - - result = run_check(root, tmp_path) - - assert result.returncode == 1 - out = output_of(result) - assert f"{AGY_MANIFEST} の description の版数が古い" in out, out - - -def test_agy_missing_version_in_description_fails(tmp_path: Path) -> None: - """`description` から版数を消しても通らない。""" - root = build_tree(tmp_path) - rewrite_agy_manifest( - root, - lambda m: m.update(description=m["description"].replace(f"(v{helpers.VERSION})", "")), - ) - - result = run_check(root, tmp_path) - - assert result.returncode == 1 - assert f"{AGY_MANIFEST} の description に `(vX.Y.Z)` 形式の版数がない" in output_of(result) - - -def test_agy_skill_count_is_read_from_its_own_manifest(tmp_path: Path) -> None: - """Skill 数の突き合わせ先は `agy-skills.txt` である。他のランタイムの数と取り違えない。""" - root = build_tree(tmp_path) - rewrite_agy_manifest( - root, - lambda m: m.update(description=m["description"].replace("2 focused skills", "3 focused skills")), - ) - - result = run_check(root, tmp_path) - - assert result.returncode == 1 - assert f"{AGY_MANIFEST} の description の Skill 数が食い違う(description: 3 / agy-skills.txt: 2)" in output_of(result) diff --git a/scripts/tests/test_validate_mcp_codex_servers.py b/scripts/tests/test_validate_mcp_codex_servers.py index f1cb6c04d..f15c5ff35 100644 --- a/scripts/tests/test_validate_mcp_codex_servers.py +++ b/scripts/tests/test_validate_mcp_codex_servers.py @@ -16,13 +16,12 @@ def _mcp_plugin(root: Path, declared, files=(".mcp.json",)) -> None: plugin = root / "plugins/mcp/mcp-x" - for rel in (".claude-plugin", ".codex-plugin", "dev.kiro"): + for rel in (".claude-plugin", ".codex-plugin"): (plugin / rel).mkdir(parents=True) for name in files: (plugin / name).write_text('{"mcpServers": {}}\n', encoding="utf-8") (plugin / ".claude-plugin/plugin.json").write_text('{"name": "mcp-x"}\n', encoding="utf-8") (plugin / ".codex-plugin/plugin.json").write_text(json.dumps({"name": "mcp-x", "mcpServers": declared}), encoding="utf-8") - (plugin / "dev.kiro/install.sh").write_text("#!/bin/sh\n", encoding="utf-8") @pytest.mark.parametrize( diff --git a/scripts/tests/validate_manifests_helpers.py b/scripts/tests/validate_manifests_helpers.py index 5181936ff..5ee312ff0 100644 --- a/scripts/tests/validate_manifests_helpers.py +++ b/scripts/tests/validate_manifests_helpers.py @@ -29,7 +29,6 @@ MANIFESTS = { "claude": ["alpha", "bravo", "charlie", "delta", "echo"], "codex": ["alpha", "bravo", "charlie"], - "agy": ["alpha", "bravo"], } @@ -79,23 +78,6 @@ def build_tree(base: Path, version: str = VERSION, described: str | None = None) encoding="utf-8", ) - # agy 向けの定義はクライアント拡張ディレクトリに置く。Skill の絞り込みを持たないため、 - # 突き合わせ先は agy-skills.txt の行数である。 - (ndf / "dev.agy").mkdir(parents=True) - (ndf / "dev.agy/plugin.json").write_text( - json.dumps( - { - "name": FAMILY, - "version": version, - "description": description(described, len(MANIFESTS["agy"])), - }, - ensure_ascii=False, - indent=2, - ) - + "\n", - encoding="utf-8", - ) - (root / ".claude-plugin").mkdir(parents=True) (root / ".claude-plugin/marketplace.json").write_text( json.dumps( diff --git a/scripts/validate-runtime-plugins.sh b/scripts/validate-runtime-plugins.sh index 74b5ff6de..e852f77de 100755 --- a/scripts/validate-runtime-plugins.sh +++ b/scripts/validate-runtime-plugins.sh @@ -12,7 +12,7 @@ run bash "$ROOT_DIR/scripts/build-runtime-plugins.sh" --check # Skill を配る plugin family を manifests/ の有無から検出する(plugins/mcp/* は別系統)。 # 固定リストを置かないのは、family を足したときにチェック対象から漏れる経路を作らないためである。 -# 後段の静的解析(claude plugin validate / Kiro installer の dry-run)もこの一覧で回す。 +# 後段の静的解析(claude plugin validate)もこの一覧で回す。 FAMILIES=() for plugin_dir in "$ROOT_DIR"/plugins/*; do [ -d "$plugin_dir/manifests" ] || continue @@ -36,14 +36,6 @@ for family in "${FAMILIES[@]}"; do run python3 -m json.tool "$root_manifest" >/dev/null done -for family in "${FAMILIES[@]}"; do - for agy_manifest in "$ROOT_DIR/plugins/$family/dev.agy/plugin.json" \ - "$ROOT_DIR/plugins/$family/dev.agy/hooks.json"; do - [ -f "$agy_manifest" ] || continue - run python3 -m json.tool "$agy_manifest" >/dev/null - done -done - while IFS= read -r mcp_config; do run python3 -m json.tool "$mcp_config" >/dev/null done < <(find "$ROOT_DIR/plugins/mcp" -maxdepth 2 -name .mcp.json | sort) @@ -60,32 +52,6 @@ else echo "==> claude CLI not found; skipped claude plugin validate" fi -# agy はクライアント拡張ディレクトリを 1 つのプラグインとして読む。CLI が無い環境では -# 読み飛ばす(claude plugin validate と同じ扱い)。 -if command -v agy >/dev/null 2>&1; then - for family in "${FAMILIES[@]}"; do - [ -f "$ROOT_DIR/plugins/$family/dev.agy/plugin.json" ] || continue - run agy plugin validate "$ROOT_DIR/plugins/$family/dev.agy" - done -else - echo "==> agy CLI not found; skipped agy plugin validate" -fi - -# Kiro の installer は Agent Plugins 仕様 §8.2 のクライアント拡張ディレクトリに置く。 -for family in "${FAMILIES[@]}"; do - installer="$ROOT_DIR/plugins/$family/dev.kiro/install.sh" - [ -f "$installer" ] || continue - run bash "$installer" --dry-run >/dev/null -done - -# --with-codex を持つのは NDF の installer だけ(Codex 向け Skill も併せて配置する経路)。 -# family 共通の引数ではないため、ここだけは対象を明示してチェックする。 -run bash "$ROOT_DIR/plugins/ndf/dev.kiro/install.sh" --dry-run --with-codex >/dev/null - -while IFS= read -r installer; do - run bash "$installer" --dry-run >/dev/null -done < <(find "$ROOT_DIR/plugins/mcp" -path '*/dev.kiro/install.sh' | sort) - run python3 "$ROOT_DIR/scripts/check-markdown-links.py" --root "$ROOT_DIR" # 説明文書(README.md / plugins/ndf/README.md)に書かれた Skill 数と版数を、 diff --git a/tests/runtime-smoke/Containerfile.agy b/tests/runtime-smoke/Containerfile.agy deleted file mode 100644 index f383e4deb..000000000 --- a/tests/runtime-smoke/Containerfile.agy +++ /dev/null @@ -1,30 +0,0 @@ -FROM ai-plugins-runtime-smoke-base - -# 版・取得先・checksum は tests/runtime-smoke/cli-versions.env に書き、scripts/runtime-smoke-test.sh が -# build-arg で渡す。ARG に既定値を持たせない(版の出所を一覧の 1 つにする)。 -# 公式のインストーラは最新の manifest だけを読むため使わず、manifest に載る版つきの url を取り、 -# 一覧の sha512 で確かめて入れる。取得・checksum・版の照合のどれかが落ちたら像の作成を落とす。 -ARG AGY_VERSION -ARG AGY_URL_X86_64 -ARG AGY_URL_AARCH64 -ARG AGY_SHA512_X86_64 -ARG AGY_SHA512_AARCH64 - -RUN set -eu; \ - test -n "$AGY_VERSION"; \ - arch="$(uname -m)"; \ - case "$arch" in \ - x86_64) url="$AGY_URL_X86_64"; sha512="$AGY_SHA512_X86_64" ;; \ - aarch64) url="$AGY_URL_AARCH64"; sha512="$AGY_SHA512_AARCH64" ;; \ - *) echo "対応していない arch: $arch" >&2; exit 1 ;; \ - esac; \ - test -n "$url"; \ - test -n "$sha512"; \ - work="$(mktemp -d)"; \ - curl -fsSL -o "$work/agy.tar.gz" "$url"; \ - echo "$sha512 $work/agy.tar.gz" | sha512sum -c -; \ - tar -xzf "$work/agy.tar.gz" -C "$work" antigravity; \ - install -m 755 "$work/antigravity" /usr/local/bin/agy; \ - rm -rf "$work"; \ - actual="$(agy --version | grep -oE '[0-9]+(\.[0-9]+)+' | head -n 1)"; \ - [ "$actual" = "$AGY_VERSION" ] || { echo "agy の版が一覧と違う: $actual != $AGY_VERSION" >&2; exit 1; } diff --git a/tests/runtime-smoke/Containerfile.kiro b/tests/runtime-smoke/Containerfile.kiro deleted file mode 100644 index 27f4aea5f..000000000 --- a/tests/runtime-smoke/Containerfile.kiro +++ /dev/null @@ -1,28 +0,0 @@ -FROM ai-plugins-runtime-smoke-base - -# 版と checksum は tests/runtime-smoke/cli-versions.env に書き、scripts/runtime-smoke-test.sh が -# build-arg で渡す。ARG に既定値を持たせない(版の出所を一覧の 1 つにする)。 -# 公式のインストーラは版を受けず、bookworm の glibc では glibc 版が動かないため、版つきの musl 版の -# tarball を取り、一覧の sha256 で確かめて入れる。取得・checksum・版の照合のどれかが落ちたら像の作成を落とす。 -ARG KIRO_CLI_VERSION -ARG KIRO_CLI_SHA256_X86_64 -ARG KIRO_CLI_SHA256_AARCH64 - -RUN set -eu; \ - test -n "$KIRO_CLI_VERSION"; \ - arch="$(uname -m)"; \ - case "$arch" in \ - x86_64) sha256="$KIRO_CLI_SHA256_X86_64" ;; \ - aarch64) sha256="$KIRO_CLI_SHA256_AARCH64" ;; \ - *) echo "対応していない arch: $arch" >&2; exit 1 ;; \ - esac; \ - test -n "$sha256"; \ - work="$(mktemp -d)"; \ - curl -fsSL -o "$work/kirocli.tar.gz" \ - "https://prod.download.cli.kiro.dev/stable/${KIRO_CLI_VERSION}/kirocli-${arch}-linux-musl.tar.gz"; \ - echo "$sha256 $work/kirocli.tar.gz" | sha256sum -c -; \ - tar -xzf "$work/kirocli.tar.gz" -C "$work"; \ - install -m 755 "$work/kirocli/bin/kiro-cli" "$work/kirocli/bin/kiro-cli-chat" "$work/kirocli/bin/kiro-cli-term" /usr/local/bin/; \ - rm -rf "$work"; \ - actual="$(kiro-cli --version | grep -oE '[0-9]+(\.[0-9]+)+' | head -n 1)"; \ - [ "$actual" = "$KIRO_CLI_VERSION" ] || { echo "kiro-cli の版が一覧と違う: $actual != $KIRO_CLI_VERSION" >&2; exit 1; } diff --git a/tests/runtime-smoke/README.md b/tests/runtime-smoke/README.md index f4ddbd985..11644129f 100644 --- a/tests/runtime-smoke/README.md +++ b/tests/runtime-smoke/README.md @@ -1,13 +1,11 @@ # Runtime Smoke Tests -`scripts/runtime-smoke-test.sh` runs Claude, Codex, Kiro, and agy plugin smoke tests in disposable Docker containers. The host home directory and credential directories are not mounted. +`scripts/runtime-smoke-test.sh` runs Claude and Codex plugin smoke tests in disposable Docker containers. The host home directory and credential directories are not mounted. ```bash bash scripts/runtime-smoke-test.sh bash scripts/runtime-smoke-test.sh --runtime claude bash scripts/runtime-smoke-test.sh --runtime codex -bash scripts/runtime-smoke-test.sh --runtime kiro -bash scripts/runtime-smoke-test.sh --runtime agy ``` Artifacts are written to `tmp/runtime-smoke//` by default: @@ -37,16 +35,14 @@ The authenticated GitHub workflow passes only `ANTHROPIC_API_KEY` and `OPENAI_AP ## CLI の版 -像は 4 ランタイムの CLI を、CLI の版の一覧 `tests/runtime-smoke/cli-versions.env` に書いた版で入れる。版を上げるときに書き換えるのはこのファイルだけで、Containerfile は版を持たない(`ARG` に既定値を置かない)。 +像は 2 ランタイムの CLI を、CLI の版の一覧 `tests/runtime-smoke/cli-versions.env` に書いた版で入れる。版を上げるときに書き換えるのはこのファイルだけで、Containerfile は版を持たない(`ARG` に既定値を置かない)。 | ランタイム | 一覧のキー | 取得先 | | --- | --- | --- | | claude | `CLAUDE_CODE_VERSION` | npm の `@anthropic-ai/claude-code@<版>` | | codex | `CODEX_VERSION` | npm の `@openai/codex@<版>` | -| kiro | `KIRO_CLI_VERSION`・`KIRO_CLI_SHA256_` | `https://prod.download.cli.kiro.dev/stable/<版>/kirocli--linux-musl.tar.gz`(sha256 で確かめる) | -| agy | `AGY_VERSION`・`AGY_URL_`・`AGY_SHA512_` | 一覧の url(sha512 で確かめる) | -`` は `uname -m` の値を大文字にしたもの(`X86_64`・`AARCH64`)。`scripts/runtime-smoke-test.sh` は一覧の各行を `--build-arg` で像の作成へ渡し、`--cli-versions <パス>` で別の一覧を受ける。像の作成は、取得・checksum・`--version` の出力から取り出した版数と一覧の版数の完全一致のどれかが落ちたら失敗する。 +`scripts/runtime-smoke-test.sh` は一覧の各行を `--build-arg` で像の作成へ渡し、`--cli-versions <パス>` で別の一覧を受ける。像の作成は、取得・`--version` の出力から取り出した版数と一覧の版数の完全一致のどれかが落ちたら失敗する。 配布の経路のスモーク(`runtime-plugin-smoke.yml` の `runtime-smoke (<ランタイム>)`)と認証つきのスモークは、一覧の版だけを入れる。その時点の最新の版は、別の workflow `runtime-plugin-smoke-latest.yml` が毎日 06:00 JST に develop の上で試す(Actions の画面から手でも起動できる。PR と push では走らず、必須のチェックに入らない)。落ちたら、題が `最新の版のスモークが落ちた: <ランタイム>` の課題をランタイムごとに 1 件作り、開いていればコメントを足す。課題には入れた版と実行の URL が載る。 @@ -55,10 +51,10 @@ The authenticated GitHub workflow passes only `ANTHROPIC_API_KEY` and `OPENAI_AP 1. 最新の版を一覧の形で出す(失敗の課題に載った版を使ってもよい) ```bash - python3 scripts/runtime-smoke-latest.py resolve --runtime + python3 scripts/runtime-smoke-latest.py resolve --runtime ``` -2. 出力の行で、`tests/runtime-smoke/cli-versions.env` の同じキーの行を置き換える(kiro と agy は checksum と url の行も) +2. 出力の行で、`tests/runtime-smoke/cli-versions.env` の同じキーの行を置き換える 3. 手元でスモークを走らせ、`tmp/runtime-smoke/<ランタイム>/version.log` の版が一覧の版と同じことを見る ```bash diff --git a/tests/runtime-smoke/adapters/agy.sh b/tests/runtime-smoke/adapters/agy.sh deleted file mode 100755 index a17de80f7..000000000 --- a/tests/runtime-smoke/adapters/agy.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -source /workspace/ai-plugins/tests/runtime-smoke/lib/common.sh - -cd "$PROJECT_DIR" -record_version agy --version - -run_step "agy validate ndf" agy plugin validate "$REPO_ROOT/plugins/ndf/dev.agy" -run_step "agy install ndf" agy plugin install "$REPO_ROOT/plugins/ndf/dev.agy" -# 2 回目で壊れないことを見る。agy には入れ替えの操作が無く、利用者は install を繰り返す。 -run_step "agy install ndf idempotent" agy plugin install "$REPO_ROOT/plugins/ndf/dev.agy" -run_step "agy plugin list" agy plugin list - -"$REPO_ROOT/tests/runtime-smoke/assertions/assert-plugin-files.sh" agy -"$REPO_ROOT/tests/runtime-smoke/assertions/assert-authenticated-smoke.sh" agy -"$REPO_ROOT/tests/runtime-smoke/assertions/assert-no-host-contamination.sh" agy -write_junit agy diff --git a/tests/runtime-smoke/adapters/kiro.sh b/tests/runtime-smoke/adapters/kiro.sh deleted file mode 100755 index 88f7ce516..000000000 --- a/tests/runtime-smoke/adapters/kiro.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -source /workspace/ai-plugins/tests/runtime-smoke/lib/common.sh - -cd "$PROJECT_DIR" -record_version kiro-cli --version - -run_step "kiro install ndf" bash "$REPO_ROOT/plugins/ndf/dev.kiro/install.sh" --project "$PROJECT_DIR" --with-slack -run_step "kiro install ndf idempotent" bash "$REPO_ROOT/plugins/ndf/dev.kiro/install.sh" --project "$PROJECT_DIR" --with-slack -run_step "kiro install playwright-kit" bash "$REPO_ROOT/plugins/playwright-kit/dev.kiro/install.sh" --project "$PROJECT_DIR" -run_step "kiro install playwright-kit idempotent" bash "$REPO_ROOT/plugins/playwright-kit/dev.kiro/install.sh" --project "$PROJECT_DIR" -run_step "kiro install mcp-bigquery" bash "$REPO_ROOT/plugins/mcp/mcp-bigquery/dev.kiro/install.sh" --project "$PROJECT_DIR" -run_step "kiro install mcp-bigquery idempotent" bash "$REPO_ROOT/plugins/mcp/mcp-bigquery/dev.kiro/install.sh" --project "$PROJECT_DIR" - -"$REPO_ROOT/tests/runtime-smoke/assertions/assert-plugin-files.sh" kiro -"$REPO_ROOT/tests/runtime-smoke/assertions/assert-mcp-config.sh" kiro "$PROJECT_DIR/.mcp.json" -"$REPO_ROOT/tests/runtime-smoke/assertions/assert-hook-fixtures.sh" kiro -"$REPO_ROOT/tests/runtime-smoke/assertions/assert-kiro-agent.sh" workspace - -run_step "kiro install ndf global" bash "$REPO_ROOT/plugins/ndf/dev.kiro/install.sh" --scope global --with-slack -"$REPO_ROOT/tests/runtime-smoke/assertions/assert-kiro-agent.sh" global -"$REPO_ROOT/tests/runtime-smoke/assertions/assert-authenticated-smoke.sh" kiro -"$REPO_ROOT/tests/runtime-smoke/assertions/assert-no-host-contamination.sh" kiro -write_junit kiro diff --git a/tests/runtime-smoke/assertions/assert-authenticated-smoke.sh b/tests/runtime-smoke/assertions/assert-authenticated-smoke.sh index cb76944d3..f7e2a1405 100755 --- a/tests/runtime-smoke/assertions/assert-authenticated-smoke.sh +++ b/tests/runtime-smoke/assertions/assert-authenticated-smoke.sh @@ -66,24 +66,6 @@ case "$runtime" in fi run_bigquery_secret_check ;; - kiro) - if command -v kiro-cli >/dev/null 2>&1 && [ -n "${ANTHROPIC_API_KEY:-}${OPENAI_API_KEY:-}" ]; then - timeout 30s kiro-cli doctor >> "$log" 2>&1 || true - auth_ran=true - fi - run_bigquery_secret_check - ;; - agy) - # agy の認証は CLI が持つ。API キーの環境変数を取らないため、`agy models` の成否で - # 認証済みかを判定する。Skill 一覧の取得は認証を要するので、ここへ置く。 - if command -v agy >/dev/null 2>&1 && timeout 60s agy models >> "$log" 2>&1; then - timeout 120s agy --output-format text --dangerously-skip-permissions \ - -p="読み込めている Skill の名前を、1 行に 1 つだけ列挙して。説明は書かないで。" \ - >> "$log" 2>&1 || true - auth_ran=true - fi - run_bigquery_secret_check - ;; *) echo "unknown runtime: $runtime" >&2; exit 2 ;; esac diff --git a/tests/runtime-smoke/assertions/assert-hook-fixtures.sh b/tests/runtime-smoke/assertions/assert-hook-fixtures.sh index c45931069..3dda52be8 100755 --- a/tests/runtime-smoke/assertions/assert-hook-fixtures.sh +++ b/tests/runtime-smoke/assertions/assert-hook-fixtures.sh @@ -17,24 +17,5 @@ case "$runtime" in PLUGIN_ROOT="$REPO_ROOT/plugins/ndf" \ python3 "$REPO_ROOT/plugins/ndf/scripts/wait-notify.py" --runtime codex < "$REPO_ROOT/tests/runtime-smoke/fixtures/hook-stop.json" >/dev/null ;; - kiro) - test -f "$PROJECT_DIR/.kiro/agents/ndf.json" - jq -e '.hooks.agentSpawn[0].command' "$PROJECT_DIR/.kiro/agents/ndf.json" >/dev/null - stop_command="$(jq -r '.hooks.stop[0].command // empty' "$PROJECT_DIR/.kiro/agents/ndf.json")" - test -n "$stop_command" - stop_script="$(python3 - "$stop_command" <<'PY' -import shlex -import sys - -parts = shlex.split(sys.argv[1]) -if len(parts) < 4 or parts[0] != "python3" or parts[2:4] != ["--runtime", "kiro"]: - raise SystemExit(1) -print(parts[1]) -PY -)" - test -f "$stop_script" - PLUGIN_ROOT="$REPO_ROOT/plugins/ndf" \ - python3 "$stop_script" --runtime kiro < "$REPO_ROOT/tests/runtime-smoke/fixtures/hook-stop.json" >/dev/null - ;; *) echo "unknown runtime: $runtime" >&2; exit 2 ;; esac diff --git a/tests/runtime-smoke/assertions/assert-kiro-agent.sh b/tests/runtime-smoke/assertions/assert-kiro-agent.sh deleted file mode 100755 index 3a4c8e7de..000000000 --- a/tests/runtime-smoke/assertions/assert-kiro-agent.sh +++ /dev/null @@ -1,390 +0,0 @@ -#!/usr/bin/env bash -# Kiro の ndf エージェントが「選択できる状態」で導入されたことをチェックする。 -# 旧実装は .kiro/agents/default.json を生成していたが、Kiro の既定は組み込みの -# kiro_default であり、生成したエージェントは選択されないままだった。 -# -# Usage: assert-kiro-agent.sh [workspace|global] -set -euo pipefail - -scope="${1:-workspace}" -: "${REPO_ROOT:=/workspace/ai-plugins}" -: "${PROJECT_DIR:=/tmp/runtime-project}" -: "${ARTIFACT_DIR:=/tmp/runtime-artifacts}" -: "${HOME:=/tmp/runtime-home}" - -AGENT_NAME="ndf" -# INSTALL_ARGS: この scope の導入先を再現する installer 引数 -case "$scope" in - workspace) - KIRO_DIR="$PROJECT_DIR/.kiro" - INSTALL_ARGS=(--project "$PROJECT_DIR") - ;; - global) - KIRO_DIR="$HOME/.kiro" - INSTALL_ARGS=(--scope global) - ;; - *) echo "unknown scope: $scope" >&2; exit 2 ;; -esac - -AGENT_FILE="$KIRO_DIR/agents/$AGENT_NAME.json" -STEERING_FILE="$KIRO_DIR/steering/ndf-policies.md" -LOG="$ARTIFACT_DIR/kiro-agent-$scope.log" - -mkdir -p "$ARTIFACT_DIR" -: > "$LOG" - -test -f "$AGENT_FILE" -test -s "$STEERING_FILE" -# `| grep -q .` は使わない(SIGPIPE で 141 になる。assert-plugin-files.sh の注記を参照) -hit="$(find -L "$KIRO_DIR/skills" -path '*/SKILL.md' -print -quit)" -[ -n "$hit" ] || { echo "not found: SKILL.md under $KIRO_DIR/skills" >&2; exit 1; } - -# Kiro には plugin.json を読む仕組みが無いため、版数は installer が埋める -# エージェント description でしか確認できない。installer が読む Claude 版 -# マニフェストの版数と一致することをチェックする。 -MANIFEST_FILE="$REPO_ROOT/plugins/ndf/.claude-plugin/plugin.json" -test -s "$MANIFEST_FILE" -ndf_version="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$MANIFEST_FILE")" -# grep だと 5.0.0 の . が任意文字に一致するため、JSON を読んで厳密に照合する。 -python3 - "$AGENT_FILE" "$ndf_version" >> "$LOG" <<'PY' -import json -import sys -from pathlib import Path - -agent_file, version = sys.argv[1:3] -description = json.loads(Path(agent_file).read_text(encoding="utf-8")).get("description", "") -expected = f"NDF統合開発エージェント(Kiro CLI用 / v{version})" -if description != expected: - raise SystemExit( - f"installed agent description must be {expected!r}, got {description!r}" - ) -print(f"kiro version surfaced: v{version}") -PY - -# ndf-policies は steering として配置する。Skill としても置くと Kiro 組み込みルールの -# Skill 読み込みと steering 読み込みで文脈へ二重注入される。 -if [ -e "$KIRO_DIR/skills/ndf-policies" ]; then - echo "ndf-policies must be delivered via steering only: $KIRO_DIR/skills/ndf-policies" >&2 - exit 1 -fi - -# エージェント定義と、起動時に読み込まれる文脈量をチェックする。 -# -# **文脈量に上限は置かない。計測して記録するだけにする。** -# 以前は 200,000 文字で落としていた。根拠は 2026-08-08 / kiro-cli 2.16.1 でのこの -# フィクスチャの実測 112,404 文字に対する余裕分で、当時のモデルのコンテキスト長を -# 前提にしていた。NDF は Kiro でも 1M コンテキストのモデルだけを対象とするようになり -# (既定 `auto` のコンテキストは 1,000,000。過去の実測の記録で、kiro-cli 2.20.1 に -# `--list-models` は無く同じ手段では再検証できない。出典は -# `scripts/check-skill-frontmatter.py` の Kiro の節にまとめてある)、 -# これは Claude Code の Opus 5 と同じである。**その Claude Code には、起動時に読み込む -# 文脈量の上限が無い。** 同じコンテキスト長のランタイムを片方だけ縛る根拠が無いため、 -# 上限を外して Claude Code へ揃える。 -# -# 数は `$LOG` へ残るため、増え方は後から追える。一覧の予算のほうは -# `scripts/check-skill-frontmatter.py` が Claude Code と同じ 1% でチェックする。 -python3 - "$AGENT_FILE" "$KIRO_DIR" "$PROJECT_DIR" "$STEERING_FILE" >> "$LOG" <<'PY' -import json -import sys -from pathlib import Path - -agent_file, kiro_dir, project_dir, steering_file = sys.argv[1:5] -config = json.loads(Path(agent_file).read_text(encoding="utf-8")) - -if config.get("name") != "ndf": - raise SystemExit(f"agent name must be 'ndf': {config.get('name')!r}") -if not config.get("tools"): - raise SystemExit(f"agent config declares no tools: {agent_file}") - -resources = config.get("resources", []) -for entry in resources: - if entry.startswith("skill://"): - raise SystemExit(f"skill:// resource duplicates the built-in rule: {entry}") - if "ndf-policies" in entry: - raise SystemExit(f"ndf-policies must be delivered via steering, not resources: {entry}") -if len(resources) != len(set(resources)): - raise SystemExit(f"duplicated resources in {agent_file}") - -context_files = sorted(Path(kiro_dir).glob("skills/*/SKILL.md")) -project = Path(project_dir) -context_files += [p for p in (project / "AGENTS.md", project / "README.md") if p.is_file()] -context_files.append(Path(steering_file)) -total = sum(len(p.read_text(encoding="utf-8")) for p in context_files) -# 上限は置かない(上のコメントを参照)。増え方を追えるように記録だけ残す。 -print(f"context files: {len(context_files)} / chars: {total}") -PY - -# 旧 installer が別 checkout から張った .kiro/skills/ndf-policies symlink は、現在の -# プラグイン配下を指さないため「自分が張ったリンクだけ消す」掃除に掛からない。旧導入済み -# プロジェクトでも steering との二重注入が解消されることをチェックする。 -STALE_ROOT="$ARTIFACT_DIR/stale-checkout-$scope/skills/ndf-policies" -mkdir -p "$STALE_ROOT" -echo "stale" > "$STALE_ROOT/SKILL.md" -ln -sfn "$STALE_ROOT" "$KIRO_DIR/skills/ndf-policies" -bash "$REPO_ROOT/plugins/ndf/dev.kiro/install.sh" "${INSTALL_ARGS[@]}" --with-slack >> "$LOG" 2>&1 -if [ -e "$KIRO_DIR/skills/ndf-policies" ] || [ -L "$KIRO_DIR/skills/ndf-policies" ]; then - echo "installer left a stale ndf-policies skill link: $KIRO_DIR/skills/ndf-policies" >&2 - exit 1 -fi -test -f "$STALE_ROOT/SKILL.md" # リンク先の実体まで消していないこと -echo "installer removed a stale ndf-policies skill link" >> "$LOG" - -# --- workspace 限定のチェック(ここから fi まで。heredoc の終端子の都合でインデントしない) --- -if [ "$scope" = workspace ]; then -# 利用者が $AGENT_FILE へ写した設定(MCP プラグインの mcpServers など)が、 -# installer の再実行で失われないことをチェックする。kiro-cli には依存しない。 -python3 - "$AGENT_FILE" <<'PY' -import json -from pathlib import Path -import sys - -path = Path(sys.argv[1]) -config = json.loads(path.read_text(encoding="utf-8")) -config.setdefault("mcpServers", {})["smoke-user-mcp"] = { - "command": "echo", - "args": ["user-managed"], -} -config.setdefault("hooks", {})["userPromptSubmit"] = [{"command": "true"}] -config["smokeUserKey"] = "keep-me" -path.write_text(json.dumps(config, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") -PY -bash "$REPO_ROOT/plugins/ndf/dev.kiro/install.sh" --project "$PROJECT_DIR" --with-slack >> "$LOG" 2>&1 -python3 - "$AGENT_FILE" <<'PY' -import json -from pathlib import Path -import sys - -path = Path(sys.argv[1]) -config = json.loads(path.read_text(encoding="utf-8")) -if "smoke-user-mcp" not in config.get("mcpServers", {}): - raise SystemExit(f"reinstall dropped a user-managed mcpServers entry: {path}") -if "userPromptSubmit" not in config.get("hooks", {}): - raise SystemExit(f"reinstall dropped a user-managed hook: {path}") -if config.get("smokeUserKey") != "keep-me": - raise SystemExit(f"reinstall dropped a user-managed key: {path}") -# installer 管理のキーはテンプレートから再生成されている -if config.get("name") != "ndf" or not config.get("hooks", {}).get("agentSpawn"): - raise SystemExit(f"reinstall did not regenerate installer-managed keys: {path}") -if not config.get("hooks", {}).get("stop"): - raise SystemExit(f"--with-slack did not regenerate the stop hook: {path}") -PY -# チェック用に注入した設定を取り除き、以降のチェックへ持ち越さない -python3 - "$AGENT_FILE" <<'PY' -import json -from pathlib import Path -import sys - -path = Path(sys.argv[1]) -config = json.loads(path.read_text(encoding="utf-8")) -config.get("mcpServers", {}).pop("smoke-user-mcp", None) -if not config.get("mcpServers"): - config.pop("mcpServers", None) -config.get("hooks", {}).pop("userPromptSubmit", None) -config.pop("smokeUserKey", None) -path.write_text(json.dumps(config, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") -PY -echo "reinstall preserved user-managed agent settings" >> "$LOG" - -# 旧 .kiro/agents/default.json からの自動移行をチェックする。kiro-cli には依存しない。 -MIGRATION_ROOT="$ARTIFACT_DIR/kiro-legacy-migration" -rm -rf "$MIGRATION_ROOT" - -# $1: プロジェクト, $2: ndf ならば旧 installer の生成物そのままの fixture、 -# それ以外なら NDF 生成物と判定されない fixture を書く。 -# -# installer の判定は「旧テンプレート固有の description の完全一致」+「旧 resources -# の skill:// 指定 または agentSpawn フックの CLAUDE.ndf.md チェック」なので、 -# 自動移行ケースの fixture は旧 default.json.template と同じ値を持たせる。 -write_legacy_agent() { - mkdir -p "$1/.kiro/agents" - if [ "$2" = ndf ]; then - cat > "$1/.kiro/agents/default.json" <<'JSON' -{ - "name": "default", - "description": "NDF統合開発エージェント(Kiro CLI用)", - "tools": ["*"], - "resources": [ - "file://AGENTS.md", - "file://README.md", - "file://.kiro/skills/ndf-policies/SKILL.md", - "skill://.kiro/skills/**/SKILL.md" - ], - "hooks": { - "agentSpawn": [ - { "command": "if [ -f \"${PWD}/CLAUDE.ndf.md\" ]; then echo \"[NDF] CLAUDE.ndf.md\"; fi" } - ] - }, - "mcpServers": { "legacy-user-mcp": { "command": "echo", "args": ["legacy"] } } -} -JSON - else - cat > "$1/.kiro/agents/default.json" <<'JSON' -{ - "name": "default", - "description": "my own agent", - "resources": ["file://AGENTS.md"], - "mcpServers": { "legacy-user-mcp": { "command": "echo", "args": ["legacy"] } } -} -JSON - fi -} -install_into() { - bash "$REPO_ROOT/plugins/ndf/dev.kiro/install.sh" --project "$1" "${@:2}" >> "$LOG" 2>&1 -} - -# 1. NDF 生成物 + ndf.json なし → 自動移行し、利用者の mcpServers を引き継ぐ -case_ndf="$MIGRATION_ROOT/ndf-generated" -write_legacy_agent "$case_ndf" ndf -install_into "$case_ndf" -test ! -e "$case_ndf/.kiro/agents/default.json" -test -f "$case_ndf/.kiro/agents/default.json.bak" -python3 - "$case_ndf/.kiro/agents/ndf.json" <<'PY' -import json -from pathlib import Path -import sys - -path = Path(sys.argv[1]) -config = json.loads(path.read_text(encoding="utf-8")) -if config.get("name") != "ndf": - raise SystemExit(f"legacy migration did not refresh installer-managed keys: {path}") -if "legacy-user-mcp" not in config.get("mcpServers", {}): - raise SystemExit(f"legacy migration dropped a user-managed mcpServers entry: {path}") -PY - -# 2. NDF 生成物と判定できない default.json → 移行せず元のまま残す -case_user="$MIGRATION_ROOT/user-owned" -write_legacy_agent "$case_user" user -install_into "$case_user" -test -f "$case_user/.kiro/agents/default.json" -test -f "$case_user/.kiro/agents/default.json.bak" -python3 - "$case_user/.kiro/agents/ndf.json" <<'PY' -import json -from pathlib import Path -import sys - -path = Path(sys.argv[1]) -config = json.loads(path.read_text(encoding="utf-8")) -if "legacy-user-mcp" in config.get("mcpServers", {}): - raise SystemExit(f"a non-NDF default.json must not be migrated automatically: {path}") -PY - -# 3. default.json と ndf.json の両方がある → 移行せず既存の ndf.json を尊重する -case_both="$MIGRATION_ROOT/both" -mkdir -p "$case_both" -install_into "$case_both" -python3 - "$case_both/.kiro/agents/ndf.json" <<'PY' -import json -from pathlib import Path -import sys - -path = Path(sys.argv[1]) -config = json.loads(path.read_text(encoding="utf-8")) -config["smokeExistingKey"] = "keep-me" -path.write_text(json.dumps(config, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") -PY -write_legacy_agent "$case_both" ndf -install_into "$case_both" -test -f "$case_both/.kiro/agents/default.json" -python3 - "$case_both/.kiro/agents/ndf.json" <<'PY' -import json -from pathlib import Path -import sys - -path = Path(sys.argv[1]) -config = json.loads(path.read_text(encoding="utf-8")) -if config.get("smokeExistingKey") != "keep-me": - raise SystemExit(f"legacy migration overwrote an existing ndf.json: {path}") -if "legacy-user-mcp" in config.get("mcpServers", {}): - raise SystemExit(f"legacy migration overwrote an existing ndf.json: {path}") -PY - -# 4. --dry-run → 移行を含め一切書き込まない -case_dry="$MIGRATION_ROOT/dry-run" -write_legacy_agent "$case_dry" ndf -dry_state() { (cd "$case_dry" && find . | sort && find . -type f -exec sha256sum {} + | sort); } -before_dry="$(dry_state)" -install_into "$case_dry" --dry-run -if [ "$(dry_state)" != "$before_dry" ]; then - echo "--dry-run modified the project: $case_dry" >&2 - exit 1 -fi -echo "installer migrated a legacy default.json only when it is safe" >> "$LOG" -fi -# --- workspace 限定のチェックここまで --- - -if ! command -v kiro-cli >/dev/null 2>&1; then - echo "kiro-cli agent checks skipped: kiro-cli is not available" >> "$LOG" - exit 0 -fi - -# kiro-cli は workspace エージェントを cwd 配下の .kiro/agents からしか検出しない。 -# scope ごとに、生成した $AGENT_FILE を検出できる cwd と installer の再実行引数を選ぶ。 -case "$scope" in - workspace) KIRO_CWD="$PROJECT_DIR" ;; - # global エージェントはどこからでも解決できるはずなので、.kiro を持たない中立の - # ディレクトリを cwd にする。$HOME を使うと $HOME/.kiro が workspace 扱いにもなり、 - # 「Global として見えている」ことのチェックにならない。 - global) KIRO_CWD="$ARTIFACT_DIR" ;; -esac -if [ "$scope" = global ] && [ -e "$KIRO_CWD/.kiro" ]; then - echo "global agent checks need a cwd without .kiro: $KIRO_CWD" >&2 - exit 1 -fi - -esc="$(printf '\033')" -agent_list() { - # kiro-cli 2.16.1 の agent list は一覧を標準エラー出力へ書く - (cd "$KIRO_CWD" && kiro-cli agent list 2>&1) | sed -e "s/${esc}\\[[0-9;]*m//g" -} -current_default() { - agent_list | awk '/^\*/ { print $2; exit }' -} - -if ! agent_list > "$ARTIFACT_DIR/kiro-agent-list-$scope.txt"; then - echo "kiro-cli agent checks skipped: agent list failed" >> "$LOG" - exit 0 -fi -# global scope の $KIRO_CWD には .kiro がないため、ここに $AGENT_NAME が出ること自体が -# 「Global: ~/.kiro/agents 経由でどこからでも解決できる」ことのチェックになる。 -# **パイプの右で判定しない。** `grep -q` は最初の一致で終わるため、`awk` がまだ書いて -# いる間にパイプが閉じ、`set -o pipefail` のもとでは一致していても 141 になる -# (assert-plugin-files.sh の注記を参照)。結果を変数で受けてから照合する。 -names="$(awk '{ print $1, $2 }' "$ARTIFACT_DIR/kiro-agent-list-$scope.txt")" -if ! grep -qw "$AGENT_NAME" <<<"$names"; then - echo "agent list ($scope) does not contain $AGENT_NAME" >&2 - cat "$ARTIFACT_DIR/kiro-agent-list-$scope.txt" >&2 - exit 1 -fi - -before_default="$(current_default)" -echo "default agent before: ${before_default:-unknown}" >> "$LOG" - -# kiro-cli の既定エージェントは ~/.local/share/kiro-cli/data.sqlite3 に保存されるマシン全体の -# 設定であり、このチェックは必ず元へ戻す必要がある。set-default は agent list と同じく workspace -# エージェントを cwd 配下からしか検出せず、しかも未検出でも終了コード 0 を返すため、 -# agent_list と同じ $KIRO_CWD から実行し、戻ったことを agent list で検証する。 -# 途中のチェックが落ちても復旧するよう trap で実行する。 -restore_default() { - [ -n "$before_default" ] || return 0 - [ "$before_default" != "$AGENT_NAME" ] || return 0 - (cd "$KIRO_CWD" && kiro-cli agent set-default "$before_default") >> "$LOG" 2>&1 || true - restored_default="$(current_default || true)" - echo "default agent restored: ${restored_default:-unknown}" >> "$LOG" - if [ "$restored_default" != "$before_default" ]; then - echo "failed to restore the default agent: ${restored_default:-unknown} (expected $before_default)" >&2 - return 1 - fi -} -# shellcheck disable=SC2154 # rc は trap の文字列の中で代入する(#1323) -trap 'rc=$?; restore_default || rc=1; exit $rc' EXIT - -# kiro-cli はエージェントを cwd / $HOME 配下からのみ検出する。workspace では --project で -# 別ディレクトリへ導入したときに --set-default が効くことをチェックするため、PROJECT_DIR 以外の -# cwd から実行する。global でも同様に $HOME 以外の cwd から実行して既定切替をチェックする。 -(cd "$ARTIFACT_DIR" && bash "$REPO_ROOT/plugins/ndf/dev.kiro/install.sh" "${INSTALL_ARGS[@]}" --with-slack --set-default --yes) >> "$LOG" 2>&1 -after_default="$(current_default)" -echo "default agent after: ${after_default:-unknown}" >> "$LOG" -if [ "$after_default" != "$AGENT_NAME" ]; then - echo "--set-default did not switch the default agent ($scope): ${after_default:-unknown}" >&2 - exit 1 -fi diff --git a/tests/runtime-smoke/assertions/assert-plugin-files.sh b/tests/runtime-smoke/assertions/assert-plugin-files.sh index 55d37e463..16a621d68 100755 --- a/tests/runtime-smoke/assertions/assert-plugin-files.sh +++ b/tests/runtime-smoke/assertions/assert-plugin-files.sh @@ -17,19 +17,6 @@ require_path() { [ -n "$hit" ] || { echo "not found: $pattern under $root" >&2; exit 1; } } -# symlink をたどる版。Kiro CLI は `.kiro/skills/` へ symlink を張る。 -require_path_follow() { - local root="$1" pattern="$2" hit - hit="$(find -L "$root" -path "$pattern" -print -quit)" - [ -n "$hit" ] || { echo "not found: $pattern under $root" >&2; exit 1; } -} - -require_name() { - local root="$1" pattern="$2" hit - hit="$(find "$root" -name "$pattern" -print -quit)" - [ -n "$hit" ] || { echo "not found: $pattern under $root" >&2; exit 1; } -} - case "$runtime" in claude) require_path "$HOME" '*/.claude-plugin/plugin.json' @@ -44,72 +31,5 @@ case "$runtime" in require_path "$HOME" '*/skills/*/SKILL.md' require_path "$HOME" '*/hooks/codex.json' ;; - kiro) - test -f "$PROJECT_DIR/.kiro/agents/ndf.json" - # tools が未宣言だと Kiro CLI はツールなしのエージェントとして読み込み、 - # skill が SKILL.md を読むことも git / gh を実行することもできなくなる。 - python3 -c ' -import json, sys -config = json.load(open(sys.argv[1])) -tools = config.get("tools") -if not tools: - sys.exit("agent config declares no tools: " + sys.argv[1]) -' "$PROJECT_DIR/.kiro/agents/ndf.json" - require_path_follow "$PROJECT_DIR/.kiro/skills" '*/SKILL.md' - # playwright-kit は別プラグインとして導入する(NDF の manifest には含まれない) - test -L "$PROJECT_DIR/.kiro/skills/playwright-planning" - test -f "$PROJECT_DIR/.kiro/skills/playwright-planning/SKILL.md" - test -f "$PROJECT_DIR/.kiro/prompts/pr.md" - test -s "$PROJECT_DIR/.kiro/steering/ndf-policies.md" - test -L "$PROJECT_DIR/.kiro/mcp_runtime/mcp-bigquery" - ;; - agy) - # agy は導入時にプラグインのディレクトリ全体を複製する。symlink は実体へ解決されるため、 - # 導入先では独立したファイルになる。 - plugin_dir="$HOME/.gemini/config/plugins/ndf" - test -f "$plugin_dir/plugin.json" - test -f "$plugin_dir/hooks.json" - require_path "$plugin_dir/skills" '*/SKILL.md' - require_name "$plugin_dir/agents" '*.md' - test -f "$plugin_dir/scripts/worktree-guard.sh" - # 配る Skill の基準は manifest だけが持つ。導入先の並びが基準と一致することを見る。 - python3 - "$REPO_ROOT/plugins/ndf/manifests/agy-skills.txt" "$plugin_dir/skills" <<'AGYPY' -import sys -from pathlib import Path - -manifest, installed = Path(sys.argv[1]), Path(sys.argv[2]) -expected = sorted( - line.split("#", 1)[0].strip() - for line in manifest.read_text(encoding="utf-8").splitlines() - if line.split("#", 1)[0].strip() -) -present = sorted(d.name for d in installed.iterdir() if (d / "SKILL.md").is_file()) -if expected != present: - sys.exit( - "installed agy skills differ from the manifest: " - f"missing={sorted(set(expected) - set(present))} " - f"extra={sorted(set(present) - set(expected))}" - ) -print(f"agy skills installed: {len(present)}") -AGYPY - # ファイルの存在だけでは、agy がどの要素を取り込んだかを確かめられない。取り込みの - # 記録を読み、並びに依存せず 3 つが揃っていることを見る。 - # agy 1.3.4 から既定の出力はテキストになり、JSON は --json のときだけ出る。形も - # {"imports": [{"components": [...]}]} から、要素ごとに skills / agents / hooks の - # 一覧を持つ配列へ変わった。どちらの形も読む。 - agy plugin list --json | python3 -c ' -import json, sys -data = json.load(sys.stdin) -imports = data.get("imports", []) if isinstance(data, dict) else data -entry = next((i for i in imports if i.get("name") == "ndf"), None) -if entry is None: - sys.exit("agy plugin list does not contain ndf") -components = entry.get("components") or [k for k in ("agents", "hooks", "skills") if entry.get(k)] -components = sorted(components) -if components != ["agents", "hooks", "skills"]: - sys.exit(f"agy imported unexpected components: {components}") -print("agy components: " + ", ".join(components)) -' - ;; *) echo "unknown runtime: $runtime" >&2; exit 2 ;; esac diff --git a/tests/runtime-smoke/cli-versions.env b/tests/runtime-smoke/cli-versions.env index a23fa084d..f8968872a 100644 --- a/tests/runtime-smoke/cli-versions.env +++ b/tests/runtime-smoke/cli-versions.env @@ -1,4 +1,4 @@ -# CLI の版の一覧: スモークの像へ入れる 4 ランタイムの CLI の版数・取得先・checksum。 +# CLI の版の一覧: スモークの像へ入れる 2 ランタイムの CLI の版数・取得先・checksum。 # 版を上げるときに書き換える唯一の場所。手順は tests/runtime-smoke/README.md の「版を上げる」。 # 1 行 1 つの KEY=VALUE。値は引用符で囲まない。キーは Containerfile の ARG の名前と同じ。 # arch の名前は `uname -m` の値(x86_64 / aarch64)に揃える。 @@ -9,14 +9,3 @@ CLAUDE_CODE_VERSION=2.1.296 # codex: npm の @openai/codex の版数 CODEX_VERSION=0.162.1 -# kiro: 版数と kirocli--linux-musl.tar.gz の sha256 -KIRO_CLI_VERSION=2.31.0 -KIRO_CLI_SHA256_X86_64=3d0782f274f4180f4f5759ebb780fd46e8b9927261e5774dae1fd486f195cf5e -KIRO_CLI_SHA256_AARCH64=d309b7ce5368cbd01eeb9fc40f18da05ba3d4b39b2ce5457a7731eec9ea3de3c - -# agy: 版数と manifest の url・sha512 -AGY_VERSION=1.3.4 -AGY_URL_X86_64=https://storage.googleapis.com/antigravity-public/antigravity-cli/1.3.4-5524158944247808/linux-x64/cli_linux_x64.tar.gz -AGY_URL_AARCH64=https://storage.googleapis.com/antigravity-public/antigravity-cli/1.3.4-5524158944247808/linux-arm/cli_linux_arm64.tar.gz -AGY_SHA512_X86_64=dbdacfc5a1d9874fbbea3a21026d3990b4728e2e3487fd0316dc53cae9a3091023f969425828f3e2e0fadda43cea07dd978ea96f4ca7c202adf2201f419c56dd -AGY_SHA512_AARCH64=24a1d463319701f4943b2d4932cb86ca34d60d2e9fedcd9018ca31809773d719f272512068911632db175d7233088551033bcecc0a2a56b1bcfa041269687eaf