Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
47ff274
Docs: 実装計画を書く(P6: 共通層と cross-review。#727 #687 #478 #648)
takemi-ohama Sep 19, 2026
0bf990a
Add: 共通層に使える者の解決・席の埋め方・止めない確認・再開の反映を足す(#727 #687 #478 #648)
takemi-ohama Sep 19, 2026
841d3c5
Add: cross-review の初期化・担当・再開・完了報告を共通層へ載せ替える(#727 #687 #478 #648)
takemi-ohama Sep 19, 2026
78c9900
Add: 席の名前を起動・監視・計測に通し、手順書と文書を新しい引数へ合わせる(#727 #687 #478 #648)
takemi-ohama Sep 19, 2026
0cbb5aa
Docs: 実装で決めた 3 件を設計文書へ書き、共通層の索引に新しい役割を足す(#727 #687 #478 #648)
takemi-ohama Sep 19, 2026
8c96ca3
Merge remote-tracking branch 'origin/develop' into feat/issue-727-par…
takemi-ohama Sep 21, 2026
c776509
Merge remote-tracking branch 'origin/develop' into feat/issue-727-par…
takemi-ohama Sep 21, 2026
6506be8
Test: detect_host の分岐を現状固定
takemi-ohama Sep 21, 2026
688966a
Docs: 改修計画を記録する(cross-refactoring 進行側)
takemi-ohama Sep 21, 2026
8c60a7a
Test: review_seats の分岐を現状固定
takemi-ohama Sep 21, 2026
f27808c
Docs: 改修計画を記録する(cross-refactoring 進行側)
takemi-ohama Sep 21, 2026
0f12eb8
Test: review_seats の使える者も埋め合わせも空の経路を現状固定 — plugins/ndf/scripts/lib/as…
takemi-ohama Sep 21, 2026
e07c95b
Docs: 改修計画を記録する(cross-refactoring 進行側)
takemi-ohama Sep 21, 2026
36dd097
Test: seat_runtime の境界値と異常値を現状固定 — plugins/ndf/scripts/lib/assignment…
takemi-ohama Sep 21, 2026
7815882
Revert "Test: seat_runtime の境界値と異常値を現状固定 — plugins/ndf/scripts/lib/as…
takemi-ohama Sep 21, 2026
15130d3
Docs: 改修計画を記録する(cross-refactoring 進行側)
takemi-ohama Sep 21, 2026
1baa87f
Test: seat_runtime の現状固定テストを追加
takemi-ohama Sep 21, 2026
4d090a6
Docs: 改修計画を記録する(cross-refactoring 進行側)
takemi-ohama Sep 21, 2026
72bc01b
Test: assign の下限未満エラーと全除外時の resolve_participants をテスト
takemi-ohama Sep 21, 2026
13e01c5
Docs: 改修計画を記録する(cross-refactoring 進行側)
takemi-ohama Sep 21, 2026
9d8fe3a
Test: characterization — plugins/ndf/scripts/lib/assignment.py#review…
takemi-ohama Sep 21, 2026
0afd36f
Docs: 改修計画を記録する(cross-refactoring 進行側)
takemi-ohama Sep 21, 2026
0c3b60c
Test: characterization — plugins/ndf/scripts/lib/assignment.py#review…
takemi-ohama Sep 21, 2026
01a221b
Revert "Test: characterization — plugins/ndf/scripts/lib/assignment.p…
takemi-ohama Sep 21, 2026
970d20c
Docs: 改修計画を記録する(cross-refactoring 進行側)
takemi-ohama Sep 21, 2026
da936fe
Test: review_assign の無効ホストを現状固定
takemi-ohama Sep 21, 2026
a8a691f
Docs: 改修計画を記録する(cross-refactoring 進行側)
takemi-ohama Sep 21, 2026
1a81a1a
Refactor: 引数オブジェクト導入とパイプライン・ヘルパー抽出による構造改善
takemi-ohama Sep 21, 2026
3bb7c14
Revert "Refactor: 引数オブジェクト導入とパイプライン・ヘルパー抽出による構造改善"
takemi-ohama Sep 21, 2026
ff3e6cf
Docs: 改修計画を記録する(cross-refactoring 進行側)
takemi-ohama Sep 21, 2026
bb4fc72
Refactor: consolidate_duplication — plugins/ndf/skills/cross-review/s…
takemi-ohama Sep 21, 2026
578a9e5
Docs: 改修計画を記録する(cross-refactoring 進行側)
takemi-ohama Sep 21, 2026
f6e9df3
Refactor: 適用ラウンド 1(R4-001〜R4-004) — 長い関数の分割と重複の統合
takemi-ohama Sep 21, 2026
6d0231b
Docs: 改修計画を記録する(cross-refactoring 進行側)
takemi-ohama Sep 21, 2026
068c92e
Refactor: extract_method — plugins/ndf/scripts/lib/run_metrics.py#_se…
takemi-ohama Sep 21, 2026
a05b552
Docs: 改修計画を記録する(cross-refactoring 進行側)
takemi-ohama Sep 21, 2026
89bb86f
Refactor: replace_with_bulk_operation / extract_method — auth.py, sta…
takemi-ohama Sep 22, 2026
2f3aa53
Revert "Refactor: replace_with_bulk_operation / extract_method — auth…
takemi-ohama Sep 22, 2026
7a5567b
Docs: 改修計画を記録する(cross-refactoring 進行側)
takemi-ohama Sep 22, 2026
b42b427
Refactor: extract_method — plugins/ndf/scripts/lib で長いメソッドを段に分ける
takemi-ohama Sep 22, 2026
63abc1f
Revert "Refactor: extract_method — plugins/ndf/scripts/lib で長いメソッドを段に…
takemi-ohama Sep 22, 2026
51b3675
Docs: 改修計画を記録する(cross-refactoring 進行側)
takemi-ohama Sep 22, 2026
5f63640
Refactor: introduce_parameter_object — plugins/ndf/scripts/lib/metric…
takemi-ohama Sep 22, 2026
225e399
Revert "Refactor: introduce_parameter_object — plugins/ndf/scripts/li…
takemi-ohama Sep 22, 2026
d3029e0
Docs: 改修計画を記録する(cross-refactoring 進行側)
takemi-ohama Sep 22, 2026
5172466
Fix: 1 者指定でも使える者が 0 者なら初期化を止める(レビュー指摘対応)
takemi-ohama Sep 22, 2026
b9888ce
Fix: 2 席目の監視の上限を席の名前でなくランタイム名で引く(レビュー指摘対応)
takemi-ohama Sep 22, 2026
7af3b78
Docs: 再開で渡した引数の扱いの置き場所を受け入れ条件へ反映する(#727 #687 #478 #648)
takemi-ohama Sep 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions issues/issue-727-687-478-664-648-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,17 +431,22 @@ P6(共通層と cross-review)→ P7(cross-refactoring と旧関数の削

## 未確認のまま残ること

7 件が残る。3 件は実装で決め、4 件は運用と #461 が決める。
4 件が残る。実装で決める 3 件は P6 で決まった(下の表の後)。残る 4 件は運用と #461 が決める。

| 項目 | 内容 | いつ決まるか |
| --- | --- | --- |
| 同じランタイムの 2 席の観点 | `claude` / `claude-2` の 2 席が、別のランタイムの 2 席より指摘を見落とすかは測っていない | この変更の後の運用(`measure.py`) |
| ホストが席に入ったときの `is_own_pr` の扱い | ホストの CLI が自分の Pull Request をレビューするとき、投稿の event が `COMMENT` へ倒れる既存の規則で足りるかは確かめていない | P6 の実装で 1 度回して見る |
| ホストが席に入ったときの `is_own_pr` の扱い | ホストの CLI が自分の Pull Request をレビューするとき、投稿の event が `COMMENT` へ倒れる既存の規則で足りるかは確かめていない。P6 では実機で回していない | P6 の後の運用で 1 度回して見る |
| cross-refactoring でホストが提案に入ることの所要 | 提案は最も遅い者を待つ。claude の提案の所要は測っていない(適用は中央値 2 分) | P7 の後の運用 |
| 確認を「言語モデルを引く最小の呼び出し」へ替えるか | #461。所要の実測が要る | マイルストーン 06 の着手時 |
| `monitor.py` の CLI 固有の検査を席の名前に通す形 | `seat_runtime` で選ぶと決めたが、`agent` の比較が 6 か所あり、共通の関数へ寄せるかは実装で決める | **実装で決める** |
| 出力の文言 | 反映した行・知らせる行・埋め合わせの行の文言は、項目名と値を含むことだけを決めた | **実装で決める** |
| テストの置き場所 | テスト設計の表の置き場所は既存ファイルに合わせた目安である | **実装で決める** |

### P6 の実装で決めた 3 件

| 項目 | 決めたこと |
| --- | --- |
| `monitor.py` の CLI 固有の検査を席の名前に通す形 | 席の名前からランタイムを引く内部関数を 1 つ置き、CLI 固有の比較 3 か所をその関数で包む。**席の形に合わない名前はそのまま返す**(担当名を任意の骨格で受ける cross-refactoring の経路を壊さないため)。位置引数の選択肢は席の形を受ける型の検査へ替えた |
| 出力の文言 | 反映した行は `↻ <項目>: <旧> → <新>`、知らせる行は `ℹ --<引数> は再開では反映しません(状態: <値> / 指定: <値>)`、通らなかった者は `⚠ <名前> を担当から外しました(<理由>)`、埋め合わせは `⚠ 使える者が <数> 者のため、席を<相手>で埋めます(観点が減ります)`。既存の初期化の出力の印(`↻` / `ℹ` / `⚠`)に揃える |
| テストの置き場所 | テスト設計の表のとおり。席の埋め方は cross-refactoring の割り当てのテスト(変更前の席の割り当ての期待値が同じファイルにある)、起動と監視と計測の席の名前は cross-review の `tests/test_seat_names.py`(新設) |

## 申し送り(並行する設計との境界)

Expand Down
2 changes: 1 addition & 1 deletion issues/issue-727-687-478-664-648-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@
| `SKILL.md` | 引数の表と `argument-hint` に `--exclude` / `--include` / `--require-all`。`--only` の説明から「デバッグ用」が消える。母集合の行が席の規則を指す |
| `docs/05-pool-and-convergence.md` | 使える者の解決と席の埋め方(3 者以上 / 2 者 / 1 者 / 0 者)、`--exclude` / `--include`、確認が把握になったこと |
| `docs/04-contracts.md` | 状態ファイルの `participants` と `resume_changes`、席の名前の形 |
| `docs/01-state-and-review.md` | 再開で反映する引数と、反映しない引数 |
| `docs/01-state-and-review.md` | 再開で渡した引数の扱いが `docs/04-contracts.md` にあることへの案内 |

### 子 issue の再現手順

Expand Down
208 changes: 208 additions & 0 deletions issues/issue-727-p6-participants-plan.md

Large diffs are not rendered by default.

399 changes: 399 additions & 0 deletions issues/refactoring-plan-rf793.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions plugins/ndf/scripts/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@
| [monitor_outcome.py](monitor_outcome.py) | 監視の結果の理由の語彙(9 語)と起動し直しの可否、結果ファイル・監視の記録の読み書き(#662)、起動 1 回の結末を 1 つの値として読む `read_launch_outcome`(#729) | 同上 |
| [launch-cli.sh](launch-cli.sh) | claude / codex / agy / kiro をランタイム名で分岐して背景起動する | 同上 |
| [_tmpdir.sh](_tmpdir.sh) | 一時ディレクトリの解決。環境変数名とディレクトリ名を引数で受ける | 同上 |
| [statefile.py](statefile.py) | 状態ファイルの読み書きと KEY=VALUE 出力、保存の後の差し込み口 | 同上 |
| [statefile.py](statefile.py) | 状態ファイルの読み書きと KEY=VALUE 出力、保存の後の差し込み口、再開で渡した引数の反映(#727) | 同上 |
| [auth.py](auth.py) | 参加する CLI の認証の確認。止めずに結果だけを返す形を持つ(#727) | 同上 |
| [run_metrics.py](run_metrics.py) | 実行の要約を作業ツリーの外へ書き、束ねて出す(`aggregate`、#662) | 同上 |
| [assignment.py](assignment.py) | ホスト判定、役割ごとの母集合の確定、担当の輪番 | 同上 |
| [assignment.py](assignment.py) | ホスト判定、母集合の確定、使える者の解決、席の埋め方と席の名前、担当の輪番(#727) | 同上 |
| [models.py](models.py) | `--model` の解析、フラグ生成、実測値の突き合わせ | 同上 |
| [metrics.py](metrics.py) | 担当ごとの指標算出と報告の整形 | 同上 |
| [post_queue.py](post_queue.py) | 上限のときに投稿を積む待ち行列と、上限の見分け(#291) | 同上 |
Expand Down
223 changes: 222 additions & 1 deletion plugins/ndf/scripts/lib/assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,23 @@
参加する 4 者はいずれも NDF の配布先であるため、**適用から外す者はいない**。
ホストは提案・レビューから外れるが適用には入るため、2 つの母集合は重なるが
一致しない。輪番の式はホストによらず同じ形になる。

## 使える者の解決と席の埋め方(#727)

参加者は「母集合の既定 ∪ 足す者 − 外す者」で決め(`resolve_participants`)、確認を
通った者だけを使える者(`available`)として記録する。cross-refactoring の母集合の
既定は `refactor_pool(host)`(`DEFAULT_REFACTOR_RUNTIMES` とホスト)、cross-review は
`review_pool(host)` のまま。担当の単位は席の名前(`SEAT_PATTERN`。`claude-2` のように
同じランタイムの 2 つ目を表す)で、cross-review の 2 席は `review_seats` が、
cross-refactoring の適用担当は `impl_assign` が決める。上の表と `impl_pool` /
`review_assign` / `assign` は、母集合が 1 つになる次の Pull Request(P7)まで残す。
"""
from __future__ import annotations

import os
from typing import Mapping, Optional
import re
from dataclasses import dataclass, field
from typing import Any, Callable, Iterable, Mapping, Optional

# 固定順。輪番の再現性を保つため並べ替えない。
ALL_RUNTIMES: tuple[str, ...] = ("claude", "codex", "agy", "kiro")
Expand All @@ -28,6 +40,15 @@
# 別の問いで、配布先でない CLI が参加 CLI に加わると 2 つは再び分かれる。
HOST_RUNTIMES: tuple[str, ...] = ALL_RUNTIMES

# cross-refactoring の既定の参加者の表(ホストを除いた部分。設計の決定 4)。ホストは
# `refactor_pool(host)` が足す。表に無い者(agy)は `--include` で足す(#727)。
DEFAULT_REFACTOR_RUNTIMES: tuple[str, ...] = ("codex", "kiro")

# 席の名前の形: `^(claude|codex|agy|kiro)(-[2-9])?$`。ランタイム名そのままが 1 つ目の席、
# ハイフンと 2〜9 の接尾辞が同じランタイムの 2 つ目以降(設計の決定 10)。ランタイム名に
# ハイフンを含むものが無いため、シェル側の切り出し(`${SEAT%%-*}`)と同じ規則になる。
SEAT_PATTERN = re.compile(rf"^({'|'.join(ALL_RUNTIMES)})(-[2-9])?$")

# ホスト推定に使う環境変数。値の中身は見ず、**存在するかどうか**だけで判定する。
HOST_ENV_HINTS: tuple[tuple[str, str], ...] = (
("CLAUDE_PLUGIN_ROOT", "claude"),
Expand Down Expand Up @@ -135,3 +156,203 @@ def assign(round_no: int, host: str) -> tuple[str, list[str]]:
dropped = (round_no // len(pool)) % len(candidates)
candidates = [r for i, r in enumerate(candidates) if i != dropped]
return impl, candidates


def _in_fixed_order(names: Iterable[str]) -> list[str]:
"""`ALL_RUNTIMES` の順に並べ直す(重複は 1 つにする)。"""
wanted = set(names)
return [r for r in ALL_RUNTIMES if r in wanted]


def refactor_pool(host: str) -> list[str]:
"""cross-refactoring の母集合の既定。`DEFAULT_REFACTOR_RUNTIMES` とホストの和集合。

ホストが変わっても一覧を書き直さずに済むように、既定は「ホストを除いた部分」
だけを持ち、ホストをここで足す(設計の決定 4)。並びは `ALL_RUNTIMES` の順。
"""
if host not in HOST_RUNTIMES:
raise AssignmentError(f"ホストになれないランタイムです: {host}")
return _in_fixed_order((*DEFAULT_REFACTOR_RUNTIMES, host))


@dataclass
class Participants:
"""使える者の解決の結果。状態ファイルの `participants` のうち `fallback` を除く 7 項目。

`fallback`(席の埋め合わせに使える者)は cross-review だけが持つため、呼び出し側が
`to_state()` の辞書へ足す。
"""
pool: list[str]
included: list[str] = field(default_factory=list)
excluded: list[str] = field(default_factory=list)
available: list[str] = field(default_factory=list)
unavailable: dict[str, str] = field(default_factory=dict)
probe_skipped: bool = False
require_all: bool = False

def to_state(self) -> dict[str, Any]:
return {
"pool": list(self.pool),
"included": list(self.included),
"excluded": list(self.excluded),
"available": list(self.available),
"unavailable": dict(self.unavailable),
"probe_skipped": self.probe_skipped,
"require_all": self.require_all,
}


# 止めない確認の形。`auth.probe_auth` を `functools.partial(auth.probe_auth, info=info)`
# のように包んで渡す。返り値は `(名前 → {"command", "ok", "detail"}, 飛ばしたか)`。
Probe = Callable[[list[str]], tuple[dict[str, dict[str, Any]], bool]]


def resolve_participants(
pool: Iterable[str],
*,
host: str,
include: Iterable[str] = (),
exclude: Iterable[str] = (),
only: Optional[str] = None,
probe: Probe,
require_all: bool = False,
) -> Participants:
"""母集合の既定・足す者・外す者・1 者指定から使える者を決める(設計の決定 2〜4)。

順序:

1. `include` / `exclude` の各名前が `ALL_RUNTIMES` にあり、重ならないことを確かめる。
`exclude` の名前が「`pool` ∪ `include`」に無ければ弾く(cross-review でホストを
外す指定はここに当たる)
2. 参加者 = `pool` ∪ `include` − `exclude`(`ALL_RUNTIMES` の順)
3. `only` があれば、参加者に含まれ `exclude` に無いことを確かめ、参加者をその 1 者にする
4. `probe(参加者)` で確かめる。飛ばされたら全員を通ったものとし `probe_skipped` を真にする
5. `require_all` が真で通らない者がいれば `AssignmentError`(欠けた者と理由を並べる)
6. 通った者を `available`、通らなかった者と理由を `unavailable` として返す

名前の綴りの検査(argparse の型)はこの前段で済んでいる前提だが、ここでも
`ALL_RUNTIMES` に無い名前は弾く。
"""
pool = list(pool)
include = list(include)
exclude = list(exclude)

for name in (*include, *exclude):
if name not in ALL_RUNTIMES:
raise AssignmentError(
f"参加できないランタイムです: {name}({'/'.join(ALL_RUNTIMES)} のいずれか)"
)
overlap = set(include) & set(exclude)
if overlap:
raise AssignmentError(
f"足す者と外す者に同じ名前があります: {', '.join(_in_fixed_order(overlap))}"
)
base = set(pool) | set(include)
outside = [n for n in exclude if n not in base]
if outside:
raise AssignmentError(
f"母集合に無い者は外せません: {', '.join(_in_fixed_order(outside))}"
f"(母集合: {', '.join(_in_fixed_order(base))})"
)

participants = _in_fixed_order(base - set(exclude))

if only is not None:
if only in exclude:
raise AssignmentError(f"--only と --exclude が矛盾しています: {only}")
if only not in participants:
raise AssignmentError(
f"--only は参加者のいずれかを指定してください: {only}"
f"(参加者: {', '.join(participants)})"
)
participants = [only]

results, skipped = probe(list(participants))
if skipped:
available, unavailable = list(participants), {}
else:
unavailable = {
n: str(results.get(n, {}).get("detail", ""))
for n in participants
if not results.get(n, {}).get("ok", False)
}
available = [n for n in participants if n not in unavailable]

if require_all and unavailable:
failed = " / ".join(f"{n}({d})" for n, d in unavailable.items())
raise AssignmentError(
"認証されていない CLI があります: " + failed + "。"
"参加者が欠けたまま進むと、その者のレビューが無いまま収束します。"
"各 CLI でログインしてから再実行してください"
)

return Participants(
pool=pool,
included=_in_fixed_order(include),
excluded=_in_fixed_order(exclude),
available=available,
unavailable=unavailable,
probe_skipped=skipped,
require_all=require_all,
)


def seat_runtime(seat: str) -> str:
"""席の名前からランタイム名を引く。形は `SEAT_PATTERN`(`kiro` / `kiro-2`)。

形に合わなければ `AssignmentError`。結果の受け口・起動スクリプト・監視が、担当の
引数の検査にこの関数を使う。
"""
m = SEAT_PATTERN.match(seat)
if m is None:
raise AssignmentError(
f"席の名前の形が違います: {seat}"
f"({'/'.join(ALL_RUNTIMES)} か、その名前に -2〜-9 を付けた形)"
)
return m.group(1)


def review_seats(round_no: int, available: list[str], fallback: list[str]) -> list[str]:
"""cross-review のラウンドの 2 席を決める(設計の決定 9・20。規則の正本はこの表)。

| 使える者の数 n | 席 |
| ---: | --- |
| 3 以上 | `available[round_no % n]` と `available[(round_no + 1) % n]` を `available` の順に並べた 2 席 |
| 2 | その 2 者 |
| 1 | その 1 者と、`fallback` のうち `available` に含まれない先頭の者。無ければ `<その 1 者>-2` |
| 0 | `fallback[0]` と `<fallback[0]>-2`。`fallback` が空なら `AssignmentError` |

`available` の並びは `ALL_RUNTIMES` の順(`resolve_participants` が保つ)。n = 3 の値は
変更前の `review_assign` と一致する。埋め合わせの候補は使える者に含まれない者だけを
使い、含まれる者は飛ばす(同じ席の名前を 2 つ返さないため)。`only` の処理は呼び出し側が
先に行う(1 者指定は埋め合わせをしない)。
"""
if round_no < 1:
raise AssignmentError(f"ラウンド番号は 1 以上です: {round_no}")
n = len(available)
if n >= 3:
picked = {available[round_no % n], available[(round_no + 1) % n]}
return [r for r in available if r in picked]
if n == 2:
return list(available)
if n == 1:
first = available[0]
extra = next((f for f in fallback if f not in available), None)
return [first, extra if extra is not None else f"{first}-2"]
if not fallback:
raise AssignmentError("使える者も席の埋め合わせに使える者もいません")
return [fallback[0], f"{fallback[0]}-2"]


def impl_assign(round_no: int, participants: list[str]) -> str:
"""cross-refactoring の適用担当 1 者を決める: `participants[round_no % len]`。

式は変更前の `assign()` と同じで、除数だけを参加者の数にする(設計の決定 7)。
ラウンド 1 が `participants[1]` から始まるため、ホスト claude の既定
(claude / codex / kiro)でもホストが最初に適用する形にならない。
"""
if round_no < 1:
raise AssignmentError(f"ラウンド番号は 1 以上です: {round_no}")
if not participants:
raise AssignmentError("適用担当を選べる参加者がいません")
return participants[round_no % len(participants)]
Loading
Loading