diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 75437d250..1c8e95934 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "ndf", "source": "./plugins/ndf", - "description": "Claude Code plugin (v10.17.0): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", + "description": "Claude Code plugin (v10.17.1): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 27c45c346..3c0acf135 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,7 +8,7 @@ -- [ ] `uv run --project plugins/playwright-kit/skills/playwright-kit-ops --with pytest pytest . -q` +- [ ] `uv run --project plugins/playwright-kit/skills/playwright-kit-ops --with pytest pytest . -q -n auto` - [ ] `python3 scripts/check-skill-frontmatter.py` - [ ] `python3 scripts/check-doc-staleness.py` - [ ] `python3 scripts/check-markdown-links.py --root .` diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 5082d3bec..8a44326ee 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -5,6 +5,10 @@ name: Python tests # 「成功」ではなく「実行されていない」として扱うため、絞り込んだままだと対象外の変更で # マージ可能にならない。push の側は絞り込みを残す(マージの可否を決めないため) # +# push の branches に release/** を置かない。release/** から出した Pull Request では +# pull_request の側が必ず回るため、push の側も起動すると同じテストを 2 回回す(#884)。 +# main / develop はマージ後の確認として残す +# # push の絞り込みは pytest . の収集範囲に合わせる。テストの置き場所(plugins / scripts / # tests)に加えて、収集の仕方そのものを変える根の設定ファイルも対象へ入れる on: @@ -13,7 +17,6 @@ on: branches: - main - develop - - "release/**" paths: - ".github/workflows/pytest.yml" - "plugins/**" @@ -24,8 +27,16 @@ on: - "pyproject.toml" jobs: - pytest: + # ファイル単位で 2 つのジョブへ分けて回す(#882)。割り当ては根の conftest.py が + # SHARD_TOTAL / SHARD_INDEX から決める。各ジョブの中は -n auto で runner のコア数だけ + # 並列に回す(pytest-xdist は --project が指す pyproject.toml の依存に入っている) + pytest-shard: + name: pytest (${{ matrix.shard }}/2) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shard: [0, 1] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -33,9 +44,22 @@ jobs: python-version: "3.x" - name: Install uv run: python3 -m pip install --upgrade uv - # 起点はリポジトリの根に置き、1 回の起動で全件を回す。playwright-kit のテストは - # その skill の pyproject.toml が宣言する依存を使うため、--project でそこを指す。 + # 起点はリポジトリの根に置く。playwright-kit のテストはその skill の pyproject.toml が + # 宣言する依存を使うため、--project でそこを指す。 # # 前提のコマンドの確認と git の身元の設定は、根の conftest.py が行う。前提を # ワークフローの側にも置くと、増えたときに片方だけが更新される - - run: uv run --project plugins/playwright-kit/skills/playwright-kit-ops --with pytest pytest . -q + - run: uv run --project plugins/playwright-kit/skills/playwright-kit-ops --with pytest pytest . -q -n auto + env: + SHARD_TOTAL: 2 + SHARD_INDEX: ${{ matrix.shard }} + + # ruleset の必須の検査は `pytest` という名前の結果を 1 つ待つ。分けたジョブの結果を + # ここで 1 つにまとめる。名前を変えると ruleset の変更が要る。 + # always() で分けたジョブが落ちても走らせ、失敗・取り消しを失敗として返す + pytest: + needs: pytest-shard + if: always() + runs-on: ubuntu-latest + steps: + - run: test "${{ needs.pytest-shard.result }}" = success diff --git a/AGENTS.md b/AGENTS.md index c937c0807..263c86d4f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -126,7 +126,7 @@ ai-plugins/ ## NDFプラグインについて -**NDFプラグイン**は、このマーケットプレイスの主要プラグインです(v10.17.0)。plugin 名は全ランタイムで `ndf` を維持し、配布物は `plugins/ndf/` の1ディレクトリにまとまっています。 +**NDFプラグイン**は、このマーケットプレイスの主要プラグインです(v10.17.1)。plugin 名は全ランタイムで `ndf` を維持し、配布物は `plugins/ndf/` の1ディレクトリにまとまっています。 - Skill の実体は `plugins/ndf/skills/` の1箇所。配布先は `plugins/ndf/manifests/*-skills.txt` が決める - Claude Code版は 8個の専門サブエージェント、公開Skills、PreToolUse/SessionStart/Stopフックを提供 - Codex版は Codex向け公開Skillsと任意Slack通知hookを提供 diff --git a/CHANGELOG.md b/CHANGELOG.md index 315a34726..3678b1d5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,16 @@ **開発版(接尾辞の付いた版)は載せない。** `9.8.0` は `9.8.0-dev.1` までしか出ておらず、 その内容は `10.0.0` で届いている。 +## [ndf 10.17.1] - 2026-09-23 + +### 変更 + +- **全体テストを並列で回し、継続的統合ではファイル単位で 2 つのジョブへ分けた**(#882)。案内するコマンドを + `-n auto` の形にし、根の `conftest.py` が `SHARD_TOTAL` / `SHARD_INDEX` を読んで自分の分のファイルだけを + 残す。まとめジョブの名前を `pytest` にしたため、必須の検査は変わらない。Skill・スクリプト・hook の振る舞いは変わらない +- **重複した競合試験を共通実装への 1 通りへ寄せ、繰り返しの回数を減らした**(#884)。 + `release/**` の push を継続的統合の契機から外し、2 重の実行をやめた + ## [ndf 10.17.0] - 2026-09-23 ### 追加 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b98ef4bb6..25f3cd131 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,8 +60,8 @@ cd .worktrees/feature/<名前> 検査です。リポジトリの根から実行します。 ```bash -# 1. テスト(継続的統合と同じ範囲を 1 回の起動で回す) -uv run --project plugins/playwright-kit/skills/playwright-kit-ops --with pytest pytest . -q +# 1. テスト(継続的統合と同じ範囲を 1 回の起動で回す。-n auto でコア数だけ並列に回す) +uv run --project plugins/playwright-kit/skills/playwright-kit-ops --with pytest pytest . -q -n auto # 2. Skill の frontmatter の規約 python3 scripts/check-skill-frontmatter.py diff --git a/README.md b/README.md index c338b4b8e..70bae63b8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Claude Code / Codex / Kiro CLI / agy 向けのスキル・MCP設定を共有す このマーケットプレイスは、チーム全体でAI開発ツール(Claude Code / Codex / Kiro CLI / agy)の導入を加速するための事前設定されたプラグインを提供します。 -**NDFプラグイン v10.17.0** は、同じ `ndf@ai-plugins` という名前で Claude Code / Codex / Kiro CLI / agy へ配布されるプラグインです。配布物は `plugins/ndf/` の1ディレクトリにまとまっており、Skill の実体は `plugins/ndf/skills/` の1箇所だけです。どのランタイムへ配るかは `plugins/ndf/manifests/*-skills.txt` が決めます。 +**NDFプラグイン v10.17.1** は、同じ `ndf@ai-plugins` という名前で Claude Code / Codex / Kiro CLI / agy へ配布されるプラグインです。配布物は `plugins/ndf/` の1ディレクトリにまとまっており、Skill の実体は `plugins/ndf/skills/` の1箇所だけです。どのランタイムへ配るかは `plugins/ndf/manifests/*-skills.txt` が決めます。 - **公開Skills**: Claude Code向け core 45個、Kiro向け core 44個、Codex向け core 43個、agy向け core 43個に分離。 - **元Skills(45個)**: @@ -110,7 +110,7 @@ hook を効かせる手順と、新しい版へ入れ替える手順は | プラグイン名 | バージョン | 説明 | 詳細 | |------------|----------|------|------| -| **ndf** | 10.17.0 | Claude Code / Codex / Kiro CLI / agy へ 1 ディレクトリから配布する NDF プラグイン。8個の専門エージェント(Claude版)、公開Skills(Claude Code向け core 45個、Kiro向け core 44個、Codex向け core 43個、agy向け core 43個)、4ランタイム共通の作業ツリー運用フック(PreToolUse / SessionStart / userPromptSubmit / agentSpawn / PreInvocation)、Claude Stopフック、Codex/Kiro向け通知・実行補助を提供。v4.0.0 で Codex MCP サーバを廃止し、`/ndf:external-ai` skill + `corder` エージェント経由の CLI 直接実行に一本化。 | [README](./plugins/ndf/README.md) | +| **ndf** | 10.17.1 | Claude Code / Codex / Kiro CLI / agy へ 1 ディレクトリから配布する NDF プラグイン。8個の専門エージェント(Claude版)、公開Skills(Claude Code向け core 45個、Kiro向け core 44個、Codex向け core 43個、agy向け core 43個)、4ランタイム共通の作業ツリー運用フック(PreToolUse / SessionStart / userPromptSubmit / agentSpawn / PreInvocation)、Claude Stopフック、Codex/Kiro向け通知・実行補助を提供。v4.0.0 で Codex MCP サーバを廃止し、`/ndf:external-ai` skill + `corder` エージェント経由の CLI 直接実行に一本化。 | [README](./plugins/ndf/README.md) | | **playwright-kit** | 2.0.3 | Playwright による E2E テストの計画・実装・証跡管理を提供するプラグイン。ページ役割からのテスト計画、動画 / trace 付きスクリプト実装、レポート生成と Drive 保管、playwright_kit ランタイム(init、a11y / CWV スキャン)の 4 Skill。NDF v7.0.0 で分離。 | [README](./plugins/playwright-kit/README.md) | ### 変更履歴 diff --git a/conftest.py b/conftest.py index ccf49f53e..ac5e4f4b4 100644 --- a/conftest.py +++ b/conftest.py @@ -12,6 +12,8 @@ 状態を保存するテストが、実行した人の状態ディレクトリへ要約を書かない(#662 の AC72) 5. テストの実行中だけ監視の上限を指す環境変数(接頭辞 `MONITOR_`)を外す。上限を延ばした シェルから起動しても、既定値を前提にするテストが同じ結果になる(#678) +6. `SHARD_TOTAL` と `SHARD_INDEX` が与えられたら、収集した項目をファイル単位で分け、 + 自分の分だけを残す。継続的統合がジョブを分けて回すために使う(#882) どの束のディレクトリを起点にしても読まれるよう、テストの基準のディレクトリ(rootdir)は 根の設定ファイル(`pytest.ini`)がリポジトリの根へ固定する。 @@ -24,6 +26,7 @@ import os import shutil import tempfile +import zlib from pathlib import Path import pytest @@ -122,7 +125,44 @@ def pytest_unconfigure(config) -> None: _saved_monitor_env.clear() +# ジョブの分割(#882)。**分ける単位はファイルで、割り当てはパスの剰余だけで決める。** +# 同じファイルのテストは同じジョブへ入るため、ファイル単位の前提(モジュールの読み込み・ +# 一時ディレクトリ)が割れない。偏りが出たら実測時間での割り当てへ進む。 +SHARD_TOTAL_ENV = "SHARD_TOTAL" +SHARD_INDEX_ENV = "SHARD_INDEX" + + +def _shard() -> tuple[int, int] | None: + """`(index, total)` を返す。指定が無ければ `None`(分けない)。""" + total = os.environ.get(SHARD_TOTAL_ENV, "") + if total == "": + return None + index = os.environ.get(SHARD_INDEX_ENV, "") + if not (total.isdigit() and index.isdigit() and int(index) < int(total)): + raise pytest.UsageError( + f"{SHARD_TOTAL_ENV}={total!r} / {SHARD_INDEX_ENV}={index!r}: " + f"0 <= {SHARD_INDEX_ENV} < {SHARD_TOTAL_ENV} の整数を指定してください" + ) + return int(index), int(total) + + +def _select_shard(config, items) -> None: + shard = _shard() + if shard is None: + return + index, total = shard + kept, dropped = [], [] + for item in items: + path = Path(str(item.fspath)).resolve() + rel = path.relative_to(ROOT).as_posix() if path.is_relative_to(ROOT) else path.as_posix() + (kept if zlib.crc32(rel.encode()) % total == index else dropped).append(item) + if dropped: + config.hook.pytest_deselected(items=dropped) + items[:] = kept + + def pytest_collection_modifyitems(config, items) -> None: + _select_shard(config, items) bundles = {b for item in items if (b := _bundle_of(Path(str(item.fspath)))) is not None} missing = _missing(bundles) if not missing: diff --git a/docs/ndf-version-decisions.md b/docs/ndf-version-decisions.md index cdb209e4a..d1071c171 100644 --- a/docs/ndf-version-decisions.md +++ b/docs/ndf-version-decisions.md @@ -1,4 +1,4 @@ -# NDF の版ごとの決定と理由(v10.12.0〜v10.17.0) +# NDF の版ごとの決定と理由(v10.12.0〜v10.17.1) `CLAUDE.md` から移した、出た版の記録である。**その版で何を決め、なぜそう決めたか**を残す。 変更点の列挙は `CHANGELOG.md` にあり、こちらは判断の理由を持つ。**`CLAUDE.md` へ書くのは @@ -277,3 +277,13 @@ v10.17.0 で待つ間の問い合わせと、長い conductor の会話を止め **hook は Claude Code にだけ置く**(#829 #830)。代わりの待ち方(`run_in_background` の通知と `Monitor`)と文脈量を読む記録を持つのが Claude Code だけで、#827 の実測も Claude Code の記録だけで ある。Codex / Kiro / agy は規約と引き継ぎの 1 行で守り、CLI 側の消費を測った後に改めて決める。 + +v10.17.1 で全体テストの待ちを縮めた(マイルストーン 26「17 トークン消費の削減」の 2'、#882 #884)。 +計画と実測は `issues/issue-882-884-test-speedup.md` と PR #891 にある。 + +**継続的統合はファイル単位で 2 つのジョブへ分け、まとめジョブの名前を `pytest` にする**(#882)。 +`-n auto` だけではジョブが 2 分を超えた。まとめジョブが必須の検査の名前を引き継ぐため、ruleset を +変えずに済む。**分け方が項目を落としても各ジョブは成功しうるため、分割の漏れと重なりは継続的統合の +外のテストで見る**(`scripts/tests/test_root_conftest.py`)。**本番のスクリプトを変える高速化は +この版で行わない** ── `bg-wait.sh` の間隔とロックの上限の注入は振る舞いの変更で、`light` の範囲を +超える。#884 は開いたまま残す。 diff --git a/docs/plugin-development-guide.md b/docs/plugin-development-guide.md index 3b5f520ba..eac548526 100644 --- a/docs/plugin-development-guide.md +++ b/docs/plugin-development-guide.md @@ -216,7 +216,7 @@ bash scripts/build-runtime-plugins.sh --check bash scripts/validate-runtime-plugins.sh python3 scripts/check-doc-staleness.py --root . python3 scripts/check-skill-frontmatter.py -uv run --with pytest pytest scripts/tests -q +uv run --with pytest --with pytest-xdist pytest scripts/tests -q -n auto ``` ## 既存プラグインの削除 @@ -275,10 +275,10 @@ bash scripts/validate-runtime-plugins.sh `scripts/` 自体を変更した場合は、その検査のテストも実行します。 ```bash -uv run --with pytest pytest scripts/tests -q +uv run --with pytest --with pytest-xdist pytest scripts/tests -q -n auto ``` -`--with pytest` を省くと `Failed to spawn: pytest` で終わります。リポジトリの根に uv の +`--with pytest` を省くと `Failed to spawn: pytest` で終わります。`-n auto` は `pytest-xdist` の指定で、コア数だけ並列に回します(`--with pytest-xdist` を省くと `-n` を解釈できずに終わります)。リポジトリの根に uv の 対象プロジェクト(`pyproject.toml`)が無く、`pytest` が環境にも入っていないためです。 `plugins/ndf/skills/*/tests/` の既存のテストも同じ形で実行します。 diff --git a/docs/specifications/ndf-worktree-declaration-and-entry-points.md b/docs/specifications/ndf-worktree-declaration-and-entry-points.md index cfc10eadf..b03e44fe6 100644 --- a/docs/specifications/ndf-worktree-declaration-and-entry-points.md +++ b/docs/specifications/ndf-worktree-declaration-and-entry-points.md @@ -353,7 +353,7 @@ clone した全員で同じ値のまま残る。 | 区切り・プロセス置換・引用符の中の `<` で出力が変わらない | 同上 | | 手順 0 が `init` の失敗で止まることが本文に書かれている | 同 `test_setup.py`(本文の抜き出し) | -実行は `uv run --with pytest pytest scripts/tests plugins/ndf -q`。配布物の同期は +実行は `uv run --with pytest --with pytest-xdist pytest scripts/tests plugins/ndf -q -n auto`。配布物の同期は `bash scripts/build-runtime-plugins.sh --check`、定義の検査は `claude plugin validate .` で見る。 ## 運用 diff --git a/docs/specifications/test-monitor-env-isolation.md b/docs/specifications/test-monitor-env-isolation.md index ade97f37a..41c2d9ceb 100644 --- a/docs/specifications/test-monitor-env-isolation.md +++ b/docs/specifications/test-monitor-env-isolation.md @@ -56,7 +56,7 @@ | 観点 | 確かめ方 | | --- | --- | -| 監視の環境変数を設定したシェルでも、全体のテストの失敗が 0 件で、設定しないシェルと件数が一致すること | `MONITOR_TIMEOUT_AGY=1800 MONITOR_STALL_AGY=1800 uv run --with pytest pytest scripts/tests plugins/ndf -q` と、設定しない同じコマンドを比べる | +| 監視の環境変数を設定したシェルでも、全体のテストの失敗が 0 件で、設定しないシェルと件数が一致すること | `MONITOR_TIMEOUT_AGY=1800 MONITOR_STALL_AGY=1800 uv run --with pytest --with pytest-xdist pytest scripts/tests plugins/ndf -q -n auto` と、設定しない同じコマンドを比べる | | 実行中のテストに接頭辞 `MONITOR_` の環境変数が 1 つも残らないこと | `scripts/tests/test_root_conftest.py` | | テストが自分で設定した値は観測できること | 同上 | | 別プロセスへ受け継がれないこと | 同上 | diff --git a/docs/versioning-and-distribution.md b/docs/versioning-and-distribution.md index 68cfc4a70..9f59436ce 100644 --- a/docs/versioning-and-distribution.md +++ b/docs/versioning-and-distribution.md @@ -57,11 +57,11 @@ semver の順序で除外されるのは、プラグイン間の依存解決(` | 版 | 形 | 意味 | | --- | --- | --- | -| 正式版 | `10.17.0` | 利用者が常用してよい | +| 正式版 | `10.17.1` | 利用者が常用してよい | | 開発版 | `10.18.0-dev.1` | 検証中。入れたくない利用者は取得を控えられる | | 公開前の確認版 | `10.18.0-rc.1` | 正式版の候補。残るのは確認だけ | -- 接尾辞は**次に出す正式版の版数へ付ける**。`10.17.0` の次を開発するなら `10.18.0-dev.1` +- 接尾辞は**次に出す正式版の版数へ付ける**。`10.17.1` の次を開発するなら `10.18.0-dev.1` - 連番は開発版を出すたびに増やす。**同じ版数で中身を差し替えない**。差し替えると、利用者の 手元にある版と `main` の版が同じ番号で別物になり、何を確かめたのかが分からなくなる - **正式版を出すときは接尾辞を外す。** `10.18.0-dev.3` の次は `10.18.0` diff --git a/issues/issue-882-884-test-speedup.md b/issues/issue-882-884-test-speedup.md new file mode 100644 index 000000000..06610d395 --- /dev/null +++ b/issues/issue-882-884-test-speedup.md @@ -0,0 +1,32 @@ +# #882 #884 テストの高速化(要求と受け入れ条件) + +モード: `light`(テストと継続的統合の設定、テストを案内する文書だけを触る。本番のスクリプトは変えない) + +## 要求 + +全体テストを回すたびの待ちを縮める。手元・継続的統合・`cross-refactoring` の `--baseline-test` の +どれでも、全体テストの所要がそのまま待ちになっている(#880)。 + +## 範囲 + +| 課題 | この変更で行う | 行わない(理由と行き先) | +| --- | --- | --- | +| #882 | 継続的統合の全体テストを `-n auto` で並列に回す。案内するコマンド(`CONTRIBUTING.md`・PR テンプレート・`docs/plugin-development-guide.md`・確定仕様 2 件)を並列の形にする。並列で壊れるテストを直す。並列にした後の継続的統合のジョブが 2 分 6 秒(テスト 1 分 53 秒)で 2 分を超えたため、ファイル単位の 2 分割(根の `conftest.py` の `SHARD_TOTAL` / `SHARD_INDEX`)とまとめジョブ `pytest` を置く(段 2) | — | +| #884 | 競合試験の重複を共通実装への 1 通りへ寄せる。繰り返しの回数を目的に必要な数まで減らす(`test_records_at_once_never_skip_a_stage` 8 → 4 回、`test_lock_held_passes` 4 → 3 回)。`release/**` の push 契機を外し、継続的統合の 2 重実行をやめる | `bg-wait.sh` のポーリング間隔(本番のスクリプトの変更)、ロックの上限 5 秒の注入(本番のスクリプトの変更。#293 の決定に関わる)、`test_git_facts.py` の猶予待ち(#883) | + +## 受け入れ条件 + +| # | 条件 | 確かめ方 | +| --- | --- | --- | +| AC1 | 継続的統合の pytest ジョブが `-n auto` で走り、全件合格する | Pull Request の `pytest` の検査 | +| AC2 | 全体テストを案内するコマンドがすべて並列の形になっている | `grep -rn "pytest scripts/tests plugins/ndf -q\|pytest \. -q"`(`issues/` と記録の文書を除く) | +| AC3 | 手元の並列実行(`-n 4`)で全件合格し、変更前の順の実行より速い | 前後の所要を実測して PR 本文へ載せる | +| AC4 | 継続的統合の所要が変更前(テスト 5 分 30 秒)の半分以下 | Pull Request の実行時間 | +| AC5 | 寄せた競合試験が、臨界区間の重なりを変更後も失敗として検出する | `ndf_lock_acquire` の排他を外した状態で `test_many_at_once_never_share_the_critical_section` が落ちることを 1 度確かめる | +| AC6 | 減らした繰り返しのテストが、見ている不具合を変更後も検出する | 同上の手順で、対象の排他を外すと落ちることを確かめる | +| AC7 | `release/**` からの Pull Request で `Python tests` が 1 回だけ起動する | `on.push.branches` に `release/**` が無い | +| AC8 | 分割しても ruleset の必須の検査 `pytest` が 1 つの結果として返り、ruleset を変えずにマージできる | Pull Request の検査の一覧で `pytest` が合格し、マージ可能になる | + +#884 の受け入れ条件「0.5 秒以上のテストの合計が 40 秒以下」は、この変更だけでは満たさない。 +残る大口(`test_git_facts.py` 21 秒・`test_bg_wait.py` 19 秒・ロックの上限を待つテスト)は +本番のスクリプトの変更を要するため、#884 は開いたまま残し、残りを本文へ記録する。 diff --git a/plugins/ndf/.claude-plugin/plugin.json b/plugins/ndf/.claude-plugin/plugin.json index 0ebbb32ea..e646b3358 100644 --- a/plugins/ndf/.claude-plugin/plugin.json +++ b/plugins/ndf/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ndf", - "version": "10.17.0", - "description": "Claude Code plugin (v10.17.0): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", + "version": "10.17.1", + "description": "Claude Code plugin (v10.17.1): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", "author": { "name": "takemi-ohama", "url": "https://github.com/takemi-ohama" diff --git a/plugins/ndf/.codex-plugin/plugin.json b/plugins/ndf/.codex-plugin/plugin.json index 93da3ca55..89c318c31 100644 --- a/plugins/ndf/.codex-plugin/plugin.json +++ b/plugins/ndf/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ndf", - "version": "10.17.0", - "description": "Codex plugin (v10.17.0): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation (Codex/agy), and optional Slack completion notifications.", + "version": "10.17.1", + "description": "Codex plugin (v10.17.1): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation (Codex/agy), and optional Slack completion notifications.", "skills": [ "./skills/cherry-pick-pr", "./skills/cross-refactoring", diff --git a/plugins/ndf/README.md b/plugins/ndf/README.md index 4b38b8107..584b95256 100644 --- a/plugins/ndf/README.md +++ b/plugins/ndf/README.md @@ -89,7 +89,7 @@ bash plugins/ndf/dev.kiro/install.sh --dry-run ```bash python3 -c "import json;print(json.load(open('.kiro/agents/ndf.json'))['description'])" -# => NDF統合開発エージェント(Kiro CLI用 / v10.17.0) +# => NDF統合開発エージェント(Kiro CLI用 / v10.17.1) ``` ### agy @@ -119,20 +119,21 @@ agy plugin list # => {"imports":[{"name":"ndf","source":"antigravity","components":["skills","agents","hooks"]}]} ``` -## v10.17.0 へ更新するとき +## v10.17.1 へ更新するとき -**Claude Code で、待つ間の繰り返しの問い合わせと、文脈が上限を超えた conductor の工程の起動を -hook が止めるようにしました**(マイルストーン 26「17 トークン消費の削減」、#829 #830)。Skill の数は -変わりません。引数・Skill・スクリプトの削除や改名は無く、記録の移行も要りません。Codex / Kiro / agy の -hook は変わりません。変更点の一覧は [CHANGELOG.md](../../CHANGELOG.md) にあります。 +**全体テストを並列・2 分割で回すようにし、重複した競合試験と繰り返しを減らしました**(マイルストーン 26 +「17 トークン消費の削減」、#882 #884)。変わったのはテストと継続的統合の設定、テストを案内する文書 +だけで、**Skill・スクリプト・hook の振る舞いは 10.17.0 と同じです。** Skill の数も変わりません。 +引数・Skill・スクリプトの削除や改名は無く、記録の移行も要りません。変更点の一覧は +[CHANGELOG.md](../../CHANGELOG.md) にあります。 -**正式版です。** `main` に載ります。中身は開発版 `10.17.0-dev.1` と同じで、版数の接尾辞だけを +**正式版です。** `main` に載ります。中身は開発版 `10.17.1-dev.1` と同じで、版数の接尾辞だけを 外しました。 | 変わったこと | 中身 | | --- | --- | -| **前景の `sleep` の待ちと、変わらないファイルの読み直しを止めます**(#829) | `while` / `until` のループの本体にある `sleep` と 5 秒を超える `sleep`、同じファイルの同じ範囲を変わらないまま 3 回続けて読む Read を、理由の欄に代わりの待ち方(`run_in_background: true` で起動して完了通知を待つ / `Monitor`)を書いて止めます。待ち方の規約は `development-workflow/references/waiting.md` にあります。`NDF_SLEEP_GUARD=0` / `NDF_READ_REPEAT_GUARD=0` で止められます | -| **文脈が 200,000 を超えた conductor の工程の起動を 1 度止めます**(#830) | 工程 Skill か持ち場の supervisor を起動すると、新しい会話で打つ 1 行(`/ndf:development-workflow #<課題>`)を示して止めます。このまま続けるなら同じ起動をもう一度行えば通ります。`NDF_CONTEXT_GUARD=0` で止め、`NDF_CONTEXT_LIMIT` で上限を変えられます | +| **全体テストを並列で回します**(#882) | 継続的統合の全体テストを `-n auto` で回し、ファイル単位で 2 つのジョブへ分けます(根の `conftest.py` が `SHARD_TOTAL` / `SHARD_INDEX` を読む。手元で指定しなければ分けない)。案内するコマンドは `uv run --project plugins/playwright-kit/skills/playwright-kit-ops --with pytest pytest . -q -n auto` の形になりました | +| **重複した競合試験と繰り返しを減らしました**(#884) | 臨界区間の競合試験を共通実装への 1 通りへ寄せ、繰り返しの回数を目的に要る数まで減らしました。`release/**` の push で継続的統合が 2 重に走らないようにしました | 正式版のチャネル(ref を指定せずに登録した取得元)なら、次で入れ替わります。**動いているセッションには 反映されない**ため、更新したあとは起動し直してください。開発版を試すために `develop` を登録した @@ -155,8 +156,8 @@ codex plugin add ndf@ai-plugins にあります。 ```bash -grep -q 'token-guard.sh' "$SCRIPTS/../hooks/claude.json"; echo "exit=$?" # 0 なら hook が登録されている -test -f "$SCRIPTS/../skills/development-workflow/references/waiting.md"; echo "exit=$?" # 0 なら待ち方の規約がある +grep -q '"version": "10.17.1"' "$SCRIPTS/../.claude-plugin/plugin.json"; echo "exit=$?" # 0 なら この版が入っている +grep -q '共通実装に対して 1 通りだけ回す' "$SCRIPTS/../skills/worktree/tests/test_registry.py"; echo "exit=$?" # 0 なら 寄せた競合試験が入っている ``` ## Playwright テストについて @@ -321,7 +322,7 @@ agy models # 認証の確認 ```text # 動く: 実体パスを示して読ませる -~/.codex/plugins/cache/ai-plugins/ndf/10.17.0/skills/deploy/SKILL.md を読んで、その手順どおりに qa/staging へ deploy PR を作成してください。 +~/.codex/plugins/cache/ai-plugins/ndf/10.17.1/skills/deploy/SKILL.md を読んで、その手順どおりに qa/staging へ deploy PR を作成してください。 # 動かない: 明示起動 ($ は展開されない) $deploy qa/staging @@ -343,14 +344,14 @@ marketplace 経由でインストールした場合、Skill の実体は **ワ ```text $CODEX_HOME/plugins/cache////skills//SKILL.md # 既定 ($CODEX_HOME=~/.codex) の例: -# ~/.codex/plugins/cache/ai-plugins/ndf/10.17.0/skills/deploy/SKILL.md +# ~/.codex/plugins/cache/ai-plugins/ndf/10.17.1/skills/deploy/SKILL.md ``` そのため「`deploy` の SKILL.md を探して読んで」のような曖昧な依頼は、Codex のファイル探索がワークスペース内に限られる状況では失敗しえます。**抑止した Skill は `$` が展開されない**ので、`codex plugin list` で実体パスを確認し、絶対パスを渡してください。 ```bash codex plugin list | grep 'ndf@ai-plugins' -# => ndf@ai-plugins installed, enabled 10.17.0 +# => ndf@ai-plugins installed, enabled 10.17.1 ``` 抑止していない Skill(`markdown-writing` など)はキャッシュ配下でも `$` で解決するため、そちらは `$` 起動が使えます。 diff --git a/plugins/ndf/dev.agy/plugin.json b/plugins/ndf/dev.agy/plugin.json index 5fd2b7a8b..3c21fe577 100644 --- a/plugins/ndf/dev.agy/plugin.json +++ b/plugins/ndf/dev.agy/plugin.json @@ -1,5 +1,5 @@ { "name": "ndf", - "version": "10.17.0", - "description": "Antigravity CLI plugin (v10.17.0): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation, and worktree guidance hooks." + "version": "10.17.1", + "description": "Antigravity CLI plugin (v10.17.1): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation, and worktree guidance hooks." } diff --git a/plugins/ndf/scripts/tests/test_token_guard.py b/plugins/ndf/scripts/tests/test_token_guard.py index 57f1a9073..cb5c1d0cf 100644 --- a/plugins/ndf/scripts/tests/test_token_guard.py +++ b/plugins/ndf/scripts/tests/test_token_guard.py @@ -362,7 +362,9 @@ def test_lock_held_passes(tmp_path, state): (lock / "held").write_text("") (lock / "pid").write_text(str(os.getpid())) (lock / "token").write_text("t") - for _ in range(4): + # 3 回目は排他を取れれば拒否される回数である(test_repeat_read_denied_on_third)。 + # 1 回ごとに排他の上限(1 秒)を待つため、それを越えて回さない(#884) + for _ in range(3): assert denied(run(read(f), state)) is None diff --git a/plugins/ndf/skills/development-workflow/tests/test_stage_check.py b/plugins/ndf/skills/development-workflow/tests/test_stage_check.py index e7b9d88b4..83dadae96 100644 --- a/plugins/ndf/skills/development-workflow/tests/test_stage_check.py +++ b/plugins/ndf/skills/development-workflow/tests/test_stage_check.py @@ -283,7 +283,8 @@ def test_records_at_once_never_skip_a_stage(repo: Path, state: Path) -> None: """#308-2 / 3: 同時の記録を繰り返しても工程が欠けず、飛ばした記録も出ない。 1 回の実行では取りこぼしがあっても通ることがあるため、繰り返して件数で見る。 - 受け入れ条件が求める 60 回は完了判定として手元で回す。 + 受け入れ条件が求める 60 回は完了判定として手元で回す。ここでの試行は 4 回にする + (#884。8 回から減らした。4 つの記録が同時に走る試行を繰り返す目的は変わらない)。 """ stages = ("設計", "計画", "実装", "実装レビュー") env = base_env(state, {"NDF_STAGE_LOCK_TIMEOUT": "20"}) @@ -291,7 +292,7 @@ def test_records_at_once_never_skip_a_stage(repo: Path, state: Path) -> None: short = [] skipped = 0 - for trial in range(8): + for trial in range(4): issue = 3080 + trial procs = [ subprocess.Popen( diff --git a/plugins/ndf/skills/worktree/tests/test_registry.py b/plugins/ndf/skills/worktree/tests/test_registry.py index f6e846383..ff4c757a3 100644 --- a/plugins/ndf/skills/worktree/tests/test_registry.py +++ b/plugins/ndf/skills/worktree/tests/test_registry.py @@ -179,7 +179,8 @@ def test_broken_registry_is_treated_as_empty(main_repo: Path) -> None: WF_LIB = Path(__file__).resolve().parents[3] / "skills/development-workflow/scripts/lib/workflow-common.sh" # 実装は `scripts/lib/lock-common.sh` の 1 箇所にあり、2 つの読み込む側が既存の名前へ -# 結んでいる(#293)。**両方の読み込む側へ同じ検査をかける。** +# 結んでいる(#293)。**両方の読み込む側へ同じ検査をかける。** 例外は競合試験で、 +# 共通実装に対して 1 通りだけ回す(下の `test_many_at_once_never_share_the_critical_section`)。 LOCK_LIBS = [ pytest.param(LIB, "wt_lock_acquire", "wt_lock_release", id="worktree"), pytest.param(WF_LIB, "wf_lock_acquire", "wf_lock_release", id="workflow"), @@ -248,17 +249,18 @@ def _run_lock_race( return result -@pytest.mark.parametrize(("parallel", "trials"), [(6, 7), (12, 3)]) -@pytest.mark.parametrize(("lib", "acquire", "release"), LOCK_LIBS) -def test_many_at_once_never_share_the_critical_section( - tmp_path: Path, lib: Path, acquire: str, release: str, parallel: int, trials: int -) -> None: +def test_many_at_once_never_share_the_critical_section(tmp_path: Path) -> None: """#297-1 / 2 / 3 と #308-5 を 1 つの測定で見る。 - 並列数を 6 と 12 で変えても結果が変わらないことが、持ち主の決定が時間に依らない - ことの担保になる。 + **競合試験は共通実装に対して 1 通りだけ回す**(#884)。2 つの入口はどちらも + `ndf_lock_acquire` を 1 行で呼ぶだけで、入口が共通実装へ届くことは + `scripts/tests/test_lock_common.py::test_the_existing_names_take_and_release_the_lock` + が見ている。入口 × 並列数の 4 通りで同じ臨界区間を試しても、検出できる不具合は増えない。 + 並列数は多い側(12)を残す。重なりは同時に取りに行く数が多いほど出やすい。 """ - got = _run_lock_race(tmp_path, lib, acquire, release, parallel=parallel, trials=trials) + got = _run_lock_race( + tmp_path, LIB, "wt_lock_acquire", "wt_lock_release", parallel=12, trials=3 + ) assert got["overlap"] == 0, f"臨界区間が重なった試行 {got['overlap']} 件" assert got["miss"] == 0, f"上限に達して取れなかった回数 {got['miss']} 回" diff --git a/scripts/tests/test_lock_common.py b/scripts/tests/test_lock_common.py index f35e48e5a..27b2fdde8 100644 --- a/scripts/tests/test_lock_common.py +++ b/scripts/tests/test_lock_common.py @@ -241,55 +241,10 @@ def test_a_missing_common_file_stops_the_write_but_not_the_step(tmp_path: Path) # --- A7: 同時に走らせても持ち主は 1 つ -------------------------------------- - -LOCK_WORKER = """#!/usr/bin/env bash -# $1 共通ファイル / $2 置き場所 -set -uo pipefail -. "$1" -while [ ! -e "$2/go" ]; do :; done -if ndf_lock_acquire "$2/lock" 6; then - : >"$2/in.$$" - if [ "$(ls "$2"/in.* 2>/dev/null | wc -l)" -gt 1 ]; then : >"$2/over.$$"; fi - sleep 0.02 - rm -f "$2/in.$$" - ndf_lock_release "$2/lock" -else - : >"$2/miss.$$" -fi -exit 0 -""" - - -def test_six_at_once_leave_one_owner(tmp_path: Path) -> None: - """A7: 6 つが同時に取りに行っても、臨界区間は重ならず取りこぼしも出ない。 - - `mkdir` コマンドは同じ名前の作成に複数を通す。1 回の実行では取りこぼしがあっても - 通ることがあるため、繰り返したうえで件数で見る。 - """ - worker = tmp_path / "worker.sh" - worker.write_text(LOCK_WORKER, encoding="utf-8") - base = tmp_path / "race" - base.mkdir() - overlap = miss = 0 - - for _ in range(7): - for stray in base.iterdir(): - shutil.rmtree(stray, ignore_errors=True) if stray.is_dir() else stray.unlink() - procs = [ - subprocess.Popen( - ["bash", str(worker), str(LOCK_LIB), str(base)], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - ) - for _ in range(6) - ] - (base / "go").touch() - for proc in procs: - proc.wait() - overlap += 1 if any(base.glob("over.*")) else 0 - miss += len(list(base.glob("miss.*"))) - - assert overlap == 0, f"臨界区間が重なった試行 {overlap} 件" - assert miss == 0, f"上限に達して取れなかった回数 {miss} 回" +# +# 競合試験は `worktree/tests/test_registry.py::test_many_at_once_never_share_the_critical_section` +# が共通実装に対して 1 通りだけ回す(#884)。同じ臨界区間をここでも試しても、検出できる +# 不具合は増えない。入口が共通実装へ届くことは上の A2 / A3 が見ている。 # --- A8: 呼び出し側のシェルの状態を変えない --------------------------------- diff --git a/scripts/tests/test_root_conftest.py b/scripts/tests/test_root_conftest.py index f575da6db..7ef4029b5 100644 --- a/scripts/tests/test_root_conftest.py +++ b/scripts/tests/test_root_conftest.py @@ -279,3 +279,36 @@ def test_the_prefix_is_declared_once() -> None: assert 'MONITOR_ENV_PREFIX = "MONITOR_"' in body assert "def pytest_unconfigure" in body + + +# ジョブの分割(#882)。**分けても項目が欠けず、重ならず、ファイルが割れないこと**を、 +# 実際の収集で確かめる。分割が項目を落としても各ジョブと集約のジョブは成功し得るため、 +# 継続的統合の結果だけでは退行に気づけない。 +SHARD_TARGET = "scripts/tests" +SHARD_TOTAL = 2 + + +def _collected_ids(shard: tuple[int, int] | None) -> list[str]: + env = {k: v for k, v in os.environ.items() if k not in ("SHARD_TOTAL", "SHARD_INDEX")} + if shard is not None: + env["SHARD_INDEX"], env["SHARD_TOTAL"] = str(shard[0]), str(shard[1]) + proc = subprocess.run( + [sys.executable, "-m", "pytest", SHARD_TARGET, "--collect-only", "-q", + "--no-header", "-p", "no:cacheprovider", "-p", "no:xdist"], + cwd=str(REPO_ROOT), capture_output=True, text=True, env=env, + ) + assert proc.returncode == 0, proc.stdout + proc.stderr + return [line for line in proc.stdout.splitlines() if "::" in line] + + +def test_the_shards_cover_every_item_exactly_once_without_splitting_a_file() -> None: + full = _collected_ids(None) + shards = [_collected_ids((i, SHARD_TOTAL)) for i in range(SHARD_TOTAL)] + + assert all(shards), "どれかの shard が空で、分割を確かめられない" + merged = [nodeid for ids in shards for nodeid in ids] + assert len(merged) == len(set(merged)), "同じ項目が 2 つの shard に入った" + assert sorted(merged) == sorted(full), "shard の和が未分割の収集と一致しない" + + files = [{nodeid.split("::", 1)[0] for nodeid in ids} for ids in shards] + assert not set.intersection(*files), "同じファイルが複数の shard に割れた"