From 7802d2098dfc81bb71f039f2e1c9a392e9268212 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sun, 25 Oct 2026 07:27:29 +0000 Subject: [PATCH 01/17] =?UTF-8?q?Docs:=20ndf-release-approved-commit.md=20?= =?UTF-8?q?=E3=82=92=E7=A2=BA=E5=AE=9A=E4=BB=95=E6=A7=98=E3=81=AB=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/glossary.md | 2 +- docs/glossary/glossary.json | 5 +- .../ndf-release-approved-commit.md | 128 ++++++++- issues/issue-1529-design.md | 260 ------------------ issues/issue-1529-requirements.md | 167 ----------- 5 files changed, 126 insertions(+), 436 deletions(-) delete mode 100644 issues/issue-1529-design.md delete mode 100644 issues/issue-1529-requirements.md diff --git a/docs/glossary.md b/docs/glossary.md index 65a08d5bb..153917bdb 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -536,7 +536,7 @@ release が走らせるリリースの種別・リリースコマンド・公開 | 互換なしの印 | — | 要求の「影響」の表の「公開インタフェース」の行に書く、互換の経路を持たないことを示す語(互換なし / 互換の経路は持たない)。release-steps.py changed-plugins が上げ幅の候補を MAJOR にする材料 | — | — | `docs/specifications/ndf-release-other-plugin-levels-and-migration.md` | | 承認したコミット | — | ゲート 2 の承認資料が対象にした、ベースブランチの先端のコミットの SHA(40 桁)。本番チャネルへ入れてよい中身の上限を示す | — | — | `docs/specifications/ndf-release-approved-commit.md` | | 比べた先端 | — | 本番の配布が承認したコミットと比べた時点の、ベースブランチの先端のコミットの SHA。本番チャネルへマージしてよいのはこのコミットだけである | — | — | `docs/specifications/ndf-release-approved-commit.md` | -| 確かめたコミット | — | 導入の確認を走らせた、ベースブランチの先頭のコミットの SHA(40 桁)。導入の確認の承認資料に書かれ、pace: fast / auto の昇格でマージしてよい先端はこのコミットに限る | — | — | — | +| 確かめたコミット | — | 導入の確認を走らせた、ベースブランチの先頭のコミットの SHA(40 桁)。導入の確認の承認資料に書かれ、pace: fast / auto の昇格でマージしてよい先端はこのコミットに限る | — | — | `docs/specifications/ndf-release-approved-commit.md` | | 承認の外のコミット | — | 承認したコミットから比べた先端までに入ったコミットのうち、この版の配布の PR のものでないもの。あれば本番の配布は承認ゲートで止まる | — | — | `docs/specifications/ndf-release-approved-commit.md` | | 承認の記録 | — | 承認資料の欄の 1 つ。ゲート 2 を承認したときの承認したコミットの SHA と、承認した者・時刻を持つ。承認資料を書き直すと消え、承認し直すまで本番の配布は通らない | — | — | `docs/specifications/ndf-release-approved-commit.md` | | 更新情報の節 | — | plugin の README の `## 更新情報` の節。版を名乗らず、CHANGELOG.md の版の節を参照する固定の案内だけを持つ。配布の手順(bump・changelog・notes)は書かない | — | — | `docs/specifications/ndf-release-readme-update-info.md` | diff --git a/docs/glossary/glossary.json b/docs/glossary/glossary.json index 5a0fc18eb..86a5b8a47 100644 --- a/docs/glossary/glossary.json +++ b/docs/glossary/glossary.json @@ -3482,7 +3482,7 @@ "term": "確かめたコミット", "context": "ndf-release", "meaning": "導入の確認を走らせた、ベースブランチの先頭のコミットの SHA(40 桁)。導入の確認の承認資料に書かれ、pace: fast / auto の昇格でマージしてよい先端はこのコミットに限る", - "pending_source": "issues/issue-1529-design.md" + "source": "docs/specifications/ndf-release-approved-commit.md" }, { "term": "承認の外のコミット", @@ -3735,7 +3735,8 @@ { "term": "テストの無い宣言", "context": "ndf-cross-refactoring", - "meaning": ".ndf/project.json の test があり、unknown と strategy を持たず、種別・雛形の誤りが無く、どの suite も command と scope_command のどちらも持たないもの(suite が 0 個のときを含む)。解析が「テストが無い」と測った {\"suites\": []} を含む。cross-review の最終スイープは検証のコマンドを名指ししない" + "meaning": ".ndf/project.json の test があり、unknown と strategy を持たず、種別・雛形の誤りが無く、どの suite も command と scope_command のどちらも持たないもの(suite が 0 個のときを含む)。解析が「テストが無い」と測った {\"suites\": []} を含む。cross-review の最終スイープは検証のコマンドを名指ししない", + "pending_source": "issues/issue-1593-design.md" }, { "term": "未解決のスレッド", diff --git a/docs/specifications/ndf-release-approved-commit.md b/docs/specifications/ndf-release-approved-commit.md index 9d1ff9820..2bdb94d91 100644 --- a/docs/specifications/ndf-release-approved-commit.md +++ b/docs/specifications/ndf-release-approved-commit.md @@ -7,6 +7,8 @@ - **食い違いに気づくのは `release-steps.py` の手順の上の確認である。** 配布物の差分の数え直しのような偶然に頼らない - **止まったときの結果 JSON だけで、承認したコミット・比べた先端・承認の外のコミットが読める。** LLM が git を 照会し直さなくてよい +- **`pace: fast` / `auto` の昇格の経路では、本番チャネルへ入るのは MVV 判定が読んだ導入の確認の記録のコミット(確かめたコミット)に限る。** + 確認の後にベースブランチが進めば、マージせずに承認ゲート 2 へ戻る(#1529。「昇格の経路の照合」) 例: ndf のある版を本番へ出すとき、承認の 6 分後に別の PR がベースブランチへマージされた。 @@ -35,13 +37,15 @@ ## 用語 定義は [用語集](../glossary.md) の「NDF のリリース(`ndf-release`)」が正である(`.ndf/glossary.json` の `document`)。 -この文書で使う語は「承認したコミット」「比べた先端」「承認の外のコミット」「承認の記録」である。ほかに次の語を使う。 +この文書で使う語は「承認したコミット」「比べた先端」「承認の外のコミット」「承認の記録」「確かめたコミット」である。ほかに次の語を使う。 | 語 | 指すもの | | --- | --- | | 配布の PR | この版の `release/v<版>` → ベースブランチの Pull Request(開発版の接尾辞を外す bump・changelog・他のプラグインの bump を含む) | | 本番チャネルの PR | ベースブランチ → 本番チャネルの Pull Request | | 承認資料 | `issues/approval-<プラグイン>-v<版>.md`。`approval-facts` が書く | +| 導入の確認の承認資料 | 昇格のプランの `verify` ステップ(`release-steps.py verify-facts`)が書く `approval-verify.md`。確かめたコミットの行を 1 行持つ | +| 昇格の Pull Request | ベースブランチ → 本番チャネルの Pull Request を `merged-steps.py promote` が作ってマージするもの。head はベースブランチそのもの | ベースブランチ・本番チャネル・プラグイン名は宣言(`.ndf/worktree.json` の `base_branch`・`production_branch` と `.ndf/supervise.json` の `release.plugin`)と引数から読み、 `develop` / `main` を埋め込まない。 @@ -51,10 +55,11 @@ | 扱う | 扱わない | | --- | --- | | `release-steps.py approval-facts` が承認したコミットを書くこと | `--channel dev` の振る舞い(開発版はマージした変更がそのまま載るチャネルで、承認を要しない。引数も結果も変わらない) | -| `release-steps.py approve` が承認の記録を書くこと、`mvv-gate.py check --gate release` が `by: mvv` で書くこと | 昇格の経路(`merged-steps.py promote`)。昇格は `compare_approved` を使わず、導入の確認の承認資料の確かめたコミットを `--expect-head-from` で受けて `--expect-head` と同じ照合を使う(#1529。振る舞いの正本は `merged-steps.py` の冒頭) | -| `release-steps.py release --channel prod` が承認したコミットを受け取り、承認の外のコミットがあれば止まること | 承認資料のほかの欄、MVV 判定の判定の中身 | -| `merged-steps.py merge-when-green --expect-head` が比べた先端だけをマージすること | 食い違ったときに承認資料を自動で作り直して再承認を求める仕組み(止まって人へ戻すまでを扱う) | -| 本番のリリースプラン(`supervise.py new release --channel prod`。`--mvv` 付きを含む)が承認資料を渡すこと | | +| `release-steps.py approve` が承認の記録を書くこと、`mvv-gate.py check --gate release` が `by: mvv` で書くこと | `pace: normal` の昇格(`promote` → 利用者の承認 → `promote-approved --gate-approved user`)の照合。導入の確認を走らせず、照合する記録が無い | +| `release-steps.py release --channel prod` が承認したコミットを受け取り、承認の外のコミットがあれば止まること | 承認資料のほかの欄、MVV 判定の判定の中身、導入の確認(`verify-facts`)の走らせ方 | +| `merged-steps.py merge-when-green --expect-head` が比べた先端だけをマージすること | 食い違ったときに承認資料を自動で作り直して再承認を求める仕組み、導入の確認と MVV 判定を自動でやり直す仕組み(止まって人へ戻すまでを扱う) | +| 本番のリリースプラン(`supervise.py new release --channel prod`。`--mvv` 付きを含む)が承認資料を渡すこと | 手動反映の本番系(`gate-2` のプラン。#1454) | +| `pace: fast` / `auto` の昇格のプラン(`plan_promote`)の `promote` ステップが、確かめたコミットのときだけ本番チャネルへマージし、違えば承認ゲート 2 へ戻ること(`merged-steps.py promote --expect-head-from`。#1529) | | ## 背景 @@ -70,6 +75,11 @@ **承認資料にコミットを書いて本番の範囲を縛る形は、手で反映する経路(`release-steps.py deploy-facts`)で既に使っていた。** 同じ形を `package-plugin` の本番の配布へ広げた。 +**昇格の経路にも同じ欠けがあった(#1529)。** `pace: fast` / `auto` の昇格のプランは導入の確認をベースブランチの先頭で走らせ、 +確かめたコミットを承認資料へ載せる(#1457)。しかし `promote` ステップは CI が通った昇格の Pull Request の先端をそのまま +マージしていた。確認の後にベースブランチが進むと(別のスプリントのマージなど)、確かめていないコミットが MVV 判定の後に +本番チャネルへ入りえた。 + ## 仕様 ### 集約 @@ -173,6 +183,8 @@ git を読むだけで書かず、終了コードも出力も持たない。ブ | `record_approval(path, sha, by, at)` | 「承認したコミット」の行の直後へ `\| 承認の記録 \| <40 桁> を が に承認 \|` を書く(あれば置き換える)。`sha` が資料の承認したコミットと違えば書かずに `Unapproved` | | `record_all(approved, by, at)` | `{承認資料: SHA}` のそれぞれへ `record_approval` を打ち、`items`(`kind: approval`・`result: recorded` か `not_recorded`)を返す。`mvv-gate.py` が使う | | `compare_approved(root, approved, tip, allowed)` | 上の比較の順序で `Verdict` を返す | +| `verified_line(base, sha)` | 導入の確認の承認資料の確かめたコミットの行 `` - ベースブランチ の先頭: ``。導入の確認はこのコミットで走らせた `` を返す(形を作るだけ)。`release_lib/deploy.py` の `verify_markdown` がこれで書く | +| `verified_sha(path)` | 導入の確認の承認資料から確かめたコミットを小文字の 40 桁で返す。資料が無い・行が無い・行が 2 つ以上・`parse_sha` が通らなければ `ValueError` | | 型 | 欄 | | --- | --- | @@ -181,6 +193,80 @@ git を読むだけで書かず、終了コードも出力も持たない。ブ | `AllowedPR` | `number`・`head`・`commits`・`merge_commit` | | `Unapproved` | `ValueError` の子。`sha`(承認したコミット)と `recorded`(記録の SHA。無ければ `None`)。`release` は 10、`approve` は 1 に読み替える | +### 昇格の経路の照合(#1529) + +`pace: fast` / `auto` の昇格のプラン(`supervise_lib/delivery_templates.py` の `plan_promote`)は +`verify` → `prepare` → `mvv` → `note` → `promote` → `record` の順に流れ、`note` か `promote` が落ちたら `handoff` へ進む。 +`promote` ステップは `merged-steps.py promote ... --expect-head-from {state_dir}/work/approval-verify.md --gate-approved mvv` を打つ。 + +| 集約 | 持ち主(書き換えてよいもの) | 根 | 値オブジェクト | +| --- | --- | --- | --- | +| 導入の確認の承認資料 | `release_lib/deploy.py` の `verify_facts`(書く)。確かめたコミットの行の形は `lib/approved_commit.py` | `approval-verify.md` | 確かめたコミット(40 桁)・ベースブランチの名前・確認の終了コード | +| 昇格のマージ | `merged_lib/merge.py` の `promote` | 昇格の Pull Request | 先端(`headRefOid`)・照合する SHA(`expect_head`) | +| 昇格のプラン | `supervise_lib/delivery_templates.py` の `plan_promote` | プラン(エンティティはステップ) | 承認資料のパス(`VERIFY_MATERIAL`) | + +昇格のマージは導入の確認の承認資料をパスで参照し、中身を書き換えない。確かめたコミットの行は書き手(`verify-facts`)と +読み手(`merged-steps.py promote`)が `lib/approved_commit.py` の 1 か所を共有して決める。 + +常に成り立つ条件: + +| # | 条件 | 破れたときの扱い | +| --- | --- | --- | +| P1 | 照合する SHA を受けた `promote` は、昇格の Pull Request の先端がその SHA に等しいときだけ `gh pr merge` を呼び、`--match-head-commit` にその SHA を渡す | 読むたびに比べ、違えば `gh pr merge` を呼ばずに `items[].result: head_moved`(`head` と `expected` つき)で終了コード 1 | +| P2 | `--expect-head-from` を受けた `promote` は、資料から確かめたコミットを読めないまま照合を飛ばさない | `gh` を 1 度も呼ばずに終了コード 2(`EXIT_UNREADABLE`)で止まる | +| P3 | 確かめたコミットの行を書く形と読む形は `verified_line` / `verified_sha` の 1 か所が決める | 読めなければ P2 の扱い | +| P4 | `fast` / `auto` の `promote` ステップは、`mvv` ステップが材料に渡すのと同じパス(`VERIFY_MATERIAL`)を `--expect-head-from` に渡し、失敗は `handoff` へ進む | プランの組み立てのテストで落とす | +| P5 | `normal` の `promote` と `promote-approved` のコマンドは `--expect-head-from` を持たない | プランの組み立てのテストで落とす | + +```mermaid +sequenceDiagram + participant S as プラン(promote ステップ) + participant P as merge.promote + participant A as approved_commit + participant G as GreenWatch + participant GH as gh + S->>P: --expect-head-from approval-verify.md --gate-approved mvv + P->>A: verified_sha(資料) + alt 読めない + A-->>P: ValueError + P-->>S: StepError(2) + S->>S: handoff(by: mvv の記録を外して 10) + else 読めた(X) + A-->>P: X + P->>GH: 昇格の PR を探す(無ければ作る) + P->>G: merge_when_green(expect_head = X) + loop 読むたび + G->>GH: pr view(headRefOid) + alt 先端が X でない + G-->>S: head_moved(1) + S->>S: handoff(10) + end + end + P->>GH: pr merge --match-head-commit X + alt 拒まれた(直前に先端が動いた) + P-->>S: stopped(1) + S->>S: handoff(10) + else マージした + P-->>S: ok(0) + S->>S: record + end + end +``` + +ドメインイベントの発生元と受け手: + +| イベント | 発生元 | 受け手 | +| --- | --- | --- | +| 導入の確認がベースブランチの先頭 X で走り、承認資料に X を書いた | `verify` ステップ(`verify-facts`) | `mvv` ステップ(材料として読む)・`promote` ステップ(X を読む) | +| ベースブランチが X から Y へ進んだ | プランの外のマージ | `promote` ステップ(先端の Y として読む。`verify` と `prepare` の間に起きても同じ) | +| 昇格の Pull Request の先端を読み、X と比べた | `promote` ステップ(`GreenWatch` の 1 回の読み) | 一致なら本番チャネルへのマージ、違えば `handoff` | +| マージせずに止まり、`handoff` が MVV 判定の記録を外して承認ゲート 2 へ戻した | `promote`(非 0)→ `handoff` | 人(承認ゲート 2) | + +`promote` の失敗の種類(`head_moved` の 1・読めない 2・マージの拒否 1・衝突 4)で行き先を分けず、どれも `on_fail: handoff` が +承認ゲート 2 へ戻す。戻った後の続きは `development-workflow` の `references/pace.md` の承認ゲート 2 の行が正で、照合で止まった +(`head_moved`・資料を読めない)ときは `run <プラン> --from verify` から流し直す。`--from promote-approved` は照合を持たず、 +導入の確認も MVV 判定もしていない今の先端をマージするためである。 + ## データ・設定 ### 承認資料の欄 @@ -222,6 +308,21 @@ git を読むだけで書かず、終了コードも出力も持たない。ブ を載せる。待つ間に別の先端でマージされた PR へタグを打たないためである。一致すれば `gh pr merge --match-head-commit ` でマージする。渡さないときは緑を確かめた先端を `--match-head-commit` に渡す。 +### `merged-steps.py promote --expect-head-from <導入の確認の承認資料>` + +`merged-steps.py promote --head <ベースブランチ> --base <本番チャネル> [--prepare] [--gate-approved user|mvv] [--expect-head-from <資料>]`。 +`--expect-head-from` は任意で、渡すと最初に `verified_sha` で確かめたコミットを読み、`merge-when-green --expect-head` と同じ照合に使う。 + +| 場合 | 終了コード | 結果 | +| --- | --- | --- | +| 先端が確かめたコミットのまま CI が通った | 0 | 今と同じ。`items[]` に `{"kind": "pr", "result": "merged", "head": , "gate_approved": "mvv"}` | +| 先端が確かめたコミットでない(待つ間に変わった・別の先端でマージ済みを含む) | 1 | `status: stopped`・`items[]` に `{"kind": "pr", "name": "#<番号>", "result": "head_moved", "head": , "expected": }`。`gh pr merge` は呼ばない | +| 最後の読みとマージの間に先端が変わった | 1 | `--match-head-commit X` で GitHub がマージを拒み、既存の「`gh pr merge --admin` が失敗」になる | +| 資料が無い・確かめたコミットの行が無いか 2 つ以上・40 桁の 16 進でない | 2 | `status: stopped`。昇格の Pull Request を探さず・作らず、`gh` を呼ばない | + +引数を渡さない呼び出し(`normal` の `promote` / `promote-approved`、手で打つ既存の呼び出し)は変わらない。`--prepare` と一緒に +渡したときも先に読み(読めなければ 2)、読めればマージへ進まない今の振る舞いのまま終える。 + ### 本番のリリースプラン `supervise.py new release --channel prod` の `release` のステップは、`--approval <承認資料>`(MVV 判定が読むのと同じ @@ -242,12 +343,20 @@ git を読むだけで書かず、終了コードも出力も持たない。ブ | 判定できないときは承認ゲートで止まる(I6) | 通すと承認の外の変更を見落としうる。失敗(1)にすると、本番のリリースプランの judge が直す(fix)か同じステップをもう一度(retry)を選び、人の承認を経ずに打ち直されうる | | `--mvv` 付きのプランは承認ゲートで `handoff` へ進む(I7) | MVV 判定が承認したのは承認資料の中身で、承認の外のコミットは判定の材料に入っていない。人が外のコミットを見てから承認し直す。`gate_next` を置かないと、10 を受けて `next` の `verify` へ進み、配っていない版の導入を確かめに行く | | 比べる関数を `lib/` に置く | 昇格の経路にも同じ欠けがあり、`release-steps.py` の中に置くと同じ役割の関数が 2 つになる。`allowed` を空にすれば promote の判定になると見込んだが、#1529 は木が等しい別のコミットを通すため採らず、昇格は `--expect-head` の照合を使う | +| 昇格の照合に `--expect-head` を使い、`compare_approved` を使わない(#1529) | `--expect-head` は CI を待つ間の読みのたびに先端を比べ、マージを `--match-head-commit` で比べた先端に固定する。待つ間の変化と、最後の読みとマージの間の変化の両方を既存のコードのまま塞げる。`compare_approved` に `allowed` を 0 個で渡す形は手元の git を 1 度読むだけで待つ間の変化を見ず、木が等しい別のコミット(中身の無いマージなど)を通す | +| `promote` は SHA ではなく承認資料のパスを受けて自分で読む(#1529) | プランのコマンドで `--expect-head "$(…)"` と SHA を置換して渡すと、読めなかったときに空の値が渡り、`_check_expected` は空を「照合しない」と扱う。パスを受けて中で読めば、読めないことを `promote` 自身が失敗として返せる(P2)。SHA を出す副命令を足してプランで繋ぐ形は、ステップが増え空の値の経路も残る | +| 確かめたコミットの行の形を `lib/approved_commit.py` に置き、`verify_markdown` もそれで書く(#1529) | 読み手だけを足すと、書き手の文言を直したときに読み手が黙って読めなくなる。承認資料に `\| 承認したコミット \|` の欄を足して `material_sha` で読む形は、`mvv-gate.py` がその欄を見て承認の記録を書き足すようになり、承認資料の中身を変える | +| プランは `mvv` と同じ定数 `VERIFY_MATERIAL` を `promote` に渡す(#1529) | MVV 判定が読んだ資料と照合に使う資料が同じファイルになる。資料を書くのは `verify` ステップだけで、`--from verify` で打ち直すと資料も作り直される。プランを組む時点の先端や `origin/<ベースブランチ>` を引き直すと、確認の後に進んだ先端を拾い照合の意味が無くなる | +| `promote` の失敗の種類で行き先を分けない(#1529) | どれも `on_fail: handoff` が承認ゲート 2 へ戻す。`on_exit` で種類ごとに行き先を変えると、戻った後の人の手順が種類ごとに分かれる。代わりに人が選ぶ続き(`--from verify`)を `pace.md` に書く | ## 既知の制約 | 項目 | 内容 | | --- | --- | | 配布の PR のコミット数 | `gh pr view --json commits` は 250 件までを返す。超える配布の PR では I3 が外のコミットと誤り、止まる側に倒れる。配布の PR は bump・changelog・説明文・消費の記録の数件である | +| `normal` の昇格の照合 | 利用者が承認ゲート 2 で見た先端と `promote-approved` がマージする先端は照合しない。同じ食い違いは起きうるが、照合の材料(承認したときの先端を残す記録)が `normal` の経路に無い | +| 照合で止まった後の続き | 人が `--from promote-approved` を選ぶと、照合を持たない `promote-approved` が確かめていない先端をマージする。防ぐのは `pace.md` の続きの行だけで、`handoff` の文言(「昇格のマージ」)も先端が動いたことを示さない。人は `promote` の出力の `head_moved` を見て選ぶ | +| 実機での昇格 | 本番チャネルへのマージを伴うため、手元では `gh` を差し替えたテストでだけ確かめた。実機はリリース後テストで、経路 `promote` を持つリポジトリが `fast` / `auto` で通るときに見る | | rebase で入れた配布の PR の `mergeCommit` | 実物で確かめていない。`mergeCommit` は承認の外のコミットの一覧から除くのにだけ使い、木の比較には使わない。違っていても、patch-id で除けないものが一覧に余分に並ぶだけで、通す・止めるの向きは止まる側である | ## テスト観点 @@ -269,6 +378,11 @@ git を読むだけで書かず、終了コードも出力も持たない。ブ | 本番のリリースプラン(I7) | 本番の `release` のステップが `--approval <承認資料>` を持ち、`--mvv` 付きは `gate_next: handoff`、無しは `end` であること。開発版のステップに `--approval` も `gate_next` も無いこと(`test_supervise_new.py`) | | 退行しない | `--channel dev` の引数・`items`・`metrics` が変わらないこと。`--channel prod` で配布の PR がマージ済みのとき、打ち直しで続きから進み、一致すれば `ok` になること | | `allowed` が空の形 | `allowed` を空にしたとき、先端の木が承認したコミットの木と等しいかで判定すること | +| 昇格: 確かめたコミットならマージする(P1) | 先端が資料の X のまま CI が通ると、`promote --expect-head-from` が `gh pr merge … --match-head-commit X` を 1 度呼んで 0 で終わること | +| 昇格: 先端が進んだらマージしない(P1) | 最初の読みで先端が Y なら `gh pr merge` を呼ばずに `head_moved`(`head: Y`・`expected: X`)と終了コード 1。1 回目の読みは X・2 回目で Y でも同じ。待つ間に Y で既にマージされた PR も成功にしないこと | +| 昇格: 資料を読めなければ何もしない(P2) | 資料が無い・行が無い・行が 2 つ・SHA が 39 桁のそれぞれで、`gh` を 1 度も呼ばずに終了コード 2 になること | +| 昇格のプラン(P4・P5) | `fast` / `auto` の `promote` ステップが `--expect-head-from` と `mvv` の `--material` に同じ `VERIFY_MATERIAL` を持ち、`on_fail` が `handoff` で、非 0 で `handoff` へ進み終了コード 10 になること。`normal` の `promote` と `promote-approved` が `--expect-head-from` を含まないこと。`base` と `production_branch` を `develop` / `main` 以外で組むとその名前だけが現れること(`test_supervise_pace_verify.py`・`test_supervise_new.py`) | +| 書き手と読み手(P3) | `verify_markdown` の出力を `verified_sha` で読むと書いた SHA が返り、出力の文字列が変わらないこと。`merged-steps.py promote --help` が `--expect-head-from` を出すこと(`test_approved_commit.py`・`test_sprint_close_merge_green.py`) | ## 関連リンク @@ -278,8 +392,10 @@ git を読むだけで書かず、終了コードも出力も持たない。ブ - [`release_lib/approval.py`](../../plugins/ndf/scripts/release_lib/approval.py) - [`merged_lib/checks.py`](../../plugins/ndf/scripts/merged_lib/checks.py)(`--expect-head` の比較) - [`supervise_lib/release_templates.py`](../../plugins/ndf/scripts/supervise_lib/release_templates.py) +- [`supervise_lib/delivery_templates.py`](../../plugins/ndf/scripts/supervise_lib/delivery_templates.py)(昇格のプラン `plan_promote`)・[`merged_lib/merge.py`](../../plugins/ndf/scripts/merged_lib/merge.py)(`promote`)・[`release_lib/deploy.py`](../../plugins/ndf/scripts/release_lib/deploy.py)(`verify_markdown`) +- `development-workflow` の [references/pace.md](../../plugins/ndf/skills/development-workflow/references/pace.md)(昇格のプランと承認ゲート 2 の続き) - [用語集](../glossary.md) - [版と配布](../versioning-and-distribution.md) - [承認資料の「含む PR」](ndf-release-approval-included-prs.md)(直前の正式版から後に入った PR の集め方) - [後片付けとスプリント課題を閉じる時点](ndf-cleanup-and-bundle-closing.md)(本番の配布の後の退避先の回収) -- 課題 [#815](https://github.com/devbasex/ai-plugins/issues/815)、関連する昇格の経路の課題 [#1529](https://github.com/devbasex/ai-plugins/issues/1529) +- 課題 [#815](https://github.com/devbasex/ai-plugins/issues/815)、昇格の経路の照合の課題 [#1529](https://github.com/devbasex/ai-plugins/issues/1529)(由来 #1457。手動反映の本番系は #1454) diff --git a/issues/issue-1529-design.md b/issues/issue-1529-design.md deleted file mode 100644 index 743099f02..000000000 --- a/issues/issue-1529-design.md +++ /dev/null @@ -1,260 +0,0 @@ -# 配布: fast / auto の昇格で、導入の確認の後にベースブランチが進むと、確かめていないコミットが本番チャネルへ入る → 確かめたコミットのときだけマージし、違えば承認ゲート 2 へ戻る(#1529) - -## 目的 - -- **何が壊れているか**: `pace: fast` / `auto` の昇格のプランは、導入の確認を走らせたコミットと照合せずに、CI が通った昇格の Pull Request の先端をマージする。確認の後にベースブランチが進むと、確かめていないコミットが MVV 判定の後にそのまま本番チャネルへ入る -- **誰が困るか**: 承認ゲート 2 を MVV 判定に任せたリポジトリの利用者(承認の材料と本番へ届く中身が食い違う)と、その本番の利用者 -- **直すと何が成り立つか**: 本番チャネルへ入るのは、MVV 判定が読んだ導入の確認の記録のコミットに限る。食い違えばマージせずに承認ゲート 2 で人へ戻る - -## 適用範囲 - -- **働く範囲**: 配布先のリポジトリでも働く(`.ndf/worktree.json` の `delivery` で経路 `promote` を組み、`.ndf/pace.json` で `fast` / `auto` を選んだリポジトリ) -- **プロジェクトごとに違うもの**: ベースブランチと本番チャネルの名前は、今と同じく引数(`--head` / `--base`)と宣言から読む。照合の材料は承認資料のパスを引数で受ける。設定のキーは足さない -- **当たるモード**: `standard`(承認ゲート 2 と配布に当たる安定版の変更) - -## あるべき姿の根拠 - -| 根拠 | 種類 | 何を示すか | -| --- | --- | --- | -| `docs/specifications/ndf-release-approved-commit.md`(#815) | 実測(本番の配布の既存の照合) | 本番の配布は、承認したコミットと配布の時点の先端を比べ、違えば止まって人へ戻す | -| #1454 の手動反映の本番系の手順 | 実測(既存の手順) | 先端が変わったら承認ゲート 2 からやり直す | -| `release_lib/deploy.py` の `verify_markdown` の出力を `^- ベースブランチ \S+ の先頭: \`(...)\`。導入の確認はこのコミットで走らせた$` で読んだ結果(2026-10-24) | 実測 | 導入の確認の承認資料から 40 桁の SHA を 1 つ読める。手動反映の承認資料(`deploy_markdown`)の行には当たらない | - -要求と受け入れ条件は #1529 の本文にある(コピーは [issue-1529-requirements.md](issue-1529-requirements.md))。この文書は「どう作るか」だけを扱う。 - -## ドメインモデル - -### コンテキスト - -| コンテキスト | 何の語が 1 つの意味に決まるか | -| --- | --- | -| NDF のリリース(`ndf-release`) | 昇格のプラン・昇格の Pull Request・承認したコミット・確かめたコミット | -| NDF の工程(`ndf-workflow`) | 導入の確認・承認資料・本番チャネル・承認ゲート 2 | - -**2 つは共有カーネルの関係にある。** 導入の確認の承認資料は `ndf-workflow` の語だが、その中の確かめたコミットの行の形は、 -書き手(`release-steps.py verify-facts`)と読み手(`merged-steps.py promote`)が `lib/approved_commit.py` の 1 か所を共有して決める。 -同じリポジトリの同じ担い手が両方を持ち、共有する部分は 1 行の形と読み取りの関数だけに保てる。 - -### 集約 - -| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | -| --- | --- | --- | --- | --- | -| 導入の確認の承認資料 | `release_lib/deploy.py` の `verify_facts`(書く)。行の形は `lib/approved_commit.py` | 承認資料(`approval-verify.md`) | — | 確かめたコミット(40 桁の SHA)・ベースブランチの名前・確認の終了コード | -| 昇格のマージ | `merged_lib/merge.py` の `promote` | 昇格の Pull Request | — | 先端(`headRefOid`)・照合する SHA(`expect_head`) | -| 昇格のプラン | `supervise_lib/delivery_templates.py` の `plan_promote` | プラン | ステップ(`verify`・`prepare`・`mvv`・`note`・`promote`・`handoff` ほか) | 承認資料のパス(`VERIFY_MATERIAL`) | - -昇格のマージは、導入の確認の承認資料をパスで参照し、中身を書き換えない。 - -### 不変条件 - -| # | 集約 | 条件 | 破れたときの扱い | -| --- | --- | --- | --- | -| I1 | 昇格のマージ | 照合する SHA を受けた `promote` は、昇格の Pull Request の先端がその SHA に等しいときだけ `gh pr merge` を呼び、`--match-head-commit` にその SHA を渡す | 読むたびに比べ、違えば `gh pr merge` を呼ばずに `items[].result: head_moved`(`head` と `expected` つき)で終了コード 1 | -| I2 | 昇格のマージ | `--expect-head-from` を受けた `promote` は、資料から確かめたコミットを読めないまま照合を飛ばさない | `gh` を 1 度も呼ばずに `StepError`(終了コード 2)で止まる | -| I3 | 導入の確認の承認資料 | 確かめたコミットの行を書く形と読む形は同じ 1 か所が決め、`verify_markdown` の出力は今と同じ文字列である | 読めなければ I2 の扱い | -| I4 | 昇格のプラン | `fast` / `auto` の `promote` ステップは、`mvv` ステップが材料に渡すのと同じパス(`VERIFY_MATERIAL`)を `--expect-head-from` に渡し、失敗は `handoff` へ進む | プランの組み立てのテストが落ちる | -| I5 | 昇格のプラン | `normal` の `promote` と `promote-approved` のコマンドは今と同じで、`--expect-head-from` を持たない | プランの組み立てのテストが落ちる | - -### ドメインイベント - -| # | イベント | 発生元 | 受け手 | -| --- | --- | --- | --- | -| E1 | 導入の確認がベースブランチの先頭 X で走り、承認資料に X を書いた | `verify` ステップ(`verify-facts`) | `mvv` ステップ(材料として読む)・`promote` ステップ(X を読む) | -| E2 | 昇格の Pull Request を用意し、承認資料を書いた | `prepare` ステップ | `mvv` ステップ | -| E3 | MVV 判定が 2 つの承認資料を読み、承認ゲート 2 を通した | `mvv` ステップ | `note` ステップ | -| E4 | 判定の記録を昇格の Pull Request へ書いた | `note` ステップ | `promote` ステップ | -| E5 | ベースブランチが X から Y へ進んだ | プランの外のマージ | `promote` ステップ(E6 で先端の Y として読む) | -| E6 | 昇格の Pull Request の先端を読み、X と比べた | `promote` ステップ(`GreenWatch` の 1 回の読み) | E7 または E8 | -| E7 | 先端が X のまま CI が通り、X を本番チャネルへマージした | `promote` ステップ | `record` ステップ | -| E8 | マージせずに止まり、`handoff` が MVV 判定の記録を外して承認ゲート 2 へ戻した | `promote` ステップ(非 0)→ `handoff` ステップ | 人(承認ゲート 2) | -| E9 | リリース記録を昇格の Pull Request へ書いた | `record` ステップ | まとめの `close`(`{queue_pr:promote}`) | - -### 用語 - -| 用語 | 意味 | 用語集への反映 | -| --- | --- | --- | -| 確かめたコミット | 導入の確認を走らせた、ベースブランチの先頭のコミットの SHA(40 桁)。導入の確認の承認資料に書かれ、`fast` / `auto` の昇格でマージしてよい先端はこのコミットに限る | 追加(`ndf-release`) | - -## 機能一覧 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| F1 | `fast` / `auto` の昇格で、確かめたコミットのときだけ本番チャネルへマージする | 昇格のプランを流す supervisor(承認ゲート 2 を MVV 判定に任せた利用者) | -| F2 | 先端が確かめたコミットと違う・確かめたコミットを読めないとき、マージせずに承認ゲート 2 へ戻す | 同上。戻った後は承認ゲート 2 の利用者 | -| F3 | `merged-steps.py promote` を手で打つときに、導入の確認の承認資料を渡して照合させる | 手で昇格を進める担い手 | - -## 構成要素 - -| 要素 | 責務 | 変更 | -| --- | --- | --- | -| `lib/approved_commit.py` | 確かめたコミットの行の形(`verified_line(base, sha)`)と、その行を承認資料から読む関数(`verified_sha(path)`。資料が無い・行が無い・行が 2 つ以上・40 桁でないなら `ValueError`)を持つ。git も gh も呼ばない | 足す | -| `release_lib/deploy.py` の `verify_markdown` | 確かめたコミットの行を `verified_line` で作る。出力は今と同じ | 変える | -| `merged-steps.py` | `promote` の副命令に `--expect-head-from <承認資料>` を足し、モジュールの docstring と `--help` に照合と失敗の形を書く | 変える | -| `merged_lib/merge.py` の `promote` | `--expect-head-from` があれば、最初に `verified_sha` で読み、`a.expect_head` に入れてから今までどおり `merge_when_green` を呼ぶ。読めなければ `StepError`(2) | 変える | -| `merged_lib/checks.py` の `GreenWatch._check_expected` と `merge.py` の `merge_when_green` | 読むたびに先端と `expect_head` を比べ、違えば `head_moved` で止める。マージは `--match-head-commit` で比べた先端に固定する | 変えない(呼ぶだけ) | -| `supervise_lib/delivery_templates.py` の `plan_promote` | `fast` / `auto` の `promote` ステップのコマンドに `--expect-head-from {VERIFY_MATERIAL}` を足す。`on_fail: handoff` は今のまま | 変える | -| `supervise_lib/verify_steps.py` の `handoff_step` | `by: mvv` の記録を外し、終了コード 10 を返す | 変えない | -| `skills/development-workflow/references/pace.md` | 昇格のプランの説明に、`promote` が確かめたコミットと照合し、違えば `handoff` で承認ゲート 2 へ戻ることを書く。承認ゲート 2 の続きの行に「`promote` が照合で止まった(`head_moved`・資料を読めない)ときは `run <プラン> --from verify` から流し直す。`--from promote-approved` は照合を持たず、導入の確認も MVV 判定もしていない今の先端をマージする」を足す | 変える | -| `docs/glossary/glossary.json` と `docs/glossary.md` | 「確かめたコミット」を足し、`glossary.py render` で作り直す | 変える | - -パスは `plugins/ndf/scripts/` からの相対(`pace.md` は `plugins/ndf/` から、用語集はリポジトリの直下から)。 - -```mermaid -graph LR - subgraph プラン["昇格のプラン(plan_promote)"] - V[verify] --> P[prepare] --> M[mvv] --> N[note] --> PR[promote] - PR -- 非 0 --> H[handoff] - PR -- 0 --> R[record] - end - V -- 書く --> MAT[(approval-verify.md)] - M -- 読む --> MAT - PR -- --expect-head-from --> CLI[merged-steps.py promote] - CLI --> PRO[merge.promote] - PRO -- verified_sha --> AC[approved_commit] - AC -- 読む --> MAT - V --> DEP[deploy.verify_markdown] - DEP -- verified_line --> AC - PRO -- expect_head --> MWG[merge_when_green] - MWG --> GW[GreenWatch._check_expected] - H --> HS[handoff_step のコマンド] -``` - -図には文書(`pace.md`・用語集)を含めない。 - -## 入出力の契約 - -### `merged-steps.py promote` - -| 項目 | 書くこと | -| --- | --- | -| 名前 | `merged-steps.py promote --head <ベースブランチ> --base <本番チャネル> [--prepare] [--gate-approved user\|mvv] [--expect-head-from <承認資料>]` | -| 入力(足す) | `--expect-head-from`: 任意。`release-steps.py verify-facts` が書いた導入の確認の承認資料のパス。読んだ確かめたコミットを `merge-when-green --expect-head` と同じ照合に使う | -| 出力(成功) | 今と同じ。`items[]` に `{"kind": "pr", "result": "merged", "head": , "gate_approved": "mvv"}`、終了コード 0 | -| 失敗の形(足す 1) | 先端が X でない(待つ間に変わった・別の先端でマージ済みを含む): `status: stopped`・`items[]` に `{"kind": "pr", "name": "#<番号>", "result": "head_moved", "head": , "expected": }`・終了コード 1。`gh pr merge` は呼ばない。最後の読みとマージの間に先端が変われば、`--match-head-commit X` で GitHub がマージを拒み、既存の「`gh pr merge --admin` が失敗」(1)になる | -| 失敗の形(足す 2) | 承認資料が無い・確かめたコミットの行が無いか 2 つ以上・40 桁の 16 進でない: `status: stopped`・終了コード 2。昇格の Pull Request を探さず・作らず、`gh` を呼ばない | -| 互換性 | 引数を渡さない呼び出し(`normal` の `promote` / `promote-approved`、手で打つ既存の呼び出し)は今と同じ。`--prepare` と一緒に渡されたときも先に読み(読めなければ 2)、読めればマージへ進まない今の振る舞いのまま終える | - -### `lib/approved_commit.py` - -| 名前 | 入力 | 出力 | 失敗の形 | -| --- | --- | --- | --- | -| `verified_line(base, sha)` | ベースブランチの名前・40 桁の SHA | `- ベースブランチ の先頭: \`\`。導入の確認はこのコミットで走らせた` | 無し(形を作るだけ) | -| `verified_sha(path)` | 承認資料のパス | 小文字の 40 桁の SHA | `ValueError`(資料が無い・行が無い・行が 2 つ以上・`parse_sha` が通らない) | - -## 処理の流れ - -```mermaid -sequenceDiagram - participant S as プラン(promote ステップ) - participant P as merge.promote - participant A as approved_commit - participant G as GreenWatch - participant GH as gh - S->>P: --expect-head-from approval-verify.md --gate-approved mvv - P->>A: verified_sha(資料) - alt 読めない - A-->>P: ValueError - P-->>S: StepError(2) - S->>S: handoff(記録を外して 10) - else 読めた(X) - A-->>P: X - P->>GH: 昇格の PR を探す(無ければ作る) - P->>G: merge_when_green(expect_head = X) - loop 読むたび - G->>GH: pr view(headRefOid) - alt 先端が X でない - G-->>S: head_moved(1) - S->>S: handoff(記録を外して 10) - end - end - P->>GH: pr merge --match-head-commit X - alt 拒まれた(直前に先端が動いた) - P-->>S: stopped(1) - S->>S: handoff(記録を外して 10) - else マージした - P-->>S: ok(0) - S->>S: record - end - end -``` - -## 非機能の実現方式 - -| 大項目 | 実現方式 | -| --- | --- | -| 運用・保守性 | 照合は既存の `--expect-head`(`GreenWatch._check_expected` と `--match-head-commit`)を呼ぶだけにし、比べる処理を書き足さない。承認資料の行の形は `lib/approved_commit.py` の 1 か所が書き手と読み手の両方に渡す | -| セキュリティ | 確かめたコミットを読めないときは、`gh` を呼ぶ前に非 0 で止まる。SHA をシェルの置換で渡さないため、空の値で照合が抜ける経路が無い | - -## 決定の記録 - -### 決定 1: 確かめたコミットと違う先端を 1 つも通さないため、照合に `--expect-head` を使い `compare_approved` を使わない - -`--expect-head` は、CI を待つ間の読みのたびに先端を比べ、マージを `--match-head-commit` で比べた先端に固定する。待つ間に先端が変わる場合(受け入れ条件 3)と、最後の読みとマージの間に変わる場合の両方を、既存のコードのまま塞げる。 - -`approved_commit.compare_approved` に `allowed` を 0 個で渡す形は採らない。手元の git を 1 度読むだけで待つ間の変化を見ず、木が等しい別のコミット(中身の無いマージなど)を通すため、受け入れ条件 2 の「Y をマージしない」を満たさない。 - -根拠: Value 6 / Vision(MVV 版 2) - -### 決定 2: 照合を飛ばす経路を作らないため、`promote` は SHA ではなく承認資料のパスを受けて自分で読む - -プランのコマンドで `--expect-head "$(…)"` のように SHA を置換して渡すと、読めなかったときに空の値が渡り、`_check_expected` は空の値を「照合しない」と扱う。`promote` が `--expect-head-from <承認資料>` を受けて中で読めば、読めないことを `promote` 自身が失敗として返せる(I2)。 - -`release-steps.py` に SHA を出す副命令を足してプランで繋ぐ形は採らない。ステップが 1 つ増え、空の値の経路も残る。 - -根拠: Value 4 / Vision(MVV 版 2) - -### 決定 3: 書き手と読み手の食い違いを防ぐため、確かめたコミットの行の形を `lib/approved_commit.py` に置き、`verify_markdown` もそれで書く - -読み手だけを足すと、行の文言を書き手の側で直したときに読み手が黙って読めなくなる(I2 で止まるが、そのたびに承認ゲート 2 へ落ちる)。`approved_commit.py` は承認資料の欄の名前と SHA の形の検査を既に持ち、`merged_lib` と `release_lib` の両方から読める。 - -承認資料に `| 承認したコミット | |` の欄を足して `material_sha` で読む形は採らない。`mvv-gate.py` がその欄を見て承認の記録を書き足すようになり、承認資料の中身を変えることになる(要求の「含まない」)。 - -根拠: Value 6(MVV 版 2) - -### 決定 4: 材料が MVV 判定と食い違わないため、プランは `mvv` と同じ定数 `VERIFY_MATERIAL` を `promote` に渡す - -同じ定数を両方のステップのコマンドに入れれば、MVV 判定が読んだ資料と照合に使う資料が同じファイルになる(受け入れ条件 6)。資料を書くのは `verify` ステップだけで、`--from verify` で打ち直すと資料も作り直される。 - -プランを組む時点の先端や `origin/<ベースブランチ>` を `promote` が引き直す形は採らない。確認の後に進んだ先端を拾い、照合の意味が無くなる。 - -根拠: Vision / Value 2(MVV 版 2) - -### 決定 5: 食い違ったときの復帰先を今の手順に揃えるため、`promote` の失敗の種類で行き先を分けない - -`head_moved`(1)・読めない(2)・マージの拒否(1)・衝突(4)のどれも、今の `on_fail: handoff` が承認ゲート 2 へ戻す(受け入れ条件 4・5)。`on_exit` で種類ごとに行き先を変えると、戻った後の人の手順(`--from promote-approved` か `--from verify`)が種類ごとに分かれ、要求の前提 4(続きは今の手順のまま)から外れる。 - -行き先を分けない代わりに、人が選ぶ続きを `pace.md` に書く。`promote` が照合で止まった(`head_moved`・資料を読めない)ときは `--from verify` から流し直す。`promote-approved` のステップは `--expect-head-from` を持たず(この設計が照合を足すのは `promote` のステップだけ)、`--from promote-approved` は導入の確認も MVV 判定もしていない今の先端をマージするためである。`handoff` の文言(「昇格のマージ」)は先端が動いたことを示さないため、人は `promote` の出力の `head_moved` を見て選ぶ。続きの手順はどちらも今あるもので、前提 4 のとおり手順そのものは変えない。 - -根拠: C4 / Value 2 / Value 6(MVV 版 2) - -## テスト設計 - -| 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | -| --- | --- | --- | -| 1 / I1 | 先端が資料の X のまま CI が通ると、`promote --expect-head-from` は `gh pr merge … --match-head-commit X` を 1 度呼んで 0 で終わる | 照合する SHA を `--match-head-commit` に渡さないように壊す | -| 2 / I1 | 最初の読みで先端が Y なら、`gh pr merge` を呼ばずに `head_moved`(`head: Y`・`expected: X`)と終了コード 1 を出す | `promote` が `expect_head` を設定しないように壊す | -| 3 / I1 | 1 回目の読みは X・2 回目の読みで Y になると、Y をマージせず 2 と同じ結果になる | 比較を最初の読みだけにするように壊す | -| 3 / I1 | 待つ間に Y で既にマージされた PR を、成功にせず `head_moved` で止める | 既存の `test_merge_when_green_expect_head_stops_when_merged_at_other_head` と同じ形を `promote` で通す | -| 4 / I4 | `fast` / `auto` のプランの `promote` ステップは `--expect-head-from` と `mvv` の `--material` に同じ `VERIFY_MATERIAL` を持ち、`on_fail` が `handoff` である | `promote` のコマンドから資料のパスを外すか、別のパスを渡すように壊す | -| 4 | プランを流して `promote` が非 0 で終わると、`handoff` へ進み終了コード 10(承認ゲート)になる | `promote` の `on_fail` を外すように壊す | -| 5 / I2 | 資料が無い・行が無い・行が 2 つ・SHA が 39 桁のそれぞれで、`gh` を 1 度も呼ばずに終了コード 2 で終わる | 読めないときに `expect_head` を空のまま進めるように壊す | -| 6 / I4 | 上の 4 / I4 の行で縛る(`mvv` と `promote` が同じパスを持つ) | `promote` だけ別の資料のパスを持たせる | -| 7 / I5 | `normal` のプランの `promote` と `promote-approved` のコマンドが今の文字列と等しく、`--expect-head-from` を含まない | `normal` の分岐にも引数を足すように壊す | -| 8 | `fast` / `auto` のプランを `base` と `production_branch` に `develop` / `main` 以外の名前で組むと、`promote` のコマンドにその名前だけが現れる | 名前を定数で埋め込むように壊す | -| 9 | `merged-steps.py promote --help` が `--expect-head-from` を出す | 引数を足さないように壊す(`pace.md` の文言は照合しない) | -| I3 | `verify_markdown` の出力を `verified_sha` で読むと、書いた SHA が返る。出力は今の文字列と等しい | 書き手の行の文言だけを変えるように壊す | - -## 設計の結果 - -| 課題 | 扱い | 取り込み先 | 触るファイル | -| --- | --- | --- | --- | -| #1529 | 実装する | — | `plugins/ndf/scripts/lib/approved_commit.py`、`plugins/ndf/scripts/release_lib/deploy.py`、`plugins/ndf/scripts/merged-steps.py`、`plugins/ndf/scripts/merged_lib/merge.py`、`plugins/ndf/scripts/supervise_lib/delivery_templates.py`、`plugins/ndf/skills/development-workflow/references/pace.md`、`plugins/ndf/scripts/tests/`、`docs/glossary/glossary.json`、`docs/glossary.md` | - -## 未確認のまま残ること - -| 項目 | 内容 | -| --- | --- | -| `normal` の昇格の照合(要求の未決 1) | 利用者が承認ゲート 2 で見た先端と `promote-approved` がマージする先端の照合は、この設計に入れない。同じ食い違いは起きうるが、照合の材料(承認したときの先端を残す記録)が `normal` の経路に無い。別の課題にするかを承認ゲート 1 で決める | -| 照合で止まった後の続き | `promote` が照合で止まった後に人が `--from promote-approved` を選ぶと、照合を持たない `promote-approved` が確かめていない先端をマージする。これを防ぐのは `pace.md` の続きの行(`--from verify` から流し直す)だけで、テストでは縛らない(テスト設計の 9 のとおり `pace.md` の文言は照合しない)。`handoff` の文言も先端が動いたことを示さない | -| 実機での昇格 | 本番チャネルへのマージを伴うため、手元では `gh` を差し替えたテストでだけ確かめる。実機はリリース後テストで、経路 `promote` を持つリポジトリが `fast` / `auto` で通るときに見る | -| 既存のテストの形 | `test_supervise_new.py` の `test_plan_promote_with_mvv_judges_before_merging` は `promote` のコマンドが `--gate-approved mvv` で終わることを見ている。引数を足す位置によってはこの検査を直す | diff --git a/issues/issue-1529-requirements.md b/issues/issue-1529-requirements.md deleted file mode 100644 index d750e0a03..000000000 --- a/issues/issue-1529-requirements.md +++ /dev/null @@ -1,167 +0,0 @@ -# #1529: 昇格のプランが、導入の確認をしたコミットと違う先頭を本番チャネルへマージしうる - -正は課題の本文(#1529)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。 - -## 何を見つけたか - -`pace: auto` / `fast` の昇格のプラン(`supervise_lib/delivery_templates.py` の `plan_promote`)の `promote` ステップ(`merged-steps.py promote --gate-approved mvv`)は、CI が通った昇格の Pull Request の先頭(`merged_lib/merge.py` の `merge_when_green` の `--match-head-commit watch.last_sha`)をマージする。#1457 の設計では、導入の確認(`.ndf/pace.json` の `<節>.verify`)をベースブランチの先頭で走らせ、確かめたコミットを承認資料に載せる。しかし確認の後にベースブランチが進むと(別のスプリントのマージなど)、昇格の Pull Request の先頭も進み、確認していないコミットが MVV 判定の後にそのまま本番チャネルへマージされる。 - -## どこで見つけたか - -#1457 の設計(`issues/old/issue-1457-design-decisions.md` の決定 7)。`plugins/ndf/scripts/merged_lib/merge.py` の `merge_when_green`・`promote`。 - -## なぜこの変更の範囲外なのか - -#1457 の受け入れ条件は、確認を走らせることと、確かめたコミットを承認資料へ載せること(AC5)までを求め、確かめたコミットとマージするコミットの照合は求めていない。照合するには、確認のステップが書いたコミットを `promote` のステップへ渡す仕組みが要る。根拠: Value 1 / Value 3(MVV 版 2) - -## 直さないと何が起きるか - -`delivery` で経路 `promote` を組むリポジトリの `auto` / `fast` で、承認ゲート 2 の MVV 判定が読んだ確認の記録と、本番チャネルへ入るコミットが食い違いうる。手動反映の本番系(#1454)は「先頭が変わったら承認ゲート 2 からやり直す」を手順で求めているが、昇格の経路には同じ照合が無い。 - -## 由来 - -issue #1457 - - -## 追記(2026-10-05、スプリント m815・PR #1763) - -#815(10.17.62)が本番の配布(`release --channel prod`)に、承認したコミットと配布の時点の先端を比べる判定を入れた。比べる関数は `plugins/ndf/scripts/lib/approved_commit.py` に置き、昇格の経路からも呼べる形にしてある(#815 の確定仕様 `docs/specifications/ndf-release-approved-commit.md` の「決定の理由」。`allowed` を 0 個にすれば「先端の木が承認したコミットの木と等しいか」の判定になる)。#815 の要求の前提 5 が「promote を直すのは別の課題にする」としたのは、この課題である。 - -`merged-steps.py merge-when-green` の `--expect-head `(`merged-steps.py:471`、照合は `merged_lib/checks.py:310`)は、PR の先端がその SHA のときだけマージし、違えば `result: head_moved` で止まる。これを渡すのは本番の配布(`release-steps.py:664`)だけである。`promote` の副命令はこの引数を持たないが、`merged_lib/merge.py:300` の `promote` は同じ引数のまま `merge_when_green` を呼び、照合は `expect_head` を読む。`promote` に同じ引数を足し、昇格のプランの `promote` ステップ(`supervise_lib/delivery_templates.py` の `plan_promote`、142 行の `--gate-approved mvv`)へ確認の記録の SHA を渡せば、確かめたコミットとマージするコミットの照合になる。 - ---- - -# 要求と受け入れ条件 - -**この節より上は起票の記録で、書き換えない。** 何を満たすかはこの節から下が持つ(どう作るかは設計が持つ)。 - -## 依頼(原文) - -> #1529。人へ問わずに進め、決められない点は前提か未決として課題の本文へ書く。 - -(書き手: AI の起動指示、日付: 2026-10-24。利用者の指示の原文としては扱わない) - -上の「何を見つけたか」〜「由来」(書き手: 起票者。#1457 の設計から `out-of-scope` で起票、日付: 2026-09-30)と -「追記」(書き手: 起票者。スプリント m815 の記録、日付: 2026-10-05)も、利用者の指示の原文ではない。 -利用者の指示の原文はこの課題に無い。 - -## 起票者の推定 - -題・本文の条件はどれも起票者が推したもので、承認ゲート 1 で利用者が確かめるまで利用者の決定として扱わない。 - -| # | 条件 | 推した出所 | この要求での扱い | -| --- | --- | --- | --- | -| S1 | 照合するのは「導入の確認をしたコミット」と「本番チャネルへマージするコミット」で、等しいときだけマージする | 本文「何を見つけたか」「なぜこの変更の範囲外なのか」 | 受け入れ条件 1・2 | -| S2 | 対象は `pace: auto` / `fast` の昇格のプラン(`--gate-approved mvv` の経路)である | 本文「何を見つけたか」「直さないと何が起きるか」 | 前提 1・含まない | -| S3 | 食い違ったら、手動反映の本番系(#1454)と同じく承認ゲート 2 からやり直す | 本文「直さないと何が起きるか」 | 受け入れ条件 3・4 | -| S4 | 直し方は、`merged-steps.py promote` に `--expect-head` を足し、昇格のプランの `promote` ステップへ確認の記録の SHA を渡すこと | 本文「追記」 | 前提 2(作り方は設計が決める) | -| S5 | 比べ方は「先端が確かめたコミットと等しいか」(#815 の `approved_commit.py` の `allowed` 0 個、または `--expect-head` の一致) | 本文「追記」 | 受け入れ条件 1(比べ方の選択は設計が決める) | - -## 目的 - -- `pace: auto` / `fast` で昇格の Pull Request を MVV 判定で通すとき、**本番チャネルへ入るコミットを、MVV 判定が読んだ導入の確認の記録のコミットと同じにする。** - 承認ゲート 2 の判定の材料と、実際に本番へ届く中身が食い違わない状態にする - -## 前提 - -- 前提 1: 対象は昇格のプランの `fast` / `auto` の経路(`verify` → `prepare` → `mvv` → `note` → `promote --gate-approved mvv`)だけである。`normal` の経路(`promote` → 利用者の承認 → `promote-approved --gate-approved user`)は導入の確認を走らせず、照合する記録が無い -- 前提 2: 照合の仕組みは、既存の `merged-steps.py merge-when-green --expect-head`(`merged_lib/checks.py` の `_check_expected` と `merge.py` の `--match-head-commit`)か `lib/approved_commit.py` を使い、新しい比べ方を作らない。どちらを使うか・SHA をどう渡すかは設計が決める -- 前提 3: 昇格の Pull Request の head はベースブランチそのものなので、導入の確認がベースブランチの先頭で確かめたコミットと、そのときの昇格の Pull Request の先端は同じコミットである(`release_lib/deploy.py` の `head_of_base` は `origin/<ベースブランチ>` を確かめる) -- 前提 4: 食い違ったときは、承認資料を作り直して自動で判定し直さず、既存の `handoff` で承認ゲート 2 へ落として人へ戻す(#815 の確定仕様が「止まって人へ戻すまでを扱う」としたのと同じ範囲)。人が承認した後の続き(`--from promote-approved` か、プランを `verify` から流し直すか)は既存の手順のまま変えない - -## 対象範囲 - -含む: -- 昇格のプランの `fast` / `auto` の `promote` ステップが、導入の確認の記録に書かれたコミットと昇格の Pull Request の先端を照合し、等しいときだけマージすること -- 食い違い・記録が読めないときに、マージせずに止まり、承認ゲート 2 へ戻ること -- `merged-steps.py promote` の引数と使い方の説明(モジュールの docstring・`--help`)と、`development-workflow` の `references/pace.md` の昇格のプランの説明を、変えた振る舞いに合わせること - -含まない: -- `normal` の昇格(`promote-approved --gate-approved user`)の照合。利用者が承認ゲート 2 で見た先端と、承認の後にマージする先端の照合は、この課題の記録(導入の確認)とは別の材料が要る(未決 1) -- 食い違ったときに、導入の確認と MVV 判定を自動でやり直す仕組み -- 本番の配布(`release --channel prod`)と手動反映の本番系(`gate-2` のプラン)。どちらも既に照合を持つ(#815・#1454) -- 導入の確認そのもの(`verify-facts`)の走らせ方と承認資料の中身 - -## ドメインイベント - -| # | イベント | 引き金 | 失敗したとき | 順序の前提 | -| --- | --- | --- | --- | --- | -| E1 | 導入の確認がベースブランチの先頭 X で走り、承認資料(`approval-verify.md`)に X を書いた | 昇格のプランの `verify` ステップ | 確認が非 0 か HEAD が先頭と違えば止まり、昇格へ進まない(既存) | — | -| E2 | 昇格の Pull Request を用意し、承認資料を書いた | `prepare` ステップ | 止まる(既存) | E1 | -| E3 | MVV 判定が 2 つの承認資料を読み、承認ゲート 2 を通した | `mvv` ステップ | 通さなければ承認ゲートへ(既存) | E2 | -| E4 | 判定の記録を昇格の Pull Request へ書いた | `note` ステップ | `handoff` へ(既存) | E3 | -| E5 | ベースブランチが X から Y へ進んだ | 別のスプリント・別の Pull Request のマージ(プランの外) | — | E1 の後ならいつでも起きうる(E6 の待ちの間を含む) | -| E6 | 昇格の Pull Request の先端を読み、X と比べた | `promote` ステップ(CI を待つ間は読むたび) | 先端が X でない・X を読めない → E8 | E4 | -| E7 | 先端が X のまま CI が通り、X を本番チャネルへマージした | E6 で一致 | マージの拒否は既存の失敗の扱い | E6 | -| E8 | マージせずに止まり、`handoff` が MVV 判定の記録を外して承認ゲート 2 へ戻した | E6 で不一致・読めない | `handoff` は記録を外せなくても承認ゲートを返す(既存) | E6 | -| E9 | リリース記録を昇格の Pull Request へ書いた | `record` ステップ | `judge-record` へ(既存) | E7 | - -E5 が E1 と E2 の間に起きた場合も、E6 で食い違いとして捉える(昇格の Pull Request の先端は Y になる)。 - -## 用語 - -| 用語 | 意味 | -| --- | --- | -| 昇格のプラン | ベースブランチから本番チャネルへの Pull Request を `merged-steps.py promote` が作ってマージする配布のプラン(`supervise_lib/delivery_templates.py` の `plan_promote`) | - -## 受け入れ条件 - -この節の「導入の確認の記録」は `release-steps.py verify-facts` が書く承認資料(`approval-verify.md`)を、「確かめたコミット」はそこに書かれた、導入の確認を走らせたベースブランチの先頭のコミット(ドメインイベントの X)を指す。 - -- [ ] 1: `fast` / `auto` の昇格のプランで、昇格の Pull Request の先端が確かめたコミット X と等しく CI が通ったとき、`promote` ステップは X を本番チャネルへマージし、続く `record` へ進む(今の成功の経路が変わらない) -- [ ] 2: 導入の確認の後に先端が X から Y へ進んでいたとき(E5 が E1〜E6 のどこで起きても)、`promote` ステップは `gh pr merge` を呼ばず、結果 JSON に `head_moved` と X・Y を出して非 0 で終わる -- [ ] 3: CI を待つ間に先端が X から Y へ進んだときも、Y をマージしない(2 と同じ結果になる) -- [ ] 4: 2・3 のとき、プランは `handoff` へ進み、承認ゲート 2 の `by: mvv` の記録を外して終了コード 10(承認ゲート)を返す。本番チャネルの先端は変わらない -- [ ] 5: 導入の確認の記録が無い・確かめたコミットの欄が読めない・40 桁の SHA でないとき、`promote` ステップはマージせずに非 0 で終わり、4 と同じく承認ゲート 2 へ戻る(照合しないままマージしない) -- [ ] 6: 照合に使うコミットは、MVV 判定(`mvv` ステップ)が読んだのと同じ導入の確認の記録から取る。プランを組む時点の先端や、ブランチ名から引き直した先端を使わない -- [ ] 7: `normal` の昇格のプラン(`promote` と `promote-approved`)の組み立てとコマンドは今と同じである -- [ ] 8: ベースブランチ・本番チャネルの名前を埋め込まない(`develop` / `main` を新しく書かない。名前は今と同じく引数と宣言から読む) -- [ ] 9: `merged-steps.py promote` の `--help` と `pace.md` の昇格のプランの説明が、照合と、食い違ったときに承認ゲート 2 へ戻ることを書いている - -## 非機能の条件 - -| 大項目 | 条件 | -| --- | --- | -| 運用・保守性 | 照合は既存の `--expect-head` か `approved_commit.py` を使い、同じ役割の比較を新たに書かない(Value 6) | -| セキュリティ | 照合できないときはマージしない(記録が読めないときに照合を飛ばして進まない) | - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| 公開インタフェース | 変わる(互換あり)。`merged-steps.py promote` に照合のための任意の引数が増えうる。引数を渡さない呼び出しの振る舞いは今と同じ | -| データ | 無し | -| 既存の振る舞い | `fast` / `auto` の昇格で、導入の確認の後にベースブランチが進んでいると、今はマージされていたものが承認ゲート 2 で止まる | - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| テスト | `uv run --frozen --project . --all-extras pytest plugins/ndf/scripts/tests -q -n 4 -p no:playwright-kit`(昇格のプランの組み立ては `test_supervise_pace_verify.py`・`test_supervise_new.py`、照合とマージは `test_sprint_close_merge_green.py` の形で、`gh` を差し替えて先端の一致・不一致・記録の欠けを通す) | -| 静的解析 | `bash scripts/check-lint.sh` | -| 手動確認 | 無し(本番チャネルへのマージを伴うため、実機の確認はリリース後テストで昇格の経路を持つリポジトリが通るときに見る) | - -## 前提とする取り決め - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | 本体は `plugins/ndf/scripts/`(`merged_lib/`・`supervise_lib/`・`lib/`)。実験版の置き場(`scripts/experimental/`)は使わない(承認ゲートと配布に当たるので安定版) | -| コーディング規約 | `AGENTS.md` と `bash scripts/check-lint.sh` | -| テスト戦略 | 単体: 照合の判定(一致・不一致・欠け)。結合: 昇格のプランの組み立て(`fast` / `auto` の `promote` に照合の材料が渡り、`normal` は変わらない)。`.md` の文言を照合するテストは書かない | - -## 境界 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 既存テストの実行、リンタの適用 | -| 確認してから行う | `merged-steps.py promote` の引数の追加(承認ゲート 1・2 で人が見る) | -| 行わない | `normal` の経路・本番の配布・`gate-2` のプランの変更、食い違ったときの自動のやり直し | - -## MVV との突き合わせ - -反する疑いは無い。根拠: Vision(承認ゲート 2 に届く判定の材料と本番へ入る中身をそろえる)・Value 2(承認を MVV 判定に任せる経路でも、判定の材料の外のコミットを本番へ入れない)・Value 6(既存の照合を使う)・Value 5(ブランチ名を埋め込まない)。 -本番チャネルへの反映は P1・C5 に当たらない(取得元・版の規則は変えない)。 - -## 未決 - -- 未決 1: `normal` の昇格で、利用者が承認ゲート 2 で見た先端と `promote-approved` がマージする先端の照合を要るとするか。この課題の受け入れ条件には入れない。要るなら別の課題にする(設計か承認ゲート 1 で決める) From 781d8580450171f7fca82908549f656b698bbe1b Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sun, 25 Oct 2026 07:27:35 +0000 Subject: [PATCH 02/17] =?UTF-8?q?Docs:=20ndf-production-release-record.md?= =?UTF-8?q?=20=E3=82=92=E7=A2=BA=E5=AE=9A=E4=BB=95=E6=A7=98=E3=81=AB?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- .../ndf-production-release-record.md | 79 +++++- issues/issue-1914-design.md | 236 ------------------ issues/issue-1914-requirements.md | 166 ------------ 3 files changed, 78 insertions(+), 403 deletions(-) delete mode 100644 issues/issue-1914-design.md delete mode 100644 issues/issue-1914-requirements.md diff --git a/docs/specifications/ndf-production-release-record.md b/docs/specifications/ndf-production-release-record.md index e8571173c..3826d57dd 100644 --- a/docs/specifications/ndf-production-release-record.md +++ b/docs/specifications/ndf-production-release-record.md @@ -7,6 +7,9 @@ - **書けた本番のリリースの PR は、プランの報告の `Pull Request` に載る。** まとめの `close` ステップは `{queue_pr:release-prod}` からその PR を `--record-pr` に受け取り、`sprint-close.py` が記録から閉じる条件 1 を判定する - **記録の書き込みが落ちても、本番の配布はやり直さない。** やり直せるのは記録を書くステップだけである +- **`pace: normal` のスプリントでも、`new sprint` が承認ゲート 2 と本番のステージ(計画名 `release-prod`)まで目録へ書き出す。** + 承認ゲート 2 の後に conductor が組み立てるコマンドは無く、目録の「本番」のステージの `command` に `--done` を足して打つだけで + 本番の配布が流れる(#1914。「`pace: normal` のスプリントの本番のステージ」) 例: 版 `10.17.30`(本番のリリースの PR は #1268。記録が無く、課題を手で閉じた版である)を今のプランで出したとき。 直前の正式版は `10.17.29`。スプリントの PR は実際には 13 本あるが、説明のため #1260 と #1262 の 2 本に絞る。 @@ -53,6 +56,7 @@ | `supervise.py new release --channel prod` が `package-plugin` から組む本番のリリースプランと、それを使うスプリントのまとめ(配布先のリポジトリでも、同じ宣言を置けば働く) | 開発版のリリースプラン(`段階: 検証` の記録は書かない。`close` ステップは本番のリリースの PR しか読まない) | | `release-steps.py record`・`lib/dist_record.py`・`release_lib/record.py`・ステップの鍵 `pr_from` | リリース後テストのブロック(`## リリース後テスト`)の中身。本番のリリースプランはこれを書かない(まとめの `release-verify` が `/ndf:release-verification` に書かせる。#1683・#1870) | | 昇格のプランの `record`(`record --promote`。#837・#1870) | 手で届ける経路・`--record-pr 0` / `unknown` の経路(merge・manual・検証の環境だけ)の記録 | +| `pace: normal` の `new sprint` が雛形の経路で承認ゲート 2 と本番のステージを書き出すこと(#1914) | `pace: fast` / `auto` の本番のステージ(先頭が MVV 判定の本番のプラン。`fast_sprint_plans`・`auto_sprint_plans`)、雛形の無い経路(`route_waves`)・手動反映の本番系 | | | `sprint-close.py` の引数・結果 JSON・閉じる条件(変えていない) | プラグイン名・ベースブランチ・本番チャネルは `.ndf/supervise.json` の `release.plugin` と `.ndf/worktree.json` から @@ -211,6 +215,67 @@ stateDiagram-v2 `release` は走らない。MVV 判定つきの本番(`pace: fast` / `auto`)は先頭に `mvv` と `note` が入るだけで、`record` の 位置は同じである。 +### `pace: normal` のスプリントの本番のステージ(#1914) + +`supervise.py new sprint`(`pace: normal`)は、`.ndf/supervise.json` に雛形のある `release.form` があり(`has_release_template`)、 +本番のブランチ(`--production-branch` か `.ndf/worktree.json` の `production_branch`)が決まっているとき(`has_prod_branch`)、 +目録(`sprint.json`)の `ステージ` の末尾を次の 3 つにする(`supervise_lib/sprint.py` の `sprint_plans`。番号は前のステージの数で変わる)。 + +| `name` | 持つキー | 中身 | +| --- | --- | --- | +| `配布` | `plans`(`release`)・`then_of: 検査` | 開発版のリリースプラン。`note` は持たない | +| `関門 2` | `gate` | 承認ゲート 2 の文(`sprint_routes.normal_gate_2_stage`) | +| `本番` | `plans`(`release-prod`)・`command` | 本番のリリースプラン。`command` は `python3 queue <番号>-release-prod.json --max 3` | + +本番のプランは開発版と同じ `sprint_waves.plan_sprint_release` に `channel="prod"` と `version=prod_version(--version)`(接尾辞を外した版。 +`10.18.0-dev.1` → `10.18.0`)を渡して組む。worktree とブランチは `release/v<正式版>`、出す PR は `{queue_pr:check}` である。 +雛形(`release_templates.plan_release`)が組んだステップを目録の側で書き換えず、`new release --version <正式版> --prs {queue_pr:check} --channel prod` +と同じ出力に `課題` が入る(スプリントの工程は雛形に対して順応者の関係にある)。 + +承認ゲート 2 の文は、開発版の配布が書いた承認資料(元のリポジトリの下の `issues/approval-<プラグイン>-v<正式版>.md` の絶対パス)を示して +本番への配布の承認を取り、承認されたら `release-steps.py approve --approval <承認資料> --approved-sha --by user` +で承認の記録を書いてから「本番」のステージの `command` を打つこと(差し戻されたら打たない)を書く。`--state` を渡したときだけ、続けて +助言の MVV 判定の扱いと `sprint-state.py gate <状態> "関門 2" --what <要約> --by user --outcome approved|rejected` を書く(`normal_gate_2`)。 + +| # | 常に成り立つ条件 | 破れたときの扱い | +| --- | --- | --- | +| N1 | 「本番」のステージは `then_of` を持たず、どの queue の `--then` にも入らない(承認ゲート 2 は利用者が答える。連結すると承認を待たずに本番が流れる) | テストで落とす | +| N2 | 並びは「配布」→「`関門 2`」→「本番」で、間に他のステージが入らない | テストで落とす | +| N3 | 本番のプランの版は `--version` から接尾辞を外したもの、チャネルは `prod`、出す PR は `{queue_pr:check}` | テストで落とす | +| N4 | `--state` を渡しても、本番のプランは MVV 判定のステップ(`mvv`・`note`・`handoff`)を持たない(`advise` は `channel` が `dev` のときだけ渡す) | テストで落とす | +| N5 | 「`関門 2`」を置くとき、承認ゲート 2 の文はその `gate` の 1 か所にだけあり、「配布」の `note` に重ねない。ほかの経路(N6 と `route_waves`)では「配布」か経路のステージの `note` が正本である | テストで落とす | +| N6 | 本番のブランチが決まっていないときは「`関門 2`」と「本番」を置かず、「配布」で終える(`--state` なら `note` に `normal_gate_2` の文)。`next` に置かなかった理由を 1 文足す(`no_prod_stage`) | テストで落とす | + +本番のステージを承認の後に単独で流すと、`queue.fill_queue_pr` は前のステージに名前の一致する計画が無いため、同じディレクトリの +検査の計画(`check`)の報告から PR を読んで `{queue_pr:check}` を埋める。報告が無い・PR が無いときは `0` に埋まり、開発版の配布(#1915)と +同じ振る舞いになる。雛形の経路のまとめの `close` は `{queue_pr:release-prod}` を読む(上の表)ため、計画名は `fast` / `auto` と同じ +`release-prod` にしてある。手順は `development-workflow` の [references/waiting.md](../../plugins/ndf/skills/development-workflow/references/waiting.md) +の normal の 1 スプリントの手順 8・9 が正である。 + +```mermaid +sequenceDiagram + participant C as conductor + participant S as new sprint + participant Q as queue + C->>S: new sprint(normal・--version 10.18.0-dev.1) + S-->>C: sprint.json(… 検査 → 配布 → 関門 2 → 本番) + C->>Q: 検査の command(--then 配布) + Q->>Q: 検査がスプリントの PR をマージ(報告に PR) + Q->>Q: 配布の {queue_pr:check} を埋めて開発版を流す + Q-->>C: gate(承認資料) + C->>C: 関門 2 の gate を読み、承認を求める + alt 差し戻された + C->>C: 本番の command を打たない + else 承認された + C->>Q: 本番の command --done(単独) + Q->>Q: 同じディレクトリの検査の報告から {queue_pr:check} を埋める + Q-->>C: 本番の配布・記録・リリース後テスト・後片付けの結果 + end +``` + +既に書き出した目録で進行中のスプリントは、単独の `supervise.py new release --channel prod` で本番のプランを作れる(スプリントの外で +リリースする場面でも使うため残す)。 + ## 既知の限界 | 項目 | 内容 | @@ -219,6 +284,9 @@ stateDiagram-v2 | まとめの queue の本番が飛ばされたとき | 最終の検査で変更が無いと、まとめの queue の `release-prod` / `promote` が飛ばされて記録の PR が `0` になり、`new sprint` の本番で出た配布のリリース後テストは見ずに閉じる | | `release-verify` の所要 | まとめのプランの上限とステップの `timeout`(3600 秒)に収まるかは、流したスプリントで測る | | 課題を閉じた後の本文 | 工程表のチェックボックスは更新しない(受け入れ条件の印は `--with-verification` の経路で閉じる前に付ける。[ndf-sprint-close-acceptance-criteria.md](ndf-sprint-close-acceptance-criteria.md))。コミットの件名の closing keywords(`Closes #N` など、GitHub が課題を自動で閉じるキーワード)で GitHub が先に閉じる経路も塞がない | +| `pace: normal` の本番のブランチの後退 | `.ndf/worktree.json` に `production_branch` が無く既定ブランチを本番にするリポジトリでは、N6 により本番のステージが置かれない(直すのは #2097) | +| `pace: normal` の承認の前の本番 | スクリプトは承認ゲート 2 の答えを確かめずに「本番」のステージの `command` を流せる。止めるのは本番のプランの `release --approval` が承認の記録を確かめる時点である([ndf-release-approved-commit.md](ndf-release-approved-commit.md) の I10) | +| `pace: normal` の本番を単独で流す初回 | 本番のプランを目録から単独で流すのは、`pace: normal` のスプリントの承認ゲート 2 の後が最初になる。検査では本番へ配らず、置き換えの処理を単独で当てて確かめた。実機はリリース後テストで見る | | 手で閉じていないことの確かめ | 昇格の形か版数を上げない配布で本番まで出たスプリントで、まとめの `close` が課題を `closed` にしたかを、キューの記録と課題を閉じた主体で確かめる(リリース後テスト) | ## 決定の理由 @@ -237,6 +305,10 @@ stateDiagram-v2 | 昇格の後の記録は、既存の `record` に `--promote` を足し、`plan_promote` のマージの後の別のステップにする | package-plugin と同じステップ・同じ `judge-record`・同じ `pr_from` を使える。`merged-steps.py promote` の中で書くと、書き込みの失敗がマージのステップの失敗になり、やり直すと開いた昇格の PR が無いため「昇格する変更が無い」で ok に終わり、記録を書かずに進む。`--promote` を `--version` と排他のフラグにしたのは、片方の形の引数だけを渡す誤りを引数の解析で止めるためである | | 昇格の `版:` は `<本番チャネル> <直前の短い SHA> → <本番チャネル> <マージのコミットの短い SHA>` にする | package-plugin の形(直前の版 → 出した版)と同じ並びになる。`parse_record` が読む `version` は矢印の右である | | LLM が配布する形は、`release` の手順に「配布の後に記録をコメントで置く」段を足すだけにする | 読む側は最後の記録を読むため、承認の前の `段階: 検証` を書き換えずに済む。承認の前に記録を置かない案は、Draft の時点で本文を用意する運用を変えるため採らない | +| `pace: normal` でも承認ゲート 2 と本番のステージを目録へ書き出す(#1914) | 本番のプランを手で作る手当てが 5 スプリント(m1693・m1385・m744・m653・m1683)で繰り返され、m1385 ではプランを作る前にキューを打って空振りした。`fast` / `auto` は同じ雛形の経路で既に本番のステージを持つ | +| 承認ゲート 2 の文を `関門 2` の `gate` へ移し、配布の `note` から外す(#1914) | 2 か所に置くと片方だけが直され、conductor が古い方を読む。`関門 1` が `gate` に答えを状態へ書くコマンドを持つ形とも揃う。雛形の無い経路は承認ゲート 2 のステージを持たないため `note` のまま残す | +| 本番のプランは `plan_sprint_release` にチャネルと版を渡して作り、`plan_mvv_release` を使わない(#1914) | 配布のプランの組み立てが 1 つになる。試作を conductor が打っていた `new release --channel prod` の出力と比べると、出す PR と `課題` の欄を除いて 13 ステップが一致した。`plan_mvv_release` は `--state` を渡した normal で本番の先頭に MVV 判定の承認ゲートを入れ、利用者の承認の後にもう一度判定で止まる(N4) | +| 本番のブランチが無ければ本番を置かず、`new sprint` は止めない(#1914) | 雛形の本番のプランは本番のブランチが無いと `DeclError` で組めない。止めると、今まで開発版まで流せたリポジトリの `new sprint` が流せなくなる | | 課題を閉じる場所はまとめのプランの `close` の 1 つに保ち、その前に `release-verify` を 1 つ置く | 欠けていたのはリリース後テストの記録だけである。確かめは判断を伴うため work にする。番号が `0` のときはステップの指示で何もせず終え、飛ばすための run ステップを足さない。本番のプランに `close` を置く案は閉じる場所が 2 つになり、プランを止めて記録を待つ案は待つ間に手で閉じる経路が残る | ## テスト観点 @@ -261,6 +333,10 @@ stateDiagram-v2 | 昇格の記録(I7) | `record --promote` がマージ済みの PR があればその PR へ書き、無ければ書かずに 3 で終わること。書いた記録を `parse_record` が `本番` で読むこと。本文に `段階: 検証(… 承認待ちのため未実施)` を置いた PR に記録をコメントで足すと `本番` として読むこと | | 昇格のプラン | normal と fast / auto の両方で、マージの後に `record`(`pr_from: release_pr_url`・`on_fail: judge-record`)があり、`judge-record` の `choices` が `record` と `stop` だけであること | | まとめのプラン(I9) | 雛形と昇格の経路で、マージ → `release-verify` → `close`(`--with-verification`)がこの順で並び、昇格の `close` が `--record-pr {queue_pr:promote}` であること。none だけの経路は `0`、merge・manual・検証の環境だけの経路は `unknown` で、どちらも `release-verify` が無いこと | +| normal の本番のステージ(N1〜N3) | 雛形・本番のブランチありの `new sprint` の `ステージ` が「… 検査・配布・`関門 2`・本番」の順で、「本番」が `then_of` を持たず `command` を持ち、プランの名前が `release-prod`、`release` のステップが `--channel prod` と `--version 10.18.0`(`10.18.0-dev.1` を渡す)、`changelog` の `--prs` が `{queue_pr:check}` であること(`test_supervise.py` の `test_normal_sprint_puts_gate_2_and_prod_after_the_dev_release`) | +| 本番を単独で流す | 一時ディレクトリに検査のプランとその報告(PR 番号つき)と本番のプランだけを置き、前のステージの結果を渡さずに `fill_queue_pr` を当てると、`{queue_pr:check}` がすべてその番号になること(`test_prod_plan_queued_alone_takes_the_check_pr`) | +| 承認ゲート 2 の文(N4・N5) | `--state` ありで「`関門 2`」の `gate` が `sprint-state.py` の `gate` のコマンド(`"関門 2"`・`--by user`)を含み、「配布」が `note` を持たず、本番のプランが `mvv`・`note`・`handoff` を持たないこと。`--state` なしで `gate` が `sprint-state.py` を含まないこと(`test_project_mvv_normal.py`) | +| 本番のブランチが無いとき(N6) | 本番のブランチの無い宣言の normal の `new sprint` が成功し、最後のステージが「配布」で、`--state` ありでは「配布」が `normal_gate_2` の文の `note` を持つこと(`test_supervise_new.py` の `test_normal_sprint_without_a_production_branch_keeps_gate_2_on_the_release`)。雛形の無い経路と `fast` / `auto` の目録は変わらないこと | | 退行しない | `test_sprint_close_issues.py`・`test_sprint_close_merge_green.py`・`test_legacy_names.py` と、リリースプランの既存のテストが通ること | | 手動 | 本番のリリースプランを通したスプリントで、本番のリリースの PR に `## 配布の記録` のコメントが付き、まとめの `close` ステップの `kept_open` の理由が「配布の記録が読めない」でないこと | @@ -271,4 +347,5 @@ stateDiagram-v2 - [後片付けを止めずに通し、スプリント課題を最終工程で閉じる](ndf-cleanup-and-bundle-closing.md) - [受け入れ条件の番号ごとの合格で閉じて本文に印を付ける](ndf-sprint-close-acceptance-criteria.md)(閉じる条件 2 と表の読み方) - [`lib/dist_record.py`](../../plugins/ndf/scripts/lib/dist_record.py) / [`release_lib/record.py`](../../plugins/ndf/scripts/release_lib/record.py) / [`release-steps.py`](../../plugins/ndf/scripts/release-steps.py) -- 課題 [#1273](https://github.com/devbasex/ai-plugins/issues/1273)・リリース後テストの書き手は [#1683](https://github.com/devbasex/ai-plugins/issues/1683)・昇格の記録と版の食い違いの読み方は [#1870](https://github.com/devbasex/ai-plugins/issues/1870) +- `development-workflow` の [references/waiting.md](../../plugins/ndf/skills/development-workflow/references/waiting.md)(normal の 1 スプリントの手順 8・9)・[`supervise_lib/sprint.py`](../../plugins/ndf/scripts/supervise_lib/sprint.py)・[`supervise_lib/sprint_waves.py`](../../plugins/ndf/scripts/supervise_lib/sprint_waves.py)・[`supervise_lib/sprint_routes.py`](../../plugins/ndf/scripts/supervise_lib/sprint_routes.py) +- 課題 [#1273](https://github.com/devbasex/ai-plugins/issues/1273)・`pace: normal` の本番のステージは [#1914](https://github.com/devbasex/ai-plugins/issues/1914)(依存 #1915)・リリース後テストの書き手は [#1683](https://github.com/devbasex/ai-plugins/issues/1683)・昇格の記録と版の食い違いの読み方は [#1870](https://github.com/devbasex/ai-plugins/issues/1870) diff --git a/issues/issue-1914-design.md b/issues/issue-1914-design.md deleted file mode 100644 index f4b338b44..000000000 --- a/issues/issue-1914-design.md +++ /dev/null @@ -1,236 +0,0 @@ -# pace: normal のスプリントでは承認ゲート 2 の後に conductor が本番のプランを手で作り、作る前にキューを打って空振りする → new sprint が承認ゲート 2 と本番のステージまで書き出し、承認の後は目録のコマンドを打つだけで本番が流れる(#1914) - -## 目的 - -- **何が壊れているか**: `pace: normal` の `new sprint` は release の雛形の経路でも「配布」(開発版)で終わり、本番のプランを書き出さない。承認ゲート 2 の後に conductor が `supervise.py new release --channel prod` を版と PR 番号を埋めて組み立てる -- **誰が困るか**: スプリントを回す conductor と、それを見守る利用者。手当ては 5 スプリントで記録があり、1 回はプランを作る前にキューを打って空振りした(m1385) -- **直すと何が成り立つか**: 承認ゲート 2 の後に打つのは、目録(`sprint.json`)の「本番」のステージの `command` に `--done` を足したものと `wait` だけになる。組み立てるコマンドは 0 本になる - -## 適用範囲 - -- **働く範囲**: 配布先のリポジトリでも働く(`plugins/ndf/scripts/supervise_lib/` の `new sprint`)。`pace: normal` で、`.ndf/supervise.json` に雛形のある `release.form` があり、本番のブランチ(`--production-branch` か `.ndf/worktree.json` の `production_branch`)が決まっているときだけ出力が変わる -- **プロジェクトごとに違うもの**: 配布の形・プラグイン名・本番のブランチは今の宣言(`.ndf/supervise.json` の `release`・`.ndf/worktree.json`)から読む。設定のキーと引数は足さない -- **当たるモード**: `standard`(安定版の `new sprint` の出力を変える) - -## あるべき姿の根拠 - -| 根拠 | 種類 | 何を示すか | -| --- | --- | --- | -| #1914 の本文(m1693・m1385・m744・m653・m1683 の手当て) | 実測 | 本番のプランを手で作る手当てが 5 スプリントで繰り返され、1 回は空振りした | -| `pace: fast` / `auto` の `new sprint`(`sprint.py` の `fast_sprint_plans`・`auto_sprint_plans`) | 既存の形 | 同じ雛形の経路で「開発版」の後に計画名 `release-prod` の「本番」を置き、版は `prod_version` で接尾辞を外す | -| 2026-10-24 の試作の比較(下の決定 2) | 実測 | 雛形の本番のプランを `new sprint` の側で組んだものと、conductor が打っていた `new release --channel prod` の出力は、出す PR の欄と `課題` の欄を除いて一致する | - -要求と受け入れ条件は #1914 の本文にある(コピーは [issue-1914-requirements.md](issue-1914-requirements.md))。この文書は「どう作るか」だけを扱う。 - -## ドメインモデル - -### コンテキスト - -| コンテキスト | 何の語が 1 つの意味に決まるか | -| --- | --- | -| NDF の開発ワークフロー(`ndf-workflow`) | ステージ・承認ゲート・キュー・プラン・目録 | -| NDF のリリース(`ndf-release`) | 開発版・本番・承認資料・リリースプラン | - -**`ndf-workflow` は `ndf-release` に対して順応者である。** `new sprint` は本番のプランを雛形(`release_templates.plan_release`)が組んだ形のまま受け取り、ステップを書き換えない。雛形が変われば、手で打つ `new release --channel prod` と同じ変化がそのまま目録へ入る。 - -### 集約 - -| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | -| --- | --- | --- | --- | --- | -| スプリントの目録 | `supervise_lib/sprint.py`(`sprint_plans`・`cmd_new_sprint`) | `sprint.json` | ステージ(番号・名前・`command` / `then_of` / `gate`) | 承認ゲート 2 の文・ステージの並び | -| 本番のリリースプラン | `supervise_lib/release_templates.py`(`plan_release`。ステップの組み立て)と `supervise_lib/sprint_waves.py`(`plan_sprint_release`。渡す版・チャネル・出す PR と、MVV 判定を渡すかを決める。I3・I4 を守る) | `<番号>-release-prod.json` | ステップ | 版・チャネル・出す PR(`{queue_pr:check}`)・承認資料のパス | - -**目録の持ち主は本番のリリースプランのステップを書き換えない。** 渡すのは版・チャネル・出す PR の 3 つの値だけで、組み立ては雛形が持つ(`sprint_waves.plan_sprint_release` が両者の間で値を渡す)。 - -### 不変条件 - -| # | 集約 | 条件 | 破れたときの扱い | -| --- | --- | --- | --- | -| I1 | スプリントの目録 | 「本番」のステージは `then_of` を持たず、どの queue の `--then` にも入らない | テストで落とす | -| I2 | スプリントの目録 | 並びは「配布」→「`関門 2`」→「本番」で、間に他のステージが入らない | テストで落とす | -| I3 | 本番のリリースプラン | 版は `--version` から接尾辞を外したもの、チャネルは `prod`、出す PR は `{queue_pr:check}` | テストで落とす | -| I4 | 本番のリリースプラン | `--state` を渡しても MVV 判定のステップ(`mvv`・`note`・`handoff`)を持たない(`pace: normal` の承認ゲート 2 は利用者が答える) | テストで落とす | -| I5 | スプリントの目録 | 雛形の経路で本番のブランチがあり「`関門 2`」を置くとき、承認ゲート 2 の文は「`関門 2`」のステージの `gate` の 1 か所にだけあり、「配布」の `note` に重ねない。それ以外の経路(I6 の場合と `route_waves`)では今の `note` が正本である | テストで落とす | -| I6 | スプリントの目録 | 本番のブランチが決まっていないときは「`関門 2`」と「本番」を置かず、出力は今と同じ(「配布」で終わる) | テストで落とす | - -### ドメインイベント - -| # | イベント | 発生元 | 受け手 | -| --- | --- | --- | --- | -| E1 | `new sprint` が配布・承認ゲート 2・本番のステージを `sprint.json` へ書き出した | `sprint.cmd_new_sprint` | conductor(ステージの番号の順に `command` を打つ) | -| E2 | 検査のプランがスプリントの PR をマージし、報告に PR を残した | 検査のプラン(`<番号>-check.json`) | 開発版の配布(`--then`)と、後で単独で流す本番のプラン(`{queue_pr:check}`) | -| E3 | 開発版の配布が承認資料を書き、承認ゲートを返した | 開発版のリリースプラン(`facts` のステップ) | conductor(「`関門 2`」のステージの `gate` を読んで承認を求める) | -| E4 | 利用者が本番への配布を承認した(`--state` があれば `sprint-state.py gate … "関門 2" --by user` を打った) | conductor | conductor(「本番」のステージの `command` を打つ) | -| E5 | 本番のプランの `{queue_pr:check}` が検査の報告の PR 番号に埋まった | `queue.fill_queue_pr`(同じディレクトリの報告を読む) | 本番のリリースプランのステップ(`changelog`・`notes`・`record`・`cleanup`) | -| E6 | 本番の配布・リリース後テスト・後片付けが終わった | 本番のリリースプラン | conductor(引継ぎ文書と次のセッション。`relay.md`) | - -### 用語 - -| 用語 | 意味 | 用語集への反映 | -| --- | --- | --- | -| ステージ | 用語集のまま(`new sprint` がステージごとにプランを書き出す。設計・ゲート 1・実装・検査・開発版・本番) | 無し(`pace: normal` もゲート 2 と本番を持つようになり、用語集の並びと一致する) | -| 承認資料 | 用語集のまま。本番の承認は開発版の配布が書いた `issues/approval-<プラグイン>-v<正式版>.md` を示して求める | 無し | -| 本番 | 用語集のまま | 無し | - -新しく足す語と意味を変える語は無い。ステージの名前の `関門 2` はコードの識別子(`関門 1` と同じ命名)で、文書の地の文では「承認ゲート 2」と書く。 - -## 機能一覧 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| F1 | `pace: normal` の `new sprint` が、配布の後に承認ゲート 2 と本番のステージを書き出す | conductor | -| F2 | 承認ゲート 2 のステージが、承認資料の場所と、`--state` があれば助言の MVV 判定と答えを状態へ書くコマンドを示す | conductor・利用者 | -| F3 | 本番のステージの `command` を承認の後に単独で打つと、出す PR が検査の報告から埋まって本番の配布が流れる | conductor | -| F4 | 手順書(`waiting.md`・`agent-layers.md`・`relay.md`)が、本番を目録のステージとして流す手順を書く | conductor・supervisor | - -## 構成要素 - -| 要素 | 責務 | 変え方 | -| --- | --- | --- | -| `sprint_waves.plan_sprint_release` | 雛形の配布のプランへ版・チャネル・出す PR(`{queue_pr:check}`)を渡す | 引数 `channel`(既定 `dev`)と `version`(既定 `a.version`)を足す。worktree とブランチは `release/v`。助言の MVV 判定は `dev` のときだけ渡す | -| `sprint.sprint_plans`(normal) | ステージの並びを組む | 雛形の経路で本番のブランチがあれば、「配布」の後に「`関門 2`」(`gate`)と「本番」(`release-prod`、`then_of` なし)を足し、「配布」の `note` を外す。本番のブランチが無ければ今のまま(「配布」で終わり、`--state` なら `note` も残す) | -| `sprint.normal_gate_2_stage`(新設) | 承認ゲート 2 のステージの文を組む | 承認資料のパスと「承認の後に本番のステージの `command` を打つ」こと。`--state` があれば今の `normal_gate_2` の文を続ける | -| `sprint.normal_gate_2` | 助言の MVV 判定と答えを状態へ書くコマンドの文 | 変えない。`route_waves` の経路の `note` と新設の関数の両方が使う | -| `sprint.next_text` | `new sprint` の `next` の文 | `advise` の文の「配布のステージの note」を「承認ゲート 2 の文(`関門 2` のステージの `gate` か、配布の後のステージの `note`)」へ直す。本番のブランチが無くて本番を置かなかったときは、その理由を 1 文足す | -| `references/waiting.md` | normal の 1 スプリントを流すコマンド | 冒頭の並びと手順 8・9。手順 9 は「本番」のステージの `command` に `--done` を足して打つことと `wait` だけにする | -| `references/agent-layers.md` | フェーズとプラン・ステージの対応表 | 「仕上げ」の行のステージを「目録の『本番』のステージ。承認ゲート 2 の承認の後に単独の `command` で流す」へ、「リリース」の行を目録のステージ(単発は `new release`)へ直す | -| `references/relay.md` | 本番の後のカットポイントの例 | 本番のプランのパスを `sprint.json` の「本番」のステージのプラン(`$O/<番号>-release-prod.json`)にする | -| テスト(`tests/test_supervise.py`・`test_supervise_new.py`・`test_project_mvv_normal.py`・`test_release_check_state.py`) | 振る舞いを縛る | 「配布」で終わる・「配布」の `note` を読む期待を直し、下のテスト設計の行を足す | - -`waiting.md` の「1 スプリントの流し方」(手順 1〜5)は `new sprint` を使わずにプランを並べる形で、本番のプランの作り方を書いていないため変えない。`release` の `release-steps.md`・`form-package-plugin.md` の `--channel prod` は単独のリリースの説明で、`pace: normal` のスプリントの手順ではないため変えない。 - -図にはテストを含めない(テストは上の要素の振る舞いを縛るだけで、呼び出しの関係を持たない)。 - -```mermaid -graph LR - subgraph 目録を組む - SP[sprint.sprint_plans] --> G2[sprint.normal_gate_2_stage] - G2 --> NG2[sprint.normal_gate_2] - SP --> PSR[sprint_waves.plan_sprint_release] - PSR --> PR[release_templates.plan_release] - SP --> NT[sprint.next_text] - end - subgraph 流す - Q[queue.fill_queue_pr] --> PLAN[本番のリリースプラン] - end - subgraph 手順書 - W[waiting.md] -.-> AL[agent-layers.md] - W -.-> RL[relay.md] - end - SP -->|sprint.json| W - PR -->|release-prod.json| Q -``` - -## 入出力の契約 - -**名前**: `supervise.py new sprint`(`pace: normal`、雛形の経路、本番のブランチあり)。入力の引数は変えない。 - -**出力**: `sprint.json` の `ステージ` の末尾が次の 3 つになる(設計の課題がある例。番号は前のステージの数で変わる)。 - -| `wave` | `name` | 持つキー | 中身 | -| --- | --- | --- | --- | -| 7 | `配布` | `plans`・`then_of: 検査` | 今と同じ。`note` は持たない(今は `--state` のとき持つ) | -| 8 | `関門 2` | `gate` | 承認ゲート 2 の文(下の表) | -| 9 | `本番` | `plans`・`command` | プランは `9-release-prod.json`。`command` は `python3 queue <9-release-prod.json> --max 3` | - -| `--state` | `関門 2` の `gate` の文 | -| --- | --- | -| 無し | 開発版の配布が書いた承認資料(`issues/approval-<プラグイン>-v<正式版>.md`)を示して本番への配布の承認を取り、承認の後に「本番」のステージの `command` を打つ。`sprint-state.py` のコマンドを含まない | -| 有り | 上の文に、今の `normal_gate_2` の文(承認資料の末尾の助言の MVV 判定と、`sprint-state.py gate <状態> "関門 2" --what <要約> --by user --outcome approved\|rejected`)を続ける | - -本番のプラン `<番号>-release-prod.json` は、`new release --version <正式版> --prs {queue_pr:check} --channel prod --worktree <リポジトリの根>/.worktrees/release/v<正式版>` と同じ雛形の出力で、`課題` にスプリントの課題が入る。 - -**失敗の形**: 変えない。雛形の宣言の不足は今と同じく `DeclError` で止まる(終了コード 2)。本番のブランチが無いときは止めずに本番を置かない(決定 3)。 - -**互換性**: 追加のみ。既存のステージの名前・番号・`command`・プランのファイルは変わらない。変わるのは `--state` のときの「配布」の `note` が「`関門 2`」の `gate` へ移ることで、これを読むのは conductor(`next` の文が指す)だけである。既に書き出した目録で進行中のスプリントは、今までどおり単独の `new release --channel prod` で本番のプランを作れる。 - -## 処理の流れ - -```mermaid -sequenceDiagram - participant C as conductor - participant S as new sprint - participant Q as queue - C->>S: new sprint(normal・--version 10.18.0-dev.1) - S-->>C: sprint.json(… 検査 → 配布 → 関門 2 → 本番) - C->>Q: 検査の command(--then 配布) - Q->>Q: 検査がスプリントの PR をマージ(報告に PR) - Q->>Q: 配布の {queue_pr:check} を埋めて開発版を流す - alt 検査か開発版が止まった - Q-->>C: stopped(本番の承認を求めない) - else facts が承認ゲート - Q-->>C: gate(承認資料) - end - C->>C: 関門 2 の gate を読み、承認を求める - alt 差し戻された - C->>C: 本番の command を打たない - else 承認された - C->>Q: 本番の command --done(単独) - Q->>Q: 同じディレクトリの検査の報告から {queue_pr:check} を埋める - Q-->>C: 本番の配布・リリース後テスト・後片付けの結果 - end -``` - -検査の報告が無い・PR が無いときは `{queue_pr:check}` が `0` に埋まり、開発版の配布(#1915)と同じ振る舞いになる。この課題で新しい扱いを足さない。承認の前に本番の `command` を打つことはスクリプトが強制しない(承認の記録を本番のプランの先頭に置くのは #1920)。 - -## 非機能の実現方式 - -| 大項目 | 条件 | 実現方式 | -| --- | --- | --- | -| 運用・保守性 | 承認ゲート 2 の後に conductor が組み立てるコマンドが 0 本 | 本番のプランの版・PR・worktreeを `new sprint` が決め、`command` を目録へ書く。手順書の手順 9 はその `command` と `wait` だけを書く | -| 移行性 | 進行中のスプリントは今までどおり本番のプランを作れる | 単独の `new release --channel prod` を残す。目録の形は追加のみ | - -## 決定の記録 - -### 決定 1: 承認ゲート 2 の文の正本を 1 か所にするため、雛形の経路では文を `関門 2` のステージの gate へ移し、配布の note から外す - -承認ゲート 2 のステージができると、承認の手順を書く場所が `gate` と「配布」の `note` の 2 つになる。2 つに置くと片方だけが直され、conductor が古い方を読む。`関門 1` が `gate` に助言の MVV 判定と答えを状態へ書くコマンドを持つ形とも揃う。雛形の無い経路(`route_waves`)は承認ゲート 2 のステージを持たないため、今の `note` のまま残す(受け入れ条件 5)。 - -「配布」の `note` に残して `gate` には承認資料だけを書く形は、承認の手順が 2 つのステージに分かれるため採らない。 - -根拠: Value 7 / Value 2(MVV 版 2) - -### 決定 2: 配布のプランの組み立てを 1 つにするため、本番のプランは plan_sprint_release にチャネルと版を渡して作る - -`plan_sprint_release` は出す PR を `{queue_pr:check}` に揃えた normal の配布の組み立てで、チャネルと版を引数にすれば本番にもそのまま使える。2026-10-24 に試作した本番のプランを、conductor が打っていた `new release --version 10.18.0 --prs 4242 --channel prod` の出力と比べると、出す PR の欄(`4242` と `{queue_pr:check}`)と `課題` の欄(手で打つ形は空)を除いて 13 ステップすべてが一致した。 - -`plan_mvv_release` を使う形は採らない。`plan_mvv_release` は `mvv=a.state` を雛形へ渡すため、`--state` を渡した normal では本番のプランの先頭に MVV 判定の承認ゲートが入り、利用者の承認の後にもう一度判定で止まる(I4)。MVV を外す引数を足すと、fast / auto 用の関数に normal の分岐が入る。 - -根拠: Value 6(MVV 版 2) - -### 決定 3: 今の出力を壊さないため、本番のブランチが決まっていないときは承認ゲート 2 と本番のステージを置かない - -雛形の本番のプランは本番のブランチ(`a.production_branch`)が無いと `DeclError` で組めない。normal の `new sprint` は今は本番のブランチを要さず、宣言に無いリポジトリでも開発版まで流せる。本番を組もうとして止めると、そのリポジトリの `new sprint` が開発版も含めて流せなくなる。本番のブランチが無ければ今と同じく「配布」で終え、`next` に本番を置かなかった理由を 1 文書く。本番のブランチを既定ブランチへ後退させないこと自体は #2097 が扱う。 - -本番のブランチが無いときに `new sprint` を止める形は、今まで流せたスプリントを止めるため採らない。 - -根拠: Value 2 / Value 1(MVV 版 2) - -## テスト設計 - -| 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | -| --- | --- | --- | -| 受け入れ条件 1・I2 | normal・雛形・本番のブランチありの `new sprint` の `ステージ` の名前が「… 検査・配布・`関門 2`・本番」の順で、「`関門 2`」が `gate` を持ち、「本番」のプランの名前が `release-prod` | 「本番」を足さない・「`関門 2`」を「本番」の後に置くと落ちる | -| 受け入れ条件 1・I3 | 「本番」のプランの `release` のステップのコマンドが `--channel prod` と `--version 10.18.0`(`--version 10.18.0-dev.1` を渡す)を持ち、`changelog` の `--prs` が `{queue_pr:check}` | `prod_version` を通さず `a.version` を渡す・チャネルを `dev` のままにすると落ちる | -| 受け入れ条件 2・I1 | 「本番」が `then_of` を持たず `command` を持ち、「検査」の `command` の `--then` に本番のプランが入らない | 「本番」に `then_of: 検査` を付けると落ちる | -| 受け入れ条件 3 | 一時ディレクトリに検査のプランとその報告(PR 番号つき)と本番のプランだけを置き、前のステージの結果を渡さずに `fill_queue_pr` を当てると、本番のプランの `{queue_pr:check}` がすべてその番号になる | 同じディレクトリの報告を読まない(`results_named` が `items` だけを見る)と `0` になって落ちる | -| 受け入れ条件 4・I5 | `--state` ありで「`関門 2`」の `gate` が `sprint-state.py` の `gate` のコマンド(`"関門 2"`・`--by user`)を含み、「配布」が `note` を持たない。`--state` なしで `gate` が `sprint-state.py` を含まない | 「配布」に `note` を残す・`--state` なしでも `normal_gate_2` を続けると落ちる | -| I4 | `--state` ありの normal の本番のプランが `mvv`・`note`・`handoff` のステップを持たない | 本番を `plan_mvv_release` で組むと落ちる | -| 受け入れ条件 5 | 雛形の無い経路(昇格・手で届ける)の normal の目録が今の並びと `note` のままで、fast / auto の目録が今の並びのまま(既存のテストの期待を変えない) | 雛形の無い経路にも「`関門 2`」を足す・`route_waves` の `note` を外すと落ちる | -| I6・I5 | 本番のブランチの無い宣言(`plain_repo`)の normal の `new sprint` が成功し、最後のステージが「配布」。`--state` ありでは「配布」が今の `normal_gate_2` の文の `note`(`--by user` を含む)を持つ | 本番のブランチが無いのに本番を組もうとすると `DeclError` で落ちる・本番のブランチの有無によらず「配布」の `note` を外すと落ちる | -| 受け入れ条件 6・7 | テストを書かない(`.md` の文言を照合しない。`AGENTS.md`)。手順 9 の形は、`new sprint` を一時ディレクトリへ打って「本番」のステージの `command` と一致すること、プランのファイルがあることを実装の中で 1 度確かめる(要求の前提 4。本番のキューは打たない) | — | -| 受け入れ条件 8 | `.ndf/project.json` の `test` の全体が通る | — | - -## 設計の結果 - -| 課題 | 扱い | 取り込み先 | 触るファイル | -| --- | --- | --- | --- | -| #1914 | 実装する | — | `plugins/ndf/scripts/supervise_lib/sprint.py`、`plugins/ndf/scripts/supervise_lib/sprint_waves.py`、`plugins/ndf/scripts/tests/`、`plugins/ndf/skills/development-workflow/references/waiting.md`、`plugins/ndf/skills/development-workflow/references/agent-layers.md`、`plugins/ndf/skills/development-workflow/references/relay.md` | - -## 未確認のまま残ること - -| 項目 | 内容 | -| --- | --- | -| 本物の本番の配布 | 本番のプランを単独で流すのは、次の `pace: normal` のスプリントの承認ゲート 2 の後が最初になる。検査では本番へ配らない(要求の前提 3)。リリース後テストで確かめる | -| 本番のブランチの後退 | `worktree.json` に `production_branch` が無く既定ブランチを本番にするリポジトリでは、決定 3 により本番のステージが置かれない。直すのは #2097 | -| 承認の前に本番を打つこと | スクリプトは承認ゲート 2 の答えを確かめずに本番の `command` を流せる。承認の記録を本番のプランの先頭に置くのは #1920 | diff --git a/issues/issue-1914-requirements.md b/issues/issue-1914-requirements.md deleted file mode 100644 index 4e9d003e7..000000000 --- a/issues/issue-1914-requirements.md +++ /dev/null @@ -1,166 +0,0 @@ -# #1914: pace: normal のスプリントでも、本番の配布のプランを new sprint が書き出す - -正は課題の本文(#1914)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。 - -## 依頼(原文) - -> ## 要求 -> -> `pace: normal` の `supervise.py new sprint` が、release の雛形で配る経路(`リリースの経路: template / package-plugin`)のとき、開発版の配布のステージの後に承認ゲート 2 と本番の配布のステージを書き出す。`waiting.md` の手順 8〜9 を、承認の後のキューと待ちだけにする。本番の版はスプリントの版(`--version`)から接尾辞を外したもの、出す PR はスプリントの PR(検査のプランの報告)から読む。 -> -> 今は fast / auto だけが本番のプランを書き、normal は「7 配布」で終わる。conductor が `supervise.py new release --channel prod` で `8-release-prod.json` を手で作る(m1693・m1385・m744・m653・m1683。m1385 では作る前にキューを打って空振りした)。 -> -> ## 今の作り -> -> - `plugins/ndf/scripts/supervise_lib/sprint.py:325-330`: normal の `sprint_plans` は `has_release_template(a)` のとき「配布」(`plan_sprint_release`、開発版)だけを足して終わる -> - `plugins/ndf/scripts/supervise_lib/sprint.py:88-93`(fast)・`122-128`(auto): 「開発版」の後に「本番」(`plan_mvv_release(..., prod_version(a.version), "prod", ...)`)を置く。auto は出す PR を `{queue_pr:check}` で渡す -> - `plugins/ndf/scripts/supervise_lib/sprint.py:45-46`: `prod_version` が版の接尾辞を外す -> - `plugins/ndf/skills/development-workflow/references/waiting.md:167-169`: 手順 8(承認ゲート 2)・9(conductor が `sv new release --version … --prs <スプリントの PR 番号> --channel prod … --out $O/8-release-prod.json` を打ってからキュー) -> -> ## 受け入れ条件 -> -> - [ ] 1. normal(release の雛形の経路)の `new sprint` の `sprint.json` が、「配布」の後に「関門 2」(`gate`)と「本番」のステージを持ち、本番のプランは `--channel prod`・版は `--version` の接尾辞を外したものになる -> - [ ] 2. 本番のプランの出す PR は、検査のステージのプランの報告から読む(`{queue_pr:check}` と同じ置き場。関門の後に本番のプランだけを単独で流しても埋まる)。一時ディレクトリで検査の報告を置き、本番のプランを単独でキューに入れて確かめる -> - [ ] 3. release の雛形の無い経路(`route_waves`)の出力は変わらない -> - [ ] 4. `waiting.md` の手順 9 が「`sprint.json` の本番のステージの `command` に `--done` を足して打つ」と `wait` だけになる。書いたコマンドを打って確かめる -> - [ ] 5. 既存のテストが通る -> -> ## 由来 -> -> #1426 の「同じ形の箇所」の表の 4 行目(受け入れ条件 4 のうち表 4)。 -> -> ## 依存 -> -> - 先に: #1915(normal の配布のプランの PR の置き場を `{queue_pr:check}` に揃える。本番も同じ置き場を使う)。済み(PR #1930)。`plugins/ndf/scripts/supervise_lib/sprint_waves.py:315` の配布のプランが `{queue_pr:check}` を使う -> - 後に: #1920(本番のプランの先頭に承認の記録を置く) -> -> ## 見積 -> -> Size 3。ステージの追加は fast / auto の形を写すだけだが、関門 2 のステージ(normal は利用者が承認する。`normal_gate_2` の note の扱い)・`--state` の有無で助言の MVV 判定を挟む分岐・`waiting.md` と `relay.md` の手順の書き直しがある。 - -(書き手: 起票者、日付: 2026-10-11。#1426 の表から AI が起票した本文で、利用者の指示の原文は含まない。下の「今の作り」は 2026-10-24 の版で読み直したもの) - -> 人へ問わずに進め、決められない点は前提か未決として課題の本文へ書く。 - -(書き手: AI の起動指示、日付: 2026-10-24) - -## 起票者の推定 - -題・本文はすべて起票者が #1426 から推して書いたもので、利用者の指示の原文は無い。承認ゲート 1 で利用者が確かめるまで、利用者の決定として扱わない。 - -| # | 条件 | 推した出所 | この要求での扱い | -| --- | --- | --- | --- | -| S1 | normal でも release の雛形の経路では、`new sprint` が本番のプランまで書き出す | #1426 の「同じ形の箇所」の表 4 行目、conductor の手当て(m1693・m1385・m744・m653・m1683) | 受け入れ条件 1 | -| S2 | 本番の版は `--version` から接尾辞を外したもの | fast / auto の `prod_version` | 受け入れ条件 1 | -| S3 | 本番の出す PR は検査の報告から読む(`{queue_pr:check}`) | #1915(PR #1930)の開発版の置き場 | 受け入れ条件 2 | -| S4 | 本番を単独でキューに入れて確かめる | 起票時の受け入れ条件 2 | 前提 3 へ置き換えた(本物の配布が走るため、置き換えの処理を単独で通して確かめる) | -| S5 | release の雛形の無い経路の出力は変わらない | 起票時の受け入れ条件 3 | 受け入れ条件 5 | -| S6 | `waiting.md` の手順 9 をキューと待ちだけにし、書いたコマンドを打って確かめる | 起票時の受け入れ条件 4 | 受け入れ条件 6・前提 4 | -| S7 | `relay.md` の手順も書き直す | 起票時の見積 | 受け入れ条件 7(`relay.md` と `agent-layers.md` の、本番のプランを目録の外で作る記述) | -| S8 | 本番のプランの先頭に承認の記録を置くのは #1920 が扱う | 起票時の依存 | 含まない | - -## 目的 - -- `pace: normal` のスプリントで、承認ゲート 2 の後に conductor が組み立てるコマンドを無くし、`sprint.json` のステージの `command` に `--done` を足して打つだけで本番の配布が流れるようにする。conductor が本番のプランを手で作る手当て(5 スプリントで記録あり。1 回はプランを作る前にキューを打って空振りした)を、スクリプトの側へ移す - -## 今の作り - -- `plugins/ndf/scripts/supervise_lib/sprint.py` の `sprint_plans`(normal): `has_release_template(a)` のとき「配布」(`plan_sprint_release`、開発版、計画名 `release`、`then_of: 検査`)だけを足して終わる。`--state` があれば `normal_gate_2` の文を「配布」の `note` に置く -- 同じファイルの `fast_sprint_plans`・`auto_sprint_plans`: 「開発版」の後に「本番」(`plan_mvv_release(..., prod_version(a.version), "prod", ...)`、計画名 `release-prod`)を置く。auto は出す PR を `{queue_pr:check}` で渡す -- `plugins/ndf/scripts/supervise_lib/sprint_routes.py` の `record_pr_ref`: 雛形の経路では配布の記録の PR を `{queue_pr:release-prod}` と読む(本番の計画名は `release-prod` を前提にしている) -- `plugins/ndf/scripts/supervise_lib/queue.py` の `fill_queue_pr`: 前のステージに名前の一致する計画が無ければ、同じディレクトリの計画の報告を読む(承認ゲートの後に単独で流すとき) -- `plugins/ndf/skills/development-workflow/references/waiting.md` の手順 8・9: 承認ゲート 2 の後、conductor が `sv new release --version … --prs <スプリントの PR 番号> --channel prod … --out $O/8-release-prod.json` を打ってからキューに入れる -- `plugins/ndf/skills/development-workflow/references/agent-layers.md` の表の「仕上げ」の行: 本番のリリースプランを「目録(`/sprint.json`)のステージの外」と書く -- `plugins/ndf/skills/development-workflow/references/relay.md` の「カットポイントの引継ぎ文書と ndf-next はスクリプトで作る」の例: 本番のプランを `$O/plan-release-prod.json` と書く - -## 前提 - -- 前提 1: 本番のプランは承認ゲート 2 の承認の後に conductor が単独で流す。開発版の配布の queue の `--then` には連結しない(normal の承認ゲート 2 は利用者が答える。連結すると承認を待たずに本番が流れる) -- 前提 2: 本番のプランは MVV 判定のステップを持たない(`pace: normal` の承認ゲート 2 は利用者の承認であり、助言の MVV 判定は開発版の配布の承認資料に載る。#1400)。承認したコミットは、開発版の配布が書いた承認資料(`issues/approval-<プラグイン>-v<版>.md`)から今の `release --channel prod --approval` と同じ形で読む -- 前提 3: 受け入れ条件 2 の確かめは、本物の配布を走らせずに行う。queue がステージを流す前に `{queue_pr:<名>}` を埋める処理を、一時ディレクトリに置いた本番のプランと検査の報告に単独で当て、埋まった PR 番号を見る(本番のプランを本当にキューへ入れると本番への配布が走る。共通原則 C4・C5 に当たる操作を検査で起こさない) -- 前提 4: 受け入れ条件 6 の「書いたコマンドを打って確かめる」は、`new sprint` を一時ディレクトリへ打ち、書き出した `sprint.json` の本番のステージの `command` が手順 9 の形と一致すること、その `command` のプランのファイルが存在することを見る。本番のキューは打たない(前提 3 と同じ理由) -- 前提 5: 検査の報告が無い・PR が無いときに `{queue_pr:check}` が `0` に埋まった後の振る舞いは、開発版の配布(#1915)と同じにする。この課題で新しい扱いを足さない -- 前提 6: `--version` が接尾辞を持たないときも `prod_version` をそのまま使う(fast / auto と同じ。開発版と本番が同じ版になる扱いはこの課題で変えない) - -## 対象範囲 - -含む: -- `pace: normal` で release の雛形の経路があるときの `new sprint` の出力(承認ゲート 2 のステージと本番のステージの追加) -- `--state` の有無による承認ゲート 2 の文(助言の MVV 判定と、利用者の答えを状態へ書くコマンド)の置き場 -- `waiting.md` の手順 8・9、`agent-layers.md` の「仕上げ」の行、`relay.md` の例の、本番のプランの作り方の記述 - -含まない: -- `pace: fast` / `pace: auto` のステージ(既に本番を持つ) -- release の雛形の無い経路(`route_waves`)・手動反映の本番系の形(`manual_production_waves`) -- 本番のプランの先頭に承認の記録を置くこと(#1920) -- `supervise.py new release --channel prod` を単独で打つ経路の削除(スプリントの外でリリースする場面で使う) -- 承認ゲートの数と置き場所の変更(`development-workflow` の「人手の承認を求める承認ゲート」が持つ) - -## ドメインイベント - -| # | イベント | 引き金 | 失敗したとき | 順序の前提 | -| --- | --- | --- | --- | --- | -| E1 | `new sprint` が配布・承認ゲート 2・本番のステージを `sprint.json` へ書き出した | conductor が `pace: normal` で `new sprint` を打った(release の雛形の経路あり) | 設定の不足なら今と同じく止まり、ステージを書かない | — | -| E2 | 検査のプランがスプリントの PR をマージし、報告に PR を残した | 検査のステージの `command` | 止まった / 承認ゲートを返したなら配布へ進まない(今と同じ) | E1 | -| E3 | 開発版の配布が承認資料を書き、承認ゲートを返した | 検査の queue の `--then` | 止まったなら本番の承認を求めない | E2 | -| E4 | 利用者が本番への配布を承認した(`--state` があれば `sprint-state.py gate … "関門 2" --by user` を打った) | conductor が承認資料を示して問うた | 差し戻されたら本番のステージを打たない | E3 | -| E5 | 本番のプランの `{queue_pr:check}` が検査の報告の PR 番号に埋まった | conductor が本番のステージの `command` に `--done` を足して打った | 報告が無ければ `0`(前提 5) | E2。E4 の後に打つことはスクリプトが強制しない(#1920 が記録を置く) | -| E6 | 本番の配布・リリース後テスト・後片付けが終わった | E5 に続くプランのステップ | プランが止まり、conductor が報告を読む(今と同じ) | E5 | - -## 受け入れ条件 - -- [ ] 1. `pace: normal` で release の雛形の経路があるとき、`new sprint` の `sprint.json` の `ステージ` が「配布」の後に、`gate` を持つ承認ゲート 2 のステージ(名前 `関門 2`)と、計画名 `release-prod` のプランを持つ本番のステージ(名前 `本番`)をこの順に持つ。本番のプランは `--channel prod` の形で、版は `--version` から接尾辞を外したもの(`10.18.0-dev.1` → `10.18.0`)になる -- [ ] 2. 本番のステージは `then_of` を持たず、自分の `command` を持つ(どの queue の `--then` にも連結されない) -- [ ] 3. 本番のプランの出す PR は `{queue_pr:check}` で書かれ、一時ディレクトリに検査の計画の報告(PR 番号つき)と本番のプランだけを置いて、前のステージの結果を渡さずに置き換えの処理を当てると、その PR 番号に埋まる -- [ ] 4. `--state` を渡したとき、承認ゲート 2 のステージの文に、利用者の答えを状態へ書くコマンド(`sprint-state.py gate <状態> "関門 2" … --by user`)と助言の MVV 判定の扱いが載る。`--state` を渡さないとき、その文は承認資料を示して本番の承認を取ることだけを書き、`sprint-state.py` のコマンドを含まない -- [ ] 5. release の雛形の経路が無いとき(`route_waves` の経路)、および `pace: fast` / `pace: auto` のとき、`new sprint` が書き出す `sprint.json` とプランのファイルは変更の前と同じになる -- [ ] 6. `waiting.md` の手順 9 が、`sprint.json` の本番のステージの `command` に `--done` を足して打つことと `wait` だけを書き、`sv new release` を含まない -- [ ] 7. `development-workflow` の参照文書(`waiting.md`・`agent-layers.md`・`relay.md`)に、`pace: normal` のスプリントで本番のプランを目録の外で作る、または `sv new release --channel prod` で作るとする記述が残らない -- [ ] 8. 既存のテストが通る - -## 非機能の条件 - -| 大項目 | 条件 | -| --- | --- | -| 運用・保守性 | 承認ゲート 2 の後に conductor が組み立てるコマンドが 0 本になる(打つのは `sprint.json` の `command` と `wait` だけ) | -| 移行性 | 既に書き出した `sprint.json` を持つ進行中のスプリントは、今までどおり `new release --channel prod` で本番のプランを作れる(単独の `new release` は残す) | - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| 公開インタフェース | 変わる。`new sprint`(normal、雛形の経路)の出力にステージが 2 つ増える。既存のステージの名前・番号・`command` は変わらない(追加のみで互換あり) | -| データ | 無し | -| 既存の振る舞い | normal の雛形の経路で、承認ゲート 2 の文の置き場が変わりうる(設計で決める)。fast / auto・雛形の無い経路は変わらない | - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| 起動 | `python3 plugins/ndf/scripts/supervise.py new sprint --name t --worktree . --issue <番号> --version 10.18.0-dev.1 --out <一時ディレクトリ>` を打ち、`sprint.json` を読む | -| テスト | `uv run --frozen --project . --all-extras pytest plugins/ndf/scripts/tests -q -n 4`(`.ndf/project.json` の `test`) | -| 静的解析・型検査 | `bash scripts/check-lint.sh`、`python3 scripts/check-script-structure.py` | -| 手動確認 | 次の `pace: normal` のスプリントの承認ゲート 2 の後に、conductor が `sprint.json` の本番のステージの `command` だけで本番の配布を流せたか(リリース後テストで確かめる) | - -## 前提とする取り決め - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | 組み立ては `plugins/ndf/scripts/supervise_lib/` に置く。`AGENTS.md` の「安定版と実験版」の安定版(既定で働く)に当たり、`experimental/` に置かない | -| コーディング規約 | `AGENTS.md` の「最小限のコード実装」。同じ役割の関数を pace ごとに分けない(MVV の Value 6)。fast / auto の本番のステージの組み立てと共通にできる部分は共通にする | -| テスト戦略 | `new sprint` の出力(ステージの並び・`then_of`・プランの版とチャネル)と、`{queue_pr:check}` の単独の置き換えを単体テストで見る。`.md` の文言を照合するテストは書かない(`AGENTS.md`) | - -## 境界 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 既存テストの実行、リンタの適用、手順に書くコマンドを書く前に打って確かめる | -| 確認してから行う | `new sprint` の出力の形の変更(承認ゲート 1・2 で人が見る) | -| 行わない | 本番のプランを開発版の queue へ連結すること、承認ゲートの数を変えること、本物の本番の配布を検査で走らせること | - -## 未決 - -| 項目 | 誰が決めるか | 期限 | -| --- | --- | --- | -| `--state` のときの助言の MVV 判定の文(今の `normal_gate_2`)を「配布」の `note` に残すか、承認ゲート 2 のステージの `gate` へ移すか | 設計(`design`)。承認ゲート 1 で利用者が確かめる | 設計 PR | -| 本番のプランを `plan_sprint_release` にチャネルと版を渡して作るか、`plan_mvv_release` を MVV なしで使うか(Value 6 の共通化の仕方) | 設計(`design`) | 設計 PR | From e6e16021a210d9af798572e8f05b8d70908f684d Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sun, 25 Oct 2026 07:27:35 +0000 Subject: [PATCH 03/17] =?UTF-8?q?Docs:=20cross-refactoring-scope-template-?= =?UTF-8?q?probe.md=20=E3=82=92=E7=A2=BA=E5=AE=9A=E4=BB=95=E6=A7=98?= =?UTF-8?q?=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/glossary.md | 4 +- docs/glossary/glossary.json | 6 +- docs/specifications/README.md | 1 + .../cross-refactoring-scope-template-probe.md | 267 +++++++++++ issues/issue-1630-design.md | 436 ------------------ issues/issue-1630-requirements.md | 203 -------- 6 files changed, 272 insertions(+), 645 deletions(-) create mode 100644 docs/specifications/cross-refactoring-scope-template-probe.md delete mode 100644 issues/issue-1630-design.md delete mode 100644 issues/issue-1630-requirements.md diff --git a/docs/glossary.md b/docs/glossary.md index 153917bdb..f3a043742 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -203,7 +203,7 @@ NDF が提供する工程・承認ゲート・モード・ステップの語。d | 代表の run | — | 宣言の解析がワークフローごとに壁時計とテストの所要を測る 1 件の run。取得した候補(ワークフローごとに新しい順の成功の run を最大 5 件)のうち、ジョブを飛ばしていない最新のもの | — | — | `docs/specifications/project-decl-ci-representative-run.md` | | 入力の指紋 | — | 解析が読んだファイルの、HEAD の木での blob の SHA と、ブランチの構成の組。プロジェクトの宣言の新しさの判定に使う | — | — | — | | テストの戦略 | `strategy` | 範囲テストの走らせ方・全体テストの置き場(手元か CI か)・落ちたテストの見分け方の組。local-full / local-scoped-ci-whole / round-only の 3 つ。宣言の test.strategy か、同じ関数が所要から導く(#1334) | — | — | — | -| 範囲テストのテンプレート | `scope_command` | {paths} を引用の外の 1 字句として含むテストのコマンド(宣言の scope_command か、{paths} を含む引数)。{paths} をシェルの引用で守った対象の並びへ置き換え、シェルで走らせる。{paths} には、改修の範囲テストではテストのファイルのパス、CI に任せる戦略の着手前のテストではテストの置き場所(ディレクトリかファイル)、静的解析では変更したファイルのパスが入る | 範囲テストの雛形 | — | — | +| 範囲テストのテンプレート | `scope_command` | {paths} を引用の外の 1 字句として含むテストのコマンド(宣言の scope_command か、{paths} を含む引数)。{paths} をシェルの引用で守った対象の並びへ置き換え、シェルで走らせる。{paths} には、改修の範囲テストではテストのファイルのパス、CI に任せる戦略の着手前のテストではテストの置き場所(ディレクトリかファイル)、静的解析では変更したファイルのパスが入る | 範囲テストの雛形 | — | `docs/specifications/cross-refactoring-scope-template-probe.md` | | JUnit の置き場 | `junit` | テストのコマンドが JUnit XML を書くファイルの、作業ディレクトリからの相対パス(宣言の suites[].junit)。NDF はコマンドへ引数を足さず、このファイルを読む | — | — | — | | スプリント | `sprint` | 1 回のリリースとして出す課題と Pull Request のセット。版数を持つプロジェクトでは 1 つの版になる。期間ではなく、1 回のリリースとして出す中身で切る。工程はスプリント単位で 1 回ずつ通し、モードもスプリントで 1 つにする | ミッション | — | `plugins/ndf/skills/development-workflow/references/glossary.md` | | スプリント状態ファイル | `sprint_state` | スプリントのプラン・done・承認ゲートの記録・MVV・版を持つファイル(パスは呼ぶ側が決め、手順書の例は `sprint-state.json`。目録 `sprint.json` とは別のファイル) | ミッション状態ファイル、ミッションの状態 | — | `plugins/ndf/skills/development-workflow/references/glossary.md` | @@ -465,7 +465,7 @@ Pull Request のレビューを CLI へ委譲し、新しい指摘が出なく | 確かめの見込み | `gate_seconds` | 最終ゲートの確かめ 1 回にかかる見込みの秒(limits.gate_seconds)。手元で全体テストを走らせるなら w、CI で見るなら c、どちらも測れていなければ 0 | — | — | `docs/specifications/cross-refactoring-verify-and-final-gate.md` | | 確かめの開始の締め切り | `gate_start_end` | 想定最大時間の終わり − 確かめの見込み(H − v)。最終ゲートの確かめを始められる最後の時刻で、最終ゲート修正と打ち切りの後の取り消しもこの時刻までに止まる | — | — | `docs/specifications/cross-refactoring-verify-and-final-gate.md` | | テストの無い宣言 | — | .ndf/project.json の test があり、unknown と strategy を持たず、種別・雛形の誤りが無く、どの suite も command と scope_command のどちらも持たないもの(suite が 0 個のときを含む)。解析が「テストが無い」と測った {"suites": []} を含む。cross-review の最終スイープは検証のコマンドを名指ししない | — | — | — | -| テンプレートの試走 | — | cross-refactoring の init が着手前のテストの後に、範囲テストのテンプレートへテストのファイルを最大 2 本入れて 1 回走らせ、{paths} に入るパスを受けるかを見分けること。不備か判定できなければ改修の前に止まる | — | — | — | +| テンプレートの試走 | — | cross-refactoring の init が着手前のテストの後に、範囲テストのテンプレートへテストのファイルを最大 2 本入れて 1 回走らせ、{paths} に入るパスを受けるかを見分けること。不備か判定できなければ改修の前に止まる | — | — | `docs/specifications/cross-refactoring-scope-template-probe.md` | ## NDF のラッパー(`ndf-relay`) diff --git a/docs/glossary/glossary.json b/docs/glossary/glossary.json index 86a5b8a47..3a74cd274 100644 --- a/docs/glossary/glossary.json +++ b/docs/glossary/glossary.json @@ -2378,8 +2378,7 @@ "context": "ndf-workflow", "meaning": "{paths} を引用の外の 1 字句として含むテストのコマンド(宣言の scope_command か、{paths} を含む引数)。{paths} をシェルの引用で守った対象の並びへ置き換え、シェルで走らせる。{paths} には、改修の範囲テストではテストのファイルのパス、CI に任せる戦略の着手前のテストではテストの置き場所(ディレクトリかファイル)、静的解析では変更したファイルのパスが入る", "code": "scope_command", - "source": "", - "pending_source": "issues/issue-1630-design.md", + "source": "docs/specifications/cross-refactoring-scope-template-probe.md", "deprecated": [ "範囲テストの雛形" ] @@ -4237,8 +4236,7 @@ "term": "テンプレートの試走", "context": "ndf-cross-refactoring", "meaning": "cross-refactoring の init が着手前のテストの後に、範囲テストのテンプレートへテストのファイルを最大 2 本入れて 1 回走らせ、{paths} に入るパスを受けるかを見分けること。不備か判定できなければ改修の前に止まる", - "source": "", - "pending_source": "issues/issue-1630-design.md" + "source": "docs/specifications/cross-refactoring-scope-template-probe.md" }, { "term": "読み手役", diff --git a/docs/specifications/README.md b/docs/specifications/README.md index c32323321..763399e94 100644 --- a/docs/specifications/README.md +++ b/docs/specifications/README.md @@ -96,5 +96,6 @@ | [ndf-slide-generation.md](ndf-slide-generation.md) | スライドの生成スクリプトの経路(汎用テンプレートのページの枠組みと座標・theme.json の重ね合わせと検査・台本からのスピーカーノートと尺・build-deck.js と script-time.js と slides-thumbnails.py の終了コード・常に成り立つ条件・決定の理由)。作り方の手順は release の form-slide.md が正 | | [ndf-document-rebuild.md](ndf-document-rebuild.md) | 文書の組み直し(文書形式と文書の目的の切り方、document-style との正本の割り当て、キーワードと読み手役の確かめ・題の付け方の置き場、スラッシュコマンドの一覧から外す Skill、常に成り立つ条件と決定の理由)。手順は document-rebuild の SKILL.md が正 | | [ndf-workflow-development-loop.md](ndf-workflow-development-loop.md) | 開発ループ(`--loop`)。`sprint-state.py loop` の副命令の契約(終了コード 0/1/3・止まった結果の形)、選定の規則、承認ゲートで止まったときの扱い、止まる条件と再開、ループの記録(`.ndf/loop/<名>/`)の形、`new close --pace auto` と品質の枠、常に成り立つ条件、決定の理由と外部の動向。手順は `development-workflow` の `references/loop.md` が正 | +| [cross-refactoring-scope-template-probe.md](cross-refactoring-scope-template-probe.md) | cross-refactoring の init が着手前のテストの後に範囲テストのテンプレートを試走し(suite ごとにテストのファイルを最大 2 本)、`{paths}` に入るパスを受けない・確かめられないなら直す箇所を出して改修の前に止まる理由と規則(候補の選び方・判定の表・`template-check.json` の形)。止まり方は `cross-refactoring` の SKILL.md、`{paths}` に入るパスの種類は `development-workflow` の `references/project-analysis.md` が正 | Skill の挙動仕様はここに置かない。Skill に関する詳細は対象 Skill の `SKILL.md` を参照する。 diff --git a/docs/specifications/cross-refactoring-scope-template-probe.md b/docs/specifications/cross-refactoring-scope-template-probe.md new file mode 100644 index 000000000..2d00ee1d2 --- /dev/null +++ b/docs/specifications/cross-refactoring-scope-template-probe.md @@ -0,0 +1,267 @@ +# cross-refactoring: ファイルのパスを受けない範囲テストのテンプレートで改修の範囲テストが毎回落ち、修正のラウンドを使い切っていた → `init` が着手前のテストの後にテンプレートを試走し、受けなければ直す箇所を出して止まる + +## 概要 + +**cross-refactoring の `init` は、着手前のテストの後に、`scope_command` を持つテストの suite ごとにテストのファイルを最大 2 本 +`{paths}` へ入れて 1 回走らせる(テンプレートの試走)。** 判定が `ok` の suite だけが続き、不備(`defect`)か判定できない(`undecided`) +の suite が 1 つでもあれば、状態ファイルを作らずに終了コード 4 で止まる。止まるときは当たった suite・今のテンプレート・入れたパス・ +直す箇所(`.ndf/project.json` の `test.suites[<番号>].scope_command`)を標準エラーと `/template-check.json` に出す。 +改修の範囲テストはそもそも走らないため、テンプレートの不備で落ちた項目が修正へ回ることは無い。 + +あわせて、宣言の契約(`project.schema.json` の `Suite` の説明と、手順 0 の案内 `project-analysis.md` の `test` の行)が、`{paths}` に入る +パスの種類を場所ごとに書く。 + +例: `scope_command` が `python3 -m unittest discover -s {paths}`、`command` が `python3 -m unittest discover -s tests`、suite の `paths` が +`["tests"]` の宣言(テストは `tests/test_x.py`・`tests/test_y.py`)で `--scope scripts/storylib` の `init` を打つと、着手前の全体テストは通り +(基準 `green`)、試走 `python3 -m unittest discover -s tests/test_x.py tests/test_y.py` が終了コード 1 で落ちて `defect` になる。 + +```text +⛔ 範囲テストのテンプレートが {paths} に入るテストのファイルを受けません(suite: unit) + 今のテンプレート: python3 -m unittest discover -s {paths} + 入れたパス: tests/test_x.py tests/test_y.py + 試走: python3 -m unittest discover -s tests/test_x.py tests/test_y.py → 終了コード 1(着手前のテストは通り、試走が終了コード 1 で落ちた。着手前のテスト: green) + 直す箇所: .ndf/project.json の test.suites[0].scope_command({paths} にはテストのファイルのパスが入る。ファイルを受けるコマンドへ直すか、--round-test に {paths} を含まない全体テストを渡す) + ログ: /template-check-0.log +``` + +**止まり方・終了コードの読み方と、宣言の書き方は Skill と宣言の契約が正である。** ここに書き写さない。 + +| 何を | 正本 | +| --- | --- | +| 終了コード 4 の理由の並び(テンプレートの試走で止まることを含む) | [`cross-refactoring` の SKILL.md](../../plugins/ndf/skills/cross-refactoring/SKILL.md) の「実行」の終了コードの表 | +| `{paths}` に入るパスの種類と、ファイルを受けないランナー(unittest)の書き方 | `development-workflow` の [references/project-analysis.md](../../plugins/ndf/skills/development-workflow/references/project-analysis.md) の `test` の行と、[`project_lib/model.py`](../../plugins/ndf/scripts/project_lib/model.py) の `Suite` の説明(公開のスキーマ `schemas/project.schema.json` は `project-decl.py schema` で作る) | +| 着手前のテストの上限と手順の枠 | [cross-refactoring-init-test-and-phase-windows.md](cross-refactoring-init-test-and-phase-windows.md) | + +この文書が扱うのは、Skill に書かない決定の理由、常に成り立つ条件、判定の表と候補の選び方、記録の形、テスト観点である。 + +## 用語 + +定義は [用語集](../glossary.md) が正である(`.ndf/glossary.json` の `document`)。この文書で使う語は「テンプレートの試走」 +「範囲テストのテンプレート」「範囲テスト」「着手前のテスト」「既存失敗」「テストの戦略」である。 + +範囲テストのテンプレートの語(`ndf-workflow`)と試走の判定の規則は `lib/test_strategy.py` の純粋な関数が持ち、cross-refactoring +(`ndf-cross-refactoring`)は走らせて結果を渡すだけにする(顧客 / 供給者の関係)。supervise・`test-run.py` も同じ関数を後から使える。 + +## 対象範囲 + +| 扱う | 扱わない | +| --- | --- | +| cross-refactoring の `init` が、改修のラウンドに入る前にテンプレートを試走して止めること | 修正担当が「コード起因の失敗なし・コミットなし」を返しても次のラウンドへ進む振る舞い(#2095) | +| `{paths}` に入るパスの種類を宣言の契約と手順 0 の案内に書くこと | `{paths}` に入れるパスの種類を揃えること(CI に任せる戦略の着手前のテストへファイルを入れるなど。#2096) | +| 試走の記録(`template-check.json` と状態ファイルの `template_checks`) | `scope_command` を持たない suite(`round-only`)と静的解析の suite(`kind: lint`)の範囲テストの組み立て(変えない) | +| | 宣言の検査(`project-decl.py check`)・戦略の解決(`test_strategy.resolve`)の振る舞い(字句の不備だけを見るまま) | + +ランナーとテンプレートは宣言(`.ndf/project.json` の `test.suites[].scope_command`)か引数(`--round-test` / `--baseline-test` の +`{paths}` を含む値)で受け、ランナーの名前とコマンドの語は見ない。配布先のリポジトリでも働く。 + +## 背景 + +改修の範囲テストは、計画の `test_targets` の `::` より前のパス(テストのファイル)を `{paths}` へ入れて走らせる(`refactor_lib/targets.py` → +`test_strategy.scope_runs`)。範囲テストの成否は終了コードだけで決まり、0・126・127 以外は「落ちた」になる。そのため、テンプレートが +ファイルを受けずに 1 や 5 で終わっても、コードの失敗と同じ扱いで修正へ回った。 + +takemi-ohama/novel のスプリント world(PR takemi-ohama/novel#58)では、`scope_command` が `python3 -m unittest discover -s {paths}` で、 +13 件の改修のうち 9 件が範囲テストで落ち、修正担当が「コード起因の失敗なし」を返すまま修正のフェーズが 10 回以上続いた。回避は +検査のプランの引数へ `--round-test "python3 -m unittest discover -s tests"` を書くことだった。 + +ランナーによって受けるパスの種類は違い、コマンドの語からは決められない(Python 3.14.4 の手元の実測)。 + +| コマンド | 結果 | +| --- | --- | +| `python3 -m unittest discover -s tests/test_x.py` | `ImportError: Start directory is not importable`、exit 1 | +| `python3 -m unittest discover -s tests/test_x.py tests/test_y.py` | 2 つ目を開始のディレクトリと読まず、exit 1 | +| `python3 -m unittest discover -s scripts/storylib`(テストの無いディレクトリ) | `NO TESTS RAN`、exit 5 | +| `python3 -m unittest discover -s tests` | exit 0 | +| `python3 -m unittest tests/test_x.py tests/test_y.py` | exit 0 | +| `python3 -m unittest tests`(`__init__.py` の無いディレクトリ) | `NO TESTS RAN`、exit 5 | + +## 決定と理由 + +| # | 決定 | 理由 | 採らなかった案 | +| ---: | --- | --- | --- | +| 1 | 試走の場所を cross-refactoring の `init` にする | テンプレートがファイルを受けるかは走らせるしか分からず、走らせるには作業ディレクトリ・依存・テストのファイルが要る。`init` はそれを用意し、着手前のテストを走らせている | `project-decl.py check` で走らせる(純粋な検査で上限も作業ディレクトリも持たず、手順 0 の度に外部のコマンドが走る)。`test_strategy.resolve` で走らせる(純粋な層で、supervise・`test-run.py`・cross-review の解決にも実行が漏れる) | +| 2 | ファイルの選び方と判定を `test_strategy.py` の純粋な関数(`probe_paths`・`template_verdict`)にし、実行だけを cross-refactoring に置く | 範囲テストの組み立ては `test_strategy.py` に寄せている。同じ層に置けば、ほかの呼び手が後から同じ見分けを使うときに関数を分けずに済む | `refactor_lib/` の中に判定まで置く(Skill ごとに見分けの関数が分かれる) | +| 3 | 着手前のテストの後に走らせ、基準が通ったか既存失敗を除けたときだけ不備とする | 試走の失敗にはテンプレートの不備とコードの既存の失敗の 2 つの原因がありうる。着手前のテストがその suite で通っていれば、落ちる理由はテンプレートにしかない。どちらも成り立たなければ人へ戻す | 着手前のテストより先に試走する(基準が無く、既存の失敗を抱えたリポジトリでいつも取り違える)。判定できないときに続ける | +| 4 | 修正へ回す側には見分けを足さない | 不備か判定できない suite があれば `init` が状態ファイルを作らずに止まるため、改修の範囲テストはそもそも走らない | 検証で落ちた範囲テストを走らせ直して見分ける(項目ごとに走らせ直しが増え、着手前に 1 回で分かることを何度も確かめる) | +| 5 | `{paths}` に入るパスの種類は変えず、契約に場所ごとに書く | 求めるのは「入る種類が分かる」ことと「受けないなら止まる」ことで、揃えなくても満たせる。着手前のテストへ入れるパスを変えると範囲テストの所要の履歴(置き場所ごとの記録)にも効く | 改修の範囲テストと着手前のテストで入るパスを揃える(#2096 に残した) | +| 6 | 候補 0 本・上限の残り 0・打ち切りを `undecided` で止め、「判定しない」で続ける経路を持たない | 続けると、試走していない suite のテンプレートの不備で落ちた項目が修正へ回る経路が残る。候補はリポジトリ全体と suite の `paths` から選び直して試走できる場合を広げ、それでも確かめられないときだけ止める。止める文には直す箇所と `--round-test` の直し方を必ず添え、1 回で続けられるようにする | `skipped` の suite は範囲テストで落ちても修正へ回さない(修正へ回す側に見分けを足すことになり、決定 4 に反する) | +| 7 | 試走の記録を着手前のテストの記録と別の集約にし、持ち主を `template_check.py` にする | 記録を組んで `template-check.json` に書くのは `template_check.py` だけで、`setup.py` は返った値を状態ファイルの `template_checks` へそのまま置く(`baseline_test` と同じ置き方)。書く場所が割れない | `baseline.py` を経由して書く(着手前のテストの実行と記録の責務に、試走の判定と実行が入る) | + +## 仕様 + +### 構成要素と責務 + +| 要素 | 責務 | +| --- | --- | +| [`lib/test_strategy.py`](../../plugins/ndf/scripts/lib/test_strategy.py) | 純粋な関数と値オブジェクト。`probe_roots(suite)`(`paths` のうち `.` と glob を除いた要素)・`probe_paths`(候補から既存失敗のファイルを除いて最大 2 本(`PROBE_MAX_FILES`)を選び、`ProbePick(paths, source, owned)` を返す)・`template_verdict`(判定の表で `ProbeVerdict(status, reason)` を返す。`stops` は `defect` か `undecided` のとき真)。プロセスもファイルも触らない | +| [`refactor_lib/template_check.py`](../../plugins/ndf/skills/cross-refactoring/scripts/refactor_lib/template_check.py) | 試走の実行と記録(集約「テンプレートの試走の記録」の持ち主)。`targets_of` で対象の suite を選び、`check_templates` が候補を集めて `probe_paths` で選び、`fill` で組んだコマンドをシェルで走らせ、`outcome` と `template_verdict` で判定して `template-check.json` を書く。入口の `enforce` は止める判定なら止める文を出して `die`(終了コード 4)、続けるなら 1 行を出して記録を返す | +| [`refactor_lib/commands/setup.py`](../../plugins/ndf/skills/cross-refactoring/scripts/refactor_lib/commands/setup.py) | `_verify_init` で、着手前のテスト・`init_test` 行の追記・打ち切りの判定の後に `template_check.enforce` を呼ぶ。返った記録を状態ファイルの `template_checks` へそのまま置く | +| [`project_lib/model.py`](../../plugins/ndf/scripts/project_lib/model.py) | `Suite` の説明に、`{paths}` に入るパスの種類を場所ごとに書き、ファイルを受けないテンプレートは `init` が試走で止めることを書く。公開のスキーマはここから作る | + +```mermaid +graph LR + subgraph cr[cross-refactoring init] + setup[commands/setup.py] + base[baseline.py] + tc[template_check.py] + end + subgraph lib[共通の層 lib] + ts[test_strategy.py
probe_paths / template_verdict / fill / outcome] + end + setup --> base + setup --> tc + tc --> ts + base --> ts +``` + +| 集約 | 持ち主(書き換えてよいもの) | 根 | 値オブジェクト | +| --- | --- | --- | --- | +| テストの戦略 | `test_strategy.resolve`(`init` で 1 度解き、以後は状態ファイルの写しを読むだけ) | `Strategy`(エンティティは `Suite`) | `ScopeRun`・`Outcome` | +| 着手前のテストの記録 | `refactor_lib/baseline.py`(変えない) | 状態ファイルの `baseline_test` | suite ごとの成否・既存失敗 | +| テンプレートの試走の記録 | `refactor_lib/template_check.py` | 状態ファイルの `template_checks` と `/template-check.json` | suite ごとの入れたパス・結果・基準・判定・直す箇所 | + +試走は着手前のテストの記録を読むだけで `baseline_test` を書き換えず、試走の結果でテストの戦略を書き換えない。 + +### 常に成り立つ条件 + +| # | 条件 | 破れたときの扱い | +| --- | --- | --- | +| I1 | 試走のコマンドは改修の範囲テストと同じ組み立て(`fill(scope_command, paths)`)で作る。コマンドの語・ランナーの名前を見ない | テストで落とす | +| I2 | 試走は `scope_command` を持つテストの種別の suite ごとに高々 1 回、`init` の 1 回の起動の中で、着手前のテストの上限の残りの中で走らせる。試走の対象の suite は判定が `ok` のときだけ続ける | 上限の残りが 1 秒未満・打ち切り・候補 0 本は `undecided` で止める | +| I3 | 試走に入れるパスはその suite が受け持つテストのファイルを最大 2 本で、既存失敗の ID を持つファイルを除く(下の「候補のファイル」) | 3 つの場所とも 0 本なら `undecided` で止める | +| I4 | 「不備」と判定するのは、候補の受け持ちが確定していて、基準(その suite の着手前のテスト)が通った、または既存失敗を読めて試走のファイルから除けたうえで、試走が落ちたときだけ。受け持ちが未確定の候補で落ちたら基準によらず `undecided` | テストで落とす | +| I5 | 判定が `defect` か `undecided` の suite が 1 つでもあれば、`init` は状態ファイルを作らずに終了コード 4 で止まる。改修の範囲テストは走らない | テストで落とす | +| I6 | 止まるときも続けるときも試走の記録を `/template-check.json` に書く。続けるときは状態ファイルの `template_checks` が同じ `suites` の並びになる | テストで落とす | +| I7 | 静的解析の suite(`kind: lint`。`{paths}` に変更したファイルが入る)と `scope_command` の無い suite、戦略 `round-only` は試走しない。範囲テストの組み立て(`scope_runs`)は変えない | 既存のテストで落とす | + +### 判定の表(`template_verdict`) + +上の行から当て、最初に当たった行の判定を返す。 + +| 試走の結果 | 候補の受け持ち | 基準(suite の着手前のテスト) | 既存失敗を除けたか | 判定 | `init` | +| --- | --- | --- | --- | --- | --- | +| 走らせなかった(候補 0 本・上限の残りが無い) | — | — | — | `undecided` | 止める | +| `timed_out` | — | — | — | `undecided` | 止める | +| `passed` | — | — | — | `ok` | 続ける | +| `failed` 以外(起動できなかった) | — | — | — | `undecided` | 止める | +| `failed` | 未確定 | — | — | `undecided` | 止める | +| `failed` | 確定 | `green` | — | `defect` | 止める | +| `failed` | 確定 | `red` か `none` | 除けた(JUnit を読めた) | `defect` | 止める | +| `failed` | 確定 | `red` か `none` | 除けない | `undecided` | 止める | + +起動の失敗(`launch_failed`)は判定の前に、着手前のテストと同じ `launch.stop(None, …, "baseline", …)` で止まる。 + +### 候補のファイル(`probe_paths`) + +次の順に、最初に 1 本以上見つかった場所から、既存失敗のファイル(`test_triage.by_file`)を除き、並べ替えた先頭から最大 2 本を選ぶ。 +2 本にするのは、1 本は受けて 2 本目を受けないテンプレート(`discover -s a b` は 2 本目を開始のディレクトリと読まない)を見分けるためである。 + +| 順 | 場所 | `source` | 受け持ち | +| --- | --- | --- | --- | +| 1 | `--scope` のテストの置き場所の配下の、テストの名前の形の追跡ファイルを、`suite_groups` と同じ規則(`suite_for`、当たらなければ最初のテストの suite)でこの suite へ振り分けたもの | `scope` | 確定 | +| 2 | リポジトリの追跡ファイルでテストの名前の形のもののうち、`suite_for` がこの suite を返したものだけ(最初の suite へ寄せない) | `repo` | `paths` が `.` か空の suite では未確定、ほかは確定 | +| 3 | その suite の `paths` のうち `.` と glob を除いた要素の配下の追跡ファイル(名前の形を問わない) | `paths` | 未確定 | + +- 2 で最初の suite へ寄せないのは、どの suite の `paths` にも当たらない宣言していないテスト(`e2e/login.spec.ts` など)で正しいテンプレートが + 落ち、`defect` で止まるのを防ぐためである。`paths` が `.` か空の suite には `suite_for` が全ファイルを当てるため、そうしたファイルも候補に入り + 受け持ちが未確定になる +- 3 はテストの名前が形(`test_*`・`*_test.*`・`*.test.*`・`*_spec.*`・`*.spec.*`)に当たらないエコシステム(`FooTest.java`・`FooTests.cs`・ + `UserTest.php`・`*.bats`・Rust の `tests/*.rs`)でも、suite がテストの置き場所を `paths` に宣言していれば試走できるようにする。配下にはテストで + ないファイル(`tests/TestCase.php`・`tests/fixtures/*.json`・`tests/common/mod.rs`)が残りうるため受け持ちは未確定である +- 基準は、`source: scope` ならその suite の着手前のテストの成否、`repo` か `paths` なら着手前のテストがその suite の `command`(全体)で + 通ったときだけ `green` でほかは `none`。既存失敗を「除けた」とするのは `source: scope` で既存失敗を読めたときだけである + +### 止めるときの文 + +suite ごとに 1 段で、「受けません」(`defect`)か「受けるかを確かめられません」(`undecided`)、今のテンプレート・入れたパス・試走の +コマンドと終了コード(走らせていなければ理由)・直す箇所を出す。直す箇所は宣言の suite なら `.ndf/project.json の test.suites[<番号>].scope_command` +(番号が引けなければ `name` で示す)、`--round-test` / `--baseline-test` の雛形の suite(`args`)なら「`--round-test` の値」のように引数の名前で出す。 +直し方は判定によらず「ファイルを受けるコマンドへ直す」と「`--round-test` に `{paths}` を含まない全体テストを渡す」を必ず添え、入れたパスが +0 本なら「その suite の `paths` にテストの置き場所を宣言する」も添える。続けるときは標準エラーに +`✅ 範囲テストのテンプレートを試走: (<入れたパス>)` の 1 行を出す。 + +## データ・設定 + +`/template-check.json`(状態ファイルの `template_checks` は同じ形の `suites` の並び。旧い状態ファイルでキーが無ければ空の並びとして読む): + +```json +{ + "checked_at": "2026-10-24T12:00:00Z", + "stopped": true, + "suites": [ + { + "suite": "unit", + "index": 0, + "fix_at": ".ndf/project.json の test.suites[0].scope_command", + "scope_command": "python3 -m unittest discover -s {paths}", + "paths": ["tests/test_x.py", "tests/test_y.py"], + "source": "repo", + "owned": true, + "command": "python3 -m unittest discover -s tests/test_x.py tests/test_y.py", + "code": 1, + "outcome": "failed", + "reference": "green", + "verdict": "defect", + "reason": "着手前のテストは通り、試走が終了コード 1 で落ちた", + "log": "/template-check-0.log" + } + ] +} +``` + +| キー | 型 | 意味 | +| --- | --- | --- | +| `stopped` | bool | `init` を止めたか | +| `suites[].index` | int か null | 宣言の `test.suites` の番号。引数の雛形か引けなければ null | +| `suites[].fix_at` | str | 直す箇所 | +| `suites[].paths` | list[str] | 試走で `{paths}` へ入れたパス(0〜2 本) | +| `suites[].source` / `owned` | str か null / bool | 候補を選んだ場所(`scope` / `repo` / `paths`。0 本なら null)と受け持ちが確定しているか | +| `suites[].command` / `code` / `outcome` | str か null / int か null / str | 試走のコマンド・終了コード・`test_strategy.outcome` の結果。走らせなかったら null・null・`not_run` | +| `suites[].reference` | str | 基準(`green` / `red` / `none`) | +| `suites[].verdict` / `reason` | str / str | 判定(`ok` / `defect` / `undecided`)と理由 | +| `suites[].log` | str か null | 試走の出力のログ。走らせなかったら null | + +試走が書いた JUnit は、後の手順が前の実行の結果を読まないように試走の後に消す(`test_triage.clear_junit`)。 + +## 運用 + +### 既知の限界 + +| 項目 | 内容 | +| --- | --- | +| 確かめられずに止まる頻度 | テストの名前が形に当たらないエコシステムで suite が `paths` を宣言していない(`.` だけ)ときは、ファイルを受けるテンプレートでも毎回 `undecided` で止まる。`paths` を宣言するか `--round-test` に全体テストを渡せば続けられる。受け持ちが未確定の候補で落ちたときも止まり、`paths` の配下の先頭にテストでないファイルが来る構成では `--round-test` でしか続けられない。どれだけ起きるかは実行で測る | +| CI に任せる戦略での止まり方 | `source` が `repo` か `paths` で着手前のテストが全体を走らせない戦略では基準が `none` になり、JUnit を書かないランナー(unittest の既定)では不備のテンプレートも `undecided` で止まる。直す箇所は出るが、不備と言い切れない文になる | +| 判定できないで止まる頻度 | 着手前のテストが落ち、JUnit を読めず、試走のファイルにコードの失敗があると止まる | +| 再開の経路 | この仕組みより前に作った状態ファイルからの再開は試走を通らない。旧い状態は `template_checks` を空として読む | +| ランナーごとの受け方 | unittest のほかに、ファイルを受けないランナーの実例は確かめていない。見分けは実行の結果だけに依るため、ランナーの種類に依らない | + +## テスト観点 + +試走の実行と `init` の止まり方は `plugins/ndf/skills/cross-refactoring/tests/test_template_check.py`、選び方と判定は +`plugins/ndf/scripts/tests/test_lib_test_strategy.py`(`test_probe_paths_*`・`test_template_verdict_follows_the_table`)にある。 + +- unittest の `discover -s {paths}` の宣言を持つ再現のリポジトリ(`--scope scripts/storylib`、テストは `tests/`)で `init` が終了コード 4 で止まり、 + 状態ファイルが無いこと。戦略ごとの判定: 全体を走らせる戦略で suite が `paths: ["tests"]` なら `defect`、`paths` が `.` か空なら `undecided`。 + CI に任せる戦略では基準が `none` で JUnit を読めないため `undecided`(I5) +- 止まったとき標準エラーと `template-check.json` に suite の名前・今の `scope_command`・入れたパス・`test.suites[<番号>].scope_command` が出ること。 + `undecided` でも `fix_at` を出すこと(I6)。`init` の後の工程へ進めず、修正のラウンドが 0 回であること +- このリポジトリの宣言(pytest の `scope_command`)では試走が `ok` で `init` が続き、`scope_runs` が組むコマンドが変わらないこと(I1) +- 静的解析の suite と `scope_command` の無い suite は試走されず、既存の範囲テストの組み立てのテストが通ること(I7) +- 試走が suite ごとに 1 回で、上限の残りが 0・打ち切りなら `undecided` で止まること(I2) +- 既存失敗のファイルを除き最大 2 本を選ぶこと。`--scope` に無ければリポジトリ全体から `suite_for` がその suite を返したものだけを選び、 + 宣言していない `e2e/login.spec.ts` を選ばないこと。名前の形に当たらないテストだけの suite は `paths` の配下から選び、テストでないファイルで + 落ちても `undecided` になること。`paths` が `.` だけなら 0 本で止まり直し方に `paths` の宣言が出ること(I3) +- 判定の表の行どおりの判定を返すこと(I4) +- 続けるとき、状態ファイルの `template_checks` が `template-check.json` の `suites` と一致し、`baseline_test` が試走の前と同じであること(決定 7) +- `project-decl.py schema` の出力と公開のスキーマが一致すること(`test_project_decl.py` の `test_published_schema_matches_the_model`) +- 案内に書いた unittest のコマンドの終了コード(`python3 -m unittest <ファイル> <ファイル>` と `discover -s tests` が 0)は、再現のリポジトリで + 走らせて確かめた。`.md` の文言を照合するテストは書かない + +## 関連リンク + +- [`cross-refactoring` の SKILL.md](../../plugins/ndf/skills/cross-refactoring/SKILL.md) +- `development-workflow` の [references/project-analysis.md](../../plugins/ndf/skills/development-workflow/references/project-analysis.md) +- [`lib/test_strategy.py`](../../plugins/ndf/scripts/lib/test_strategy.py)・[`refactor_lib/template_check.py`](../../plugins/ndf/skills/cross-refactoring/scripts/refactor_lib/template_check.py)・[`project_lib/model.py`](../../plugins/ndf/scripts/project_lib/model.py) +- [cross-refactoring-init-test-and-phase-windows.md](cross-refactoring-init-test-and-phase-windows.md)(着手前のテストの上限。試走はその残りの中で走る) +- [cross-refactoring-lint-suite-and-ci-coverage.md](cross-refactoring-lint-suite-and-ci-coverage.md)(静的解析の suite の範囲テスト) +- 課題: #1630(由来 takemi-ohama/novel issue #32 / PR #58)。残した主題は #2095(修正のラウンドの止め方)・#2096(着手前のテストへ入れるパスの種類) diff --git a/issues/issue-1630-design.md b/issues/issue-1630-design.md deleted file mode 100644 index 718c7bc22..000000000 --- a/issues/issue-1630-design.md +++ /dev/null @@ -1,436 +0,0 @@ -# cross-refactoring: ファイルのパスを受けない範囲テストのテンプレートで改修の範囲テストが毎回落ち、修正のラウンドを使い切る → 着手前にテンプレートを試して止まり、直す箇所が出る(#1630) - -## 目的 - -- **何が壊れているか**: 宣言の `scope_command` が `{paths}` に入るテストのファイルを受けない形(unittest の `discover -s {paths}` など)だと、改修の範囲テストがコードと関係なく毎回落ち、落ちた項目が修正へ回る -- **誰が困るか**: ファイルのパスを受けないランナーを宣言に書いたプロジェクトで cross-refactoring を回す人。修正のラウンドを上限まで使い、時間と context を無駄にする -- **直すと何が成り立つか**: 改修のラウンドに入る前にテンプレートを 1 回試し、受けないと分かれば当たった suite・今のテンプレート・入れたパス・直す箇所を出して止まる。宣言の契約から `{paths}` に何が入るかが分かる - -## 適用範囲 - -- **働く範囲**: 配布先のリポジトリでも働く(cross-refactoring の `init` と、宣言の契約・手順 0 の案内) -- **プロジェクトごとに違うもの**: ランナーとテンプレートは宣言(`.ndf/project.json` の `test.suites[].scope_command`)か引数(`--round-test` / `--baseline-test` の `{paths}` を含む値)で受ける。ランナーの名前とコマンドの語は見ない -- **当たるモード**: cross-refactoring を回すすべての実行。`development-workflow` のモードには依らない - -## あるべき姿の根拠 - -| 根拠 | 種類 | 何を示すか | -| --- | --- | --- | -| `python3 -m unittest discover -s tests/test_x.py` が exit 1、`discover -s tests` が exit 0、`python3 -m unittest tests/test_x.py tests/test_y.py` が exit 0、`python3 -m unittest tests` が exit 5(Python 3.14.4、2026-10-24 に手元で実行) | 実測 | ランナーによって受けるパスの種類が違い、コマンドの語からは決められない。走らせれば分かる | -| takemi-ohama/novel の PR #58 で 13 件中 9 件が範囲テストで落ち、修正のフェーズが 10 回以上続いた(#1630 の本文) | 実測 | 改修のラウンドに入ってから見分けると、修正のラウンドを使い切る | -| `test_strategy.py` の I1(種別と戦略をコマンドの語で決めない)・I2(加工は `{paths}` の置き換えだけ) | 既存の形に揃える | 見分けは字句ではなく実行の結果で行う | - -要求と受け入れ条件は #1630 の本文にある(コピーは `issues/issue-1630-requirements.md`)。この文書は「どう作るか」だけを扱う。 - -## ドメインモデル - -### コンテキスト - -| コンテキスト | 何の語が 1 つの意味に決まるか | -| --- | --- | -| `ndf-workflow` | 範囲テストのテンプレート・テストの戦略・全体テスト(`lib/test_strategy.py` が持つ語) | -| `ndf-cross-refactoring` | 着手前のテスト・既存失敗・改修の範囲テスト(`refactor_lib/` が持つ語) | - -`ndf-workflow` が供給者、`ndf-cross-refactoring` が顧客の関係(顧客 / 供給者)。試走のファイルの選び方と判定の規則は -`ndf-workflow` の純粋な関数が持ち、cross-refactoring は走らせて結果を渡すだけにする。supervise・`test-run.py` も -同じ関数を後から使える(I10 と同じ置き方)。 - -### 集約 - -| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | -| --- | --- | --- | --- | --- | -| テストの戦略 | `lib/test_strategy.py` の `resolve`(`init` で 1 度解き、以後は状態ファイルの写しを読むだけ) | `Strategy` | `Suite` | `ScopeRun`・`Outcome` | -| 着手前のテストの記録 | `refactor_lib/baseline.py`(`init` の中だけで書く。変えない) | 状態ファイルの `baseline_test` | — | suite ごとの成否・既存失敗 | -| **テンプレートの試走の記録(新設)** | `refactor_lib/template_check.py` の `run`(`init` の中だけで組む) | 状態ファイルの `template_checks` と `/template-check.json` | — | suite ごとの入れたパス・結果・基準・判定・直す箇所 | - -テンプレートの試走の記録は、着手前のテストの記録と別の集約にする(決定 7)。記録の中身を組むのも -`template-check.json` を書くのも `template_check.run` だけで、`commands/setup.py` は `run` が返した記録を -状態ファイルの `template_checks` へそのまま置くだけにする(`baseline.run_baseline` の記録を `baseline_test` へ -そのまま置く今の形と同じ)。`setup.py` も `baseline.py` も試走の記録の中身を書き換えない。試走は着手前のテストの記録を -読むだけで、`baseline_test` を書き換えない。テストの戦略も変えない(試走の結果で戦略を書き換えない)。 - -### 不変条件 - -| # | 集約 | 条件 | 破れたときの扱い | -| --- | --- | --- | --- | -| I1 | テストの戦略 | 試走に使うコマンドは、改修の範囲テストと同じ組み立て(`fill(scope_command, paths)`)で作る。コマンドの語・ランナーの名前を見ない | 試走が改修の範囲テストと違うものを確かめることになり、見分けが外れる。テストで落とす | -| I2 | テンプレートの試走の記録 | 試走は `scope_command` を持つテストの種別の suite ごとに高々 1 回、`init` の 1 回の起動の中で、着手前のテストの上限の残りの中で走らせる。**試走の対象の suite は、判定が `ok` のときだけ続ける**(「判定しない」で続ける経路を持たない。決定 6) | 上限の残りが無い・打ち切り(`timed_out`)・候補が 0 本のときは `undecided` で止める。続けると、確かめていない suite のテンプレートの不備が改修の範囲テストへ届く | -| I3 | テンプレートの試走の記録 | 試走に入れるパスは、その suite が受け持つテストのファイルを最大 2 本。次の順に、最初に 1 本以上見つかった場所から選ぶ。(1) `--scope` のテストの置き場所の配下でテストの名前の形のもの(`source: scope`)(2) リポジトリの追跡ファイルでテストの名前の形のもののうち、`suite_for` が**その suite を返したもの**だけ(最初の suite へ寄せない。`source: repo`)(3) その suite が宣言した `paths`(`.` と glob を除く)の配下の追跡ファイル(名前の形を問わない。`source: paths`)。どれも既存失敗の ID を持つファイルを除く。`source: paths` の候補と、`paths` が `.` か空の suite での `source: repo` の候補は、その suite のランナーが受けるテストだと言い切れない(受け持ちが「未確定」)。`source: paths` は名前の形を問わないため `tests/TestCase.php`・`tests/fixtures/*.json` のようなテストでないファイルが入り、`paths` が `.` か空の suite には `suite_for` が全ファイルを当てるため宣言していない `e2e/login.spec.ts` が入る | 既存失敗のファイルを入れると、コードの失敗をテンプレートの不備と取り違える。受け持たないファイルを入れると、正しいテンプレートを不備と取り違える。3 つとも 0 本なら `undecided` で止める(I2)。受け持ちが未確定の候補で落ちたときは `defect` にしない(I4) | -| I4 | テンプレートの試走の記録 | 「不備」と判定するのは、候補の受け持ちが確定していて(I3)、基準(その suite の着手前のテスト)が通った、または既存失敗を読めて試走のファイルから除けたうえで、試走が `failed` で終わったときだけ。受け持ちが未確定の候補で `failed` なら、基準によらず `undecided` | 不備でないテンプレートで止まる。テストで落とす | -| I5 | テンプレートの試走の記録 | 判定が「不備」か「判定できない」の suite が 1 つでもあれば、`init` は状態ファイルを作らずに終了コード 4 で止まる。改修の範囲テストは走らない | テンプレートの不備で落ちた項目が修正へ回る(#1630 の現象) | -| I6 | テンプレートの試走の記録 | 止まるときも続けるときも、`template_check.run` が試走の記録(suite・テンプレート・入れたパス・結果・判定・直す箇所)を `/template-check.json` に書く。続けるときは `setup.py` が `run` の返した記録を状態ファイルの `template_checks` へそのまま置く | 止めた理由を後から読めない(非機能の運用・保守性) | -| I7 | テストの戦略 | 静的解析の suite(`kind: lint`)と `scope_command` の無い suite は試走しない。範囲テストの組み立て(`scope_runs`)は変えない | 前提 4 の退行。既存のテストで落とす | - -### ドメインイベント - -| # | イベント | 発生元 | 受け手 | -| --- | --- | --- | --- | -| E1 | 宣言に `scope_command` を書いた | 手順 0 の解析か人の手書き | 宣言の検査(E2)。案内(`project-analysis.md`)と `Suite` の説明に `{paths}` に入るパスの種類が載る | -| E2 | 宣言を検査した | `project-decl.py check`・`test_strategy.resolve` | `init`(E3)。字句の不備だけを見る(変えない) | -| E3 | 着手前のテストを走らせた | cross-refactoring の `init`(`baseline.run_baseline`) | テンプレートの試走(E3a) | -| E3a | **テンプレートを試走した(新設)** | `init`(`template_check.run`) | `init`。不備・判定できないなら止まり、ほかは記録して E4 へ | -| E4 | 改修の範囲テストを組み立てた | 改修の計画・実装・検証 | 範囲テストの実行(変えない) | -| E5 | 範囲テストが落ちた | E4 のコマンドの終了コード | 修正(E6)。試走の対象の suite はどれも判定が `ok` のときだけ E4 へ進む(I2・I5)ため、テンプレートの不備では起きない | -| E6 | 落ちた項目を修正へ回した | E5 | 修正担当(変えない。止め方は #2095) | - -要求の E2 と E3 のあいだに足すとしたイベントを、E3 の直後の E3a として置く。基準(着手前のテストの成否と既存失敗)が -E3 で決まってからでないと、試走の失敗をテンプレートの不備と言えないためである(決定 3)。 - -### 用語 - -| 用語 | 意味 | 用語集への反映 | -| --- | --- | --- | -| テンプレートの試走 | cross-refactoring の `init` が着手前のテストの後に、範囲テストのテンプレートへテストのファイルを最大 2 本入れて 1 回走らせ、`{paths}` に入るパスを受けるかを見分けること。不備か判定できなければ改修の前に止まる | 追加(`ndf-cross-refactoring`) | -| 範囲テストのテンプレート | `{paths}` を引用の外の 1 字句として含むテストのコマンド(宣言の `scope_command` か、`{paths}` を含む引数)。`{paths}` には、改修の範囲テストではテストのファイルのパス、CI に任せる戦略の着手前のテストではテストの置き場所(ディレクトリかファイル)、静的解析では変更したファイルのパスが、シェルの引用で守った並びで入る | 意味の変更(`ndf-workflow`。入るパスの種類を足す) | - -## 機能一覧 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| F1 | 改修のラウンドに入る前に、範囲テストのテンプレートが `{paths}` に入るテストのファイルを受けるかを試し、受けなければ直す箇所を出して止まる | cross-refactoring を回す人(supervise の検査のフェーズを含む) | -| F2 | 宣言の契約(`Suite` の説明・公開のスキーマ)で、`{paths}` に入るパスの種類を場所ごとに示す | 宣言を書く AI と人 | -| F3 | 手順 0 の案内で、ファイルのパスを受けないランナー(unittest)の書き方を示す | 手順 0 で解析する AI と確かめる人 | - -## 構成要素 - -| 要素 | 責務 | -| --- | --- | -| `plugins/ndf/scripts/lib/test_strategy.py`(変更) | 純粋な関数を 2 つと、判定の値オブジェクト `ProbeVerdict` を足す。`probe_paths`(suite が受け持つ候補のファイルから、既存失敗のファイルを除いて最大 2 本を選ぶ)と `template_verdict`(基準の成否・既存失敗を読めたか・候補の受け持ちが確定しているか・試走の結果から判定を返す)。プロセスもファイルも触らない | -| `plugins/ndf/skills/cross-refactoring/scripts/refactor_lib/template_check.py`(新設) | 試走の実行と記録(集約「テンプレートの試走の記録」の持ち主)。候補のファイルを「候補のファイル」の節の順(`--scope` のテストの置き場所 → リポジトリ全体で `suite_for` が当たったもの → suite の `paths` の配下)で集め、`probe_paths` で選び、`fill` で組んだコマンドをシェルで走らせ、`outcome` と `template_verdict` で判定する。記録を `/template-check.json` へ書き、記録と、止める判定なら直す箇所を添えた文を返す | -| `plugins/ndf/skills/cross-refactoring/scripts/refactor_lib/commands/setup.py`(変更) | `_verify_init` で、着手前のテストの後・配分の履歴の追記の後に `template_check.run` を呼ぶ。止める判定なら `die`(終了コード 4)、続けるなら `run` の返した記録を状態ファイルの `template_checks` へそのまま置く(中身を書き換えない) | -| `plugins/ndf/scripts/project_lib/model.py`(変更) | `Suite` の説明に、`{paths}` に入るパスの種類を場所ごと(改修の範囲テスト・CI に任せる戦略の着手前のテスト・静的解析)に書き、ファイルを受けないテンプレートは `init` が試走で止めることを書く | -| `plugins/ndf/skills/development-workflow/schemas/project.schema.json`(作り直し) | `project-decl.py schema --out` で `model.py` から作り直す。手で直さない | -| `plugins/ndf/skills/development-workflow/references/project-analysis.md`(変更) | `test` の行に、`{paths}` に入るパスの種類と、ファイルを受けないランナーの書き方(unittest は `scope_command` を `python3 -m unittest {paths}`、`command` を `python3 -m unittest discover -s <テストのディレクトリ>`)を書く。書く前に再現のリポジトリで走らせて終了コードを確かめる | -| `plugins/ndf/skills/cross-refactoring/SKILL.md`(変更) | 終了コード 1(中断)の `metrics.exit` 4 の理由の並びに「範囲テストのテンプレートが `{paths}` に入るテストのファイルを受けない(試走で見分けた)」を足す | -| `docs/glossary/glossary.json`・`docs/glossary.md`(変更) | 用語の表の 2 行を反映し、`glossary.py render` で作り直す | - -```mermaid -graph LR - subgraph cr[cross-refactoring init] - setup[commands/setup.py] - base[baseline.py] - tc[template_check.py 新設] - end - subgraph lib[共通の層 lib] - ts[test_strategy.py
probe_paths / template_verdict / fill / outcome] - end - subgraph decl[宣言の契約] - model[project_lib/model.py] - schema[project.schema.json] - pa[project-analysis.md] - end - setup --> base - setup --> tc - tc --> ts - base --> ts - model -->|schema で生成| schema - pa -.->|案内が指す| model -``` - -`SKILL.md` と用語集は呼び出しの関係を持たないため、図に含めない。 - -## 構造 - -```mermaid -classDiagram - class Strategy { - +name: str - +suites: list~Suite~ - +scoped_suites(kind) list~Suite~ - } - class Suite { - +name: str - +command: str - +scope_command: str - +kind: str - } - class Outcome { - +status: str - +code: int - } - class ProbeVerdict { - <<新設 値>> - +status: ok|defect|undecided - +reason: str - +stops: bool - } - class template_check { - <<新設 モジュール>> - +run(strategy, baseline, scope, work, tmp_dir, timeout, started) Report - } - Strategy "1" o-- "*" Suite - template_check ..> Strategy : 読む - template_check ..> Outcome : outcome() で作る - template_check ..> ProbeVerdict : template_verdict() で作る -``` - -`ProbeVerdict` は `test_strategy.py` に置く値オブジェクトで、`stops` は `status` が `defect` か `undecided` のとき真である。 - -## 入出力の契約 - -### `init` の止まり方(コマンドの約束) - -| 項目 | 書くこと | -| --- | --- | -| 名前 | `refactor.py init`(cross-refactoring の着手) | -| 入力 | 変わらない(`--scope`・`--round-test`・`--baseline-test` など) | -| 出力(続けるとき) | 変わらない。標準エラーに試走の 1 行(`✅ 範囲テストのテンプレートを試走: …`)を足す。状態ファイルの `template_checks` に記録を置く | -| 失敗の形(止めるとき) | 終了コード 4。状態ファイルを作らない。標準エラーに下の「止めるときの文」。`/template-check.json` に記録 | -| 互換性 | 不備(`defect`)で止まるのは、今まで改修の範囲テストが必ず落ちていた宣言だけである。ファイルを受けるテンプレート(このリポジトリの pytest)は試走が通り、続く。ほかに、ファイルを受けるテンプレートでも確かめられずに(`undecided`)止まる宣言がある: テストの名前が形(`test_*`・`*_test.*`・`*.test.*`・`*_spec.*`・`*.spec.*`)に当たらず(`FooTest.java`・`FooTests.cs`・`UserTest.php`・`*.bats`・Rust の `tests/*.rs` など)、かつその suite が `paths` を宣言していない(`.` だけ)とき。止める文が `paths` の宣言と `--round-test` の 2 つの直し方を出す。`paths` を宣言した suite でも、受け持ちが未確定の候補(`source: paths`。並べ替えの先頭にテストでないファイルが来る構成)で試走が落ちれば、テンプレートが正しくても `undecided` で止まる(`defect` で「受けません」とは言わない)。その構成では `paths` の宣言だけでは続けられず、`--round-test` に全体テストを渡す直し方で続ける。互換の経路は要らない | - -止めるときの文(suite ごとに 1 段。判定が `defect` の例): - -```text -⛔ 範囲テストのテンプレートが {paths} に入るテストのファイルを受けません(suite: unit) - 今のテンプレート: python3 -m unittest discover -s {paths} - 入れたパス: tests/test_x.py tests/test_y.py - 試走: python3 -m unittest discover -s tests/test_x.py tests/test_y.py → 終了コード 1(着手前のテストは通っていた) - 直す箇所: .ndf/project.json の test.suites[0].scope_command({paths} にはテストのファイルのパスが入る。 - ファイルを受けるコマンドへ直すか、--round-test に {paths} を含まない全体テストを渡す) -``` - -判定が `undecided` のときは、「受けません」の代わりに「受けるかを確かめられません」と理由(候補のファイルが無い・ -上限の残りが無い・打ち切り・既存失敗を除けない)を出す。`undecided` でも `defect` と同じく、直す箇所の行に `fix_at` -(宣言の suite なら `.ndf/project.json の test.suites[<番号>].scope_command`)を必ず出し、直し方に「`--round-test` に -`{paths}` を含まない全体テストを渡す」を必ず添える。理由が「候補のファイルが無い」ときは、直し方に「その suite の -`paths` にテストの置き場所を宣言する」も添える。 - -`--round-test` / `--baseline-test` の雛形の suite(`args`)で止まったときは、直す箇所を -「`--round-test` の値」のように引数の名前で出す。 - -### `template-check.json`(記録の形) - -```json -{ - "checked_at": "2026-10-24T12:00:00Z", - "stopped": true, - "suites": [ - { - "suite": "unit", - "index": 0, - "fix_at": ".ndf/project.json の test.suites[0].scope_command", - "scope_command": "python3 -m unittest discover -s {paths}", - "paths": ["tests/test_x.py", "tests/test_y.py"], - "source": "scope", - "command": "python3 -m unittest discover -s tests/test_x.py tests/test_y.py", - "code": 1, - "outcome": "failed", - "reference": "green", - "verdict": "defect", - "reason": "着手前のテストは通り、試走が終了コード 1 で落ちた", - "log": "/template-check-0.log" - } - ] -} -``` - -| キー | 型 | 意味 | -| --- | --- | --- | -| `stopped` | bool | `init` を止めたか | -| `suites[].index` | int か null | 宣言の `test.suites` の番号。引数の雛形なら null | -| `suites[].fix_at` | str | 直す箇所 | -| `suites[].paths` | list[str] | 試走で `{paths}` へ入れたパス(0〜2 本。0 本なら判定は `undecided`) | -| `suites[].source` | str か null | 候補を選んだ場所。`scope`(`--scope` のテストの置き場所)/ `repo`(リポジトリの追跡ファイルのうち、名前がテストの形で `suite_for` が当たったもの)/ `paths`(suite の `paths` の配下の追跡ファイル)。0 本なら null | -| `suites[].code` / `outcome` | int か null / str | 試走の終了コードと `test_strategy.outcome` の結果。走らせなかったら null と `not_run` | -| `suites[].reference` | str | 基準(その suite の着手前のテストの成否)。`green` / `red` / `none`(走っていない)。`source` が `repo` か `paths` のときは、着手前のテストがその suite の `command`(全体)で通ったときだけ `green` で、ほかは `none` | -| `suites[].verdict` | str | `ok` / `defect` / `undecided` | - -状態ファイルの `template_checks` は同じ形の `suites` の並びである。旧い状態ファイル(キーが無い)は -空の並びとして読む。 - -## 処理の流れ - -```mermaid -sequenceDiagram - participant S as setup._verify_init - participant B as baseline.run_baseline - participant T as template_check.run - participant L as test_strategy - S->>B: 着手前のテスト(戦略どおり) - B-->>S: 記録(suites の成否・既存失敗・seconds) - S->>S: 配分の履歴へ追記・打ち切りなら止める(今と同じ) - S->>T: 戦略・記録・--scope・残りの上限 - loop scope_command を持つテストの suite ごと - T->>L: probe_paths(候補, 既存失敗) - alt 候補が 0 本 または 残りの上限が 0 - T->>T: undecided を記録(止める) - else - T->>L: fill(scope_command, paths) - T->>T: シェルで 1 回走らせる - T->>L: outcome(code, timed_out) - T->>L: template_verdict(基準, 既存失敗を読めたか, 受け持ちが確定しているか, 結果) - end - end - T->>T: template-check.json を書く - T-->>S: 報告(止めるか・文・記録) - alt 止める - S->>S: die(終了コード 4。状態ファイルを作らない) - else 続ける - S->>S: 返った記録を template_checks へそのまま置いて状態を保存 - end -``` - -宣言の契約の文書(`model.py`・スキーマ・案内)・`SKILL.md`・用語集は実行の流れに現れないため、図に含めない。 - -起動の失敗(`launch_failed`)は今の `launch.stop(None, …, "baseline", …)` と同じ扱いで止まる(試走も -テストのコマンドの起動であり、起動の失敗の見分けは 1 か所に置く)。 - -### 判定の表(`template_verdict`) - -| 試走の結果 | 基準(suite の着手前のテスト) | 既存失敗のファイルを除けたか | 判定 | `init` | -| --- | --- | --- | --- | --- | -| 走らせなかった(候補 0 本・残りの上限 0) | — | — | `undecided` | 止める | -| `timed_out` | — | — | `undecided` | 止める | -| `passed` | — | — | `ok` | 続ける | -| `failed`(候補の受け持ちが未確定: `source: paths`、または `paths` が `.` か空の suite の `source: repo`) | — | — | `undecided` | 止める | -| `failed` | `green` | — | `defect` | 止める | -| `failed` | `red` か `none` | 除けた(JUnit を読めた) | `defect` | 止める | -| `failed` | `red` か `none` | 除けない(JUnit を読めない) | `undecided` | 止める | - -`launch_failed` はこの表に来る前に `launch.stop` で止まる。表は上の行から順に当て、最初に当たった行の判定を返す。 -`template_verdict` は受け持ちが確定しているか(`owned`)を引数に受け、`probe_paths` が候補の `source` と suite の `paths` から決める。 - -### 候補のファイル(`probe_paths`) - -- 候補は `scope.test_locations(--scope)` のそれぞれの配下の `test_files_under`(追跡ファイルのうちテストの名前の形)を、 - `suite_groups` と同じ受け持ちの規則(`suite_for`、当たらなければ最初のテストの suite)で suite へ振り分けたもの -- その suite の候補が `--scope` の配下に 1 本も無ければ、`test_files_under(".")`(リポジトリ全体)のうち、`suite_for` が - その suite を返したファイルだけを候補にする(`source: repo`)。`suite_for` が None を返したファイル(どの suite の - `paths` にも当たらない。たとえば宣言していない `e2e/login.spec.ts`)は、`suite_groups` と違って最初の suite へ寄せず、 - 候補にしない。寄せると、正しいテンプレートでも受け持たないファイルで試走が落ち、`defect` で止まる。テストを本体と別の場所に - 置き、その suite のテストが `--scope` に無い構成でも試走できる。ただし `paths` が `.` か空の suite には `suite_for` が全ファイルを - 当てる(`_match_len` が `.` を常に一致とする)ため、宣言していない `e2e/login.spec.ts` もその suite の候補に入り、並べ替えると - `tests/` より先に来る。このため、`paths` が `.` か空の suite の `source: repo` の候補は受け持ちが未確定とし、試走が `failed` なら - 基準によらず `undecided` で止める(判定の表。直し方に `paths` の宣言を添える) -- それでも 0 本なら、その suite が宣言した `paths` の要素のうち `.` と glob を除いたものの配下の追跡ファイル(名前の形を - 問わない)を候補にする(`source: paths`)。テストの名前が形に当たらないエコシステム(`FooTest.java`・`FooTests.cs`・ - `UserTest.php`・`*.bats`・Rust の `tests/*.rs`)でも、suite がテストの置き場所を `paths` に宣言していれば試走できる。 - `.` と glob を除くのは、テストでないファイル(`README.md` など)を入れて正しいテンプレートを不備と取り違えないためである。 - それでも `paths` の配下にはテストでないファイル(Laravel の `tests/TestCase.php`(abstract)・`tests/CreatesApplication.php`(trait)、 - Rust の `tests/common/mod.rs`、`tests/fixtures/*.json`)が残り、並べ替えの先頭に来ることがある。このため `source: paths` の候補は - 受け持ちが未確定とし、試走が `failed` なら基準によらず `undecided` で止める(判定の表)。`passed` なら `ok` で続ける -- `source` が `repo` か `paths` のとき、基準は着手前のテストがその suite の `command`(全体)で通ったときだけ `green` とし、 - ほかは `none`・既存失敗は「除けない」として扱う(範囲を絞った着手前のテストは、`--scope` の外のファイルの成否を示さない) -- 3 つとも 0 本なら `probe_paths` は空を返し、判定は `undecided`(止める。直し方に `paths` の宣言を添える) -- 既存失敗の ID を `test_triage.by_file` でファイルへ直し、そのファイルを候補から除く -- 並べ替えた順の先頭から最大 2 本。2 本にするのは、1 本は受けて 2 本目を受けないテンプレート(`discover -s a b` は 2 本目を - 開始のディレクトリと読まない)を見分けるためである - -## 非機能の実現方式 - -| 大項目 | 要求の条件 | 実現方式 | 確かめ方 | -| --- | --- | --- | --- | -| 性能・拡張性 | 受け入れ条件 1 の見分けのために足す実行は、cross-refactoring の 1 回の起動につき suite ごとに高々 1 回とし、その所要を着手前のテストの上限(`INIT_TEST_SHARE`)の中に収める | 試走は suite ごとに 1 回。上限は着手前のテストと同じ `init_test_timeout` から、着手前のテストの開始からの経過を引いた残り(`test_triage.run_within` に同じ `started` を渡す)。残りが 0 なら走らせず `undecided` で止める | 試走の回数を数える単体テストと、上限の残りが 0 のときに走らせず止めるテスト | -| 運用・保守性 | 見分けた結果(止めた理由・当たった suite)が結果の JSON か状態ファイルに残り、後から読める | 止めても続けても `/template-check.json` に書く。続けるときは `run` の返した記録を状態ファイルの `template_checks` へそのまま置く | 止める結合のテストで JSON のキーを読む | - -## 決定の記録 - -### 決定 1: 走らせて見分けるため、試走の場所を宣言の検査ではなく cross-refactoring の `init` にする - -テンプレートがファイルを受けるかは、コマンドの語から決められない(前提 1。unittest でも `discover -s` と -`python3 -m unittest` で逆になる)。見分けるには走らせるしかなく、走らせるには作業ディレクトリ・依存・テストの -ファイルが要る。`init` はそれをすでに用意し、着手前のテストを走らせている。 - -`project-decl.py check` で走らせる案は採らない。宣言の検査は純粋な検査で、所要の上限も作業ディレクトリも持たず、 -手順 0 の度に外部のコマンドを走らせることになる。`test_strategy.resolve` で走らせる案も採らない。`resolve` は純粋な -処理だけを置く層で(冒頭の約束)、supervise・`test-run.py`・cross-review の解決にも実行が漏れる。 - -根拠: Value 4 / Value 5 / Value 6(MVV 版 2) - -### 決定 2: 判定の規則を共通の層に置くため、ファイルの選び方と判定を `test_strategy.py` の純粋な関数にし、実行だけを cross-refactoring に置く - -範囲テストの組み立ては `test_strategy.py` に寄せている(I10)。試走の選び方と判定も同じ層に置けば、supervise や -`test-run.py` が後から同じ見分けを使うときに関数を分けずに済む。実行(プロセス・ログ・上限)は呼ぶ側の責務で、 -`baseline.py` が着手前のテストを走らせるのと同じ置き方である。 - -`refactor_lib/` の中に判定まで置く案は採らない。Skill ごとに見分けの関数が分かれる(Value 6)。 - -根拠: Value 6(MVV 版 2) - -### 決定 3: 試走の失敗をコードの失敗と取り違えないため、着手前のテストの後に走らせ、基準が通ったか既存失敗を除けたときだけ不備とする - -試走の失敗には、テンプレートの不備とコードの既存の失敗の 2 つの原因がありうる。着手前のテストがその suite で通って -いれば、同じコードの一部のファイルが落ちる理由はテンプレートにしかない。落ちていても、JUnit から既存失敗のファイルが -分かれば、それを除いたファイルで試せる。どちらも成り立たないときは「判定できない」として止まり、人へ戻す -(共通原則の「決められないときも人へ戻す」)。 - -着手前のテストより先に試走する案は採らない。基準が無く、既存の失敗を抱えたリポジトリでいつも取り違える。 -判定できないときに続ける案も採らない。続けると、テンプレートの不備で落ちた項目が修正へ回る経路が残る(受け入れ条件 3)。 - -根拠: Mission / Value 1(MVV 版 2) - -### 決定 4: 改修の範囲テストへ不備が届かないことを入口で保証するため、修正へ回す側には見分けを足さない - -不備か判定できない suite があれば `init` が状態ファイルを作らずに止まるため、改修の範囲テストはそもそも走らない -(受け入れ条件 3 を構造で満たす)。検証で落ちた範囲テストを走らせ直してテンプレートの不備かを見分ける案は採らない。 -項目ごとに走らせ直しが増え、着手前に 1 回で分かることを何度も確かめることになる。修正担当が「コード起因の失敗なし」を -返したときの止め方は別の主題として #2095 に残した(要求の未決 2)。 - -根拠: Value 6 / Mission(MVV 版 2) - -### 決定 5: 入るパスの種類は変えず、契約に場所ごとに書く - -`{paths}` に入るパスを、改修の範囲テスト(テストのファイル)と CI に任せる戦略の着手前のテスト(テストの置き場所)で -そろえる案(要求の未決 3)はこの課題では採らない。受け入れ条件は「入る種類が分かる」ことと「受けないなら止まる」ことで、 -そろえなくても満たせる。着手前のテストへ入れるパスを変えると、範囲テストの所要の履歴(置き場所ごとの記録)にも効く。 -CI に任せる戦略でファイルだけを受けるテンプレートの着手前のテストが落ちる件は #2096 に残した。 - -根拠: Value 1 / Value 6(MVV 版 2) - -### 決定 6: 試走の対象の suite を確かめずに続ける経路を作らないため、候補 0 本・上限の残り 0・打ち切りを `undecided` で止める - -「判定しない(`skipped`)」で続けると、試走していない suite もそのまま改修の範囲テストへ進み、ファイルを受けない -テンプレートで落ちた項目が修正へ回る経路が残る(E5 の保証と受け入れ条件 1・3 が成り立たない)。候補が `--scope` に無い -ときはリポジトリ全体(`suite_for` が当たったものだけ)から、名前の形に当たらなければ suite の `paths` の配下から選び直して -試走できる場合を広げ、それでも確かめられないときだけ止めて人へ戻す -(共通原則の「決められないときも人へ戻す」)。止める文には `defect` と同じ直す箇所(`fix_at`)と、`--round-test` に全体テストを渡す直し方を必ず添え、 -利用者が 1 回で続けられるようにする。 - -`skipped` の suite は改修の範囲テストで落ちても修正へ回さない(全体テストへ落とす)案は採らない。修正へ回す側に見分けを -足すことになり、決定 4(入口で保証し、回す側には見分けを足さない)に反する。 - -根拠: Mission / Value 1 / Value 6(MVV 版 2) - -### 決定 7: 書き換えてよいものを 1 つに決めるため、試走の記録を別の集約にし、持ち主を `template_check.py` にする - -試走の記録を着手前のテストの記録(持ち主 `baseline.py`)の一部にすると、中身を組む `template_check.py` と状態へ写す -`setup.py` を含め、書く場所が 3 つに割れる。記録を組んで `template-check.json` に書くのは `template_check.run` だけにし、 -`setup.py` は返った値を状態ファイルの `template_checks` へそのまま置く(`baseline_test` と同じ置き方)。 - -`baseline.py` を経由して書く案は採らない。`baseline.py` は着手前のテストの実行と記録の責務で、試走の判定(`ndf-workflow` の -関数)と実行を持ち込むと、変えない約束の集約に新しい責務が入る。 - -根拠: Value 6 / Value 7(MVV 版 2) - -## テスト設計 - -| 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | -| --- | --- | --- | -| 受け入れ条件 1・I5 | `scope_command: python3 -m unittest discover -s {paths}`・`command: python3 -m unittest discover -s tests` の宣言を持つ再現のリポジトリ(`--scope scripts/storylib`、テストは `tests/`)で `init` を打つと、終了コード 4 で止まり、状態ファイルが無い。戦略ごとに期待する判定を分けて縛る: 着手前のテストが全体(`command`)を走らせる戦略では、suite が `paths: ["tests"]` を宣言していれば基準が `green` になり `defect`、`paths` が `.` か空(既定)なら `source: repo` の受け持ちが未確定のため `undecided`。CI に任せる戦略では `source: repo` の基準が `none`・unittest は JUnit を書かないため `undecided` | 試走を呼ばないと落ちる。止める判定でも状態を保存すると落ちる。戦略ごとの判定を取り違えると落ちる | -| 受け入れ条件 2・I6 | 受け入れ条件 1 の両方の戦略で、止まったとき標準エラーと `template-check.json` に suite の名前・今の `scope_command`・入れたパス・`test.suites[<番号>].scope_command` が出る(`undecided` でも `fix_at` を出す) | 直す箇所の番号を宣言の並びと違えると落ちる。記録を書かずに止まると落ちる。`undecided` の文で `fix_at` を省くと落ちる | -| 受け入れ条件 3 | 受け入れ条件 1 の宣言では `init` の後の工程(計画・実装・検証)へ進めず、修正のラウンドが 0 回である | 不備の判定でも状態ファイルを作ると落ちる | -| 受け入れ条件 4 | `project-decl.py schema` の出力と公開のスキーマが一致する(既存の `test_published_schema_matches_the_model`) | `model.py` を直してスキーマを作り直さないと落ちる | -| 受け入れ条件 5 | 案内に書く unittest の 2 つのコマンドを再現のリポジトリで走らせた終了コード(`python3 -m unittest <ファイル> <ファイル>` が 0、`discover -s tests` が 0)を、実装の Pull Request の本文に添える | (`.md` の文言を照合するテストは書かない。AGENTS.md) | -| 受け入れ条件 6 | このリポジトリの宣言(pytest の `scope_command`)で、試走が `ok` になり `init` が続く。`scope_runs` が組むコマンドは今と同じ | 試走で `{paths}` の置き換えを `fill` 以外にすると落ちる | -| 受け入れ条件 7・I7 | 静的解析の suite と `scope_command` の無い suite は試走されず、既存の範囲テストの組み立てのテストが通る | 試走の対象に `kind: lint` を入れると落ちる | -| I1 | 試走のコマンドが `fill(scope_command, paths)` と一致する | コマンドの語で分岐を足すと落ちる | -| I2 | 試走が suite ごとに 1 回で、上限の残りが 0・打ち切りなら `undecided` で `init` が止まる | 2 回走らせると落ちる。上限を別に取ると落ちる。上限の残りが 0 で続けると落ちる | -| I3 | 既存失敗の ID を持つファイルが候補から除かれ、最大 2 本が選ばれる。`--scope` にその suite のテストが無ければリポジトリ全体から `suite_for` がその suite を返したファイルだけが選ばれ(`--scope` が本体のディレクトリ、suite のテストは別の置き場所の構成)、どの suite の `paths` にも当たらない `e2e/login.spec.ts` は選ばれない。`paths` が `.` の suite では `e2e/login.spec.ts` も候補に入りうるため、その試走が `failed` なら基準が `green` でも `defect` ではなく `undecided` で止まる。名前の形に当たらないテスト(`tests/FooTest.php`)だけを持ち、`paths: ["tests"]` を宣言した suite では `paths` の配下から選ばれ(`source: paths`)、テストでないファイル(`tests/CreatesApplication.php`・`tests/TestCase.php`)が並べ替えの先頭に来て試走が `failed` になっても、基準が `green` でも `defect` にならず `undecided` で止まる。同じテストで `paths` が `.` だけなら 0 本で `undecided` で止まり、直し方に `paths` の宣言が出る | 既存失敗のファイルを選ぶと落ちる。3 本以上選ぶと落ちる。`--scope` の外から選ばずに止めると落ちる。受け持たないファイルを最初の suite へ寄せると落ちる。`paths` の配下から選ばずに止めると落ちる。`paths` が `.` で全追跡ファイルから選ぶと落ちる。0 本で続けると落ちる。受け持ちが未確定の候補で落ちたのに `defect` を返すと落ちる | -| I6・決定 7 | 続けるとき、状態ファイルの `template_checks` が `template-check.json` の `suites` と一致し、`baseline_test` が試走の前と同じである | `setup.py` が記録を書き換えると落ちる。`baseline_test` へ書くと落ちる | -| I4 | 判定の表の 7 行どおりの判定を返す(`template_verdict` の単体テスト) | 基準 `red` で既存失敗を除けないのに `defect` を返すと落ちる。受け持ちが未確定(`owned` が偽)で `failed` のとき、基準 `green` でも `defect` を返すと落ちる。`passed` で止めると落ちる。走らせなかった・`timed_out` で続けると落ちる | - -## 設計の結果 - -| 課題 | 扱い | 取り込み先 | 触るファイル | -| --- | --- | --- | --- | -| #1630 | 実装する | — | `plugins/ndf/scripts/lib/test_strategy.py`、`plugins/ndf/skills/cross-refactoring/scripts/refactor_lib/template_check.py`、`plugins/ndf/skills/cross-refactoring/scripts/refactor_lib/commands/setup.py`、`plugins/ndf/skills/cross-refactoring/SKILL.md`、`plugins/ndf/scripts/project_lib/model.py`、`plugins/ndf/skills/development-workflow/schemas/project.schema.json`、`plugins/ndf/skills/development-workflow/references/project-analysis.md`、`plugins/ndf/scripts/tests/`、`plugins/ndf/skills/cross-refactoring/scripts/tests/`、`docs/glossary/glossary.json`、`docs/glossary.md` | - -## 未確認のまま残ること - -| 項目 | 内容 | -| --- | --- | -| 確かめられずに止まる頻度 | 候補のファイルが 3 つの場所のどれにも無い・上限の残りが 0・打ち切りのときは `undecided` で止まる(決定 6)。テストの名前が形に当たらないエコシステム(Java・C#・PHP・bats・Rust など)で suite が `paths` を宣言していない(`.` だけ)ときは、ファイルを受けるテンプレートでも毎回止まる。止まったときは `paths` を宣言するか `--round-test` に全体テストを渡せば続けられる。受け持ちが未確定の候補(`source: paths`、`paths` が `.` か空の suite の `source: repo`)で落ちたときも `undecided` で止まり、`paths` の配下の先頭にテストでないファイルが来る構成では `--round-test` でしか続けられない。どれだけ起きるかは実行で測る | -| CI に任せる戦略での止まり方 | `source` が `repo` か `paths` で着手前のテストが全体を走らせない戦略では、基準が `none` になり、JUnit を読めないランナーでは不備のテンプレートも `defect` ではなく `undecided` で止まる。止める文は `fix_at` を必ず出すため直す箇所は分かるが、不備と言い切れない文になる頻度は実行で測る | -| 判定できないで止まる頻度 | 着手前のテストが落ち、JUnit を読めず(unittest は既定で JUnit を書かない)、試走のファイルにコードの失敗があると止まる。止まったときの直し方(JUnit を宣言に書く・`--round-test`)を文に出すが、どれだけ起きるかは実行で測る | -| 再開の経路 | この変更より前に作った状態ファイルからの再開は試走を通らない。旧い状態は `template_checks` を空として読む | -| ランナーごとの受け方 | unittest のほかに、ファイルを受けないランナーの実例は確かめていない。見分けは実行の結果だけに依るため、ランナーの種類に依らない | diff --git a/issues/issue-1630-requirements.md b/issues/issue-1630-requirements.md deleted file mode 100644 index bbbf476c6..000000000 --- a/issues/issue-1630-requirements.md +++ /dev/null @@ -1,203 +0,0 @@ -# #1630: cross-refactoring の範囲テストが変わったファイルのディレクトリを discover し、テストを tests/ にだけ置くプロジェクトで落ちて fix を繰り返す - -正は課題の本文(#1630)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。 - -## 何を見つけたか - -cross-refactoring の改修ごとのテスト(範囲テスト)は、`.ndf/project.json` の `scope_command` の `{paths}` へ、計画の `test_targets`(テストのファイルのパス)を入れて走らせる(`plugins/ndf/scripts/lib/test_strategy.py` の `scope_runs` と `suite_groups`。変更したファイルを入れるのは静的解析の suite だけ)。unittest の `discover -s` は開始のディレクトリしか受けないため、`scope_command` が `python3 -m unittest discover -s {paths}` だと、`{paths}` にファイルのパスが入った時点で落ちる。コードに原因が無いのに「範囲テストが落ちた項目 9 件を修正へ回します」となり、fix の担当が「コード起因の失敗なし・コミットなし」を返すまま、fix のラウンドを上限まで繰り返した。 - -手元の再現(`scripts/storylib` に `__init__.py` と `x.py`、`tests/test_x.py` に 1 本のテスト): - -| `discover -s` に渡すもの | 結果 | -| --- | --- | -| `scripts/storylib`(テストの無いディレクトリ) | `NO TESTS RAN`、exit 5 | -| `tests/test_x.py`(ファイル) | `ImportError`、exit 1 | -| `tests`(テストのディレクトリ) | exit 0 | - -宣言の `{paths}` にファイルとディレクトリのどちらが入るかの契約は宣言の側に書かれておらず、development-workflow の手順 0 で宣言を作るときにも unittest の `scope_command` の書き方の案内が無い。 - -回避として、検査のプランの cross-refactoring の引数へ `--round-test "python3 -m unittest discover -s tests"` を書き、改修ごとのテストを全体に固定して流し直した。 - -## どこで見つけたか - -takemi-ohama/novel のスプリント world(PR takemi-ohama/novel#58)の検査のフェーズ(supervise.py の 5-check の `04-refactor`)。13 件の改修のうち 9 件が範囲テストで落ち、fix のフェーズが 10 回以上続いた。 - -## なぜこの変更の範囲外なのか - -開発対象リポジトリの変更ではなく、範囲テストの `{paths}` に何が入るか(`test_targets` のファイルのパス)と、宣言の `scope_command` がそれを受けられるかの契約の問題である(`scripts/lib/test_strategy.py` と development-workflow の手順 0 の宣言の解析)。開発対象リポジトリでは `--round-test` を固定する回避しかできない(MVV なし: 上流)。 - -## 直さないと何が起きるか - -ファイルのパスを受けないランナー(unittest の `discover -s` など)を `scope_command` に書いたプロジェクトでは、範囲テストが常に落ち、改修が fix のラウンドを使い切るか、全体テストへ手で固定するまで進まない。fix の時間と context を無駄に使う。 - -## 受け入れ条件(案) - -- [ ] `scope_command` に `python3 -m unittest discover -s {paths}` のようなファイルのパスを受けない雛形を書いた宣言で、範囲テストが落ちて改修が fix へ回ることが無い(範囲テストが通るか、宣言の検査の時点で止まり直し方が出る) -- [ ] 宣言の契約(`project.schema.json` の `Suite` の説明と、development-workflow の手順 0 の宣言の作成の案内)から、`{paths}` にテストのファイルのパスが入ることが分かる - -## 直す場所 - -- 現象レイヤー: cross-refactoring の範囲テストが毎回落ち、fix のラウンドを使い切る(supervise の検査のフェーズ) -- 修正レイヤー: 宣言の契約。`scope_command` の `{paths}` にファイルのパスが入ることの明記と、宣言の検査(`project-decl.py check` / 手順 0)での受けられない雛形の検出 -- 修正方針: 向きの修正と新設。`{paths}` にテストのファイルのパス(`test_targets` の `::` より前)が入る契約を `plugins/ndf/skills/development-workflow/schemas/project.schema.json` の `Suite` の説明と `plugins/ndf/skills/development-workflow/references/project-analysis.md` の `test` の行に書き、`project-decl.py check`(または cross-refactoring の `init` の着手前の範囲テスト)でファイルを受けない `scope_command` を見分けて止めるか案内する - -## 由来 - -takemi-ohama/novel issue #32 / PR #58(スプリント world の振り返り) - -## 依頼(原文) - -> #1630。人へ問わずに進め、決められない点は前提か未決として課題の本文へ書く。本文を書いたら `spec-copy.py write 1630 issues/issue-1630-requirements.md` で写しを作り、コミットする(push しない)。 - -(書き手: AI の起動指示、日付: 2026-10-24。利用者の指示の原文ではない) - -この課題の題と、本文の「何を見つけたか」〜「由来」の節は、takemi-ohama/novel のスプリント world の振り返りで AI が書いたものである(書き手: 起票者)。利用者の指示の原文は無い。題と本文の条件は、次の「起票者の推定」で扱いを決める。 - -## 起票者の推定 - -題・本文のうち、利用者の指示の原文に無く起票者が推して足した条件。承認ゲート 1 で利用者が確かめるまで、利用者の決定として扱わない。 - -| # | 条件 | 推した出所 | この要求での扱い | -| --- | --- | --- | --- | -| S1 | 範囲テストは「変わったファイルのディレクトリ」を `discover` する(題) | 題 | 採らない。コードは計画の `test_targets` の `::` より前のパス(テストのファイル)を `{paths}` へ入れる(`plugins/ndf/scripts/lib/test_strategy.py` の `suite_groups`・`scope_runs`)。変更したファイルを入れるのは静的解析の suite だけである。本文の「何を見つけたか」も同じことを書いている | -| S2 | 落ちるのは「テストを tests/ にだけ置くプロジェクト」である(題) | 題 | 採らない。落ちる条件はテストの置き場所ではなく、`scope_command` のランナーがテストのファイルのパスを受けないことである(下の「調べたこと」の表)。置き場所が `tests/` でも、ファイルを受けるランナー(pytest・`python3 -m unittest <ファイル>`)なら落ちない | -| S3 | ファイルを受けないテンプレートで、範囲テストが落ちて改修が fix へ回ることが無い(範囲テストが通るか、宣言の検査の時点で止まり直し方が出る)(受け入れ条件(案)1) | 本文「受け入れ条件(案)」 | 受け入れ条件 1〜3 へ。ただし「宣言の検査の時点」は前提 2 のとおり、着手前に止まることも含める | -| S4 | 宣言の契約(`project.schema.json` の `Suite` の説明と手順 0 の案内)から、`{paths}` にテストのファイルのパスが入ることが分かる(受け入れ条件(案)2) | 本文「受け入れ条件(案)」 | 受け入れ条件 4・5 へ。ただし「ファイルのパスが入る」だけでは足りない(着手前の範囲テストはディレクトリを入れる。下の「調べたこと」)ため、入るパスの種類を `{paths}` を埋める場所ごとに書く条件にする | -| S5 | 受けられないテンプレートを `project-decl.py check`(または cross-refactoring の `init` の着手前の範囲テスト)で見分ける(直す場所) | 本文「直す場所」 | 前提 2・未決 1。見分ける場所と手段は設計が決める | -| S6 | fix のラウンドを上限まで繰り返した原因は、テンプレートがファイルを受けないことである(何を見つけたか) | 本文「何を見つけたか」 | 受け入れ条件 1 の対象。fix の担当が「コード起因の失敗なし・コミットなし」を返しても次のラウンドへ進む振る舞いそのものは含まない(未決 2) | - -## 調べたこと - -`{paths}` を埋める場所(2026-10-24 の develop のコード): - -| 場所 | `{paths}` に入るもの | 根拠 | -| --- | --- | --- | -| cross-refactoring の改修ごとの範囲テスト(計画・実装・検証) | 計画の `test_targets` の `::` より前のパス(テストのファイル。項目が足すテストのファイルを含む) | `refactor_lib/targets.py` の `limited_runs`・`scope_runs_for` → `test_strategy.scope_runs` | -| cross-refactoring の `init` の着手前の範囲テスト(全体テストを CI に任せる戦略のときだけ) | `--scope` のうちテストの置き場所とみなしたもの(ディレクトリかファイル) | `refactor_lib/baseline.py` の `locations_of`・`commands_of` | -| `test-run.py` の範囲テスト | 呼ぶ側が渡したパス | `plugins/ndf/scripts/test-run.py` | -| 全体テストの組み立て(`command` が無いとき) | `.` | `test_strategy.whole_command_of` | - -範囲テストの成否は終了コードだけで決まり(`test_strategy.outcome`)、0 でも 126・127 でもない終了コードは「テストが落ちた(failed)」になる。テンプレートがパスを受けずに 1 や 5 で終わったときも、コードの失敗と同じ扱いで fix へ回る。 - -unittest の手元の再現(Python 3.14.4。`scripts/storylib/{__init__,x}.py` と `tests/test_x.py`・`tests/test_y.py`): - -| コマンド | 結果 | -| --- | --- | -| `python3 -m unittest discover -s scripts/storylib` | `NO TESTS RAN`、exit 5 | -| `python3 -m unittest discover -s tests/test_x.py` | `ImportError: Start directory is not importable`、exit 1 | -| `python3 -m unittest discover -s tests/test_x.py tests/test_y.py` | 2 つ目を開始のディレクトリと読まず、exit 1 | -| `python3 -m unittest discover -s tests` | exit 0 | -| `python3 -m unittest tests/test_x.py tests/test_y.py` | exit 0 | -| `python3 -m unittest tests`(`__init__.py` の無いディレクトリ) | `NO TESTS RAN`、exit 5 | -| `python3 -m unittest .` | `ValueError: Empty module name`、exit 1 | - -unittest には、ファイルとディレクトリの両方を受ける 1 つの書き方が無い。`discover -s {paths}` はファイルを受けず、`python3 -m unittest {paths}` はファイルを受けるがディレクトリと `.` を受けない。 - -## 目的 - -- `scope_command` に、NDF が `{paths}` へ入れるパスを受けないテンプレートを書いたプロジェクトでも、cross-refactoring の改修が fix のラウンドを無駄に使わずに進むか、着手の前に止まって直し方が分かるようにする -- 宣言を書く人(手順 0 で解析する AI と、それを確かめる人)が、`{paths}` に何が入るかを宣言の契約から知れるようにする - -## 前提 - -- 前提 1: テンプレートへの加工は「`{paths}` の字句を、シェルの引用で守った対象の並びへ置き換える」ことだけのまま変えない(`test_strategy.py` の I2)。種別と戦略をコマンドの語・ランナーの名前で決めない(I1)。したがって、テンプレートがファイルを受けるかを、コマンドの語(`discover -s` など)を見て判定しない -- 前提 2: 受けられないテンプレートを見分ける時点は、改修のラウンドに入る前(宣言の検査か、cross-refactoring の `init`)のどちらでもよい。どちらにするか、また実際に走らせて見分けるかは設計が決める(未決 1) -- 前提 3: 対象のランナーは unittest の `discover -s` を代表に確かめるが、受け入れ条件はランナーに依らない形で書く(ファイルを受けないランナーは unittest に限らない) -- 前提 4: `scope_command` を持たない suite(`round-only`)と、静的解析の suite(`kind: lint`。`{paths}` に変更したファイルが入る)の振る舞いは変えない - -## 対象範囲 - -含む: -- `{paths}` に入るパスの種類を、埋める場所ごとに宣言の契約(`project.schema.json` の `Suite` の説明と、それを生成する `project_lib/model.py`、`development-workflow` の `references/project-analysis.md` の `test` の行)へ書くこと -- 手順 0 の宣言の作成の案内で、ファイルのパスを受けないランナー(unittest の `discover -s` など)をどう書くかを示すこと -- テンプレートが `{paths}` に入るパスを受けないことを、改修のラウンドに入る前に見分けて止め、直し方を出すこと(場所は前提 2) -- テンプレートの不備で範囲テストが落ちたものを、コードの失敗として fix へ回さないこと - -含まない: -- fix の担当が「コード起因の失敗なし・コミットなし」を返しても次のラウンドへ進む、fix のラウンドの止め方そのもの(未決 2) -- `{paths}` に入れるパスの種類を変えること(ファイルをディレクトリへ直すなど)。設計で必要と分かったら、受け入れ条件を書き換えた事実を残して足す -- takemi-ohama/novel の宣言を直すこと(開発対象リポジトリの変更) -- `.ndf/supervise.json` の `test` と `.ndf/project.json` の `test.suites` の二重持ち(#2011) - -## ドメインイベント - -| # | イベント | 引き金 | 失敗したとき | 順序の前提 | -| --- | --- | --- | --- | --- | -| E1 | 宣言に `scope_command` を書いた | 手順 0 の解析か、人の手書き | テンプレートが `{paths}` に入るパスを受けない形で書かれる(今はここで分からない) | — | -| E2 | 宣言を検査した | `project-decl.py check`・戦略の解決(`test_strategy._check_templates`) | 字句の不備(`{paths}` が無い・引用の中)だけで止まる。パスを受けるかは見ない | E1 | -| E3 | 着手前のテストを走らせた | cross-refactoring の `init` | 全体テストが落ちれば止まる。CI に任せる戦略ではディレクトリで範囲テストを走らせる | E2 | -| E4 | 改修の範囲テストを組み立てた | 改修の計画・実装・検証 | `test_targets` が使えなければ項目を見送る | E3 | -| E5 | 範囲テストが落ちた | E4 のコマンドの終了コードが 0・126・127 以外 | テンプレートの不備でもコードの失敗と同じ `failed` になる(この課題の現象) | E4 | -| E6 | 落ちた項目を fix へ回した | E5 | fix の担当がコードの失敗なしを返しても、同じ失敗を繰り返す | E5 | - -E2 と E3 のあいだに、テンプレートが `{paths}` に入るパスを受けるかを確かめるイベントが無い。受け入れ条件 1・2 がこれを足す。 - -## 用語 - -| 用語 | 意味 | -| --- | --- | -| 範囲テスト | 用語集のとおり。ここでは範囲テストのテンプレートの `{paths}` に対象のパスを入れて走らせるテスト | -| 範囲テストのテンプレート | 用語集のとおり(宣言の `scope_command` か、`{paths}` を含む引数) | -| 着手前のテスト | 用語集のとおり(cross-refactoring の `init` が改修の前に 1 回走らせるテスト) | - -## 受け入れ条件 - -- [ ] 1. `scope_command` が `python3 -m unittest discover -s {paths}` で、`command` が `python3 -m unittest discover -s tests` の宣言を持つ再現のリポジトリ(「調べたこと」の配置)で cross-refactoring を走らせると、改修のラウンドに入る前に、0 以外の終了コードで止まる -- [ ] 2. 受け入れ条件 1 で止まったとき、標準エラーか結果の JSON に、当たった suite の名前・今の `scope_command`・`{paths}` に入れたパス・直す箇所(`.ndf/project.json` の `test.suites[<番号>].scope_command`)が出る -- [ ] 3. 受け入れ条件 1 の宣言で改修の範囲テストが走ることがあっても、テンプレートの不備による失敗の項目が fix の担当へ回らない(fix のラウンドが 0 回である) -- [ ] 4. `project.schema.json` の `Suite` の説明(`project_lib/model.py` から生成したもの)に、`{paths}` に入るパスの種類が、上の「調べたこと」の場所ごと(改修の範囲テスト・着手前の範囲テスト・静的解析)に書かれている。`python3 plugins/ndf/scripts/project-decl.py schema` の出力と公開のスキーマが一致する(`test_project_decl.py` の `test_published_schema_matches_the_model` が通る) -- [ ] 5. `references/project-analysis.md` の `test` の行(手順 0 の案内)に、`{paths}` に入るパスの種類と、ファイルのパスを受けないランナーの書き方(unittest を例に)が書かれている。書いた例のコマンドを再現のリポジトリで走らせ、案内どおりの結果(終了コード)になることを確かめてから書く -- [ ] 6. 退行しない: このリポジトリの宣言(pytest の `scope_command`)で、テストのファイルを `{paths}` に入れた範囲テストが今と同じコマンドで組まれ、受け入れ条件 1 の止まり方が起きない -- [ ] 7. 退行しない: 静的解析の suite(`kind: lint`)と `scope_command` の無い suite(`round-only`)の範囲テストの組み立てが変わらない(既存のテストが通る) - -## 非機能の条件 - -| 大項目 | 条件 | -| --- | --- | -| 性能・拡張性 | 受け入れ条件 1 の見分けのために足す実行は、cross-refactoring の 1 回の起動につき suite ごとに高々 1 回とし、その所要を着手前のテストの上限(`INIT_TEST_SHARE`)の中に収める | -| 運用・保守性 | 見分けた結果(止めた理由・当たった suite)が結果の JSON か状態ファイルに残り、後から読める | - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| 公開インタフェース | 変わる。cross-refactoring が改修の前に止まる場合が増える(宣言のテンプレートが `{paths}` に入るパスを受けないとき)。宣言の形(`project.schema.json` のキー)は変えない。今まで落ちていた宣言だけが止まるため、互換の経路は要らない | -| データ | 変わらない(`.ndf/project.json` の形は同じ) | -| 既存の振る舞い | テンプレートの不備で落ちていた範囲テストが fix へ回らず、着手前に止まる | - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| テスト | `uv run --frozen --project . --all-extras pytest plugins/ndf/scripts/tests -q -n 4` | -| 静的解析 | `python3 scripts/check-script-structure.py`・`python3 scripts/check-skill-frontmatter.py`・`python3 plugins/ndf/scripts/instructions-check.py --root .` | -| スキーマ | `python3 plugins/ndf/scripts/project-decl.py schema --out plugins/ndf/skills/development-workflow/schemas/project.schema.json` で作り直し、`test_published_schema_matches_the_model` が通ること | -| 手動確認 | リリース後テストで、unittest の `discover -s {paths}` を宣言に書いた再現のリポジトリ(または takemi-ohama/novel の写し)で `/ndf:cross-refactoring` を走らせ、受け入れ条件 1・2 の止まり方と出力を見る | - -## 前提とする取り決め - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | 範囲テストの組み立ては `plugins/ndf/scripts/lib/test_strategy.py` に寄せる(I10。cross-refactoring・supervise・`test-run.py`・cross-review が同じ関数を使う)。Skill ごとに見分けの関数を分けない(MVV の Value 6) | -| コーディング規約 | 標準ライブラリだけで書く(`test_strategy.py` の冒頭)。`AGENTS.md` の「コードやスクリプトが呼ぶ外部コマンドも、書く前に実行して挙動を確かめる」 | -| テスト戦略 | テンプレートの見分けは単体テスト(`plugins/ndf/scripts/tests/test_lib_test_strategy.py` の並び)で、止まり方は cross-refactoring の `init` の結合のテストで確かめる。`.md` の文言を照合するテストは書かない | - -## 境界 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 既存のテストの実行、案内に書くコマンドの事前の実行 | -| 確認してから行う | cross-refactoring が止まる条件を増やすこと(承認ゲート 1 で人が見る) | -| 行わない | コマンドの語・ランナーの名前によるテンプレートの判定(前提 1)、開発対象リポジトリの宣言の書き換え | - -## 未決 - -| 項目 | 誰が決めるか | 期限 | -| --- | --- | --- | -| 1. テンプレートがパスを受けるかを見分ける場所(`project-decl.py check` / 戦略の解決 / cross-refactoring の `init`)と手段(1 つのテストのファイルで実際に走らせるか、ほかの手段か)。全体テストと範囲テストの結果が食い違ったときにテンプレートの不備とみなす規則を含む | 設計(`design`) | 承認ゲート 1 | -| 2. fix の担当が「コード起因の失敗なし・コミットなし」を返したのに同じ失敗で次のラウンドへ進む振る舞いを、この課題で止めるか、別の課題にするか | 設計(`design`)。別にするなら `out-of-scope` で起票する | 承認ゲート 1 | -| 3. `{paths}` に入るパスの種類をそろえる(例: 着手前の範囲テストもファイルを入れる)かどうか。そろえれば unittest の `python3 -m unittest {paths}` が 1 つの書き方で通る | 設計(`design`) | 承認ゲート 1 | - -## MVV との突き合わせ - -反する疑いは無い。どのプロジェクトのランナーでも同じ工程を通すこと(Value 5)、現象の場所(fix のラウンド)ではなく宣言の契約と範囲テストの組み立ての共通の層で直すこと(Value 6)、fix の時間を無駄にしないこと(Mission)に沿う。共通原則の C1〜C8 と P1 に当たる操作は含まない(利用者の `.ndf/` は書き換えず、NDF のテンプレートと説明だけを変える)。 From 34d30f29f253f69b4abdd07ce99421fd6e6d4cfe Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sun, 25 Oct 2026 07:27:36 +0000 Subject: [PATCH 04/17] =?UTF-8?q?Docs:=20cross-review-no-test-declaration-?= =?UTF-8?q?sweep.md=20=E3=82=92=E7=A2=BA=E5=AE=9A=E4=BB=95=E6=A7=98?= =?UTF-8?q?=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/glossary.md | 2 +- docs/glossary/glossary.json | 2 +- docs/specifications/README.md | 1 + .../cross-review-no-test-declaration-sweep.md | 239 +++++++++++++++ issues/issue-1593-design.md | 287 ------------------ issues/issue-1593-requirements.md | 191 ------------ 6 files changed, 242 insertions(+), 480 deletions(-) create mode 100644 docs/specifications/cross-review-no-test-declaration-sweep.md delete mode 100644 issues/issue-1593-design.md delete mode 100644 issues/issue-1593-requirements.md diff --git a/docs/glossary.md b/docs/glossary.md index f3a043742..6f461342a 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -464,7 +464,7 @@ Pull Request のレビューを CLI へ委譲し、新しい指摘が出なく | 想定最大時間の終わり | `final_end_at` | started_at + 想定最大時間(limits.final_end_at)。中断して再開した実行では、止まっていた分だけずれる。子プロセスへ渡す上限はすべてこの時刻で切る | — | — | `docs/specifications/cross-refactoring-verify-and-final-gate.md` | | 確かめの見込み | `gate_seconds` | 最終ゲートの確かめ 1 回にかかる見込みの秒(limits.gate_seconds)。手元で全体テストを走らせるなら w、CI で見るなら c、どちらも測れていなければ 0 | — | — | `docs/specifications/cross-refactoring-verify-and-final-gate.md` | | 確かめの開始の締め切り | `gate_start_end` | 想定最大時間の終わり − 確かめの見込み(H − v)。最終ゲートの確かめを始められる最後の時刻で、最終ゲート修正と打ち切りの後の取り消しもこの時刻までに止まる | — | — | `docs/specifications/cross-refactoring-verify-and-final-gate.md` | -| テストの無い宣言 | — | .ndf/project.json の test があり、unknown と strategy を持たず、種別・雛形の誤りが無く、どの suite も command と scope_command のどちらも持たないもの(suite が 0 個のときを含む)。解析が「テストが無い」と測った {"suites": []} を含む。cross-review の最終スイープは検証のコマンドを名指ししない | — | — | — | +| テストの無い宣言 | — | .ndf/project.json の test があり、unknown と strategy を持たず、種別・雛形の誤りが無く、どの suite も command と scope_command のどちらも持たないもの(suite が 0 個のときを含む)。解析が「テストが無い」と測った {"suites": []} を含む。cross-review の最終スイープは検証のコマンドを名指ししない | — | — | `docs/specifications/cross-review-no-test-declaration-sweep.md` | | テンプレートの試走 | — | cross-refactoring の init が着手前のテストの後に、範囲テストのテンプレートへテストのファイルを最大 2 本入れて 1 回走らせ、{paths} に入るパスを受けるかを見分けること。不備か判定できなければ改修の前に止まる | — | — | `docs/specifications/cross-refactoring-scope-template-probe.md` | ## NDF のラッパー(`ndf-relay`) diff --git a/docs/glossary/glossary.json b/docs/glossary/glossary.json index 3a74cd274..2ebb4208a 100644 --- a/docs/glossary/glossary.json +++ b/docs/glossary/glossary.json @@ -3735,7 +3735,7 @@ "term": "テストの無い宣言", "context": "ndf-cross-refactoring", "meaning": ".ndf/project.json の test があり、unknown と strategy を持たず、種別・雛形の誤りが無く、どの suite も command と scope_command のどちらも持たないもの(suite が 0 個のときを含む)。解析が「テストが無い」と測った {\"suites\": []} を含む。cross-review の最終スイープは検証のコマンドを名指ししない", - "pending_source": "issues/issue-1593-design.md" + "source": "docs/specifications/cross-review-no-test-declaration-sweep.md" }, { "term": "未解決のスレッド", diff --git a/docs/specifications/README.md b/docs/specifications/README.md index 763399e94..e854f72a0 100644 --- a/docs/specifications/README.md +++ b/docs/specifications/README.md @@ -97,5 +97,6 @@ | [ndf-document-rebuild.md](ndf-document-rebuild.md) | 文書の組み直し(文書形式と文書の目的の切り方、document-style との正本の割り当て、キーワードと読み手役の確かめ・題の付け方の置き場、スラッシュコマンドの一覧から外す Skill、常に成り立つ条件と決定の理由)。手順は document-rebuild の SKILL.md が正 | | [ndf-workflow-development-loop.md](ndf-workflow-development-loop.md) | 開発ループ(`--loop`)。`sprint-state.py loop` の副命令の契約(終了コード 0/1/3・止まった結果の形)、選定の規則、承認ゲートで止まったときの扱い、止まる条件と再開、ループの記録(`.ndf/loop/<名>/`)の形、`new close --pace auto` と品質の枠、常に成り立つ条件、決定の理由と外部の動向。手順は `development-workflow` の `references/loop.md` が正 | | [cross-refactoring-scope-template-probe.md](cross-refactoring-scope-template-probe.md) | cross-refactoring の init が着手前のテストの後に範囲テストのテンプレートを試走し(suite ごとにテストのファイルを最大 2 本)、`{paths}` に入るパスを受けない・確かめられないなら直す箇所を出して改修の前に止まる理由と規則(候補の選び方・判定の表・`template-check.json` の形)。止まり方は `cross-refactoring` の SKILL.md、`{paths}` に入るパスの種類は `development-workflow` の `references/project-analysis.md` が正 | +| [cross-review-no-test-declaration-sweep.md](cross-review-no-test-declaration-sweep.md) | テストの無い宣言(`{"suites": []}` を含む)で cross-review の最終スイープが検証のコマンドを名指しせずに進み、駆動の件数に継続的統合の判定 `ci`・`ci_reason` を載せ、supervise の止まった理由に元の終了コードを添える理由と境界(`verify_commands` の判定の順・種別と雛形の誤りは止める)。件数の一覧は `cross-review` の SKILL.md、drive のステップの読み方は `supervise_lib/plan.py` が正 | Skill の挙動仕様はここに置かない。Skill に関する詳細は対象 Skill の `SKILL.md` を参照する。 diff --git a/docs/specifications/cross-review-no-test-declaration-sweep.md b/docs/specifications/cross-review-no-test-declaration-sweep.md new file mode 100644 index 000000000..df990d8a1 --- /dev/null +++ b/docs/specifications/cross-review-no-test-declaration-sweep.md @@ -0,0 +1,239 @@ +# cross-review: テストも CI も無いリポジトリで、収束した cross-review が最終スイープの手前で終了コード 2 で止まり、検査のプランが毎回止まっていた → テストの無い宣言では検証のコマンドを名指しせずに最終スイープへ進み、止まったときは元の終了コードと理由が報告の 1 行に出る + +## 概要 + +**`.ndf/project.json` の `test` が「テストの無い宣言」(解析が測った `{"suites": []}` を含む)のとき、cross-review の最終スイープの指示は +検証のコマンドを名指しせずに組まれ、駆動は止まらずに最終スイープへ進む。** 種別か雛形に誤りのある宣言は、今までどおり終了コード 2 で止まる。 + +あわせて次の 2 つを足した。 + +- **駆動の結果の件数(`metrics`)に、最後のラウンドの継続的統合の判定 `ci` と理由 `ci_reason` が載る。** supervise の報告の件数の行へ + そのまま写るため、継続的統合を確かめられなかったこと(`ci unverified`)が承認する人に届く +- **supervise の drive のステップが駆動の `stopped` で失敗したとき、止まった理由の 1 行に、駆動の `summary` と元の終了コード(`metrics.exit`)が載る。** + +例: 継続的統合を持たず、`test` が `{"suites": []}` のリポジトリで検査のプランを流すと、cross-review が `final: approved` で収束した後、 +最終スイープへ進み、駆動が `ok` を返す。`review` のステップが成功して `base-sync`・`test-all` まで流れ、報告の `review` の件数の行に +`ci unverified` と理由(「チェックジョブを照会できない(…)」)が出る。`supervise.py run --from test-all` で手で再開しなくてよい。 + +種別の誤りで止まる場合の、supervise の止まった理由の形(検査のプランが `abort` へ回したとき): + +```text +ステップ abort が失敗した(exit=1): …: review: 最終スイープの指示を組めない: .ndf/project.json の test が不正: test.suites[0].kind は test か lint で書く(今: unit)(元の終了コード 2) +``` + +**駆動の件数の一覧と読み方、drive のステップの読み方は Skill とスクリプトの説明が正である。** ここに書き写さない。 + +| 何を | 正本 | +| --- | --- | +| 駆動の結果の件数(`ci`・`ci_reason` を含む)の一覧 | [`cross-review` の SKILL.md](../../plugins/ndf/skills/cross-review/SKILL.md) と [`drive.py`](../../plugins/ndf/skills/cross-review/scripts/drive.py) の docstring | +| drive のステップの読み方(件数の写し方・止まった理由に元の終了コードが添うこと) | [`supervise_lib/plan.py`](../../plugins/ndf/scripts/supervise_lib/plan.py) の docstring | +| 最終スイープが検証のコマンドを探す既定の探し方 | `cross-review` の [`docs/02-fix-and-rotation.md`](../../plugins/ndf/skills/cross-review/docs/02-fix-and-rotation.md) の Step 7.5 | +| `metrics.exit`(止めたスクリプトの元の終了コード)の共通の意味 | [`lib/drive_pause.py`](../../plugins/ndf/scripts/lib/drive_pause.py) | + +この文書が扱うのは、止まっていた原因の特定、決定の理由、常に成り立つ条件、`verify_commands` の判定の順、契約、テスト観点である。 + +## 用語 + +定義は [用語集](../glossary.md) が正である(`.ndf/glossary.json` の `document`)。この文書で使う語は「テストの無い宣言」「最終スイープ」 +「全体テスト」「テストの戦略」である。 + +`review_status` の `unverified`(最終スイープが確かめられていないこと。`lib/loop_drive.py`)と、継続的統合の照会の `unverified` +(`ci.verdict`。チェックジョブを照会できない)は別の意味である。この文書の `ci unverified` は後者を指す。 + +コンテキストの関係: cross-review の駆動は cross-refactoring のテストの戦略の語(全体テスト・宣言の `test`)を `lib/test_strategy.py` で +共有する(共有カーネル)。supervise は駆動の結果 JSON を `lib/step_result.py` の形で読み、`metrics` を件数として写すだけで解釈しない +(公開された言語)。例外は `metrics.exit` で、`lib/drive_pause.py` が全駆動に共通の意味を定める。 + +## 対象範囲 + +| 扱う | 扱わない | +| --- | --- | +| テストの無い宣言で、cross-review の駆動が最終スイープへ進むこと(単独の `/ndf:cross-review`・スプリントの検査・単発の検査・cross-refactoring の最終ゲートの入れ子のすべて) | cross-review の収束の判定そのもの(ラウンド・承認の数え方) | +| 駆動の件数に継続的統合の判定と理由を載せること | 継続的統合があるリポジトリでのチェックジョブの失敗の扱い(`code_failure` は今のとおり修正へ回す) | +| supervise の止まった理由に元の終了コードを添えること | `review_status` と継続的統合の照会の `unverified` の改名 | +| | 検査のプランの雛形(`templates.py`)。継続的統合の有無で分岐する値をプランへ書かない | +| | supervise の `new sprint` と cross-refactoring の全体テストの解決(`resolve`。テストの無い宣言では今のとおり止まる) | + +テストの有無は宣言(`.ndf/project.json` の `test`)で受け、継続的統合の有無は宣言で受けず、実行の時点の PR の head のチェックジョブの +照会結果で受ける。配布先のリポジトリでも働く。宣言は利用者の設定で、この仕様は読む側の解釈だけを変え、宣言を書き換えない。 + +## 背景 + +CI を持たない takemi-ohama/novel のスプリント pipeline(PR takemi-ohama/novel#35)で、検査のプランの `review`(cross-review の駆動。 +`on_fail: abort`)が「exit 2(unverified)」で止まり、指摘が収束していてもプランが進まなかった。続けるには `supervise.py run --from test-all` で +手で再開するしか無かった(#1426 の「プランの外の手当てをなくす」に反する)。状態ファイルは `final: approved`・`sweep: null`・最後のラウンドの +`ci.verdict` が `unverified` だった。 + +コードを読むと、題の「unverified(exit 2)」は止まった原因ではなかった。 + +| # | 読んだこと | +| --- | --- | +| 1 | supervise の drive のステップは、駆動の結果 JSON の `status` が `ok` のときだけ成功とする。`metrics.review_status` は成否に使わない | +| 2 | cross-review の駆動の終了コードは 0・1・20〜23 だけで、止まったときは止めたスクリプトの元の終了コードを `metrics.exit` へ写す。「exit 2」はこの値だった | +| 3 | 継続的統合の照会が `unverified` でも、判定は「⚠ 継続的統合を確かめられないまま収束する」を出して収束として exit 0 で返す | +| 4 | 状態の `sweep` は `verify-sweep` が書く。`final: approved` で `sweep: null` は、収束の後、`verify-sweep` より前で駆動が止まったことを示す | +| 5 | 収束の直後に最終スイープの指示を組む `sweep_verify_lines` は、宣言の `test` を解けないと `Stop`(終了コード 2)を上げる。解析はテストの手段・コマンド・記述がどれも無いリポジトリの `test` を `{"suites": []}` と書き(`project_lib/measure_repo.measure_test`)、`verify_commands` はこれを「`test.suites に command を持つ suite が無い`」の `StrategyError` にしていた | + +1〜5 の 3 つの値(`final: approved`・`sweep: null`・`metrics.exit` = 2)とテストも CI も無いことに合うのは 5 である。原因は継続的統合ではなく、 +解析が正しく測った「テストが無い」を不正な宣言として扱うことだった。 + +## 決定と理由 + +| # | 決定 | 理由 | 採らなかった案 | +| ---: | --- | --- | --- | +| 1 | 検査のプランの雛形を変えず、最終スイープの検証コマンドの解き方(`verify_commands`)を直す | 止めているのは `sweep_verify_lines` が `StrategyError` を終了コード 2 の `Stop` にする経路で、継続的統合の照会は収束を妨げない。単独の `/ndf:cross-review` も同じ経路を通るため、雛形で直すと単独の利用で止まりが残る | 起票時の提案(継続的統合の有無を見て `review` の `on_fail` を切り替える・`test-all` を確かめの代わりにする)。原因が継続的統合でないため直らず、雛形に継続的統合の有無の分岐を足すと「同じ雛形が通る」から外れる | +| 2 | 種別と雛形の検査をテストの有無の判定より先に当てる | 不正な宣言(種別・雛形の誤り)を `StrategyError` で止めるのは、最終スイープの既定の探し方へ黙って流さないためである。`command` の無い suite に `kind: unit` があることもあり、それまで `None` にすると黙って流れる | `resolve` の「`command` を持つ suite が無い」を全体で `None` にする(`new sprint` と cross-refactoring は全体テストを要し、そこでは止めるのが正しい) | +| 3 | 継続的統合を確かめられなかったことは駆動の件数へ最後のラウンドの判定を写して残し、`test-all` を代わりの確かめとは書かない | 状態ファイルに最後のラウンドの `ci.verdict` と理由が既にあり、写せば supervise は今の仕組み(ステップの `counts` → 報告の件数の行)のまま載せられる。`test-all` は手元の全体テストで、継続的統合が走らせるチェック(別の環境・別のジョブ)を確かめたとは言えない | `test-all` を継続的統合の代わりの確かめとして承認資料に書く(測っていないことを確かめたと示す) | +| 4 | 元の終了コードは supervise の 1 か所(`_result_summary`。`_stop_detail` が使う)で添える | `metrics.exit` は全駆動に共通の意味を持つ。1 か所で添えれば cross-review と cross-refactoring の両方の駆動に効く。駆動の側は理由文で止めた所を名指しすることだけを持つ(`sweep_verify_lines` の理由文に「最終スイープの指示を組めない」を足した) | 駆動の `summary` へ終了コードを書き足す(単独の cross-review でも読まれ、同じ値を 2 か所に持つ) | +| 5 | 宣言の不正を検査のプランの書き出しで先に見つける検査は足さない | 決定 1・2 の後で最終スイープが止まる宣言は、種別・雛形の誤りか戦略を解けない宣言である。`--test-cmd` を渡さない `new sprint` / `new check` は同じ `resolve` で宣言を解き、`DeclError` として書き出しで止まる。`--test-cmd` を渡したときだけ最終スイープで止まり、その止まりは決定 4 で報告に出る | 書き出しに同じ検査を重ねる | + +## 仕様 + +### 構成要素と責務 + +| 要素 | 責務 | +| --- | --- | +| [`lib/test_strategy.py`](../../plugins/ndf/scripts/lib/test_strategy.py) の `verify_commands` | 宣言の `test` から最終スイープに名指しする全体テストのコマンドを返す。テストの無い宣言では `None`、種別・雛形の誤りは `StrategyError` | +| [`drive.py`](../../plugins/ndf/skills/cross-review/scripts/drive.py) の `sweep_verify_lines` | 最終スイープの検証の段落を組む。`verify_commands` が `None` なら既定の探し方の段落、`StrategyError` なら理由文「最終スイープの指示を組めない: .ndf/project.json の test が不正: …」の `Stop`(2) | +| `drive.py` の `Drive.counts` と [`review_lib/ci.py`](../../plugins/ndf/skills/cross-review/scripts/review_lib/ci.py) の `ci_counts` | 駆動の結果の件数を組む。最後のラウンドの `ci.verdict` を `ci`、理由を `ci_reason` として足す | +| [`supervise_lib/engine.py`](../../plugins/ndf/scripts/supervise_lib/engine.py) の `_result_summary`・`_stop_detail` | 止まった理由へ、ステップの結果の `summary` と、`abort` へ回した直前のステップの `summary` を添える。結果 JSON が `stopped` で `metrics.exit` が整数なら、要約の後ろへ「(元の終了コード N)」を添える | + +```mermaid +graph LR + subgraph CR[cross-review の駆動] + SP[Drive.sweep_prompt] --> SVL[sweep_verify_lines] + CNT[Drive.counts] --> CC[ci.ci_counts] + STP[Drive.stopped] + end + subgraph LIB[lib] + VC[test_strategy.verify_commands] + end + subgraph SV[supervise] + DS[DriveStep.execute] + SD[engine._stop_detail] + RP[state.write_report] + end + ST[(状態ファイル)] + SVL --> VC + CC -.-> ST + STP --> CNT + DS -->|結果 JSON| CNT + DS -->|counts| RP + SD -->|理由| RP + DS -.->|stopped の JSON| SD +``` + +| 集約 | 持ち主(書き換えてよいもの) | 根 | 値オブジェクト | +| --- | --- | --- | --- | +| 収束ループの状態 | cross-review の `state.py`(駆動は読むだけ) | 状態ファイル(`cross-review-pr-state.json`。エンティティはラウンド) | 継続的統合の判定(`verdict`・`reason`)・最終スイープの記録(`sweep`) | +| 駆動の結果 | `drive.py`(`Drive.counts`・`Drive.stopped`) | 結果 JSON(1 行) | 件数(`metrics`)・止まりの理由(`summary`)・元の終了コード(`metrics.exit`) | +| ステップの記録 | supervise の `engine.py`・`worker_steps.py` | ステップの記録(`state.cur`) | 件数(`counts`)・止まった理由 | + +### 常に成り立つ条件 + +| # | 条件 | 破れたときの扱い | +| --- | --- | --- | +| I1 | 宣言の `test` がテストの無い宣言のとき、最終スイープの指示を組む処理は止まらず、検証のコマンドを名指ししない | テストで落とす | +| I2 | 宣言の `test` に種別か雛形の誤りがあるときは、テストの有無にかかわらず終了コード 2 で止める | テストで落とす | +| I3 | 最後のラウンドに継続的統合の判定があれば、駆動の件数の `ci` にその判定が載る。判定が `unverified`・`meta_only`・`pending` なら理由(`ci_reason`)も載る | テストで落とす | +| I4 | 件数へ足すのは新しいキーだけで、既存のキー(`review_status` ほか)の値と意味を変えない(cross-refactoring の最終ゲートの判定が変わるため) | テストで落とす | +| I5 | drive のステップが駆動の `stopped` で失敗したとき、supervise の止まった理由の 1 行に駆動の `summary` と元の終了コード(`metrics.exit`)が載る | テストで落とす | +| I6 | 継続的統合の判定は駆動の成否を変えない。`code_failure` は修正へ回し(判定の終了コード 2)、`unverified` は収束を妨げない | 既存のテストで落とす | + +### `verify_commands` の判定の順 + +上から当てる。 + +| # | 宣言の `test` | 返すもの | +| --- | --- | --- | +| 1 | 無い・`unknown` を持つ | `None` | +| 2 | 種別か雛形に誤りがある | `StrategyError`(I2) | +| 3 | `strategy` を書かず、どの suite も `command` と `scope_command` のどちらも持たない(suite が 0 個のときを含む。テストの無い宣言) | `None`(I1) | +| 4 | 戦略を解けない(`local-scoped-ci-whole` を宣言したのに `scope_command` が無い など) | `StrategyError` | +| 5 | 解ける | 全体テストのコマンドの並び(空なら `None`) | + +2 は `resolve` が使うのと同じ種別と雛形の検査をそのまま使い、新しい検査を作らない。 + +### 処理の流れ + +```mermaid +sequenceDiagram + participant SV as supervise DriveStep + participant DR as drive.py + participant TS as test_strategy + participant W as worker + SV->>DR: drive.py を打つ + DR->>DR: judge(CI unverified でも収束。final=approved) + DR->>DR: 段階 sweep-start + DR->>TS: verify_commands(宣言) + alt 種別・雛形の誤り(I2) + TS-->>DR: StrategyError + DR-->>SV: stopped(summary に理由・metrics.exit=2) + SV->>SV: _stop_detail が理由と元の終了コードを添える(I5) + else テストの無い宣言(I1) + TS-->>DR: None + DR-->>SV: gate sweep(既定の探し方の段落) + SV->>W: 最終スイープ + W-->>SV: 結果ファイル + SV->>DR: 打ち直し → finish → verify-sweep + DR-->>SV: ok(metrics.ci=unverified・ci_reason) + SV->>SV: review 成功 → base-sync → test-all + else command を持つ suite がある + TS-->>DR: コマンドの並び + DR-->>SV: gate sweep(コマンドを名指しする段落) + end +``` + +## データ・設定 + +駆動の結果 JSON の `metrics` へ足した 2 つのキー(既存のキーは変えない): + +| キー | 型 | いつ載るか | 値 | +| --- | --- | --- | --- | +| `ci` | 文字列か `null` | いつも(`gate`・`ok`・`stopped` のどれでも。件数を組めるとき) | 最後のラウンドの `ci.verdict`(判定の値そのもの)。最後のラウンドに `ci` が無ければ `null` | +| `ci_reason` | 文字列か `null` | いつも | `ci` が `unverified` なら `ci.reason`、`meta_only` なら `ci.note`、`pending` なら未完了のチェックジョブの名前を空白で連ねたもの。ほかは `null` | + +supervise の件数の行(`counts_text`)は `null` のキーを出さないため、継続的統合の判定が無い実行の件数の行は変わらない。判定の値の集合は +`review_lib/ci.py` の `_round_ci` が持ち、件数は写すだけである。 + +supervise の止まった理由は `ステップ が失敗した(exit=<ステップの exit>): <要約>` の形で、要約は結果 JSON の `summary` である。結果 JSON の +`status` が `stopped` で `metrics.exit` が整数(真偽値を除く)のとき、要約の後ろへ `(元の終了コード )` を添える。検査のプランの +`abort` へ回したときの「直前のステップの要約」にも同じ規則を当てる。`status` が `ok` の結果と `metrics.exit` を持たない結果は今の形のままである。 + +## 運用 + +### 既知の限界 + +| 項目 | 内容 | +| --- | --- | +| novel の宣言 | takemi-ohama/novel の `test` が `{"suites": []}` だったかは記録が手元に無く確かめられない。CI もテストも無い一時リポジトリ(`project-decl.py measure` が `test` を `{"suites": []}` と測った)で、直す前の実装が同じ値(`final: approved`・`unresolved: null`・`metrics.exit` 2)で止まることを確かめた(#1593 のコメント) | +| テストの無いリポジトリでの `test-all` | テストの無い宣言で `new sprint` を流すには `--test-cmd` か `.ndf/supervise.json` の `test.command` が要る(決定 2)。`test-all` がその雛形で通るかはこの仕様の範囲外である | +| 最終スイープの探し方 | テストの無い宣言では、最終スイープは既定の探し方(Step 7.5)で検証のコマンドを探し、見つからなければ検証を走らせない。宣言が無いときと同じ振る舞いである | + +## テスト観点 + +- `verify_commands({"test": {"suites": []}})` と、`command` も `scope_command` も無い suite だけを持つ宣言が `None` を返すこと。`strategy: local-scoped-ci-whole` + を書いた `suites: []` は `StrategyError` のままであること。テストの無い宣言の作業ディレクトリで `sweep_verify_lines` が `Stop` を上げないこと(I1。 + `plugins/ndf/scripts/tests/test_lib_test_strategy.py`・`plugins/ndf/skills/cross-review/tests/test_drive_sweep_verify.py`) +- `command` の無い suite に `kind: unit` を書いた宣言と、`{paths}` の誤った雛形を持つ宣言で `StrategyError` が上がり、`sweep_verify_lines` が終了コード 2 の + `Stop` を上げること(I2。判定の順の 2 と 3 を入れ替えると落ちる) +- 最後のラウンドの `ci` が `unverified`(理由つき)の状態から組んだ件数が `ci` = `unverified`・`ci_reason` = 理由になり、`ci` の無いラウンドでは両方 `null`。 + `meta_only` では `note` が理由になること。supervise の報告の件数の行に `ci unverified` が出ること(I3。`test_supervise_stop_exit.py` の `test_the_counts_line_carries_the_unverified_ci`) +- 同じ状態から組んだ件数の既存のキー(`review_status` ほか)の値が変わらないこと(I4) +- drive のステップの結果 JSON が `stopped`・`metrics.exit` = 2 のとき、止まった理由に駆動の `summary` と「元の終了コード 2」が載り、`abort` へ回したときの直前の + ステップの要約にも載ること。`metrics.exit` が無い結果・`status` が `ok` の結果では今の形のままであること(I5。`test_supervise_stop_exit.py`) +- 継続的統合が `code_failure` のとき判定が終了コード 2 で修正へ回すこと、駆動が `stopped` を返したとき `review` のステップが失敗して `on_fail: abort` へ進むこと + (I6。既存のテスト) +- 実機の再現(#1593 のコメント): CI の無い一時リポジトリの PR で `new check --test-cmd 'true {paths}'` の検査のプランを `queue` で流すと、直す前の実装は + cross-review が承認で収束したのに駆動が `stopped`・`metrics.exit` 2・理由「.ndf/project.json の test が不正: test.suites に command を持つ suite が無い」で + 止まり、`review` が `abort` へ進んだ。直した後は `review` が `ok` で `base-sync` → `test-all` へ進み、報告の件数の行に + `ci unverified / ci_reason チェックジョブを照会できない(未 push・権限・チェックジョブ 0 件のいずれか)` が出た。`progress.jsonl` の `review` の行は件数のキーを持たず、件数は報告の側に載る + +## 関連リンク + +- [`cross-review` の SKILL.md](../../plugins/ndf/skills/cross-review/SKILL.md)・[`drive.py`](../../plugins/ndf/skills/cross-review/scripts/drive.py)・[`review_lib/ci.py`](../../plugins/ndf/skills/cross-review/scripts/review_lib/ci.py) +- [`lib/test_strategy.py`](../../plugins/ndf/scripts/lib/test_strategy.py)・[`lib/drive_pause.py`](../../plugins/ndf/scripts/lib/drive_pause.py)・[`lib/loop_drive.py`](../../plugins/ndf/scripts/lib/loop_drive.py) +- [`supervise_lib/engine.py`](../../plugins/ndf/scripts/supervise_lib/engine.py)・[`supervise_lib/plan.py`](../../plugins/ndf/scripts/supervise_lib/plan.py) +- [ndf-supervise-base-sync.md](ndf-supervise-base-sync.md)(検査のプランの並びと `_stop_detail` の直前のステップの要約。#1917) +- [cross-review-evidence-based.md](cross-review-evidence-based.md)(状態ファイルの契約) +- 課題: #1593(由来 takemi-ohama/novel PR #35。関連 #1426・#1340・#1483) diff --git a/issues/issue-1593-design.md b/issues/issue-1593-design.md deleted file mode 100644 index aecba5a7a..000000000 --- a/issues/issue-1593-design.md +++ /dev/null @@ -1,287 +0,0 @@ -# CI もテストも無いリポジトリで、収束した cross-review が最終スイープの手前で終了コード 2 で止まり、検査のプランが毎回止まる → テストの無い宣言では検証のコマンドを名指しせずに最終スイープへ進み、止まったときは元の終了コードと理由が報告の 1 行に出る(#1593) - -## 目的 - -- **何が壊れているか**: 解析は、テストの無いリポジトリの宣言を `.ndf/project.json` の `test` = `{"suites": []}` と書く。cross-review の最終スイープの指示を組む関数は、この値を「不正な宣言」として終了コード 2 で止める。収束した後に毎回止まり、継続的統合の未検証と見分けがつかない -- **誰が困るか**: テストも継続的統合も持たないリポジトリ(文章・設定だけのリポジトリなど)でスプリントの検査を流す conductor。`supervise.py run --from test-all` で手で再開している(#1426) -- **直すと何が成り立つか**: テストの無い宣言では、検証のコマンドを名指しせずに最終スイープへ進み、`review` の先(`base-sync`・`test-all`)へ流れる。駆動が止まるべきときは、止めた理由と元の終了コードが supervise の報告の 1 行に出る。継続的統合を確かめられなかったことは `review` の件数に残る - -## 適用範囲 - -- **働く範囲**: 配布先のリポジトリでも働く。対象は cross-review の駆動と supervise の drive のステップである - - cross-review の駆動: 単独の `/ndf:cross-review` を含む - - supervise の drive のステップ: スプリントの検査・単発の検査・cross-refactoring の最終ゲートの入れ子 -- **プロジェクトごとに違うもの**: テストの有無は宣言(`.ndf/project.json` の `test`)で受ける。継続的統合の有無は宣言で受けず、PR の head のチェックジョブの照会結果で受ける(要求の前提 4)。雛形に値を埋め込まない -- **当たるモード**: `standard` - -## あるべき姿の根拠 - -| 根拠 | 種類 | 何を示すか | -| --- | --- | --- | -| `test_strategy.verify_commands({"test": {"suites": []}})` が `StrategyError: test.suites に command を持つ suite が無い` を上げる(2026-10-24、`4602364f` で実行) | 実測 | テストの無い宣言を、最終スイープの指示を組む経路が「不正」として扱う | -| `measure_repo.measure_test` は、テストの手段・コマンド・記述がどれも無いリポジトリに `measured({"suites": []})` を返す(`plugins/ndf/scripts/project_lib/measure_repo.py:354-356`) | 実測(コードの読み) | `{"suites": []}` は解析が正しく測った値であり、壊れた宣言ではない | -| `verify_commands` の docstring「宣言が無いときと分けないと、不正な宣言が最終スイープの既定の探し方へ黙って流れる」(#1483 I13) | 既存の決定 | 不正な宣言(種別・雛形の誤り)を止める理由は「黙って流れる」ことであり、テストが無いと測った宣言には当たらない | - -要求と受け入れ条件は #1593 の本文にある(コピーは `issues/issue-1593-requirements.md`)。この文書は「どう作るか」だけを扱う。 - -## ドメインモデル - -### コンテキスト - -| コンテキスト | 何の語が 1 つの意味に決まるか | -| --- | --- | -| cross-review(`ndf-cross-review`) | 収束ループ・最終スイープ・継続的統合の判定(`ci.verdict`)・駆動の止まり | -| cross-refactoring(`ndf-cross-refactoring`) | 宣言の `test` から解くテストの戦略と全体テスト(`lib/test_strategy.py`) | -| 開発ワークフロー(`ndf-workflow`) | プラン・ステップ・件数・フェーズレポート(supervise) | - -コンテキストの関係は次の 2 つである。 - -| 使う側 | 使われる側 | 関係 | 中身 | -| --- | --- | --- | --- | -| cross-review の駆動 | cross-refactoring(テストの戦略の語) | 共有カーネル | 全体テスト・宣言の `test` の語を使う。`lib/test_strategy.py` は同じチームが持ち、両方が読む | -| supervise | cross-review の駆動(結果 JSON) | 公開された言語 | `lib/step_result.py` の形で読む。`metrics` の中身は駆動が決め、supervise は件数として写すだけで解釈しない。例外は `metrics.exit` で、`lib/drive_pause.py` が全駆動に共通の意味を定める | - -### 集約 - -| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | -| --- | --- | --- | --- | --- | -| 収束ループの状態 | cross-review の `state.py`(駆動は読むだけ) | 状態ファイル(`cross-review-pr-state.json`) | ラウンド | 継続的統合の判定(`verdict`・`reason`)・最終スイープの記録(`sweep`) | -| 駆動の結果 | cross-review の `drive.py`(`Drive.counts`・`Drive.stopped`) | 結果 JSON(1 行) | — | 件数(`metrics`)・止まりの理由(`summary`)・元の終了コード(`metrics.exit`) | -| ステップの記録 | supervise の `engine.py`・`worker_steps.py` | ステップの記録(`state.cur`) | — | 件数(`counts`)・止まった理由 | - -**宣言の `test` はこの変更の集約に含めない。** 宣言は利用者の設定(C7)で、この変更は読む側の解釈だけを変え、宣言を書き換えない。 - -### 不変条件 - -| # | 集約 | 条件 | 破れたときの扱い | -| --- | --- | --- | --- | -| I1 | 駆動の結果 | 宣言の `test` が「テストの無い宣言」のとき、最終スイープの指示を組む処理は止まらず、検証のコマンドを名指ししない | 止まれば受け入れ条件 2 を満たさない。テストで落とす | -| I2 | 駆動の結果 | 宣言の `test` に種別か雛形の誤りがあるときは、テストの有無にかかわらず今のとおり終了コード 2 で止める(#1483 I13 を保つ) | 黙って進めば不正な宣言が既定の探し方へ流れる。テストで落とす | -| I3 | 駆動の結果 | 最後のラウンドに継続的統合の判定があれば、駆動の結果の件数(`metrics.ci`)にその判定が載る。判定が `unverified`・`meta_only`・`pending` なら理由(`metrics.ci_reason`)も載る | 載らなければ受け入れ条件 3 を満たさない | -| I4 | 駆動の結果 | 件数へ足すのは新しいキーだけで、既存のキー(`review_status` ほか)の値と意味を変えない | cross-refactoring の最終ゲートの判定が変わる。テストで落とす | -| I5 | ステップの記録 | drive のステップが駆動の `stopped` で失敗したとき、supervise の止まった理由の 1 行に、駆動の `summary` と元の終了コード(`metrics.exit`)が載る | 載らなければ受け入れ条件 4 を満たさない | -| I6 | 駆動の結果 | 継続的統合の判定は、今のとおり駆動の成否を変えない。`code_failure` は修正へ回し、`unverified` は収束を妨げない | 継続的統合のあるリポジトリの判定が緩む・きつくなる(受け入れ条件 5)。テストで落とす | - -### ドメインイベント - -| # | イベント | 発生元 | 受け手 | -| --- | --- | --- | --- | -| E3 | 判定が継続的統合を照会し、`unverified` を記録した | `state.py judge` | 状態ファイル → `Drive.counts`(I3) | -| E4 | 状態に `final: approved` を書いた | `state.py judge` | 駆動の段階 `sweep-start` | -| E5 | 最終スイープの指示を組んだ | `Drive.sweep_prompt`(`sweep_verify_lines`) | supervise の drive のステップ(pause を worker へ渡す)。テストの無い宣言では止まらない(I1) | -| E7 | `verify-sweep` が状態の `sweep` を書いた | `state.py verify-sweep` | `Drive.finish` | -| E8 | 駆動が `ok` を返した | `Drive.finish` | supervise の drive のステップ(`review` が成功して `base-sync` へ) | -| E9 | 継続的統合を確かめられなかったことが報告に載った | supervise の `write_report`(件数の行) | conductor・承認ゲートの資料 | -| E11 | 駆動が `stopped` を返した(理由と元の終了コードつき) | `Drive.stopped` | supervise の `_stop_detail`(I5) | - -E1・E2・E6・E10 は要求のとおりで、この変更は発生元も受け手も変えない。E11 は要求に無いイベントで、受け入れ条件 4 の経路を表すために足した。 - -### 用語 - -| 用語 | 意味 | 用語集への反映 | -| --- | --- | --- | -| テストの無い宣言 | `.ndf/project.json` の `test` があり、`unknown` と `strategy` を持たず、種別・雛形の誤りが無く、どの suite も `command` と `scope_command` のどちらも持たないもの(suite が 0 個のときを含む)(解析が「テストが無い」と測った `{"suites": []}` を含む) | 追加(`ndf-cross-refactoring`) | - -## 機能一覧 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| F1 | テストの無い宣言のリポジトリで、収束した cross-review が最終スイープへ進み、駆動が `ok` で終わる | cross-review を使う開発者・supervise の検査のプラン | -| F2 | 駆動の結果の件数に、最後のラウンドの継続的統合の判定と理由が載る | supervise の報告を読む conductor・承認する人 | -| F3 | drive のステップが駆動の止まりで失敗したとき、止まった理由の 1 行に駆動の理由文と元の終了コードが載る | supervise の報告を読む conductor | -| F4 | 止まった出所の再現(受け入れ条件 1)を、手元の再現と一時リポジトリの再現の 2 段で行う | 実装の担当(conductor) | - -## 構成要素 - -| 要素 | 責務 | 変えること | -| --- | --- | --- | -| `plugins/ndf/scripts/lib/test_strategy.py` の `verify_commands` | 宣言の `test` から、最終スイープに名指しする全体テストのコマンドを返す | テストの無い宣言では `None` を返す(I1)。種別・雛形の誤りは今のとおり `StrategyError`(I2) | -| `plugins/ndf/skills/cross-review/scripts/drive.py` の `Drive.counts` | 駆動の結果の件数を組む | 最後のラウンドの `ci.verdict` を `ci`、理由を `ci_reason` として足す(I3・I4)。docstring の件数の一覧も足す | -| `plugins/ndf/skills/cross-review/scripts/drive.py` の `sweep_verify_lines` | 最終スイープの検証の段落を組む | 変えない(`verify_commands` が `None` を返せば今の探し方の段落になる)。`Stop` の理由文の頭に「最終スイープの指示を組めない」を足し、継続的統合の未検証と見分けられるようにする | -| `plugins/ndf/scripts/supervise_lib/engine.py` の `_stop_detail` | 止まった理由へ、ステップの結果の要約を添える | 結果 JSON が `stopped` で `metrics.exit` を持つとき、要約の後ろへ「(元の終了コード N)」を添える(I5) | -| `plugins/ndf/scripts/supervise_lib/plan.py` の docstring(drive のステップの節) | drive のステップの読み方を説明する | 件数に `ci`・`ci_reason` が載ること、止まった理由に元の終了コードが添うことを足す | -| `plugins/ndf/skills/cross-review/SKILL.md`(駆動の件数の一覧。170 行目付近)と `drive.py` の docstring(17 行目) | 駆動の結果の読み方を説明する | 件数の一覧へ `ci`・`ci_reason` を足す | -| テスト(`plugins/ndf/scripts/tests/test_lib_test_strategy.py`・cross-review の駆動のテスト・supervise の engine のテスト) | 不変条件を縛る | 下の「テスト設計」の行を足す | - -構成要素図(変える要素だけ。点線は読むだけの関係): - -```mermaid -graph LR - subgraph CR[cross-review の駆動] - SP[Drive.sweep_prompt] --> SVL[sweep_verify_lines] - CNT[Drive.counts] - STP[Drive.stopped] - end - subgraph LIB[lib] - VC[test_strategy.verify_commands] - end - subgraph SV[supervise] - DS[DriveStep.execute] - SD[engine._stop_detail] - RP[state.write_report] - end - ST[(状態ファイル)] - SVL --> VC - CNT -.-> ST - STP --> CNT - DS -->|結果 JSON| CNT - DS -->|counts| RP - SD -->|理由| RP - DS -.->|stopped の JSON| SD -``` - -`plan.py` の docstring・`SKILL.md`・テストは説明と検査のため、図に含めない。 - -## 入出力の契約 - -**駆動の結果 JSON の `metrics` へ 2 つのキーを足す。** 既存のキーは変えない(I4)。 - -| キー | 型 | いつ載るか | 値 | -| --- | --- | --- | --- | -| `ci` | 文字列か `null` | いつも(`gate`・`ok`・`stopped` のどれでも。`counts` を組めるとき) | 最後のラウンドの `ci.verdict`(`passed` など判定の値そのもの)。最後のラウンドに `ci` が無ければ `null` | -| `ci_reason` | 文字列か `null` | いつも | `ci` が `unverified` なら `ci.reason`、`meta_only` なら `ci.note`、`pending` なら未完了のチェックジョブの名前を空白で連ねたもの。ほかは `null` | - -supervise の件数の行(`counts_text`)は `null` のキーを出さないため、継続的統合の判定が無い実行では件数の行は今と同じになる。判定の値の集合は `review_lib/ci.py` の `_round_ci` が持ち、この変更は写すだけで集合を変えない。 - -**supervise の止まった理由の形を足す。** 今の形は `ステップ が失敗した(exit=<ステップの exit>): <要約>` で、要約は結果 JSON の `summary` である。結果 JSON の `status` が `stopped` で `metrics.exit` が整数のとき、要約の後ろへ `(元の終了コード )` を添える。検査のプランの `abort` へ回したときの「直前のステップの要約」(#1917 の決定 7)にも同じ規則を当てる。 - -例(テストの無い宣言で今の実装が止まる場合の、直した後の理由。受け入れ条件 4 の形): - -```text -ステップ abort が失敗した(exit=1): …: review: 最終スイープの指示を組めない: .ndf/project.json の test が不正: test.suites[0].kind は test か lint で書く(今: unit)(元の終了コード 2) -``` - -## 処理の流れ - -最終スイープの指示を組むまでの流れ(I1・I2 の分岐): - -```mermaid -sequenceDiagram - participant SV as supervise DriveStep - participant DR as drive.py - participant TS as test_strategy - participant W as worker - SV->>DR: drive.py を打つ - DR->>DR: judge(CI unverified でも収束。final=approved) - DR->>DR: 段階 sweep-start → pause("sweep") - DR->>TS: verify_commands(宣言) - alt 種別・雛形の誤り(I2) - TS-->>DR: StrategyError - DR-->>SV: stopped(summary に理由・metrics.exit=2) - SV->>SV: _stop_detail が理由と元の終了コードを添える(I5) - else テストの無い宣言(I1) - TS-->>DR: None - DR-->>SV: gate sweep(今の探し方の段落) - SV->>W: 最終スイープ - W-->>SV: 結果ファイル - SV->>DR: 打ち直し → finish → verify-sweep - DR-->>SV: ok(metrics.ci=unverified・ci_reason) - SV->>SV: review 成功 → base-sync → test-all - else command を持つ suite がある - TS-->>DR: コマンドの並び - DR-->>SV: gate sweep(コマンドを名指しする段落。今のまま) - end -``` - -`verify_commands` の判定の順(上から当てる): - -| # | 宣言の `test` | 返すもの | -| --- | --- | --- | -| 1 | 無い・`unknown` を持つ | `None`(今のまま) | -| 2 | 種別か雛形に誤りがある | `StrategyError`(今のまま。I2) | -| 3 | `strategy` を書かず、どの suite も `command` と `scope_command` のどちらも持たない(suite が 0 個のときを含む。テストの無い宣言) | `None`(変える。I1) | -| 4 | 戦略を解けない(`local-scoped-ci-whole` を宣言したのに `scope_command` が無い など) | `StrategyError`(今のまま) | -| 5 | 解ける | 全体テストのコマンドの並び(今のまま。空なら `None`) | - -2 を 3 より先に当てるのは、`command` の無い suite でも種別か雛形の誤りは不正な宣言であり、黙って流さないためである(I2)。2 の検査は `resolve` が今使う `_check_kind` と `_check_templates` をそのまま使い、新しい検査を作らない。 - -## 非機能の実現方式 - -| 大項目 | 条件 | 実現方式 | -| --- | --- | --- | -| 運用・保守性 | 止まった理由は、supervise の報告の 1 行から、止めたスクリプト・終了コード・理由文が読める | 駆動の `Stop` の理由文が止めた所(「最終スイープの指示を組めない」・「state.py verify-sweep が終了コード N で止まった」など、今の `must` の文)を持ち、supervise の `_stop_detail` が元の終了コードを添える。報告の「理由」の行は 1 行のまま | -| システム環境 | 継続的統合の手段に依らず同じ雛形が通る | 雛形(`templates.py`)を変えない。継続的統合の有無で分岐する値をプランへ書かず、判定は実行の時点の照会結果を件数へ写すだけにする | - -## 決定の記録 - -### 決定 1: 止まる原因の層で直すため、検査のプランの雛形を変えず、最終スイープの検証コマンドの解き方を直す - -止めているのは、`sweep_verify_lines` が `verify_commands` の `StrategyError` を終了コード 2 の `Stop` にする経路である(`drive.py:58-83`)。継続的統合の照会は収束を妨げない(`judge.py:216-219`)。そのため、継続的統合の有無を見て雛形の `on_fail` を切り替えても、この止まりは消えない。原因は、解析が正しく測った `{"suites": []}` を不正として扱うことである。`verify_commands` が「テストの無い宣言」を `None`(名指しするコマンドが無い)として返すように直す。単独の `/ndf:cross-review` も同じ経路を通るため、雛形で直すと単独の利用で止まりが残る。 - -起票者の提案(プランを作るときに継続的統合の有無を見て `review` の `on_fail` を切り替える・`test-all` を確かめの代わりにする)は採らない。止まる原因が継続的統合ではないため直らず、雛形に継続的統合の有無の分岐を足すと Value 5 の「同じ雛形が通る」から外れる。 - -根拠: Value 6 / Value 5(MVV 版 2) - -### 決定 2: 不正な宣言を黙って流さないため、種別と雛形の検査をテストの有無の判定より先に当てる - -#1483 I13 が `StrategyError` を上げるのは、不正な宣言を最終スイープの既定の探し方へ黙って流さないためである。テストの無い宣言を `None` にするとき、`command` の無い suite に種別の誤り(`kind: unit` など)があることもある。それまで `None` になると、この決定を破る。そのため `_check_kind`・`_check_templates` を先に当て、誤りが無いときだけ「テストの無い宣言」として扱う。 - -`resolve` の「`command` を持つ suite が無い」を全体で `None` にする案は採らない。supervise の `new sprint` と cross-refactoring は全体テストを要し、そこでは今のとおり止めるのが正しい。テストの無いリポジトリでスプリントを流すときは、`--test-cmd` か `supervise.json` の `test.command` を渡す。 - -根拠: Value 6(MVV 版 2) - -### 決定 3: 継続的統合を確かめられなかったことを残すため、駆動の件数へ最後のラウンドの判定を写し、`test-all` を代わりの確かめとは書かない(未決 1) - -状態ファイルには最後のラウンドの `ci.verdict` と `reason` が既にある。駆動の `counts` がそれを `ci`・`ci_reason` として写せば、supervise は今の仕組みのまま報告へ載せられる(ステップの `counts` → 報告の件数の行)。supervise 側に新しい読み方を足さずに済む。 - -`test-all` を継続的統合の代わりの確かめとして承認資料に書く案は採らない。`test-all` は手元の全体テストで、継続的統合が走らせるチェック(別の環境・別のジョブ)と同じものを確かめたとは言えない。書くと、測っていないことを確かめたと示すことになる。承認する人は件数の `ci unverified` と理由を読み、`test-all` の結果と合わせて自分で判断する。 - -根拠: Value 3 / Vision(未検証のまま残ることを承認へ届ける)(MVV 版 2) - -### 決定 4: 止まった理由から止めたスクリプトと終了コードを読めるようにするため、supervise の `_stop_detail` で `metrics.exit` を添える - -`metrics.exit` は `lib/drive_pause.py` が全駆動に共通の意味(止まったスクリプトの元の終了コード)を定めている。添える所を supervise の 1 か所にすれば、cross-review と cross-refactoring の両方の駆動に効く。駆動ごとに理由文へ終了コードを書き足さずに済む。駆動の側は、理由文で止めた所を名指しすることだけを持つ。`must` の「<スクリプト> が終了コード N で止まった」は既にそうなっている。`sweep_verify_lines` の理由文にだけ「最終スイープの指示を組めない」を足す。 - -駆動の `summary` へ終了コードを書き足す案は採らない。駆動の結果 JSON は単独の cross-review でも読まれ、終了コードは `metrics.exit` に既にある。同じ値を 2 か所に持つことになる。 - -根拠: Value 6(同じ役割を Skill ごとに分けない)(MVV 版 2) - -### 決定 5: 宣言の不正を検査のプランの書き出しで先に見つける検査は足さない(未決 2) - -決定 1・2 の後で最終スイープが止まる宣言は、種別・雛形の誤りか戦略を解けない宣言である。supervise の `new sprint` / `new check` は、`--test-cmd` を渡さないときは同じ `resolve` で宣言を解く。これらの宣言は `DeclError` として既に書き出しで止まる(`supervise_lib/decl.py:275-279`)。`--test-cmd` を渡したときだけ書き出しを通り、最終スイープで止まる。その止まりは決定 4 で理由と終了コードが報告に出る(受け入れ条件 4)。そのため、同じ検査を書き出しへ重ねない。 - -根拠: Value 1(処理の改善・削減に寄与しない検査を足さない)/ Value 6(MVV 版 2) - -### 決定 6: 受け入れ条件 1 の再現を、手元の再現と一時リポジトリの再現の 2 段に分ける(未決 3) - -再現の 2 段は次のとおりである。 - -| 段 | 何をするか | 外部への影響 | いつ走らせるか | -| --- | --- | --- | --- | -| 手元の再現 | `.ndf/project.json` の `test` を `{"suites": []}` にした作業ディレクトリで駆動の段階 `sweep-start` を通し、`stopped`・`metrics.exit` = 2・理由文を確かめる | 無い(GitHub も外部への公開も要らない) | 実装の最初のタスクとして、直す前に | -| 一時リポジトリの再現 | 継続的統合を持たない GitHub のリポジトリに PR を開き、検査のプランを `supervise.py queue` で流す。受け入れ条件 1・2・3 の値を実機で取る | リポジトリの作成と削除が C5・C4 に当たる | 承認ゲート 1 で利用者が置き場と削除を決めたとき(要求の未決 3) | - -承認が無ければ、一時リポジトリの再現はリリース後テスト(`release-verification`)へ回す。実装では、手元の再現と単体テストで受け入れ条件 2・3・4 の振る舞いを縛る。 - -手元の再現で止まらなかった(別の所で止まる・止まらない)ときは、要求の前提 1 が崩れたものとして実装を止め、要求を書き直す(要求の前提 1 のとおり)。 - -根拠: Value 3 / C4 / C5(MVV 版 2) - -## テスト設計 - -| 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | -| --- | --- | --- | -| I1・受け入れ条件 2 | `verify_commands({"test": {"suites": []}})` と、`command` も `scope_command` も無い suite だけを持つ宣言が `None` を返す。`strategy: local-scoped-ci-whole` を書いた `suites: []` は今のとおり `StrategyError`。テストの無い宣言の作業ディレクトリで `sweep_verify_lines` が空の段落を返し、`Stop` を上げない | 判定の順の 3 を消して `resolve` へ流すと落ちる | -| I2 | `command` の無い suite に `kind: unit` を書いた宣言と、`{paths}` の誤った雛形を持つ宣言で `StrategyError` が上がり、`sweep_verify_lines` が終了コード 2 の `Stop` を上げる | 判定の順の 2 と 3 を入れ替えると落ちる | -| I3・受け入れ条件 3 | 最後のラウンドの `ci` が `unverified`(理由つき)の状態ファイルから `Drive.counts` を組むと、`ci` = `unverified`・`ci_reason` = 理由になる。`ci` の無いラウンドでは両方 `null`。supervise の報告の件数の行に `ci unverified` が出る | `counts` が最初のラウンドの `ci` を読むように壊すと落ちる。`ci_reason` を `unverified` 以外でも `null` にすると `meta_only` の行で落ちる | -| I4 | 同じ状態ファイルから組んだ `counts` の既存のキー(`review_status` ほか)の値が、変更の前後で同じ | `review_status` を `ci` で置き換えると落ちる | -| I5・受け入れ条件 4 | drive のステップの結果 JSON が `stopped`・`metrics.exit` = 2 のとき、`_stop_detail` の理由に駆動の `summary` と「元の終了コード 2」が載る。`abort` へ回したときの直前のステップの要約にも載る。`metrics.exit` が無い結果では今の形のまま | `metrics.exit` を読まないように壊すと落ちる。`status` が `ok` の結果にも添えるように壊すと落ちる | -| I6・受け入れ条件 5 | 継続的統合が `code_failure` のとき、判定は今のとおり終了コード 2 で修正へ回す(既存のテストが縛る。変えない) | 既存のテストが落ちる | -| 受け入れ条件 6 | 駆動が `stopped` を返したとき、`review` のステップは今のとおり失敗して `on_fail: abort` へ進む(既存の supervise のテストが縛る) | 既存のテストが落ちる | -| 受け入れ条件 1 | 手元の再現(決定 6)で、直す前の実装が `stopped`・`metrics.exit` = 2・理由「.ndf/project.json の test が不正: test.suites に command を持つ suite が無い」を返すことを確かめ、値を課題のコメントへ書く。一時リポジトリの再現は承認ゲート 1 の判断に従う | 手動確認。値が合わなければ前提 1 が崩れたとして止める | -| 受け入れ条件 7 | `.ndf/project.json` の `test` の suites がすべて通る | — | - -## 設計の結果 - -| 課題 | 扱い | 取り込み先 | 触るファイル | -| --- | --- | --- | --- | -| #1593 | 実装する | — | `plugins/ndf/scripts/lib/test_strategy.py`、`plugins/ndf/skills/cross-review/scripts/drive.py`、`plugins/ndf/scripts/supervise_lib/engine.py`、`plugins/ndf/scripts/supervise_lib/plan.py`、`plugins/ndf/skills/cross-review/`、`plugins/ndf/scripts/tests/` | - -## 未確認のまま残ること - -| 項目 | 内容 | -| --- | --- | -| takemi-ohama/novel の宣言 | novel の `.ndf/project.json` の `test` が `{"suites": []}` だったかは確かめられない(記録が手元に無い。要求の前提 3)。状態ファイルの 3 つの値(`final: approved`・`sweep: null`・止まりが 2)と、novel がテストも継続的統合も持たないことに合う候補として選んだ。ほかの候補(`state.py init` が `TMP_DIR` を返さない、子のスクリプトが 2 を返した)は、`sweep: null` と `final: approved` の両方には合わない(`init` はラウンドより前)か、出所の再現でしか分けられない。決定 6 の手元の再現と一時リポジトリの再現で確かめる | -| 一時リポジトリの置き場 | 作る場所と、確かめた後に消すかは利用者が承認ゲート 1 で決める(要求の未決 3。C4・C5)。決まらなければ受け入れ条件 1〜3 の実機の値はリリース後テストで取る | -| テストの無いリポジトリでの `test-all` | テストの無い宣言で `new sprint` を流すには `--test-cmd` か `supervise.json` の `test.command` が要る(決定 2)。novel がどちらで流したかは分からない。`test-all` がその雛形で通るかはこの変更の範囲外で、受け入れ条件 2 は「`test-all` まで流れる」までを確かめる | -| 最終スイープの探し方 | テストの無い宣言では、最終スイープは今の探し方(`docs/02-fix-and-rotation.md` の Step 7.5)で検証のコマンドを探す。探して見つからなければ検証を走らせない。この振る舞いは今の「宣言が無い」ときと同じで、変えない | diff --git a/issues/issue-1593-requirements.md b/issues/issue-1593-requirements.md deleted file mode 100644 index 0382b63eb..000000000 --- a/issues/issue-1593-requirements.md +++ /dev/null @@ -1,191 +0,0 @@ -# #1593: CI の無いリポジトリで、スプリントの検査のプランが cross-review の unverified(exit 2)と on_fail: abort で毎回止まる - -正は課題の本文(#1593)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。 - -## 何を見つけたか - -CI(GitHub Actions など)を持たないリポジトリでは、スプリントの検査のプランの `review`(cross-review の収束)が、確かめる CI が無いために exit 2(unverified)で返る。検査のプランの `review` は `on_fail: abort` のため、指摘が収束していてもプランがそこで止まる。続けるには `supervise.py run --from test-all` で手で再開するしか無かった。 - -## どこで見つけたか - -takemi-ohama/novel のスプリント pipeline(PR takemi-ohama/novel#35)の検査のフェーズ。このリポジトリは CI を持たず、外部のレビューの検査(Amazon Q Developer)だけがある。 - -## exit 2 の経路(記録から分かること) - -- 検査のプランの `review` は `plugins/ndf/scripts/supervise_lib/templates.py:260-269` の `drive: cross-review` で、`on_fail: abort` である -- PR takemi-ohama/novel#35 の cross-review の状態ファイル(`/work/novel/.git/ndf/worktree-trash/pr35-20261001213433/.cross_review/cross-review-pr35-state.json`): 3 ラウンドで `final: approved`(2026-10-01 23:36 JST)。最後のラウンドの CI の判定は `unverified`(「チェックジョブを照会できない(未 push・権限・チェックジョブ 0 件のいずれか)」)。`sweep` は `null` で、最終スイープの結果が記録されていない -- CI の照会の経路は exit 2 にならない。`plugins/ndf/skills/cross-review/scripts/review_lib/commands/judge.py:216-217` は CI が `unverified` のとき「⚠ 継続的統合を確かめられないまま収束する」を出し、exit 0 で返す -- 駆動の結果の `review_status` は `plugins/ndf/scripts/lib/loop_drive.py:164-176` が決める。`final: approved` でも最終スイープの `verified` が真でなければ `unverified` を返す。PR #35 は `sweep: null` のため、この経路で `unverified` になる -- exit 2 を返したスクリプトは特定できていない。novel の supervise の記録(状態ディレクトリと進行のログ)が手元に無い。cross-review の駆動(`drive.py`)自身の終了コードは 0(終わった)・1(止まった)・20〜23(LLM を待つ止まり)で、2 を返さない(`plugins/ndf/scripts/lib/drive_pause.py:38-47`) - -## 再現の手順と確かめる点 - -exit 2 の出所は novel の記録が無いため、CI の無い一時リポジトリで再現して特定する。 - -1. GitHub に CI(`.github/workflows/`)を持たない一時リポジトリを作り、Pull Request を 1 本開く -2. `supervise.py new sprint` で検査のプランを書き出し、`supervise.py queue` で流す -3. 次を確かめる - - `review` ステップの駆動(`drive.py`)の結果 JSON の `status` と、`lib/loop_drive.py` の `review_status` の値(`unverified` になるか)の関係 - - `supervise.py run` と `review` ステップの終了コード(どこで 2 になるか) - - 状態ファイルの `sweep` が `null` になる条件(最終スイープが走らないのか、走って記録されないのか) - -## なぜこの変更の範囲外なのか - -検査のプランの雛形と cross-review の終了コードの扱いの問題で、開発対象リポジトリでは直せない(MVV なし: 上流)。 - -## 直さないと何が起きるか - -CI の無いリポジトリでは、スプリントの検査が毎回 `review` で止まり、conductor がプランの外で再開を手当てする(#1426 の「プランの外の手当てをなくす」に反する)。 - -## 関連 - -- #1426 — conductor がプランの外で打つ手当てをなくす。この課題の止まり方もその手当ての 1 つ -- #1340(閉じた) — cross-review を収束・異常終了の後にもう一度回す手段(`lib/loop_drive.py`) - -## 由来 - -takemi-ohama/novel の PR #35 の振り返り。提案: プランを作るときにリポジトリの CI の有無を見て、CI が無ければ `review` の unverified を「止めない」(または `test-all` を確かめの代わりにする)扱いへ切り替える。 - -## 依頼(原文) - -> CI(GitHub Actions など)を持たないリポジトリでは、スプリントの検査のプランの `review`(cross-review の収束)が、確かめる CI が無いために exit 2(unverified)で返る。検査のプランの `review` は `on_fail: abort` のため、指摘が収束していてもプランがそこで止まる。続けるには `supervise.py run --from test-all` で手で再開するしか無かった。 - -> 提案: プランを作るときにリポジトリの CI の有無を見て、CI が無ければ `review` の unverified を「止めない」(または `test-all` を確かめの代わりにする)扱いへ切り替える。 - -(書き手: 起票者(takemi-ohama/novel の PR #35 の振り返りを記録した AI)、日付: 2026-10-01。上の節「何を見つけたか」と「由来」の文。利用者の指示の原文は無い) - -> #1593。人へ問わずに進め、決められない点は前提か未決として課題の本文へ書く。 - -(書き手: AI の起動指示、日付: 2026-10-24。進め方の指示で、要求の中身を持たない) - -## 要求を書くときに読んだコード(2026-10-24、`design/issue-1593` = `4602364f`) - -題の「unverified(exit 2)」は、今のコードの経路とは合わない。要求はこの読みを前提に書く。 - -| # | 読んだこと | 場所 | -| --- | --- | --- | -| R1 | supervise の drive のステップは、駆動の結果 JSON の `status` が `ok` のときだけ成功とする。`metrics.review_status` は成否に使わない | `plugins/ndf/scripts/supervise_lib/worker_steps.py:184-186`・`plugins/ndf/scripts/supervise_lib/plan.py:52-56` | -| R2 | cross-review の駆動の終了コードは 0・1・20〜23 だけである。止まったときは、止めたスクリプトの元の終了コードを `metrics.exit` へ写す。「exit 2」はこの `metrics.exit` の値だった見込みが高い | `plugins/ndf/scripts/lib/drive_pause.py:76-84` | -| R3 | 継続的統合の照会が `unverified` でも、判定は収束として exit 0 で返す | `plugins/ndf/skills/cross-review/scripts/review_lib/commands/judge.py:216-219` | -| R4 | 状態の `sweep` は、最終スイープの結果を `verify-sweep` が確かめた時点で書かれる。`final: approved` で `sweep: null` は、収束の後、`verify-sweep` より前で駆動が止まったことを示す | `plugins/ndf/skills/cross-review/scripts/review_lib/commands/report.py:120`・`plugins/ndf/skills/cross-review/scripts/drive.py:362-367` | -| R5 | 収束の直後に最終スイープの指示を組む `sweep_verify_lines` は、`.ndf/project.json` の `test` を解けないと終了コード 2 で止める。R2・R4 の 3 つ(`final: approved`・`sweep: null`・`metrics.exit` = 2)にすべて合う候補である。ほかに 2 で止まる所: `state.py init` が `TMP_DIR` を返さない(`drive.py:264-267`)、子のスクリプトが 2 を返した(`drive.py:111-115` の `must`) | `plugins/ndf/skills/cross-review/scripts/drive.py:58-83` | -| R6 | `review_status` の `unverified` は最終スイープが確かめられていないことを指し、継続的統合の照会の `unverified`(R3)とは別の意味である | `plugins/ndf/scripts/lib/loop_drive.py:164-176` | - -## 起票者の推定 - -| # | 条件 | 推した出所 | この要求での扱い | -| --- | --- | --- | --- | -| S1 | 止まった原因は、継続的統合が無く確かめられないこと(unverified)である | 題・「何を見つけたか」 | 前提にしない。R1〜R5 の読みでは継続的統合の照会は止める経路に無い。原因は受け入れ条件 1 で特定する | -| S2 | CI の無いリポジトリでは毎回止まる | 題 | 受け入れ条件 1 の再現で確かめる | -| S3 | プランを作るときにリポジトリの CI の有無を見て扱いを切り替える | 「由来」の提案 | 採らない(手段として)。どの手段で満たすかは設計が決める。受け入れ条件は手段を問わない | -| S4 | CI が無ければ `review` の unverified を止めない | 「由来」の提案 | 受け入れ条件 2・3(継続的統合を確かめられないことだけでは止めず、確かめられなかったことを残す) | -| S5 | CI の代わりに `test-all` を確かめにする | 「由来」の提案 | 受け入れ条件 2(`review` の後に今の並びで `test-all` が流れる)。`test-all` を継続的統合の代わりと記録に書くかは未決 1 | - -## 目的 - -- 継続的統合を持たないリポジトリでも、cross-review が収束したスプリントの検査が、conductor の手当て(`supervise.py run --from`)なしに `review` の先へ進む(#1426) -- 駆動が本当に止まるべきとき(収束しない・宣言が壊れている など)は、止まった理由が継続的統合の未検証と区別できる形で報告に出る - -## 前提 - -- 前提 1: 止める経路は cross-review の駆動が `stopped` を返したこと(R1)であり、継続的統合の照会の結果そのものではない。受け入れ条件 1 の再現でこの前提が崩れたら、要求を書き直す -- 前提 2: 検査のプランの雛形はスプリントの検査(`new sprint`)と単発の検査(`new check`)で共有している(`plugins/ndf/scripts/supervise_lib/templates.py` の `plan_check`)。直した振る舞いは両方に効く -- 前提 3: takemi-ohama/novel の supervise の記録は手元に無い(`/work/novel` が無いことを 2026-10-24 に確かめた)。原因の特定は一時リポジトリでの再現で行う -- 前提 4: 継続的統合の有無は、PR の head のチェックジョブが 0 件であることで観測する(R3 の `unverified` の理由の 1 つ) - -## 対象範囲 - -含む: -- CI の無いリポジトリでの、スプリントの検査と単発の検査の `review` のステップの止まり方 -- 止めたスクリプトと終了コード 2 の出所の特定(再現) -- 止まったときの報告の理由の出し方(どのスクリプトが何で止めたか) -- 継続的統合を確かめられなかったことの記録 - -含まない: -- cross-review の収束の判定そのもの(ラウンド・承認の数え方) -- 継続的統合があるリポジトリでの、チェックジョブの失敗の扱い(今のとおり修正へ回す) -- `review_status` と継続的統合の照会の `unverified` の改名(R6。語の整理は別の課題にする) -- cross-refactoring の最終ゲートの判定(`review_status` を finalize へ渡す経路) -- takemi-ohama/novel 側の設定の修正 - -## ドメインイベント - -| # | イベント | 引き金 | 失敗したとき | 順序の前提 | -| --- | --- | --- | --- | --- | -| E1 | 検査のプランを書き出した | `supervise.py new sprint` / `new check` | 宣言が無い・壊れていると止まる(今のまま) | — | -| E2 | `review` のステップが cross-review の駆動を起動した | プランの `refactor` の後 | 駆動の結果 JSON を読めない → ステップの失敗 | E1 | -| E3 | 判定が継続的統合を照会し、`unverified` を記録した | 収束したラウンドの判定 | 照会の失敗も `unverified` として収束する(R3) | E2 | -| E4 | 状態に `final: approved` を書いた | E3 | — | E3 | -| E5 | 最終スイープの指示を組んだ | 段階 `sweep-start` | 宣言の `test` を解けない → 終了コード 2 で止まる(R5)。受け入れ条件 1・4 | E4 | -| E6 | worker が最終スイープの結果ファイルを書いた | E5 の止まりを supervise が worker へ渡す | 書かない → ステップの失敗 | E5 | -| E7 | `verify-sweep` が状態の `sweep` を書いた | E6 | 0・6 以外 → 止まる | E6 | -| E8 | 駆動が `ok` を返した | E7 | `stopped` → `on_fail: abort` | E7 | -| E9 | 継続的統合を確かめられなかったことが報告に載った | E8 | 載らない → 受け入れ条件 3 を満たさない | E3・E8 | -| E10 | `base-sync`・`test-all` が流れた | `review` の `next` | 今のまま | E8 | - -## 受け入れ条件 - -- [ ] 1. 前提: GitHub に `.github/workflows/` を持たず、宣言(`.ndf/`)が正しい一時リポジトリと、Pull Request が 1 本ある - 操作: `supervise.py new sprint`(または `new check`)で検査のプランを書き出し、`supervise.py queue` で流す - 結果: 次の値を課題のコメントか設計文書に書く。`review` の駆動の結果 JSON の `status`・`summary`・`metrics.exit`・`metrics.review_status`、`supervise.py run` の終了コード、状態ファイルの `final`・`sweep`・最後のラウンドの `ci.verdict`。止まったなら、止めた箇所をファイルと行で書く。止まらなかったなら、止まらなかった事実と試した条件を書く -- [ ] 2. 前提: 受け入れ条件 1 の一時リポジトリで、cross-review が `final: approved` で収束し、最終スイープの未解決が 0 件である - 操作: `supervise.py queue` で検査のプランを流す - 結果: `review` のステップが成功として `base-sync` へ進み、`test-all` まで流れる。`supervise.py run --from` を打たない -- [ ] 3. 受け入れ条件 2 の実行で、継続的統合を確かめられなかったこと(判定 `unverified` と理由)が、`review` のステップの件数(`progress.jsonl` の行か supervise の報告)に載る -- [ ] 4. 前提: 宣言の `test` を解けない(R5)など、駆動が止めるべき理由がある - 操作: 検査のプランを流す - 結果: `review` のステップが今のとおり止まり、supervise の報告の理由に、止めたスクリプトの理由文と元の終了コードが載る。理由文から継続的統合の未検証と区別できる -- [ ] 5. 退行しない: 継続的統合があり、チェックジョブがコードの失敗(`code_failure`)のときは、今のとおり判定が修正へ回す(`judge.py` の終了コード 2) -- [ ] 6. 退行しない: cross-review が収束しない(`final` が `approved` 以外)か駆動が `stopped` を返したときは、`review` の `on_fail: abort` のとおり止まる。ただし受け入れ条件 2 で止まらなくなった経路は除く -- [ ] 7. 既存のテスト(`.ndf/project.json` の `test` の suites)がすべて通る - -## 非機能の条件 - -| 大項目 | 条件 | -| --- | --- | -| 運用・保守性 | 止まった理由は、supervise の報告の 1 行から、止めたスクリプト・終了コード・理由文が読める | -| システム環境 | 継続的統合の手段(GitHub Actions の有無)に依らず同じ雛形が通る(Value 5) | - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| 公開インタフェース | 結果 JSON の `metrics` へキーを足す見込みがある(足すだけで、既存のキーの意味を変えない)。プランの雛形の JSON の形は変わる見込みがある(互換あり。既存のプランの読み方は変えない) | -| データ | 変わらない | -| 既存の振る舞い | CI の無いリポジトリで、`review` の止まり方と報告の理由 | - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| テスト | `.ndf/project.json` の `test` の suites(`uv run --frozen --project . --all-extras pytest . -q -n 4` ほか) | -| 静的解析・型検査 | `python3 scripts/check-script-structure.py`・`ruff`(`.ndf/supervise.json` の `format`) | -| 手動確認 | 受け入れ条件 1・2・3: CI の無い一時リポジトリで検査のプランを `supervise.py queue` で流し、報告を見る。リリース後テストで確かめる | - -## 前提とする取り決め - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | `AGENTS.md`・`plugins/ndf/scripts/lib/README.md`。直す場所は止める原因の層(Value 6)。雛形で個別の値を埋め込まない(Value 5) | -| コーディング規約 | `AGENTS.md` の「最小限のコード実装」。`scripts/check-script-structure.py` の行数の上限 | -| テスト戦略 | 単体: 駆動の止まりと supervise の drive のステップの成否・報告の理由。結合: 再現は手動確認(一時リポジトリを GitHub に作るため) | - -## 境界 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 既存テストの実行、整形の適用 | -| 確認してから行う | 結果 JSON の契約・プランの雛形の形の変更(承認ゲート 1・2 で人が見る) | -| 行わない | 継続的統合のあるリポジトリでの判定の緩和、takemi-ohama/novel への書き込み、cross-review の収束の判定の変更 | - -## 未決 - -| 項目 | 誰が決めるか | 期限 | -| --- | --- | --- | -| 1. 継続的統合が無いとき、`test-all` を継続的統合の代わりの確かめとして承認資料に書くか(S5) | 設計(承認ゲート 1 で利用者) | 設計 PR | -| 2. 受け入れ条件 1 で止めた原因が宣言の不正(R5)だったとき、宣言の不正を検査のプランの書き出し(E1)で先に見つけるか | 設計 | 設計 PR | -| 3. 一時リポジトリを GitHub のどこに作り、確かめた後に消すか(作る・消すは他のリポジトリへの公開とデータの削除に当たる。C4・C5) | 利用者(承認ゲート 1) | 再現の前 | - -## MVV との突き合わせ(2026-10-24) - -反する疑いは無い。進める。根拠: Value 4(conductor のプランの外の手当てを雛形の側でなくす)・Value 5(継続的統合の手段に依らず同じ雛形が通る)・Value 6(止める原因の層で直す。受け入れ条件 1 で原因を特定してから直す)・Vision(未検証のまま残ることを承認へ届ける。受け入れ条件 3)。継続的統合のあるリポジトリの判定は緩めない(受け入れ条件 5)。一時リポジトリの作成と削除は C4・C5 に当たるため未決 3 で利用者へ残す。 From 0e7881ddf31047b8ca4cde92e2e6c9e55c6b6c88 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sun, 25 Oct 2026 07:27:55 +0000 Subject: [PATCH 05/17] =?UTF-8?q?Docs:=20=E7=A2=BA=E5=AE=9A=E4=BB=95?= =?UTF-8?q?=E6=A7=98=E3=81=AE=E7=B4=A2=E5=BC=95=E3=81=A8=E9=96=A2=E9=80=A3?= =?UTF-8?q?=E3=83=AA=E3=83=B3=E3=82=AF=E3=82=92=20#1529=E3=83=BB#1914?= =?UTF-8?q?=E3=83=BB#1630=20=E3=81=AE=E8=BF=BD=E8=A8=98=E3=81=AB=E5=90=88?= =?UTF-8?q?=E3=82=8F=E3=81=9B=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01D6yDhQP9E5vZSDFNXDauo4 --- docs/specifications/README.md | 4 ++-- .../cross-refactoring-init-test-and-phase-windows.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/specifications/README.md b/docs/specifications/README.md index e854f72a0..4174c8561 100644 --- a/docs/specifications/README.md +++ b/docs/specifications/README.md @@ -53,8 +53,8 @@ | [project-decl-ci-representative-run.md](project-decl-ci-representative-run.md) | 宣言の解析が CI の所要に採る代表の run(重いジョブを飛ばした run を採らず、テストを走らせた run から CI 待ちとテストの上限を出すこと・解析器の版 2 と再解析)、常に成り立つ条件と部品の契約、決定の理由。手順は `development-workflow` の `references/project-analysis.md` が正 | | [cross-assignee-reassignment-on-no-result.md](cross-assignee-reassignment-on-no-result.md) | cross-review / cross-refactoring で結果を残さなかった担当を、利用上限か 2 度目の結果なしで別の claude のアカウントか残りの参加者へ振り替えること(`assignment`・`assignee_env`・`reassign`・`assignee_launch`)、常に成り立つ条件と部品の契約、決定の理由。規則の表と手順は `cross-refactoring`・`cross-review` の SKILL.md と `docs/`、`plugins/ndf/scripts/lib/README.md` が正 | | [ndf-google-workspace-gws.md](ndf-google-workspace-gws.md) | Google Workspace の操作の gws への一本化(`gws-check.py` の 4 つの状態と終了コード・失敗の形・導入の承認資料、常に成り立つ条件、`google-auth` / `google-drive` と playwright-kit の Drive への保管の消えた約束、決定の理由)。手順と操作の対応表は `google-workspace` の SKILL.md が正 | -| [ndf-release-approved-commit.md](ndf-release-approved-commit.md) | 本番への配布の承認したコミット(承認資料に控えるベースブランチの先端・承認の記録・本番チャネルへ入れる直前の比較の順序・承認ゲートで止まる時点と結果 JSON・`merge-when-green --expect-head`)。手順は `release` の SKILL.md と `references/release-steps.md` が正 | -| [ndf-production-release-record.md](ndf-production-release-record.md) | 本番のリリースプランが本番の配布の後に本番のリリースの PR へ書くリリース記録(`release-steps.py record` の前提・結果 JSON・終了コード、記録の値の決め方)、常に成り立つ条件、記録の失敗を `judge-record` へ回し配布へ戻らない流れ、報告の Pull Request を埋める `pr_from`、決定の理由。記録の形とスプリントを閉じる手順は `progress-tracking` の SKILL.md、ステップの並びは `release` の `references/form-package-plugin.md` が正 | +| [ndf-release-approved-commit.md](ndf-release-approved-commit.md) | 本番への配布の承認したコミット(承認資料に控えるベースブランチの先端・承認の記録・本番チャネルへ入れる直前の比較の順序・承認ゲートで止まる時点と結果 JSON・`merge-when-green --expect-head`)と、`pace: fast` / `auto` の昇格が導入の確認の確かめたコミットのときだけ本番チャネルへマージし違えば承認ゲート 2 へ戻ること(`merged-steps.py promote --expect-head-from`)。手順は `release` の SKILL.md と `references/release-steps.md` が正 | +| [ndf-production-release-record.md](ndf-production-release-record.md) | 本番のリリースプランが本番の配布の後に本番のリリースの PR へ書くリリース記録(`release-steps.py record` の前提・結果 JSON・終了コード、記録の値の決め方)、常に成り立つ条件、記録の失敗を `judge-record` へ回し配布へ戻らない流れ、報告の Pull Request を埋める `pr_from`、`pace: normal` の `new sprint` が承認ゲート 2 と本番のステージまで書き出すこと、決定の理由。記録の形とスプリントを閉じる手順は `progress-tracking` の SKILL.md、ステップの並びは `release` の `references/form-package-plugin.md` が正 | | [ndf-release-other-plugin-levels-and-migration.md](ndf-release-other-plugin-levels-and-migration.md) | 他のプラグインの上げ幅の候補(互換なしの印と移行の手順から MAJOR / PATCH を字面で決める規則)、承認資料の「版を上げる他のプラグイン」の表と `--decided` の確かめ(常に成り立つ条件・途中からの再開)、PR 本文の「移行の手順」の差し替えと CHANGELOG の `### 移行の手順` への写し、決定の理由。手順は `release` の `references/form-package-plugin.md`・`references/release-steps.md` と `pr` の SKILL.md が正 | | [ci-required-checks-reconcile.md](ci-required-checks-reconcile.md) | ワークフローのジョブと ruleset の必須のチェックの突き合わせ(`scripts/check-required-checks.py` の入出力・終了コード 0/1/2・差の種類、チェックの名前を matrix から決める規則、宣言 `scripts/required-checks-allow.json` の形、共有の関数 `ci_workflows.required_contexts`)、常に成り立つ条件、承認ゲート 2 で ruleset へ足す手順、決定の理由。承認資料の形は `development-workflow` の `references/approval-request.md` が正 | | [ndf-sprint-acceptance-check-and-prepush-fix.md](ndf-sprint-acceptance-check-and-prepush-fix.md) | `supervise.py new sprint` が設計の無い課題の本文を計画を書く前に確かめ、受け入れ条件が無い・空・本文を取れない課題で `stopped`(終了コード 1)で止まること(`items` の `issue`・`reason`・`gh`、`next` の次の手)、実装のプランの `pr` のステップが push 前の検査の不合格だけを `on_fail_only: push-check` で `fix-push` へ回し、同じコミットで 2 回続けて落ちたら止まること、常に成り立つ条件、決定の理由。ステップのキーの書き方は `supervise_lib/plan.py`、ステージは `development-workflow` の `references/agent-layers.md`、終了コードの表は `plugins/ndf/scripts/lib/README.md` が正 | diff --git a/docs/specifications/cross-refactoring-init-test-and-phase-windows.md b/docs/specifications/cross-refactoring-init-test-and-phase-windows.md index 3757bcdcc..bbf123686 100644 --- a/docs/specifications/cross-refactoring-init-test-and-phase-windows.md +++ b/docs/specifications/cross-refactoring-init-test-and-phase-windows.md @@ -269,3 +269,4 @@ init が終了コード 4 で止まった。同じ範囲の実測は 320 秒(6 - [cross-refactoring-time-budget.md](cross-refactoring-time-budget.md)(想定最大時間と配分テーブル) - 課題: #1385(取り込んだ課題 #1555。関連 #968・#1522・#1689)・Pull Request #1729 - [cross-drive-rerun-pause-and-denials.md](cross-drive-rerun-pause-and-denials.md)(計画の前の中断で起点にずらした止まっていた時間を足すこと。#1944) +- [cross-refactoring-scope-template-probe.md](cross-refactoring-scope-template-probe.md)(着手前のテストの後、上限の残りの中で範囲テストのテンプレートを試走すること。#1630) From da0eb4023485ae0ecef2f95eb602dc05b6c3b371 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sun, 25 Oct 2026 10:26:52 +0000 Subject: [PATCH 06/17] =?UTF-8?q?Docs:=20#2104=20=E3=81=AE=E8=A6=81?= =?UTF-8?q?=E6=B1=82=E3=81=A8=E5=8F=97=E3=81=91=E5=85=A5=E3=82=8C=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6=E3=81=AE=E5=86=99=E3=81=97=E3=82=92=E7=BD=AE=E3=81=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pace: normal の昇格で、承認ゲート 2 の承認資料に載せた先端の SHA と promote-approved がマージする先端を照合する要求を、課題の本文から写した。 Co-Authored-By: Claude Opus 5.5 --- issues/issue-2104-requirements.md | 160 ++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 issues/issue-2104-requirements.md diff --git a/issues/issue-2104-requirements.md b/issues/issue-2104-requirements.md new file mode 100644 index 000000000..ad8bce1ec --- /dev/null +++ b/issues/issue-2104-requirements.md @@ -0,0 +1,160 @@ +# #2104: pace: normal の昇格で、承認ゲート 2 で見た先端と promote-approved がマージする先端を照合しない + +正は課題の本文(#2104)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。 + +## 何を見つけたか + +`pace: normal` の昇格の経路では、利用者が承認ゲート 2 で見た先端と、承認の後に `supervise.py run <昇格のプラン> --from promote-approved` がマージする先端を照合しない。承認の後にベースブランチが進むと(別のスプリントのマージなど)、利用者が見ていないコミットが本番チャネルへ入る。#1529 は同じ食い違いを `fast` / `auto`(MVV 判定の経路)について、導入の確認の承認資料の SHA と `--expect-head` で照合して直すが、`normal` の経路には照合の材料(承認したときの先端を残す記録)が無い。 + +## どこで見つけたか + +#1529 の設計 PR #2094(`issues/issue-1529-design.md` の「未確認のまま残ること」の 1 行目、`issues/issue-1529-requirements.md` の未決 1)。承認ゲート 1 で利用者が「別の課題で起票する」と決めた。 + +## なぜこの変更の範囲外なのか + +#1529 の受け入れ条件は `fast` / `auto` の昇格のプランの照合だけを求める。`normal` には照合の材料を残す仕組みから足す必要があり、設計の対象が違う。根拠: Value 1 / Value 3(MVV 版 2) + +## 直さないと何が起きるか + +`delivery` で経路 `promote` を組むリポジトリの `pace: normal` で、承認ゲート 2 で利用者が承認した中身と、本番チャネルへ入る中身が食い違いうる。本番の配布(`release --channel prod`)は #815 で承認したコミットと照合しており、昇格の経路だけが照合を持たない。 + +## 起票者の推定 + +- 照合の材料は「承認ゲート 2 の時点の先端の SHA を承認の記録に残す」形になる(推した出所: #1529 の設計の決定 2、#815 の `lib/approved_commit.py`) +- 食い違えば承認ゲート 2 からやり直す(推した出所: #1454 の手動反映の本番系の手順、#1529 の決定 5) + +## 由来 + +PR #2094 + +--- + +# 要求と受け入れ条件 + +## 依頼(原文) + +> `pace: normal` の昇格の経路では、利用者が承認ゲート 2 で見た先端と、承認の後に `supervise.py run <昇格のプラン> --from promote-approved` がマージする先端を照合しない。承認の後にベースブランチが進むと(別のスプリントのマージなど)、利用者が見ていないコミットが本番チャネルへ入る。 + +(書き手: 起票者(#1529 の設計 PR #2094 から起票した AI)、日付: 2026-10-25。利用者の指示の原文として扱わない) + +> 承認ゲート 1 で利用者が「別の課題で起票する」と決めた。 + +(書き手: 起票者が利用者の決定を写した文、日付: 2026-10-25。利用者が決めたのは「#1529 の範囲外とし、別の課題にする」ことだけで、直し方の形は決めていない) + +> 人へ問わずに進め、決められない点は前提か未決として課題の本文へ書く。 + +(書き手: AI の起動指示、日付: 2026-10-25) + +## 起票者の推定 + +| # | 条件 | 推した出所 | この要求での扱い | +| --- | --- | --- | --- | +| S1 | 照合の材料は「承認ゲート 2 の時点の先端の SHA を承認の記録に残す」形になる | #1529 の設計の決定 2、#815 の `lib/approved_commit.py` | 一部を採る。先端の SHA を承認ゲート 2 の承認資料へ載せることは受け入れ条件 1。#815 の「承認の記録」の行(`approve` が書く行)まで求めるかは未決 1 | +| S2 | 食い違えば承認ゲート 2 からやり直す | #1454 の手動反映の本番系の手順、#1529 の決定 5 | 受け入れ条件 3・4 | + +## 目的 + +- 昇格の経路で本番チャネルへ入るのは、利用者が承認ゲート 2 で承認資料に見た昇格の Pull Request の先端(承認したコミット)だけにする。承認の後にベースブランチが進んでいれば、マージせずに承認ゲート 2 へ戻る +- 本番の配布(`release --channel prod`。#815)と `fast` / `auto` の `promote`(#1529)が既に持つ「承認した先端とマージする先端の照合」を、昇格の承認の後のマージ(`promote-approved`)にも持たせ、照合の無い本番チャネルへの経路を無くす + +## 前提 + +- 前提 1: 照合の基準にする SHA は、`normal` の `promote` ステップが承認ゲート 2 で止まるときに読んだ昇格の Pull Request の先端(`headRefOid`)である。止まる時点は今どおり CI を待つ前で、CI の結果は `promote-approved` が今どおり待つ +- 前提 2: `promote-approved` のコマンドは今 `normal` と `fast` / `auto` で同じ(`test_supervise_pace_verify.py` が等しいことを確かめている)。照合は両方の `promote-approved` に効かせる。`fast` / `auto` で `handoff` の後に `--from promote-approved` で続けるときは、`prepare` が書いた昇格の承認資料(`{state_dir}/work/approval-promote.md`。承認ゲート 2 で利用者が見るもの)の先端と照合する +- 前提 3: `promote-approved` が読む承認資料のパスはプランが決めて渡す。利用者も conductor もパスを手で渡さない +- 前提 4: `fast` / `auto` の `promote` ステップ(`--expect-head-from approval-verify.md --gate-approved mvv`。#1529 の P1〜P4)は変えない + +## 対象範囲 + +含む: +- `normal` の `promote` ステップが承認ゲート 2 で止まるときに、昇格の Pull Request の先端の SHA を承認資料へ載せること +- `fast` / `auto` の `prepare` が書く昇格の承認資料にも同じ SHA の行を載せること(前提 2) +- `promote-approved` が承認資料の SHA と昇格の Pull Request の先端を照合し、違えばマージせずに止まること、資料を読めなければ照合を飛ばさずに止まること +- 止まった後の続け方(承認ゲート 2 からやり直す)を、結果の `next` と `development-workflow` の `references/pace.md` の承認ゲート 2 の行に書くこと +- 確定仕様 `docs/specifications/ndf-release-approved-commit.md` の P5(`normal` の `promote` と `promote-approved` は `--expect-head-from` を持たない)と「含まない」の `pace: normal` の行を、この変更に合わせて書き直すこと + +含まない: +- `release --channel prod`(#815)と `fast` / `auto` の `promote` ステップ(#1529)の照合の変更 +- 昇格でない本番チャネルへのマージ(経路 `merge` で宛先が自動反映の本番チャネルのとき、`merge-when-green` が承認ゲート 2 で止まり `--from merge-approved` で続ける経路)の照合。同じ形の欠けがありうるが、この課題の対象(昇格の経路)と違う。別の課題の候補として conductor へ返す +- 手動反映の本番系(`gate-2` のプランと手で行う「本番」のステージ。#1454) +- 承認ゲートの数と置き場所の変更(承認の工程を足さない) + +## ドメインイベント + +| # | イベント | 引き金 | 失敗したとき | 順序の前提 | +| --- | --- | --- | --- | --- | +| E1 | `normal` の `promote` が昇格の Pull Request を見つけた(無ければ作った) | 昇格のプランの `promote` ステップ | `gh` が失敗すれば stopped。昇格する変更が無ければ ok で終える(今どおり) | — | +| E2 | `promote` が昇格の Pull Request の先端 X を読み、承認資料へ X を載せて承認ゲート 2 で止まった | E1 の後の最初の読み | 資料を書けなければマージせずに止まる(受け入れ条件 1 の退行が無いこと) | E1 | +| E3 | 利用者が承認資料を見て承認ゲート 2 を承認した | 人 | 差し戻しなら続きは流さない(今どおり) | E2 | +| E4 | ベースブランチが X から Y へ進んだ | プランの外のマージ | — | E2 の後のどの時点でも起きうる(E3 の前でも後でも同じ扱い) | +| E5 | `promote-approved` が承認資料から X を読んだ | conductor の `run <プラン> --from promote-approved` | 資料が無い・行が無い・形が違えば、`gh` を呼ばずに止まる(受け入れ条件 4) | E3 | +| E6 | `promote-approved` が昇格の Pull Request の先端を読み、X と比べた | E5 の後、CI を待つ間の読みのたび | 先端が X でなければマージせずに止まる(受け入れ条件 3) | E5 | +| E7 | X で CI が通り、`--match-head-commit X` で本番チャネルへマージした | E6 で一致し CI が緑 | マージが拒まれれば stopped(今どおり) | E6 | +| E8 | 照合で止まった後、`--from promote` から流し直し、新しい先端 Y で承認ゲート 2 に止まった | conductor(結果の `next` に従う) | E2 と同じ | E5 か E6 の失敗 | + +## 受け入れ条件 + +- [ ] 1. `pace: normal` の昇格のプランの `promote` ステップが承認ゲート 2 で止まる(終了コード 10・status `gate`)とき、書いた承認資料に、止まる時点で読んだ昇格の Pull Request の先端の 40 桁の SHA が 1 行で載り、結果の `metrics` か `items` にも同じ SHA が載る +- [ ] 2. 前提: 承認資料の SHA が X で、昇格の Pull Request の先端が X のまま CI が通る + 操作: `run <昇格のプラン> --from promote-approved` + 結果: `gh pr merge` が `--match-head-commit X` つきで 1 度呼ばれ、本番チャネルへマージされ、続く `record` へ進む(今の成功の経路が変わらない) +- [ ] 3. 前提: 承認資料の SHA が X で、昇格の Pull Request の先端が Y(X と違う)。承認の前に進んだときも、`promote-approved` が CI を待つ間に進んだときも同じ + 操作: `run <昇格のプラン> --from promote-approved` + 結果: `gh pr merge` を呼ばず、`items[].result: head_moved`(`head` と `expected` つき)で非 0 で止まり、結果の `next` が承認ゲート 2 からやり直す打ち方(`--from promote` から流し直す)を示す +- [ ] 4. 前提: `promote-approved` に渡す承認資料が無い、SHA の行が無い、または行が 40 桁の 16 進でない + 操作: `run <昇格のプラン> --from promote-approved` + 結果: `gh` を 1 度も呼ばずに終了コード 2(`EXIT_UNREADABLE`)で止まる(照合を飛ばしてマージしない) +- [ ] 5. `promote-approved` のコマンドに、照合に使う承認資料のパスがプランの組み立てで入る(`normal` と `fast` / `auto` の両方。前提 2・3)。プランの組み立てのテストで確かめる +- [ ] 6. `fast` / `auto` の `prepare` が書く昇格の承認資料に、受け入れ条件 1 と同じ形の SHA の行が載る +- [ ] 7. `fast` / `auto` の `promote` ステップのコマンドと振る舞い(`--expect-head-from approval-verify.md --gate-approved mvv`・失敗で `handoff`)が変わらない。#1529 の既存テストがすべて通る +- [ ] 8. SHA の行を書く形と読む形は 1 か所(`lib/approved_commit.py` の関数)が決め、書き手と読み手がそれを共有する +- [ ] 9. `development-workflow` の `references/pace.md` の承認ゲート 2 の行と、確定仕様 `docs/specifications/ndf-release-approved-commit.md` から「`promote-approved` は照合を持たず、今の先端をマージする」旨の記述が無くなり、照合で止まったときの続け方が書かれる +- [ ] 10. 全体テスト(下の検証手段)が通る + +## 非機能の条件 + +| 大項目 | 条件 | +| --- | --- | +| セキュリティ | 本番チャネルへ入るのは、承認資料に載った SHA と等しい先端だけである(受け入れ条件 3・4)。資料を読めないときに照合を飛ばす経路を持たない | +| 運用・保守性 | 照合で止まった結果は、どの SHA を期待し今どの SHA かを `items` に残し、続け方を `next` に書く。conductor がプランの外で手当てをしなくても承認ゲート 2 へ戻れる | + +## 影響 + +| 対象 | 影響 | +| --- | --- | +| 公開インタフェース | `merged-steps.py promote` の承認ゲート 2 の結果に SHA が増える(追加のみ)。`promote-approved` のコマンドに照合の引数が増える(プランが組み立てるため、利用者の打ち方は変わらない)。既に書き出したプラン(引数の無い `promote-approved`)は今どおり照合なしで動くか、止まるかは未決 2 | +| データ | 承認資料(Markdown)に SHA の行が 1 行増える。スキーマの移行は無い | +| 既存の振る舞い | 承認の後にベースブランチが進んだとき、`promote-approved` がマージせずに止まるようになる(今はその先端をマージする) | + +## 検証手段 + +| 項目 | 手段 | +| --- | --- | +| テスト | `uv run --frozen --project . --all-extras pytest . -q -n 4` | +| 静的解析・型検査 | `uv run --frozen --project . --group lint ruff check .` | +| 文書の検査 | `python3 plugins/ndf/scripts/instructions-check.py --root .`、`python3 scripts/check-skill-frontmatter.py` | +| 手動確認 | 昇格の経路を組むリポジトリで `pace: normal` の承認ゲート 2 を 1 度通し、承認資料に先端の SHA が載ることと、承認の後にマージされた先端がその SHA であることを見る(リリース後テストで確かめる) | + +## 前提とする取り決め + +| 項目 | 参照先 / 決めたこと | +| --- | --- | +| プロジェクト構造 | `plugins/ndf/scripts/` の既存のモジュール(`merged_lib/`・`supervise_lib/`・`lib/approved_commit.py`)に置く。実験版の置き場(`scripts/experimental/`)には置かない(既定で働く承認ゲートの振る舞いのため安定版。`AGENTS.md` の「安定版と実験版」) | +| コーディング規約 | `AGENTS.md` の「最小限のコード実装」。同じ役割の関数を分けない(MVV Value 6) | +| テスト戦略 | `merged-steps.py promote` の単体テスト(`gh` を偽物にして呼んだ回数と引数を数える)とプランの組み立てのテストで受け入れ条件 1〜8 を担保する。`.md` の文言を照合するテストは書かない(`AGENTS.md`) | + +## 境界 + +| 区分 | 内容 | +| --- | --- | +| 常に行う | 既存テストの実行、#1529 と #815 のテストを通したままにすること | +| 確認してから行う | `merged-steps.py` の引数の追加と承認資料の行の追加(承認ゲート 1・2 で人が見る) | +| 行わない | 承認ゲートの追加、`release --channel prod` と `fast` / `auto` の `promote` の変更、経路 `merge` の `merge-approved` の変更 | + +## 未決 + +| 項目 | 誰が決めるか | 期限 | +| --- | --- | --- | +| 1. 照合の基準を承認資料の SHA の行だけにするか、#815 の「承認の記録」の行(`approve --by user` が書く)がそろうことまで求めるか(S1)。後者は承認の後に conductor の操作が 1 つ増える | `design`(承認ゲート 1 で利用者が確かめる) | 設計 PR | +| 2. この変更の前に書き出したプランの `promote-approved`(照合の引数を持たない)を、今どおり照合なしで通すか、照合の材料が無いとして止めるか | `design` | 設計 PR | +| 3. 経路 `merge` の `merge-approved`(昇格でない本番チャネルへのマージ)にも同じ欠けがあるかの確認と起票 | conductor | この課題の設計 PR まで | From c6aad7df020d86360827b87558843043c3924a44 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sun, 25 Oct 2026 10:27:32 +0000 Subject: [PATCH 07/17] =?UTF-8?q?Docs:=20#2087=20=E3=81=AE=E8=A6=81?= =?UTF-8?q?=E6=B1=82=E3=81=A8=E5=8F=97=E3=81=91=E5=85=A5=E3=82=8C=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6=E3=81=AE=E5=86=99=E3=81=97=E3=82=92=E7=BD=AE=E3=81=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agy 1.4 系がエージェント定義 8 本の model(sonnet / opus)を拒む件の要求。 agy に配る定義の形と model の扱いは未決として設計へ渡す。 Co-Authored-By: Claude Opus 5.5 --- issues/issue-2087-requirements.md | 164 ++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 issues/issue-2087-requirements.md diff --git a/issues/issue-2087-requirements.md b/issues/issue-2087-requirements.md new file mode 100644 index 000000000..6f66a4a44 --- /dev/null +++ b/issues/issue-2087-requirements.md @@ -0,0 +1,164 @@ +# #2087: agy 1.4.2 が NDF のエージェント定義 8 本の model(sonnet / opus)を拒み、agy の利用者に読み込まれず pre-push も落ちる + +正は課題の本文(#2087)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。 + +## 何を見つけたか + +agy CLI 1.4.2(手元の `~/.local/bin/agy`、2026-10-19 08:01 更新)の `agy plugin validate plugins/ndf/dev.agy` が、エージェント定義 8 本の `model: sonnet` / `model: opus` を受け付けず、exit 1 で落ちる。 + +```text +✗ agents/code-reviewer.md: model output error: invalid tool call error (invalid_args) Invalid model tier sonnet. The loader skips this agent +...(corder・data-analyst・debugger・devops-engineer・director(opus)・qa・researcher の計 8 本) +Error: validation failed: 8 errors, 5 warnings +``` + +- **利用者側の影響**: 出力のとおりなら、agy 1.4.2 の利用者には 8 本のエージェントが読み込まれない(`The loader skips this agent`) +- **開発側の影響**: `.githooks/pre-push` が `scripts/validate-runtime-plugins.sh` を通すため、agy の入った環境ではどのブランチも push できない(develop の `/work/ai-plugins/plugins/ndf/dev.agy` でも同じく落ちる) +- CI(`runtime-plugin-validate.yml`)は agy を入れないため、この検査を飛ばして成功している + +## 再現の確認(2026-10-25、develop 6a86aa5ab) + +- `agy --version` → `1.4.2`(`~/.local/bin/agy`) +- 回避の包みを `PATH` に足さずに `bash scripts/validate-runtime-plugins.sh`(pre-push と同じ検査)→ exit 1。`agy plugin validate plugins/ndf/dev.agy` の段で上と同じ 8 本が `Invalid model tier sonnet` / `opus` で落ち、`agents 3`(`model` を持たない定義だけ)が読み込まれる +- `plugins/ndf/dev.agy/agents` は `../agents` への symlink で、Claude Code の定義と同じファイルを読む。そのため `model` の値を書き換えると Claude Code 側が変わる +- agy の changelog は、エージェント定義の `model` を `flash` / `pro` などの tier 名(既定 `inherit`)と書く。`sonnet` / `opus` は tier に無い +- 手元では、失敗が `Invalid model tier` だけのときに通す包み(`~/.local/state/ndf/sv/s2086/bin/agy`)を `PATH` の先頭に置いて push している。包みは手元の回避で、利用者の側の「読み込まれない」は解消しない + +## どこで見つけたか + +2026-10-24、スプリント s2086 の設計のプランの `pr` ステップ(`git push` の pre-push)。記録は `~/.local/state/ndf/sv/s2086/1-design-2086-state/05-pr.out`。 + +## なぜこの変更の範囲外なのか + +#2086 は `document-restructuring` と文書系 Skill の本文を変える。エージェント定義(`plugins/ndf/agents/*.md`)と agy の版の追従は受け入れ条件に無い。 + +## 直さないと何が起きるか + +- agy の利用者がエージェントを使えない +- 開発者は push のたびに検査を手で回して `--no-verify` にする必要がある + + +## 起票者の推定 + +題・本文のうち、利用者の指示の原文に無く起票者が推して足した条件。承認ゲート 1 で利用者が確かめるまで、利用者の決定として扱わない。 + +| # | 条件 | 推した出所 | この要求での扱い | +| --- | --- | --- | --- | +| S1 | `dev.agy` の agents を symlink でなく生成物にし、`model` を agy の tier 名(`flash` / `pro` / `inherit`)へ変換するか外す | 上の再現の確認と agy の changelog | 受け入れ条件 1〜6 は形を問わない形で書いた。生成物にするか・変換か外すかは未決 1(設計で決める) | +| S2 | CI でも agy の validate を通すか、agy の版を固定するかを決める | 本文の「CI は agy を入れない」 | 含まない(下の「調べて分かったこと」のとおり、両方とも既にある)。PR の時点で agy なしに落とす検査は受け入れ条件 7 | + +## 依頼(原文) + +> #2087。人へ問わずに進め、決められない点は前提か未決として課題の本文へ書く。 + +(書き手: AI の起動指示、日付: 2026-10-25。利用者の指示の原文ではない。課題の本文「何を見つけたか」〜「直さないと何が起きるか」と 2 件のコメントは、書き手: 起票者、日付: 2026-10-24〜2026-10-25) + +## 調べて分かったこと(2026-10-25、design/issue-2087 d98516e5) + +- 手元の agy 1.4.7(`~/.local/bin/agy`)で `agy plugin validate plugins/ndf/dev.agy` を打つと、本文と同じ 8 本が `Invalid model tier sonnet` / `opus` で落ち、集計は `agents 3`・`validation failed: 8 errors, 10 warnings` だった +- **CI は agy の validate を既に 2 か所で回している。** 本文の「CI は agy を入れない」は `runtime-plugin-validate.yml` に限った話である + - PR の `runtime-smoke (agy)`(`runtime-plugin-smoke.yml`)は `tests/runtime-smoke/cli-versions.env` の `AGY_VERSION=1.3.4` に版を固定して `agy plugin validate` を回す(`tests/runtime-smoke/adapters/agy.sh`)。1.3.4 は `model` を検査しないため通っている + - 日次の `runtime-smoke-latest (agy)`(`runtime-plugin-smoke-latest.yml`、最新の版)は 2026-10-18 06:00 JST の回から毎日失敗している(直近は run 40140471141、2026-10-25 07:53 JST)。失敗の行は本文と同じ 8 本の `Invalid model tier` である。claude・codex・kiro の行は成功している +- `model` を持たない 3 本(`supervisor`・`supervisor-waits`・`worker`)は agy 1.4.7 で読み込まれる +- `plugins/ndf/dev.agy/agents` は `scripts/build-runtime-plugins.sh` が張る symlink(`../agents`)で、`--check` が向き先を検査する。`plugins/ndf/README.md` の 37 行目がこの形を説明している +- Kiro の導入(`plugins/ndf/dev.kiro/install.sh`)と Codex 版は `model` を読まない + +## 目的 + +以下で「agy に配る定義」は `plugins/ndf/dev.agy/agents/` の下にあり agy が読むエージェント定義を指す(今は `plugins/ndf/agents/` への symlink で、Claude Code の定義と同じファイル)。 + + +- agy 1.4 系の利用者に、NDF のエージェント定義 11 本がすべて読み込まれる +- agy の入った手元で、包みも `--no-verify` も無しに push とプランの `validate` が通る +- Claude Code の定義の `model` を変えずに、agy へ配る定義だけを agy の受け付ける形にする + +## 前提 + +- 前提 1: agy 1.4 系が `model` に受け付ける値は `flash`・`pro`・`inherit` と、欄が無いこと(欄が無いときは既定の `inherit`)の 4 通りである。根拠は 2026-10-25 のコメントの確認(1.4.7 で `flash`・`pro`・`inherit` が通り、`fast`・`default`・`sonnet`・`opus` が落ちた)と、上の 3 本が読み込まれること +- 前提 2: Claude Code の定義(`plugins/ndf/agents/*.md`)の `model: sonnet` / `model: opus` はこの変更で変えない。Claude Code では、エージェントごとに安い型と高い型を選び分けるために置いている値である +- 前提 3: agy に配る定義の `model` が 1.3.4 でも受け付けられる。1.3.4 は `model` を検査しないため、どの形を選んでも PR の `runtime-smoke (agy)` は通り続ける。設計で 1.3.4 の validate を 1 度通して確かめる +- 前提 4: 手元の包み(`~/.local/state/ndf/sv/s2086/bin/agy`)は NDF の配布物ではない。この変更では消さず、直した後は `PATH` に足さなくてよくなる + +## 対象範囲 + +含む: +- agy に配る定義の `model` を、agy の受け付ける値にする仕組み(`plugins/ndf/dev.agy/agents` の形と、それを作る・検査する `scripts/build-runtime-plugins.sh`) +- agy の入っていない環境(PR の CI の単体テスト)でも、agy に配る定義の `model` が受け付けられない値になったら落ちる検査 +- `dev.agy/agents` の形を説明する文書(`plugins/ndf/README.md` など)を新しい形に合わせること + +含まない: +- Claude Code の定義の `model` の値を変えること(前提 2) +- `tests/runtime-smoke/cli-versions.env` の agy の固定版(1.3.4)を上げること。版の更新は `scripts/runtime-smoke-latest.py` の既存の手順が持つ +- agy 1.4.7 が出す残りの警告(`hooks.json` の matcher が Claude Code の Tool 名を含む 5 件、Skill の本文が 24000 バイトを超える 5 件)。どれも validate を落とさない +- `.githooks/pre-push` と `scripts/validate-runtime-plugins.sh` の検査を緩めること(`Invalid model tier` だけを通す、など) +- 手元の包みを消すこと(前提 4) + +## ドメインイベント + +| # | イベント | 引き金 | 失敗したとき | 順序の前提 | +| --- | --- | --- | --- | --- | +| E1 | 開発者が `plugins/ndf/agents/*.md` を足した・変えた | 開発 | — | — | +| E2 | agy に配る定義が E1 に追従した | `bash scripts/build-runtime-plugins.sh`(形は未決 1) | 追従し忘れたまま E3 へ進む | E1 | +| E3 | pre-push と CI が、agy に配る定義と Claude Code の定義の食い違いを見つけた | `build-runtime-plugins.sh --check`・単体テスト | exit 1 で push と PR が止まる(受け入れ条件 6・7) | E1 | +| E4 | agy が配る定義を検査した | `agy plugin validate`(手元の pre-push、PR の 1.3.4、日次の最新版) | `Invalid model tier` で exit 1(今の不具合) | E2 | +| E5 | 利用者が agy に NDF を導入し、エージェントが読み込まれた | `agy plugin install` | 受け付けない `model` の定義を読み飛ばす(今の不具合) | E4 と同じ定義 | + +E2 の引き金は今も手で打つ生成であり、E3 が追従し忘れを塞ぐ。E5 は配布の後に起き、確かめるのはリリース後テストである。 + +## 受け入れ条件 + +- [ ] 1. agy 1.4.7 で `agy plugin validate plugins/ndf/dev.agy` を打つと、`Invalid model tier` の行が 0 件で、exit 0 で終わる +- [ ] 2. 同じ出力の集計の行が `agents 11` になる(今は `agents 3`) +- [ ] 3. agy 1.4.7 が `PATH` にあり包みが無い環境で、`bash scripts/validate-runtime-plugins.sh` が exit 0 で終わる +- [ ] 4. PR の `runtime-smoke (agy)`(agy 1.3.4 に固定)が成功する +- [ ] 5. `plugins/ndf/agents/*.md` の `model` の値が変わらない(`git diff develop -- plugins/ndf/agents` に `model:` の行の差が無い) +- [ ] 6. agy に配る定義と Claude Code の定義は、`model` の欄の他(名前・説明・本文・ほかの frontmatter)が一致する。Claude Code の定義を変えて agy に配る定義を追従させないと、`bash scripts/build-runtime-plugins.sh --check` が exit 1 で終わり、食い違うファイルを示す +- [ ] 7. agy に配る定義の `model` が `flash`・`pro`・`inherit`・欄なしのどれでもないと、agy の無い環境で動く単体テスト(`uv run --frozen --project . --all-extras pytest`)が落ちる。`plugins/ndf/agents/` に `model: sonnet` の定義を 1 本足しても、agy に配る定義はこの条件を満たし、テストが通る +- [ ] 8. `dev.agy/agents` の形を説明する文書(`plugins/ndf/README.md` の 37 行目の段落)が、新しい形と一致する +- [ ] 9. マージとリリースの後、日次の `runtime-smoke-latest (agy)` の `agy validate ndf` のステップが成功する + +## 非機能の条件 + +| 大項目 | 条件 | +| --- | --- | +| 運用・保守性 | エージェント定義の正は `plugins/ndf/agents/` の 1 か所に保つ。agy に配る側を手で直す運用を作らない(受け入れ条件 6) | +| システム環境 | agy 1.3.4(CI の固定版)と 1.4.7(手元と日次の最新版)の両方で validate が通る(受け入れ条件 1・4・9) | + +## 影響 + +| 対象 | 影響 | +| --- | --- | +| 公開インタフェース | 変わる。agy の利用者に読み込まれるエージェントが 3 本から 11 本になり、8 本の `model` は agy の tier(または既定)になる。Claude Code・Codex・Kiro の利用者には変わらない。互換を壊さない(今まで読み込まれなかった定義が読み込まれるようになるだけ) | +| データ | 無し | +| 既存の振る舞い | `scripts/build-runtime-plugins.sh` が `dev.agy/agents` に作るもの(今は symlink 1 本)と、その `--check` の検査 | + +## 検証手段 + +| 項目 | 手段 | +| --- | --- | +| 起動 | `~/.local/bin/agy plugin validate plugins/ndf/dev.agy`(受け入れ条件 1・2) | +| テスト | `uv run --frozen --project . --all-extras pytest . -q -n 4`(受け入れ条件 6・7) | +| 静的解析・型検査 | `PATH=$HOME/.local/bin:$PATH bash scripts/validate-runtime-plugins.sh`(受け入れ条件 3・6)。PR の CI の `runtime-smoke (agy)`(受け入れ条件 4) | +| 手動確認 | リリース後に、日次の `runtime-plugin-smoke-latest.yml` の agy の行を見る(受け入れ条件 9)。agy 1.4 系の手元で `agy plugin install` の後に `agy plugin list` でエージェントが 11 本あることを見る | + +## 前提とする取り決め + +| 項目 | 参照先 / 決めたこと | +| --- | --- | +| プロジェクト構造 | 生成物は `scripts/build-runtime-plugins.sh` が作り、`--check` が食い違いを見る(`plugins/ndf/README.md`)。エージェント定義の実体は `plugins/ndf/agents/` に置く | +| コーディング規約 | `AGENTS.md` の「最小限のコード実装」。確認は `bash scripts/check-lint.sh`(pre-push と CI の lint と同じ検査) | +| テスト戦略 | 生成と検査の形は単体テスト(`scripts/tests/` か `plugins/ndf/scripts/tests/`、agy なしで動く)で見る。agy の実物での読み込みは CI の smoke(1.3.4)と日次の最新版で見る。`.md` の文言を照合するテストは書かない | + +## 境界 + +| 区分 | 内容 | +| --- | --- | +| 常に行う | 生成のし直し(`bash scripts/build-runtime-plugins.sh`)と `--check`、全体テスト、手元の agy 1.4.7 での validate | +| 確認してから行う | agy の利用者に見える `model` の値の決め方(未決 1)。承認ゲート 1 で人が見る | +| 行わない | Claude Code の定義の `model` の変更、検査を緩めること、固定版の更新、範囲外の警告の手直し | + +## 未決 + +| 項目 | 誰が決めるか | 期限 | +| --- | --- | --- | +| 1. agy に配る定義の形(symlink をやめて生成した写しにする、など)と、`model` を外すか(既定の `inherit`)・`sonnet`→`flash`・`opus`→`pro` のような対応で変換するか | 設計(`design`)で案を比べ、承認ゲート 1 で人が決める | 設計 PR の承認まで | From 9b4754f72f835e4c1bddd1c72e274be08daa1651 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sun, 25 Oct 2026 10:31:44 +0000 Subject: [PATCH 08/17] =?UTF-8?q?Docs:=20#2104=20=E3=81=AE=E8=A8=AD?= =?UTF-8?q?=E8=A8=88=E3=82=92=E8=B6=B3=E3=81=99=EF=BC=88=E6=98=87=E6=A0=BC?= =?UTF-8?q?=E3=81=AE=E6=89=BF=E8=AA=8D=E3=81=AE=E5=BE=8C=E3=81=AF=E3=80=81?= =?UTF-8?q?=E6=89=BF=E8=AA=8D=E8=B3=87=E6=96=99=E3=81=AE=E5=85=88=E7=AB=AF?= =?UTF-8?q?=E3=81=AE=E3=81=A8=E3=81=8D=E3=81=A0=E3=81=91=E3=83=9E=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=81=99=E3=82=8B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pace: normal の昇格で、承認ゲート 2 の停止が昇格の承認資料へ承認したコミットの行を書き、 promote-approved が --approved-from でその SHA と先端を照合する設計を書いた。 照合の基準は承認資料の行だけにし(未決 1)、資料なしの --gate-approved user は 2 で止める(未決 2)。 用語集へ「昇格の承認資料」を足した。 Co-Authored-By: Claude Opus 5.5 --- docs/glossary.md | 1 + docs/glossary/glossary.json | 6 + issues/issue-2104-design.md | 281 ++++++++++++++++++++++++++++++++++++ 3 files changed, 288 insertions(+) create mode 100644 issues/issue-2104-design.md diff --git a/docs/glossary.md b/docs/glossary.md index 6f461342a..c7d769095 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -530,6 +530,7 @@ release が走らせるリリースの種別・リリースコマンド・公開 | リリースの経路 | `release_route` | 変更が本番系へ届く道筋の種類。template(release.form の雛形で組む)/ merge(マージで反映)/ manual(手で反映)/ none(届けない)。release.form があればそれ、無ければ宣言の delivery から決まる。リリースの形とは別の軸 | — | — | — | | 昇格の Pull Request | `promotion_pr` | ベースブランチから本番チャネルへ変更を入れる Pull Request | — | — | — | | 昇格のプラン | — | リリースの経路 promote のための「本番」のステージのプラン(plan_promote)。昇格の Pull Request を作り、承認ゲート 2 の後にマージする | — | — | — | +| 昇格の承認資料 | — | 昇格の承認ゲート 2 で利用者か MVV 判定が見る承認資料({state_dir}/work/approval-promote.md)。承認したコミットの欄を持ち、normal では promote の承認ゲート 2 の停止が、fast / auto では prepare が書く。promote-approved はこの欄の SHA が昇格の Pull Request の先端のときだけマージする | — | — | — | | 他のプラグイン | — | 配布の宣言の release.plugin 以外で、前のタグからの差分にファイルがあるプラグイン。release-steps.py changed-plugins が列挙する | — | — | `docs/specifications/ndf-release-other-plugin-levels-and-migration.md` | | 上げ幅 | — | 版数のどの桁を上げるか(MAJOR / MINOR / PATCH)。セマンティックバージョニングに従う | — | — | `docs/specifications/ndf-release-other-plugin-levels-and-migration.md` | | 移行の手順 | — | 利用者が新しい版へ移るときに自分で行う操作(引数の置き換え・認証のし直し・導入し直しなど)。PR 本文の「移行の手順」の節が正 | — | — | `docs/specifications/ndf-release-other-plugin-levels-and-migration.md` | diff --git a/docs/glossary/glossary.json b/docs/glossary/glossary.json index 2ebb4208a..0103e78bc 100644 --- a/docs/glossary/glossary.json +++ b/docs/glossary/glossary.json @@ -2647,6 +2647,12 @@ "meaning": "リリースの経路 promote のための「本番」のステージのプラン(plan_promote)。昇格の Pull Request を作り、承認ゲート 2 の後にマージする", "pending_source": "issues/old/issue-1457-design.md" }, + { + "term": "昇格の承認資料", + "context": "ndf-release", + "meaning": "昇格の承認ゲート 2 で利用者か MVV 判定が見る承認資料({state_dir}/work/approval-promote.md)。承認したコミットの欄を持ち、normal では promote の承認ゲート 2 の停止が、fast / auto では prepare が書く。promote-approved はこの欄の SHA が昇格の Pull Request の先端のときだけマージする", + "pending_source": "issues/issue-2104-design.md" + }, { "term": "プロダクトバックログ", "context": "ndf-issue-upkeep", diff --git a/issues/issue-2104-design.md b/issues/issue-2104-design.md new file mode 100644 index 000000000..efd6a9252 --- /dev/null +++ b/issues/issue-2104-design.md @@ -0,0 +1,281 @@ +# 昇格の承認の後のマージ: 承認ゲート 2 で利用者が見た先端だけを本番チャネルへ入れる(#2104) + +## 目的 + +- **何が壊れているか**: `pace: normal` の昇格のプランは、承認ゲート 2 で止まるときに昇格の Pull Request の先端を承認資料へ残さない。承認の後の `run <プラン> --from promote-approved` は、そのときの先端を照合せずにマージする +- **誰が困るか**: 経路 `promote` を組むリポジトリの利用者。承認の後にベースブランチが進むと(別のスプリントのマージなど)、見ていないコミットが本番チャネルへ入る +- **直すと何が成り立つか**: `promote-approved` は承認資料に載った「承認したコミット」が昇格の Pull Request の先端のときだけマージし、違えば・資料を読めなければマージせずに止まって承認ゲート 2 へ戻る。本番チャネルへの経路のうち昇格だけが照合を持たない状態が無くなる + +## 適用範囲 + +- **働く範囲**: 配布先のリポジトリでも働く(`merged-steps.py promote` と、スプリントが書き出す昇格のプラン `plan_promote`) +- **プロジェクトごとに違うもの**: ベースブランチと本番チャネルの名前は今どおり引数(`--head` / `--base`。`.ndf/worktree.json` の `base_branch` / `production_branch` からプランが組む)。承認資料のパスはプランが決めて渡す(要求の前提 3)。新しい設定は足さない +- **当たるモード**: 経路 `promote` の昇格のプランを持つすべてのモード(`standard` / `light` / `operation` / `legacy-refactor`)。`pace` は `normal` と、`fast` / `auto` の `handoff` の後に `--from promote-approved` で続ける場合 + +## あるべき姿の根拠 + +| 根拠 | 種類 | 何を示すか | +| --- | --- | --- | +| `docs/specifications/ndf-release-approved-commit.md` の I5・P1〜P4(#815・#1529) | 実測(既存の実装とテスト) | 本番の配布と `fast` / `auto` の `promote` は、承認したコミット・確かめたコミットを `--expect-head` と `--match-head-commit` で照合して既に動いている。同じ照合の部品(`GreenWatch._check_expected`・`_merge_pr`)を使える | +| `plugins/ndf/scripts/lib/approved_commit.py` の `material_row` / `material_sha` | 実測(既存の実装) | 承認資料の「承認したコミット」の行を書く形と読む形が 1 か所に既にある。昇格の承認資料もこの行を持てば、形を新しく決めずに済む | +| #2104 の本文の「依頼(原文)」と、#1529 の承認ゲート 1 で利用者が「別の課題で起票する」と決めたこと | 利用者の指示の原文 | `normal` の昇格の照合の欠けを直す対象とすること | + +要求と受け入れ条件は #2104 の本文にある(コピーは `issues/issue-2104-requirements.md`)。この文書は「どう作るか」だけを扱う。 + +## ドメインモデル + +### コンテキスト + +| コンテキスト | 何の語が 1 つの意味に決まるか | +| --- | --- | +| リリース(`ndf-release`) | 承認したコミット・昇格の Pull Request・昇格のプラン・承認資料の欄 | +| 開発の工程(`ndf-workflow`) | 承認ゲート 2・承認資料・`pace`・承認の後の続け方 | + +リリースが供給者、開発の工程が顧客の関係(顧客 / 供給者)。開発の工程(conductor・`pace.md`)は、リリースの `promote-approved` が返す結果(`head_moved` と `next`)だけを受けて、承認ゲート 2 へ戻る。 + +### 集約 + +| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | +| --- | --- | --- | --- | --- | +| 昇格の承認資料 | `merged_lib/merge.py` の `gate_stop`(`normal`)と `_prepare_promote`(`fast` / `auto`)が書く。行の形は `lib/approved_commit.py` の `material_row` / `material_sha` | `{state_dir}/work/approval-promote.md` | — | 承認したコミット(40 桁)・判定の行 | +| 昇格のマージ | `merged_lib/merge.py` の `promote` | 昇格の Pull Request | — | 先端(`headRefOid`)・照合する SHA(`expect_head`) | +| 昇格のプラン | `supervise_lib/delivery_templates.py` の `plan_promote` | プラン | ステップ(`promote`・`prepare`・`promote-approved`) | 昇格の承認資料のパス(`PROMOTE_MATERIAL`) | + +昇格のマージは、承認資料をパスで参照して読むだけで書き換えない。承認資料の書き手は 2 つあるが、`pace` ごとにどちらか 1 つだけが同じプランの中で書く(`normal` は `gate_stop`、`fast` / `auto` は `prepare`)。 + +### 不変条件 + +| # | 集約 | 条件 | 破れたときの扱い | +| --- | --- | --- | --- | +| I1 | 昇格の承認資料 | `promote` が昇格の Pull Request で承認ゲート 2 に止まる(終了コード 10)とき、承認資料の「2. 承認の判断に使うもの」の表に、止まる判定に使ったのと同じ 1 回の読みの `headRefOid` が `\| 承認したコミット \| <40 桁> \|` の 1 行で載り、結果の `metrics.approved_sha` と gate の `items[].head` も同じ値である | 先端が 40 桁の 16 進で読めなければ資料を書かずに終了コード 2(`EXIT_UNREADABLE`)で止まる。マージしない | +| I2 | 昇格の承認資料 | `fast` / `auto` の `prepare`(`--prepare`)が書く承認資料も、I1 と同じ形の「承認したコミット」の行を 1 行持つ | I1 と同じ | +| I3 | 昇格のマージ | `--approved-from <資料>` を受けた `promote` は、昇格の Pull Request の先端が資料の承認したコミットに等しいときだけ `gh pr merge` を呼び、`--match-head-commit` にその SHA を渡す | 読むたびに比べ、違えば `gh pr merge` を呼ばずに `items[].result: head_moved`(`head` と `expected` つき)・終了コード 1 で止まり、`next` に承認ゲート 2 からのやり直しを書く | +| I4 | 昇格のマージ | `--approved-from` を受けた `promote` は、資料から承認したコミットを読めないまま照合を飛ばさない | `gh` を 1 度も呼ばずに終了コード 2 で止まる | +| I5 | 昇格のマージ | `promote` は `--gate-approved user` を `--approved-from` 無しで受けない(照合の材料の無い利用者の承認でマージしない) | `gh` を 1 度も呼ばずに終了コード 2 で止まり、`next` に資料つきの打ち方を書く | +| I6 | 昇格の承認資料 | 「承認したコミット」の行を書く形と読む形は `approved_commit.material_row` / `material_sha` の 1 か所が決める | 読めなければ I4 の扱い | +| I7 | 昇格のプラン | `normal` の `promote` ステップは `--out PROMOTE_MATERIAL` を持ち、`promote-approved` ステップは `normal` と `fast` / `auto` の両方で `--approved-from PROMOTE_MATERIAL --gate-approved user` を持つ(`fast` / `auto` の `prepare` の `--out` と同じパス) | プランの組み立てのテストで落とす | +| I8 | 昇格のプラン | `fast` / `auto` の `promote` ステップのコマンド(`--expect-head-from VERIFY_MATERIAL --gate-approved mvv`・`on_fail: handoff`)は変わらない | #1529 のテストで落とす | + +### ドメインイベント + +| # | イベント | 発生元 | 受け手 | +| --- | --- | --- | --- | +| E1 | `normal` の `promote` が昇格の Pull Request を見つけた(無ければ作った) | `promote` ステップ(`_find_or_create_promote_pr`) | `merge_when_green` | +| E2 | `promote` が先端 X を読み、承認資料へ X を載せて承認ゲート 2 で止まった | `promote` ステップ(`GreenWatch` の最初の読み → `gate_stop`) | 人(承認ゲート 2)・conductor | +| E3 | 利用者が承認資料を見て承認ゲート 2 を承認した | 人 | conductor(`sprint-state.py gate ... --by user --outcome approved` → `--from promote-approved`) | +| E4 | ベースブランチが X から Y へ進んだ | プランの外のマージ | `promote-approved`(先端の Y として読む。E3 の前でも後でも同じ) | +| E5 | `promote-approved` が承認資料から X を読んだ | `promote-approved` ステップ(`material_sha`) | `GreenWatch`(`expect_head = X`) | +| E6 | `promote-approved` が先端を読み、X と比べた | `GreenWatch` の読みのたび | 一致ならマージ、違えば conductor(`head_moved` と `next`) | +| E7 | X で CI が通り、`--match-head-commit X` で本番チャネルへマージした | `_merge_pr` | `record` ステップ | +| E8 | 照合で止まった後、`--from promote` から流し直し、新しい先端 Y で承認ゲート 2 に止まった | conductor(結果の `next` と `pace.md`) | E2 と同じ | + +### 用語 + +| 用語 | 意味 | 用語集への反映 | +| --- | --- | --- | +| 承認したコミット | ゲート 2 の承認資料が対象にした、ベースブランチの先端のコミットの SHA(40 桁)。本番チャネルへ入れてよい中身の上限を示す | 無し(既存の語を同じ意味で昇格の承認資料にも使う。昇格の Pull Request の先端はベースブランチの先端である) | +| 昇格の承認資料 | 昇格の承認ゲート 2 で利用者(か MVV 判定)が見る承認資料(`{state_dir}/work/approval-promote.md`)。`normal` では `promote` の承認ゲート 2 の停止が、`fast` / `auto` では `prepare` が書く | 追加(`ndf-release`) | + +## 機能一覧 + +| # | 機能 | 誰が使うか | +| --- | --- | --- | +| F1 | 承認ゲート 2 で止まるとき、昇格の Pull Request の先端(承認したコミット)を承認資料と結果に載せる | 承認ゲート 2 で承認する利用者・conductor | +| F2 | 承認の後のマージ(`promote-approved`)が、承認資料の承認したコミットの先端だけをマージする | conductor(プランの `--from promote-approved`) | +| F3 | 照合で止まったとき、承認ゲート 2 からやり直す打ち方を結果の `next` と `pace.md` で示す | conductor | + +## 構成要素 + +| 要素 | 責務 | +| --- | --- | +| `lib/approved_commit.py`(変えない) | 「承認したコミット」の行の形(`material_row` で書く項目と値・`material_sha` で読む)。書き手と読み手が共有する | +| `merged_lib/merge.py` の `gate_stop` | `approved_sha` と `path` を受けたとき、判断の表の先頭へ `material_row(approved_sha)` を置いて `path` へ書き、`metrics.approved_sha` と gate の `items[].head` に同じ値を載せる。受けなければ今どおり(経路 `merge` の `merge-when-green` は渡さない) | +| `merged_lib/merge.py` の `merge_when_green` | 承認ゲート 2 の判定の閉包(`gate`)へ、`promote` から受けた「先端を承認資料へ載せるか」と資料のパスを通す。載せるときは最初の読みの `headRefOid` を `parse_sha` で確かめ、読めなければ終了コード 2 | +| `merged_lib/merge.py` の `promote` | `--approved-from` を受けたら `gh` より先に `material_sha` で承認したコミットを読み `a.expect_head` に入れる(読めなければ 2)。`--gate-approved user` が `--approved-from` 無しなら 2 で止まる。止まる前の承認ゲート 2 では `gate_stop` へ先端と `a.out` を渡し、`next` の打ち方に `--approved-from <書いた資料>` を入れる | +| `merged_lib/merge.py` の `_prepare_promote` | 判断の表の先頭へ `material_row(info["headRefOid"])` を置く | +| `merged_lib/checks.py` の `GreenWatch._check_expected` | `head_moved` で止まるとき、呼び手が渡した続け方(`a.moved_next`)があれば結果の `next` に載せる。無ければ今どおり `next` を持たない(#815・#1529 の呼び手は渡さない) | +| `merged-steps.py` の `promote` の引数 | `--approved-from <資料>` を足す(`--expect-head-from` と同時に受けない)。`--out` の説明を「`--prepare` と承認ゲート 2 の停止の承認資料の置き場」へ広げる。docstring の用法を合わせる | +| `supervise_lib/delivery_templates.py` の `plan_promote` | `PROMOTE_MATERIAL = "{state_dir}/work/approval-promote.md"` を置き、`normal` の `promote` へ `--out PROMOTE_MATERIAL`、両方の `promote-approved` へ `--approved-from PROMOTE_MATERIAL` を足す。`fast` / `auto` の `prepare` と `mvv` の `--material` も同じ定数を使う | +| `plugins/ndf/skills/development-workflow/references/pace.md` | 承認ゲート 2 の行の「`promote-approved` は照合を持たず…」を、照合で止まったときの続け方(`normal` は `--from promote`、`fast` / `auto` は `--from verify`)へ書き換える。昇格のプランの説明(229 行の辺り)に承認資料の照合を足す | +| `docs/specifications/ndf-release-approved-commit.md` | P5・「含まない」の `pace: normal` の行・「照合で止まった後の続き」の未確認の行・テストの P4・P5 の行を、この変更の形へ書き直す | + +```mermaid +graph LR + subgraph plan[昇格のプラン plan_promote] + PN[promote(normal)] + PP[prepare(fast / auto)] + PA[promote-approved] + end + subgraph merge[merged_lib] + PR[promote] + PRE[_prepare_promote] + MWG[merge_when_green] + GS[gate_stop] + GW[GreenWatch._check_expected] + end + AC[approved_commit
material_row / material_sha] + M[(approval-promote.md)] + PACE[pace.md の承認ゲート 2 の行] + SPEC[ndf-release-approved-commit.md] + PN --> PR + PP --> PR + PA --> PR + PR --> PRE + PR --> MWG + MWG --> GS + MWG --> GW + PRE --> AC + GS --> AC + PR --> AC + GS --> M + PRE --> M + PR -- 読む --> M + GW -- next --> PACE + SPEC -. 確定仕様として記す .- PR +``` + +## 入出力の契約 + +### `merged-steps.py promote` + +| 項目 | 書くこと | +| --- | --- | +| 名前 | `merged-steps.py promote --head <ベースブランチ> --base <本番チャネル> [--prepare] [--out <資料>] [--gate-approved user\|mvv] [--expect-head-from <資料> \| --approved-from <資料>]` | +| 入力(足すもの) | `--approved-from <資料>`: 昇格の承認資料。「承認したコミット」の欄の SHA が昇格の Pull Request の先端のときだけマージする。任意。`--expect-head-from` と同時には受けない(argparse の相互排他) | +| 入力(意味を広げるもの) | `--out <資料>`: `--prepare` に加え、承認ゲート 2 で止まるときの承認資料の置き場。無ければ今どおり承認資料の既定の置き場(`presentation_dir()`) | +| 出力(承認ゲート 2・終了コード 10) | 今の結果に加え、`metrics.approved_sha`(40 桁)と、`items[]` の `result: gate` の行の `head`(同じ値)。`next` は `承認を得たら promote --head --base --gate-approved user --approved-from <書いた資料>(プランなら run <プラン> --from promote-approved)` | +| 出力(`--prepare`・終了コード 0) | 今の結果のまま。承認資料に「承認したコミット」の行が増える | +| 失敗の形 | 資料が無い・欄が無い・40 桁の 16 進でない → `gh` を呼ばずに 2(I4)。`--gate-approved user` で `--approved-from` が無い → `gh` を呼ばずに 2(I5)。先端が違う → `items[].result: head_moved`(`head`・`expected`)で 1、`next` に「承認ゲート 2 からやり直す: プランなら run <プラン> --from promote(fast / auto は --from verify)。承認資料を新しい先端で作り直してから承認を求める」(I3)。承認ゲート 2 の停止で先端を読めない → 2(I1) | +| 互換性 | 引数を足すだけで、`--gate-approved mvv` の呼び出し(`fast` / `auto` の `promote` ステップ)と `--expect-head-from` は変わらない。`--gate-approved user` を資料なしで打つ既存の呼び出し(この変更の前に書き出したプランの `promote-approved`、手で打つ打ち方)は 2 で止まるようになる(決定 3) | + +### 昇格の承認資料の行 + +`approval_present` の「## 2. 承認の判断に使うもの」の表の先頭の行に `| 承認したコミット | <40 桁> |` を置く(`_judge_table` が書く `| 項目 | 内容 |` の表の 1 行)。本番の配布の承認資料(`approval-facts`)と同じ欄の名前・同じ位置である。読み手は `material_sha`(`_cells` で欄の名前を引く)で、行の位置に頼らない。 + +`fast` / `auto` では、`mvv` ステップがこの資料を `mvv-gate.py check --gate release --material` に渡すため、`mvv-gate.py` は従うときに「承認の記録」(`by: mvv`)をこの行の直後へ書く(`material_sha_or_none` で欄を見つけた資料へ書く既存の振る舞い。#815 の I10)。`promote-approved` は承認の記録を読まない(決定 2)。 + +## 処理の流れ + +```mermaid +sequenceDiagram + participant C as conductor + participant S as プラン + participant P as merge.promote + participant A as approved_commit + participant G as GreenWatch + participant GH as gh + C->>S: run(normal) + S->>P: promote --out approval-promote.md + P->>GH: 昇格の PR を探す(無ければ作る) + P->>G: merge_when_green(承認資料へ先端を載せる) + G->>GH: pr view(headRefOid = X) + G->>P: gate(本番チャネル・承認なし) + P->>A: material_row(X) + P-->>S: gate(10)・approved_sha X・資料に X + S-->>C: 承認ゲート 2 + C->>C: 利用者が資料を見て承認 + C->>S: run --from promote-approved + S->>P: promote --approved-from approval-promote.md --gate-approved user + P->>A: material_sha(資料) + alt 読めない + A-->>P: ValueError + P-->>S: 2(gh を呼ばない) + else 読めた(X) + P->>GH: 昇格の PR を探す + P->>G: merge_when_green(expect_head = X) + loop 読むたび + G->>GH: pr view(headRefOid) + alt 先端が X でない + G-->>S: head_moved(1)・next: --from promote + S-->>C: 止まる → 承認ゲート 2 からやり直す + end + end + P->>GH: pr merge --match-head-commit X + P-->>S: ok(0) + S->>S: record + end +``` + +`fast` / `auto` では、`prepare` が同じ資料へ先端 X を書き、`promote`(`--expect-head-from approval-verify.md --gate-approved mvv`)が確かめたコミットで照合する(#1529。変えない)。`note` か `promote` が落ちて `handoff` から承認ゲート 2 に戻り、利用者が承認して `--from promote-approved` で続けたときは、上の図の `run --from promote-approved` 以降と同じ照合を `prepare` が書いた X で行う。照合で止まったときの続きは `--from verify`(導入の確認を新しい先端で走らせ直す。#1529 の決定)。 + +## 非機能の実現方式 + +| 大項目 | 要求の条件 | 実現方式 | 確かめ方 | +| --- | --- | --- | --- | +| セキュリティ | 本番チャネルへ入るのは、承認資料に載った SHA と等しい先端だけである(受け入れ条件 3・4)。資料を読めないときに照合を飛ばす経路を持たない | `promote-approved` は `--approved-from` の資料から `material_sha` で読んだ SHA を `expect_head` にし、`GreenWatch` が読むたびに比べ、`--match-head-commit` で同じ SHA を固定してマージする。資料を読めないときと、`--gate-approved user` が資料なしのときは `gh` を呼ばずに 2 で止まる(I3〜I5) | `gh` を偽物にした `promote` の単体テストで、先端が違う・資料が無い・欄が無い・形が違う・`--approved-from` が無いの各場合に `gh pr merge` の呼び出しが 0 回であることを数える | +| 運用・保守性 | 照合で止まった結果は、どの SHA を期待し今どの SHA かを `items` に残し、続け方を `next` に書く。conductor がプランの外で手当てをしなくても承認ゲート 2 へ戻れる | `head_moved` の `items` に `head` と `expected` を残し(既存)、`promote` が `a.moved_next` に承認ゲート 2 からのやり直しの打ち方を入れて `next` に載せる。`pace.md` の承認ゲート 2 の行が同じ続け方を持つ | `head_moved` の結果の `next` に `--from promote` が入ることを単体テストで見る | + +## 決定の記録 + +### 決定 1: 照合の材料を本番の配布と同じ形にするため、昇格の承認資料に「承認したコミット」の行を置き、読み書きを `material_row` / `material_sha` で共有する + +`approved_commit.py` には、ゲート 2 の承認資料の対象を表す「承認したコミット」の欄の書き方(`material_row`)と読み方(`material_sha`)が既にある。昇格の Pull Request の先端はベースブランチの先端で、語の意味がそのまま当たる。新しい関数も行の形も足さずに受け入れ条件 8 を満たせる。 + +導入の確認の承認資料の行(`verified_line`)を流用する形は採らない。その行は「導入の確認をこのコミットで走らせた」を意味し、導入の確認を走らせない `normal` で書くと語の意味が変わる。 + +根拠: Value 6 / Value 8(MVV 版 3) + +### 決定 2: 承認の後に conductor の操作を増やさないため、照合の基準は承認資料の「承認したコミット」の行だけにし、承認の記録の行は求めない(未決 1) + +`normal` の承認ゲート 2 の承認は、conductor が `sprint-state.py gate ... --by user --outcome approved` で記録してから `--from promote-approved` で続ける形で既に残る。#815 の承認の記録(`release-steps.py approve --by user`)まで求めると、承認の後に打つ操作が 1 つ増え、昇格の経路だけ手順が変わる。照合が防ぐのは「利用者が見た先端と違う先端のマージ」で、資料の SHA との一致で足りる。 + +承認の記録をそろえる形は採らない。承認ゲートの工程を増やさずに守れる範囲が同じで、操作だけが増える。 + +根拠: Value 2 / Value 1(MVV 版 3) + +### 決定 3: 照合の材料の無い承認でマージする経路を残さないため、`--gate-approved user` の `promote` は `--approved-from` を必須にし、無ければ 2 で止める(未決 2) + +この変更の前に書き出したプランの `promote-approved` は `--approved-from` を持たない。今どおり通すと、要求の非機能の条件(資料を読めないときに照合を飛ばす経路を持たない)を満たさない経路が残る。止めても、`next` が `--from promote` からの流し直し(新しいプランは同じスプリントの書き出しで作られる)を示すため、戻せない損は無い。手で打つ打ち方も、承認ゲート 2 の停止の `next` が資料つきの形を示す。 + +古いプランを照合なしで通す形は採らない。プランの書き出しはスプリントごとで、移行のために照合の無い経路を残す費用が見合わない。 + +根拠: Value 1 / Vision(MVV 版 3) + +### 決定 4: プランの組み立てを 1 つの形に保つため、承認資料のパスは `normal` と `fast` / `auto` で同じ `PROMOTE_MATERIAL` にし、`promote-approved` のコマンドは両方で等しいままにする + +`fast` / `auto` の `prepare` は既に `{state_dir}/work/approval-promote.md` へ書く。`normal` の `promote` に同じパスを `--out` で渡せば、`promote-approved` は `pace` によらず同じコマンドになり、`test_supervise_pace_verify.py` の「両方の `promote-approved` が等しい」はそのまま成り立つ。 + +`promote-approved` が PR の番号から既定の置き場(`presentation_dir()`)を求める形は採らない。プランがパスを決めて渡すという前提 3 と合わず、読み手と書き手の結び付きがコードの中に隠れる。 + +根拠: Value 4 / Value 5(MVV 版 3) + +### 決定 5: 照合で止まった後の続け方を 1 つの文で示すため、`head_moved` の `next` は `pace` で分けず `--from promote`(`fast` / `auto` は `--from verify`)を並べて書く + +`promote-approved` のコマンドは `pace` によらず等しい(決定 4)ため、自分の `pace` を知らない。`pace` ごとに引数を足して `next` を分けると、等しさが崩れ、プランの引数が 1 つ増える。続け方の正本は `pace.md` の承認ゲート 2 の行で、`next` はそこと同じ 2 つの打ち方を示す。 + +照合の失敗を `on_fail` で `promote` へ自動で戻す形は採らない。`on_fail` は CI の失敗やマージの拒否にも掛かり、それらを承認ゲート 2 の取り直しに読み替えてしまう。 + +根拠: Value 4 / Value 2(MVV 版 3) + +## テスト設計 + +| 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | +| --- | --- | --- | +| AC1・I1 | `normal` の `promote`(`--out` つき)が本番チャネルへの PR で 10 を返したとき、資料の「承認したコミット」の欄を `material_sha` で読むと偽の `gh` が返した `headRefOid` に等しく、`metrics.approved_sha` と gate の `items[].head` も同じ | 行を書かない・別の読み(2 回目の `pr view`)の SHA を書く・`metrics` に載せないと落ちる | +| I1(読めない先端) | `headRefOid` が 40 桁でない PR では、資料を書かずに 2 で終える | 形を確かめずに資料を書く・10 で止まると落ちる | +| AC2・I3 | 資料が X・先端が X のまま CI が緑のとき、`gh pr merge` が `--match-head-commit X` つきで 1 回呼ばれ 0 で終える | `--match-head-commit` を外す・別の SHA を渡すと落ちる | +| AC3・I3 | 資料が X・最初の読みの先端が Y のとき、および CI を待つ間に Y へ変わったとき、`gh pr merge` を呼ばず `head_moved`(`head`・`expected`)で 1、`next` に `--from promote` を含む | 待ち直してマージする・`next` を持たないと落ちる | +| AC4・I4 | 資料が無い・欄が無い・40 桁の 16 進でないとき、`gh` の呼び出しが 0 回で 2 | 読む前に `gh pr list` を打つ・照合を飛ばしてマージすると落ちる | +| I5 | `--gate-approved user` を `--approved-from` 無しで打つと、`gh` の呼び出しが 0 回で 2 | 資料なしで今どおりマージすると落ちる | +| AC5・I7 | プランの組み立てで、`normal` の `promote` が `--out {state_dir}/work/approval-promote.md` を持ち、`normal` と `fast` / `auto` の `promote-approved` が等しく `--approved-from {state_dir}/work/approval-promote.md` を持つ。`fast` / `auto` の `prepare` の `--out` と同じパスである | 片方の `pace` にだけ付ける・パスを `prepare` と違えると落ちる | +| AC6・I2 | `--prepare` の資料の「承認したコミット」の欄が、偽の `gh` の `headRefOid` に等しい | `_prepare_promote` に行を足さないと落ちる | +| AC7・I8 | `fast` / `auto` の `promote` ステップのコマンドが `--expect-head-from {state_dir}/work/approval-verify.md --gate-approved mvv` で終わり、`on_fail: handoff` のまま。#1529 の `promote` のテストが通る | `promote` ステップへ `--approved-from` を足すと落ちる | +| AC8・I6 | `gate_stop`・`_prepare_promote` が書いた資料を `material_sha` で読めること(書き手と読み手が同じ関数を通ること)を、上の AC1・AC6 の行で縛る | 書き手が独自の文字列で欄を書き、`material_row` の項目名を変えると読めずに落ちる | +| AC9 | 文書の変更なので振る舞いのテストは書かない(`.md` の文言テストは書かない)。`instructions-check.py` と `check-skill-frontmatter.py` が通ること | — | +| AC10 | 全体テストと `ruff check .` が通る | — | + +既存の `test_supervise_pace_verify.py` の `test_the_promote_plan_uses_the_declared_branch_names` は、`normal` の `promote` と `promote-approved` のコマンドが引数を持たない形(`endswith(base)`)を確かめている。受け入れ条件 5 と食い違う行だけを新しい形へ書き換え、`fast` の `promote` の行(`want`)と、ブランチ名の行は変えない。 + +## 設計の結果 + +| 課題 | 扱い | 取り込み先 | 触るファイル | +| --- | --- | --- | --- | +| #2104 | 実装する | — | `plugins/ndf/scripts/merged_lib/merge.py`、`plugins/ndf/scripts/merged_lib/checks.py`、`plugins/ndf/scripts/merged-steps.py`、`plugins/ndf/scripts/supervise_lib/delivery_templates.py`、`plugins/ndf/scripts/tests/`、`plugins/ndf/skills/development-workflow/references/pace.md`、`docs/specifications/ndf-release-approved-commit.md`、`docs/glossary/glossary.json`、`docs/glossary.md` | + +## 未確認のまま残ること + +| 項目 | 内容 | +| --- | --- | +| 経路 `merge` の `merge-approved` | 宛先が自動反映の本番チャネルのとき、`merge-when-green` の承認ゲート 2 の停止も先端を資料へ残さず、`--from merge-approved` は今の先端をマージする。同じ形の欠けを読みで確かめた(`gate_stop` の `next` が `merge-when-green --gate-approved user` で照合の引数を持たない)。この課題の範囲外として conductor へ返す(要求の未決 3) | +| 実機での承認ゲート 2 | 昇格の経路を組むリポジトリで `pace: normal` の承認ゲート 2 を 1 度通し、資料の SHA とマージされた先端が等しいことは、リリース後テストで確かめる | +| 書き出し済みのプランの停止 | 版の更新をまたいで続くスプリントの `promote-approved` は 2 で止まる(決定 3)。止まった後に `--from promote` で流し直すと、書き出し済みのプランの `promote` は `--out` を持たないため、資料は既定の置き場へ書かれ、`next` が示す手打ちの形で続けることになる | From 15cdb7fa74c232ef669432341bfd083c20e41764 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sun, 25 Oct 2026 10:31:50 +0000 Subject: [PATCH 09/17] =?UTF-8?q?Docs:=20#2087=20=E3=81=AE=E8=A8=AD?= =?UTF-8?q?=E8=A8=88=E3=82=92=E6=9B=B8=E3=81=8F=EF=BC=88agy=20=E3=81=AB?= =?UTF-8?q?=E9=85=8D=E3=82=8B=E5=AE=9A=E7=BE=A9=E3=82=92=E7=94=9F=E6=88=90?= =?UTF-8?q?=E3=81=97=E3=81=9F=E5=86=99=E3=81=97=E3=81=AB=E3=81=97=20model?= =?UTF-8?q?=20=E3=82=92=20tier=20=E5=90=8D=E3=81=B8=E5=A4=89=E6=8F=9B?= =?UTF-8?q?=E3=81=99=E3=82=8B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dev.agy/agents の symlink をやめ、scripts/lib/agy_agents.py が plugins/ndf/agents から 写しを生成する。model は対応表で sonnet→flash・opus→pro に変換する(外すかは承認ゲート 1 で決める)。 agy 1.4.7 と 1.3.4 で変換・外す両方の形が validate を通ることを実測した。 用語集に「agy に配る定義」「model の対応表」を足す。 Co-Authored-By: Claude Opus 5.5 --- docs/glossary.md | 2 + docs/glossary/glossary.json | 12 ++ issues/issue-2087-design.md | 274 ++++++++++++++++++++++++++++++++++++ 3 files changed, 288 insertions(+) create mode 100644 issues/issue-2087-design.md diff --git a/docs/glossary.md b/docs/glossary.md index 6f461342a..e1c9dc53a 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -17,6 +17,8 @@ | 未登録の語 | `unregistered` | 用語として書かれているのに用語集に無い語。見出しが「用語」の節の表の 1 列目に書いた語を指す | — | — | `docs/specifications/ndf-ubiquitous-language.md` | | 用語集の取り込み | — | ベースブランチの取り込みで衝突した用語集を、共通の祖先・自分の側・相手の側の 3 つの版から語の単位で合わせ、文書を作り直して取り込みを終えること。`glossary.py merge` が行う | — | — | `docs/specifications/ndf-glossary-merge.md` | | 語の衝突 | — | 用語集の取り込みで、両側が同じ語(コンテキストと語の組)を違う中身にしたこと、または片側が消して片側が書き換えたこと。コンテキストの `id` でも同じ。スクリプトは解かずに止まる | — | — | `docs/specifications/ndf-glossary-merge.md` | +| agy に配る定義 | — | `plugins/ndf/dev.agy/agents/` の下にあり agy が読むエージェント定義。`plugins/ndf/agents/` から生成した写しで、`model` だけが agy の tier 名に変わる。手で直さない | — | — | — | +| model の対応表 | — | Claude Code のエージェント定義の `model` の値(型の名前)から agy の tier 名への対応。`scripts/lib/agy_agents.py` が持つ | — | — | — | ## NDF の開発ワークフロー(`ndf-workflow`) diff --git a/docs/glossary/glossary.json b/docs/glossary/glossary.json index 2ebb4208a..26fbb6784 100644 --- a/docs/glossary/glossary.json +++ b/docs/glossary/glossary.json @@ -4243,6 +4243,18 @@ "context": "ndf-document", "meaning": "組み直した文書と想定する読み手の説明だけを渡され、会話や経緯の文脈を持たずに読んで、要点の言い直し・分からなかった語と箇所・納得できなかった主張を返す者(サブエージェントか external-ai で呼ぶ別の CLI)", "source": "plugins/ndf/skills/document-rebuild/SKILL.md" + }, + { + "term": "agy に配る定義", + "context": "project-glossary", + "meaning": "`plugins/ndf/dev.agy/agents/` の下にあり agy が読むエージェント定義。`plugins/ndf/agents/` から生成した写しで、`model` だけが agy の tier 名に変わる。手で直さない", + "source": "" + }, + { + "term": "model の対応表", + "context": "project-glossary", + "meaning": "Claude Code のエージェント定義の `model` の値(型の名前)から agy の tier 名への対応。`scripts/lib/agy_agents.py` が持つ", + "source": "" } ] } diff --git a/issues/issue-2087-design.md b/issues/issue-2087-design.md new file mode 100644 index 000000000..8be0416e4 --- /dev/null +++ b/issues/issue-2087-design.md @@ -0,0 +1,274 @@ +# agy のエージェント定義: agy 1.4 系でも 11 本すべてが読み込まれ、agy の入った手元で push が通る(#2087) + +## 目的 + +- **何が壊れているか**: agy 1.4 系は `model` に tier 名(`flash` / `pro` / `inherit`)しか受け付けず、Claude Code と同じファイルを symlink で配っている 8 本(`model: sonnet` / `opus`)を読み飛ばす。`agy plugin validate` も exit 1 で落ちる +- **誰が困るか**: agy 1.4 系の利用者(8 本のエージェントを使えない)と、agy の入った手元で開発する人(pre-push が落ちて push できない) +- **直すと何が成り立つか**: agy に配る定義だけを生成した写しにして `model` を tier 名へ変換し、Claude Code の定義を変えずに 11 本すべてを agy に読み込ませる。写しの追従し忘れは `--check` と単体テストが塞ぐ + +## 適用範囲 + +- **働く範囲**: このリポジトリの生成スクリプト(`scripts/build-runtime-plugins.sh`)と、その生成物を含む配布物(`plugins/ndf/dev.agy/agents/`)。生成物は配布物として利用者の agy に届く +- **プロジェクトごとに違うもの**: 無し(ai-plugins の配布物を作る生成の仕組みで、利用者のリポジトリでは動かない) +- **当たるモード**: `standard` + +## あるべき姿の根拠 + +| 根拠 | 種類 | 何を示すか | +| --- | --- | --- | +| agy 1.4.7 で `model` を `flash` / `pro` に変換した写し(`/tmp` に作った試作)の `agy plugin validate` が exit 0・`agents 11`(2026-10-25) | 実測 | 変換した写しで受け入れ条件 1・2 が成り立つ | +| agy 1.4.7 で `model` の行を消した写しも exit 0・`agents 11`(同日) | 実測 | 外す形でも成り立つ(決定 2 の比べた案) | +| agy 1.3.4(CI の固定版、sha512 を照合して取得)で、変換・外す・今の symlink の 3 つとも exit 0・`agents : 11 processed`(同日) | 実測 | 前提 3 が成り立つ。どの形でも PR の `runtime-smoke (agy)` は通り続ける | +| agy の changelog がエージェント定義の `model` を tier 名(既定 `inherit`)と書く | 外部の一次情報 | `sonnet` / `opus` は agy の値の集合に無い | + +要求と受け入れ条件は #2087 の本文にある(コピーは `issues/issue-2087-requirements.md`)。この文書は「どう作るか」だけを扱う。 + +## ドメインモデル + +### コンテキスト + +| コンテキスト | 何の語が 1 つの意味に決まるか | +| --- | --- | +| プロジェクト全体(`project-glossary`) | エージェント定義・agy に配る定義・model の対応表・tier | + +1 つのコンテキストに収まる。Claude Code と agy の `model` の値は語として別のもの(Claude Code の型の名前 / agy の tier 名)で、対応表が 2 つを結ぶ。 + +### 集約 + +| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | +| --- | --- | --- | --- | --- | +| エージェント定義の集合 | 開発者(手で書く) | `plugins/ndf/agents/` | 定義 1 本(`<名前>.md`) | `model` の値(Claude Code の型の名前) | +| agy に配る定義の集合 | 生成スクリプトだけ(手で書かない) | `plugins/ndf/dev.agy/agents/` | 写し 1 本(`<名前>.md`) | `model` の値(agy の tier 名) | + +agy に配る定義の集合は、エージェント定義の集合と model の対応表から導ける。持ち主を生成スクリプトに限るのは、正を `plugins/ndf/agents/` の 1 か所に保つためである(要求の非機能の条件)。 + +### 不変条件 + +| # | 集約 | 条件 | 破れたときの扱い | +| --- | --- | --- | --- | +| I1 | agy に配る定義の集合 | 写しのファイル名の集合が、エージェント定義のファイル名の集合と等しい | `--check` が足りない写し・余った写しを示して exit 1 | +| I2 | agy に配る定義の集合 | 写し 1 本は、元の定義の frontmatter の `model:` の行だけを対応表で置き換えたバイト列と等しい(他の行・本文は同じ) | `--check` が食い違う写しを示して exit 1 | +| I3 | agy に配る定義の集合 | 写しの `model` は `flash` / `pro` / `inherit` / 欄なしのどれか | 単体テストが落ちる(agy なしで動く) | +| I4 | model の対応表 | 対応表の値はすべて agy の tier 名の集合に入る | 生成が始まる前に exit 1 | +| I5 | エージェント定義の集合 | 対応表に無い `model` の値を持つ定義があると、生成も `--check` も成り立たない(黙って外さない・黙って通さない) | 定義のパスと値を示して exit 1 | +| I6 | エージェント定義の集合 | 生成はエージェント定義のファイルを書き換えない(`dev.agy/agents` が古い symlink のままでも、その向き先へ書き込まない) | 単体テストが落ちる | + +### ドメインイベント + +| # | イベント | 発生元 | 受け手 | +| --- | --- | --- | --- | +| E1 | 開発者がエージェント定義を足した・変えた | 開発者 | 生成スクリプト(E2)、`--check`(E3) | +| E2 | agy に配る定義が E1 に追従した | `bash scripts/build-runtime-plugins.sh` | git(写しをコミットする) | +| E3 | 食い違いを見つけた | `build-runtime-plugins.sh --check`(pre-push・CI の `runtime-plugin-validate.yml`)、単体テスト | 開発者(exit 1 と食い違うパス) | +| E4 | agy が配る定義を検査した | `agy plugin validate`(手元の pre-push・PR の 1.3.4・日次の最新版) | 開発者・CI | +| E5 | 利用者の agy にエージェントが読み込まれた | `agy plugin install` | 利用者 | + +### 用語 + +| 用語 | 意味 | 用語集への反映 | +| --- | --- | --- | +| agy に配る定義 | `plugins/ndf/dev.agy/agents/` の下にあり agy が読むエージェント定義。`plugins/ndf/agents/` から生成した写しで、`model` だけが agy の tier 名に変わる。手で直さない | 追加 | +| model の対応表 | Claude Code のエージェント定義の `model` の値(型の名前)から、agy の tier 名への対応。`scripts/lib/agy_agents.py` が持つ | 追加 | + +## 機能一覧 + +| # | 機能 | 誰が使うか | +| --- | --- | --- | +| F1 | エージェント定義から agy に配る定義を生成する(`model` を対応表で変換する) | 開発者(`bash scripts/build-runtime-plugins.sh`) | +| F2 | agy に配る定義の追従し忘れ・手直し・余り・足りなさを見つける | pre-push と CI(`build-runtime-plugins.sh --check`) | +| F3 | agy に配る定義の `model` が agy の受け付けない値なら落とす(agy なし) | CI の単体テスト | +| F4 | agy の利用者に 11 本のエージェントが読み込まれる | agy の利用者 | + +## 構成要素 + +| 要素 | 責務 | +| --- | --- | +| `scripts/lib/agy_agents.py`(新規) | model の対応表と agy の tier 名の集合を持つ。エージェント定義から写しを作り、書く(生成)か突き合わせる(`--check`)。古い symlink を外してからディレクトリを作る。食い違いを標準エラーへ出し、終了コードで返す | +| `scripts/build-runtime-plugins.sh`(変更) | `sync_agy_skill_links` の symlink の一覧から `agents` を外し、`agy_agents.py` を呼ぶ(`codex_skill_policies.py` と同じ呼び方)。冒頭の usage と、共有を説明するコメントを直す | +| `plugins/ndf/dev.agy/agents/*.md`(生成物・11 本) | agy に配る定義。symlink 1 本から実体のファイル 11 本へ変わる | +| `scripts/tests/test_agy_agents.py`(新規) | 不変条件 I1〜I6 と受け入れ条件 6・7 を agy なしで縛る | +| `scripts/tests/test_agy_distribution.py`(変更) | `test_dev_agy_shares_the_implementation` の `agents` の行を外す(`scripts` の行は残す) | +| `plugins/ndf/README.md`(変更) | 37 行目の段落を新しい形に合わせる(受け入れ条件 8) | +| `docs/specifications/ndf-worker-agent-and-skill-excerpts.md`(変更) | 64 行目の「`../agents` への symlink で同じ定義が配られる」を新しい形に合わせる | + +変えないもの: `plugins/ndf/agents/*.md`(前提 2)、`scripts/validate-runtime-plugins.sh` と `.githooks/pre-push`(既に `--check` と `agy plugin validate` を呼んでおり、検査を緩めない)、`tests/runtime-smoke/`(`dev.agy` を丸ごと validate・install するため、形が変わっても手順は同じ)。 + +```mermaid +graph LR + subgraph 手で書く + SRC["plugins/ndf/agents/*.md
model: sonnet / opus"] + end + subgraph 生成 + BUILD["build-runtime-plugins.sh"] + LIB["scripts/lib/agy_agents.py
model の対応表"] + end + subgraph 生成物 + OUT["dev.agy/agents/*.md
model: flash / pro"] + end + subgraph 検査 + CHECK["--check
pre-push・CI"] + TEST["test_agy_agents.py
test_agy_distribution.py"] + AGY["agy plugin validate
1.3.4・最新版"] + end + DOC["README.md・確定仕様"] + SRC --> LIB + BUILD --> LIB + LIB --> OUT + CHECK --> LIB + TEST --> LIB + TEST --> OUT + AGY --> OUT + DOC -. 形を説明 .-> OUT +``` + +### パッケージ・モジュール構成 + +```text +scripts/ +├── build-runtime-plugins.sh # 変更: agents の symlink をやめ、agy_agents.py を呼ぶ +├── lib/ +│ ├── codex_skill_policies.py # 既存(呼び方の手本) +│ └── agy_agents.py # 新規 +└── tests/ + ├── test_agy_agents.py # 新規 + └── test_agy_distribution.py # 変更 +plugins/ndf/ +├── agents/*.md # 変えない(正) +└── dev.agy/ + ├── agents/*.md # symlink → 生成した写し 11 本 + ├── scripts -> ../scripts # 変えない + └── skills/ # 変えない +``` + +## 入出力の契約 + +`scripts/lib/agy_agents.py` はコマンドとして呼ばれる。呼び方は `codex_skill_policies.py` に揃える。 + +```text +python3 scripts/lib/agy_agents.py +``` + +| 引数・出力 | 形 | +| --- | --- | +| 1 つ目 | プラグインのディレクトリ(`plugins/ndf`)。`agents/` と `dev.agy/` を読む。`dev.agy/` が無いプラグインでは何もせず 0 で終わる | +| 2 つ目 | `true` なら書かずに突き合わせる(`--check`)。`false` なら書く | +| 終了コード 0 | 書き終えた、または食い違いが無い | +| 終了コード 1 | 食い違いがある・対応表に無い値がある・対応表が壊れている。理由ごとに 1 行を標準エラーへ出す | +| 標準エラーの行 | `Generated agent missing: <パス>` / `Generated agent is out of date: <パス>` / `Generated agent is stale: <パス>` / `Generated agents must be files, not a symlink: <パス>` / `Unknown model for agy: <パス>: <値>` | + +標準エラーの行は `sync_agy_skill_links` の `Generated link …` と同じ形に揃え、パスはリポジトリの根からの相対で出す。標準出力には何も出さない(`build-runtime-plugins.sh` の最後の 1 行だけが出る)。 + +### model の対応表 + +| Claude Code の値 | agy の値 | 理由 | +| --- | --- | --- | +| `sonnet` | `flash` | 安い型を選んだ意図を、agy の安い tier へ写す | +| `opus` | `pro` | 高い型を選んだ意図を、agy の高い tier へ写す | +| `haiku` | `flash` | 安い型。今は使っていないが、Claude Code の型の名前の残り 1 つとして持つ | +| `inherit` | `inherit` | どちらも「呼び出し元の型を使う」 | +| 欄なし | 欄なし | 欄が無いままにする(agy の既定は `inherit`) | + +型の名前でない値(`claude-opus-5-5` のような型 ID など)は対応表に無く、I5 のとおり exit 1 で止まる。足すときは対応表に 1 行足す。 + +## 処理の流れ + +```mermaid +sequenceDiagram + participant B as build-runtime-plugins.sh + participant L as agy_agents.py + participant S as agents/*.md + participant O as dev.agy/agents/ + B->>L: plugin_dir, check + L->>L: 対応表の値が tier の集合に入るか(I4) + L->>S: 全定義を読む + L->>L: frontmatter の model: の行を置き換えた写しを作る + alt 対応表に無い値(I5) + L-->>B: exit 1 + else check = true + L->>O: symlink でないか・ファイル名の集合・バイト列を突き合わせる(I1・I2) + L-->>B: 食い違いがあれば exit 1 + else check = false + L->>O: symlink なら外す(I6)、ディレクトリを作る + L->>O: 写しを書き、余った写しを消す + L-->>B: exit 0 + end +``` + +写しは、frontmatter(先頭の `---` から次の `---` まで)の中の `model:` で始まる 1 行だけを置き換えて作る。YAML として読み直して書き出さない(I2 のバイト列の一致を保つため。決定 3)。 + +`build-runtime-plugins.sh` の中の順序は、`sync_agy_skill_links` の後に `agy_agents.py` を呼ぶ。`sync_agy_skill_links` は `agents` を扱わなくなるため、2 つの順序に依存は無い。 + +## 非機能の実現方式 + +| 大項目 | 実現方式 | +| --- | --- | +| 運用・保守性 | 正は `plugins/ndf/agents/` の 1 か所。写しの持ち主は生成スクリプトだけで、手直しは `--check`(pre-push と CI の `runtime-plugin-validate.yml`)が I2 で落とす。対応表と tier の集合は `agy_agents.py` の 1 か所に置き、テストもそこを読む | +| システム環境 | agy 1.3.4 と 1.4.7 の両方で、変換した写しが exit 0・11 本になることを設計の時点で実測した(あるべき姿の根拠)。実装後は手元の 1.4.7 と PR の 1.3.4、リリース後は日次の最新版で見る | + +## 決定の記録 + +### 決定 1: Claude Code の定義を変えずに agy だけの値を持たせるため、agy に配る定義を symlink から生成した写しへ変える + +`dev.agy/agents` が `../agents` への symlink のままでは、agy が読む `model` を変えると Claude Code の定義が変わる(前提 2 に反する)。agy の `plugin install` は `dev.agy/` を丸ごと取り込み、取り込むときに中身を変える手段を持たないため、agy の受け付ける値は配布物の時点で入っていなければならない。そこで写しを生成し、コミットする。Kiro の MCP の installer(`dev.kiro/install.sh`)と同じく、生成物を追跡し `--check` で食い違いを見る形である。 + +採らなかった形: Claude Code の定義から `model` を外す(前提 2 に反し、Claude Code の型の選び分けを失う)。pre-push と validate の側で `Invalid model tier` だけを通す(検査を緩めるだけで、利用者の agy に読み込まれない状態が残る。要求の対象範囲で含まない)。 + +根拠: Value 6 / Value 1(MVV 版 3) + +### 決定 2: Claude Code で選び分けた費用の意図を agy でも保つため、`model` は外さずに対応表で tier 名へ変換する(承認ゲート 1 で人が決める) + +Claude Code の定義の `sonnet` / `opus` は、エージェントごとに安い型と高い型を選び分けた値である(前提 2)。agy の `flash` / `pro` は同じく安い tier と高い tier で、変換すればこの選び分けが agy でも保たれる。外す(既定の `inherit`)と、8 本はすべて利用者が選んだ型で動き、`sonnet` を選んだ 7 本も高い型で動きうる。費用を先に置く判断の基準(Value 10)に沿うのは変換である。 + +採らなかった案: `model` の行を外して既定の `inherit` にする。利用者が選んだ型をそのまま使えて対応表も要らないが、上の選び分けを agy で失う。agy 1.4.7 と 1.3.4 の実測ではどちらも通るため、どちらを採っても受け入れ条件 1〜4 は変わらない。人が外す方を選んだときは、対応表の `sonnet` / `opus` / `haiku` の値を `inherit` へ変えるだけで済む(構成要素とテスト設計は変わらない)。 + +根拠: Value 10 / Mission(MVV 版 3) + +### 決定 3: 写しが元の定義と `model` の他で一致することを確かめやすくするため、frontmatter を YAML として読み直さず `model:` の 1 行だけを置き換える + +YAML として読み直して書き出すと、引用符・改行の形・キーの順が書き手の形から変わりうる。受け入れ条件 6 の「`model` の欄の他が一致する」をバイト列で確かめられなくなり、`--check` の比較も読み直した結果どうしの比較になる。1 行の置き換えなら、写しと元の差は `model:` の 1 行だけになり、`diff` でも人が確かめられる。今 `model` を持つ 8 本は、すべて frontmatter の 3 行目に `model: <値>` の 1 行で書いている。 + +採らなかった案: `lib/yamlio.py`(ruamel.yaml)で読み書きする(`codex_skill_policies.py` は読むだけで書き戻さないため、手本にならない)。 + +根拠: Value 3(MVV 版 3) + +### 決定 4: 対応表に無い値を黙って通さないため、型の名前でない `model` は生成を止める + +対応表に無い値を外して `inherit` にすると、開発者が型 ID を書いたときに agy の側だけ黙って型の選び分けが消える。そのまま写すと agy に読み飛ばされ、今の不具合が戻る。止めれば、開発者は対応表に 1 行足すかを決める場面に必ず出会う。 + +採らなかった案: 対応表に無い値は外す(上のとおり黙って意図が消える)。 + +根拠: Value 6 / Value 7(MVV 版 3) + +## テスト設計 + +| 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | +| --- | --- | --- | +| 受け入れ条件 1・2 | 手元の agy 1.4.7 で `agy plugin validate plugins/ndf/dev.agy` が exit 0、`Invalid model tier` が 0 行、集計が `agents 11` | 写しの `model` を `sonnet` に戻すと exit 1・`agents 3` 系になる(設計時の実測と同じ) | +| 受け入れ条件 3 | 包みを `PATH` に置かずに `bash scripts/validate-runtime-plugins.sh` が exit 0 | 生成をし忘れると `--check` の段で exit 1 | +| 受け入れ条件 4 | PR の `runtime-smoke (agy)`(1.3.4)が成功する | —(1.3.4 は `model` を検査しない。設計時に 3 つの形で exit 0 を実測済み) | +| 受け入れ条件 5 | `git diff develop -- plugins/ndf/agents` に `model:` の行の差が無い | 生成が元の定義へ書き込むと差が出る(I6 のテストでも縛る) | +| 受け入れ条件 6・I1 | 一時のツリーで生成した直後は `--check` が 0。定義を 1 本足して生成し直さないと `--check` が 1 で、足りない写しのパスを出す。定義を 1 本消すと余った写しのパスを出す | ファイル名の集合を比べないように壊すと、足した・消した定義で落ちない | +| 受け入れ条件 6・I2 | 定義の本文を 1 文字変えて生成し直さないと `--check` が 1 で、そのパスを出す。写しの本文を手で直しても 1 | 比較を `model` の値だけにすると、本文の食い違いで落ちない | +| 受け入れ条件 7・I3 | リポジトリの `plugins/ndf/dev.agy/agents/*.md` の `model` がすべて tier の集合か欄なし。一時のツリーに `model: sonnet` の定義を 1 本足して生成すると、その写しは `flash` になる | 写しの `model` を `sonnet` にすると落ちる。変換を外すと、足した定義の写しが `sonnet` のままで落ちる | +| I4 | 対応表の値がすべて tier の集合に入る | 対応表の値を tier に無い値にすると、生成が exit 1 で止まる | +| I5 | 対応表に無い値(例: 型 ID)の定義があると、生成も `--check` も exit 1 で、パスと値を出す | 無い値を外す・そのまま写すように壊すと落ちない | +| I6 | 一時のツリーで `dev.agy/agents` を `../agents` への symlink にしてから生成すると、symlink が実体のディレクトリに変わり、元の定義のバイト列は変わらない。`--check` は symlink のときに 1 | symlink を外さずに書くと、元の定義の `model` が書き換わって落ちる | +| 受け入れ条件 8 | `plugins/ndf/README.md` の段落が「生成した写し・`model` を tier 名へ変換」と書く | —(`.md` の文言テストは書かない。レビューで見る) | +| 受け入れ条件 9 | リリース後に日次の `runtime-smoke-latest (agy)` の `agy validate ndf` が成功する | —(リリース後テストで見る) | + +既存の `test_dev_agy_shares_the_implementation` は `scripts` の行だけを残す。`agents` の行を残すと、symlink を前提にしたまま落ちる。 + +## 設計の結果 + +| 課題 | 扱い | 取り込み先 | 触るファイル | +| --- | --- | --- | --- | +| #2087 | 実装する | — | `scripts/build-runtime-plugins.sh`、`scripts/lib/agy_agents.py`、`scripts/tests/test_agy_agents.py`、`scripts/tests/test_agy_distribution.py`、`plugins/ndf/dev.agy/agents/`、`plugins/ndf/README.md`、`docs/specifications/ndf-worker-agent-and-skill-excerpts.md` | + +## 未確認のまま残ること + +| 項目 | 内容 | +| --- | --- | +| `model` を変換するか外すか(未決 1) | 決定 2 で変換を推す。承認ゲート 1 で人が決める。外す方に決まったら、対応表の値を `inherit` へ変えるだけで、ほかの節は変わらない | +| agy の tier の選び方の効き目 | `flash` / `pro` が Claude Code の `sonnet` / `opus` と同じ費用と品質の差を持つかは測っていない。validate と読み込みの成否だけを実測した | +| `agy plugin install` の後の 11 本 | 設計では validate だけを実測した。install と `agy plugin list` は実装後の手元とリリース後テストで見る(受け入れ条件 9 と要求の検証手段) | +| 写しに対する既存の検査の扱い | 写しの `.md` 11 本が新たに追跡される。今の 11 本は相対リンクを持たず、`check-markdown-links.py` など `.md` を走査する検査に掛かる要素は見当たらないが、全体の検査(`check-lint.sh`・全体テスト)は実装で走らせて確かめる | From d57196277d35f458a4328d9f0e27092ad92fe978 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sun, 25 Oct 2026 10:35:06 +0000 Subject: [PATCH 10/17] =?UTF-8?q?docs(design):=20#2104=20=E6=98=87?= =?UTF-8?q?=E6=A0=BC=E3=81=AE=E6=89=BF=E8=AA=8D=E8=B3=87=E6=96=99=E3=81=AE?= =?UTF-8?q?=E6=8C=81=E3=81=A1=E4=B8=BB=E3=82=92=E8=A1=8C=E3=81=94=E3=81=A8?= =?UTF-8?q?=E3=81=AB=E6=9B=B8=E3=81=8D=E3=80=81mvv-gate.py=20=E3=81=AE?= =?UTF-8?q?=E6=89=BF=E8=AA=8D=E3=81=AE=E8=A8=98=E9=8C=B2=E3=81=AE=E8=A1=8C?= =?UTF-8?q?=E3=82=92=E8=B6=B3=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- issues/issue-2104-design.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/issues/issue-2104-design.md b/issues/issue-2104-design.md index efd6a9252..d9efa2354 100644 --- a/issues/issue-2104-design.md +++ b/issues/issue-2104-design.md @@ -37,18 +37,18 @@ | 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | | --- | --- | --- | --- | --- | -| 昇格の承認資料 | `merged_lib/merge.py` の `gate_stop`(`normal`)と `_prepare_promote`(`fast` / `auto`)が書く。行の形は `lib/approved_commit.py` の `material_row` / `material_sha` | `{state_dir}/work/approval-promote.md` | — | 承認したコミット(40 桁)・判定の行 | +| 昇格の承認資料 | 資料の本文と「承認したコミット」の行は `merged_lib/merge.py` の `gate_stop`(`normal`)と `_prepare_promote`(`fast` / `auto`)が書く。「承認の記録」(`by: mvv`)の行だけは `fast` / `auto` の `mvv` ステップの `mvv-gate.py check --gate release --material` が書く。行の形は `lib/approved_commit.py` の `material_row` / `material_sha` | `{state_dir}/work/approval-promote.md` | — | 承認したコミット(40 桁)・判定の行 | | 昇格のマージ | `merged_lib/merge.py` の `promote` | 昇格の Pull Request | — | 先端(`headRefOid`)・照合する SHA(`expect_head`) | | 昇格のプラン | `supervise_lib/delivery_templates.py` の `plan_promote` | プラン | ステップ(`promote`・`prepare`・`promote-approved`) | 昇格の承認資料のパス(`PROMOTE_MATERIAL`) | -昇格のマージは、承認資料をパスで参照して読むだけで書き換えない。承認資料の書き手は 2 つあるが、`pace` ごとにどちらか 1 つだけが同じプランの中で書く(`normal` は `gate_stop`、`fast` / `auto` は `prepare`)。 +昇格のマージは、承認資料をパスで参照して読むだけで書き換えない。承認資料の持ち主は行ごとに決まる。資料の本文と「承認したコミット」の行は、`pace` ごとにどちらか 1 つだけが同じプランの中で書く(`normal` は `gate_stop`、`fast` / `auto` は `prepare`)。「承認の記録」(`by: mvv`)の行は `fast` / `auto` の `mvv-gate.py` だけが「承認したコミット」の行の直後へ書き、ほかの行を書き換えない(`normal` では書かれない)。そのため `fast` / `auto` の handoff の後に利用者が見る資料には `by: mvv` の承認の記録が残る。 ### 不変条件 | # | 集約 | 条件 | 破れたときの扱い | | --- | --- | --- | --- | | I1 | 昇格の承認資料 | `promote` が昇格の Pull Request で承認ゲート 2 に止まる(終了コード 10)とき、承認資料の「2. 承認の判断に使うもの」の表に、止まる判定に使ったのと同じ 1 回の読みの `headRefOid` が `\| 承認したコミット \| <40 桁> \|` の 1 行で載り、結果の `metrics.approved_sha` と gate の `items[].head` も同じ値である | 先端が 40 桁の 16 進で読めなければ資料を書かずに終了コード 2(`EXIT_UNREADABLE`)で止まる。マージしない | -| I2 | 昇格の承認資料 | `fast` / `auto` の `prepare`(`--prepare`)が書く承認資料も、I1 と同じ形の「承認したコミット」の行を 1 行持つ | I1 と同じ | +| I2 | 昇格の承認資料 | `fast` / `auto` の `prepare`(`--prepare`)が書く承認資料も、I1 と同じ形の「承認したコミット」の行を 1 行持つ。続く `mvv-gate.py` がその直後へ「承認の記録」(`by: mvv`)の行を足した後も、「承認したコミット」の行は 1 行のままで値は変わらない | I1 と同じ | | I3 | 昇格のマージ | `--approved-from <資料>` を受けた `promote` は、昇格の Pull Request の先端が資料の承認したコミットに等しいときだけ `gh pr merge` を呼び、`--match-head-commit` にその SHA を渡す | 読むたびに比べ、違えば `gh pr merge` を呼ばずに `items[].result: head_moved`(`head` と `expected` つき)・終了コード 1 で止まり、`next` に承認ゲート 2 からのやり直しを書く | | I4 | 昇格のマージ | `--approved-from` を受けた `promote` は、資料から承認したコミットを読めないまま照合を飛ばさない | `gh` を 1 度も呼ばずに終了コード 2 で止まる | | I5 | 昇格のマージ | `promote` は `--gate-approved user` を `--approved-from` 無しで受けない(照合の材料の無い利用者の承認でマージしない) | `gh` を 1 度も呼ばずに終了コード 2 で止まり、`next` に資料つきの打ち方を書く | From 91ff98f9f0126c71676e081c13b5d5f7bdc85bbd Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sun, 25 Oct 2026 10:36:10 +0000 Subject: [PATCH 11/17] =?UTF-8?q?docs(design):=20#2087=20=E3=81=AE?= =?UTF-8?q?=E8=A7=A6=E3=82=8B=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=AB?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E8=A3=9C=E5=8A=A9=20agy=5Fbuild=5Fh?= =?UTF-8?q?elpers.py=20=E3=82=92=E8=B6=B3=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一時ツリーへ生成スクリプトだけを複製する build_tree では agy_agents.py が 見つからず生成テストが落ちるため、構成要素・構成・設計の結果へ足す。 Co-Authored-By: Claude Opus 5.5 --- issues/issue-2087-design.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/issues/issue-2087-design.md b/issues/issue-2087-design.md index 8be0416e4..e7c88e1c4 100644 --- a/issues/issue-2087-design.md +++ b/issues/issue-2087-design.md @@ -87,6 +87,7 @@ agy に配る定義の集合は、エージェント定義の集合と model の | `scripts/build-runtime-plugins.sh`(変更) | `sync_agy_skill_links` の symlink の一覧から `agents` を外し、`agy_agents.py` を呼ぶ(`codex_skill_policies.py` と同じ呼び方)。冒頭の usage と、共有を説明するコメントを直す | | `plugins/ndf/dev.agy/agents/*.md`(生成物・11 本) | agy に配る定義。symlink 1 本から実体のファイル 11 本へ変わる | | `scripts/tests/test_agy_agents.py`(新規) | 不変条件 I1〜I6 と受け入れ条件 6・7 を agy なしで縛る | +| `scripts/tests/agy_build_helpers.py`(変更) | `build_tree` が一時ツリーへ `build-runtime-plugins.sh` に加えて `scripts/lib/agy_agents.py` も複製する。複製しないと、生成スクリプトが `agy_agents.py` を見つけられず、一時ツリーで生成する既存と新設のテストが落ちる | | `scripts/tests/test_agy_distribution.py`(変更) | `test_dev_agy_shares_the_implementation` の `agents` の行を外す(`scripts` の行は残す) | | `plugins/ndf/README.md`(変更) | 37 行目の段落を新しい形に合わせる(受け入れ条件 8) | | `docs/specifications/ndf-worker-agent-and-skill-excerpts.md`(変更) | 64 行目の「`../agents` への symlink で同じ定義が配られる」を新しい形に合わせる | @@ -130,6 +131,7 @@ scripts/ │ ├── codex_skill_policies.py # 既存(呼び方の手本) │ └── agy_agents.py # 新規 └── tests/ + ├── agy_build_helpers.py # 変更: 一時ツリーへ agy_agents.py も複製する ├── test_agy_agents.py # 新規 └── test_agy_distribution.py # 変更 plugins/ndf/ @@ -262,7 +264,7 @@ YAML として読み直して書き出すと、引用符・改行の形・キー | 課題 | 扱い | 取り込み先 | 触るファイル | | --- | --- | --- | --- | -| #2087 | 実装する | — | `scripts/build-runtime-plugins.sh`、`scripts/lib/agy_agents.py`、`scripts/tests/test_agy_agents.py`、`scripts/tests/test_agy_distribution.py`、`plugins/ndf/dev.agy/agents/`、`plugins/ndf/README.md`、`docs/specifications/ndf-worker-agent-and-skill-excerpts.md` | +| #2087 | 実装する | — | `scripts/build-runtime-plugins.sh`、`scripts/lib/agy_agents.py`、`scripts/tests/test_agy_agents.py`、`scripts/tests/test_agy_distribution.py`、`scripts/tests/agy_build_helpers.py`、`plugins/ndf/dev.agy/agents/`、`plugins/ndf/README.md`、`docs/specifications/ndf-worker-agent-and-skill-excerpts.md` | ## 未確認のまま残ること From a866e232a627349507bf6c541ef6c370812f9341 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 27 Oct 2026 10:06:45 +0000 Subject: [PATCH 12/17] =?UTF-8?q?Add:=20=E5=88=A9=E7=94=A8=E8=80=85?= =?UTF-8?q?=E3=81=AE=20Chrome=20=E3=82=92=E9=96=8B=E7=99=BA=E3=82=B3?= =?UTF-8?q?=E3=83=B3=E3=83=86=E3=83=8A=E3=81=8B=E3=82=89=20CDP=20=E3=81=A7?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E3=81=99=E3=82=8B=20Skill=20chrome-cdp=20?= =?UTF-8?q?=E3=82=92=E8=B6=B3=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ログインと目視は利用者の Chrome で行い、大量の操作は Cookie を取り出して コンテナの headless で回す。リモートのホスト(EC2 など)の上のコンテナからは、 SSH の RemoteForward とホストの中継(socat)で届かせる。9222 をネットワークへ 開かないこと、対象の origin の Cookie だけを取ることを決まりにする。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KYHP7zFtYgAYtprVz6q6a2 --- .claude-plugin/marketplace.json | 2 +- README.md | 6 +- docs/ndf-plugin-reference.md | 14 +- plugins/ndf/.claude-plugin/plugin.json | 3 +- plugins/ndf/.codex-plugin/plugin.json | 3 +- plugins/ndf/README.md | 12 +- plugins/ndf/dev.agy/plugin.json | 2 +- plugins/ndf/dev.agy/skills/chrome-cdp | 1 + plugins/ndf/manifests/agy-skills.txt | 1 + plugins/ndf/manifests/claude-skills.txt | 1 + plugins/ndf/manifests/codex-skills.txt | 1 + plugins/ndf/manifests/kiro-skills.txt | 1 + plugins/ndf/skills/chrome-cdp/SKILL.md | 157 ++++++++++++++++++ .../chrome-cdp/references/local-side-setup.md | 67 ++++++++ scripts/tests/test_agy_distribution.py | 2 +- 15 files changed, 252 insertions(+), 21 deletions(-) create mode 120000 plugins/ndf/dev.agy/skills/chrome-cdp create mode 100644 plugins/ndf/skills/chrome-cdp/SKILL.md create mode 100644 plugins/ndf/skills/chrome-cdp/references/local-side-setup.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index d26d4fcac..efec42665 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.22.0): 8 specialized agents (plus 1 worker agent for the 3-layer workflow) and 48 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.22.0): 8 specialized agents (plus 1 worker agent for the 3-layer workflow) and 49 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/README.md b/README.md index 987977e04..0a250bb3d 100644 --- a/README.md +++ b/README.md @@ -89,12 +89,12 @@ Claude Code では `/ndf:` と打って呼びます。**Skill** は ### NDF プラグインの Skill の一覧 **NDFプラグイン v10.22.0** に入る Skill の数は、AI ツールごとに違います。 -Claude Code向け core 48個、Kiro向け core 45個、Codex向け core 44個、agy向け core 44個です。 -元Skills(48個)の内訳は次のとおりです。 +Claude Code向け core 49個、Kiro向け core 46個、Codex向け core 45個、agy向け 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 - 原則と書き方 (11): ndf-policies, implementation-plan, plan-to-spec, investigation-rules, decision-request, problem-solving, logging-guidelines, document-style, notion-writing, issue-plan-strategy, ml-model-structure - - 品質と実行環境 (3): qa-security-scan, docker-container-access, official-skills-autoloader + - 品質と実行環境 (4): qa-security-scan, docker-container-access, chrome-cdp, official-skills-autoloader - 外部サービス (2): google-workspace, document-systems - 複数の AI によるレビューとリファクタリング (3): cross-review, cross-refactoring, external-ai - 作業場所 (1): worktree diff --git a/docs/ndf-plugin-reference.md b/docs/ndf-plugin-reference.md index 37ff524cb..d5e790f5b 100644 --- a/docs/ndf-plugin-reference.md +++ b/docs/ndf-plugin-reference.md @@ -6,10 +6,10 @@ NDF は Claude Code / Codex / Kiro CLI 向けの開発支援プラグイン群 | ランタイム | 導入方法 | 公開 Skill | |---|---|---| -| Claude Code | `.claude-plugin/marketplace.json` の `ndf` | 48 個 | -| Codex | `.claude-plugin/marketplace.json` の `ndf` | 44 個 | -| Kiro CLI | `plugins/ndf/dev.kiro/install.sh` | 45 個 | -| agy | `plugins/ndf/dev.agy/` を `agy plugin install` | 44 個 | +| 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/` を直接編集します。 @@ -20,7 +20,7 @@ Skill の実体は `plugins/ndf/skills/` の 1 箇所だけです。ランタイ plugins/ndf/ ├── .claude-plugin/plugin.json # Claude Code(agents / hooks / skills 配列) ├── .codex-plugin/plugin.json # Codex(hooks / skills 配列) -├── skills/ # 配布 Skill の唯一の実体(48 個) +├── skills/ # 配布 Skill の唯一の実体(49 個) ├── manifests/ # ランタイム別の配布 Skill 一覧 ├── agents/ # Claude Code のサブエージェント(専門 8 個と、3 層の定義 3 個(supervisor 2・worker 1)) ├── hooks/claude.json # Claude Code の SessionStart / Stop hook @@ -37,8 +37,8 @@ plugin install 時に build を実行する必要はありません。 | Runtime | 読むもの | |---|---| -| Claude Code | `.claude-plugin/plugin.json`(`agents/` 11 個(専門 8 個と、3 層の定義 3 個(supervisor 2・worker 1))、`hooks/claude.json`、`skills` 配列 48 個)、`scripts/` | -| Codex | `.codex-plugin/plugin.json`(`hooks/codex.json`、`skills` 配列 44 個)、`scripts/` | +| 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・`agents` と `scripts` への symlink) | diff --git a/plugins/ndf/.claude-plugin/plugin.json b/plugins/ndf/.claude-plugin/plugin.json index 340442aaf..2aa3004cf 100644 --- a/plugins/ndf/.claude-plugin/plugin.json +++ b/plugins/ndf/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ndf", "version": "10.22.0", - "description": "Claude Code plugin (v10.22.0): 8 specialized agents (plus 2 supervisor agents and 1 worker agent for the 3-layer workflow) and 48 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.22.0): 8 specialized agents (plus 2 supervisor agents and 1 worker agent for the 3-layer workflow) and 49 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" @@ -42,6 +42,7 @@ "./skills/document-style", "./skills/notion-writing", "./skills/docker-container-access", + "./skills/chrome-cdp", "./skills/implementation-plan", "./skills/layout-review", "./skills/investigation-rules", diff --git a/plugins/ndf/.codex-plugin/plugin.json b/plugins/ndf/.codex-plugin/plugin.json index 861baa401..dac9cd22c 100644 --- a/plugins/ndf/.codex-plugin/plugin.json +++ b/plugins/ndf/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ndf", "version": "10.22.0", - "description": "Codex plugin (v10.22.0): 44 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.", + "description": "Codex plugin (v10.22.0): 45 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", @@ -9,6 +9,7 @@ "./skills/deploy", "./skills/development-workflow", "./skills/docker-container-access", + "./skills/chrome-cdp", "./skills/external-ai", "./skills/fix", "./skills/implementation-plan", diff --git a/plugins/ndf/README.md b/plugins/ndf/README.md index 768a62d98..bf9f26b76 100644 --- a/plugins/ndf/README.md +++ b/plugins/ndf/README.md @@ -8,10 +8,10 @@ PR 運用、レビュー、調査、実装計画、仕様書化、開発方法 | ランタイム | 公開 Skill | マニフェスト | | --- | --- | --- | -| Claude Code | 48 個 | `.claude-plugin/plugin.json` | -| Codex | 44 個 | `.codex-plugin/plugin.json` | -| Kiro CLI | 45 個 | `dev.kiro/install.sh`(プラグイン機構が無いため installer で導入) | -| agy | 44 個 | `dev.agy/plugin.json`(取得元の登録が無いため clone から導入) | +| 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 から導入) | ## レイアウト @@ -19,7 +19,7 @@ PR 運用、レビュー、調査、実装計画、仕様書化、開発方法 plugins/ndf/ ├── .claude-plugin/plugin.json # Claude Code のマニフェスト ├── .codex-plugin/plugin.json # Codex のマニフェスト -├── skills/ # 配布 Skill の唯一の実体(48 個) +├── skills/ # 配布 Skill の唯一の実体(49 個) ├── skills/AUTHORING.md # Skill 執筆の規約 ├── manifests/ # ランタイム別の配布 Skill 一覧 ├── agents/ # Claude Code のサブエージェント定義(専門 8 個と、3 層の定義 3 個(supervisor 2・worker 1)) @@ -102,7 +102,7 @@ agy plugin install plugins/ndf/dev.agy # 初回 agy plugin uninstall ndf && agy plugin install plugins/ndf/dev.agy # 新しい版へ ``` -導入すると `manifests/agy-skills.txt` に載る Skill 44 個と、エージェント 11 個(専門 8 個と、3 層の定義 3 個(supervisor 2・worker 1))、hook 1 個が +導入すると `manifests/agy-skills.txt` に載る Skill 45 個と、エージェント 11 個(専門 8 個と、3 層の定義 3 個(supervisor 2・worker 1))、hook 1 個が `~/.gemini/config/plugins/ndf/` へコピーされます。symlink は実体へ解決されてコピーされるため、 clone を消しても導入した内容は残ります。 diff --git a/plugins/ndf/dev.agy/plugin.json b/plugins/ndf/dev.agy/plugin.json index 6e4a21bfe..41479cb15 100644 --- a/plugins/ndf/dev.agy/plugin.json +++ b/plugins/ndf/dev.agy/plugin.json @@ -1,5 +1,5 @@ { "name": "ndf", "version": "10.22.0", - "description": "Antigravity CLI plugin (v10.22.0): 44 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation, and worktree guidance hooks." + "description": "Antigravity CLI plugin (v10.22.0): 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/skills/chrome-cdp b/plugins/ndf/dev.agy/skills/chrome-cdp new file mode 120000 index 000000000..3c83baec8 --- /dev/null +++ b/plugins/ndf/dev.agy/skills/chrome-cdp @@ -0,0 +1 @@ +../../skills/chrome-cdp \ No newline at end of file diff --git a/plugins/ndf/manifests/agy-skills.txt b/plugins/ndf/manifests/agy-skills.txt index ad4e01093..4b6339464 100644 --- a/plugins/ndf/manifests/agy-skills.txt +++ b/plugins/ndf/manifests/agy-skills.txt @@ -4,6 +4,7 @@ cross-review deploy development-workflow docker-container-access +chrome-cdp external-ai fix implementation-plan diff --git a/plugins/ndf/manifests/claude-skills.txt b/plugins/ndf/manifests/claude-skills.txt index 01a333f2d..2947b2312 100644 --- a/plugins/ndf/manifests/claude-skills.txt +++ b/plugins/ndf/manifests/claude-skills.txt @@ -9,6 +9,7 @@ ndf-policies document-style notion-writing docker-container-access +chrome-cdp implementation-plan layout-review investigation-rules diff --git a/plugins/ndf/manifests/codex-skills.txt b/plugins/ndf/manifests/codex-skills.txt index ad4e01093..4b6339464 100644 --- a/plugins/ndf/manifests/codex-skills.txt +++ b/plugins/ndf/manifests/codex-skills.txt @@ -4,6 +4,7 @@ cross-review deploy development-workflow docker-container-access +chrome-cdp external-ai fix implementation-plan diff --git a/plugins/ndf/manifests/kiro-skills.txt b/plugins/ndf/manifests/kiro-skills.txt index fd8e089d0..483b9a15f 100644 --- a/plugins/ndf/manifests/kiro-skills.txt +++ b/plugins/ndf/manifests/kiro-skills.txt @@ -9,6 +9,7 @@ ndf-policies document-style notion-writing docker-container-access +chrome-cdp implementation-plan layout-review investigation-rules diff --git a/plugins/ndf/skills/chrome-cdp/SKILL.md b/plugins/ndf/skills/chrome-cdp/SKILL.md new file mode 100644 index 000000000..255b5f5ac --- /dev/null +++ b/plugins/ndf/skills/chrome-cdp/SKILL.md @@ -0,0 +1,157 @@ +--- +name: chrome-cdp +description: "Connect from a dev container to the user's own desktop Chrome over CDP, for login and visual checks, then run tests headless with its cookies. Use when the browser must be the user's Chrome(手元のChromeに接続・ローカルChromeを操作・ログイン済みのブラウザを使う)." +allowed-tools: + - Read + - Write + - Bash +--- + +# 利用者の Chrome を開発コンテナから操作する + +開発コンテナの中のエージェントが、**利用者の PC で動いている Chrome** を CDP(Chrome DevTools Protocol)で操作する手順。 + +**使う場面:** 認証のある画面(管理画面・SSO)を確かめるとき。ログインは利用者がいつもの画面で行い、エージェントはその Chrome を見る・操作する。 + +**使わない場面:** 認証の要らないページの自動テスト。コンテナの中の headless の Chromium で足りる。 + +## 方針: ログインは利用者の Chrome、大量の操作はコンテナで + +CDP の操作は 1 回ごとに利用者の PC と往復する。リモートの開発環境では数十 ms の往復が積み重なり、数百回クリックするテストは目に見えて遅くなる。そこで役割を分ける。 + +| 何を | どこで | +| --- | --- | +| ログイン・画面の目視・少数の操作 | 利用者の Chrome(CDP) | +| E2E のような大量の操作 | コンテナの headless の Chromium。利用者の Chrome から**対象の origin の Cookie だけ**を取り出して `storageState` として渡す | +| テスト結果の表示 | `playwright show-report` などを、エディタのポート転送でコンテナから利用者の PC へ出す | + +## 1. 経路を決める + +まずコンテナから利用者の PC へ届く経路を判定する。 + +```bash +getent hosts host.docker.internal && echo "経路A の候補" +cat /proc/net/route | awk 'NR>1 && $2=="00000000"{print "既定ゲートウェイ(16進):", $3}' +env | grep -qE '^REMOTE_CONTAINERS=' && echo "Dev Containers の中" +``` + +| 経路 | 当たる環境 | 利用者の PC までの道 | +| --- | --- | --- | +| A. 同じ PC の Docker | Docker Desktop(macOS / Windows)の上のコンテナ。`host.docker.internal` が引ける | コンテナ → `host.docker.internal:9222` → Chrome | +| B. リモートのホスト | クラウドの VM(EC2 など)の上のコンテナに、エディタが SSH でつないでいる。`host.docker.internal` が引けない | Chrome ← SSH の `RemoteForward` ← ホストの `127.0.0.1:9222` ← 中継コンテナ ← コンテナ | + +どちらにも当たらない(ホストへ SSH していない・PC から VM へ届く経路が無い)ときは、ここで止めて利用者に構成を尋ねる。**Tailscale などで 9222 をネットワークへ開く案は採らない**(下の「安全のための決まり」)。 + +## 2. 利用者の PC 側の設定(利用者が行う) + +利用者に頼む。利用者の PC で動くエージェント(WSL の Claude など)がいるなら、[references/local-side-setup.md](references/local-side-setup.md) をそのまま指示書として渡す。 + +### Chrome を専用プロファイルで起動する + +**Chrome 136 以降は、既定のプロファイルではリモート操作の口が開かない。** 専用の `--user-data-dir` で別の Chrome を起動する。ログインは初回だけ要る。 + +``` +# Windows +"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir=%LOCALAPPDATA%\chrome-cdp +# macOS +open -na "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir="$HOME/.chrome-cdp" +``` + +### 経路 B だけ: SSH の転送を足す + +エディタ(VS Code の Remote-SSH など)が使う SSH の設定の、そのホストの節に 1 行足す。**SSH の接続を張り直すまで効かない**(ウィンドウを閉じて開き直す)。 + +``` +Host <リモートのホスト> + RemoteForward 9222 127.0.0.1:9222 +``` + +**エディタのポート転送では代わりにならない。** あれはコンテナのポートを PC へ見せる向きで、PC のポートをコンテナへ届ける向きを持たない。 + +## 3. 経路 B だけ: ホストに中継を立てる + +sshd の既定(`GatewayPorts no`)では、転送はホストの `127.0.0.1` にしか開かない。ブリッジネットワークのコンテナからは届かないので、ホストのネットワークで動く中継を 1 つ立てる。docker のソケットが使えるときは次で足りる。 + +```bash +# コンテナが属するネットワークのゲートウェイ(ホスト側の口) +GW=$(docker network inspect "$(docker inspect "$(hostname)" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}')" --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}') +docker run -d --name cdp-relay --restart unless-stopped --network host alpine/socat \ + TCP-LISTEN:9222,bind="$GW",fork,reuseaddr TCP:127.0.0.1:9222 +``` + +- `bind` はブリッジのゲートウェイに限る。`0.0.0.0` にしない(VM の外へ 9222 が開く) +- docker のソケットが使えないときは、ホストで同じ socat を動かしてもらうか、sshd に `GatewayPorts clientspecified` を足して `RemoteForward <ゲートウェイ>:9222 127.0.0.1:9222` にしてもらう。どちらもホストの管理者に頼む +- 中継は複数のコンテナで共有してよい。既にあれば作り直さない(`docker ps --filter name=cdp-relay`) + +## 4. つながったか確かめる + +```bash +CDP_HOST=host.docker.internal # 経路 B なら上の $GW +curl -s -m 5 "http://$CDP_HOST:9222/json/version" -H 'Host: localhost:9222' +``` + +**Host ヘッダを `localhost:9222` に上書きする。** Chrome は Host が IP でも localhost でもない接続を拒む(`Host header is specified and is not an IP address or localhost`)。WebSocket の接続も同じ。 + +| 応答 | 意味 | +| --- | --- | +| `Browser` を含む JSON | つながった | +| 空の応答・`socket hang up` | 経路 B で、中継は生きているが、その先(SSH の転送か Chrome)が無い。接続の張り直しと Chrome の起動を利用者に確かめてもらう | +| 接続できない・名前が引けない | 経路の判定が違う。1 へ戻る | + +## 5. 操作する(Playwright) + +Node の `fetch` は Host ヘッダを変えられないので、エンドポイントは `http.get`(または `curl`)で取る。取った WebSocket の URL のホストを、届く側の名前へ差し替える。 + +```js +const http = require('http'); +const { chromium } = require('playwright'); // または '@playwright/test' + +const CDP = { host: process.env.CDP_HOST, port: 9222 }; +const HOST = 'localhost:9222'; + +function wsEndpoint() { + return new Promise((resolve, reject) => { + http.get({ ...CDP, path: '/json/version', headers: { Host: HOST }, timeout: 5000 }, (res) => { + let body = ''; + res.on('data', (c) => { body += c; }); + res.on('end', () => { + const ws = new URL(JSON.parse(body).webSocketDebuggerUrl); + ws.hostname = CDP.host; + resolve(ws.toString()); + }); + }).on('error', reject); + }); +} + +const browser = await chromium.connectOverCDP(await wsEndpoint(), { headers: { Host: HOST } }); +const context = browser.contexts()[0]; // 利用者のログイン状態を持つ文脈 +const page = await context.newPage(); // 利用者のタブは触らず、新しいタブで操作する +// ... +await browser.close(); // CDP の接続を切るだけ。利用者の Chrome は閉じない +``` + +- 画面を利用者に見せたいときは `await page.bringToFront()` +- **Chrome DevTools の MCP は、手元に Chrome の実行ファイルを探して起動しようとする。** リモートの Chrome へ向けられない構成では使えないので、上の Playwright で操作する + +### Cookie を取り出して headless で回す + +```js +const origin = new URL(process.env.TARGET_URL).origin; +const cookies = await context.cookies(origin); // 対象の origin だけを取る +fs.writeFileSync('.auth/state.json', JSON.stringify({ cookies, origins: [] })); +``` + +Playwright のテストでは `use: { storageState: '.auth/state.json' }` で読ませる。`.auth/` はリポジトリに入れない(`.gitignore` に足す)。セッションの期限が切れたら、利用者に Chrome でログインし直してもらい、取り出し直す。 + +## 安全のための決まり + +- **9222 をネットワークへ開かない。** CDP には認証が無く、届けば誰でもログイン済みのブラウザを操作できる。Chrome の待ち受けは `127.0.0.1` のまま、`--remote-debugging-address=0.0.0.0`・Windows の `netsh portproxy`・Tailscale での公開はしない +- **見るのは対象の origin だけ。** `context.cookies()` を引数なしで呼ぶと、利用者の全サイトの Cookie(名前と値)が取れてしまう。一覧を出力しない。必要な origin を渡す +- **利用者のタブを閉じない・移動しない。** 操作は `newPage()` で開いたタブで行い、終わったら接続だけを切る +- 共有の検証環境では、書き込み(保存・送信・削除)を伴う操作は利用者に確かめてから行う + +## 完了報告に書くこと + +- 使った経路(A / B)と、立てた・使った中継(名前と待ち受けアドレス) +- 取り出した Cookie の origin と件数(値は書かない) +- 利用者に頼んだ設定と、まだ済んでいないもの diff --git a/plugins/ndf/skills/chrome-cdp/references/local-side-setup.md b/plugins/ndf/skills/chrome-cdp/references/local-side-setup.md new file mode 100644 index 000000000..4daae99f9 --- /dev/null +++ b/plugins/ndf/skills/chrome-cdp/references/local-side-setup.md @@ -0,0 +1,67 @@ +# 指示書: 利用者の PC 側の設定(PC で動くエージェントへ渡す) + +この文書は、**利用者の PC で動くエージェント**(Windows の WSL の Claude など)へそのまま渡す。目的は、リモートのホストの上の開発コンテナから、この PC の Chrome を CDP で操作できるようにすること。コンテナ側(中継)の準備は、コンテナ側のエージェントが済ませている。 + +## 全体の形 + +``` +この PC の Chrome(127.0.0.1:9222) + ↑ SSH の RemoteForward(エディタの Remote-SSH の接続に相乗りする) +リモートのホストの 127.0.0.1:9222 + ↑ 中継コンテナ cdp-relay(ブリッジのゲートウェイで受ける) +開発コンテナ +``` + +## やること + +### 1. エディタが使っている SSH の設定ファイルと Host を特定する + +- Windows の VS Code の Remote-SSH は、普通は **Windows の OpenSSH** と `C:\Users\<名前>\.ssh\config` を使う。WSL の `~/.ssh/config` ではない +- VS Code の設定 `remote.SSH.configFile` と `remote.SSH.path` を見る(WSL からは `/mnt/c/Users/<名前>/AppData/Roaming/Code/User/settings.json`)。指定が無ければ既定の場所 +- その設定の中で、リモートのホストを指す `Host` の節(VS Code のリモートエクスプローラーに出ている名前と一致するもの)を探す +- macOS なら `~/.ssh/config` + +### 2. その Host の節に 1 行足す + +``` + RemoteForward 9222 127.0.0.1:9222 +``` + +- 先に `config.bak-<日付>` としてバックアップを取る +- 既存の行は変えない。改行コード(Windows は CRLF)と文字コードを保つ +- 既に `RemoteForward 9222` があれば足さない + +### 3. CDP 用の Chrome を起動する手段を用意する + +Chrome 136 以降は、**既定のプロファイルではリモート操作の口が開かない。** 専用のプロファイルで別に起動する。 + +``` +"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir=%LOCALAPPDATA%\chrome-cdp +``` + +- Windows はデスクトップに「Chrome (CDP)」のショートカットを作る。WSL からは `powershell.exe -NoProfile -Command` で `WScript.Shell` の `CreateShortcut` を呼ぶ +- Chrome の場所が違えば `C:\Program Files (x86)\...` や `%LOCALAPPDATA%\Google\Chrome\Application\` を探す +- macOS は `open -na "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir="$HOME/.chrome-cdp"` + +### 4. 確かめる + +1. CDP 用の Chrome を起動する +2. PC の側で 9222 が開いたことを確かめる。Windows は `powershell.exe -NoProfile -Command "(Invoke-WebRequest -UseBasicParsing http://127.0.0.1:9222/json/version).Content"` で `webSocketDebuggerUrl` を含む JSON が返る(WSL の `curl localhost:9222` は WSL のネットワークの設定によっては Windows に届かないので、判定に使わない) +3. 利用者に頼む(このエージェントにはできない) + - **エディタのウィンドウを閉じて、リモートのホストへ接続し直す**(転送は SSH の接続を張り直すまで効かない) + - CDP 用の Chrome で、対象のサイトにログインする +4. 利用者に「コンテナ側のエージェントに、PC 側の設定が済んだと伝えてください」と案内して終える + +## やらないこと + +- 9222 をネットワークへ開く設定(`--remote-debugging-address=0.0.0.0`・`netsh portproxy`・ファイアウォールの許可・Tailscale での公開)。CDP には認証が無く、届けば誰でもログイン済みのブラウザを操作できる +- いつものプロファイルに `--remote-debugging-port` を付けること +- SSH の設定の、`RemoteForward` 以外の行の変更 + +## うまくいかないとき + +| 症状 | 見るところ | +| --- | --- | +| 9222 が開かない | 同じ `--user-data-dir` の Chrome が既に動いていないか。CDP 用だけを閉じて起動し直す | +| 接続のときに `remote port forwarding failed for listen port 9222` | ホストの 9222 を、残っている古い SSH の接続が使っている。古いエディタのウィンドウを閉じる | +| コンテナ側で `socket hang up` | SSH の転送か Chrome のどちらかが無い。2 と 3 を確かめる | diff --git a/scripts/tests/test_agy_distribution.py b/scripts/tests/test_agy_distribution.py index dcc480def..e1faf292f 100644 --- a/scripts/tests/test_agy_distribution.py +++ b/scripts/tests/test_agy_distribution.py @@ -38,7 +38,7 @@ # 4 つとも同じ 5 だけ増えている(#116 / #144)。Google の 2 つの Skill を `google-workspace` # の 1 つへまとめたため、4 つとも同じ 1 だけ減っている(#744)。見積もりの `estimate` を # 4 つとも足したため、同じ 1 だけ増えている(#1856)。 -EXPECTED_COUNTS = {"claude": 48, "codex": 44, "kiro": 45, "agy": 44} +EXPECTED_COUNTS = {"claude": 49, "codex": 45, "kiro": 46, "agy": 45} def manifest_names(runtime: str) -> list[str]: From 5d5aa5f578212bf1e41f12e85fc2539b8bec5fef Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 27 Oct 2026 15:08:29 +0000 Subject: [PATCH 13/17] =?UTF-8?q?Update:=20chrome-cdp=20=E3=81=AB=E3=80=81?= =?UTF-8?q?=E3=83=96=E3=83=A9=E3=82=A6=E3=82=B6=E5=85=A8=E4=BD=93=E3=81=B8?= =?UTF-8?q?=E3=81=AE=E5=91=BD=E4=BB=A4=E3=81=8C=E6=8B=92=E3=81=BE=E3=82=8C?= =?UTF-8?q?=E3=82=8B=20Chrome=20=E3=81=8B=E3=82=89=E3=82=BF=E3=83=96?= =?UTF-8?q?=E5=8D=98=E4=BD=8D=E3=81=A7=20Cookie=20=E3=82=92=E8=AA=AD?= =?UTF-8?q?=E3=82=80=E6=89=8B=E9=A0=86=E3=82=92=E8=B6=B3=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KYHP7zFtYgAYtprVz6q6a2 --- plugins/ndf/skills/chrome-cdp/SKILL.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/plugins/ndf/skills/chrome-cdp/SKILL.md b/plugins/ndf/skills/chrome-cdp/SKILL.md index 255b5f5ac..684bbfe6a 100644 --- a/plugins/ndf/skills/chrome-cdp/SKILL.md +++ b/plugins/ndf/skills/chrome-cdp/SKILL.md @@ -141,6 +141,23 @@ const cookies = await context.cookies(origin); // 対象の origin だ fs.writeFileSync('.auth/state.json', JSON.stringify({ cookies, origins: [] })); ``` +#### `Browser context management is not supported` で拒まれたとき + +Chrome の起動のしかたによっては、ブラウザ全体への命令が拒まれる。`connectOverCDP` は接続の直後に `Browser.setDownloadBehavior` を送るので、そこで失敗する。`Storage.getCookies` も同じ理由で通らない。 + +**タブ(page)単位につなぎ、`Network.getCookies` に対象の URL を渡す。** この経路なら通る。URL を渡すので、ほかのサイトの Cookie は返らない。 + +```js +const WebSocket = require('ws'); // Node 標準の WebSocket は Host を変えられない +const page = (await getJson('/json/list')).find((t) => t.type === 'page'); // getJson は Host を上書きした http.get +const u = new URL(page.webSocketDebuggerUrl); u.hostname = CDP.host; +const ws = new WebSocket(u.toString(), { headers: { Host: 'localhost:9222' } }); +ws.on('open', () => ws.send(JSON.stringify({ id: 1, method: 'Network.getCookies', params: { urls: [`${origin}/`] } }))); +// 応答の cookies を storageState の形(name・value・domain・path・expires(session なら -1)・httpOnly・secure・sameSite)へ直して保存する +``` + +この状態の Chrome では、Playwright で画面を操作することもできない。目視や操作は利用者に頼み、テストはコンテナの headless で回す。 + Playwright のテストでは `use: { storageState: '.auth/state.json' }` で読ませる。`.auth/` はリポジトリに入れない(`.gitignore` に足す)。セッションの期限が切れたら、利用者に Chrome でログインし直してもらい、取り出し直す。 ## 安全のための決まり From 832b6236ae247b5b104ed7f4a98887c425c0bc28 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 28 Oct 2026 09:40:38 +0000 Subject: [PATCH 14/17] =?UTF-8?q?Fix:=20chrome-cdp=20=E3=81=AE=E7=B5=8C?= =?UTF-8?q?=E8=B7=AF=E5=88=A4=E5=AE=9A=E3=81=AE=20awk=20=E3=81=AB=E3=81=82?= =?UTF-8?q?=E3=81=A3=E3=81=9F=E4=BD=8D=E7=BD=AE=E5=BC=95=E6=95=B0=EF=BC=88?= =?UTF-8?q?$2,=20$3=EF=BC=89=E3=82=92=E3=80=81=E5=90=8D=E5=89=8D=E4=BB=98?= =?UTF-8?q?=E3=81=8D=E3=81=AE=E5=A4=89=E6=95=B0=E3=81=A7=20/proc/net/route?= =?UTF-8?q?=20=E3=82=92=E8=AA=AD=E3=82=80=E5=BD=A2=E3=81=AB=E7=BD=AE?= =?UTF-8?q?=E3=81=8D=E6=8F=9B=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude Code が Skill 本文の $2 $3 を起動時の引数に置き換えるため、skill-frontmatter-check が落ちていた。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KYHP82g1gdLBLQ9WcKeiJo --- plugins/ndf/skills/chrome-cdp/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ndf/skills/chrome-cdp/SKILL.md b/plugins/ndf/skills/chrome-cdp/SKILL.md index 684bbfe6a..af3d73df1 100644 --- a/plugins/ndf/skills/chrome-cdp/SKILL.md +++ b/plugins/ndf/skills/chrome-cdp/SKILL.md @@ -31,7 +31,7 @@ CDP の操作は 1 回ごとに利用者の PC と往復する。リモートの ```bash getent hosts host.docker.internal && echo "経路A の候補" -cat /proc/net/route | awk 'NR>1 && $2=="00000000"{print "既定ゲートウェイ(16進):", $3}' +while read -r iface dest gw _; do if [ "$dest" = 00000000 ]; then echo "既定ゲートウェイ(16進): $gw"; fi; done < /proc/net/route env | grep -qE '^REMOTE_CONTAINERS=' && echo "Dev Containers の中" ``` From 6f072532066d1a6c29fb19ec1f190ed72c0b8ca3 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 28 Oct 2026 09:45:54 +0000 Subject: [PATCH 15/17] =?UTF-8?q?Update:=20playwright-kit=20=E3=81=AE?= =?UTF-8?q?=E5=88=A9=E7=94=A8=E8=80=85=E3=81=AE=20Chrome=20=E3=81=B8?= =?UTF-8?q?=E3=81=AE=E6=8E=A5=E7=B6=9A=E3=82=92=20ndf=20=E3=81=AE=20chrome?= =?UTF-8?q?-cdp=20=E3=81=B8=E5=AF=84=E3=81=9B=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - browser-connection.md から Chrome の起動フラグ・WSL2 / macOS の経路・ネットワーク別の到達手段を外し、/ndf:chrome-cdp を案内する。pytest の cdp-remote 設定と conftest の統合は残す - 既定のプロファイルで CDP を開く手順は Chrome 136 以降で動かず、--remote-allow-origins=* は検査ごと外すため、どちらも使わない - 同じ前提に立つ scripts/start-host-chrome.sh を削除する - cdp_endpoint は IP で書く(名前だと Chrome が Host ヘッダを理由に拒む。IP なら Host を書き換えずに connect_over_cdp が通ることを Chrome 155 で確認)。テンプレートの例も IP に揃える - chrome-cdp に、Host の書き換えが要るのは名前で接続するときだけであることを書き足す Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KYHP82g1gdLBLQ9WcKeiJo --- plugins/ndf/skills/chrome-cdp/SKILL.md | 2 +- .../skills/playwright-authoring/SKILL.md | 6 +- .../references/browser-connection.md | 227 ++---------------- .../scripts/start-host-chrome.sh | 172 ------------- .../templates/scenario.config.yaml | 12 +- 5 files changed, 31 insertions(+), 388 deletions(-) delete mode 100755 plugins/playwright-kit/skills/playwright-authoring/scripts/start-host-chrome.sh diff --git a/plugins/ndf/skills/chrome-cdp/SKILL.md b/plugins/ndf/skills/chrome-cdp/SKILL.md index af3d73df1..8fcef7652 100644 --- a/plugins/ndf/skills/chrome-cdp/SKILL.md +++ b/plugins/ndf/skills/chrome-cdp/SKILL.md @@ -90,7 +90,7 @@ CDP_HOST=host.docker.internal # 経路 B なら上の $GW curl -s -m 5 "http://$CDP_HOST:9222/json/version" -H 'Host: localhost:9222' ``` -**Host ヘッダを `localhost:9222` に上書きする。** Chrome は Host が IP でも localhost でもない接続を拒む(`Host header is specified and is not an IP address or localhost`)。WebSocket の接続も同じ。 +**Host ヘッダを `localhost:9222` に上書きする。** Chrome は Host が IP でも localhost でもない接続を拒む(`Host header is specified and is not an IP address or localhost`)。WebSocket の接続も同じ。上書きが要るのは `host.docker.internal` のような名前で接続するときで、IP(経路 B の中継など)で接続するなら上書きしなくても通る(Chrome 155 で確認)。上書きしても害はないので、下の手順は常に上書きする。 | 応答 | 意味 | | --- | --- | diff --git a/plugins/playwright-kit/skills/playwright-authoring/SKILL.md b/plugins/playwright-kit/skills/playwright-authoring/SKILL.md index 3c4f6267a..7f28a83b3 100644 --- a/plugins/playwright-kit/skills/playwright-authoring/SKILL.md +++ b/plugins/playwright-kit/skills/playwright-authoring/SKILL.md @@ -231,9 +231,9 @@ browser: cdp_endpoint: ${CDP_ENDPOINT:-http://localhost:9222} ``` -WSL2 / macOS / Linux ホストの Chrome へ CDP 接続する手順、`scripts/start-host-chrome.sh` によるホスト -Chrome の起動、`conftest.py` への統合、ネットワーク到達性の確保、トラブルシュートは -[references/browser-connection.md](references/browser-connection.md) を参照。 +`cdp-remote` の `cdp_endpoint` は IP で書く(名前だと Chrome が接続を拒む)。`conftest.py` への統合と +トラブルシュートは [references/browser-connection.md](references/browser-connection.md) を参照。 +利用者の Chrome の起動と、コンテナから届かせる経路は ndf プラグインの `/ndf:chrome-cdp` に従う。 ## MCP でのブラウザ動作確認 diff --git a/plugins/playwright-kit/skills/playwright-authoring/references/browser-connection.md b/plugins/playwright-kit/skills/playwright-authoring/references/browser-connection.md index 29e7d4de0..f452057b1 100644 --- a/plugins/playwright-kit/skills/playwright-authoring/references/browser-connection.md +++ b/plugins/playwright-kit/skills/playwright-authoring/references/browser-connection.md @@ -2,23 +2,6 @@ E2E テスト実行時のブラウザ接続先を構成する手順。概要と設定項目は `SKILL.md` の「ブラウザ接続」節を参照。 -## Chrome 起動フラグ - -CDP 接続に使う Chrome は次のフラグで起動する。OS ごとの差はバイナリパスだけである。 - -| フラグ | 役割 | -|---|---| -| `--remote-debugging-port=9222` | CDP エンドポイントを 9222 で公開 | -| `--remote-allow-origins=*` | CDP WebSocket の Host ヘッダ検証を無効化し、コンテナ等リモートからの接続を許可 (Chrome 106+) | -| `--user-data-dir=/tmp/chrome-debug` | 専用プロファイルで起動し、通常の Chrome と共存させる。任意のパスでよい | -| `--disable-features=DialMediaRouteProvider` | DIAL (Cast) 探索を無効化し、CDP ログのノイズと不要な通信を抑制 | -| `--remote-debugging-address=0.0.0.0` | 全インターフェースで listen する。loopback bind でコンテナから届かない場合のみ付与 | - -既存プロファイルのログイン済み Session をそのまま使う場合は、**全 Chrome プロセスを終了してから** -`--user-data-dir` を外して起動する (デフォルトプロファイルを使用)。 - -> **Security**: `--remote-allow-origins=*` と `--remote-debugging-address=0.0.0.0` は信頼できるネットワーク内でのみ使用する。ファイアウォールでポート 9222 へのアクセスを制限することを推奨。 - ## パターン 1: ローカルコンテナ Chromium (デフォルト) 設定不要。`run.sh` 初回実行時に `playwright install chromium` が自動実行される。 @@ -28,139 +11,39 @@ browser: mode: local ``` -## パターン 2: Windows ホスト Chrome (WSL2 + Docker → CDP) +## パターン 2: 利用者の Chrome (cdp-remote) -``` -Docker container (playwright) - ↓ http://host.docker.internal:9222 -Docker Desktop (WSL2 backend) → Windows host → localhost:9222 - ↓ -Chrome (--remote-debugging-port=9222 --remote-allow-origins=*) -``` +**Chrome の起動と、コンテナから Chrome まで届かせる経路は、ndf プラグインの `chrome-cdp` スキル(`/ndf:chrome-cdp`)に従う。** playwright-kit では扱わない。 -**Step 1: Windows Chrome をリモートデバッグモードで起動** +`chrome-cdp` が扱う範囲: -```powershell -& "C:\Program Files\Google\Chrome\Application\chrome.exe" ` - --remote-debugging-port=9222 ` - --remote-allow-origins=* ` - --user-data-dir="C:\tmp\chrome-debug" -``` +- Chrome を専用プロファイル(`--user-data-dir`)で起動する。Chrome 136 以降は、既定のプロファイルではリモート操作の口が開かない +- 経路の判定。同じ PC の Docker(`host.docker.internal` が引ける)と、EC2 などリモートのホスト(SSH の `RemoteForward` と中継コンテナ) +- つながったかの確かめ方と、うまくいかないときの切り分け +- ログインだけ利用者の Chrome で行い、Cookie を取り出して `local` モードの headless で回す方法 -> Chrome はデフォルトで `127.0.0.1` にバインドするため、`--remote-allow-origins=*` だけでは WSL2/Docker から接続できない場合がある。その場合は後述の「ネットワーク別接続ガイド」で到達性を確保する。 +`--remote-allow-origins=*` や `--remote-debugging-address=0.0.0.0` で Chrome の検査を外す方法は使わない。同じネットワークの誰からでも利用者の Chrome を操作できてしまう。 -**Step 2: WSL2 .wslconfig を NAT mode にする** - -```ini -# %USERPROFILE%\.wslconfig -[wsl2] -networkingMode=NAT -``` +### scenario.config.yaml -**Step 3: scenario.config.yaml** - -Docker Desktop (WSL2 backend) は `host.docker.internal` を標準サポートしている。 +**`cdp_endpoint` は IP アドレスで書く。** Chrome は、Host ヘッダが IP でも localhost でもない接続を拒む。`host.docker.internal` のような名前で書くと、`connect_over_cdp` が `Host header is specified and is not an IP address or localhost` で失敗する。IP で書けば、Host ヘッダを書き換えずに接続できる。 ```yaml browser: mode: cdp-remote - cdp_endpoint: ${CDP_ENDPOINT:-http://host.docker.internal:9222} + cdp_endpoint: ${CDP_ENDPOINT:-http://localhost:9222} ``` -> **Docker Desktop を使わず WSL2 から直接実行する場合**: `host.docker.internal` は Docker Desktop 固有の DNS 名のため使えない。Windows ホストの IP を直接指定する。 -> ```bash -> export CDP_ENDPOINT="http://$(grep nameserver /etc/resolv.conf | awk '{print $2}'):9222" -> ``` - -## パターン 3: macOS ホスト Chrome (Docker → CDP) - -``` -Docker container (playwright) - ↓ http://host.docker.internal:9222 -macOS host → localhost:9222 - ↓ -Chrome (--remote-debugging-port=9222 --remote-allow-origins=*) -``` - -**Step 1: ホスト側で Chrome を起動** - ```bash -"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ - --remote-debugging-port=9222 \ - --user-data-dir=/tmp/chrome-debug \ - --remote-allow-origins=* \ - --disable-features=DialMediaRouteProvider +# 同じ PC の Docker: 名前を IP に引き直す +export CDP_ENDPOINT="http://$(getent hosts host.docker.internal | cut -d' ' -f1):9222" +# リモートのホスト: chrome-cdp で立てた中継の IP(コンテナのネットワークのゲートウェイ) +export CDP_ENDPOINT="http://172.18.0.1:9222" ``` -> **Note**: 上記は macOS Docker Desktop で動作実績のあるコマンド。macOS Docker Desktop は `host.docker.internal` がホストの loopback (127.0.0.1) バインドのサービスに到達できるため、`--remote-debugging-address=0.0.0.0` は不要で、全インターフェース公開によるセキュリティ低下も避けられる。Linux / WSL2 ホストで loopback bind が問題になる場合のみ `0.0.0.0` を付与する。 +### 大量の操作は local モードで回す -**Step 2: scenario.config.yaml** - -`--remote-allow-origins=*` で Host ヘッダ検証を無効化しているため、WSL2 と異なり **proxy 不要**。 - -```yaml -browser: - mode: cdp-remote - cdp_endpoint: ${CDP_ENDPOINT:-http://host.docker.internal:9222} -``` - -**Step 3 (任意): コンテナからホスト Chrome を起動する** → 次節。 - -## コンテナからホスト Chrome を起動する (SSH 経由) - -### なぜ直接は起動できないのか - -Docker コンテナはホストとプロセス空間が分離されているため、**コンテナ内のプロセスがホスト上に直接プロセスを生成することはできない**。 -特に macOS / Windows の Docker Desktop はコンテナを LinuxKit VM 内で実行するため、`nsenter` やホスト PID namespace を使う Linux 系の回避策も VM 止まりでホストには届かない。 - -したがって「コンテナからホストの Chrome を起動する」には、**ホスト側に起動を受け付ける口** が必要になる。最も導入が容易でスクリプト化しやすいのは **SSH** (macOS の「リモートログイン」= sshd) を使う方法。 - -``` -Docker container ──ssh──▶ host.docker.internal:22 (macOS sshd) - └─▶ Google Chrome --remote-debugging-port=9222 ... (バックグラウンド起動) -Docker container ──CDP──▶ host.docker.internal:9222 (起動後に接続) -``` - -### ホスト側の準備 (一度だけ) - -1. **リモートログインを有効化**: システム設定 > 一般 > 共有 > 「リモートログイン」を ON (CLI: `sudo systemsetup -setremotelogin on`) -2. **SSH 鍵を登録** (パスワードレス実行のため): コンテナ側の公開鍵をホストの `~/.ssh/authorized_keys` に追加 -3. ログインユーザーは **コンソールにログイン中の本人** であること。macOS では GUI アプリ (Chrome) は WindowServer に接続するため、コンソールセッションの所有者として起動する必要がある - -### スクリプト - -`scripts/start-host-chrome.sh` をコンテナ内から実行する。冪等で、既に CDP が起動済みなら何もしない。 - -```bash -# コンテナ内 -HOST_SSH_USER= ./scripts/start-host-chrome.sh -``` - -| 変数 | デフォルト | 説明 | -|---|---|---| -| `HOST_SSH_USER` | (必須) | ホスト (mac) のログインユーザー名 | -| `HOST_SSH_HOST` | `host.docker.internal` | SSH 接続先ホスト | -| `CDP_PORT` | `9222` | リモートデバッグポート | -| `CDP_BIND_ADDRESS` | (空=loopback) | Chrome の listen address。空なら Chrome 既定の loopback bind (macOS Docker Desktop はこれで動作・実証済み)。Linux/WSL2 等で到達できない場合のみ `0.0.0.0` 等を指定 | -| `CHROME_USER_DATA_DIR` | `/tmp/chrome-debug` | 起動プロファイル。空にするとデフォルトプロファイル (ログイン済み Session) を使用 | -| `CHROME_BIN` | `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome` | Chrome バイナリパス | -| `MANUAL_WAIT` | `120` | 手動フォールバック時の起動待ち秒。`0` で待たず即終了 | - -動作: - -1. `http://host.docker.internal:9222/json/version` に疎通すれば **起動済み** とみなし即終了 -2. 未起動なら SSH (`BatchMode=yes`) でホストに接続し、Chrome をバックグラウンド (`nohup ... &`) で起動 -3. CDP エンドポイントが応答するまで最大 30 秒ポーリングして待機 - -### SSH が使えない場合のフォールバック - -`HOST_SSH_USER` 未設定 / コンテナに `ssh` クライアントが無い / SSH 接続・実行に失敗 (鍵未登録・リモートログイン無効・到達不可) のいずれかで、スクリプトは **自動で手動フォールバックに切り替わる**。 - -フォールバック時は、ホスト側で実行すべき起動コマンドをそのまま画面に出力し、`MANUAL_WAIT` 秒 (既定 120s) のあいだ CDP の起動をポーリングして待機する。利用者はその間にホストのターミナルへコマンドを貼り付けて実行すればよい。CI など人手が介在しない環境では `MANUAL_WAIT=0` を指定すれば、案内を出して即座に非ゼロ終了する。 - -> **Note (SSH を使わない代替手段)**: ホスト側に常駐ランチャ (launchd エージェントや FIFO 監視スクリプト、簡易 HTTP エンドポイント等) を置き、コンテナからネットワーク経由でトリガする方法もある。ただし SSH 方式が最も追加実装が少なく確実。X11 forwarding (XQuartz + socat) は「コンテナ内 GUI をホスト画面に表示する」用途であり、本件には不要。 - -> **Note (Linux ホストの場合)**: ホストで sshd が動いていれば同じスクリプトが使える。`CHROME_BIN=google-chrome`、`HOST_SSH_HOST` をホスト IP (`172.17.0.1` 等) に設定する。GUI セッションへの接続には `DISPLAY` 等の追加考慮が必要。 +CDP の操作は 1 回ごとに利用者の PC と往復するので、リモートの開発環境では遅い。テストの本数が多いときは、`chrome-cdp` の手順で Cookie を `storageState` として取り出し、`local` モードの headless の Chromium で回す。`cdp-remote` は、ログイン状態の確認や画面を見せながらの少数の操作に使う。 ## conftest.py への統合 @@ -247,80 +130,14 @@ def page(context, pwk_config): `playwright install` を実行するが、接続先がリモートの場合はスキップして問題ない。 ```bash -# エンドポイントの疎通確認 -curl -s http://host.docker.internal:9222/json/version | python3 -m json.tool -``` - -## ネットワーク別接続ガイド - -Chrome はデフォルトで `127.0.0.1` にバインドするため、同一ホストからしか CDP エンドポイントにアクセスできない。 -Docker コンテナや WSL2 からリモート接続する場合は、次のいずれかで到達性を確保する。 - -### 方法 1: `--remote-debugging-address=0.0.0.0` (推奨) - -Chrome 起動時に全インターフェースでリッスンさせる。最もシンプル。フラグの詳細は冒頭の表を参照。 - -### 方法 2: socat によるポートフォワード (Linux) - -Chrome を `127.0.0.1` バインドのまま維持し、socat でリモートからのアクセスを中継する。 - -```bash -socat TCP-LISTEN:9222,bind=0.0.0.0,reuseaddr,fork TCP:127.0.0.1:9222 -``` - -### 方法 3: netsh portproxy (Windows → WSL2) - -```powershell -# 管理者権限の PowerShell で実行 -netsh interface portproxy add v4tov4 ` - listenaddress=0.0.0.0 listenport=9222 ` - connectaddress=127.0.0.1 connectport=9222 - -netsh interface portproxy show all -netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=9222 +# エンドポイントの疎通確認(Browser を含む JSON が返ればよい) +curl -s -m 5 "$CDP_ENDPOINT/json/version" | python3 -m json.tool ``` -### 接続先の早見表 - -| 実行環境 | Chrome の場所 | 推奨方法 | CDP エンドポイント | -|---|---|---|---| -| ローカル (同一ホスト) | 同一ホスト | 設定不要 | `http://localhost:9222` | -| Docker → ホスト (macOS) | macOS ホスト | 方法 1 | `http://host.docker.internal:9222` | -| Docker → ホスト (Linux) | Linux ホスト | 方法 1 or 2 | `http://host.docker.internal:9222` or `http://172.17.0.1:9222` | -| Docker (WSL2) → Windows | Windows ホスト | 方法 1 or 3 | `http://host.docker.internal:9222` | -| WSL2 → Windows | Windows ホスト | 方法 1 or 3 | `http://$(grep nameserver /etc/resolv.conf \| awk '{print $2}'):9222` | - ## トラブルシュート -### 共通 - -| 症状 | 原因 | 対策 | -|---|---|---| -| `connect_over_cdp` で接続拒否 | Chrome が起動していない / ポートが違う | `curl http:///json/version` で確認 | -| WebSocket handshake 失敗 | Host ヘッダ不一致 | Chrome 起動時に `--remote-allow-origins=*` を付与 | -| ページ操作が異常に遅い | VPN / DNS 解決の遅延 | `extra_hosts` で IP 直指定 | - -### Windows (WSL2) 固有 - | 症状 | 原因 | 対策 | |---|---|---| -| `host.docker.internal` 解決不能 | Docker Desktop 未使用 or 古いバージョン | Docker Desktop を使用するか、WSL2 直接の場合は Windows ホスト IP を直接指定 | -| IPv6 でバインドされる | WSL2 が IPv6 優先 | listen address に `0.0.0.0` を明示 | -| `netsh portproxy` で接続ループ | portproxy の自己参照 | `--remote-allow-origins=*` を使い proxy を廃止 | -| mirrored mode で動かない | mirrored は localhost 共有だが CDP の WS 接続でポート競合 | NAT mode に戻す | - -### macOS 固有 - -| 症状 | 原因 | 対策 | -|---|---|---| -| `host.docker.internal` 解決不能 | Docker Desktop が古い / Linux Docker | `--add-host=host.docker.internal:host-gateway` を指定 | -| ファイアウォールでブロック | macOS のアプリファイアウォール | システム設定 > ネットワーク > ファイアウォール で Chrome を許可 | -| SSH 起動で Chrome が表示されない / WindowServer エラー | コンソール非ログインユーザーで SSH した | コンソールにログイン中の本人ユーザーで SSH する | -| `start-host-chrome.sh` が SSH で認証失敗 | リモートログイン未有効 / 鍵未登録 | `sudo systemsetup -setremotelogin on` と `authorized_keys` 登録を確認 | - -## CDP 接続のメリット - -- **GUI Chrome をそのまま操作可能** — OBS 録画可、人間と AI の協調操作が可能 -- **ログイン済み Session の再利用** — Google / AWS / Slack 等の MFA 済み Session をそのまま使える -- **ブラウザ拡張機能が有効** — テスト時にも拡張機能の影響を確認可能 -- **AI Agent との相性** — Claude Code / Browser Use / OpenHands がリアルブラウザを操作 +| `Host header is specified and is not an IP address or localhost` | `cdp_endpoint` を名前で書いた | IP で書く(上の「scenario.config.yaml」) | +| `connect_over_cdp` で接続拒否・空の応答 | Chrome が起動していない / 経路が切れている | `/ndf:chrome-cdp` の「つながったか確かめる」で切り分ける | +| ページ操作が異常に遅い | 利用者の PC との往復が積み重なっている | Cookie を取り出して `local` モードで回す | diff --git a/plugins/playwright-kit/skills/playwright-authoring/scripts/start-host-chrome.sh b/plugins/playwright-kit/skills/playwright-authoring/scripts/start-host-chrome.sh deleted file mode 100755 index 8c7b1d088..000000000 --- a/plugins/playwright-kit/skills/playwright-authoring/scripts/start-host-chrome.sh +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env bash -# コンテナ内から SSH 経由でホスト (macOS / Linux) の Chrome を -# リモートデバッグモードで起動するスクリプト。 -# -# 背景: -# Docker コンテナはホストとプロセス空間が分離されているため、 -# コンテナから直接ホストのプロセスを起動できない。 -# ホストの sshd (macOS: システム設定 > 共有 > リモートログイン) に接続し、 -# 起動コマンドを実行することで間接的にホスト Chrome を起動する。 -# -# SSH が使えない場合 (鍵未登録・リモートログイン無効など) は、 -# ホスト側で手動実行する起動コマンドを案内し、起動されるまで待機する -# フォールバックに切り替わる。 -# -# 前提 (SSH 自動起動を使う場合のみ・ホスト側で一度だけ設定): -# 1) リモートログインを有効化 (macOS: sudo systemsetup -setremotelogin on) -# 2) コンテナの公開鍵を ~/.ssh/authorized_keys に登録 (パスワードレス実行) -# 3) コンソールにログイン中の本人ユーザーで SSH すること -# (GUI アプリは WindowServer 接続のためコンソールセッション所有者が必要) -# -# 使い方 (コンテナ内): -# # SSH 自動起動 -# HOST_SSH_USER= ./scripts/start-host-chrome.sh -# # SSH を使わず手動起動の案内のみ -# ./scripts/start-host-chrome.sh -# -# 環境変数: -# HOST_SSH_USER ホストのログインユーザー名。未設定なら手動フォールバック -# HOST_SSH_HOST SSH 接続先 (default: host.docker.internal) -# CDP_HOST CDP 疎通確認先ホスト (default: HOST_SSH_HOST) -# CDP_PORT リモートデバッグポート (default: 9222) -# CDP_BIND_ADDRESS Chrome の listen address。空 (default) なら付与せず -# Chrome 既定の loopback bind (macOS Docker Desktop は -# host.docker.internal がホスト loopback に到達するため -# これで動作する)。Linux/WSL2 等で loopback bind だと -# コンテナから到達できない場合のみ 0.0.0.0 等を指定する。 -# 0.0.0.0 は全インターフェース公開のためセキュリティ注意。 -# CHROME_USER_DATA_DIR 起動プロファイル (default: /tmp/chrome-debug) -# 空にするとデフォルトプロファイル (ログイン済み Session) を使用 -# CHROME_BIN Chrome バイナリパス -# (default: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome) -# STARTUP_TIMEOUT SSH 起動後の待機タイムアウト秒 (default: 30) -# MANUAL_WAIT 手動フォールバック時の待機秒。0 で待たず即終了 (default: 120) -set -euo pipefail - -HOST_SSH_USER="${HOST_SSH_USER:-}" -HOST_SSH_HOST="${HOST_SSH_HOST:-host.docker.internal}" -CDP_HOST="${CDP_HOST:-$HOST_SSH_HOST}" -CDP_PORT="${CDP_PORT:-9222}" -CDP_BIND_ADDRESS="${CDP_BIND_ADDRESS:-}" -CHROME_USER_DATA_DIR="${CHROME_USER_DATA_DIR-/tmp/chrome-debug}" -CHROME_BIN="${CHROME_BIN:-/Applications/Google Chrome.app/Contents/MacOS/Google Chrome}" -STARTUP_TIMEOUT="${STARTUP_TIMEOUT:-30}" -MANUAL_WAIT="${MANUAL_WAIT:-120}" - -# curl は冪等チェック・起動待機・手動フォールバックの全経路で CDP 疎通確認に -# 使う必須コマンド。無いと終了コード 127 で常に未起動扱いとなり、起動成功時でも -# タイムアウトしてしまうため、ここでフェイルファストする。 -if ! command -v curl >/dev/null 2>&1; then - echo "✗ curl が見つかりません。CDP 疎通確認に必須です。" >&2 - echo " コンテナに curl をインストールしてから再実行してください" >&2 - echo " (例: apt-get install -y curl / apk add curl)。" >&2 - exit 1 -fi - -cdp_up() { - # --max-time でネットワークハング時も待機ループ周期が壊れないようにする - curl -sf --max-time 2 "http://${CDP_HOST}:${CDP_PORT}/json/version" >/dev/null 2>&1 -} - -# --user-data-dir は空文字なら付与しない (デフォルトプロファイル使用) -userdata_arg="" -if [ -n "${CHROME_USER_DATA_DIR}" ]; then - userdata_arg="--user-data-dir='${CHROME_USER_DATA_DIR}'" -fi - -# --remote-debugging-address は CDP_BIND_ADDRESS が空なら付与しない。 -# 既定 (空) では Chrome は loopback (127.0.0.1) のみ listen する。macOS -# Docker Desktop は host.docker.internal がホストの loopback に到達するため -# これで動作する (ユーザー実証済み・0.0.0.0 不要)。 -# Linux/WSL2 等で loopback bind だとコンテナから到達できない場合のみ -# CDP_BIND_ADDRESS=0.0.0.0 等を指定する。0.0.0.0 は CDP を全インターフェース -# へ公開し、CDP は認証なしでブラウザ操作できるためセキュリティに注意すること。 -bind_arg="" -if [ -n "${CDP_BIND_ADDRESS}" ]; then - bind_arg="--remote-debugging-address=${CDP_BIND_ADDRESS} " -fi - -# ホスト側で実行する Chrome 起動コマンド (人間がコピペできる体裁) -host_launch_cmd() { - printf '"%s" --remote-debugging-port=%s %s%s --remote-allow-origins=* --disable-features=DialMediaRouteProvider' \ - "${CHROME_BIN}" "${CDP_PORT}" "${bind_arg}" "${userdata_arg}" -} - -# 手動フォールバック: ホストで実行するコマンドを案内し、起動を待機する -manual_fallback() { - local reason="$1" - echo "" >&2 - echo "──────────────────────────────────────────────────────────────" >&2 - echo "⚠ SSH 自動起動を利用できません (${reason})。" >&2 - echo " ホスト (mac) 側のターミナルで以下を実行してください:" >&2 - echo "" >&2 - echo " $(host_launch_cmd)" >&2 - echo "" >&2 - echo "──────────────────────────────────────────────────────────────" >&2 - - if [ "${MANUAL_WAIT}" -le 0 ]; then - echo "✗ Chrome 未起動のまま終了します (MANUAL_WAIT=0)。" >&2 - exit 1 - fi - - echo "→ ホストでの起動を待機中 (最大 ${MANUAL_WAIT}s, Ctrl-C で中断)..." >&2 - # seq 外部依存も bash 専用 for ((...)) も避け、POSIX 互換 while で待機する - # (最小コンテナ / /bin/sh しかない環境でも動作) - i=0 - while [ "$i" -lt "$MANUAL_WAIT" ]; do - if cdp_up; then - echo "✓ Chrome 起動を検知しました: http://${CDP_HOST}:${CDP_PORT}" - exit 0 - fi - sleep 1 - i=$((i + 1)) - done - echo "✗ 待機タイムアウト。手動起動後に再実行してください。" >&2 - exit 1 -} - -# 1) 既に起動済みなら何もしない (冪等) -if cdp_up; then - echo "✓ Chrome は既に CDP http://${CDP_HOST}:${CDP_PORT} で起動済みです" - exit 0 -fi - -# 2) HOST_SSH_USER 未設定 → 手動フォールバック -if [ -z "${HOST_SSH_USER}" ]; then - manual_fallback "HOST_SSH_USER が未設定" -fi - -# 3) ssh コマンドが無い → 手動フォールバック -if ! command -v ssh >/dev/null 2>&1; then - manual_fallback "コンテナに ssh クライアントが無い" -fi - -# 4) SSH でホストに接続し、Chrome をバックグラウンド起動 -echo "→ ${HOST_SSH_USER}@${HOST_SSH_HOST} で Chrome を起動します..." -# &2 -exit 1 diff --git a/plugins/playwright-kit/skills/playwright-kit-ops/templates/scenario.config.yaml b/plugins/playwright-kit/skills/playwright-kit-ops/templates/scenario.config.yaml index 9c02fb651..2b8c4f193 100644 --- a/plugins/playwright-kit/skills/playwright-kit-ops/templates/scenario.config.yaml +++ b/plugins/playwright-kit/skills/playwright-kit-ops/templates/scenario.config.yaml @@ -14,15 +14,13 @@ # --- ブラウザ接続設定 -------------------------------------------------- # local: playwright install chromium でインストールしたローカルブラウザ (デフォルト) -# cdp-remote: Chrome DevTools Protocol 経由でリモートブラウザに接続 -# → Windows / macOS のホスト Chrome を GUI 付きで操作可能 -# → ログイン済み Session (Google, AWS, Slack 等) をそのまま再利用 -# → 詳細は /playwright-kit:playwright-authoring を参照 +# cdp-remote: Chrome DevTools Protocol 経由で利用者の Chrome に接続 +# → ログイン済み Session をそのまま再利用 +# → Chrome の起動とコンテナから届かせる経路は /ndf:chrome-cdp を参照 browser: mode: local - # cdp-remote 時のみ有効。環境変数で切り替え推奨: - # Windows (WSL2 Docker): export CDP_ENDPOINT=http://host-gateway:9222 - # macOS (Docker Desktop): export CDP_ENDPOINT=http://host.docker.internal:9222 + # cdp-remote 時のみ有効。IP で書く(名前だと Chrome が Host ヘッダを理由に拒む): + # export CDP_ENDPOINT=http://172.18.0.1:9222 # cdp_endpoint: ${CDP_ENDPOINT:-http://localhost:9222} # --- 接続先 ---------------------------------------------------------- From 2ddffd96a8471335d1538d45f85681bd324d4248 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 28 Oct 2026 09:47:10 +0000 Subject: [PATCH 16/17] =?UTF-8?q?Docs:=20chrome-cdp=20=E3=81=AE=20Host=20?= =?UTF-8?q?=E3=83=98=E3=83=83=E3=83=80=E3=81=AE=E8=AA=AC=E6=98=8E=E3=81=8B?= =?UTF-8?q?=E3=82=89=E7=A2=BA=E8=AA=8D=E3=81=97=E3=81=9F=E7=89=88=E3=81=AE?= =?UTF-8?q?=E8=A8=98=E8=BC=89=E3=82=92=E5=A4=96=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KYHP82g1gdLBLQ9WcKeiJo --- plugins/ndf/skills/chrome-cdp/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ndf/skills/chrome-cdp/SKILL.md b/plugins/ndf/skills/chrome-cdp/SKILL.md index 8fcef7652..ade43f4bb 100644 --- a/plugins/ndf/skills/chrome-cdp/SKILL.md +++ b/plugins/ndf/skills/chrome-cdp/SKILL.md @@ -90,7 +90,7 @@ CDP_HOST=host.docker.internal # 経路 B なら上の $GW curl -s -m 5 "http://$CDP_HOST:9222/json/version" -H 'Host: localhost:9222' ``` -**Host ヘッダを `localhost:9222` に上書きする。** Chrome は Host が IP でも localhost でもない接続を拒む(`Host header is specified and is not an IP address or localhost`)。WebSocket の接続も同じ。上書きが要るのは `host.docker.internal` のような名前で接続するときで、IP(経路 B の中継など)で接続するなら上書きしなくても通る(Chrome 155 で確認)。上書きしても害はないので、下の手順は常に上書きする。 +**Host ヘッダを `localhost:9222` に上書きする。** Chrome は Host が IP でも localhost でもない接続を拒む(`Host header is specified and is not an IP address or localhost`)。WebSocket の接続も同じ。上書きが要るのは `host.docker.internal` のような名前で接続するときで、IP(経路 B の中継など)で接続するなら上書きしなくても通る。上書きしても害はないので、下の手順は常に上書きする。 | 応答 | 意味 | | --- | --- | From 9b64cb4c6ca2a3fb9a30c2bbf9a60540f0f92b3e Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 28 Oct 2026 10:02:59 +0000 Subject: [PATCH 17/17] Release: ndf v10.23.0 / playwright-kit v3.0.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ndf 10.22.0 → 10.23.0(MINOR: Skill chrome-cdp を追加。#2153) - playwright-kit 3.0.1 → 3.0.2(PATCH: 接続の説明を chrome-cdp へ移し、動かなくなっていた start-host-chrome.sh を削除) - CHANGELOG の版の節とトークン消費の記録 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KYHP82JyPwKtTWquozHjQ9 --- .claude-plugin/marketplace.json | 4 +- CHANGELOG.md | 6 + README.md | 6 +- docs/metrics/ndf-token-usage/2026-10-28.json | 797 ++++++++++++++++++ docs/metrics/ndf-token-usage/2026-10-28.md | 115 +++ docs/versioning-and-distribution.md | 12 +- plugins/ndf/.claude-plugin/plugin.json | 4 +- plugins/ndf/.codex-plugin/plugin.json | 4 +- plugins/ndf/README.md | 8 +- plugins/ndf/dev.agy/plugin.json | 4 +- .../playwright-kit/.claude-plugin/plugin.json | 4 +- plugins/playwright-kit/plugin.json | 4 +- 12 files changed, 943 insertions(+), 25 deletions(-) create mode 100644 docs/metrics/ndf-token-usage/2026-10-28.json create mode 100644 docs/metrics/ndf-token-usage/2026-10-28.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index efec42665..eb43aa402 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.22.0): 8 specialized agents (plus 1 worker agent for the 3-layer workflow) and 49 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.23.0): 8 specialized agents (plus 1 worker agent for the 3-layer workflow) and 49 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" @@ -22,7 +22,7 @@ { "name": "playwright-kit", "source": "./plugins/playwright-kit", - "description": "Claude Code plugin (v3.0.1): 4 Playwright E2E skills — test planning by page role, script authoring with video / trace evidence, report generation with collected evidence, and the playwright_kit runtime (init, a11y / CWV scans).", + "description": "Claude Code plugin (v3.0.2): 4 Playwright E2E skills — test planning by page role, script authoring with video / trace evidence, report generation with collected evidence, and the playwright_kit runtime (init, a11y / CWV scans).", "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ad052625..4fd253c43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ **開発版(接尾辞の付いた版)は載せない。** `9.8.0` は `9.8.0-dev.1` までしか出ておらず、 その内容は `10.0.0` で届いている。 +## [ndf 10.23.0] - 2026-10-28 + +- Docs: スプリント m1914 の要求・設計を確定仕様にする(#1914 #1529 #1630 #1593)(#2141) +- ndf に Skill `chrome-cdp` が加わった。開発コンテナ(リモートのホストの上のものを含む)の中のエージェントが、利用者の PC の Chrome を CDP で操作できる。経路の判定・PC 側の設定・中継の立て方・Cookie の取り出しと、安全のための決まりを持つ。Claude Code・Codex・Kiro CLI・agy のすべてへ配る(#2153) +- playwright-kit 3.0.2: 利用者の Chrome への接続の説明を `/ndf:chrome-cdp` へ移し、Chrome 136 以降で動かない `scripts/start-host-chrome.sh` を削除した。`cdp_endpoint` は IP で書く(名前だと Chrome が Host ヘッダを理由に拒む)(#2153) + ## [ndf 10.22.0] - 2026-10-25 - Docs: 開発ループ(--loop)の確定仕様を docs/specifications へ置く(#2089)(#2128) diff --git a/README.md b/README.md index 0a250bb3d..69fb517d2 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,8 @@ Claude Code では `/ndf:` と打って呼びます。**Skill** は | プラグイン名 | バージョン | できること | 詳細 | | --- | --- | --- | --- | -| **ndf** | 10.22.0 | 開発の作業手順(Skill)と専門のサブエージェント。4 つの AI ツールへ同じ名前で入る | [README](./plugins/ndf/README.md) | -| **playwright-kit** | 3.0.1 | Playwright による E2E テストの計画・スクリプト作成・証跡のレポート | [README](./plugins/playwright-kit/README.md) | +| **ndf** | 10.23.0 | 開発の作業手順(Skill)と専門のサブエージェント。4 つの AI ツールへ同じ名前で入る | [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 向け)。 使うものだけを入れます(Claude Code なら `/plugin install <プラグイン名>@ai-plugins`)。 @@ -88,7 +88,7 @@ Claude Code では `/ndf:` と打って呼びます。**Skill** は ### NDF プラグインの Skill の一覧 -**NDFプラグイン v10.22.0** に入る Skill の数は、AI ツールごとに違います。 +**NDFプラグイン v10.23.0** に入る Skill の数は、AI ツールごとに違います。 Claude Code向け core 49個、Kiro向け core 46個、Codex向け core 45個、agy向け core 45個です。 元Skills(49個)の内訳は次のとおりです。 - Pull Request とレビュー (7): pr, pr-tests, fix, pr-review, cherry-pick-pr, deploy, merged diff --git a/docs/metrics/ndf-token-usage/2026-10-28.json b/docs/metrics/ndf-token-usage/2026-10-28.json new file mode 100644 index 000000000..8497e5370 --- /dev/null +++ b/docs/metrics/ndf-token-usage/2026-10-28.json @@ -0,0 +1,797 @@ +{ + "per_pr": [ + { + "version": "10.21.0", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "-", + "sessions": 1, + "sessions_with_pr": 1, + "prs": 2, + "conductor_cost": 352633.9000000001, + "supervisor_cost": 873531.175, + "worker_cost": 710785.275, + "context": 18393014.0, + "out": 78969.5, + "minutes": 24.508533332745234, + "codex_input": 0.0, + "codex_out": 0.0, + "kiro_credit": 0.0, + "claude_seat_cost": 0.0 + }, + { + "version": "10.21.0", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "claude", + "sessions": 1, + "sessions_with_pr": 0, + "prs": 0 + }, + { + "version": "10.22.0", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "claude", + "sessions": 1, + "sessions_with_pr": 1, + "prs": 4, + "conductor_cost": 140990.83750000002, + "supervisor_cost": 388907.5625, + "worker_cost": 883506.2124999999, + "context": 10718241.25, + "out": 61116.0, + "minutes": 22.55310833354791, + "codex_input": 0.0, + "codex_out": 0.0, + "kiro_credit": 0.0, + "claude_seat_cost": 143981.03750000003 + }, + { + "version": "10.22.0", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "-", + "sessions": 7, + "sessions_with_pr": 0, + "prs": 0 + }, + { + "version": "10.22.0", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "claude+codex", + "sessions": 1, + "sessions_with_pr": 0, + "prs": 0 + }, + { + "version": "10.22.0", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "-", + "sessions": 3, + "sessions_with_pr": 1, + "prs": 1, + "conductor_cost": 7566553.300000004, + "supervisor_cost": 0.0, + "worker_cost": 0.0, + "context": 59578167.0, + "out": 217288.0, + "minutes": 599.1305666605632, + "codex_input": 0.0, + "codex_out": 0.0, + "kiro_credit": 0.0, + "claude_seat_cost": 0.0 + }, + { + "version": "10.22.0", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "claude", + "sessions": 1, + "sessions_with_pr": 0, + "prs": 0 + }, + { + "version": "10.22.0", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.311", + "reviewers": "-", + "sessions": 3, + "sessions_with_pr": 1, + "prs": 5, + "conductor_cost": 340157.73, + "supervisor_cost": 0.0, + "worker_cost": 0.0, + "context": 3155091.4, + "out": 12078.4, + "minutes": 29.249676666259763, + "codex_input": 0.0, + "codex_out": 0.0, + "kiro_credit": 0.0, + "claude_seat_cost": 0.0 + }, + { + "version": "10.22.0", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.312", + "reviewers": "-", + "sessions": 1, + "sessions_with_pr": 0, + "prs": 0 + } + ], + "per_role": [ + { + "version": "10.21.0", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "-", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 1, + "cost": 705267.8000000002, + "context": 6778570.0, + "out": 23905.0, + "minutes": 49.01706666549047, + "p": 45526.0, + "k": 55.0, + "w5": 0.0, + "w1h": 126512.0, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 484038, + "rewrite_gap_median": null + }, + { + "version": "10.21.0", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "-", + "layer": "supervisor", + "role": "仕上げ", + "agent_type": "ndf:supervisor", + "count": 1, + "cost": 1747062.35, + "context": 20753267.0, + "out": 75895.0, + "minutes": 26.24556666612625, + "p": 48516.0, + "k": 128.0, + "w5": 274734.0, + "w1h": 0.0, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": null + }, + { + "version": "10.21.0", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "-", + "layer": "worker", + "role": "その他", + "agent_type": "ndf:worker", + "count": 1, + "cost": 309393.55, + "context": 2415131.0, + "out": 19637.0, + "minutes": 4.266183332602183, + "p": 37218.0, + "k": 32.0, + "w5": 75326.0, + "w1h": 0.0, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": null + }, + { + "version": "10.21.0", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "-", + "layer": "worker", + "role": "修正", + "agent_type": "ndf:worker", + "count": 1, + "cost": 1003114.4, + "context": 6347860.0, + "out": 34794.0, + "minutes": 17.8967666665713, + "p": 38773.0, + "k": 41.0, + "w5": 426393.0, + "w1h": 0.0, + "rewrites": 1, + "rewrites_after_5m": 1, + "rewrites_untimed": 0, + "rewrite_tokens": 211799, + "rewrite_tokens_after_5m": 211799, + "read_tokens_after_5m": 0, + "rewrite_gap_median": 10.697683334350586 + }, + { + "version": "10.21.0", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "-", + "layer": "worker", + "role": "検証", + "agent_type": "ndf:worker", + "count": 1, + "cost": 109062.6, + "context": 491200.0, + "out": 3708.0, + "minutes": 0.898633333047231, + "p": 36941.0, + "k": 10.0, + "w5": 54953.0, + "w1h": 0.0, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": null + }, + { + "version": "10.21.0", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "claude", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 1, + "cost": 642710.8, + "context": 6206645.0, + "out": 21851.0, + "minutes": 117.73070000012716, + "p": 40036.0, + "k": 58.0, + "w5": 0.0, + "w1h": 114359.0, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 510586, + "rewrite_gap_median": null + }, + { + "version": "10.22.0", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "claude", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 1, + "cost": 563963.3500000001, + "context": 2575061.0, + "out": 14610.0, + "minutes": 90.21243333419164, + "p": 40719.0, + "k": 25.0, + "w5": 0.0, + "w1h": 185694.0, + "rewrites": 1, + "rewrites_after_5m": 1, + "rewrites_untimed": 0, + "rewrite_tokens": 87680, + "rewrite_tokens_after_5m": 87680, + "read_tokens_after_5m": 577462, + "rewrite_gap_median": 122.4413166642189 + }, + { + "version": "10.22.0", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "claude", + "layer": "supervisor", + "role": "仕上げ", + "agent_type": "ndf:supervisor", + "count": 1, + "cost": 1102782.35, + "context": 9074813.0, + "out": 50041.0, + "minutes": 26.831983335812886, + "p": 44169.0, + "k": 71.0, + "w5": 332242.0, + "w1h": 0.0, + "rewrites": 1, + "rewrites_after_5m": 1, + "rewrites_untimed": 0, + "rewrite_tokens": 116235, + "rewrite_tokens_after_5m": 116235, + "read_tokens_after_5m": 0, + "rewrite_gap_median": 8.504666666189829 + }, + { + "version": "10.22.0", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "claude", + "layer": "supervisor", + "role": "取り込み", + "agent_type": "ndf:supervisor", + "count": 1, + "cost": 452847.9, + "context": 1628354.0, + "out": 10654.0, + "minutes": 30.936749998728434, + "p": 43599.0, + "k": 26.0, + "w5": 265086.0, + "w1h": 0.0, + "rewrites": 3, + "rewrites_after_5m": 3, + "rewrites_untimed": 0, + "rewrite_tokens": 194630, + "rewrite_tokens_after_5m": 194630, + "read_tokens_after_5m": 0, + "rewrite_gap_median": 10.093599998950959 + }, + { + "version": "10.22.0", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "claude", + "layer": "worker", + "role": "その他", + "agent_type": "ndf:worker", + "count": 6, + "cost": 382753.875, + "context": 2901634.8333333335, + "out": 18978.333333333332, + "minutes": 7.063391666279899, + "p": 33981.333333333336, + "k": 36.5, + "w5": 118925.66666666667, + "w1h": 0.0, + "rewrites": 2, + "rewrites_after_5m": 2, + "rewrites_untimed": 0, + "rewrite_tokens": 188218, + "rewrite_tokens_after_5m": 188218, + "read_tokens_after_5m": 0, + "rewrite_gap_median": 8.677425003051757 + }, + { + "version": "10.22.0", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "claude", + "layer": "worker", + "role": "修正", + "agent_type": "ndf:worker", + "count": 1, + "cost": 1237501.5999999999, + "context": 12184928.0, + "out": 55289.0, + "minutes": 8.502433331807454, + "p": 35146.0, + "k": 62.0, + "w5": 293077.0, + "w1h": 0.0, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": null + }, + { + "version": "10.22.0", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "-", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 7, + "cost": 510358.1357142857, + "context": 2867749.5714285714, + "out": 29324.285714285714, + "minutes": 5.495030953202929, + "p": 52091.71428571428, + "k": 26.571428571428573, + "w5": 0.0, + "w1h": 112973.71428571429, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": null + }, + { + "version": "10.22.0", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "claude+codex", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 1, + "cost": 941902.1499999999, + "context": 4317137.0, + "out": 76624.0, + "minutes": 13.185533332824708, + "p": 48191.0, + "k": 34.0, + "w5": 0.0, + "w1h": 175826.0, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": null + }, + { + "version": "10.22.0", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "-", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 3, + "cost": 2732319.066666668, + "context": 21330423.666666668, + "out": 77901.0, + "minutes": 212.98910555309718, + "p": 68121.0, + "k": 104.66666666666667, + "w5": 0.0, + "w1h": 654401.6666666666, + "rewrites": 7, + "rewrites_after_5m": 7, + "rewrites_untimed": 0, + "rewrite_tokens": 1304038, + "rewrite_tokens_after_5m": 1304038, + "read_tokens_after_5m": 3682705, + "rewrite_gap_median": 126.51821666558584 + }, + { + "version": "10.22.0", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "-", + "layer": "supervisor", + "role": "pr", + "agent_type": "claude -p", + "count": 1, + "cost": 14553.0, + "context": 5205.0, + "out": 829.0, + "minutes": 0.145, + "k": 1.0, + "w5": 0.0, + "w1h": 5203.0 + }, + { + "version": "10.22.0", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "-", + "layer": "worker", + "role": "work", + "agent_type": "claude -p", + "count": 1, + "cost": 90494.25, + "context": 403789.0, + "out": 5258.0, + "minutes": 11.803333333333335, + "k": 12.0, + "w5": 0.0, + "w1h": 22560.0 + }, + { + "version": "10.22.0", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "claude", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 1, + "cost": 1168681.8, + "context": 9966930.0, + "out": 35229.0, + "minutes": 127.01998333136241, + "p": 43738.0, + "k": 76.0, + "w5": 0.0, + "w1h": 253352.0, + "rewrites": 1, + "rewrites_after_5m": 1, + "rewrites_untimed": 0, + "rewrite_tokens": 116467, + "rewrite_tokens_after_5m": 116467, + "read_tokens_after_5m": 769959, + "rewrite_gap_median": 61.73666666746139 + }, + { + "version": "10.22.0", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.311", + "reviewers": "-", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 3, + "cost": 2972034.016666667, + "context": 29440378.0, + "out": 108132.0, + "minutes": 294.09906111293367, + "p": 35659.666666666664, + "k": 162.0, + "w5": 0.0, + "w1h": 491799.0, + "rewrites": 7, + "rewrites_after_5m": 6, + "rewrites_untimed": 0, + "rewrite_tokens": 665504, + "rewrite_tokens_after_5m": 628140, + "read_tokens_after_5m": 6169979, + "rewrite_gap_median": 93.00081666707993 + }, + { + "version": "10.22.0", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.312", + "reviewers": "-", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 1, + "cost": 1443442.0499999998, + "context": 10608792.0, + "out": 43206.0, + "minutes": 181.72081666787466, + "p": 36430.0, + "k": 78.0, + "w5": 0.0, + "w1h": 357337.0, + "rewrites": 3, + "rewrites_after_5m": 3, + "rewrites_untimed": 0, + "rewrite_tokens": 154561, + "rewrite_tokens_after_5m": 154561, + "read_tokens_after_5m": 289416, + "rewrite_gap_median": 119.95824999809265 + } + ], + "external": [ + { + "version": "10.21.0", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "claude", + "runtime": "claude", + "skill": "cross-review", + "cli_model": "claude-opus-5-5", + "count": 2, + "context": 163502.5, + "out": 2114.5, + "cost": 68050.15, + "minutes": 0.5113166670004526, + "p": 24023.0, + "k": 6.0, + "w5": 0.0, + "w1h": 25277.5, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": null, + "call_seats": 2 + }, + { + "version": "10.21.0", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "claude", + "runtime": "claude", + "skill": "cross-refactoring", + "cli_model": "claude-opus-5-5", + "count": 5, + "context": 758889.0, + "out": 9222.2, + "cost": 182342.47, + "minutes": 2.603163334528605, + "p": 22884.0, + "k": 15.6, + "w5": 0.0, + "w1h": 50388.4, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": null, + "call_seats": 5 + }, + { + "version": "10.22.0", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "claude", + "runtime": "claude", + "skill": "cross-review", + "cli_model": "claude-opus-5-5", + "count": 18, + "context": 55109.166666666664, + "out": 1826.8333333333333, + "cost": 31995.78611111112, + "minutes": 0.3982138889807242, + "p": 13908.5, + "k": 3.611111111111111, + "w5": 0.0, + "w1h": 10307.333333333334, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": null, + "call_seats": 18 + }, + { + "version": "10.22.0", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "claude+codex", + "runtime": "claude", + "skill": "cross-review", + "cli_model": "claude-opus-5-5", + "count": 3, + "context": 332272.0, + "out": 6398.0, + "cost": 139735.13333333333, + "minutes": 1.2893111122979057, + "p": 18942.0, + "k": 8.333333333333334, + "w5": 0.0, + "w1h": 46726.0, + "rewrites": 1, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 37257, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": 0.10601666768391928, + "call_seats": 3 + }, + { + "version": "10.22.0", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "claude+codex", + "runtime": "codex", + "skill": "cross-review", + "cli_model": "gpt-6.1-sol", + "count": 4, + "input": 401546.25, + "cached": 341856.0, + "out": 2282.75, + "minutes": 0.9056958327690761, + "p": 19658.5, + "k": 8.0, + "rewrites": 1, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 26805, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": 0.13416666587193807, + "call_seats": 4 + }, + { + "version": "10.22.0", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "claude", + "runtime": "claude", + "skill": "cross-review", + "cli_model": "claude-opus-5-5", + "count": 12, + "context": 243302.66666666666, + "out": 3538.0, + "cost": 102175.44166666667, + "minutes": 0.6846222218539979, + "p": 26167.666666666668, + "k": 6.333333333333333, + "w5": 0.0, + "w1h": 37081.166666666664, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": null, + "call_seats": 12 + } + ], + "meta": { + "by": [ + "version", + "mode", + "model", + "cc", + "reviewers" + ], + "min_version": "10.21.0", + "until": "2026-10-28T10:01:00Z", + "sessions": 19, + "sessions_with_pr": 4, + "unlinked_external": 560, + "unlinked_ledger": 0, + "skipped": { + "ndf の Skill が無い": 499, + "版を判定できない": 17 + }, + "released": "10.23.0", + "previous": "2026-10-25-3.json" + } +} diff --git a/docs/metrics/ndf-token-usage/2026-10-28.md b/docs/metrics/ndf-token-usage/2026-10-28.md new file mode 100644 index 000000000..1accbdc9b --- /dev/null +++ b/docs/metrics/ndf-token-usage/2026-10-28.md @@ -0,0 +1,115 @@ +# ndf の版ごとのトークン消費と所要時間(2026-10-28 集計・10.23.0 の配布) + +**10.23.0 を正式版として出したときの記録である。** 打ち切りの時刻は `2026-10-28T10:01:00Z`。表の最初の行は 10.21.0(前の記録: 2026-10-25-3.md)。既定では前の記録の最後の版で、記録どうしが 1 行ずつ重なる。生の記録・会話の本文・リポジトリ名・会話の ID は残していない。集計した日時点の記録で、以後の記録の増減には追随しない。 + +## 作り方 + +同じ打ち切りの時刻で走らせ直すと、記録が残っている間は同じ `.json` ができる。ただし kiro の記録はターンに時刻を持たないため、打ち切りをまたいで進行中だった席は credit が変わりうる。 + +```bash +python3 scripts/token-usage-snapshot.py --released 10.23.0 --until 2026-10-28T10:01:00Z --min-version 10.21.0 +python3 scripts/token-usage.py --min-version 10.21.0 --until 2026-10-28T10:01:00Z --by version # 末尾の「集計の出力」 +``` + +## 比べるときの注意 + +- **PR を作った会話が 2 件以下の版:** 10.21.0(1 件)。作業の中身の違いが版の違いより大きい +- **CHANGELOG.md にあって表に出ない版:** 10.23.0。その版で始めた PR つきの会話が無い(会話の版は最初に読んだ版で決まる) +- **打ち切りの 30 分前以降にも行がある会話を含む版:** 10.22.0。進行中だった会話は、値が打ち切りの時刻までの分である + +## 版ごとの差(PR 1 本あたり) + +換算の合計は conductor・supervisor・worker の換算の和。前の版との差は、表の 1 つ上の行との比である。±30% を超えた版を下の「読み取り」で扱う。 + +| ndf の版 | 会話 | PR | 換算の合計 | 前の版との差 | conductor | supervisor | worker | 入力 | 所要 | codex 入力 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 10.21.0 | 1 | 2 | 1.94M | - | 0.35M | 0.87M | 0.71M | 18.39M | 24.5 | 0.00M | +| 10.22.0 | 3 | 10 | 1.49M | -23% | 0.98M | 0.16M | 0.35M | 11.82M | 83.6 | 0.00M | + +## 版と層ごとの呼び出しとキャッシュ(1 起動あたり) + +P と k と書き込みは 1 起動あたり、書き直しとうち 5 分超は合計の回数である。 + +| ndf の版 | 層 | 起動 | P | k | 書き込み 5 分 | 書き込み 1 時間 | 書き直し | うち 5 分超 | 1 起動あたりの書き直し | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 10.21.0 | conductor | 2 | 42.8k | 56.5 | 0.00M | 0.12M | 0 | 0 | 0.00 | +| 10.21.0 | supervisor | 1 | 48.5k | 128.0 | 0.27M | 0.00M | 0 | 0 | 0.00 | +| 10.21.0 | worker | 3 | 37.6k | 27.7 | 0.19M | 0.00M | 1 | 1 | 0.33 | +| 10.22.0 | conductor | 17 | 49.7k | 70.5 | 0.00M | 0.31M | 19 | 18 | 1.12 | +| 10.22.0 | supervisor | 3 | 43.9k | 32.7 | 0.20M | 0.00M | 4 | 4 | 2.00 | +| 10.22.0 | worker | 8 | 34.1k | 36.6 | 0.13M | 0.00M | 2 | 2 | 0.29 | + +## 読み取り + +差の大きい版は無い(すべて ±30% の内)。読み取りは要らない。 + +## 集計の出力 + +対象の会話: 19 件 / PR を作った会話: 4 件 / 寄せ先の無い外部 CLI: 560 件 / 寄せ先の無い帳簿の行: 0 件 + +換算は input を 1 とした費用(cache read はモデル別: claude-opus-5-5 0.05 / claude-fable-5-1 0.025 / 他 0.1。cache write 5 分 1.25・1 時間 2 / output 5)。入力は input + cache read + cache write。所要は分。 + +### PR 1 本あたり + +| 動いた版 | 会話 | PR | conductor 換算 | supervisor 換算 | worker 換算 | 入力 | 出力 | 所要 | codex 入力 | codex 出力 | kiro credit | claude 席 換算 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 10.21.0 | 1 | 2 | 0.35M | 0.87M | 0.71M | 18.39M | 79.0k | 24.5 | 0.00M | 0.0k | 0.00 | 0.00M | +| 10.22.0 | 3 | 10 | 0.98M | 0.16M | 0.35M | 11.82M | 52.2k | 83.6 | 0.00M | 0.0k | 0.00 | 0.06M | + +### フェーズごと(1 起動あたり) + +| 動いた版 | 層 | フェーズ | 定義 | 起動 | 換算 | 入力 | 出力 | 所要 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 10.21.0 | conductor | - | - | 2 | 0.67M | 6.49M | 22.9k | 83.4 | +| 10.21.0 | supervisor | 仕上げ | ndf:supervisor | 1 | 1.75M | 20.75M | 75.9k | 26.2 | +| 10.21.0 | worker | その他 | ndf:worker | 1 | 0.31M | 2.42M | 19.6k | 4.3 | +| 10.21.0 | worker | 修正 | ndf:worker | 1 | 1.00M | 6.35M | 34.8k | 17.9 | +| 10.21.0 | worker | 検証 | ndf:worker | 1 | 0.11M | 0.49M | 3.7k | 0.9 | +| 10.22.0 | conductor | - | - | 17 | 1.46M | 11.76M | 54.9k | 116.0 | +| 10.22.0 | supervisor | pr | claude -p | 1 | 0.01M | 0.01M | 0.8k | 0.1 | +| 10.22.0 | supervisor | 仕上げ | ndf:supervisor | 1 | 1.10M | 9.07M | 50.0k | 26.8 | +| 10.22.0 | supervisor | 取り込み | ndf:supervisor | 1 | 0.45M | 1.63M | 10.7k | 30.9 | +| 10.22.0 | worker | work | claude -p | 1 | 0.09M | 0.40M | 5.3k | 11.8 | +| 10.22.0 | worker | その他 | ndf:worker | 6 | 0.38M | 2.90M | 19.0k | 7.1 | +| 10.22.0 | worker | 修正 | ndf:worker | 1 | 1.24M | 12.18M | 55.3k | 8.5 | + +### フェーズごとの呼び出しとキャッシュ + +P は起動ごとの固定費(最初の呼び出しの文脈)、k は呼び出し回数(どちらも 1 起動あたり)。書き直しは 2 回目以降の呼び出しのうち、書き込みが文脈の 50% を超え、かつ 20k を超えたものの回数(合計)。5 分超はそのうち直前の呼び出しとの間隔が 5 分を超えた回数、間隔は直前の間隔の中央値(分)。5 分超の書き直し・読み込みは、直前の間隔が 5 分を超えた呼び出しの書き直しの量と、書き直しでない呼び出しの読み込みの量(どちらも合計)。定義はサブエージェントの定義の名前。 + +| 動いた版 | 層 | フェーズ | 定義 | 起動 | P | k | 書き込み 5 分 | 書き込み 1 時間 | 書き直し | 5 分超 | 5 分超の書き直し | 5 分超の読み込み | 間隔 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 10.21.0 | conductor | - | - | 2 | 42.8k | 56.5 | 0.00M | 0.12M | 0 | 0 | 0.00M | 0.99M | - | +| 10.21.0 | supervisor | 仕上げ | ndf:supervisor | 1 | 48.5k | 128.0 | 0.27M | 0.00M | 0 | 0 | 0.00M | 0.00M | - | +| 10.21.0 | worker | その他 | ndf:worker | 1 | 37.2k | 32.0 | 0.08M | 0.00M | 0 | 0 | 0.00M | 0.00M | - | +| 10.21.0 | worker | 修正 | ndf:worker | 1 | 38.8k | 41.0 | 0.43M | 0.00M | 1 | 1 | 0.21M | 0.00M | 10.7 | +| 10.21.0 | worker | 検証 | ndf:worker | 1 | 36.9k | 10.0 | 0.05M | 0.00M | 0 | 0 | 0.00M | 0.00M | - | +| 10.22.0 | conductor | - | - | 17 | 49.7k | 70.5 | 0.00M | 0.31M | 19 | 18 | 2.29M | 11.49M | 105.9 | +| 10.22.0 | supervisor | pr | claude -p | 1 | - | 1.0 | 0.00M | 0.01M | - | - | - | - | - | +| 10.22.0 | supervisor | 仕上げ | ndf:supervisor | 1 | 44.2k | 71.0 | 0.33M | 0.00M | 1 | 1 | 0.12M | 0.00M | 8.5 | +| 10.22.0 | supervisor | 取り込み | ndf:supervisor | 1 | 43.6k | 26.0 | 0.27M | 0.00M | 3 | 3 | 0.19M | 0.00M | 10.1 | +| 10.22.0 | worker | work | claude -p | 1 | - | 12.0 | 0.00M | 0.02M | - | - | - | - | - | +| 10.22.0 | worker | その他 | ndf:worker | 6 | 34.0k | 36.5 | 0.12M | 0.00M | 2 | 2 | 0.19M | 0.00M | 8.7 | +| 10.22.0 | worker | 修正 | ndf:worker | 1 | 35.1k | 62.0 | 0.29M | 0.00M | 0 | 0 | 0.00M | 0.00M | - | + +### 外部 CLI(1 起動あたり) + +kiro はトークン数を記録しない(値が 0)ため credit だけを載せる。agy は読まない。 + +| 動いた版 | ランタイム | Skill | モデル | 起動 | 入力 | cache | 出力 | credit | 所要 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 10.21.0 | claude | cross-review | claude-opus-5-5 | 2 | 0.16M | 0.00M | 2.1k | 0.00 | 0.5 | +| 10.21.0 | claude | cross-refactoring | claude-opus-5-5 | 5 | 0.76M | 0.00M | 9.2k | 0.00 | 2.6 | +| 10.22.0 | claude | cross-review | claude-opus-5-5 | 33 | 0.15M | 0.00M | 2.9k | 0.00 | 0.6 | +| 10.22.0 | codex | cross-review | gpt-6.1-sol | 4 | 0.40M | 0.34M | 2.3k | 0.00 | 0.9 | + +### 外部 CLI の呼び出しとキャッシュ(1 起動あたり) + +codex は input のうち cached に当たらなかった分を書き込みとみなし、呼び出しごとの値(last_token_usage)を持つ席だけで数える。kiro は呼び出し回数を記録しない(利用者のターン数は JSON の turns)。取れない値は -。 + +| 動いた版 | ランタイム | Skill | モデル | 起動 | P | k | 書き直し | 5 分超 | 5 分超の書き直し | 5 分超の読み込み | 間隔 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 10.21.0 | claude | cross-review | claude-opus-5-5 | 2 | 24.0k | 6.0 | 0 | 0 | 0.00M | 0.00M | - | +| 10.21.0 | claude | cross-refactoring | claude-opus-5-5 | 5 | 22.9k | 15.6 | 0 | 0 | 0.00M | 0.00M | - | +| 10.22.0 | claude | cross-review | claude-opus-5-5 | 33 | 18.8k | 5.0 | 1 | 0 | 0.00M | 0.00M | 0.1 | +| 10.22.0 | codex | cross-review | gpt-6.1-sol | 4 | 19.7k | 8.0 | 1 | 0 | 0.00M | 0.00M | 0.1 | diff --git a/docs/versioning-and-distribution.md b/docs/versioning-and-distribution.md index e09c0a788..6cf0c526b 100644 --- a/docs/versioning-and-distribution.md +++ b/docs/versioning-and-distribution.md @@ -57,15 +57,15 @@ semver の順序で除外されるのは、プラグイン間の依存解決(` | 版 | 形 | 意味 | | --- | --- | --- | -| 正式版 | `10.22.0` | 利用者が常用してよい | -| 開発版 | `10.23.0-dev.1` | 検証中。入れたくない利用者は取得を控えられる | -| 公開前の確認版 | `10.23.0-rc.1` | 正式版の候補。残るのは確認だけ | +| 正式版 | `10.23.0` | 利用者が常用してよい | +| 開発版 | `10.24.0-dev.1` | 検証中。入れたくない利用者は取得を控えられる | +| 公開前の確認版 | `10.24.0-rc.1` | 正式版の候補。残るのは確認だけ | -- 接尾辞は**次に出す正式版の版数へ付ける**。`10.22.0` の次を開発するなら `10.23.0-dev.1` +- 接尾辞は**次に出す正式版の版数へ付ける**。`10.23.0` の次を開発するなら `10.24.0-dev.1` - 連番は開発版を出すたびに増やす。**同じ版数で中身を差し替えない**。差し替えると、利用者の 手元にある版と `main` の版が同じ番号で別物になり、何を確かめたのかが分からなくなる -- **正式版を出すときは接尾辞を外す。** `10.23.0-dev.3` の次は `10.23.0` -- 順序は semver に従い `10.23.0-dev.1` < `10.23.0-rc.1` < `10.23.0` になる +- **正式版を出すときは接尾辞を外す。** `10.24.0-dev.3` の次は `10.24.0` +- 順序は semver に従い `10.24.0-dev.1` < `10.24.0-rc.1` < `10.24.0` になる ## ランタイムごとの取得と導入 diff --git a/plugins/ndf/.claude-plugin/plugin.json b/plugins/ndf/.claude-plugin/plugin.json index 2aa3004cf..905fffc1b 100644 --- a/plugins/ndf/.claude-plugin/plugin.json +++ b/plugins/ndf/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ndf", - "version": "10.22.0", - "description": "Claude Code plugin (v10.22.0): 8 specialized agents (plus 2 supervisor agents and 1 worker agent for the 3-layer workflow) and 49 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.23.0", + "description": "Claude Code plugin (v10.23.0): 8 specialized agents (plus 2 supervisor agents and 1 worker agent for the 3-layer workflow) and 49 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 dac9cd22c..22bf83a22 100644 --- a/plugins/ndf/.codex-plugin/plugin.json +++ b/plugins/ndf/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ndf", - "version": "10.22.0", - "description": "Codex plugin (v10.22.0): 45 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.23.0", + "description": "Codex plugin (v10.23.0): 45 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 bf9f26b76..e3793fee5 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.22.0) +# => NDF統合開発エージェント(Kiro CLI用 / v10.23.0) ``` ### agy @@ -412,7 +412,7 @@ agy models # 認証確認 ```text # 動く: 実体パスを示して読ませる -~/.codex/plugins/cache/ai-plugins/ndf/10.22.0/skills/deploy/SKILL.md を読んで、その手順どおりに qa/staging へ deploy PR を作成してください。 +~/.codex/plugins/cache/ai-plugins/ndf/10.23.0/skills/deploy/SKILL.md を読んで、その手順どおりに qa/staging へ deploy PR を作成してください。 # 動かない: 明示起動 ($ は展開されない) $deploy qa/staging @@ -434,14 +434,14 @@ marketplace 経由でインストールした場合、Skill の実体は **ワ ```text $CODEX_HOME/plugins/cache////skills//SKILL.md # 既定 ($CODEX_HOME=~/.codex) の例: -# ~/.codex/plugins/cache/ai-plugins/ndf/10.22.0/skills/deploy/SKILL.md +# ~/.codex/plugins/cache/ai-plugins/ndf/10.23.0/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.22.0 +# => ndf@ai-plugins installed, enabled 10.23.0 ``` 抑止していない Skill(`document-style` など)はキャッシュ配下でも `$` で解決するため、そちらは `$` 起動が使えます。 diff --git a/plugins/ndf/dev.agy/plugin.json b/plugins/ndf/dev.agy/plugin.json index 41479cb15..a8104cd94 100644 --- a/plugins/ndf/dev.agy/plugin.json +++ b/plugins/ndf/dev.agy/plugin.json @@ -1,5 +1,5 @@ { "name": "ndf", - "version": "10.22.0", - "description": "Antigravity CLI plugin (v10.22.0): 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." + "version": "10.23.0", + "description": "Antigravity CLI plugin (v10.23.0): 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/playwright-kit/.claude-plugin/plugin.json b/plugins/playwright-kit/.claude-plugin/plugin.json index b1de78855..73fe74efb 100644 --- a/plugins/playwright-kit/.claude-plugin/plugin.json +++ b/plugins/playwright-kit/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "playwright-kit", - "version": "3.0.1", - "description": "Claude Code plugin (v3.0.1): 4 Playwright E2E skills — test planning by page role, script authoring with video / trace evidence, report generation with collected evidence, and the playwright_kit runtime (init, a11y / CWV scans).", + "version": "3.0.2", + "description": "Claude Code plugin (v3.0.2): 4 Playwright E2E skills — test planning by page role, script authoring with video / trace evidence, report generation with collected evidence, and the playwright_kit runtime (init, a11y / CWV scans).", "author": { "name": "takemi-ohama", "url": "https://github.com/takemi-ohama" diff --git a/plugins/playwright-kit/plugin.json b/plugins/playwright-kit/plugin.json index f3cc03eb3..4f35ccda5 100644 --- a/plugins/playwright-kit/plugin.json +++ b/plugins/playwright-kit/plugin.json @@ -1,8 +1,8 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "playwright-kit", - "version": "3.0.1", - "description": "Agent Plugins package (v3.0.1): 4 Playwright E2E skills — test planning by page role, script authoring with video / trace evidence, report generation with collected evidence, and the playwright_kit runtime (init, a11y / CWV scans).", + "version": "3.0.2", + "description": "Agent Plugins package (v3.0.2): 4 Playwright E2E skills — test planning by page role, script authoring with video / trace evidence, report generation with collected evidence, and the playwright_kit runtime (init, a11y / CWV scans).", "author": { "name": "takemi-ohama", "url": "https://github.com/takemi-ohama"