diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 2f61703bf..797d090b2 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.27.14): 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.27.15): 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/.github/workflows/pr-base-guard.yml b/.github/workflows/pr-base-guard.yml index 71c0cda66..554f6c557 100644 --- a/.github/workflows/pr-base-guard.yml +++ b/.github/workflows/pr-base-guard.yml @@ -11,6 +11,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 + with: + # release/v* の head が起点ブランチの祖先かを git merge-base で判定するため、履歴をすべて取る + fetch-depth: 0 - name: Check the branch this pull request came from # 宛先が既定ブランチでなければ、チェックは行わずに成功で終わる。既定ブランチの名前は # ワークフローへ書かず、リポジトリの設定から受け取る @@ -18,4 +21,5 @@ jobs: # ブランチ名をそのままシェルの本文へ埋めない。名前に置換の記法を含められる。 env: HEAD_REF: ${{ github.head_ref }} - run: bash scripts/check-pr-base.sh "$HEAD_REF" + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: bash scripts/check-pr-base.sh "$HEAD_REF" "$HEAD_SHA" diff --git a/.serena/project.yml b/.serena/project.yml index 84d4ee6f9..4fb3db77a 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -180,7 +180,6 @@ activation_command_timeout: 180.0 language_servers: - python - typescript -- bash # list of APIs (facades or facade methods, e.g. "lsp" or "lsp.get_diagnostics_for_symbol") to include in the REPL # that would otherwise be disabled (particularly optional methods, which are disabled by default). diff --git a/AGENTS.md b/AGENTS.md index e4517f7c0..0bae76b67 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,8 +20,9 @@ 宛先のチェックがこの設定を読む(`follow_branch: true` のときはメインディレクトリの追従先にも なる)。設定が無いリポジトリは既定ブランチのまま動く - **`--base develop` の付け忘れは継続的統合が塞ぐ。** `main` 宛の Pull Request は - `develop` から出たものだけを通す(`scripts/check-pr-base.sh`)。判定は設定に起点が - 書かれていて、そのブランチが origin にあるときだけ働く + `develop` に含まれるコミットだけを通す(head は `develop` か、先頭が `origin/develop` の祖先の + `release/v<版>`。`scripts/check-pr-base.sh`)。判定は設定に起点が書かれていて、そのブランチが + origin にあるときだけ働く - **開発の変更は `.worktrees/<ブランチ名>` の worktree の中で行う**(`/ndf:worktree`)。clone したディレクトリ(メインディレクトリ)は編集対象から外す - `issues/` `docs/` と各ランタイムの設定はメインディレクトリで編集してよい - メインディレクトリの編集は拒否されない。案内が出ても操作は成立する diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a9197e27..9e82861d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,27 @@ **開発版(接尾辞の付いた版)は載せない。** `9.8.0` は `9.8.0-dev.1` までしか出ておらず、 その内容は `10.0.0` で届いている。 +## [ndf 10.27.15] - 2026-11-03 + +- Docs: 発表資料「AI が回すソフトウェア工場」を 2026-11-03 時点で更新する(#2450) +- ndf: 並列の計画を検査できます。計画は状態ディレクトリの決まった名前のファイルへ書き出されます。ベースブランチと配るプラグインは、計画の宣言から読み取ります(#2451)(#2452) +- ndf: 並列のレーンごとに配布の依頼のプランを組み立てて提示します。提示は承認ゲート 2 の手順の中に並びます(#2451)(#2452) +- ndf: 配布の依頼のメッセージの形が契約で決まり、書き手の検査でその形かを確かめます(#2451)(#2452) +- ndf: 多重起動のとき、統括役の役割を数えます。数えるための副命令 `role` を使えます(#2451)(#2452) +- ndf: 並列の計画の作り方(issue-plan-strategy)と、オーガナイザーの手順(development-workflow)を参照できます(#2451)(#2452) +- ndf プラグインの本番の配布で、本番の Pull Request は `release/v<版>` ブランチから `main` へ出します。承認したコミットと `release/v<版>` の先端が一致しない場合、配布は進みません。(#2454)(#2452) +- `main` 宛ての Pull Request は、`develop` の祖先にある `release/v*` ブランチからも出せます。(#2454)(#2452) +- `release/v<版>` を取り込むときに版数だけが衝突した場合、本番の版を採って自動で解消します。(#2454)(#2452) +- 本番の配布を再開するときは、承認したコミットを含むマージと同じコミットを指すタグだけを、済んだ配布として扱います。(#2454)(#2452) +- マージ後の後片付けと主ディレクトリへの取り込みは、主ディレクトリを排他して 1 本ずつ実行します。(#2454)(#2452) +- ndf の並列の計画で、各レーンの周回は配布待ちか質問待ちに達した時点で締まります。答えは周回の切り替わりで読み込まれ、残りの作業が続けて流れます(#2455)(#2452) +- レーンと一時レーン(`--parallel-helper`)の問いはオーガナイザーへ届き、答えは問いを出したレーンへ返ります(#2455)(#2452) +- 複数のレーンが同じ原因で問いを出した場合、人にしかできない操作で、かつ選択肢が一致するときだけ 1 つにまとめます。各レーンの元の文面は残ります(#2455)(#2452) +- 次の異常系は、中断しても再開できるワークフロー(耐久ワークフロー)で扱います: 動かないレーン、一時レーンの停止、起動の断り、`stop --force`、終わりの前に残った保留の周回(#2455)(#2452) +- 主ディレクトリを追従させる checkout と fetch は、排他ロック(`ndf-main.lock`)で 1 本ずつ実行されます(#2455)(#2452) +- Add: relay.py account use <名前> で次の区間から使うアカウントを指定する(#2463) +- Fix: relay.py に --help を足し、副命令の --help で副命令を実行しないようにする(#2465) + ## [ndf 10.27.14] - 2026-11-02 - Factory の Web 画面 8 つは、共通の部品と国際化の土台の上で表示されます。部品には見出し・ナビゲーション・検索欄・選択欄・状態の表示などがあります。(#2389)(#2397) diff --git a/README.md b/README.md index 85a776e88..8de4787b8 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Claude Code では `/ndf:` と打って呼びます。**Skill** は | プラグイン名 | バージョン | できること | 詳細 | | --- | --- | --- | --- | -| **ndf** | 10.27.14 | 開発の作業手順(Skill)と専門のサブエージェント。Claude Code と Codex へ同じ名前で入る | [README](./plugins/ndf/README.md) | +| **ndf** | 10.27.15 | 開発の作業手順(Skill)と専門のサブエージェント。Claude Code と Codex へ同じ名前で入る | [README](./plugins/ndf/README.md) | | **playwright-kit** | 4.0.0 | Playwright による E2E テストの計画・スクリプト作成・証跡のレポート | [README](./plugins/playwright-kit/README.md) | 外部サービスへつなぐ MCP サーバーの設定のプラグインが 10 個あります(Claude Code / Codex 向け)。 @@ -69,7 +69,7 @@ Claude Code では `/ndf:` と打って呼びます。**Skill** は ### NDF プラグインの Skill の一覧 -**NDFプラグイン v10.27.14** に入る Skill の数は、AI ツールごとに違います。 +**NDFプラグイン v10.27.15** に入る Skill の数は、AI ツールごとに違います。 Claude Code向け core 49個、Codex向け core 45個です。 元Skills(49個)の内訳は次のとおりです。 - Pull Request とレビュー (7): pr, pr-tests, fix, pr-review, cherry-pick-pr, deploy, merged diff --git a/docs/glossary.md b/docs/glossary.md index 80d6456a2..2f6071b3f 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -346,35 +346,43 @@ NDF が提供する工程・承認ゲート・モード・ステップの語。d | Skill の渡し方 | — | Skill を worker へ届ける形。プランの worker には本文を system プロンプトへ入れ(本文)、ndf:worker には SKILL.md の絶対パスを起動指示の「読む Skill」に書く(パス。#2376) | — | — | `docs/specifications/ndf-worker-stage-skills.md` | | 読む Skill | — | supervisor が ndf:worker の起動指示に書く項目。その作業で読む規則の Skill の名前と SKILL.md の絶対パスを並べ、worker はここに挙がった SKILL.md だけを読む(#2376) | — | — | `docs/specifications/ndf-worker-stage-skills.md` | | 規則の Skill | — | 手順を持たず、書き方・記録の仕方のような規則だけを持つ Skill(document-style・logging-guidelines など)。Skill の対応表に置けるのはこれだけで、工程 Skill は置かない(#2376) | — | — | `docs/specifications/ndf-worker-stage-skills.md` | -| 差分の棚卸し | — | 毎スプリントのまとめで行う棚卸しの深さ。そのスプリントの変更で食い違った本文の訂正・直ったのに開いたままの課題を閉じる・閉じた親の子の見直し・そのスプリントで起票した課題への見積の段階の付与だけを行う(#2315) | — | — | — | -| 見直し | — | 数スプリントに 1 回行う棚卸しの深さ。後回しを除く open の課題の枝刈り・順位付け・マイルストーンの再編と、ニーズ・シーズの調査からの起票を行う(#2315) | — | — | — | -| 大整理 | — | 十数スプリントに 1 回行う棚卸しの深さ。未完了の課題すべてを原因の型で分け、Issue 単位では直らない根本原因を探し、見つかれば大方針を立てて課題とマイルストーンを組み直す。後回しの再評価を含む(#2315) | — | — | — | -| 後回しの再評価 | — | 後回しのマイルストーンの課題を、前倒し・やらない・据え置きに分けること。大整理の一部(#2315) | — | — | — | -| ニーズの調査 | — | 宣言した利用の記録と市場・競合から、利用者が求めている変更を探すこと(#2315) | — | — | — | -| シーズの調査 | — | 宣言した技術の構成・ライブラリの競合・周辺環境の変化から、取り入れられる変更を探すこと(#2315) | — | — | — | -| 調査の記録 | — | 調査の項目ごとに出典と調べた日時を持つ結果。見直しのフェーズだけが読んで起票に使う(#2315) | — | — | — | -| 材料の宣言 | — | プロジェクトがニーズとシーズの調査で読む先(プロダクトの種類と利用者・利用の記録と読む範囲・市場と競合・技術の構成・ライブラリの競合・周辺環境・後回しのマイルストーン)を書いたもの(#2315) | — | — | — | -| 見直しの持ち主 | — | 見直しのフェーズ(待ち時間の調査を含む)を受け持つ 1 つのセッション。宣言をロックの中で取り、終わったら解放する(#2315) | — | — | — | -| 大方針 | — | 大整理で見つけた根本原因を直す親の Issue。柱と受け入れ条件を持つ(例 #2335)(#2315) | — | — | — | -| 統括役 | `relay_hub` | 同じコンテナで最初に起動したラッパーが兼ねる役。ほかのラッパーの登録・起動・同時に動かす数の判定・一覧だけを持ち、各ラッパーの状態機械には触れない | — | — | — | -| 名前つきのラッパー | `named_relay` | 統括役に名前で登録され、tmux の 1 つの window で動く relay.py run の 1 本。カットポイントで区間を切り替えても同じ名前で続く | — | — | — | -| 端末の結び付け | `attach` | 利用者の端末を、名前つきのラッパーの tmux の window へ attach すること。端末を閉じても window の中のラッパーは動き続ける | — | — | — | -| 登録簿 | `relay_registry` | 統括役だけが書く、名前つきのラッパーの名前と cwd(起動元のリポジトリ)・最初の引数・tmux のセッションの表(registry.json。作業ディレクトリのパスは持たない) | — | — | — | -| 起動の依頼 | `spawn_request` | 頼む側が統括役の受付箱へ置く 1 件のファイル。起動・やり直しの種類と名前・作業ディレクトリ・引数を持つ | — | — | — | -| 起動のブロック | `spawn_block` | conductor の応答に置く、情報文字列が ndf-spawn <名前> の囲み。中身が最初の入力になる | — | — | — | -| 再起動の方針 | — | conductor が工程境界で会話を切るかを決める規則の組。現行方式と試行方式の 2 つ | — | — | — | -| 現行方式 | — | 運用上限 200K を超えたら次の工程を止め、4 つのカットポイントではコンテキスト量に関わらず ndf-next を出す再起動の方針。既定 | — | — | — | -| 試行方式 | — | カットポイントを再起動を判断できる境界として扱い、判断開始の値・運用上限・容量で継続か再起動かを決める再起動の方針。明示的に選んだときだけ働く | — | — | — | -| 判断開始の値 | — | 試行方式で、これを超えたら工程境界で継続と再起動を比べ始めるコンテキスト量(初期値 200K) | — | — | — | -| 運用上限 | — | これを超えたら次の工程への進入を止めるコンテキスト量(現行方式 200K・試行方式の初期値 400K)。ツール呼び出しの途中では止めない | — | — | — | -| 容量の余白 | — | 実行環境の窓の上限(または自動圧縮が始まる量)から、次の作業・ツール結果・出力と進行の保存のために空けておく量 | — | — | — | -| 再起動の理由 | — | 工程境界で会話を切った・止めたときの理由。容量・運用上限・費用・独立性・手動と、現行方式のカットポイントの 6 つ | — | — | — | -| 復元の区間 | — | 再起動した新しいセッションが、引継ぎ文書を読み始めてから最初の工程を起動するまで | — | — | — | -| 境界の判定 | — | 工程の起動 1 回ごとに出す継続 / 再起動 / 拒否と、その帯・理由・使った値の組。1 件ずつ記録に残る | — | — | — | -| 適用値 | — | 優先順位を当てて解決した、そのセッションで効く再起動の方針・判断開始の値・運用上限・容量の余白と、それぞれの出所 | — | — | — | -| 作業の識別子 | — | 複数のセッションを 1 つの作業として結ぶ鍵。起動の引数かスプリントに含まれる課題番号の組(#829 #830) | — | — | — | -| 費用の見込み | — | 判断の帯で、続けた場合と再起動した場合の次の工程までの費用を、記録から較正した中央値で比べた値 | — | — | — | -| 引継ぎの区間 | — | conductor が引継ぎ文書を書き始める前に記録した「引継ぎ開始」の時点から、そのセッションの最後の行まで | — | — | — | +| 差分の棚卸し | — | 毎スプリントのまとめで行う棚卸しの深さ。そのスプリントの変更で食い違った本文の訂正・直ったのに開いたままの課題を閉じる・閉じた親の子の見直し・そのスプリントで起票した課題への見積の段階の付与だけを行う(#2315) | — | — | `docs/specifications/ndf-backlog-refinement-depths.md` | +| 見直し | — | 数スプリントに 1 回行う棚卸しの深さ。後回しを除く open の課題の枝刈り・順位付け・マイルストーンの再編と、ニーズ・シーズの調査からの起票を行う(#2315) | — | — | `docs/specifications/ndf-backlog-refinement-depths.md` | +| 大整理 | — | 十数スプリントに 1 回行う棚卸しの深さ。未完了の課題すべてを原因の型で分け、Issue 単位では直らない根本原因を探し、見つかれば大方針を立てて課題とマイルストーンを組み直す。後回しの再評価を含む(#2315) | — | — | `docs/specifications/ndf-backlog-refinement-depths.md` | +| 後回しの再評価 | — | 後回しのマイルストーンの課題を、前倒し・やらない・据え置きに分けること。大整理の一部(#2315) | — | — | `docs/specifications/ndf-backlog-refinement-depths.md` | +| ニーズの調査 | — | 宣言した利用の記録と市場・競合から、利用者が求めている変更を探すこと(#2315) | — | — | `docs/specifications/ndf-backlog-refinement-depths.md` | +| シーズの調査 | — | 宣言した技術の構成・ライブラリの競合・周辺環境の変化から、取り入れられる変更を探すこと(#2315) | — | — | `docs/specifications/ndf-backlog-refinement-depths.md` | +| 調査の記録 | — | 調査の項目ごとに出典と調べた日時を持つ結果。見直しのフェーズだけが読んで起票に使う(#2315) | — | — | `docs/specifications/ndf-backlog-refinement-depths.md` | +| 材料の宣言 | — | プロジェクトがニーズとシーズの調査で読む先(プロダクトの種類と利用者・利用の記録と読む範囲・市場と競合・技術の構成・ライブラリの競合・周辺環境・後回しのマイルストーン)を書いたもの(#2315) | — | — | `docs/specifications/ndf-backlog-refinement-depths.md` | +| 見直しの持ち主 | — | 見直しのフェーズ(待ち時間の調査を含む)を受け持つ 1 つのセッション。宣言をロックの中で取り、終わったら解放する(#2315) | — | — | `docs/specifications/ndf-backlog-refinement-depths.md` | +| 大方針 | — | 大整理で見つけた根本原因を直す親の Issue。柱と受け入れ条件を持つ(例 #2335)(#2315) | — | — | `docs/specifications/ndf-backlog-refinement-depths.md` | +| 統括役 | `relay_hub` | 同じコンテナで最初に起動したラッパーが兼ねる役。ほかのラッパーの登録・起動・同時に動かす数の判定・一覧だけを持ち、各ラッパーの状態機械には触れない | — | — | `docs/specifications/ndf-relay-multi-session-supervisor.md` | +| 名前つきのラッパー | `named_relay` | 統括役に名前で登録され、tmux の 1 つの window で動く relay.py run の 1 本。カットポイントで区間を切り替えても同じ名前で続く | — | — | `docs/specifications/ndf-relay-multi-session-supervisor.md` | +| 端末の結び付け | `attach` | 利用者の端末を、名前つきのラッパーの tmux の window へ attach すること。端末を閉じても window の中のラッパーは動き続ける | — | — | `docs/specifications/ndf-relay-multi-session-supervisor.md` | +| 登録簿 | `relay_registry` | 統括役だけが書く、名前つきのラッパーの名前と cwd(起動元のリポジトリ)・最初の引数・tmux のセッションの表(registry.json。作業ディレクトリのパスは持たない) | — | — | `docs/specifications/ndf-relay-multi-session-supervisor.md` | +| 起動の依頼 | `spawn_request` | 頼む側が統括役の受付箱へ置く 1 件のファイル。起動・やり直しの種類と名前・作業ディレクトリ・引数を持つ | — | — | `docs/specifications/ndf-relay-multi-session-supervisor.md` | +| 起動のブロック | `spawn_block` | conductor の応答に置く、情報文字列が ndf-spawn <名前> の囲み。中身が最初の入力になる | — | — | `docs/specifications/ndf-relay-multi-session-supervisor.md` | +| 並列の計画 | — | 1 つのリポジトリで同時に進めるレーンの一覧と、レーンに共通の決まりを書いた 1 つの文書。オーガナイザーが読む。名前(計画名)を持つ | — | — | `plugins/ndf/skills/issue-plan-strategy/references/parallel-plan.md` | +| レーン | — | 並列の計画の下で、名前つきのラッパー 1 本が進める開発の流れ。最初の入力は開発ループかスプリントの 1 つ | — | — | `plugins/ndf/skills/issue-plan-strategy/references/parallel-plan.md` | +| オーガナイザー | — | --parallel <計画名> で起動した conductor の役割。レーンを起動・監視し、配布の依頼をまとめて本番の配布を 1 本にする。統括役とは別 | — | — | `plugins/ndf/skills/development-workflow/references/parallel.md` | +| 配布の依頼 | — | レーンが承認ゲート 2 の手前で、本番の配布をオーガナイザーへ頼むために送る 1 件のメッセージ(parallel.py request) | — | — | `plugins/ndf/skills/development-workflow/references/parallel.md` | +| 一時レーン | — | 並列の計画のオーガナイザーが、持ち主のいない共有の作業(まとめた配布のプランの実行・その失敗の対応・止まったレーンの保留の周回の残り)のためだけに起こすラッパー(<計画名>-rel<連番>、役割 helper)。同時に 1 本までで、作業を終えると自分で止まる | — | — | `plugins/ndf/skills/development-workflow/references/parallel.md` | +| 保留の周回 | — | 並列の計画のループのレーンで、答えを待たずに締めた周回(配布待ち・質問待ち)。課題は締め直すまで選定から外し、答えの後に残りの工程を流して 1 回だけ締め直す | — | — | `plugins/ndf/skills/development-workflow/references/loop.md` | +| 配布待ち | — | 配布の依頼を送り、答えを待たずに締めた周回の結果。まとめはまだ流していない | — | — | `plugins/ndf/skills/development-workflow/references/loop.md` | +| 質問待ち | — | 作業の途中の問いを問いの hook がオーガナイザーへ送り、止まったプランとステップを残して締めた周回の結果 | — | — | `plugins/ndf/skills/development-workflow/references/loop.md` | +| 再起動の方針 | — | conductor が工程境界で会話を切るかを決める規則の組。現行方式と試行方式の 2 つ | — | — | `docs/specifications/ndf-context-restart-policy.md` | +| 現行方式 | — | 運用上限 200K を超えたら次の工程を止め、4 つのカットポイントではコンテキスト量に関わらず ndf-next を出す再起動の方針。既定 | — | — | `docs/specifications/ndf-context-restart-policy.md` | +| 試行方式 | — | カットポイントを再起動を判断できる境界として扱い、判断開始の値・運用上限・容量で継続か再起動かを決める再起動の方針。明示的に選んだときだけ働く | — | — | `docs/specifications/ndf-context-restart-policy.md` | +| 判断開始の値 | — | 試行方式で、これを超えたら工程境界で継続と再起動を比べ始めるコンテキスト量(初期値 200K) | — | — | `docs/specifications/ndf-context-restart-policy.md` | +| 運用上限 | — | これを超えたら次の工程への進入を止めるコンテキスト量(現行方式 200K・試行方式の初期値 400K)。ツール呼び出しの途中では止めない | — | — | `docs/specifications/ndf-context-restart-policy.md` | +| 容量の余白 | — | 実行環境の窓の上限(または自動圧縮が始まる量)から、次の作業・ツール結果・出力と進行の保存のために空けておく量 | — | — | `docs/specifications/ndf-context-restart-policy.md` | +| 再起動の理由 | — | 工程境界で会話を切った・止めたときの理由。容量・運用上限・費用・独立性・手動と、現行方式のカットポイントの 6 つ | — | — | `docs/specifications/ndf-context-restart-policy.md` | +| 復元の区間 | — | 再起動した新しいセッションが、引継ぎ文書を読み始めてから最初の工程を起動するまで | — | — | `docs/specifications/ndf-context-restart-policy.md` | +| 境界の判定 | — | 工程の起動 1 回ごとに出す継続 / 再起動 / 拒否と、その帯・理由・使った値の組。1 件ずつ記録に残る | — | — | `docs/specifications/ndf-context-restart-policy.md` | +| 適用値 | — | 優先順位を当てて解決した、そのセッションで効く再起動の方針・判断開始の値・運用上限・容量の余白と、それぞれの出所 | — | — | `docs/specifications/ndf-context-restart-policy.md` | +| 作業の識別子 | — | 複数のセッションを 1 つの作業として結ぶ鍵。起動の引数かスプリントに含まれる課題番号の組(#829 #830) | — | — | `docs/specifications/ndf-context-restart-policy.md` | +| 費用の見込み | — | 判断の帯で、続けた場合と再起動した場合の次の工程までの費用を、記録から較正した中央値で比べた値 | — | — | `docs/specifications/ndf-context-restart-policy.md` | +| 引継ぎの区間 | — | conductor が引継ぎ文書を書き始める前に記録した「引継ぎ開始」の時点から、そのセッションの最後の行まで | — | — | `docs/specifications/ndf-context-restart-policy.md` | ## NDF の Slack 通知(`ndf-notification`) @@ -619,7 +627,7 @@ release が走らせるリリースの種別・リリースコマンド・公開 | 移行の手順 | — | 利用者が新しい版へ移るときに自分で行う操作(引数の置き換え・認証のし直し・導入し直しなど)。PR 本文の「移行の手順」の節が正 | — | — | `docs/specifications/ndf-release-other-plugin-levels-and-migration.md` | | 互換なしの印 | — | 要求の「影響」の表の「公開インタフェース」の行に書く、互換の経路を持たないことを示す語(互換なし / 互換の経路は持たない)。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(本番の配布では `release/v<版>`、昇格の経路ではベースブランチ)。本番チャネルへマージしてよいのはこのコミットだけである | — | — | `docs/specifications/ndf-release-approved-commit.md` | | 確かめたコミット | — | 導入の確認を走らせた、ベースブランチの先頭のコミットの SHA(40 桁)。導入の確認の承認資料に書かれ、pace: fast / auto の昇格でマージしてよい先端はこのコミットに限る | — | — | `docs/specifications/ndf-release-approved-commit.md` | | 承認の外のコミット | — | 承認したコミットから比べた先端までに入ったコミットのうち、この版の配布の PR のものでないもの。配布物(`plugins/`・`.claude-plugin/`)に触れるものがあれば本番の配布は承認ゲートで止まる | — | — | `docs/specifications/ndf-release-approved-commit.md` | | 承認の記録 | — | 承認資料の欄の 1 つ。ゲート 2 を承認したときの承認したコミットの SHA と、承認した者・時刻を持つ。承認資料を書き直すと消え、承認し直すまで本番の配布は通らない | — | — | `docs/specifications/ndf-release-approved-commit.md` | @@ -698,9 +706,9 @@ backlog-refinement と out-of-scope が課題を分類し、起票するとき | 人の判断待ち | — | 着手の前に人の判断(needs-decision・承認ゲート)を要する状態。費用に数えず、順位の値を変えずに印として出す | — | — | — | | 候補の上限 | — | 1 回の棚卸しで区分を決める候補の数の上限。candidates の --limit、.ndf/backlog.json の candidate_limit、既定 10 の順に決まる | — | — | `plugins/ndf/skills/backlog-refinement/SKILL.md` | | 持ち越し | — | 候補の上限を超えたため、その回で区分を決めずに残した候補。candidates の metrics.deferred に番号だけが載る | — | — | `plugins/ndf/skills/backlog-refinement/SKILL.md` | -| 棚卸しの深さ | — | 1 回の棚卸しが扱う範囲。差分の棚卸し・見直し・大整理の 3 つ。`upkeep.py` の `--depth diff\|review\|overhaul`(#2315) | — | — | — | -| 調査の項目 | — | 調査を小さく分けた単位。側面と対象の組(例: `seeds.library:pytest`)。1 項目が 1 つの調査の記録になる(#2315) | — | — | — | -| 付け直しの記録 | — | 大整理が課題の親とマイルストーンを変える前に残す、課題ごとの前の状態。`upkeep.py revert` が読んで戻す(#2315) | — | — | — | +| 棚卸しの深さ | — | 1 回の棚卸しが扱う範囲。差分の棚卸し・見直し・大整理の 3 つ。`upkeep.py` の `--depth diff\|review\|overhaul`(#2315) | — | — | `docs/specifications/ndf-backlog-refinement-depths.md` | +| 調査の項目 | — | 調査を小さく分けた単位。側面と対象の組(例: `seeds.library:pytest`)。1 項目が 1 つの調査の記録になる(#2315) | — | — | `docs/specifications/ndf-backlog-refinement-depths.md` | +| 付け直しの記録 | — | 大整理が課題の親とマイルストーンを変える前に残す、課題ごとの前の状態。`upkeep.py revert` が読んで戻す(#2315) | — | — | `docs/specifications/ndf-backlog-refinement-depths.md` | ## NDF の worktree(`ndf-worktree`) @@ -856,15 +864,15 @@ NDF を Web UI から操作できるサービスとして提供するときの | 進捗の受け口 | — | ランナー・GitHub の Webhook・承認の API から進捗の事象を受け、出所ごとに認証して冪等に追記する、factory/api/ の HTTP の経路 | — | — | `docs/specifications/ndf-factory-contracts-and-progress.md` | | 実行ジョブの token | — | 実行の制御が実行ジョブ 1 つごとに発行する、期限つきの token。そのジョブのスプリントの実行にだけ進捗の事象を書け、Pull Request の参照はそのジョブの取り込むリポジトリのものだけを残せる。承認の依頼の答えを読める | — | — | `docs/specifications/ndf-factory-contracts-and-progress.md` | | 冪等の鍵 | — | 進捗の事象の重複を判定する鍵(idempotency_key)。形は出所ごとに決まり、同じ鍵の事象は 1 つだけ残る | — | — | `docs/specifications/ndf-factory-contracts-and-progress.md` | -| アカウント | — | Neural Development Factory で、プランと使える量を持ち、リポジトリの登録とスプリントの実行を持つ単位。個人とチームの 2 種類。`account_id` で指す | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | +| アカウント | — | Neural Development Factory で、料金プランと使える量を持ち、リポジトリの登録とスプリントの実行を持つ単位。個人とチームの 2 種類。`account_id` で指す | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | | 席 | — | チームのアカウントの中で、1 人の利用者に割り当てる枠。種類は standard と premium | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | -| プラン | — | アカウントの契約の種類。Free・個人 Pro・個人 Max・Team の 4 つ。Team の使える量は席の種類で決まる | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | -| 使える量 | — | プランか席の種類ごとに決まる上限。同時に動かせるスプリントの実行の数と、月の token の上限の 2 つ | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | +| 料金プラン | — | アカウントの契約の種類。Free・個人 Pro・個人 Max・Team の 4 つ。Team の使える量は席の種類で決まる | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | +| 使える量 | — | 料金プランか席の種類ごとに決まる上限。同時に動かせるスプリントの実行の数と、月の token の上限の 2 つ | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | | 利用者 | — | Google か GitHub の 1 つのアカウントで Neural Development Factory にログインした人。Cognito の `sub` を利用者の ID として持つ。同じ人でも IdP が違えば別の利用者である | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | | チームの持ち主 | — | チームのアカウントを作った利用者。席を足す・種類を変える・外すことができる唯一の利用者 | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | | 使える量の判定 | — | 呼び出す側が渡した今の値(動いているスプリントの実行の数・その月の token の数)を使える量の上限と照らし、許すか断るかと理由を返すこと | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | | GitHub の仲立ち | — | GitHub の OAuth を Cognito が受け取れる OIDC の形へ変える、`factory/api/` のプログラム | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | -| 実行の形態 | — | アカウントのランナーが Claude をどう呼ぶか。`local`(手元のランナー)・`byok`(キーの持ち込み)・`managed`(サービスの Bedrock)の 3 つで、アカウントごとに 1 つ持つ。Free は `managed` を選べず、月の token の上限は `managed` だけに掛かる | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | +| 実行の形態 | — | アカウントのランナーが Claude をどう呼ぶか。`local`(手元のマシン)・`byok`(キーの持ち込み)・`managed`(サービスの Bedrock)の 3 つで、アカウントごとに 1 つ持つ。Free は `managed` を選べず、月の token の上限は `managed` だけに掛かる | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | | 起票の依頼 | — | 実行ジョブの中で NDF が別のリポジトリへの起票を決めたとき、ランナーが進捗の事象 issue.requested でサービスへ送る依頼。起票先・題・本文を持ち、ランナーは自分では起票しない | — | — | — | | 起票の依頼の結果 | — | 起票の依頼 1 件ごとに 1 件残る進捗の事象 issue.request_resolved。サービスが起票した Issue の参照か、下書きへ回した理由を持つ | — | — | — | | 起票の代行 | — | 記録された起票の依頼を読み、起票してよい先へはサービスが 1 リポジトリと Issues の書き込みだけに絞った導入の token で起票し、そうでない先は下書きを人へ回す、サービスの処理 | — | — | — | @@ -888,16 +896,19 @@ NDF を Web UI から操作できるサービスとして提供するときの | 工程の記録 | — | スプリントの詳細に並ぶ step.recorded の 1 件ずつの行。NDF のプランのステップ 1 つが 1 行になる(#2172) | — | — | — | | 工程の中身 | — | 1 件の工程の記録の、触ったファイル・コミットの範囲・決めたこと・出力(作業の報告かログ)。詳細パネルが出す(#2172) | — | — | — | | スプリント全体の欄 | — | スプリントの詳細で、課題の番号を持たない工程の記録を並べる欄(#2172) | — | — | — | -| 区間 | — | 1 つの実行ジョブの中でランナーが起動する claude -p の 1 回分。conductor の ndf-next で次の区間へ移る | — | — | — | -| 区間の上限 | — | 1 つの実行ジョブで起動してよい区間の数の上限(既定 20)。超えたジョブは section_limit で終わる | — | — | — | -| 同時のジョブの数 | `max_jobs` | 手元のランナーが同時に動かす実行ジョブの数の上限。ランナーが登録と心拍で申告し、サービスはそれを超えて指示を渡さない | — | — | — | -| マイルストーンの持ち分 | — | 終わりの状態でない開発ループが、リポジトリのマイルストーン 1 つを選定の対象として占めていることを表す実行の枠の行。同じリポジトリのループどうしで重ならない | — | — | — | -| 課題の持ち分 | — | 終わりの状態でない手動のスプリントの実行が、リポジトリの課題 1 つを持っていることを表す実行の枠の行。同じ課題を 2 つの実行に入れない | — | — | — | -| 手元のランナーの枠 | — | 1 つの手元のランナーが running で持つ local の実行ジョブの数を数えた実行の枠の行。同時のジョブの数を超えて受け取らせない | — | — | — | -| 画面の地図 | — | Factory の全画面の経路・役割ごとの主な動線・確定仕様の機能との対応を 1 つにまとめた表 | — | — | — | -| デザインのトークン | — | 色・文字・余白の値に名前を付け、画面の部品がその名前で参照する値の集合 | — | — | — | -| ダミーの API クライアント | — | 画面が呼ぶ API クライアントのインタフェースを満たし、決まったデータを返す実装。本物の API クライアントと差し替えられる | — | — | — | -| 翻訳のファイル | — | 言語ごとに、画面の文言のキーと文言を持つファイル | — | — | — | -| 見本の画面 | — | 土台の部品だけで組み、ダミーデータで動かす画面の 1 組(スプリントの一覧と詳細)。後続の課題が中身を置き換える出発点 | — | — | — | -| つながりの状態 | — | 画面が今サーバーへつながっているか・最後に受け取った時刻・つながってから確定する操作の数の組 | — | — | — | -| 言語と地域の設定 | — | 画面の文言の言語・日時と数の書式の地域・表示の時刻帯の組。今はブラウザの設定から決める | — | — | — | +| 区間 | — | 1 つの実行ジョブの中でランナーが起動する claude -p の 1 回分。conductor の ndf-next で次の区間へ移る | — | — | `docs/specifications/ndf-factory-dev-loops.md` | +| 区間の上限 | — | 1 つの実行ジョブで起動してよい区間の数の上限(既定 20)。超えたジョブは section_limit で終わる | — | — | `docs/specifications/ndf-factory-dev-loops.md` | +| 同時のジョブの数 | `max_jobs` | 手元のマシンが同時に動かす実行ジョブの数の上限。手元のマシンが登録と心拍で申告し、サービスはそれを超えて指示を渡さない | — | — | `docs/specifications/ndf-factory-dev-loops.md` | +| マイルストーンの持ち分 | — | 終わりの状態でない開発ループが、リポジトリのマイルストーン 1 つを選定の対象として占めていることを表す実行の枠の行。同じリポジトリのループどうしで重ならない | — | — | `docs/specifications/ndf-factory-dev-loops.md` | +| 課題の持ち分 | — | 終わりの状態でない手動のスプリントの実行が、リポジトリの課題 1 つを持っていることを表す実行の枠の行。同じ課題を 2 つの実行に入れない | — | — | `docs/specifications/ndf-factory-dev-loops.md` | +| 手元のマシンの枠 | — | 1 つの手元のマシンが running で持つ local の実行ジョブの数を数えた実行の枠の行。同時のジョブの数を超えて受け取らせない | 手元のランナーの枠 | — | `docs/specifications/ndf-factory-dev-loops.md` | +| 手元のマシン | — | 利用者の環境に登録し、同時のジョブの数まで実行ジョブを受け取るマシン。受け取った実行ジョブごとに別のランナーが動く | 手元のランナー | — | — | +| Factory のオーガナイザー | — | 同じリポジトリで並行する Factory の実行をまとめる役割。1 台の中のオーガナイザーの仕事すべて(配布を 1 本にまとめる・問いを集める・答えの後の作業を持ち主へ返す・共有の作業を一時のレーンへ渡す)に、Factory 固有の仕事(実行ジョブの起動・停止・答えでの再開、実行の枠と手元のマシンの枠、ジョブをまたぐ状態の置き場)を足したもの。API の handler と見回りの中で動き、常駐しない。1 台の中のオーガナイザーと同じ層の別の実装で、互いに排他 | — | — | — | +| 配布の実行 | — | Factory のオーガナイザーが、まとめた配布 1 つのために起こすスプリントの実行。範囲のどの実行でもなく、承認資料を作り、本番の配布を流し、結果を返して終わる。同じリポジトリで同時に 1 本まで | — | — | — | +| 画面の地図 | — | Factory の全画面の経路・役割ごとの主な動線・確定仕様の機能との対応を 1 つにまとめた表 | — | — | `docs/specifications/ndf-factory-ui-components.md` | +| デザインのトークン | — | 色・文字・余白の値に名前を付け、画面の部品がその名前で参照する値の集合 | — | — | `docs/specifications/ndf-factory-ui-components.md` | +| ダミーの API クライアント | — | 画面が呼ぶ API クライアントのインタフェースを満たし、決まったデータを返す実装。本物の API クライアントと差し替えられる | — | — | `docs/specifications/ndf-factory-ui-components.md` | +| 翻訳のファイル | — | 言語ごとに、画面の文言のキーと文言を持つファイル | — | — | `docs/specifications/ndf-factory-ui-components.md` | +| 見本の画面 | — | 土台の部品だけで組み、ダミーデータで動かす画面の 1 組(スプリントの一覧と詳細)。後続の課題が中身を置き換える出発点 | — | — | `docs/specifications/ndf-factory-ui-components.md` | +| つながりの状態 | — | 画面が今サーバーへつながっているか・最後に受け取った時刻・つながってから確定する操作の数の組 | — | — | `docs/specifications/ndf-factory-ui-components.md` | +| 言語と地域の設定 | — | 画面の文言の言語・日時と数の書式の地域・表示の時刻帯の組。今はブラウザの設定から決める | — | — | `docs/specifications/ndf-factory-ui-components.md` | diff --git a/docs/glossary/glossary.json b/docs/glossary/glossary.json index dc23eac77..f34e1760d 100644 --- a/docs/glossary/glossary.json +++ b/docs/glossary/glossary.json @@ -3533,7 +3533,7 @@ { "term": "比べた先端", "context": "ndf-release", - "meaning": "本番の配布が承認したコミットと比べた時点の、ベースブランチの先端のコミットの SHA。本番チャネルへマージしてよいのはこのコミットだけである", + "meaning": "本番の配布が承認したコミットと比べた時点の、本番チャネルへ出すブランチの先端のコミットの SHA(本番の配布では `release/v<版>`、昇格の経路ではベースブランチ)。本番チャネルへマージしてよいのはこのコミットだけである", "source": "docs/specifications/ndf-release-approved-commit.md" }, { @@ -4390,7 +4390,7 @@ { "term": "アカウント", "context": "ndf-factory", - "meaning": "Neural Development Factory で、プランと使える量を持ち、リポジトリの登録とスプリントの実行を持つ単位。個人とチームの 2 種類。`account_id` で指す", + "meaning": "Neural Development Factory で、料金プランと使える量を持ち、リポジトリの登録とスプリントの実行を持つ単位。個人とチームの 2 種類。`account_id` で指す", "source": "docs/specifications/ndf-factory-accounts-and-plans.md" }, { @@ -4400,7 +4400,7 @@ "source": "docs/specifications/ndf-factory-accounts-and-plans.md" }, { - "term": "プラン", + "term": "料金プラン", "context": "ndf-factory", "meaning": "アカウントの契約の種類。Free・個人 Pro・個人 Max・Team の 4 つ。Team の使える量は席の種類で決まる", "source": "docs/specifications/ndf-factory-accounts-and-plans.md" @@ -4408,7 +4408,7 @@ { "term": "使える量", "context": "ndf-factory", - "meaning": "プランか席の種類ごとに決まる上限。同時に動かせるスプリントの実行の数と、月の token の上限の 2 つ", + "meaning": "料金プランか席の種類ごとに決まる上限。同時に動かせるスプリントの実行の数と、月の token の上限の 2 つ", "source": "docs/specifications/ndf-factory-accounts-and-plans.md" }, { @@ -4438,7 +4438,7 @@ { "term": "実行の形態", "context": "ndf-factory", - "meaning": "アカウントのランナーが Claude をどう呼ぶか。`local`(手元のランナー)・`byok`(キーの持ち込み)・`managed`(サービスの Bedrock)の 3 つで、アカウントごとに 1 つ持つ。Free は `managed` を選べず、月の token の上限は `managed` だけに掛かる", + "meaning": "アカウントのランナーが Claude をどう呼ぶか。`local`(手元のマシン)・`byok`(キーの持ち込み)・`managed`(サービスの Bedrock)の 3 つで、アカウントごとに 1 つ持つ。Free は `managed` を選べず、月の token の上限は `managed` だけに掛かる", "source": "docs/specifications/ndf-factory-accounts-and-plans.md" }, { @@ -4956,291 +4956,353 @@ "term": "区間", "context": "ndf-factory", "meaning": "1 つの実行ジョブの中でランナーが起動する claude -p の 1 回分。conductor の ndf-next で次の区間へ移る", - "pending_source": "issues/issue-2347-requirements.md" + "source": "docs/specifications/ndf-factory-dev-loops.md" }, { "term": "区間の上限", "context": "ndf-factory", "meaning": "1 つの実行ジョブで起動してよい区間の数の上限(既定 20)。超えたジョブは section_limit で終わる", - "pending_source": "issues/issue-2347-requirements.md" + "source": "docs/specifications/ndf-factory-dev-loops.md" }, { "term": "同時のジョブの数", "context": "ndf-factory", "code": "max_jobs", - "meaning": "手元のランナーが同時に動かす実行ジョブの数の上限。ランナーが登録と心拍で申告し、サービスはそれを超えて指示を渡さない", - "pending_source": "issues/issue-2347-requirements.md" + "meaning": "手元のマシンが同時に動かす実行ジョブの数の上限。手元のマシンが登録と心拍で申告し、サービスはそれを超えて指示を渡さない", + "source": "docs/specifications/ndf-factory-dev-loops.md" }, { "term": "マイルストーンの持ち分", "context": "ndf-factory", "meaning": "終わりの状態でない開発ループが、リポジトリのマイルストーン 1 つを選定の対象として占めていることを表す実行の枠の行。同じリポジトリのループどうしで重ならない", - "pending_source": "issues/issue-2347-design.md" + "source": "docs/specifications/ndf-factory-dev-loops.md" }, { "term": "課題の持ち分", "context": "ndf-factory", "meaning": "終わりの状態でない手動のスプリントの実行が、リポジトリの課題 1 つを持っていることを表す実行の枠の行。同じ課題を 2 つの実行に入れない", - "pending_source": "issues/issue-2347-design.md" + "source": "docs/specifications/ndf-factory-dev-loops.md" + }, + { + "term": "手元のマシンの枠", + "context": "ndf-factory", + "meaning": "1 つの手元のマシンが running で持つ local の実行ジョブの数を数えた実行の枠の行。同時のジョブの数を超えて受け取らせない", + "source": "docs/specifications/ndf-factory-dev-loops.md", + "deprecated": [ + "手元のランナーの枠" + ] + }, + { + "term": "手元のマシン", + "context": "ndf-factory", + "meaning": "利用者の環境に登録し、同時のジョブの数まで実行ジョブを受け取るマシン。受け取った実行ジョブごとに別のランナーが動く", + "deprecated": [ + "手元のランナー" + ], + "source": "", + "pending_source": "issues/issue-2439-design.md" }, { - "term": "手元のランナーの枠", + "term": "Factory のオーガナイザー", "context": "ndf-factory", - "meaning": "1 つの手元のランナーが running で持つ local の実行ジョブの数を数えた実行の枠の行。同時のジョブの数を超えて受け取らせない", - "pending_source": "issues/issue-2347-design.md" + "meaning": "同じリポジトリで並行する Factory の実行をまとめる役割。1 台の中のオーガナイザーの仕事すべて(配布を 1 本にまとめる・問いを集める・答えの後の作業を持ち主へ返す・共有の作業を一時のレーンへ渡す)に、Factory 固有の仕事(実行ジョブの起動・停止・答えでの再開、実行の枠と手元のマシンの枠、ジョブをまたぐ状態の置き場)を足したもの。API の handler と見回りの中で動き、常駐しない。1 台の中のオーガナイザーと同じ層の別の実装で、互いに排他", + "source": "", + "pending_source": "issues/issue-2439-design.md" + }, + { + "term": "配布の実行", + "context": "ndf-factory", + "meaning": "Factory のオーガナイザーが、まとめた配布 1 つのために起こすスプリントの実行。範囲のどの実行でもなく、承認資料を作り、本番の配布を流し、結果を返して終わる。同じリポジトリで同時に 1 本まで", + "source": "", + "pending_source": "issues/issue-2439-design.md" }, { "term": "画面の地図", "context": "ndf-factory", "meaning": "Factory の全画面の経路・役割ごとの主な動線・確定仕様の機能との対応を 1 つにまとめた表", - "pending_source": "issues/issue-2382-requirements.md" + "source": "docs/specifications/ndf-factory-ui-components.md" }, { "term": "デザインのトークン", "context": "ndf-factory", "meaning": "色・文字・余白の値に名前を付け、画面の部品がその名前で参照する値の集合", - "pending_source": "issues/issue-2382-requirements.md" + "source": "docs/specifications/ndf-factory-ui-components.md" }, { "term": "ダミーの API クライアント", "context": "ndf-factory", "meaning": "画面が呼ぶ API クライアントのインタフェースを満たし、決まったデータを返す実装。本物の API クライアントと差し替えられる", - "pending_source": "issues/issue-2382-requirements.md" + "source": "docs/specifications/ndf-factory-ui-components.md" }, { "term": "翻訳のファイル", "context": "ndf-factory", "meaning": "言語ごとに、画面の文言のキーと文言を持つファイル", - "pending_source": "issues/issue-2382-requirements.md" + "source": "docs/specifications/ndf-factory-ui-components.md" }, { "term": "見本の画面", "context": "ndf-factory", "meaning": "土台の部品だけで組み、ダミーデータで動かす画面の 1 組(スプリントの一覧と詳細)。後続の課題が中身を置き換える出発点", - "pending_source": "issues/issue-2382-design.md" + "source": "docs/specifications/ndf-factory-ui-components.md" }, { "term": "つながりの状態", "context": "ndf-factory", "meaning": "画面が今サーバーへつながっているか・最後に受け取った時刻・つながってから確定する操作の数の組", - "pending_source": "issues/issue-2382-design.md" + "source": "docs/specifications/ndf-factory-ui-components.md" }, { "term": "言語と地域の設定", "context": "ndf-factory", "meaning": "画面の文言の言語・日時と数の書式の地域・表示の時刻帯の組。今はブラウザの設定から決める", - "pending_source": "issues/issue-2382-design.md" + "source": "docs/specifications/ndf-factory-ui-components.md" }, { "term": "差分の棚卸し", "context": "ndf-workflow", "meaning": "毎スプリントのまとめで行う棚卸しの深さ。そのスプリントの変更で食い違った本文の訂正・直ったのに開いたままの課題を閉じる・閉じた親の子の見直し・そのスプリントで起票した課題への見積の段階の付与だけを行う(#2315)", - "source": "", - "pending_source": "issues/issue-2315-requirements.md" + "source": "docs/specifications/ndf-backlog-refinement-depths.md" }, { "term": "見直し", "context": "ndf-workflow", "meaning": "数スプリントに 1 回行う棚卸しの深さ。後回しを除く open の課題の枝刈り・順位付け・マイルストーンの再編と、ニーズ・シーズの調査からの起票を行う(#2315)", - "source": "", - "pending_source": "issues/issue-2315-requirements.md" + "source": "docs/specifications/ndf-backlog-refinement-depths.md" }, { "term": "大整理", "context": "ndf-workflow", "meaning": "十数スプリントに 1 回行う棚卸しの深さ。未完了の課題すべてを原因の型で分け、Issue 単位では直らない根本原因を探し、見つかれば大方針を立てて課題とマイルストーンを組み直す。後回しの再評価を含む(#2315)", - "source": "", - "pending_source": "issues/issue-2315-requirements.md" + "source": "docs/specifications/ndf-backlog-refinement-depths.md" }, { "term": "後回しの再評価", "context": "ndf-workflow", "meaning": "後回しのマイルストーンの課題を、前倒し・やらない・据え置きに分けること。大整理の一部(#2315)", - "source": "", - "pending_source": "issues/issue-2315-requirements.md" + "source": "docs/specifications/ndf-backlog-refinement-depths.md" }, { "term": "ニーズの調査", "context": "ndf-workflow", "meaning": "宣言した利用の記録と市場・競合から、利用者が求めている変更を探すこと(#2315)", - "source": "", - "pending_source": "issues/issue-2315-requirements.md" + "source": "docs/specifications/ndf-backlog-refinement-depths.md" }, { "term": "シーズの調査", "context": "ndf-workflow", "meaning": "宣言した技術の構成・ライブラリの競合・周辺環境の変化から、取り入れられる変更を探すこと(#2315)", - "source": "", - "pending_source": "issues/issue-2315-requirements.md" + "source": "docs/specifications/ndf-backlog-refinement-depths.md" }, { "term": "調査の記録", "context": "ndf-workflow", "meaning": "調査の項目ごとに出典と調べた日時を持つ結果。見直しのフェーズだけが読んで起票に使う(#2315)", - "source": "", - "pending_source": "issues/issue-2315-requirements.md" + "source": "docs/specifications/ndf-backlog-refinement-depths.md" }, { "term": "材料の宣言", "context": "ndf-workflow", "meaning": "プロジェクトがニーズとシーズの調査で読む先(プロダクトの種類と利用者・利用の記録と読む範囲・市場と競合・技術の構成・ライブラリの競合・周辺環境・後回しのマイルストーン)を書いたもの(#2315)", - "source": "", - "pending_source": "issues/issue-2315-requirements.md" + "source": "docs/specifications/ndf-backlog-refinement-depths.md" }, { "term": "見直しの持ち主", "context": "ndf-workflow", "meaning": "見直しのフェーズ(待ち時間の調査を含む)を受け持つ 1 つのセッション。宣言をロックの中で取り、終わったら解放する(#2315)", - "source": "", - "pending_source": "issues/issue-2315-requirements.md" + "source": "docs/specifications/ndf-backlog-refinement-depths.md" }, { "term": "大方針", "context": "ndf-workflow", "meaning": "大整理で見つけた根本原因を直す親の Issue。柱と受け入れ条件を持つ(例 #2335)(#2315)", - "source": "", - "pending_source": "issues/issue-2315-requirements.md" + "source": "docs/specifications/ndf-backlog-refinement-depths.md" }, { "term": "棚卸しの深さ", "context": "ndf-issue-upkeep", "meaning": "1 回の棚卸しが扱う範囲。差分の棚卸し・見直し・大整理の 3 つ。`upkeep.py` の `--depth diff|review|overhaul`(#2315)", - "source": "", - "pending_source": "issues/issue-2315-design.md" + "source": "docs/specifications/ndf-backlog-refinement-depths.md" }, { "term": "調査の項目", "context": "ndf-issue-upkeep", "meaning": "調査を小さく分けた単位。側面と対象の組(例: `seeds.library:pytest`)。1 項目が 1 つの調査の記録になる(#2315)", - "source": "", - "pending_source": "issues/issue-2315-design.md" + "source": "docs/specifications/ndf-backlog-refinement-depths.md" }, { "term": "付け直しの記録", "context": "ndf-issue-upkeep", "meaning": "大整理が課題の親とマイルストーンを変える前に残す、課題ごとの前の状態。`upkeep.py revert` が読んで戻す(#2315)", - "source": "", - "pending_source": "issues/issue-2315-design.md" + "source": "docs/specifications/ndf-backlog-refinement-depths.md" }, { "term": "統括役", "code": "relay_hub", "context": "ndf-workflow", "meaning": "同じコンテナで最初に起動したラッパーが兼ねる役。ほかのラッパーの登録・起動・同時に動かす数の判定・一覧だけを持ち、各ラッパーの状態機械には触れない", - "pending_source": "issues/issue-2346-requirements.md" + "source": "docs/specifications/ndf-relay-multi-session-supervisor.md" }, { "term": "名前つきのラッパー", "code": "named_relay", "context": "ndf-workflow", "meaning": "統括役に名前で登録され、tmux の 1 つの window で動く relay.py run の 1 本。カットポイントで区間を切り替えても同じ名前で続く", - "pending_source": "issues/issue-2346-requirements.md" + "source": "docs/specifications/ndf-relay-multi-session-supervisor.md" }, { "term": "端末の結び付け", "code": "attach", "context": "ndf-workflow", "meaning": "利用者の端末を、名前つきのラッパーの tmux の window へ attach すること。端末を閉じても window の中のラッパーは動き続ける", - "pending_source": "issues/issue-2346-requirements.md" + "source": "docs/specifications/ndf-relay-multi-session-supervisor.md" }, { "term": "登録簿", "code": "relay_registry", "context": "ndf-workflow", "meaning": "統括役だけが書く、名前つきのラッパーの名前と cwd(起動元のリポジトリ)・最初の引数・tmux のセッションの表(registry.json。作業ディレクトリのパスは持たない)", - "pending_source": "issues/issue-2346-design.md" + "source": "docs/specifications/ndf-relay-multi-session-supervisor.md" }, { "term": "起動の依頼", "code": "spawn_request", "context": "ndf-workflow", "meaning": "頼む側が統括役の受付箱へ置く 1 件のファイル。起動・やり直しの種類と名前・作業ディレクトリ・引数を持つ", - "pending_source": "issues/issue-2346-design.md" + "source": "docs/specifications/ndf-relay-multi-session-supervisor.md" }, { "term": "起動のブロック", "code": "spawn_block", "context": "ndf-workflow", "meaning": "conductor の応答に置く、情報文字列が ndf-spawn <名前> の囲み。中身が最初の入力になる", - "pending_source": "issues/issue-2346-design.md" + "source": "docs/specifications/ndf-relay-multi-session-supervisor.md" + }, + { + "term": "並列の計画", + "context": "ndf-workflow", + "meaning": "1 つのリポジトリで同時に進めるレーンの一覧と、レーンに共通の決まりを書いた 1 つの文書。オーガナイザーが読む。名前(計画名)を持つ", + "source": "plugins/ndf/skills/issue-plan-strategy/references/parallel-plan.md" + }, + { + "term": "レーン", + "context": "ndf-workflow", + "meaning": "並列の計画の下で、名前つきのラッパー 1 本が進める開発の流れ。最初の入力は開発ループかスプリントの 1 つ", + "source": "plugins/ndf/skills/issue-plan-strategy/references/parallel-plan.md" + }, + { + "term": "オーガナイザー", + "context": "ndf-workflow", + "meaning": "--parallel <計画名> で起動した conductor の役割。レーンを起動・監視し、配布の依頼をまとめて本番の配布を 1 本にする。統括役とは別", + "source": "plugins/ndf/skills/development-workflow/references/parallel.md" + }, + { + "term": "配布の依頼", + "context": "ndf-workflow", + "meaning": "レーンが承認ゲート 2 の手前で、本番の配布をオーガナイザーへ頼むために送る 1 件のメッセージ(parallel.py request)", + "source": "plugins/ndf/skills/development-workflow/references/parallel.md" + }, + { + "term": "一時レーン", + "context": "ndf-workflow", + "meaning": "並列の計画のオーガナイザーが、持ち主のいない共有の作業(まとめた配布のプランの実行・その失敗の対応・止まったレーンの保留の周回の残り)のためだけに起こすラッパー(<計画名>-rel<連番>、役割 helper)。同時に 1 本までで、作業を終えると自分で止まる", + "source": "plugins/ndf/skills/development-workflow/references/parallel.md" + }, + { + "term": "保留の周回", + "context": "ndf-workflow", + "meaning": "並列の計画のループのレーンで、答えを待たずに締めた周回(配布待ち・質問待ち)。課題は締め直すまで選定から外し、答えの後に残りの工程を流して 1 回だけ締め直す", + "source": "plugins/ndf/skills/development-workflow/references/loop.md" + }, + { + "term": "配布待ち", + "context": "ndf-workflow", + "meaning": "配布の依頼を送り、答えを待たずに締めた周回の結果。まとめはまだ流していない", + "source": "plugins/ndf/skills/development-workflow/references/loop.md" + }, + { + "term": "質問待ち", + "context": "ndf-workflow", + "meaning": "作業の途中の問いを問いの hook がオーガナイザーへ送り、止まったプランとステップを残して締めた周回の結果", + "source": "plugins/ndf/skills/development-workflow/references/loop.md" }, { "term": "再起動の方針", "context": "ndf-workflow", "meaning": "conductor が工程境界で会話を切るかを決める規則の組。現行方式と試行方式の 2 つ", - "pending_source": "issues/issue-2262-requirements.md" + "source": "docs/specifications/ndf-context-restart-policy.md" }, { "term": "現行方式", "context": "ndf-workflow", "meaning": "運用上限 200K を超えたら次の工程を止め、4 つのカットポイントではコンテキスト量に関わらず ndf-next を出す再起動の方針。既定", - "pending_source": "issues/issue-2262-requirements.md" + "source": "docs/specifications/ndf-context-restart-policy.md" }, { "term": "試行方式", "context": "ndf-workflow", "meaning": "カットポイントを再起動を判断できる境界として扱い、判断開始の値・運用上限・容量で継続か再起動かを決める再起動の方針。明示的に選んだときだけ働く", - "pending_source": "issues/issue-2262-requirements.md" + "source": "docs/specifications/ndf-context-restart-policy.md" }, { "term": "判断開始の値", "context": "ndf-workflow", "meaning": "試行方式で、これを超えたら工程境界で継続と再起動を比べ始めるコンテキスト量(初期値 200K)", - "pending_source": "issues/issue-2262-requirements.md" + "source": "docs/specifications/ndf-context-restart-policy.md" }, { "term": "運用上限", "context": "ndf-workflow", "meaning": "これを超えたら次の工程への進入を止めるコンテキスト量(現行方式 200K・試行方式の初期値 400K)。ツール呼び出しの途中では止めない", - "pending_source": "issues/issue-2262-requirements.md" + "source": "docs/specifications/ndf-context-restart-policy.md" }, { "term": "容量の余白", "context": "ndf-workflow", "meaning": "実行環境の窓の上限(または自動圧縮が始まる量)から、次の作業・ツール結果・出力と進行の保存のために空けておく量", - "pending_source": "issues/issue-2262-requirements.md" + "source": "docs/specifications/ndf-context-restart-policy.md" }, { "term": "再起動の理由", "context": "ndf-workflow", "meaning": "工程境界で会話を切った・止めたときの理由。容量・運用上限・費用・独立性・手動と、現行方式のカットポイントの 6 つ", - "pending_source": "issues/issue-2262-design.md" + "source": "docs/specifications/ndf-context-restart-policy.md" }, { "term": "復元の区間", "context": "ndf-workflow", "meaning": "再起動した新しいセッションが、引継ぎ文書を読み始めてから最初の工程を起動するまで", - "pending_source": "issues/issue-2262-requirements.md" + "source": "docs/specifications/ndf-context-restart-policy.md" }, { "term": "境界の判定", "context": "ndf-workflow", "meaning": "工程の起動 1 回ごとに出す継続 / 再起動 / 拒否と、その帯・理由・使った値の組。1 件ずつ記録に残る", - "pending_source": "issues/issue-2262-design.md" + "source": "docs/specifications/ndf-context-restart-policy.md" }, { "term": "適用値", "context": "ndf-workflow", "meaning": "優先順位を当てて解決した、そのセッションで効く再起動の方針・判断開始の値・運用上限・容量の余白と、それぞれの出所", - "pending_source": "issues/issue-2262-design.md" + "source": "docs/specifications/ndf-context-restart-policy.md" }, { "term": "作業の識別子", "context": "ndf-workflow", "meaning": "複数のセッションを 1 つの作業として結ぶ鍵。起動の引数かスプリントに含まれる課題番号の組(#829 #830)", - "pending_source": "issues/issue-2262-design.md" + "source": "docs/specifications/ndf-context-restart-policy.md" }, { "term": "費用の見込み", "context": "ndf-workflow", "meaning": "判断の帯で、続けた場合と再起動した場合の次の工程までの費用を、記録から較正した中央値で比べた値", - "pending_source": "issues/issue-2262-design.md" + "source": "docs/specifications/ndf-context-restart-policy.md" }, { "term": "引継ぎの区間", "context": "ndf-workflow", "meaning": "conductor が引継ぎ文書を書き始める前に記録した「引継ぎ開始」の時点から、そのセッションの最後の行まで", - "pending_source": "issues/issue-2262-design.md" + "source": "docs/specifications/ndf-context-restart-policy.md" } ] } diff --git a/docs/metrics/ndf-token-usage/2026-11-03.json b/docs/metrics/ndf-token-usage/2026-11-03.json new file mode 100644 index 000000000..dd6135a9e --- /dev/null +++ b/docs/metrics/ndf-token-usage/2026-11-03.json @@ -0,0 +1,1362 @@ +{ + "per_pr": [ + { + "version": "10.27.10", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "sessions": 13, + "sessions_with_pr": 0, + "prs": 0 + }, + { + "version": "10.27.10", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "-", + "sessions": 1, + "sessions_with_pr": 0, + "prs": 0 + }, + { + "version": "10.27.10", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "sessions": 12, + "sessions_with_pr": 0, + "prs": 0 + }, + { + "version": "10.27.14", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "sessions": 1, + "sessions_with_pr": 0, + "prs": 0 + }, + { + "version": "10.27.14", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "sessions": 8, + "sessions_with_pr": 0, + "prs": 0 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "sessions": 12, + "sessions_with_pr": 0, + "prs": 0 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "claude+codex", + "sessions": 3, + "sessions_with_pr": 0, + "prs": 0 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "-", + "sessions": 5, + "sessions_with_pr": 1, + "prs": 1, + "conductor_cost": 4171825.35, + "supervisor_cost": 230606.95, + "worker_cost": 15982.45, + "context": 33455140.0, + "out": 113847.0, + "minutes": 124.12879999876023, + "codex_input": 0.0, + "codex_out": 0.0, + "kiro_credit": 0.0, + "claude_seat_cost": 0.0 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "claude", + "sessions": 1, + "sessions_with_pr": 1, + "prs": 1, + "conductor_cost": 1450376.1500000001, + "supervisor_cost": 93896.65, + "worker_cost": 2128754.45, + "context": 35761494.0, + "out": 129836.0, + "minutes": 185.8829666654269, + "codex_input": 2762603.0, + "codex_out": 12374.0, + "kiro_credit": 0.0, + "claude_seat_cost": 1397183.6500000001 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "claude+codex", + "sessions": 2, + "sessions_with_pr": 1, + "prs": 3, + "conductor_cost": 570927.3333333333, + "supervisor_cost": 1125546.6666666667, + "worker_cost": 398174.8166666667, + "context": 17237121.333333332, + "out": 80676.0, + "minutes": 66.08177777661218, + "codex_input": 6232840.0, + "codex_out": 13218.333333333334, + "kiro_credit": 0.0, + "claude_seat_cost": 326214.6166666667 + } + ], + "per_role": [ + { + "version": "10.27.10", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 13, + "cost": 550325.6692307693, + "context": 3943351.3076923075, + "out": 23909.846153846152, + "minutes": 19.820847436097953, + "p": 60450.230769230766, + "k": 30.384615384615383, + "w5": 0.0, + "w1h": 119768.69230769231, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 1376326, + "rewrite_gap_median": null + }, + { + "version": "10.27.10", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "supervisor", + "role": "judge", + "agent_type": "claude -p", + "count": 1, + "cost": 26166.0, + "context": 11249.0, + "out": 734.0, + "minutes": 0.15833333333333333, + "k": 1.0, + "w5": 0.0, + "w1h": 11247.0 + }, + { + "version": "10.27.10", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "supervisor", + "role": "mvv", + "agent_type": "claude -p", + "count": 2, + "cost": 64966.05, + "context": 29729.5, + "out": 1324.5, + "minutes": 0.2825, + "k": 1.0, + "w5": 0.0, + "w1h": 29156.5 + }, + { + "version": "10.27.10", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "supervisor", + "role": "pr", + "agent_type": "claude -p", + "count": 2, + "cost": 24394.225, + "context": 13856.0, + "out": 1078.0, + "minutes": 0.21166666666666664, + "k": 1.0, + "w5": 0.0, + "w1h": 9389.5 + }, + { + "version": "10.27.10", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "supervisor", + "role": "その他", + "agent_type": "ndf:worker", + "count": 1, + "cost": 275669.55, + "context": 1726697.0, + "out": 15765.0, + "minutes": 2.6419166684150697, + "p": 39185.0, + "k": 23.0, + "w5": 92055.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.27.10", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "worker", + "role": "work", + "agent_type": "claude -p", + "count": 7, + "cost": 73563.88571428572, + "context": 397870.85714285716, + "out": 3548.5714285714284, + "minutes": 7.859999999999999, + "k": 8.857142857142858, + "w5": 0.0, + "w1h": 18416.0 + }, + { + "version": "10.27.10", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.310", + "reviewers": "-", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 1, + "cost": 4785973.55, + "context": 53567004.0, + "out": 142470.0, + "minutes": 250.90755000114441, + "p": 56476.0, + "k": 246.0, + "w5": 0.0, + "w1h": 715251.0, + "rewrites": 1, + "rewrites_after_5m": 1, + "rewrites_untimed": 0, + "rewrite_tokens": 337216, + "rewrite_tokens_after_5m": 337216, + "read_tokens_after_5m": 2569851, + "rewrite_gap_median": 1598.0600166678428 + }, + { + "version": "10.27.10", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 12, + "cost": 347800.46249999997, + "context": 1760330.75, + "out": 15286.0, + "minutes": 3.4011847221189075, + "p": 62880.333333333336, + "k": 16.75, + "w5": 0.0, + "w1h": 94011.33333333333, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 79648, + "rewrite_gap_median": null + }, + { + "version": "10.27.10", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "supervisor", + "role": "mvv", + "agent_type": "claude -p", + "count": 4, + "cost": 47845.025, + "context": 20430.0, + "out": 1508.75, + "minutes": 0.30833333333333335, + "k": 1.0, + "w5": 0.0, + "w1h": 20142.5 + }, + { + "version": "10.27.10", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "supervisor", + "role": "pr", + "agent_type": "claude -p", + "count": 5, + "cost": 13297.45, + "context": 13437.0, + "out": 767.4, + "minutes": 0.16800000000000004, + "k": 1.0, + "w5": 0.0, + "w1h": 4506.0 + }, + { + "version": "10.27.10", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "supervisor", + "role": "その他", + "agent_type": "general-purpose", + "count": 2, + "cost": 98859.25, + "context": 96767.0, + "out": 4272.5, + "minutes": 0.7314749995867411, + "p": 36219.5, + "k": 2.0, + "w5": 60545.5, + "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.27.10", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "supervisor", + "role": "その他", + "agent_type": "ndf:worker", + "count": 7, + "cost": 269811.87142857147, + "context": 1669632.0, + "out": 14982.285714285714, + "minutes": 2.433390476022448, + "p": 26234.428571428572, + "k": 13.285714285714286, + "w5": 92828.0, + "w1h": 0.0, + "rewrites": 1, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 36170, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": 0.0668999989827474 + }, + { + "version": "10.27.10", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "worker", + "role": "work", + "agent_type": "claude -p", + "count": 11, + "cost": 54196.93636363637, + "context": 208953.45454545456, + "out": 1893.8181818181818, + "minutes": 0.4186363636363637, + "k": 6.090909090909091, + "w5": 0.0, + "w1h": 17573.81818181818 + }, + { + "version": "10.27.14", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 1, + "cost": 2230388.25, + "context": 25325328.0, + "out": 85662.0, + "minutes": 156.03301666577656, + "p": 59125.0, + "k": 128.0, + "w5": 0.0, + "w1h": 274635.0, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 785684, + "rewrite_gap_median": null + }, + { + "version": "10.27.14", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 8, + "cost": 597208.2312500001, + "context": 4112205.25, + "out": 30221.0, + "minutes": 5.422079166769981, + "p": 63245.125, + "k": 29.375, + "w5": 0.0, + "w1h": 123300.375, + "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.27.14", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "supervisor", + "role": "judge", + "agent_type": "claude -p", + "count": 3, + "cost": 24053.666666666668, + "context": 11432.0, + "out": 238.33333333333334, + "minutes": 0.08499999999999999, + "k": 1.0, + "w5": 0.0, + "w1h": 11430.0 + }, + { + "version": "10.27.14", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "supervisor", + "role": "その他", + "agent_type": "Explore", + "count": 1, + "cost": 479496.25, + "context": 3645205.0, + "out": 16873.0, + "minutes": 2.733216667175293, + "p": 22421.0, + "k": 30.0, + "w5": 177345.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.27.14", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "worker", + "role": "work", + "agent_type": "claude -p", + "count": 1, + "cost": 355876.9, + "context": 2410698.0, + "out": 18300.0, + "minutes": 3.5766666666666667, + "k": 35.0, + "w5": 0.0, + "w1h": 73732.0 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 12, + "cost": 631608.525, + "context": 5363592.75, + "out": 20661.5, + "minutes": 22.369047223528227, + "p": 58379.416666666664, + "k": 41.166666666666664, + "w5": 0.0, + "w1h": 133351.25, + "rewrites": 2, + "rewrites_after_5m": 1, + "rewrites_untimed": 0, + "rewrite_tokens": 238086, + "rewrite_tokens_after_5m": 203643, + "read_tokens_after_5m": 1259355, + "rewrite_gap_median": 1180.558641664187 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "supervisor", + "role": "judge", + "agent_type": "claude -p", + "count": 3, + "cost": 26260.666666666668, + "context": 12366.333333333334, + "out": 306.0, + "minutes": 0.10555555555555556, + "k": 1.0, + "w5": 0.0, + "w1h": 12364.333333333334 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "supervisor", + "role": "pr", + "agent_type": "claude -p", + "count": 2, + "cost": 31806.0, + "context": 13501.5, + "out": 961.0, + "minutes": 0.17749999999999996, + "k": 1.0, + "w5": 0.0, + "w1h": 13499.5 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "supervisor", + "role": "その他", + "agent_type": "ndf:worker", + "count": 2, + "cost": 95555.575, + "context": 81119.5, + "out": 5064.0, + "minutes": 0.8375750005245208, + "p": 25971.0, + "k": 2.0, + "w5": 55146.5, + "w1h": 0.0, + "rewrites": 1, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 35745, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": 0.42241666714350384 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "layer": "worker", + "role": "work", + "agent_type": "claude -p", + "count": 6, + "cost": 156385.69166666668, + "context": 773600.6666666666, + "out": 7048.0, + "minutes": 1.4133333333333333, + "k": 17.166666666666668, + "w5": 0.0, + "w1h": 42274.166666666664 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "claude+codex", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 3, + "cost": 2066599.8, + "context": 20189527.666666668, + "out": 81084.0, + "minutes": 92.74148333337573, + "p": 42265.666666666664, + "k": 115.33333333333333, + "w5": 0.0, + "w1h": 334075.6666666667, + "rewrites": 2, + "rewrites_after_5m": 2, + "rewrites_untimed": 0, + "rewrite_tokens": 326267, + "rewrite_tokens_after_5m": 326267, + "read_tokens_after_5m": 983941, + "rewrite_gap_median": 310.9080333332221 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "claude+codex", + "layer": "supervisor", + "role": "judge", + "agent_type": "claude -p", + "count": 4, + "cost": 28995.0, + "context": 13547.25, + "out": 380.5, + "minutes": 0.10458333333333333, + "k": 1.0, + "w5": 0.0, + "w1h": 13545.25 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "claude+codex", + "layer": "supervisor", + "role": "その他", + "agent_type": "ndf:worker", + "count": 3, + "cost": 1329678.2666666666, + "context": 12323186.666666666, + "out": 85079.33333333333, + "minutes": 14.021288888984257, + "p": 39828.666666666664, + "k": 55.0, + "w5": 240009.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.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "claude+codex", + "layer": "worker", + "role": "work", + "agent_type": "claude -p", + "count": 8, + "cost": 155325.53125, + "context": 931603.125, + "out": 8316.5, + "minutes": 12.26875, + "k": 15.125, + "w5": 0.0, + "w1h": 34428.25 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "-", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 5, + "cost": 2008636.75, + "context": 18214713.6, + "out": 60167.8, + "minutes": 113.65868999958039, + "p": 46531.4, + "k": 95.4, + "w5": 0.0, + "w1h": 408642.6, + "rewrites": 5, + "rewrites_after_5m": 5, + "rewrites_untimed": 0, + "rewrite_tokens": 977888, + "rewrite_tokens_after_5m": 977888, + "read_tokens_after_5m": 4151984, + "rewrite_gap_median": 82.70849999984105 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "-", + "layer": "supervisor", + "role": "pr", + "agent_type": "claude -p", + "count": 1, + "cost": 37832.0, + "context": 14572.0, + "out": 1738.0, + "minutes": 0.29, + "k": 1.0, + "w5": 0.0, + "w1h": 14570.0 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "-", + "layer": "supervisor", + "role": "その他", + "agent_type": "ndf:worker", + "count": 4, + "cost": 439571.5625, + "context": 3467574.75, + "out": 18937.25, + "minutes": 5.604016666611035, + "p": 39808.75, + "k": 38.75, + "w5": 142860.0, + "w1h": 0.0, + "rewrites": 1, + "rewrites_after_5m": 1, + "rewrites_untimed": 0, + "rewrite_tokens": 120320, + "rewrite_tokens_after_5m": 120320, + "read_tokens_after_5m": 0, + "rewrite_gap_median": 6.833383333683014 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "-", + "layer": "worker", + "role": "work", + "agent_type": "claude -p", + "count": 13, + "cost": 388545.8884615385, + "context": 2894218.4615384615, + "out": 18046.0, + "minutes": 12.107051282051282, + "k": 32.23076923076923, + "w5": 0.0, + "w1h": 78742.84615384616 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "claude", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 1, + "cost": 1450376.1500000001, + "context": 17406221.0, + "out": 42594.0, + "minutes": 185.8829666654269, + "p": 42054.0, + "k": 108.0, + "w5": 0.0, + "w1h": 188136.0, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 1169837, + "rewrite_gap_median": null + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "claude", + "layer": "supervisor", + "role": "judge", + "agent_type": "claude -p", + "count": 1, + "cost": 23471.0, + "context": 10569.0, + "out": 467.0, + "minutes": 0.12000000000000001, + "k": 1.0, + "w5": 0.0, + "w1h": 10567.0 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "claude", + "layer": "supervisor", + "role": "pr", + "agent_type": "claude -p", + "count": 2, + "cost": 28970.225, + "context": 14794.0, + "out": 1618.0, + "minutes": 0.2641666666666667, + "k": 1.0, + "w5": 0.0, + "w1h": 10327.5 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "claude", + "layer": "supervisor", + "role": "slow", + "agent_type": "claude -p", + "count": 2, + "cost": 6242.6, + "context": 4466.0, + "out": 333.0, + "minutes": 0.10416666666666667, + "k": 1.0, + "w5": 0.0, + "w1h": 2232.0 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "claude", + "layer": "worker", + "role": "work", + "agent_type": "claude -p", + "count": 9, + "cost": 236528.27222222224, + "context": 2034020.4444444445, + "out": 9208.111111111111, + "minutes": 7.2122222222222225, + "k": 19.11111111111111, + "w5": 0.0, + "w1h": 45513.666666666664 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "claude+codex", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 2, + "cost": 1212627.4249999998, + "context": 12768465.5, + "out": 45360.5, + "minutes": 142.56740833123524, + "p": 43382.5, + "k": 85.5, + "w5": 0.0, + "w1h": 178048.0, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 1897810, + "rewrite_gap_median": null + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "claude+codex", + "layer": "supervisor", + "role": "judge", + "agent_type": "claude -p", + "count": 2, + "cost": 23871.5, + "context": 11320.5, + "out": 246.5, + "minutes": 0.08499999999999999, + "k": 1.0, + "w5": 0.0, + "w1h": 11318.5 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "claude+codex", + "layer": "supervisor", + "role": "その他", + "agent_type": "ndf:supervisor-waits", + "count": 2, + "cost": 683517.6500000001, + "context": 6714933.0, + "out": 20952.5, + "minutes": 43.530224998792015, + "p": 47219.5, + "k": 58.0, + "w5": 0.0, + "w1h": 124553.0, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 666373, + "rewrite_gap_median": null + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "claude+codex", + "layer": "supervisor", + "role": "その他", + "agent_type": "ndf:worker", + "count": 5, + "cost": 479728.9800000001, + "context": 3296146.4, + "out": 19737.8, + "minutes": 14.766906666755677, + "p": 38888.0, + "k": 34.0, + "w5": 180137.2, + "w1h": 0.0, + "rewrites": 4, + "rewrites_after_5m": 4, + "rewrites_untimed": 0, + "rewrite_tokens": 363420, + "rewrite_tokens_after_5m": 363420, + "read_tokens_after_5m": 0, + "rewrite_gap_median": 10.28914166688919 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "claude+codex", + "layer": "worker", + "role": "work", + "agent_type": "claude -p", + "count": 10, + "cost": 272037.95, + "context": 1873421.2, + "out": 13462.5, + "minutes": 9.409333333333333, + "k": 25.5, + "w5": 0.0, + "w1h": 56927.0 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "claude+codex", + "layer": "worker", + "role": "その他", + "agent_type": "ndf:worker", + "count": 5, + "cost": 95287.73000000001, + "context": 417721.4, + "out": 3117.6, + "minutes": 0.6927400000890096, + "p": 38091.0, + "k": 8.2, + "w5": 48998.4, + "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 + } + ], + "external": [ + { + "version": "10.27.14", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "runtime": "claude", + "skill": "cross-refactoring", + "cli_model": "claude-opus-5-5", + "count": 6, + "context": 460378.0, + "out": 13319.5, + "cost": 172859.375, + "minutes": 2.1404444442854986, + "p": 14023.333333333334, + "k": 11.5, + "w5": 0.0, + "w1h": 42677.5, + "rewrites": 1, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 25038, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": 0.2872333327929179, + "call_seats": 6 + }, + { + "version": "10.27.14", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "-", + "runtime": "codex", + "skill": "cross-refactoring", + "cli_model": "gpt-6.1-sol", + "count": 1, + "input": 316125.0, + "cached": 267776.0, + "out": 5091.0, + "minutes": 2.1235833326975504, + "p": 19588.0, + "k": 7.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, + "call_seats": 1 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "claude+codex", + "runtime": "claude", + "skill": "cross-review", + "cli_model": "claude-opus-5-5", + "count": 13, + "context": 291746.3846153846, + "out": 4742.076923076923, + "cost": 119866.56923076922, + "minutes": 0.8594807701233106, + "p": 27253.615384615383, + "k": 6.538461538461538, + "w5": 881.0769230769231, + "w1h": 41281.61538461538, + "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": 13 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.316", + "reviewers": "claude+codex", + "runtime": "codex", + "skill": "cross-review", + "cli_model": "gpt-6.1-sol", + "count": 13, + "input": 589642.7692307692, + "cached": 519817.8461538461, + "out": 3031.3846153846152, + "minutes": 1.379767948541886, + "p": 27049.384615384617, + "k": 8.692307692307692, + "rewrites": 4, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 105330, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": 0.05647499958674113, + "call_seats": 13 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "-", + "runtime": "claude", + "skill": "cross-refactoring", + "cli_model": "claude-opus-5-5", + "count": 12, + "context": 337833.3333333333, + "out": 4251.833333333333, + "cost": 97335.18333333335, + "minutes": 1.0118541657924651, + "p": 21327.333333333332, + "k": 6.833333333333333, + "w5": 3765.3333333333335, + "w1h": 28027.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, + "call_seats": 12 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "-", + "runtime": "codex", + "skill": "cross-refactoring", + "cli_model": "gpt-6.1-sol", + "count": 1, + "input": 2318796.0, + "cached": 2189568.0, + "out": 10045.0, + "minutes": 4.696166666348775, + "p": 28049.0, + "k": 22.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, + "call_seats": 1 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "claude", + "runtime": "claude", + "skill": "cross-review", + "cli_model": "claude-opus-5-5", + "count": 1, + "context": 93639.0, + "out": 2097.0, + "cost": 42145.25, + "minutes": 0.43571666479110716, + "p": 13987.0, + "k": 5.0, + "w5": 22474.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, + "call_seats": 1 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "claude", + "runtime": "claude", + "skill": "cross-refactoring", + "cli_model": "claude-opus-5-5", + "count": 13, + "context": 434776.53846153844, + "out": 4106.615384615385, + "cost": 104233.72307692308, + "minutes": 0.9215076920313713, + "p": 21753.0, + "k": 6.846153846153846, + "w5": 3144.3846153846152, + "w1h": 29833.615384615383, + "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": 13 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "claude", + "runtime": "codex", + "skill": "cross-refactoring", + "cli_model": "gpt-6.1-sol", + "count": 1, + "input": 2762603.0, + "cached": 2599168.0, + "out": 12374.0, + "minutes": 6.470816663901011, + "p": 27952.0, + "k": 24.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, + "call_seats": 1 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "claude+codex", + "runtime": "claude", + "skill": "cross-review", + "cli_model": "claude-opus-5-5", + "count": 16, + "context": 754047.375, + "out": 5900.0, + "cost": 167805.3625, + "minutes": 1.1417895836134753, + "p": 33245.3125, + "k": 8.8125, + "w5": 7330.6875, + "w1h": 47071.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": 16 + }, + { + "version": "10.27.14", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.317", + "reviewers": "claude+codex", + "runtime": "codex", + "skill": "cross-review", + "cli_model": "gpt-6.1-sol", + "count": 12, + "input": 2373908.25, + "cached": 2234048.0, + "out": 5096.25, + "minutes": 2.996583333280351, + "p": 34933.833333333336, + "k": 16.666666666666668, + "rewrites": 2, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 132293, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": 0.5240333338578542, + "call_seats": 12 + } + ], + "meta": { + "by": [ + "version", + "mode", + "model", + "cc", + "reviewers" + ], + "min_version": "10.27.10", + "until": "2026-11-03T18:45:00Z", + "sessions": 58, + "sessions_with_pr": 3, + "unlinked_external": 3108, + "unlinked_ledger": 0, + "skipped": { + "ndf の Skill が無い": 866, + "版を判定できない": 20 + }, + "released": "10.27.15", + "previous": "2026-11-02-3.json" + } +} diff --git a/docs/metrics/ndf-token-usage/2026-11-03.md b/docs/metrics/ndf-token-usage/2026-11-03.md new file mode 100644 index 000000000..0cccc046a --- /dev/null +++ b/docs/metrics/ndf-token-usage/2026-11-03.md @@ -0,0 +1,121 @@ +# ndf の版ごとのトークン消費と所要時間(2026-11-03 集計・10.27.15 の配布) + +**10.27.15 を正式版として出したときの記録である。** 打ち切りの時刻は `2026-11-03T18:45:00Z`。表の最初の行は 10.27.10(前の記録: 2026-11-02-3.md)。既定では前の記録の最後の版で、記録どうしが 1 行ずつ重なる。生の記録・会話の本文・リポジトリ名・会話の ID は残していない。集計した日時点の記録で、以後の記録の増減には追随しない。 + +## 作り方 + +同じ打ち切りの時刻で走らせ直すと、記録が残っている間は同じ `.json` ができる。ただし kiro の記録はターンに時刻を持たないため、打ち切りをまたいで進行中だった席は credit が変わりうる。 + +```bash +python3 scripts/token-usage-snapshot.py --released 10.27.15 --until 2026-11-03T18:45:00Z --min-version 10.27.10 +python3 scripts/token-usage.py --min-version 10.27.10 --until 2026-11-03T18:45:00Z --by version # 末尾の「集計の出力」 +``` + +## 比べるときの注意 + +- **PR を作った会話が 2 件以下の版:** 無し。作業の中身の違いが版の違いより大きい +- **CHANGELOG.md にあって表に出ない版:** 10.27.10, 10.27.12, 10.27.13, 10.27.15。その版で始めた PR つきの会話が無い(会話の版は最初に読んだ版で決まる) +- **打ち切りの 30 分前以降にも行がある会話を含む版:** 10.27.14。進行中だった会話は、値が打ち切りの時刻までの分である + +## 版ごとの差(PR 1 本あたり) + +換算の合計は conductor・supervisor・worker の換算の和。前の版との差は、表の 1 つ上の行との比である。±30% を超えた版を下の「読み取り」で扱う。 + +| ndf の版 | 会話 | PR | 換算の合計 | 前の版との差 | conductor | supervisor | worker | 入力 | 所要 | codex 入力 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 10.27.14 | 3 | 5 | 2.88M | - | 1.47M | 0.74M | 0.67M | 24.19M | 101.7 | 4.29M | + +## 版と層ごとの呼び出しとキャッシュ(1 起動あたり) + +P と k と書き込みは 1 起動あたり、書き直しとうち 5 分超は合計の回数である。 + +| ndf の版 | 層 | 起動 | P | k | 書き込み 5 分 | 書き込み 1 時間 | 書き直し | うち 5 分超 | 1 起動あたりの書き直し | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 10.27.10 | conductor | 26 | 61.4k | 32.4 | 0.00M | 0.13M | 1 | 1 | 0.04 | +| 10.27.10 | supervisor | 24 | 29.5k | 5.6 | 0.04M | 0.01M | 1 | 0 | 0.10 | +| 10.27.10 | worker | 18 | - | 7.2 | 0.00M | 0.02M | - | - | - | +| 10.27.14 | conductor | 32 | 54.8k | 61.2 | 0.00M | 0.20M | 9 | 8 | 0.28 | +| 10.27.14 | supervisor | 37 | 37.8k | 17.8 | 0.07M | 0.01M | 6 | 5 | 0.35 | +| 10.27.14 | worker | 52 | 38.1k | 22.0 | 0.00M | 0.05M | 0 | 0 | 0.00 | + +## 読み取り + +差の大きい版は無い(すべて ±30% の内)。読み取りは要らない。 + +## 集計の出力 + +対象の会話: 58 件 / PR を作った会話: 3 件 / 寄せ先の無い外部 CLI: 3108 件 / 寄せ先の無い帳簿の行: 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.27.14 | 3 | 5 | 1.47M | 0.74M | 0.67M | 24.19M | 97.1k | 101.7 | 4.29M | 10.4k | 0.00 | 0.48M | + +### フェーズごと(1 起動あたり) + +| 動いた版 | 層 | フェーズ | 定義 | 起動 | 換算 | 入力 | 出力 | 所要 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 10.27.10 | conductor | - | - | 26 | 0.62M | 4.84M | 24.5k | 21.1 | +| 10.27.10 | supervisor | judge | claude -p | 1 | 0.03M | 0.01M | 0.7k | 0.2 | +| 10.27.10 | supervisor | mvv | claude -p | 6 | 0.05M | 0.02M | 1.4k | 0.3 | +| 10.27.10 | supervisor | pr | claude -p | 7 | 0.02M | 0.01M | 0.9k | 0.2 | +| 10.27.10 | supervisor | その他 | general-purpose | 2 | 0.10M | 0.10M | 4.3k | 0.7 | +| 10.27.10 | supervisor | その他 | ndf:worker | 8 | 0.27M | 1.68M | 15.1k | 2.5 | +| 10.27.10 | worker | work | claude -p | 18 | 0.06M | 0.28M | 2.5k | 3.3 | +| 10.27.14 | conductor | - | - | 32 | 1.08M | 9.91M | 39.1k | 55.8 | +| 10.27.14 | supervisor | judge | claude -p | 13 | 0.03M | 0.01M | 0.3k | 0.1 | +| 10.27.14 | supervisor | pr | claude -p | 5 | 0.03M | 0.01M | 1.4k | 0.2 | +| 10.27.14 | supervisor | slow | claude -p | 2 | 0.01M | 0.00M | 0.3k | 0.1 | +| 10.27.14 | supervisor | その他 | Explore | 1 | 0.48M | 3.65M | 16.9k | 2.7 | +| 10.27.14 | supervisor | その他 | ndf:supervisor-waits | 2 | 0.68M | 6.71M | 21.0k | 43.5 | +| 10.27.14 | supervisor | その他 | ndf:worker | 14 | 0.60M | 4.82M | 31.4k | 10.0 | +| 10.27.14 | worker | work | claude -p | 47 | 0.26M | 1.90M | 12.3k | 9.1 | +| 10.27.14 | worker | その他 | ndf:worker | 5 | 0.10M | 0.42M | 3.1k | 0.7 | + +### フェーズごとの呼び出しとキャッシュ + +P は起動ごとの固定費(最初の呼び出しの文脈)、k は呼び出し回数(どちらも 1 起動あたり)。書き直しは 2 回目以降の呼び出しのうち、書き込みが文脈の 50% を超え、かつ 20k を超えたものの回数(合計)。5 分超はそのうち直前の呼び出しとの間隔が 5 分を超えた回数、間隔は直前の間隔の中央値(分)。5 分超の書き直し・読み込みは、直前の間隔が 5 分を超えた呼び出しの書き直しの量と、書き直しでない呼び出しの読み込みの量(どちらも合計)。定義はサブエージェントの定義の名前。 + +| 動いた版 | 層 | フェーズ | 定義 | 起動 | P | k | 書き込み 5 分 | 書き込み 1 時間 | 書き直し | 5 分超 | 5 分超の書き直し | 5 分超の読み込み | 間隔 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 10.27.10 | conductor | - | - | 26 | 61.4k | 32.4 | 0.00M | 0.13M | 1 | 1 | 0.34M | 4.03M | 1598.1 | +| 10.27.10 | supervisor | judge | claude -p | 1 | - | 1.0 | 0.00M | 0.01M | - | - | - | - | - | +| 10.27.10 | supervisor | mvv | claude -p | 6 | - | 1.0 | 0.00M | 0.02M | - | - | - | - | - | +| 10.27.10 | supervisor | pr | claude -p | 7 | - | 1.0 | 0.00M | 0.01M | - | - | - | - | - | +| 10.27.10 | supervisor | その他 | general-purpose | 2 | 36.2k | 2.0 | 0.06M | 0.00M | 0 | 0 | 0.00M | 0.00M | - | +| 10.27.10 | supervisor | その他 | ndf:worker | 8 | 27.9k | 14.5 | 0.09M | 0.00M | 1 | 0 | 0.00M | 0.00M | 0.1 | +| 10.27.10 | worker | work | claude -p | 18 | - | 7.2 | 0.00M | 0.02M | - | - | - | - | - | +| 10.27.14 | conductor | - | - | 32 | 54.8k | 61.2 | 0.00M | 0.20M | 9 | 8 | 1.51M | 10.25M | 100.6 | +| 10.27.14 | supervisor | judge | claude -p | 13 | - | 1.0 | 0.00M | 0.01M | - | - | - | - | - | +| 10.27.14 | supervisor | pr | claude -p | 5 | - | 1.0 | 0.00M | 0.01M | - | - | - | - | - | +| 10.27.14 | supervisor | slow | claude -p | 2 | - | 1.0 | 0.00M | 0.00M | - | - | - | - | - | +| 10.27.14 | supervisor | その他 | Explore | 1 | 22.4k | 30.0 | 0.18M | 0.00M | 0 | 0 | 0.00M | 0.00M | - | +| 10.27.14 | supervisor | その他 | ndf:supervisor-waits | 2 | 47.2k | 58.0 | 0.00M | 0.12M | 0 | 0 | 0.00M | 0.67M | - | +| 10.27.14 | supervisor | その他 | ndf:worker | 14 | 37.5k | 35.3 | 0.16M | 0.00M | 6 | 5 | 0.48M | 0.00M | 8.7 | +| 10.27.14 | worker | work | claude -p | 47 | - | 23.5 | 0.00M | 0.06M | - | - | - | - | - | +| 10.27.14 | worker | その他 | ndf:worker | 5 | 38.1k | 8.2 | 0.05M | 0.00M | 0 | 0 | 0.00M | 0.00M | - | + +### 外部 CLI(1 起動あたり) + +kiro はトークン数を記録しない(値が 0)ため credit だけを載せる。agy は読まない。 + +| 動いた版 | ランタイム | Skill | モデル | 起動 | 入力 | cache | 出力 | credit | 所要 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 10.27.14 | claude | cross-review | claude-opus-5-5 | 30 | 0.53M | 0.00M | 5.3k | 0.00 | 1.0 | +| 10.27.14 | claude | cross-refactoring | claude-opus-5-5 | 31 | 0.40M | 0.00M | 5.9k | 0.00 | 1.2 | +| 10.27.14 | codex | cross-review | gpt-6.1-sol | 25 | 1.45M | 1.34M | 4.0k | 0.00 | 2.2 | +| 10.27.14 | codex | cross-refactoring | gpt-6.1-sol | 3 | 1.80M | 1.69M | 9.2k | 0.00 | 4.4 | + +### 外部 CLI の呼び出しとキャッシュ(1 起動あたり) + +codex は input のうち cached に当たらなかった分を書き込みとみなし、呼び出しごとの値(last_token_usage)を持つ席だけで数える。kiro は呼び出し回数を記録しない(利用者のターン数は JSON の turns)。取れない値は -。 + +| 動いた版 | ランタイム | Skill | モデル | 起動 | P | k | 書き直し | 5 分超 | 5 分超の書き直し | 5 分超の読み込み | 間隔 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 10.27.14 | claude | cross-review | claude-opus-5-5 | 30 | 30.0k | 7.7 | 0 | 0 | 0.00M | 0.00M | - | +| 10.27.14 | claude | cross-refactoring | claude-opus-5-5 | 31 | 20.1k | 7.7 | 1 | 0 | 0.00M | 0.00M | 0.3 | +| 10.27.14 | codex | cross-review | gpt-6.1-sol | 25 | 30.8k | 12.5 | 6 | 0 | 0.00M | 0.00M | 0.1 | +| 10.27.14 | codex | cross-refactoring | gpt-6.1-sol | 3 | 25.2k | 17.7 | 0 | 0 | 0.00M | 0.00M | - | diff --git a/docs/presentations/2026-11-03-neural-development-factory/README.md b/docs/presentations/2026-11-03-neural-development-factory/README.md new file mode 100644 index 000000000..aa5ee4630 --- /dev/null +++ b/docs/presentations/2026-11-03-neural-development-factory/README.md @@ -0,0 +1,57 @@ +# AI が回すソフトウェア工場 — NDF から Neural Development Factory へ + +社外の勉強会・登壇で話すスライド(台本の 30 ページと、章扉 3 つ・中締めの扉 1 つの 34 ページ)。Software Factory の考え方を冒頭に置き、 +前半で NDF(工場のラインをプラグインにしたもの)を、後半で Neural Development Factory(そのラインを Web から回す +サービス)の構想と画面モックを紹介する。前身は [2026-10-31-neural-development-factory](../2026-10-31-neural-development-factory/README.md) +で、NDF は 2026-11-03 時点の実装、Neural Development Factory は同じ時点の Issue の構想で更新した(足したページは [notes.md](notes.md))。`release` の `form-slide.md` の生成スクリプトの経路で作った。 + +| ファイル | 中身 | +| --- | --- | +| [script.md](script.md) | 台本(第 1 稿)。ページごとの見出しと読み上げ。スピーカーノートの正本。1 文 1 行で書き、`>` だけの行で段落を分ける。改行と空行はそのままノートに入る(`build-deck.js` が読み直す) | +| [notes.md](notes.md) | 構成メモ。章立てと、ページごとの見出し・役割・根拠 | +| [slides/build-deck.js](slides/build-deck.js) | 生成スクリプト。スライドの正本 | +| [slides/theme.json](slides/theme.json) | トーン&マナーのファイル | +| `slides/lib/` | NDF の汎用テンプレートの枠組み(`release` の `assets/slides/lib/`)の写し。書き換えない | + +## 成果物 + +Google Slides を成果物にする。2026-10-31 の資料の Google Slides を同じ URL のまま差し替えた(前の版は Google の版の +履歴に残る)。pptx は生成スクリプトから作り直せるため、コミットしない。Google Slides の URL はリポジトリに書かない。 + +## トーン&マナー + +暗い地(`0A0E1A`)にシアン(`22D3EE`)とバイオレット(`7C5CFF`)を差し色にした。承認など人が判断する箇所は琥珀 +(`FFB547`)の地に暗い字で示す。書体は既定値(Noto Sans JP)。ロゴは置かない。章扉の大きな章番号・表紙と章扉の +半透明の円・見出しの下のアクセントの線は、`lib/` を変えずに生成スクリプトが描き足す。 + +| 項目 | 値 | +| --- | --- | +| 版面 | 16:9(10 × 5.625 インチ) | +| 書体 | 既定値 | +| 色 | `theme.json` の `colors` | +| 著作権表示 | `NDF — github.com/devbasex/ai-plugins` | +| ロゴ | なし | +| 写した時点の `lib/` の内容のハッシュ | `e1fbac1642aef0fe49a57b00d7a2575eaf363bbfd5c55b324d53aa778f632800`(`cat slides/lib/*.js \| sha256sum`) | + +## 画面モック(23〜27 ページ) + +Neural Development Factory の画面モック(claude.ai の Design のキャンバス「Neural Development Factory 画面モック」。 +#2172・#2173 の本文から辿れる)のうち、スプリント一覧・スプリント詳細・承認ゲート・スプリントを始めるの 4 画面と、開発ループの画面(#2347 で実装した画面を元にした)を、 +スライドの図形で描き直した。スクリーンショットにしないのは、スライドの大きさで文字が読めるように要素を絞るためと、 +Google Slides の上で直せるようにするためである。出ている数値・リポジトリ名は例。 + +## 生成と確認の手順 + +```bash +cd docs/presentations/2026-10-31-neural-development-factory +(cd slides && npm ci) +node slides/lib/script-time.js script.md --limit 50 # 尺(約 24 分の読み上げ) +node slides/build-deck.js /tmp/ndf-factory.pptx +``` + +Google Slides への差し替えと、ページごとのサムネイルでの確認は `release` の `form-slide.md` の「変換と確認」の順に行う。 + +## 発表日時点の記録 + +発表日時点の記録で、以後の NDF と Factory の変更に追随しない([../README.md](../README.md) の「この資料の位置づけ」)。 +数値は 2026-11-03 時点の実測(事例は 2026-10-17 時点)。 diff --git a/docs/presentations/2026-11-03-neural-development-factory/notes.md b/docs/presentations/2026-11-03-neural-development-factory/notes.md new file mode 100644 index 000000000..010975d02 --- /dev/null +++ b/docs/presentations/2026-11-03-neural-development-factory/notes.md @@ -0,0 +1,56 @@ +# 構成メモ: AI が回すソフトウェア工場 + +台本([script.md](script.md))を、台本の 30 ページと、章扉 3 つ・中締めの扉 1 つのスライドにした構成。各ページの読み上げはスピーカーノートに入る。 + +2026-10-31 の版([../2026-10-31-neural-development-factory/](../2026-10-31-neural-development-factory/README.md))を、NDF は 2026-11-03 時点の実装、Neural Development Factory は同じ時点の Issue の構想で更新した。足したページは NDF の開発ループ(17)と、Factory のループの画面・オーガナイザー(27・28)である。10-31 の版から、職人型と工場型の対比(旧 4)・判断の画面(旧 28)を外し、数値のページ(旧 8)を 6 に、モードの表(旧 10)を 8 の流れの図にまとめた。各ページの読み上げはスピーカーノートに入る。 + +## 構成 + +キーメッセージ「人は『何を作るか』と『出してよいか』を決める。あとは工場が回す」から逆算した。Software Factory の +4 つの部品(標準工程・品質ゲート・計測・改善)を冒頭で示し、NDF のページは部品の順(標準工程 6〜13 → 品質ゲート 14〜15 → 止めずに回す 16〜18 → 改善 19)に並べた。3 つ目の計測は CLI では弱く、後半の Neural Development Factory(一覧と詳細で見える)が埋める。これを前半から後半へ渡す伏線にした。各ページの読み上げは、次のページで話すことへの橋渡しで終える。 +後半は「CLI では人がラインに張り付く」という課題から Neural Development Factory を導き、仕組み・実行の形態・画面・ +現在地の順に置いた。2026-10-17 の資料から、フォークして育てる話(旧 19〜21)と特徴的な機能の表(旧 16)は外した。 + +表の番号は台本の番号で、章扉は数えない。 + +| # | 見出し | 役割 | 根拠 | +| ---: | --- | --- | --- | +| 1 | 表紙 | — | — | +| 2 | Software Factory とは | 入力(Issue)→ 工場(4 工程)→ 出力の図と、人が決める 2 つ | — | +| 3 | 「工場」は新しい言葉ではない | 1970 年前後の日本のソフトウェア工場と、2026 年の AI エージェントの Software Factory の対比。注記の 3 つの出典にリンクを張った | Cusumano『Japan's Software Factories』(1991)、StrongDM の Software Factory(2026-02)、Dan Shapiro の 5 段階 | +| 4 | 工場に必要な 4 つの部品 | 標準工程・品質ゲート・計測・改善の 4 区画 | — | +| 5 | 指示書だけでは、ラインは回らない | CLAUDE.md への追記の 4 つの壁 | 旧 3 | +| 6 | 実務はスクリプト、AI は規定と監視 | スクリプト 196 本・Skill 49 個・リリース 168 回・導入 10+ と GitHub の URL(旧 8 から移した)。スクリプトと Skill(規定と監視)・hook(強制)・プラグイン(配る先は Claude Code・Codex、Kiro・agy は相手役) | 本数は `plugins/ndf/scripts` 直下と `plugins/ndf/skills` の .py・.sh(テストを除く。10-31 の 186 本も同じ数え方で再現する)。配布先は CHANGELOG 10.27.5(#2306) | +| 7 | Issue をまとめて渡せば、並列に進む | マイルストーン → グループ → スプリント → リリースの図と 2 回の承認 | 旧 6 | +| 8 | 機能追加は、この流れで 1 周 | standard の工程を 3 行で。下に 5 つのモードの帯(旧 10 をまとめた) | 旧 8 | +| 9 | 要件定義・テストファースト: 実績のある技法を組み込む | DDD(ユビキタス言語とドメインモデル)・TDD・Agile の 3 列 | 旧 9 | +| 10 | 構成の見直し: コードの可読性から、日本語の可読性の時代へ | AI 以前にコードの可読性へ向けたこだわりを、日本語の可読性へ向ける時代という主張。文章の指針の年表(逆ピラミッド・IMRAD・Information Mapping・BLUF・ミニマリズム・日本語)と、組み直した 3 本の前後の実測、document-rebuild の 4 手順。11 の「構成の見直し」を取り上げる。開発技法の次に、文章にも技法があると続ける | `plugins/ndf/skills/document-rebuild/SKILL.md`、#391(参考文献と 3 本の実測 #389・#390・#339)。年代は Wikipedia の各項目で確かめ、確かめられなかった BLUF と日本語の 2 つは年を書かない | +| 11 | クロスレビュー・リファクタ: 複数の AI で見て直す | cross-review と cross-refactoring の流れ | 旧 11 | +| 12 | 完了判定: 「完了」も「無い」も証拠で | quality-gates と investigation-rules(検査工程として) | 旧 14 | +| 13 | 3 層のエージェントで止まらない進行 | conductor・supervisor・worker と再開(DBOS で耐久の記録)・切り替え(切り替えは次のページへ) | 旧 12 | +| 14 | NDF Relay: コンテキストを溜めずに走り続ける | 切る基準は費用の比べ合いへ移す構想(既定は 200k、試行方式は context-window.md の「再起動の方針」)。コンテキストが膨らむ 3 つの害と実測(平均 410k Token)、区切りで起動し直す流れ、守り 3 つ。呼び名 NDF Relay は発表用(実体は `plugins/ndf/scripts/relay.py`)。「Agent Relay」は同名の製品があるため避けた | `development-workflow` の `references/context-window.md`(#827 の実測)・`references/relay.md`、`docs/specifications/ndf-relay-segment-restart.md` | +| 15 | 迷ったら MVV で判断する | MVV と必ず人が承認する操作 | 旧 13 | +| 16 | 振り返り・棚卸し: 詰まりを工程へ戻して直す | 改善の 3 つの Skill(一覧の棚卸を含む)と棚卸しの数値 | 旧 15 | +| 17 | 開発ループ: 終わったら次のスプリントを自分で選ぶ | 1 周の流れ、人に戻る 2 か所、止まる条件。実測は 5 本で計 17 周 | `development-workflow` の `references/loop.md`、手元の `.ndf/loop/*/loop.json`(2026-11-03) | +| 18 | 事例: ほかのプロジェクトでも動く | 4 つのプロジェクトの結果の表 | 旧 17・18(2026-10-17 の実測) | +| 19 | CLI の工場には、人が張り付く | CLI の 3 つの課題と Web での解き方の対 | #2168 の背景 | +| 20 | Neural Development Factory の構想 | 登録・始める・見る・承認するの 4 段と 3 つの約束 | #2168 の方針 | +| 21 | 仕組み: 承認を待つ間も、ラインは次の周回へ進む | 画面・API・オーケストレーター・実行ジョブ・GitHub・進捗の受け口(#2171)と、API が事象から作るもの(一覧・詳細 #2172・承認と通知 #2173・課金 #2174)の全体図と、承認を待つ間も次の周回が始まる流れ。オーケストレーターは常駐せず、オーガナイザーを含む | `docs/specifications/ndf-factory-service-boundary.md`、#2347 の定義 6、#2439(受け入れ条件 7) | +| 22 | AI をどう動かすかは 3 つから選ぶ | 実行の形態 3 つの表とプラン | #2168・#2189・`docs/specifications/ndf-factory-accounts-and-plans.md` | +| 23 | 画面: スプリント一覧 | モックの描き直しと注記 3 つ | 画面モック「スプリント一覧」 | +| 24 | 画面: スプリント詳細 | 同上 | 画面モック「スプリント詳細」・#2172 | +| 25 | 画面: 承認ゲート | 同上 | 画面モック「承認ゲートで承認する」・#2173 | +| 26 | 画面: スプリントを始める | 同上 | 画面モック「スプリントを始める」 | +| 27 | 画面: 開発ループ | ループの一覧のモック(周回・予算・利用上限の待ち)と注記 3 つ | #2347、CHANGELOG 10.27.14(#2393)、`docs/specifications/ndf-factory-dev-loops.md` | +| 28 | 並べても、本番の配布は 1 本にまとめる | オーガナイザーの図と、取り合いを防ぐ 4 つの決まり | #2347(配布済み)・#2439(設計を承認、実装前) | +| 29 | 現在地: 子 Issue で順に作っている | 子 Issue 12 行と状態 | #2168 の子課題の表、CHANGELOG とタグ(2026-11-03) | +| 30 | まとめ: 人は 2 つだけ決める | Software Factory + NDF = Neural Development Factory の式と、リポジトリの URL | — | + +## 章立て + +| 章扉 | 名前 | 一言 | ページ | +| --- | --- | --- | --- | +| 1 | Software Factory という考え方 | 人は「何を作るか」と「出してよいか」を決める | 2〜4 | +| 2 | NDF — 工場のラインをプラグインに | Claude Code・Codex で動く開発ワークフロー | 5〜18 | +| — | ここまでは実践、ここからは構想(中締め) | 章番号を持たない扉。前半の NDF は実装済みで実際に使っている(COMBAT PROVEN)、後半は構想(VISION)と分ける | 18 と 19 の間 | +| 3 | Neural Development Factory | 工場を Web へ。CLI の前にいなくても回る | 19〜30 | diff --git a/docs/presentations/2026-11-03-neural-development-factory/script.md b/docs/presentations/2026-11-03-neural-development-factory/script.md new file mode 100644 index 000000000..e7277f8e6 --- /dev/null +++ b/docs/presentations/2026-11-03-neural-development-factory/script.md @@ -0,0 +1,490 @@ +# 台本(第 1 稿): AI が回すソフトウェア工場 — NDF から Neural Development Factory へ + +- 想定: 社外の勉強会・登壇。35 分前後・30 ページ(章扉 3 つと中締めの扉を除く) +- 対象: Claude Code / Codex などの AI コーディングエージェントを使っている、または使い始めたエンジニア +- キーメッセージ: **人は「何を作るか」と「出してよいか」を決める。あとは工場が回す** +- 前身: 2026-10-17 の「独自プラグイン開発の薦め」。NDF の紹介のページはそこから流用し、Software Factory の視点で並べ直した +- 2026-10-31 の版を、NDF は 2026-11-03 時点の実装、Neural Development Factory は同じ時点の Issue の構想で更新した +- 数値は 2026-11-03 時点の実測(事例は 2026-10-17 時点)。事例のプロジェクトは種類で呼び、名前は出さない +- 後半の画面は構想段階のモック。一部の画面は実装済みで、出ている数値は例 +- 各ページの読み上げは、次のページで話すことへの橋渡しで終える。全体を 1 本の話として通すため + +## 構成 + +| パート | ページ | 問い | +| --- | --- | --- | +| 0. Software Factory | 1〜4 | ソフトウェアを「工場」で作るとは何か。何がそろえば工場になるのか | +| 1. NDF | 5〜18 | 工場のラインをプラグインにすると、何ができるのか。他のプロジェクトでも動くのか | +| 2. Neural Development Factory | 19〜29 | なぜ Web のサービスにするのか。どんな画面で、どう動くのか | +| 3. まとめ | 30 | 人は何を決めるのか | + +--- + +## 1. タイトル + +AI が回すソフトウェア工場 — NDF から Neural Development Factory へ + +> 今日は「AI が回すソフトウェア工場」の話をします。 +> +> 前半は、私が開発している NDF というプラグインで、工場のラインをどう作ったか。 +> 後半は、それを Web のサービスにする新しい構想、Neural Development Factory を、画面のモックと一緒に紹介します。 +> +> まず、Software Factory とは何かから始めます。 + +## 2. Software Factory とは + +> Software Factory は、ソフトウェアを職人の手作業ではなく、工場のラインで作るという考え方です。 +> 入口に Issue や仕様を入れると、設計・実装・テスト・リリースの工程を AI エージェントが進め、出口からテスト済みのソフトウェアが出てくる。 +> +> 人がやるのは 2 つだけです。 +> 何を作るかを決めることと、出してよいかを決めること。 +> +> この考え方は、実は 50 年前からあります。 + +## 3. 「工場」は新しい言葉ではない + +> ソフトウェア工場という言葉は新しくありません。 +> +> 1970 年前後、日立・東芝・NEC・富士通が「ソフトウェア工場」を作り、標準の工程・部品の再利用・品質管理で大規模な開発を回しました。 +> MIT の Cusumano が 1991 年の本でまとめています。 +> +> 2026 年、この言葉が AI エージェントの文脈で戻ってきました。 +> 2 月に StrongDM が「人はコードを書かない、レビューもしない」というルールの Software Factory を公開し、Dan Shapiro は AI による開発の 5 段階の最上位を、人がいなくても動く「Dark Factory」と呼びました。 +> +> 作業者が人から AI に替わったことで、50 年前の構想が現実味を帯びています。 +> +> 今の AI コーディングは、人がそばに座ってコードを読んで直す職人型です。 +> 工場型では、人の仕事は書くことから判断することへ移ります。 +> +> 工場にするには、何がそろえばよいのか。 + +## 4. 工場に必要な 4 つの部品 + +> 工場にするには 4 つの部品が必要です。 +> +> どの順に何をするかを決めた標準工程。 +> 不良品を次の工程に流さない品質ゲート。 +> どこまで進んだかが見える計測。 +> そして、詰まったところを工程に戻して直す改善です。 +> +> NDF は、この 4 つを Skill・スクリプト・hook として実装したプラグインです。 +> +> 3 つ目の計測は、CLI のままでは弱いところです。 +> 後半でまた戻ってきます。 +> +> ここから NDF の話に入ります。 +> まず、なぜ指示書だけでは足りないのかから。 + +## 5. 指示書だけでは、ラインは回らない + +> 工場のルールを CLAUDE.md や AGENTS.md に書き足すだけでは、ラインは回りません。 +> +> 4 つの壁に当たります。 +> コンテキスト: 毎回すべてを読み込むので、書くほど重要なルールが埋もれる。 +> 強制力: 書いても守られない。 +> 手順: どの順に何を実行するかは文章では伝わりにくい。 +> ポータビリティ: リポジトリやランタイムを変えると失われる。 +> +> では、何に何を任せれば回るのか。 +> 次のページが NDF の基本の考え方です。 + +## 6. 実務はスクリプト、AI は規定と監視 + +> そこで役割を分けました。 +> 実務を担うのはスクリプトです。 +> コマンドの実行、CI の待ち、マージ、リリース、進行の記録。 +> +> 決まった処理は、AI に任せずスクリプトが毎回同じ結果で行い、結果を JSON で返します。 +> 例えば PR を作る手順で AI に残るのは、コミットメッセージと PR の本文と、進めてよいかの判断だけです。 +> +> Skill、つまり AI エージェントの仕事は 2 つ。 +> いつ・何を・どの順でやるかを規定することと、スクリプトの結果を見て、異常があれば止めることです。 +> 守らせたいルールは hook で強制します。 +> +> この考え方で作ったのが NDF です。 +> スクリプトは 196 本、Skill は 49 個。 +> 2026 年 4 月に開発を始め、7 か月で 168 回リリースし、自分以外の 10 以上のリポジトリに導入されています。 +> GitHub で公開していて、NDF 自身もほぼ NDF のラインで開発しています。 +> +> プラグインとして配る先は Claude Code と Codex の 2 つです。 +> Kiro と agy には配らず、クロスレビューとクロスリファクタリングの相手役として起動するだけにしました。 +> +> では、NDF を使うと開発はどう進むのか。 + +## 7. Issue をまとめて渡せば、並列に進む + +> 使い方は、Issue を 1 つずつ渡すのではなく、マイルストーン単位でまとめて渡します。 +> AI が触る場所と依存関係から Issue をグループに分け、スプリントとして並列に、設計・実装・レビュー・リリースまで進めます。 +> +> 人が判断するのは 2 か所だけ。 +> 設計の承認と、本番リリースの承認です。 +> どちらにも、決めたこと・未検証の点・判断の根拠がそろって届きます。 +> +> まとめて渡しても、全部の変更が同じ工程を通るわけではありません。 + +## 8. 機能追加は、この流れで 1 周 + +> すべての変更にフルの工程を課すと遅くなります。 +> development-workflow は、まず変更を 5 つのモードに分けます。 +> 小さな変更なら実装してレビューするだけ、機能追加ならこの図の流れを 1 周します。 +> +> 要件定義で受け入れ条件を決め、設計を書き、構成を見直してからクロスレビュー。 +> ここで 1 回目の承認。 +> +> 承認後はテストファーストで実装し、クロスリファクタリングとクロスレビュー、証跡による完了判定を経てマージします。 +> 開発版をリリースして確かめ、2 回目の承認で本番へ。 +> +> 最後にリリース後テスト・振り返り・Issue の棚卸しで 1 周です。 +> +> 色を変えた工程は、この後のページで、流れの順に紹介します。 +> まず要件定義とテストファーストから。 + +## 9. 要件定義・テストファースト: 実績のある技法を組み込む + +> 工程の中身は新しい発明ではありません。 +> 人間のチームで実績のある技法を、AI が毎回守れる手順にしています。 +> +> DDD からはユビキタス言語とドメインモデル。 +> 用語集で 1 語 1 意味に決め、CI が食い違いを検査します。 +> 設計はドメインモデルから書き、集約と不変条件を先に決めて、その部分だけを先にレビューします。 +> +> テストファーストからは、失敗するテストから書くサイクル。 +> +> アジャイルからは、マイルストーンをバックログにしたスプリントと振り返りです。 +> +> 次は、設計の後にある「構成の見直し」です。 +> 開発技法と同じように、文章にも実績のある技法があります。 + +## 10. 構成の見直し: コードの可読性から、日本語の可読性の時代へ + +> 設計の後にある「構成の見直し」が、document-rebuild という工程です。 +> +> AI が書いた設計書やレポートは、長いうえに、何を言っているのか分からないことがよくあります。 +> 原因の多くは並び順です。 +> AI の文書は調べた順に並び、読み手が最初に知りたい結論が、一番最後に来ます。 +> +> AI エージェントの前、私たちはコードの可読性にこだわってきました。 +> 名前の付け方、関数の長さ、コメントの書き方。 +> 今はコードを AI が書き、人が読むのは AI の書いた設計書やレポートです。 +> コードの可読性と同じぐらい、日本語の可読性を追究する時代に入ったと言えます。 +> +> そして、読みやすい文章にも、コーディングと同じぐらい研究の歴史があります。 +> 19 世紀の新聞は、大事なことから書く逆ピラミッドを生みました。 +> 科学論文は 20 世紀前半から、IMRAD という決まった構成を使っています。 +> 1960 年代には Robert Horn が、情報の型ごとに見せ方を変える Information Mapping を考えました。 +> 米陸軍は、結論を最初の一文に置く BLUF を規程にしています。 +> 1990 年には John Carroll が、書きすぎないミニマリズムを唱えました。 +> 日本語にも、テクニカルコミュニケーター協会の日本語スタイルガイドや、文の長さから読みやすさを推定する可読性の式があります。 +> +> document-rebuild は、これらの指針に従って組み直します。 +> 測って、読み手が問う順へ並べ替え、文を整えて、会話の文脈を渡さない別の AI に読ませ、測り直します。 +> 内容は足しません。変えるのは並びと文だけです。 +> +> それだけで、3 本の文書で、結論が 158 行目から 1 行目へ移り、平均の文の長さは 46 字から 33 字、88 字から 31 字に縮みました。 +> 新しい規則を発明する必要はありません。 +> 先人の研究に従うだけで、格段に読みやすくなります。 +> +> ここまでが工場の 1 つ目の部品、標準工程です。 +> +> 次は 2 つ目の部品、品質ゲートです。 +> まずレビューから。 + +## 11. クロスレビュー・リファクタ: 複数の AI で見て直す + +> 同じ AI に書かせてレビューさせると、同じ見落としをします。 +> +> cross-review は、2 つの AI にレビューさせ、組み合わせを入れ替えながら新しい指摘が出なくなるまで繰り返します。 +> +> cross-refactoring は、参加する AI 全員が直す場所を提案し、1 つの AI が既定で 30 分の持ち時間に収まる分だけ直します。 +> テストが壊れた変更は取り消します。 +> +> レビューの次は、完了の判定です。 + +## 12. 完了判定: 「完了」も「無い」も証拠で + +> AI の「完了しました」「該当のコードはありません」は、そのまま信じると危険です。 +> +> quality-gates は、実行したコマンド・終了コード・時刻・範囲がそろわない限り完了と認めません。 +> +> investigation-rules は、「無い」という結論に必ず検索のコマンドと結果を添えさせます。 +> +> 工場でいえば検査工程です。 +> +> 品質の関門がそろっても、ラインが途中で止まっては工場になりません。 +> 次は、人がいなくても回り続けるための仕組みを 3 つ紹介します。 +> 1 つ目は、止まっても続きから再開することです。 + +## 13. 3 層のエージェントで止まらない進行 + +> 長い作業は 1 つのセッションでは終わりません。 +> +> 指揮役の conductor、1 つのフェーズを通す supervisor、調査や修正を行う worker の 3 層で進めます。 +> +> フェーズは JSON の計画としてスクリプトが実行します。 +> 進行の記録には DBOS という耐久ワークフローのライブラリを使っています。 +> プロセスが落ちても、同じコマンドを打ち直せば、流れていたステップから続きます。 +> +> ただ、続きから再開できても、1 つの会話が長くなりすぎる問題が残ります。 +> 2 つ目はそれを解く NDF Relay です。 + +## 14. NDF Relay: コンテキストを溜めずに走り続ける + +> 3 層で進めても、指揮役の conductor の会話はどんどん長くなります。 +> +> コンテキストが膨らむと困ったことが 3 つ起きます。 +> 判断の質が落ちること。 +> 応答のたびに溜まった会話を丸ごと読み直すので、費用が増えること。 +> そして自動の要約は情報を落とし、何を落としたかに気づけないことです。 +> +> NDF では 200k Token で切ると決めていましたが、決めただけでは守られませんでした。 +> +> NDF 自身の開発の 30 日間を数えると、conductor の会話は平均 410k Token で、45 件中 37 件が 200k を超えていました。 +> +> そこで NDF Relay です。 +> 中身は relay.py という 1 本のスクリプトです。 +> +> 工程の区切りに来たら、続けるか切るかを決め、切るときは conductor が引継ぎ文書と次のコマンドを残します。 +> NDF Relay が /exit を入力し、プラグインを更新して、同じ端末で新しいセッションを起動します。 +> 工程の状態は Issue・PR・引継ぎ文書にあるので、会話を捨てても戻せます。 +> +> 承認の質問が出ている間は何も入力しないので、承認を代わりに答えることはありません。 +> 利用上限に来たら、登録した別のアカウントで続きます。 +> +> 切る基準は、決まった上限から、続けた場合と切り替えた場合の費用の比べ合いへ移していきます。 +> 窓が大きいモデルなら、安いうちは続けます。 +> +> セッションを切り替えて回り続けても、人に聞かずに決めなければならない場面は残ります。 +> 3 つ目は、AI が迷ったときの判断の基準です。 + +## 15. 迷ったら MVV で判断する + +> AI は判断のたびに迷います。 +> +> NDF はプロジェクトの Mission・Vision・Value を明文化し、AI がそれを根拠に判断して、根拠の番号をログに残します。 +> +> 例えば「まず届ける」「処理の改善に寄与しない指摘は直さない」。 +> +> 本番デプロイのように必ず人が承認する操作もここに定義し、AI は省きません。 +> +> これで、人がいなくてもラインは回ります。 +> 残るのは、回しながら良くしていくことです。 + +## 16. 振り返り・棚卸し: 詰まりを工程へ戻して直す + +> 工場の 4 つ目の部品、改善です。 +> +> out-of-scope は、作業中に見つけた範囲外の問題をその場で Issue にします。 +> retrospective は、リリースのたびに進め方を見直します。 +> backlog-refinement は、スプリントの終了後にバックログを現状に合わせて整え直します。 +> +> ある時点の棚卸しでは、open の Issue の 56% の本文が現状とずれていました。 +> 放っておくと、直った Issue を直すことになります。 +> +> 棚卸しで Issue に順位が付くと、次に何をやるかも機械で選べます。 + +## 17. 開発ループ: 終わったら次のスプリントを自分で選ぶ + +> そこで開発ループです。 +> `--loop` を付けて始めると、スプリントを 1 つ終えるたびに、棚卸しの順位から次のスプリントを自分で選んで周回を続けます。 +> +> 1 周は、選ぶ、設計から本番まで流す、振り返りと棚卸しでまとめる、周回を締める、の順です。 +> 周回ごとに NDF Relay がセッションを切り替えるので、会話は溜まりません。 +> +> 人に戻るのは 2 か所だけです。 +> 設計の MVV 判定が「従う」にならなかった課題は、判断待ちへ外して残りの課題で続けます。 +> 本番の承認は人が答えます。 +> +> 止まる条件も決めてあります。 +> 周回の数・費用・時間の予算、同じ失敗の繰り返し、MVV の変更、選べる Issue が尽きたとき、です。 +> +> NDF 自身の開発では、5 本のループが合わせて 17 周しました。 +> 主題ごとにループを分け、3 本を並べて回したこともあります。 +> +> ここまでの仕組みが、NDF 以外のプロジェクトでも動くのかを見ます。 + +## 18. 事例: ほかのプロジェクトでも動く + +> 言語もチームも違うプロジェクトで動くかを見ます。 +> +> AWS のインフラでは、1 日あたりのマージ PR が 0.95 から 3.38 件に増えました(社内ツールの移行と重なっています)。 +> 20 人で開発する PHP の業務システムでは、設計の承認から 51 時間で 31 本の実装 PR が入りました。 +> 機械学習のモデルでは、データの欠損を見つけて 1 スプリントで本番に出し、相関係数が 0.73 から 0.954 に上がりました。 +> +> どこも最初は詰まり、それが Issue になって次の版で直りました。 +> +> ここまでは、実装して実際に使っているものの話でした。 +> ここで一度区切って、ここからはこの先の構想です。 + +## 19. CLI の工場には、人が張り付く + +> ここから後半です。 +> +> NDF Relay でセッションの切り替えは自動になりました。 +> それでも NDF は CLI で動くので、承認のたびに人がその端末の前にいる必要があります。 +> +> 進み具合は Issue の本文・ボード・ログに散らばり、並べて回すループを一望できません。 +> 工場の部品でいえば、3 つ目の計測が足りず、人がラインの横に張り付いている。 +> +> これを解くのが Neural Development Factory です。 +> +> その構想から説明します。 + +## 20. Neural Development Factory の構想 + +> Neural Development Factory は、NDF を Web から操作できる Software Factory のサービスです。 +> +> GitHub のリポジトリを登録し、スプリントかループを始め、一覧と詳細で進み具合を見て、承認ゲートに来たら通知を受けて Web から承認する。 +> Slack にも届きます。 +> +> 承認ゲートは 2 つのまま増やしません。 +> プラグインとしての NDF は OSS のままで、CLI の利用者の振る舞いは変わりません。 +> ループを並べても、本番の配布は 1 本にまとめます。 +> +> では、どういう仕組みで動かすのか。 + +## 21. 仕組み: 承認を待つ間も、ラインは次の周回へ進む + +> 仕組みです。 +> +> 画面と API の後ろにオーケストレーターがいて、スプリントやループの実行を起こします。 +> 手元の NDF で relay.py が担っていた役割です。 +> ただし常駐はしません。API が呼ばれたときと 5 分ごとの見回りのときだけ動くので、待っているだけの費用はかかりません。 +> 同じリポジトリで並べた実行の配布をまとめるオーガナイザーも、ここで動きます。 +> +> スプリントは実行ジョブというコンテナで動き、中で NDF の conductor・supervisor・worker が動きます。 +> ジョブの中では、ランナーが ndf-next の区切りで次の区間を起動します。 +> ランナーは NDF の進捗を事象に翻訳して、進捗の受け口へ追記だけで送ります。GitHub の PR と CI、承認の結果も同じ受け口に入ります。 +> 一覧と詳細、承認の通知、課金は、API がこの事象から作ります。オーケストレーターの見回りも、ここを読んで止まった実行を見つけます。 +> +> 承認の依頼を出したら、そのジョブは終えてコンテナを止めます。 +> それでもラインは止まりません。 +> ループは答えを待たずに次の周回を別のジョブで始め、答えは依頼を出した実行か、そのループの次の周回へ届きます。 +> +> AI の動かし方は、利用者が選べます。 + +## 22. AI をどう動かすかは 3 つから選ぶ + +> AI の動かし方は 3 つから選べます。 +> +> 1 つ目は手元のマシン。 +> 自分の PC やサーバーで、自分がログインした claude を使います。 +> サービスは Claude の認証情報を持ちません。 +> +> 2 つ目はキーの持ち込みで、自分の Anthropic API キーか、自分の AWS の Bedrock を使います。 +> +> 3 つ目はサービスの Bedrock で、使った Token の分を課金します。 +> +> 料金プランは Free・Pro・Max・Team の 4 つで、Free は 1 つ目と 2 つ目を選べます。 +> +> ここからは画面のモックで、使い方を追います。 +> まずスプリントの一覧です。 + +## 23. 画面: スプリント一覧 + +> ここからは画面のモックです。 +> +> スプリント一覧では、登録したリポジトリのスプリントが並び、それぞれが設計・実装・検査・取り込み・仕上げのどのフェーズにいるかが一目で分かります。 +> +> 上には、同時に動いているスプリントの数、自分の対応待ちの数、今月の Token の使用量が出ます。 +> 承認待ちや判断待ちの行からは、そのまま答えに行けます。 +> +> 一覧から 1 つのスプリントを開くと、詳細の画面です。 + +## 24. 画面: スプリント詳細 + +> スプリントの詳細では、フェーズごとの工程と、工程の記録が時系列で並びます。 +> +> 記録を押すと、触ったファイル、その工程で決めたこと、終了コード・時間・費用が出ます。 +> +> AI がどこで何を決めたかを、後から辿れるのがポイントです。 +> +> 右側には承認ゲートの状態、Pull Request と CI の結果、このスプリントの使用量が出ます。 +> +> 工程が承認ゲートに来ると、人に通知が届きます。 + +## 25. 画面: 承認ゲート + +> 承認ゲートの画面です。 +> 判断の材料は、必ず設計文書と仕様とコードまで辿れるようにします。 +> +> 決めたことを開くと、決定の理由・依拠する仕様・その決定が触るコードの差分へのリンクが出ます。 +> 満たす受け入れ条件の数、未確認のまま残ること、レビューが収束したかも並びます。 +> +> 承認すると設計の Pull Request をマージして実装へ進み、差し戻すと理由を添えて設計をやり直します。 +> +> 承認ゲートの外の問いは、hook が止めて AI に判断させます。 +> 費用がかかる・取り消せない操作だけが、理由と推奨の選択肢つきで同じ対応待ちに並びます。 +> +> 最後に、スプリントを始める画面です。 +> ここから新しい 1 周が始まります。 + +## 26. 画面: スプリントを始める + +> スプリントを始める画面です。 +> +> リポジトリを選ぶと、AI が直近の棚卸しの順位から、前提がそろい 1 つの版に収まる Issue の組を選びます。 +> 外す・足すのは自由です。 +> +> 始める前に、同時に動くスプリントの数、今月の Token、NDF の版を確かめてから始めます。 +> +> スプリントの代わりに、ループを始めることもできます。 + +## 27. 画面: 開発ループ + +> 開発ループの画面です。 +> 前半で紹介した開発ループを、Web から始めて見張ります。 +> +> 始めるときに、周回の数・費用・時間の 3 つの予算を決めます。 +> 予算を超えたら、周回の境で止まります。 +> +> 利用上限に当たったループは止めて待ち、リセット時刻の後に自動で再開します。 +> 待つ間の費用はかかりません。 +> +> 人に戻った課題だけが、対応待ちに並びます。 +> +> ループを並べたとき、本番の配布はどうなるのか。 + +## 28. 並べても、本番の配布は 1 本にまとめる + +> 同じリポジトリでスプリントやループを並べると、取り合いが起きます。 +> +> まず、機械で防ぐ決まりを 4 つ入れて、配布しました。 +> 同じ Issue を 2 つの実行に入れない。 +> 選ぶ対象のマイルストーンが重なるループは同時に動かさない。 +> 古い版の NDF では 1 本に絞る。 +> 同時に動かす数は、リポジトリごとに宣言する。 +> +> その上で、Factory の側にオーガナイザーを置きます。 +> 並んだ実行の本番の配布の依頼を集めて 1 本にまとめるので、本番の承認は 1 回で済みます。 +> 承認と問いも、リポジトリごとに 1 か所へ並びます。 +> +> オーガナイザーは設計を承認したところで、これから実装します。 +> +> このサービスは、今どこまでできているのか。 + +## 29. 現在地: 子 Issue で順に作っている + +> 今の進み具合です。 +> +> 全体の設計、認証と料金プラン、進捗の受け口、実行の制御とランナーは配布しました。 +> 11 月に入って、UX の土台と画面の部品、開発ループと並行の調整も配布しました。 +> +> スプリントの一覧と詳細は、ダミーデータの見本が動いています。 +> オーガナイザーは設計を承認したところです。 +> 承認の通知、課金、手元のマシン、オフラインと MCP がその後に続きます。 +> +> このサービス自体も、NDF の開発ループで開発しています。 +> +> 最後にまとめます。 + +## 30. まとめ: 人は 2 つだけ決める + +> まとめです。 +> +> Software Factory という考え方では、人が決めるのは「何を作るか」と「出してよいか」の 2 つだけです。 +> +> NDF は工場のラインをプラグインにしました。 +> この 2 つを足したものが Neural Development Factory で、そのラインを Web から回せるサービスにします。 +> +> プラグインは OSS で公開しているので、ぜひ試してください。 +> ありがとうございました。 diff --git a/docs/presentations/2026-11-03-neural-development-factory/slides/build-deck.js b/docs/presentations/2026-11-03-neural-development-factory/slides/build-deck.js new file mode 100644 index 000000000..a913d84ad --- /dev/null +++ b/docs/presentations/2026-11-03-neural-development-factory/slides/build-deck.js @@ -0,0 +1,881 @@ +// 発表資料「AI が回すソフトウェア工場 — NDF から Neural Development Factory へ」の生成スクリプト +// (台本の 30 ページと、章扉 3 つ・中締めの扉 1 つの 34 ページ)。 +// +// node build-deck.js <出力>.pptx [] +// +// 枠組みは lib/(NDF の release の assets/slides/lib/ の写し。書き換えない)、 +// トーン&マナーは theme.json(暗い地にシアンとバイオレット)、スピーカーノートは ../script.md(台本)から入る。 +// 色は theme.json の項目名で指す。図形を直接置くときは d.color(<項目名>) で値を引く。 +// 後半の画面(23〜27)は Factory の画面モックを、スライドの図形で描き直したもの。 +const path = require("path"); +const { run } = require("./lib/deck-frame"); + +const SCRIPT = path.join(__dirname, "..", "script.md"); +const REPO = "https://github.com/devbasex/ai-plugins"; + +const X0 = 0.26; +const W = 9.48; + +// 横に n 等分した区画の x と幅 +function cols(n, gap, x0, w) { + const g = gap == null ? 0.16 : gap; + const left = x0 == null ? X0 : x0; + const total = w == null ? W : w; + const cw = (total - g * (n - 1)) / n; + return Array.from({ length: n }, (_, i) => ({ x: left + i * (cw + g), w: cw })); +} + +// 箇条書き("- " で始まる項目はインデントを 1 つ下げる) +function bullets(items, size, color) { + return items.map((t, i) => { + const sub = t.startsWith("- "); + const o = { bullet: { indent: 12 }, indentLevel: sub ? 1 : 0, fontSize: size || 10, breakLine: i < items.length - 1 }; + if (color) o.color = color; + return { text: sub ? t.slice(2) : t, options: o }; + }); +} + +run(__dirname, { script: SCRIPT, title: "AI が回すソフトウェア工場", author: "NDF" }, (d) => { + const P = d.pptx; + // スピーカーノートは台本の改行と空行を残す(lib は段落を改行 1 つでつなぎ、段落の中の改行を消すため、ここで読み直す) + const fs = require("fs"); + const notesByNo = new Map(); + let no = null; + for (const line of fs.readFileSync(SCRIPT, "utf8").split(/\r?\n/)) { + const h = line.match(/^##\s+(\d+)\./); + if (h) { no = Number(h[1]); notesByNo.set(no, []); continue; } + if (/^#{1,2}\s/.test(line)) { no = null; continue; } + const q = no != null && line.match(/^>\s?(.*)$/); + if (q) notesByNo.get(no).push(q[1].trim().replace(/\*\*/g, "").replace(/`/g, "")); + } + d.script.forEach((sp) => { + const ls = notesByNo.get(sp.no); + if (ls) sp.reading = ls.join("\n").replace(/\n{3,}/g, "\n\n").trim(); + }); + let s; + let chapter = 0; + + // ---- 部品 ---- + // 角の丸い箱。line に項目名を渡すと枠線を引く + function rr(sl, x, y, w, h, fill, line, radius) { + sl.addShape(P.ShapeType.roundRect, { + x, y, w, h, rectRadius: radius == null ? 0.08 : radius, + fill: { color: d.color(fill) }, + line: line ? { color: d.color(line), width: 1 } : { type: "none" }, + }); + } + function dot(sl, x, y, r, fill, transparency) { + sl.addShape(P.ShapeType.ellipse, { x, y, w: r, h: r, fill: { color: d.color(fill), transparency: transparency || 0 }, line: { type: "none" } }); + } + // 丸い札(状態の表示) + function pill(sl, x, y, w, h, t, fill, color, size) { + rr(sl, x, y, w, h, fill, null, h / 2); + d.text(sl, t, { x, y, w, h, fontSize: size || 8, bold: true, color: color || "text", align: "center", valign: "middle" }); + } + // ページ。見出しの罫線の左端にアクセントの線を重ねる + // size を渡すと、その見出しだけ文字の大きさを変える(長い見出しを 1 行に収めるため) + function pg(title, lead, size) { + const sl = d.page(size ? [{ text: title, options: { fontSize: size } }] : title, lead); + d.box(sl, 0.24, 0.785, 1.1, 0.035, "accent", false); + return sl; + } + // 章扉。右に大きな章番号を薄く置く + function sec(title, sub) { + chapter++; + const sl = d.section(title, sub); + d.text(sl, String(chapter).padStart(2, "0"), { x: 6.6, y: 0.15, w: 3.2, h: 1.6, fontSize: 110, bold: true, color: "panel2", align: "right", valign: "middle" }); + dot(sl, 8.3, 3.7, 2.4, "header", 82); + dot(sl, 7.5, 4.3, 1.4, "accent", 85); + return sl; + } + // 見出しと本文を持つ区画 + function card(sl, x, y, w, h, head, body, fill, opt) { + const o = opt || {}; + rr(sl, x, y, w, h, fill || "panel1", o.line || null); + if (o.band) d.box(sl, x + 0.14, y, 0.5, 0.05, o.band, false); + const hh = o.headH || 0.34; + d.text(sl, head, { x: x + 0.16, y: y + 0.1, w: w - 0.32, h: hh, fontSize: o.headSize || 12, bold: true, color: o.headColor || "text", valign: "middle" }); + if (body) d.text(sl, body, { x: x + 0.16, y: y + 0.14 + hh, w: w - 0.32, h: h - 0.24 - hh, fontSize: o.bodySize || 10.5, color: o.bodyColor || "ink", valign: "top", paraSpaceAfter: 3 }); + } + // 大きな数値 + function stat(sl, x, y, w, h, value, unit, label, accent) { + rr(sl, x, y, w, h, "panel1"); + d.box(sl, x + 0.18, y + 0.16, 0.4, 0.05, accent || "accent", false); + d.text(sl, [ + { text: value, options: { fontSize: 34, bold: true, color: accent || "accent" } }, + { text: " " + unit, options: { fontSize: 13, bold: true, color: "text" } }, + ], { x: x + 0.18, y: y + 0.26, w: w - 0.3, h: 0.7, valign: "middle" }); + d.text(sl, label, { x: x + 0.18, y: y + 0.98, w: w - 0.3, h: h - 1.05, fontSize: 10.5 }); + } + // 横に並べた工程。{ text } の項目は承認(琥珀の地に暗い字)、{ hi } の項目は後のページで紹介する工程(淡いシアンの地) + function flow(sl, x, y, w, h, labels, opt) { + const o = opt || {}; + const gap = o.gap == null ? 0.22 : o.gap; + const cw = (w - gap * (labels.length - 1)) / labels.length; + labels.forEach((l, i) => { + const cx = x + i * (cw + gap); + const hi = typeof l === "object" && l.hi != null; + const gate = typeof l === "object" && !hi; + rr(sl, cx, y, cw, h, gate ? "caution" : hi ? "accentSoft" : o.fill || "panel2", hi ? "accent" : null); + d.text(sl, gate ? l.text : hi ? l.hi : l, { x: cx, y, w: cw, h, fontSize: o.size || 9.5, bold: true, color: gate ? "background" : "text", align: "center", valign: "middle" }); + if (i < labels.length - 1) d.arrow(sl, cx + cw + 0.02, y + h / 2, gap - 0.04, 0, "accent"); + }); + } + // 丸数字の注記(モックのページの右列) + function notes(sl, x, y, w, items) { + items.forEach((it, i) => { + const yy = y + i * 1.12; + dot(sl, x, yy + 0.02, 0.34, "accent"); + d.text(sl, String(i + 1), { x, y: yy + 0.02, w: 0.34, h: 0.34, fontSize: 11, bold: true, color: "background", align: "center", valign: "middle" }); + d.text(sl, it[0], { x: x + 0.46, y: yy, w: w - 0.46, h: 0.38, fontSize: 12, bold: true, color: "text", valign: "middle" }); + d.text(sl, it[1], { x: x + 0.46, y: yy + 0.38, w: w - 0.46, h: 0.66, fontSize: 9.5, valign: "top" }); + }); + } + // モックの画面の枠。ブラウザのバー・アプリのヘッダ・左のメニューを描き、中身の領域を返す + function screen(sl, x, y, w, h, urlPath, active, opt) { + const o = opt || {}; + rr(sl, x, y, w, h, "panel1", "rule", 0.06); + // ブラウザのバー + d.box(sl, x + 0.02, y + 0.02, w - 0.04, 0.24, "panel2", false); + ["warn", "caution", "accent"].forEach((c, i) => dot(sl, x + 0.1 + i * 0.13, y + 0.08, 0.09, c)); + rr(sl, x + 0.55, y + 0.05, Math.min(3.2, w - 0.7), 0.16, "background", null, 0.08); + d.text(sl, "factory.example.com/" + urlPath, { x: x + 0.62, y: y + 0.05, w: 3.0, h: 0.16, fontSize: 6.5, color: "mute", valign: "middle" }); + // アプリのヘッダ + const hy = y + 0.3; + rr(sl, x + 0.12, hy + 0.04, 0.3, 0.2, "accent", null, 0.04); + d.text(sl, "NDF", { x: x + 0.12, y: hy + 0.04, w: 0.3, h: 0.2, fontSize: 6, bold: true, color: "background", align: "center", valign: "middle" }); + d.text(sl, "Neural Development Factory", { x: x + 0.48, y: hy + 0.04, w: 2.2, h: 0.2, fontSize: 7.5, bold: true, color: "text", valign: "middle" }); + if (!o.noInbox) pill(sl, x + w - 1.95, hy + 0.04, 0.82, 0.2, "対応待ち 3", "caution", "background", 6.5); + pill(sl, x + w - 1.05, hy + 0.04, 0.93, 0.2, "新しいスプリント", "accent", "background", 6.5); + d.box(sl, x + 0.02, hy + 0.3, w - 0.04, 0.01, "rule", false); + // 左のメニュー + const my = hy + 0.42; + ["スプリント", "ループ", "対応待ち", "スプリントを始める", "バックログ", "設定"].forEach((m, i) => { + const on = m === active; + if (on) rr(sl, x + 0.08, my + i * 0.26, 1.0, 0.22, "accentSoft", null, 0.04); + d.text(sl, m, { x: x + 0.16, y: my + i * 0.26, w: 0.9, h: 0.22, fontSize: 6.5, bold: on, color: on ? "accent" : "ink", valign: "middle" }); + }); + return { x: x + 1.2, y: hy + 0.42, w: w - 1.32, h: h - (hy - y) - 0.52 }; + } + // フェーズの 5 本の棒(done 本は濃く、cur 番目は薄く) + function phaseBar(sl, x, y, w, done, cur) { + cols(5, 0.03, x, w).forEach((c, i) => d.box(sl, c.x, y, c.w, 0.05, i < done ? "accent" : i === cur ? "accentSoft" : "rule", false)); + } + function tx(sl, t, x, y, w, h, size, opt) { + d.text(sl, t, Object.assign({ x, y, w, h, fontSize: size || 7, valign: "middle" }, opt || {})); + } + + // 1. タイトル + s = d.cover("AI が回すソフトウェア工場", "NDF から Neural Development Factory へ", "Software Factory / NDF / Neural Development Factory"); + dot(s, 6.9, 1.6, 3.6, "header", 78); + dot(s, 7.9, 0.9, 2.2, "accent", 80); + dot(s, 6.3, 3.6, 1.2, "warn", 82); + d.text(s, "SOFTWARE FACTORY × AI AGENTS", { x: 1.02, y: 0.86, w: 6, h: 0.3, fontSize: 11, bold: true, color: "accent", charSpacing: 3 }); + d.text(s, [{ text: REPO, options: { hyperlink: { url: REPO }, color: "accent" } }], { x: 1.02, y: 4.2, w: 6, h: 0.3, fontSize: 11 }); + + // ==== 章 1 ==== + sec("Software Factory という考え方", "人は「何を作るか」と「出してよいか」を決める"); + + // 2. Software Factory とは + s = pg("Software Factory とは", "仕様を入れると、テスト済みのソフトウェアが出てくる"); + rr(s, X0, 1.62, 1.7, 1.4, "panel2"); + d.text(s, [{ text: "INPUT\n", options: { fontSize: 9, bold: true, color: "accent", charSpacing: 2 } }, { text: "Issue・仕様", options: { fontSize: 14, bold: true, color: "text" } }], { x: X0, y: 1.62, w: 1.7, h: 1.4, align: "center", valign: "middle" }); + d.arrow(s, 2.0, 2.32, 0.34, 0, "accent"); + rr(s, 2.4, 1.36, 5.2, 1.92, "panel1", "accent"); + d.text(s, "工場(AI エージェントが動かすライン)", { x: 2.56, y: 1.44, w: 4.9, h: 0.3, fontSize: 10.5, bold: true, color: "accent", valign: "middle" }); + flow(s, 2.56, 1.92, 4.88, 0.7, ["設計", "実装", "検査", "リリース"], { size: 12 }); + d.text(s, "並列に何本でも", { x: 2.56, y: 2.78, w: 4.88, h: 0.3, fontSize: 9.5, color: "mute", align: "center", valign: "middle" }); + d.arrow(s, 7.66, 2.32, 0.34, 0, "accent"); + rr(s, 8.04, 1.62, 1.7, 1.4, "panel2"); + d.text(s, [{ text: "OUTPUT\n", options: { fontSize: 9, bold: true, color: "accent", charSpacing: 2 } }, { text: "テスト済みの\nソフトウェア", options: { fontSize: 13, bold: true, color: "text" } }], { x: 8.04, y: 1.62, w: 1.7, h: 1.4, align: "center", valign: "middle" }); + cols(2).forEach((c, i) => { + const r = [["人が決める 1", "何を作るか"], ["人が決める 2", "出してよいか"]][i]; + rr(s, c.x, 3.6, c.w, 1.0, "caution"); + d.text(s, r[0], { x: c.x + 0.2, y: 3.66, w: c.w - 0.4, h: 0.3, fontSize: 10, bold: true, color: "background", valign: "middle" }); + d.text(s, r[1], { x: c.x + 0.2, y: 3.96, w: c.w - 0.4, h: 0.54, fontSize: 20, bold: true, color: "background", valign: "middle" }); + }); + + // 3. 「工場」は新しい言葉ではない + s = pg("「工場」は新しい言葉ではない", "作業者が人から AI に替わり、50 年前の構想が現実に"); + d.box(s, 0.7, 2.6, 8.6, 0.03, "rule", false); + [ + { x: X0, year: "1970 年前後", head: "日本のソフトウェア工場", body: ["日立・東芝・NEC・富士通", "標準工程・部品の再利用・品質管理", "作業者は人"], fill: "panel1", acc: "header" }, + { x: 5.08, year: "2026 年", head: "AI エージェントの Software Factory", body: ["StrongDM: 人はコードを書かない・読まない", "Dan Shapiro: 5 段階の最上位は「Dark Factory」", "作業者は AI"], fill: "panel1", acc: "accent" }, + ].forEach((r) => { + dot(s, r.x + 2.18, 2.5, 0.24, r.acc); + d.text(s, r.year, { x: r.x, y: 1.4, w: 4.66, h: 0.5, fontSize: 22, bold: true, color: r.acc, align: "center", valign: "middle" }); + d.text(s, r.head, { x: r.x, y: 1.92, w: 4.66, h: 0.4, fontSize: 13, bold: true, color: "text", align: "center", valign: "middle" }); + card(s, r.x, 2.9, 4.66, 1.7, "", bullets(r.body, 12.5), r.fill, { headH: 0.02 }); + }); + d.note(s, [ + { text: "出典: " }, + { text: "Cusumano『Japan’s Software Factories』(1991)", options: { hyperlink: { url: "https://academic.oup.com/book/52424" }, color: "accent" } }, + { text: " / " }, + { text: "StrongDM “The StrongDM Software Factory”(2026-02)", options: { hyperlink: { url: "https://www.strongdm.com/blog/the-strongdm-software-factory-building-software-with-ai" }, color: "accent" } }, + { text: " / " }, + { text: "Dan Shapiro “The Five Levels”(2026-01)", options: { hyperlink: { url: "https://www.danshapiro.com/blog/2026/01/the-five-levels-from-spicy-autocomplete-to-the-software-factory/" }, color: "accent" } }, + ]); + + // 4. 工場に必要な 4 つの部品 + s = pg("工場に必要な 4 つの部品", "NDF は、この 4 つを Skill・スクリプト・hook にした"); + [ + ["01", "標準工程", "どの順に何をするか", "ライン"], + ["02", "品質ゲート", "不良を次へ流さない", "検査"], + ["03", "計測", "どこまで進んだか見える", "見える化"], + ["04", "改善", "詰まりを工程へ戻す", "カイゼン"], + ].forEach((r, i) => { + const c = cols(4)[i]; + rr(s, c.x, 1.5, c.w, 2.8, "panel1"); + d.text(s, r[0], { x: c.x + 0.2, y: 1.62, w: c.w - 0.4, h: 0.6, fontSize: 30, bold: true, color: i % 2 ? "header" : "accent", valign: "middle" }); + d.text(s, r[1], { x: c.x + 0.2, y: 2.3, w: c.w - 0.4, h: 0.5, fontSize: 18, bold: true, color: "text", valign: "middle" }); + d.text(s, r[2], { x: c.x + 0.2, y: 2.84, w: c.w - 0.4, h: 0.7, fontSize: 12, valign: "top" }); + pill(s, c.x + 0.2, 3.72, 1.2, 0.3, r[3], "panel2", "accent", 9.5); + }); + + // ==== 章 2 ==== + sec("NDF — 工場のラインをプラグインに", "Claude Code・Codex で動く開発ワークフロー"); + + // 5. 指示書だけでは、ラインは回らない + s = pg("指示書だけでは、ラインは回らない", "CLAUDE.md への追記は、すぐに 4 つの壁に当たる"); + [ + ["コンテキスト", "毎回全部読む。書くほど埋もれる"], + ["強制力", "書いても守られない"], + ["手順", "順序は文章で伝わらない"], + ["ポータビリティ", "環境を変えると消える"], + ].forEach((r, i) => { + const c = cols(2)[i % 2]; + card(s, c.x, 1.45 + Math.floor(i / 2) * 1.6, c.w, 1.4, r[0], r[1], "panel1", { band: "warn", headSize: 18, bodySize: 15, headH: 0.5 }); + }); + + // 6. 実務はスクリプト、AI は規定と監視(2026-10-31 の 8 の数値と URL をここへ移した) + s = pg("実務はスクリプト、AI は規定と監視", "Skill が手順を決めて結果を見張り、スクリプトが毎回同じに実行する"); + rr(s, X0, 1.4, 5.5, 1.5, "accentSoft", "accent"); + d.text(s, [ + { text: "スクリプト", options: { fontSize: 20, bold: true, color: "accent" } }, + { text: " = 実務を担う", options: { fontSize: 14, bold: true, color: "text" } }, + ], { x: X0 + 0.2, y: 1.48, w: 5.1, h: 0.46, valign: "middle" }); + d.text(s, bullets(["実行・待ち・マージ・リリース", "決まった処理を、毎回同じ結果で", "結果は JSON で返す"], 12.5, "text"), { x: X0 + 0.2, y: 1.98, w: 5.1, h: 0.86, valign: "top", paraSpaceAfter: 2 }); + rr(s, 6.0, 1.4, 3.74, 0.94, "panel1", "header"); + d.text(s, "Skill(AI エージェント)", { x: 6.18, y: 1.44, w: 3.4, h: 0.34, fontSize: 13, bold: true, color: "header", valign: "middle" }); + d.text(s, bullets(["規定: いつ・何を・どの順で", "監視: 結果を見て、異常なら止める"], 11), { x: 6.18, y: 1.8, w: 3.45, h: 0.5, valign: "top", paraSpaceAfter: 1 }); + rr(s, 6.0, 2.44, 3.74, 0.46, "panel1"); + d.text(s, [ + { text: "hook", options: { fontSize: 13, bold: true, color: "caution" } }, + { text: " ルールを強制する", options: { fontSize: 11, color: "ink" } }, + ], { x: 6.18, y: 2.44, w: 3.45, h: 0.46, valign: "middle" }); + // 数値と URL(GitHub で公開。NDF 自身も NDF のラインで開発) + [ + ["196", "本", "スクリプト", "accent"], + ["49", "個", "Skill", "header"], + ["168", "回", "リリース(2026-04〜11)", "accent"], + ["10+", "", "自分以外の導入リポジトリ", "header"], + ].forEach((r, i) => { + const c = cols(5, 0.12)[i]; + rr(s, c.x, 3.04, c.w, 0.74, "panel1"); + d.text(s, [ + { text: r[0], options: { fontSize: 22, bold: true, color: r[3] } }, + { text: r[1] ? " " + r[1] : "", options: { fontSize: 11, bold: true, color: "text" } }, + ], { x: c.x + 0.12, y: 3.06, w: c.w - 0.2, h: 0.42, valign: "middle" }); + d.text(s, r[2], { x: c.x + 0.12, y: 3.48, w: c.w - 0.16, h: 0.26, fontSize: 9, valign: "middle" }); + }); + const uc = cols(5, 0.12)[4]; + rr(s, uc.x, 3.04, uc.w, 0.74, "panel2", "accent"); + d.text(s, [ + { text: "GitHub で公開\n", options: { fontSize: 9, bold: true, color: "text" } }, + { text: "github.com/\ndevbasex/ai-plugins", options: { fontSize: 10, bold: true, hyperlink: { url: REPO }, color: "accent" } }, + ], { x: uc.x + 0.12, y: 3.04, w: uc.w - 0.16, h: 0.74, valign: "middle" }); + rr(s, X0, 3.92, W, 0.62, "panel2"); + d.text(s, [ + { text: "プラグイン", options: { fontSize: 14, bold: true, color: "text" } }, + { text: " まとめて配る", options: { fontSize: 11.5, color: "ink" } }, + ], { x: X0 + 0.2, y: 3.92, w: 2.9, h: 0.62, valign: "middle" }); + // 配る先は 2 つ。Kiro・agy には配らず、クロスレビュー・リファクタの相手役として起動するだけ + ["Claude Code", "Codex"].forEach((r, i) => { + pill(s, 3.1 + i * 1.5, 4.05, 1.38, 0.36, r, i % 2 ? "header" : "accent", "background", 11); + }); + d.text(s, "相手役", { x: 6.2, y: 4.05, w: 0.8, h: 0.36, fontSize: 10, bold: true, color: "mute", align: "right", valign: "middle" }); + ["Kiro", "agy"].forEach((r, i) => { + pill(s, 7.1 + i * 1.32, 4.05, 1.2, 0.36, r, "panel1", "ink", 11); + }); + d.note(s, [ + { text: "2026-11-03 時点。スクリプトは plugins/ndf の scripts 直下と skills の .py・.sh(テストを除く)、リリースは正式版の版数の数、Skill は Claude Code 版のマニフェストの行数、導入先は 2026-10-17 時点。公式: " }, + { text: "Claude Code プラグイン", options: { hyperlink: { url: "https://code.claude.com/docs/ja/plugins" }, color: "accent" } }, + { text: " / " }, + { text: "Codex Skills", options: { hyperlink: { url: "https://developers.openai.com/codex/skills" }, color: "accent" } }, + ]); + + // 7. Issue をまとめて渡せば、並列に進む + s = pg("Issue をまとめて渡せば、並列に進む", "人が判断するのは 2 回の承認だけ"); + rr(s, X0, 1.62, 1.55, 0.9, "panel2"); + d.text(s, "マイルストーン\n(複数の Issue)", { x: X0, y: 1.62, w: 1.55, h: 0.9, fontSize: 10, bold: true, color: "text", align: "center", valign: "middle" }); + d.arrow(s, 1.85, 2.07, 0.3, 0, "accent"); + rr(s, 2.19, 1.62, 1.55, 0.9, "panel2"); + d.text(s, "触る場所と依存で\nグループに分ける", { x: 2.19, y: 1.62, w: 1.55, h: 0.9, fontSize: 10, bold: true, color: "text", align: "center", valign: "middle" }); + d.arrow(s, 3.78, 2.07, 0.3, 0, "accent"); + rr(s, 4.12, 1.36, 4.0, 1.98, "panel1", "accent"); + d.text(s, "スプリント(グループごとに並列)", { x: 4.24, y: 1.4, w: 3.8, h: 0.28, fontSize: 10, bold: true, color: "accent", valign: "middle" }); + ["グループ A", "グループ B", "グループ C"].forEach((g, i) => { + const y = 1.74 + i * 0.52; + d.text(s, g, { x: 4.24, y, w: 0.9, h: 0.4, fontSize: 9, valign: "middle" }); + flow(s, 5.12, y, 2.9, 0.4, ["設計", "実装", "レビュー"], { gap: 0.18, size: 9 }); + }); + d.arrow(s, 8.16, 2.35, 0.3, 0, "accent"); + rr(s, 8.5, 1.9, 1.24, 0.9, "panel2"); + d.text(s, "リリース", { x: 8.5, y: 1.9, w: 1.24, h: 0.9, fontSize: 11, bold: true, color: "text", align: "center", valign: "middle" }); + cols(2).forEach((c, i) => { + const g = [["承認 1: 設計", "設計をまとめて 1 回"], ["承認 2: 本番リリース", "開発版で確かめて 1 回"]][i]; + card(s, c.x, 3.56, c.w, 0.9, g[0], g[1], "caution", { headSize: 12.5, bodySize: 11.5, headH: 0.3, headColor: "background", bodyColor: "background" }); + }); + d.note(s, "判断の材料(決めたこと・未検証の点・根拠)はそろって届く"); + + // 8. 機能追加は、この流れで 1 周 + s = pg("機能追加は、この流れで 1 周", "/ndf:development-workflow — 変更を 5 つのモードに分け、軽い変更は工程を省く"); + [ + ["設計", [{ hi: "要件定義" }, "作業用の\nworktree", "設計", { hi: "構成の\n見直し" }, { hi: "クロス\nレビュー" }, { text: "承認 1\n設計" }]], + ["実装", ["実装計画", { hi: "テスト\nファースト" }, { hi: "クロス\nリファクタ" }, { hi: "クロス\nレビュー" }, { hi: "完了判定\n(証跡)" }, "マージ"]], + ["リリース", ["仕様書\n確定", "開発版\nリリース", { text: "承認 2\n本番" }, "本番\nリリース", "リリース後\nテスト", { hi: "振り返り" }, { hi: "Issue の\n棚卸し" }]], + ].forEach((row, i) => { + const y = 1.4 + i * 0.98; + d.text(s, row[0], { x: X0, y, w: 0.8, h: 0.7, fontSize: 11, bold: true, color: "accent", valign: "middle" }); + const n = row[1].length; + const g = 0.2; + flow(s, 1.06, y, (8.68 * n + g * (n - 7)) / 7, 0.7, row[1], { gap: g, size: 9.5 }); + if (i < 2) d.arrow(s, X0 + 0.3, y + 0.72, 0, 0.24, "accent"); + }); + [["light", "小さな変更"], ["operation", "外部の作業"], ["standard", "機能追加"], ["legacy-refactor", "整理"], ["documentation", "文書"]].forEach((m, i) => { + const c = cols(5, 0.1)[i]; + const on = i === 2; + rr(s, c.x, 4.3, c.w, 0.4, on ? "accentSoft" : "panel1", on ? "accent" : null, 0.06); + d.text(s, [{ text: m[0] + " ", options: { bold: true, color: on ? "accent" : "text", fontSize: 9.5 } }, { text: m[1], options: { fontSize: 9, color: "ink" } }], { x: c.x + 0.08, y: 4.3, w: c.w - 0.12, h: 0.4, align: "center", valign: "middle" }); + }); + d.note(s, "淡いシアンの工程は、次のページ以降で紹介する。レビューの指摘は直して繰り返す。開発版は自分で使って確かめてから承認 2 へ"); + + // 9. 要件定義・テストファースト: 実績のある技法を組み込む + s = pg("要件定義・テストファースト: 実績のある技法を組み込む", "新しい発明ではない。AI が毎回守れる手順にした"); + [ + ["DDD", "ユビキタス言語とドメインモデル", ["要件定義の前に用語集", "1 語 1 意味を CI が検査", "設計はドメインモデルから", "モデルを先にレビュー"]], + ["TDD", "テストファースト", ["失敗するテストから", "- → 実装 → 整理", "薄いコードは先にテスト"]], + ["Agile", "スプリントと振り返り", ["マイルストーン = バックログ", "スプリントごとに振り返り", "優先順位は WSJF"]], + ].forEach((r, i) => { + const c = cols(3)[i]; + rr(s, c.x, 1.45, c.w, 3.1, "panel1"); + d.text(s, r[0], { x: c.x + 0.2, y: 1.55, w: c.w - 0.4, h: 0.6, fontSize: 26, bold: true, color: i === 1 ? "header" : "accent", valign: "middle" }); + d.text(s, r[1], { x: c.x + 0.2, y: 2.15, w: c.w - 0.4, h: 0.4, fontSize: 12, bold: true, color: "text", valign: "middle" }); + d.text(s, bullets(r[2], 12.5), { x: c.x + 0.2, y: 2.7, w: c.w - 0.4, h: 1.7, valign: "top", paraSpaceAfter: 4 }); + }); + + // 10. 構成の見直し: コードの可読性から、日本語の可読性の時代へ + s = pg("構成の見直し: コードの可読性から、日本語の可読性の時代へ", "/ndf:document-rebuild — 「読みやすさの研究」の成果を取り入れて文章を組み直す", 19.5); + d.box(s, 0.6, 1.98, 8.8, 0.03, "rule", false); + [ + ["19 世紀", "逆ピラミッド", "新聞。大事なことから", "https://helpcenter.veeam.com/docs/styleguide/tw/inverted_pyramid.html"], + ["20 世紀前半〜", "IMRAD", "論文の決まった構成", "https://en.wikipedia.org/wiki/IMRAD"], + ["1960 年代", "Information Mapping", "情報の型で見せ方を", "https://en.wikipedia.org/wiki/Structured_writing"], + ["米陸軍の規程", "BLUF", "結論を最初の一文に", "https://en.wikipedia.org/wiki/BLUF_(communication)"], + ["1990", "ミニマリズム", "書きすぎない", "https://en.wikipedia.org/wiki/Minimalism_(technical_communication)"], + ["日本語", "日本語スタイルガイド", "可読性の式も", "https://jtca.org/learn-tc/publication/guide_jsg/"], + ].forEach((r, i) => { + const c = cols(6, 0.08)[i]; + const acc = i === 5 ? "caution" : i % 2 ? "header" : "accent"; + d.text(s, r[0], { x: c.x, y: 1.4, w: c.w, h: 0.4, fontSize: 11.5, bold: true, color: acc, align: "center", valign: "middle" }); + dot(s, c.x + c.w / 2 - 0.1, 1.895, 0.2, acc); + d.text(s, [{ text: r[1], options: { hyperlink: { url: r[3] }, color: "text" } }], { x: c.x - 0.04, y: 2.14, w: c.w + 0.08, h: 0.5, fontSize: 10.5, bold: true, align: "center", valign: "middle" }); + d.text(s, r[2], { x: c.x, y: 2.62, w: c.w, h: 0.3, fontSize: 9.5, align: "center", valign: "middle" }); + }); + d.tbl(s, [ + [d.H("組み直した前後(実測)"), d.H("文書 A"), d.H("文書 B"), d.H("文書 C")], + [d.B("結論の位置", { color: "text" }), "158 → 1 行目", "23 → 1 行目", "14 → 5 行目"], + [d.B("平均の文の長さ", { color: "text" }), "46 → 33 字", "40 → 30 字", "88 → 31 字"], + [d.B("一番長い文", { color: "text" }), "186 → 105 字", "95 → 75 字", "131 → 73 字"], + ], { x: X0, y: 3.1, w: 6.1, colW: [2.2, 1.3, 1.3, 1.3], rowH: [0.36, 0.38, 0.38, 0.38], fontSize: 10.5, align: "center" }); + rr(s, 6.54, 3.1, 3.2, 1.5, "panel1", "accent"); + d.text(s, "document-rebuild の手順", { x: 6.7, y: 3.16, w: 2.9, h: 0.32, fontSize: 11.5, bold: true, color: "accent", valign: "middle" }); + d.text(s, [ + { text: "1. 測る", options: { breakLine: true } }, + { text: "2. 読み手が問う順へ並べ替える", options: { breakLine: true } }, + { text: "3. 文を整え、別の AI に読ませる", options: { breakLine: true } }, + { text: "4. 測り直して前後を並べる" }, + ], { x: 6.7, y: 3.5, w: 2.96, h: 1.04, fontSize: 10.5, valign: "top", paraSpaceAfter: 2 }); + d.note(s, [ + { text: "実測は #391 の 3 本(#389・#390・#339)。内容は足さず、並びと文だけを変えた。根拠の一覧は #391 の「参考」(ほかに " }, + { text: "jReadability", options: { hyperlink: { url: "https://jreadability.net/sys/" }, color: "accent" } }, + { text: "・Carroll『The Nurnberg Funnel』)" }, + ]); + + // 11. クロスレビュー・リファクタ: 複数の AI で見て直す + s = pg("クロスレビュー・リファクタ: 複数の AI で見て直す", "見落としは別の AI が拾い、直すのは時間内に"); + [ + ["/ndf:cross-review — 指摘が尽きるまで", ["2 つの AI が\nレビュー", "指摘を修正", "組み替えて\n繰り返す"], "新しい指摘が出なくなったら終了", "観点は PR の中身で切り替え"], + ["/ndf:cross-refactoring — 時間内に直す", ["全員の AI が\n提案", "1 つが既定\n30 分で実施", "壊れたら\n取り消す"], "数値は持ち時間から逆算", "採用と取り消しを報告"], + ].forEach((r, i) => { + const c = cols(2, 0.3)[i]; + rr(s, c.x, 1.42, c.w, 3.1, "panel1"); + d.box(s, c.x + 0.14, 1.42, 0.5, 0.05, i ? "header" : "accent", false); + d.text(s, r[0], { x: c.x + 0.16, y: 1.54, w: c.w - 0.32, h: 0.36, fontSize: 13, bold: true, color: "text", valign: "middle" }); + flow(s, c.x + 0.16, 2.04, c.w - 0.32, 0.96, r[1], { gap: 0.2, size: 12 }); + d.text(s, bullets([r[2], r[3]], 13.5), { x: c.x + 0.16, y: 3.2, w: c.w - 0.32, h: 1.1, valign: "middle", paraSpaceAfter: 8 }); + }); + + // 12. 完了判定: 「完了」も「無い」も証拠で + s = pg("完了判定: 「完了」も「無い」も証拠で", "工場の検査工程。AI の言葉を、そのままは信じない"); + [ + ["「完了しました」", "/ndf:quality-gates — 完了の判定", "4 点がそろって完了\nコマンド・終了コード・時刻・範囲"], + ["「該当のコードはありません」", "/ndf:investigation-rules — 調査の規則", "検索のコマンドと結果を添える\nAI は「無い」と断定しがち"], + ].forEach((r, i) => { + const c = cols(2)[i]; + rr(s, c.x, 1.48, c.w, 0.72, "panel2"); + d.text(s, r[0], { x: c.x, y: 1.48, w: c.w, h: 0.72, fontSize: 15, bold: true, color: "warn", align: "center", valign: "middle" }); + d.arrow(s, c.x + c.w / 2, 2.26, 0, 0.3, "accent"); + card(s, c.x, 2.62, c.w, 1.6, r[1], r[2], "panel1", { band: "accent", headSize: 12.5, bodySize: 13 }); + }); + + // 13. 3 層のエージェントで止まらない進行 + s = pg("3 層のエージェントで止まらない進行", "長い作業も、止まったら続きから"); + [ + ["conductor", "指揮役。人と話し全体を進める", "accent"], + ["supervisor", "監督役。1 つのフェーズを通す", "header"], + ["worker", "作業役。調査や修正を行う", "panel2"], + ].forEach((r, i) => { + const y = 1.46 + i * 1.02; + rr(s, X0 + i * 0.3, y, 1.75, 0.74, r[2]); + d.text(s, r[0], { x: X0 + i * 0.3, y, w: 1.75, h: 0.74, fontSize: 13, bold: true, color: i === 0 ? "background" : "text", align: "center", valign: "middle" }); + d.text(s, r[1], { x: 2.16 + i * 0.3, y, w: 2.95, h: 0.74, fontSize: 12, valign: "middle" }); + if (i < 2) d.arrow(s, 1.1 + i * 0.3, y + 0.76, 0.2, 0.24, "accent"); + }); + card(s, 5.7, 1.46, 4.04, 1.36, "止まっても続きから", "JSON の計画をスクリプトが実行。\n進行は DBOS(耐久ワークフロー)が記録", "panel1", { band: "accent", headSize: 14, bodySize: 12.5 }); + card(s, 5.7, 3.0, 4.04, 1.48, "セッションを自動で切り替え", "次のコマンドを残し、NDF Relay が起動し直す", "panel1", { band: "header", headSize: 14, bodySize: 12.5 }); + d.note(s, [ + { text: "DBOS: " }, + { text: "dbos.dev", options: { hyperlink: { url: "https://www.dbos.dev" }, color: "accent" } }, + { text: "(Python 版 " }, + { text: "dbos-transact-py", options: { hyperlink: { url: "https://github.com/dbos-inc/dbos-transact-py" }, color: "accent" } }, + { text: ")。落ちた後に同じコマンドを打ち直すと、流れていたステップから続く" }, + ]); + + // 14. NDF Relay: コンテキストを溜めずに走り続ける + s = pg("NDF Relay: コンテキストを溜めずに走り続ける", "会話が膨らむと判断が鈍る。区切りで新しいセッションへ移す(relay.py)"); + rr(s, X0, 1.36, 5.9, 1.5, "panel1", "warn"); + d.text(s, "会話が長くなると", { x: X0 + 0.2, y: 1.42, w: 5.5, h: 0.32, fontSize: 12.5, bold: true, color: "warn", valign: "middle" }); + d.text(s, bullets(["判断の質が落ちる", "応答のたびに会話を丸ごと読み直し、費用が増える", "自動の要約は情報を落とし、落ちたことに気づけない"], 11.5), { x: X0 + 0.2, y: 1.76, w: 5.55, h: 1.04, valign: "top", paraSpaceAfter: 3 }); + rr(s, 6.32, 1.36, 3.42, 1.5, "panel1"); + d.text(s, [ + { text: "410k", options: { fontSize: 32, bold: true, color: "warn" } }, + { text: " Token", options: { fontSize: 12.5, bold: true, color: "text" } }, + ], { x: 6.48, y: 1.42, w: 3.1, h: 0.66, valign: "middle" }); + d.text(s, "conductor の会話の平均。200k を超えた会話は 45 件中 37 件", { x: 6.48, y: 2.1, w: 3.14, h: 0.7, fontSize: 10.5, valign: "top" }); + flow(s, X0, 3.04, W, 0.72, ["工程の区切りで\n続けるか切るか決める", "引継ぎ文書と\n次のコマンドを残す", "NDF Relay が\n/exit・更新・起動", "新しいセッションで\n続きから"], { size: 11.5, gap: 0.3 }); + [ + ["承認は代わりに答えない", "caution"], + ["利用上限は別のアカウントで続ける", "panel2"], + ["1 日 20 回まで。空回りで止まる", "panel2"], + ].forEach((r, i) => { + const c = cols(3)[i]; + pill(s, c.x, 3.96, c.w, 0.5, r[0], r[1], r[1] === "caution" ? "background" : "text", 11.5); + }); + d.note(s, "切る基準は、決まった上限から、続けた場合と切り替えた場合の費用の比べ合いへ移す(既定はまだ 200k で切る。費用で決める方式は試行中)。410k は NDF 自身の開発の 30 日間の実測(#827)"); + + // 15. 迷ったら MVV で判断する + s = pg("迷ったら MVV で判断する", "判断の根拠の番号をログに残す"); + [["Mission", "何のためにやるか"], ["Vision", "目指す姿"], ["Value", "判断の基準"]].forEach((r, i) => { + const c = cols(3, 0.16, X0, 5.9)[i]; + rr(s, c.x, 1.5, c.w, 1.36, "panel1"); + d.text(s, r[0], { x: c.x, y: 1.6, w: c.w, h: 0.56, fontSize: 18, bold: true, color: i === 1 ? "header" : "accent", align: "center", valign: "middle" }); + d.text(s, r[1], { x: c.x, y: 2.16, w: c.w, h: 0.5, fontSize: 12, align: "center", valign: "middle" }); + }); + card(s, 6.4, 1.5, 3.34, 1.36, "必ず人が承認する操作", "本番デプロイなど。AI は省かない", "caution", { headSize: 13, bodySize: 12, headColor: "background", bodyColor: "background" }); + rr(s, X0, 3.1, W, 0.62, "panel2"); + d.text(s, "Value の例:「まず届ける」「処理の改善に寄与しない指摘は直さない」", { x: X0 + 0.2, y: 3.1, w: W - 0.4, h: 0.62, fontSize: 13, color: "text", valign: "middle" }); + d.text(s, "AI に任せる範囲は、事前に設定で選ぶ", { x: X0, y: 3.95, w: W, h: 0.4, fontSize: 12, bold: true, color: "accent", align: "center", valign: "middle" }); + + // 16. 振り返り・棚卸し: 詰まりを工程へ戻して直す + s = pg("振り返り・棚卸し: 詰まりを工程へ戻して直す", "工場の改善。Issue の一覧を、いつも現状に"); + [ + ["/ndf:out-of-scope — 見つけたら起票", "範囲外の問題を即 Issue に"], + ["/ndf:retrospective — 振り返り", "リリースごとに進め方を見直す"], + ["/ndf:backlog-refinement — 一覧の棚卸", "スプリント終了後にバックログを現状に合わせて再整備"], + ].forEach((r, i) => { + const y = 1.42 + i * 1.08; + card(s, X0, y, 4.9, 0.92, r[0], r[1], "panel1", { headSize: 12, bodySize: 11, headH: 0.28, band: i === 1 ? "header" : "accent" }); + if (i < 2) d.arrow(s, 2.7, y + 0.93, 0, 0.14, "accent"); + }); + stat(s, 5.4, 1.42, 4.34, 1.44, "56", "%", "本文が現状とずれた open の Issue(43 件中 24 件)", "warn"); + stat(s, 5.4, 3.0, 4.34, 1.5, "49", "件", "本文が古かった Issue(71 件を調査)", "caution"); + d.note(s, "56% はある時点の棚卸し、49 件は 2026-10-17 の調査。放っておくと、直った Issue を直すことになる"); + + // 17. 開発ループ: 終わったら次のスプリントを自分で選ぶ + s = pg("開発ループ: 終わったら次のスプリントを自分で選ぶ", "/ndf:development-workflow --loop — 棚卸しの順位から選び、予算の内で周回を続ける"); + flow(s, X0, 1.45, W, 0.8, ["棚卸しの順位\nから選ぶ", "スプリント\n設計〜本番", "まとめ\n振り返り・棚卸し", "周回を締める", "次のセッションで\n次の周回"], { size: 11.5, gap: 0.3 }); + d.text(s, "1 周ごとに NDF Relay がセッションを切り替え、最初の「選ぶ」へ戻る", { x: X0, y: 2.3, w: W, h: 0.3, fontSize: 10.5, color: "mute", align: "center", valign: "middle" }); + card(s, X0, 2.75, 4.66, 1.8, "人に戻るのは 2 か所だけ", bullets(["設計の MVV 判定が「従う」でない課題は、判断待ちへ外して残りで続ける", "本番の承認(ゲート 2)は人が答える"], 12), "caution", { headSize: 14, headColor: "background", bodyColor: "background", headH: 0.36 }); + card(s, 5.08, 2.75, 4.66, 1.8, "止まる条件", bullets(["予算: 周回の数・費用・時間", "同じ失敗が続いた・MVV が変わった", "選べる Issue が尽きた(1 回見直してから)"], 12), "panel1", { band: "accent", headSize: 14, headH: 0.36 }); + d.note(s, "NDF 自身の開発では 5 本のループが計 17 周した(手元の .ndf/loop の記録、2026-11-03 時点)。手順は development-workflow の references/loop.md"); + + // 18. 事例: ほかのプロジェクトでも動く + s = pg("事例: ほかのプロジェクトでも動く", "言語もチームも違うプロジェクト。詰まりは次の版で直る"); + d.tbl(s, [ + [d.H("プロジェクトの種類"), d.H("結果")], + [d.B("AWS のインフラ", { color: "text" }), "マージ PR 0.95 → 3.38 件/日(社内ツールの移行と重なる)"], + [d.B("PHP の業務システム(20 人)", { color: "text" }), "設計の承認から 51 時間で実装 PR 31 本"], + [d.B("機械学習のモデル", { color: "text" }), "データ欠損を発見し 1 スプリントで本番へ。相関係数 0.73 → 0.954"], + [d.B("価格推定", { color: "text" }), "PR 数は同じ。レビュー必須の開発へ"], + ], { x: X0, y: 1.42, w: W, colW: [2.9, 6.58], rowH: [0.38, 0.62, 0.62, 0.62, 0.62], fontSize: 12.5 }); + d.note(s, "インフラは導入前後 37 日の比較。PHP では最初に 17 件の Issue が上がり、次の版で直った。数値は 2026-10-17 時点の実測"); + + // ==== 中締め(章番号を持たない扉)==== + // ここまでは実装して実際に使っている NDF、ここからは構想の Neural Development Factory + s = d.section("ここまでは実践、ここからは構想", null); + d.text(s, "中締め", { x: 0.9, y: 1.3, w: 3, h: 0.45, fontSize: 18, bold: true, color: "accent", valign: "middle" }); + [ + ["ここまで", "COMBAT PROVEN", "NDF は実装済み。\n実際のプロジェクトで稼働中", "accent", "panel1"], + ["ここから", "VISION", "Neural Development Factory は\n作っている途中の構想。画面はモック", "header", "panel2"], + ].forEach((r, i) => { + const x = 0.9 + i * 4.45; + rr(s, x, 3.1, 3.95, 1.75, r[4], r[3]); + d.text(s, r[0], { x: x + 0.22, y: 3.22, w: 1.4, h: 0.4, fontSize: 14, bold: true, color: "text", valign: "middle" }); + pill(s, x + 3.95 - 0.22 - 1.6, 3.27, 1.6, 0.3, r[1], r[3], "background", 10); + d.text(s, r[2], { x: x + 0.22, y: 3.7, w: 3.5, h: 1.0, fontSize: 13, bold: true, color: "ink", valign: "middle" }); + if (i === 0) d.arrow(s, x + 3.99, 3.97, 0.42, 0, "accent"); + }); + + // ==== 章 3 ==== + sec("Neural Development Factory", "工場を Web へ。CLI の前にいなくても回る"); + + // 19. CLI の工場には、人が張り付く + s = pg("CLI の工場には、人が張り付く", "ラインは自動なのに、人がラインの横から離れられない"); + d.text(s, "いま(CLI の NDF)", { x: X0, y: 1.4, w: 4.3, h: 0.34, fontSize: 12, bold: true, color: "warn", valign: "middle" }); + d.text(s, "Neural Development Factory", { x: 5.44, y: 1.4, w: 4.3, h: 0.34, fontSize: 12, bold: true, color: "accent", valign: "middle" }); + [ + ["承認のたびに、CLI の前へ", "通知が届き、Web・Slack から承認"], + ["進捗が Issue・ボード・ログに散らばる", "一覧と詳細の 1 か所で見える"], + ["並べたループを一望できない", "スプリントとループを並べて見張る"], + ].forEach((r, i) => { + const y = 1.84 + i * 0.95; + rr(s, X0, y, 4.3, 0.76, "panel1", "warn"); + d.text(s, r[0], { x: X0 + 0.2, y, w: 3.9, h: 0.76, fontSize: 13, bold: true, color: "text", valign: "middle" }); + d.arrow(s, 4.64, y + 0.38, 0.72, 0, "accent"); + rr(s, 5.44, y, 4.3, 0.76, "accentSoft", "accent"); + d.text(s, r[1], { x: 5.64, y, w: 3.9, h: 0.76, fontSize: 13, bold: true, color: "text", valign: "middle" }); + }); + d.note(s, "セッションの切り替えは NDF Relay で自動になった。残るのは、人が判断する場面"); + + // 20. Neural Development Factory の構想 + s = pg("Neural Development Factory の構想", "NDF を Web から操作できる Software Factory のサービスに"); + [ + ["01", "登録する", "GitHub の\nリポジトリを取り込む"], + ["02", "始める", "スプリントか\nループを開始"], + ["03", "見る", "一覧と詳細で\nフェーズと工程を確認"], + ["04", "承認する", "通知を受けて\nWeb・Slack から"], + ].forEach((r, i) => { + const c = cols(4, 0.3)[i]; + rr(s, c.x, 1.45, c.w, 2.1, "panel1", i === 3 ? "caution" : null); + d.text(s, r[0], { x: c.x + 0.18, y: 1.55, w: c.w - 0.36, h: 0.5, fontSize: 22, bold: true, color: i === 3 ? "caution" : "accent", valign: "middle" }); + d.text(s, r[1], { x: c.x + 0.18, y: 2.08, w: c.w - 0.36, h: 0.46, fontSize: 17, bold: true, color: "text", valign: "middle" }); + d.text(s, r[2], { x: c.x + 0.18, y: 2.6, w: c.w - 0.36, h: 0.8, fontSize: 11, valign: "top" }); + if (i < 3) d.arrow(s, c.x + c.w + 0.03, 2.5, 0.24, 0, "accent"); + }); + cols(3).forEach((c, i) => pill(s, c.x, 3.9, c.w, 0.5, ["承認ゲートは 2 つのまま", "プラグインの NDF は OSS のまま", "並べても配布は 1 本"][i], "panel2", i === 0 ? "caution" : "accent", 12)); + + // 21. 仕組み: 承認を待つ間も、ラインは次の周回へ進む(オーケストレーターは #2347 の定義 6・#2439) + s = pg("仕組み: 承認を待つ間も、ラインは次の周回へ進む", "進捗は 1 本の事象の列。オーケストレーターは常駐せず、呼ばれたときと見回りで動く"); + // 上段: 画面 → API → オーケストレーター → 実行ジョブ → GitHub + const bx = (x, w, t) => { rr(s, x, 1.4, w, 0.86, "panel2"); d.text(s, t, { x, y: 1.4, w, h: 0.86, fontSize: 11.5, bold: true, color: "text", align: "center", valign: "middle" }); }; + bx(X0, 1.35, "画面\nWeb・Slack"); + d.arrow(s, X0 + 1.37, 1.83, 0.26, 0, "accent"); + bx(1.91, 1.35, "API\n認証・アカウント"); + d.arrow(s, 3.28, 1.83, 0.26, 0, "accent"); + rr(s, 3.56, 1.32, 2.1, 1.7, "accentSoft", "accent"); + d.text(s, "オーケストレーター", { x: 3.68, y: 1.36, w: 1.9, h: 0.3, fontSize: 11, bold: true, color: "accent", valign: "middle" }); + pill(s, 3.68, 1.68, 1.0, 0.22, "常駐しない", "accent", "background", 8); + d.text(s, bullets(["開始・停止・見回り", "ループの周回を起こす", "配布をまとめるオーガナイザー"], 9), { x: 3.68, y: 1.96, w: 1.92, h: 1.02, valign: "top", paraSpaceAfter: 2 }); + d.arrow(s, 5.68, 1.83, 0.26, 0, "accent"); + rr(s, 5.96, 1.32, 2.38, 1.7, "panel1", "accent"); + d.text(s, "実行ジョブ(コンテナ 1 つ)", { x: 6.08, y: 1.36, w: 2.2, h: 0.3, fontSize: 10.5, bold: true, color: "accent", valign: "middle" }); + d.text(s, bullets(["conductor → supervisor → worker", "区間はランナーがつなぐ", "版はスプリントの開始で固定"], 9), { x: 6.08, y: 1.7, w: 2.2, h: 1.28, valign: "top", paraSpaceAfter: 2 }); + d.arrow(s, 8.36, 1.83, 0.26, 0, "accent"); + bx(8.64, 1.1, "GitHub\nPR・CI"); + // 中段: 進捗の受け口(#2171)→ API が事象から作るもの。見回り(オーケストレーター)も受け口の記録を読む + d.arrow(s, 7.15, 3.06, 0, 0.28, "accent"); + rr(s, 4.9, 3.38, 3.44, 0.62, "header"); + d.text(s, [{ text: "進捗の受け口\n", options: { fontSize: 11.5, bold: true } }, { text: "ランナー・GitHub・承認の事象を追記だけで残す", options: { fontSize: 8.5 } }], { x: 5.0, y: 3.38, w: 3.24, h: 0.62, color: "onHeader", align: "center", valign: "middle" }); + d.arrow(s, 5.3, 3.36, 0, -0.3, "accent"); + d.text(s, "見回りが読む", { x: 5.36, y: 3.08, w: 0.6, h: 0.26, fontSize: 7, color: "accent", valign: "middle" }); + d.arrow(s, 4.88, 3.69, -0.3, 0, "accent"); + rr(s, X0, 3.38, 4.3, 0.62, "panel2"); + d.text(s, [{ text: "API が事象から作る\n", options: { fontSize: 9, color: "mute" } }, { text: "一覧・詳細 / 承認と通知 / 課金", options: { fontSize: 11.5, bold: true, color: "text" } }], { x: X0, y: 3.38, w: 4.3, h: 0.62, align: "center", valign: "middle" }); + d.arrow(s, 0.9, 3.34, 0, -1.04, "accent"); + // 下段: 承認を待つ間もラインは止まらない + flow(s, X0, 4.22, W, 0.5, [{ text: "承認の依頼を出す" }, "ジョブを終える(費用 0)", "次の周回は待たずに始まる", "答えは持ち主か次の周回へ"], { size: 10.5, gap: 0.3 }); + d.note(s, "オーケストレーターは #2347 の定義 6(実行をまたぐ制御は常駐せず、ジョブの中の区切りはランナーが持つ)と #2439(Factory のオーガナイザー。受け入れ条件 7)。利用上限に当たったジョブも止めて待ち、リセット時刻の後に続ける"); + + // 22. AI をどう動かすかは 3 つから選ぶ + s = pg("AI をどう動かすかは 3 つから選ぶ", "アカウントごとに選ぶ。どれでもスプリントは同じに流れる"); + d.tbl(s, [ + [d.H("実行の形態"), d.H("Claude をどう呼ぶか"), d.H("Token の費用")], + [d.B("① 手元のマシン", { color: "accent" }), "自分の PC・サーバーで、自分がログインした claude\nサービスは Claude の認証情報を持たない", "自分持ち"], + [d.B("② キーの持ち込み", { color: "accent" }), "自分の Anthropic API キーか、自分の AWS の Bedrock", "自分持ち"], + [d.B("③ サービスの Bedrock", { color: "accent" }), "サービスの AWS の Bedrock", "使った分をサブスクリプションに上乗せ"], + ], { x: X0, y: 1.42, w: W, colW: [2.3, 4.88, 2.3], rowH: [0.38, 0.74, 0.56, 0.56], fontSize: 11.5 }); + d.text(s, "料金プラン", { x: X0, y: 3.96, w: 1.1, h: 0.44, fontSize: 12, bold: true, color: "text", valign: "middle" }); + cols(4, 0.14, 1.4, 8.34).forEach((c, i) => pill(s, c.x, 3.96, c.w, 0.44, ["Free(①・②)", "Pro", "Max", "Team(席: standard / premium)"][i], i ? "panel2" : "accentSoft", "text", i === 3 ? 9.5 : 11)); + + // 23. 画面: スプリント一覧 + s = pg("画面: スプリント一覧", "どのスプリントが、いまどのフェーズにいるかが一目で"); + let a = screen(s, X0, 1.3, 6.5, 3.5, "sprints", "スプリント"); + [["同時に動いている", "2 / 3"], ["あなたの対応待ち", "3"], ["今月の Token", "8.4M / 20M"]].forEach((k, i) => { + const c = cols(3, 0.08, a.x, a.w)[i]; + rr(s, c.x, a.y, c.w, 0.5, "panel2", i === 1 ? "caution" : null, 0.05); + tx(s, k[0], c.x + 0.08, a.y + 0.03, c.w - 0.16, 0.18, 6.5, { color: "mute" }); + tx(s, k[1], c.x + 0.08, a.y + 0.2, c.w - 0.16, 0.26, 11, { bold: true, color: i === 1 ? "caution" : "text" }); + }); + const ty = a.y + 0.62; + [["スプリント", 0], ["フェーズ", 1.15], ["状態", 2.85], ["最後の動き", 4.2]].forEach((h) => tx(s, h[0], a.x + h[1], ty, 1.2, 0.18, 6.5, { color: "mute" })); + [ + ["m2171", "devbasex/ai-plugins", 1, 1, "実装", "実行中", "accentSoft", "accent", "2 分前"], + ["m2186", "devbasex/ai-plugins", 0, 0, "設計", "承認待ち(ゲート 1)", "caution", "background", "15 分前"], + ["s0412", "acme/web-shop", 2, 2, "検査", "判断待ち", "warn", "text", "1 時間前"], + ["r0077", "acme/api", 3, 3, "取り込み", "承認待ち(ゲート 2)", "caution", "background", "3 時間前"], + ["m2170", "devbasex/ai-plugins", 5, -1, "仕上げ", "終わった", "header", "text", "10/30"], + ].forEach((r, i) => { + const y = ty + 0.22 + i * 0.39; + d.box(s, a.x, y - 0.02, a.w, 0.01, "rule", false); + tx(s, r[0], a.x, y, 1.1, 0.2, 8, { bold: true, color: "accent" }); + tx(s, r[1], a.x, y + 0.18, 1.1, 0.16, 5.5, { color: "mute" }); + phaseBar(s, a.x + 1.15, y + 0.08, 1.5, r[2], r[3]); + tx(s, r[4], a.x + 1.15, y + 0.16, 1.5, 0.18, 6.5, { bold: true, color: "text" }); + pill(s, a.x + 2.85, y + 0.06, 1.2, 0.22, r[5], r[6], r[7], 6); + tx(s, r[8], a.x + 4.2, y + 0.06, 0.8, 0.22, 6.5, { color: "mute" }); + }); + notes(s, 7.0, 1.4, 2.74, [ + ["フェーズが一目で", "設計 → 実装 → 検査 → 取り込み → 仕上げの 5 本の棒"], + ["対応待ちから直行", "承認待ち・判断待ちの行から、そのまま答えへ"], + ["使える量も見える", "同時に流せる数と、今月の Token"], + ]); + + // 24. 画面: スプリント詳細 + s = pg("画面: スプリント詳細", "AI がどこで何を決めたかを、工程ごとに辿れる"); + a = screen(s, X0, 1.3, 6.5, 3.5, "sprints/m2171", "スプリント"); + tx(s, "m2171", a.x, a.y, 0.8, 0.24, 11, { bold: true, color: "text" }); + pill(s, a.x + 0.72, a.y + 0.03, 0.5, 0.18, "実行中", "accentSoft", "accent", 6); + tx(s, "devbasex/ai-plugins · #2171 contracts と進捗の受け口", a.x + 1.3, a.y, 3.8, 0.24, 6.5, { color: "mute" }); + cols(5, 0.06, a.x, a.w).forEach((c, i) => { + d.box(s, c.x, a.y + 0.34, c.w, 0.06, i === 0 ? "accent" : i === 1 ? "accentSoft" : "rule", false); + tx(s, ["設計 完了", "実装 実行中", "検査", "取り込み", "仕上げ"][i], c.x, a.y + 0.42, c.w, 0.18, 6.5, { bold: i < 2, color: i < 2 ? "text" : "mute" }); + }); + const ly = a.y + 0.7; + tx(s, "工程の記録", a.x, ly, 2.2, 0.2, 7.5, { bold: true, color: "text" }); + [ + ["tdd-cycle を実行中", "失敗するテストを書いた", "accent", true], + ["implementation-plan", "触るファイル 14 本", "header"], + ["ゲート 1 を承認した", "Web から承認", "caution"], + ["cross-review が収束", "2 ラウンド · 未解決 0", "header"], + ["design", "決定 12 件 · 条件 21 件", "header"], + ].forEach((r, i) => { + const y = ly + 0.24 + i * 0.33; + if (r[3]) rr(s, a.x - 0.04, y - 0.02, 2.36, 0.31, "accentSoft", null, 0.04); + dot(s, a.x + 0.02, y + 0.06, 0.1, r[2]); + tx(s, r[0], a.x + 0.18, y - 0.01, 2.1, 0.16, 7, { bold: true, color: "text" }); + tx(s, r[1], a.x + 0.18, y + 0.14, 2.1, 0.14, 5.5, { color: "mute" }); + }); + const px = a.x + 2.45; + const pw = a.w - 2.45; + rr(s, px, ly, pw, 2.08, "panel2", "accent", 0.05); + tx(s, "tdd-cycle を実行中", px + 0.1, ly + 0.05, pw - 0.2, 0.2, 8, { bold: true, color: "text" }); + [["終了コード", "—"], ["秒", "1,082"], ["費用", "$1.84"]].forEach((k, i) => { + const c = cols(3, 0.05, px + 0.1, pw - 0.2)[i]; + tx(s, k[0], c.x, ly + 0.3, c.w, 0.14, 5.5, { color: "mute" }); + tx(s, k[1], c.x, ly + 0.43, c.w, 0.18, 8, { bold: true, color: "text" }); + }); + tx(s, "触ったファイル", px + 0.1, ly + 0.68, pw - 0.2, 0.15, 6, { bold: true, color: "mute" }); + ["factory/api/src/progress/authorize.ts +142", "…/authorize.test.ts +218", "…/run-job-token.ts +96"].forEach((f, i) => tx(s, f, px + 0.1, ly + 0.84 + i * 0.16, pw - 0.2, 0.15, 6, { color: "accent" })); + tx(s, "この工程で決めたこと", px + 0.1, ly + 1.36, pw - 0.2, 0.15, 6, { bold: true, color: "mute" }); + tx(s, "403 の本文に理由を載せるか\n→ 載せない(設計の決定 11 による)", px + 0.1, ly + 1.52, pw - 0.2, 0.5, 6.5, { color: "text", valign: "top" }); + notes(s, 7.0, 1.4, 2.74, [ + ["工程の記録を時系列で", "新しいものが上。押すと右に詳細"], + ["触ったファイルと決定", "GitHub の差分へ飛べる。AI の判断の根拠も残る"], + ["秒と費用まで", "終了コード・所要時間・費用を工程ごとに"], + ]); + + // 25. 画面: 承認ゲート + s = pg("画面: 承認ゲート", "判断の材料は、設計文書・仕様・コードまで辿れる"); + a = screen(s, X0, 1.3, 6.5, 3.5, "inbox/m2186", "対応待ち"); + pill(s, a.x, a.y, 1.9, 0.2, "承認 · ゲート 1 設計 PR のマージ", "caution", "background", 6); + tx(s, "m2186 · 15 分待っている", a.x + 2.0, a.y, 2.0, 0.2, 6, { color: "mute" }); + tx(s, "実行の制御とランナー: 登録したリポジトリでスプリントを並列に流せる", a.x, a.y + 0.24, a.w, 0.26, 8.5, { bold: true, color: "text" }); + const gw = a.w - 1.62; + tx(s, "決めたこと(押すと根拠とコードが開く)", a.x, a.y + 0.56, gw, 0.18, 6.5, { bold: true, color: "mute" }); + rr(s, a.x, a.y + 0.76, gw, 0.86, "panel2", "accent", 0.04); + tx(s, "決定 1: 実行ジョブはコンテナ 1 つ、承認の依頼で終える", a.x + 0.08, a.y + 0.78, gw - 0.16, 0.18, 6.5, { bold: true, color: "text" }); + tx(s, "理由: 承認待ちは数日かかる。待つ間にコンテナの費用を払わない", a.x + 0.08, a.y + 0.98, gw - 0.16, 0.16, 5.8); + tx(s, "依拠する仕様: 確定仕様「実行の単位と NDF の扱い」", a.x + 0.08, a.y + 1.16, gw - 0.16, 0.16, 5.8, { color: "accent" }); + tx(s, "触るコード: factory/api/src/control/start-job.ts", a.x + 0.08, a.y + 1.34, gw - 0.16, 0.16, 5.8, { color: "accent" }); + ["決定 2: NDF の版はスプリントの開始で固定する", "決定 3: 同時に流せる数を超えたら開始を断る"].forEach((t, i) => { + rr(s, a.x, a.y + 1.68 + i * 0.26, gw, 0.22, "panel2", null, 0.04); + tx(s, t, a.x + 0.08, a.y + 1.68 + i * 0.26, gw - 0.16, 0.22, 6.5, { color: "text" }); + }); + [["受け入れ条件", "18 件"], ["未確認", "4 件"], ["レビュー", "収束"]].forEach((k, i) => { + const c = cols(3, 0.06, a.x, gw)[i]; + rr(s, c.x, a.y + 2.24, c.w, 0.4, "panel2", null, 0.04); + tx(s, k[0], c.x + 0.06, a.y + 2.25, c.w - 0.12, 0.14, 5.5, { color: "mute" }); + tx(s, k[1], c.x + 0.06, a.y + 2.39, c.w - 0.12, 0.22, 9, { bold: true, color: "text" }); + }); + const ax = a.x + gw + 0.1; + rr(s, ax, a.y + 0.56, 1.52, 2.08, "panel2", "caution", 0.05); + tx(s, "答える", ax + 0.08, a.y + 0.6, 1.36, 0.2, 7.5, { bold: true, color: "text" }); + tx(s, "承認すると設計 PR をマージし、実装へ進む", ax + 0.08, a.y + 0.82, 1.36, 0.36, 5.5, { valign: "top" }); + rr(s, ax + 0.08, a.y + 1.2, 1.36, 0.56, "background", "rule", 0.03); + tx(s, "差し戻す理由", ax + 0.14, a.y + 1.22, 1.2, 0.16, 5.5, { color: "mute" }); + pill(s, ax + 0.08, a.y + 1.86, 1.36, 0.28, "承認する", "accent", "background", 7); + pill(s, ax + 0.08, a.y + 2.22, 1.36, 0.28, "差し戻す", "background", "warn", 7); + notes(s, 7.0, 1.4, 2.74, [ + ["決定ごとに根拠を開く", "理由・依拠する仕様・触るコードの差分"], + ["そろった材料で 1 回", "受け入れ条件・未確認の点・レビューの収束"], + ["承認か差し戻し", "差し戻しは理由が必須。Slack から開いても同じ画面"], + ]); + d.note(s, "承認ゲートの外の問いは hook が止めて AI に判断させ、費用がかかる・取り消せない操作だけが、理由と推奨の選択肢つきで同じ対応待ちに並ぶ"); + + // 26. 画面: スプリントを始める + s = pg("画面: スプリントを始める", "AI が棚卸しの順位から、1 版に収まる Issue を選ぶ"); + a = screen(s, X0, 1.3, 6.5, 3.5, "sprints/new", "スプリントを始める"); + const nw = a.w - 1.62; + tx(s, "1 リポジトリ", a.x, a.y, nw, 0.2, 7.5, { bold: true, color: "text" }); + [["devbasex/ai-plugins", ".ndf/ あり", true], ["acme/web-shop", "判断待ち 1", false], ["acme/api", ".ndf/ 無し(既定で動く)", false]].forEach((r, i) => { + const y = a.y + 0.24 + i * 0.25; + rr(s, a.x, y, nw, 0.22, r[2] ? "accentSoft" : "panel2", r[2] ? "accent" : null, 0.04); + dot(s, a.x + 0.07, y + 0.06, 0.1, r[2] ? "accent" : "rule"); + tx(s, r[0], a.x + 0.24, y, 1.6, 0.22, 6.5, { bold: true, color: "text" }); + tx(s, r[1], a.x + 1.9, y, nw - 2.0, 0.22, 5.5, { color: "mute" }); + }); + tx(s, "2 スプリントに入れる Issue", a.x, a.y + 1.04, 2.4, 0.2, 7.5, { bold: true, color: "text" }); + pill(s, a.x + 2.0, a.y + 1.05, 0.8, 0.18, "AI が選んだ", "header", "text", 5.5); + [ + ["#2186 実行の制御とランナー", "順位 1 · 前提がそろった", true], + ["#2206 hook: ゲート外の問いを AI に", "順位 2 · 同じ版に収まる", true], + ["#2173 承認の通知", "前提 #2186 が未完了", false], + ["#2174 課金", "決済の提供元が未決", false], + ].forEach((r, i) => { + const y = a.y + 1.28 + i * 0.27; + rr(s, a.x, y, nw, 0.24, r[2] ? "panel2" : "panel1", r[2] ? "accent" : "rule", 0.04); + rr(s, a.x + 0.06, y + 0.06, 0.12, 0.12, r[2] ? "accent" : "panel1", r[2] ? null : "mute", 0.02); + tx(s, r[0], a.x + 0.24, y, 2.4, 0.24, 6.5, { bold: true, color: r[2] ? "text" : "mute" }); + tx(s, r[1], a.x + 2.66, y, nw - 2.7, 0.24, 5.5, { color: "mute" }); + }); + const cx2 = a.x + nw + 0.1; + rr(s, cx2, a.y, 1.52, 2.62, "panel2", "rule", 0.05); + tx(s, "始める前の確認", cx2 + 0.08, a.y + 0.04, 1.36, 0.2, 7, { bold: true, color: "text" }); + [["同時のスプリント", "2 → 3 / 3"], ["今月の Token", "8.4M / 20M"], ["実行の形態", "③ サービスの Bedrock"], ["NDF の版", "10.27.14"]].forEach((k, i) => { + tx(s, k[0], cx2 + 0.08, a.y + 0.3 + i * 0.4, 1.36, 0.14, 5.5, { color: "mute" }); + tx(s, k[1], cx2 + 0.08, a.y + 0.44 + i * 0.4, 1.36, 0.18, 7, { bold: true, color: "text" }); + }); + pill(s, cx2 + 0.08, a.y + 2.04, 1.36, 0.28, "スプリントを始める", "accent", "background", 6.5); + tx(s, "止まるのは承認の 2 回だけ", cx2 + 0.08, a.y + 2.36, 1.36, 0.18, 5.5, { color: "mute", align: "center" }); + notes(s, 7.0, 1.4, 2.74, [ + ["リポジトリを選ぶ", "GitHub App で登録。.ndf/ の設定を読む"], + ["Issue は AI が選ぶ", "棚卸しの順位から、前提がそろい 1 版に収まる組"], + ["上限を見てから開始", "同時の数・Token・NDF の版を確認"], + ]); + + // 27. 画面: 開発ループ + s = pg("画面: 開発ループ", "予算を決めて始めれば、周回は止まるまで続く"); + a = screen(s, X0, 1.3, 6.5, 3.5, "loops", "ループ"); + [["動いているループ", "2 / 3"], ["あなたの対応待ち", "1"], ["今月の Token", "8.4M / 20M"]].forEach((k, i) => { + const c = cols(3, 0.08, a.x, a.w)[i]; + rr(s, c.x, a.y, c.w, 0.5, "panel2", i === 1 ? "caution" : null, 0.05); + tx(s, k[0], c.x + 0.08, a.y + 0.03, c.w - 0.16, 0.18, 6.5, { color: "mute" }); + tx(s, k[1], c.x + 0.08, a.y + 0.2, c.w - 0.16, 0.26, 11, { bold: true, color: i === 1 ? "caution" : "text" }); + }); + const ly2 = a.y + 0.62; + [["ループ", 0], ["周回", 1.2], ["費用 / 予算", 1.95], ["状態", 2.9]].forEach((h) => tx(s, h[0], a.x + h[1], ly2, 1.2, 0.18, 6.5, { color: "mute" })); + [ + ["lfactory2", "devbasex/ai-plugins", "4 / 10", "$31 / $50", "実行中", "accentSoft", "accent"], + ["lweb", "acme/web-shop", "2 / 5", "$12 / $30", "利用上限の待ち · 14:00 に再開", "panel2", "ink"], + ["lapi", "acme/api", "1 / 3", "$4 / $20", "判断待ち 1", "warn", "text"], + ["lroot2", "devbasex/ai-plugins", "5 / 5", "$40 / $40", "予算で止まった", "header", "text"], + ].forEach((r, i) => { + const y = ly2 + 0.22 + i * 0.39; + d.box(s, a.x, y - 0.02, a.w, 0.01, "rule", false); + tx(s, r[0], a.x, y, 1.1, 0.2, 8, { bold: true, color: "accent" }); + tx(s, r[1], a.x, y + 0.18, 1.1, 0.16, 5.5, { color: "mute" }); + tx(s, r[2], a.x + 1.2, y + 0.06, 0.7, 0.22, 7.5, { bold: true, color: "text" }); + tx(s, r[3], a.x + 1.95, y + 0.06, 0.9, 0.22, 7, { color: "text" }); + pill(s, a.x + 2.9, y + 0.06, 1.95, 0.22, r[4], r[5], r[6], 6); + }); + ["ループを始める", "止める", "予算を変える"].forEach((t, i) => pill(s, a.x + i * 1.0, a.y + 2.5, 0.92, 0.24, t, i ? "panel2" : "accent", i ? "text" : "background", 6.5)); + notes(s, 7.0, 1.4, 2.74, [ + ["予算は 3 つ", "周回の数・費用・時間。超えたら周回の境で止まる"], + ["利用上限で止めて待つ", "リセット時刻の後に自動で再開。待つ間の費用は 0"], + ["人に戻った課題だけ届く", "判断待ちは対応待ちに並ぶ"], + ]); + + // 28. 並べても、本番の配布は 1 本にまとめる + s = pg("並べても、本番の配布は 1 本にまとめる", "同じリポジトリの実行の間を、Factory のオーガナイザーが取り持つ"); + ["スプリント A", "ループ B", "ループ C"].forEach((t, i) => { + const y = 1.42 + i * 0.6; + rr(s, X0, y, 1.9, 0.48, "panel2"); + d.text(s, t, { x: X0, y, w: 1.9, h: 0.48, fontSize: 12, bold: true, color: "text", align: "center", valign: "middle" }); + d.arrow(s, 2.2, y + 0.24, 0.52, 2.26 - (y + 0.24), "accent"); + }); + rr(s, 2.76, 1.42, 3.0, 1.68, "panel1", "accent"); + d.text(s, "オーガナイザー", { x: 2.92, y: 1.5, w: 2.7, h: 0.34, fontSize: 13, bold: true, color: "accent", valign: "middle" }); + d.text(s, bullets(["本番の配布の依頼を集めて 1 本に", "承認と問いを 1 か所に集める"], 11), { x: 2.92, y: 1.9, w: 2.75, h: 1.1, valign: "top", paraSpaceAfter: 4 }); + d.arrow(s, 5.8, 2.26, 0.4, 0, "accent"); + rr(s, 6.24, 1.42, 3.5, 1.68, "caution"); + d.text(s, [{ text: "本番の配布 1 本\n", options: { fontSize: 15, bold: true } }, { text: "承認ゲート 2 は 1 回で済む", options: { fontSize: 12 } }], { x: 6.4, y: 1.42, w: 3.2, h: 1.68, color: "background", valign: "middle" }); + d.text(s, "取り合いは機械が防ぐ(配布済み)", { x: X0, y: 3.3, w: W, h: 0.3, fontSize: 11.5, bold: true, color: "text", valign: "middle" }); + [ + "同じ Issue を\n2 つの実行に入れない", + "マイルストーンが重なる\nループは同時に動かさない", + "古い版の NDF は\n1 本だけで動かす", + "同時に動かす数を\nリポジトリごとに宣言", + ].forEach((t, i) => { + const c = cols(4, 0.12)[i]; + rr(s, c.x, 3.66, c.w, 0.86, "panel1"); + d.text(s, t, { x: c.x + 0.12, y: 3.66, w: c.w - 0.24, h: 0.86, fontSize: 10.5, bold: true, color: "ink", valign: "middle" }); + }); + d.note(s, "取り合いの防ぎ方は v10.27.14 で配布(#2347)。配布をまとめるオーガナイザーは #2439 で設計を承認し、これから実装する。手元の 1 台で並べるときのまとめ役とは排他"); + + // 29. 現在地: 子 Issue で順に作っている + s = pg("現在地: 子 Issue で順に作っている", "このサービス自体も、NDF の開発ループで開発中"); + const ST = { done: ["完了", "header", "text"], ship: ["配布済み", "header", "text"], design: ["設計を承認", "caution", "background"], sample: ["見本が動く", "accent", "background"], next: ["次", "panel2", "mute"] }; + [ + ["#2169", "置き場の分離と全体設計", "done", "v10.25.0"], + ["#2170", "認証と料金プラン", "ship", "v10.26.0"], + ["#2171", "契約と進捗の受け口", "ship", "v10.26.1"], + ["#2186", "実行の制御とランナー", "done", "v10.27.1"], + ["#2382", "UX の土台と画面の部品", "ship", "v10.27.14"], + ["#2347", "開発ループと並行の調整", "ship", "v10.27.14"], + ["#2439", "オーガナイザー", "design", ""], + ["#2172", "スプリントの一覧と詳細", "sample", ""], + ["#2173", "承認の通知と Web の承認", "next", ""], + ["#2174", "Token の課金", "next", ""], + ["#2189", "手元のマシン", "next", ""], + ["#2383・#2384", "オフライン・API と MCP", "next", ""], + ].forEach((r, i) => { + const col = i < 6 ? 0 : 1; + const c = cols(2, 0.3)[col]; + const y = 1.36 + (i % 6) * 0.57; + rr(s, c.x, y, c.w, 0.49, "panel1"); + const st = ST[r[2]]; + d.text(s, r[0], { x: c.x + 0.14, y: r[3] ? y + 0.03 : y, w: 1.15, h: r[3] ? 0.26 : 0.49, fontSize: 10, bold: true, color: "accent", valign: "middle" }); + if (r[3]) d.text(s, r[3], { x: c.x + 0.14, y: y + 0.26, w: 1.15, h: 0.2, fontSize: 8, color: "mute", valign: "middle" }); + d.text(s, r[1], { x: c.x + 1.3, y, w: c.w - 2.66, h: 0.49, fontSize: 11, bold: true, color: "text", valign: "middle" }); + pill(s, c.x + c.w - 1.24, y + 0.1, 1.1, 0.29, st[0], st[1], st[2], 9.5); + }); + d.note(s, "親 Issue: #2168。2026-11-03 時点。配布済みの #2170・#2171 は実環境での確認を待って開いている"); + + // 30. まとめ: 人は 2 つだけ決める + s = pg("まとめ: 人は 2 つだけ決める", "あとは工場が回す"); + // Software Factory + NDF = Neural Development Factory の式 + const BW = 2.7, OW = (W - BW * 3) / 2; + [ + ["Software Factory", "考え方。人は「何を作るか」と\n「出してよいか」だけを決める", "caution", "panel1"], + ["NDF", "工場のラインを\nプラグインにした", "accent", "panel1"], + ["Neural Development Factory", "そのラインを\nWeb から回すサービス", "header", "panel2"], + ].forEach((r, i) => { + const x = X0 + i * (BW + OW); + rr(s, x, 1.5, BW, 2.0, r[3], r[2]); + d.text(s, r[0], { x: x + 0.1, y: 1.6, w: BW - 0.2, h: 0.78, fontSize: 15, bold: true, color: r[2], align: "center", valign: "middle" }); + d.text(s, r[1], { x: x + 0.12, y: 2.42, w: BW - 0.24, h: 0.92, fontSize: 12, bold: true, color: "text", align: "center", valign: "middle" }); + if (i < 2) d.text(s, i === 0 ? "+" : "=", { x: x + BW, y: 1.5, w: OW, h: 2.0, fontSize: 40, bold: true, color: "text", align: "center", valign: "middle" }); + }); + d.text(s, [{ text: REPO, options: { hyperlink: { url: REPO }, color: "accent" } }], { x: X0, y: 3.86, w: W, h: 0.5, fontSize: 18, bold: true, align: "center", valign: "middle" }); +}); diff --git a/docs/presentations/2026-11-03-neural-development-factory/slides/lib/deck-frame.js b/docs/presentations/2026-11-03-neural-development-factory/slides/lib/deck-frame.js new file mode 100644 index 000000000..629959582 --- /dev/null +++ b/docs/presentations/2026-11-03-neural-development-factory/slides/lib/deck-frame.js @@ -0,0 +1,433 @@ +"use strict"; +// ページの枠組み(汎用テンプレート)。pptxgenjs で 16:9(10 × 5.625 インチ)の pptx を作る。 +// +// - 座標は FRAME の定数だけが持つ。トーン&マナー(theme.json)は座標を持たない +// - 書体と色の値は DEFAULT_THEME と theme.json だけが持つ。関数に渡す色は theme.colors の項目名で受ける +// - 台本(script.md)を渡すと、ページを作るたびに同じ番号の読み上げをスピーカーノートへ付け、 +// 保存の前に台本のページ数と作ったページ数を照合する +// - 章扉(section)は構成を見せるページで、スピーカーノートを持たず、台本の番号を進めない。 +// ページ数の照合からも外す(ページ番号は章扉も数える) +// +// 資料のディレクトリへ写して使う。写した後に書き換えない(プロジェクトの値は theme.json へ書く)。 + +const fs = require("fs"); +const path = require("path"); + +const EXIT_INPUT = 1; // 引数・theme.json・ページ数の照合の失敗 +const EXIT_SCRIPT = 2; // 台本の形の失敗 + +class DeckError extends Error { + constructor(message, code) { + super(message); + this.code = code; + } +} + +// ---- 座標(インチ)。トーン&マナーに依らない ---- +const FRAME = Object.freeze({ + slide: { w: 10, h: 5.625 }, + title: { x: 0.26, y: 0.15, w: 8.29, h: 0.63 }, + rule: { x: 0.24, y: 0.8, w: 9.52 }, + logo: { x: 8.78, y: 0.24, w: 0.96, h: 0.23 }, + lead: { x: 0.26, y: 0.9, w: 9.5, h: 0.34 }, + copyright: { x: 0.26, y: 5.18, w: 3.2, h: 0.3 }, + pageNo: { x: 9.27, y: 5.18, w: 0.5, h: 0.3 }, + note: { x: 0.26, y: 4.88, w: 9.5, h: 0.28 }, + body: { x: 0.26, y: 1.34, w: 9.48 }, + callout: { x: 0.26, w: 9.48, padX: 0.22, headH: 0.28 }, + section: { + band: { x: 0, y: 0, w: 0.36, h: 5.625 }, + title: { x: 0.9, y: 1.9, w: 8.4, h: 0.8 }, + accent: { x: 0.92, y: 2.82, w: 1.2, width: 4 }, + sub: { x: 0.9, y: 3.0, w: 8.4, h: 0.4 }, + copyright: { x: 0.56, y: 5.18, w: 3.2, h: 0.3 }, + }, + cover: { + band: { x: 0, y: 0, w: 10, h: 0.36 }, + logo: { x: 0.26, y: 0.6, w: 0.96, h: 0.23 }, + title: { x: 1.02, y: 1.3, w: 8.2, h: 0.72 }, + accent: { x: 1.04, y: 2.1, w: 1.2, width: 4 }, + sub: { x: 1.02, y: 2.26, w: 8.2, h: 0.62 }, + meta: { x: 1.02, y: 3.55, w: 8.2, h: 0.4 }, + }, +}); + +// ---- 既定のトーン&マナー(会社色を持たない)。正本はここ ---- +const DEFAULT_THEME = Object.freeze({ + font: "Noto Sans JP", + colors: Object.freeze({ + accent: "2B6CB0", + accentSoft: "E3EDF7", + text: "000000", + ink: "303030", + mute: "7F7F7F", + rule: "D9D9D9", + panel1: "F3F3F3", + panel2: "EEEEEE", + warn: "F4CCCC", + caution: "FFF2CC", + header: "303030", + onHeader: "FFFFFF", + background: "FFFFFF", + }), + sizes: Object.freeze({ + title: 20, + lead: 12, + body: 10, + table: 10, + note: 8, + footer: 9, + calloutHead: 11.5, + calloutBody: 9.5, + sectionTitle: 32, + sectionSub: 14, + coverTitle: 28, + coverSub: 16, + coverMeta: 12, + }), + copyright: "", + logo: null, +}); + +const THEME_KEYS = ["font", "colors", "sizes", "copyright", "logo"]; +const HEX = /^[0-9A-Fa-f]{6}$/; + +// theme.json の値を既定値へ重ねる。colors と sizes は項目ごと、ほかは値ごと置き換える。 +function mergeTheme(override, baseDir) { + const t = { + font: DEFAULT_THEME.font, + colors: { ...DEFAULT_THEME.colors }, + sizes: { ...DEFAULT_THEME.sizes }, + copyright: DEFAULT_THEME.copyright, + logo: DEFAULT_THEME.logo, + }; + if (override == null) return t; + if (typeof override !== "object" || Array.isArray(override)) { + throw new DeckError("theme.json はオブジェクトでなければならない", EXIT_INPUT); + } + const unknown = Object.keys(override).filter((k) => !THEME_KEYS.includes(k)); + if (unknown.length) throw new DeckError(`theme.json に知らない項目がある: ${unknown.join(", ")}`, EXIT_INPUT); + if ("font" in override) { + if (typeof override.font !== "string" || !override.font) throw new DeckError("theme.json の font は空でない文字列", EXIT_INPUT); + t.font = override.font; + } + for (const group of ["colors", "sizes"]) { + if (!(group in override)) continue; + const g = override[group]; + if (typeof g !== "object" || g == null || Array.isArray(g)) throw new DeckError(`theme.json の ${group} はオブジェクト`, EXIT_INPUT); + const bad = Object.keys(g).filter((k) => !(k in DEFAULT_THEME[group])); + if (bad.length) throw new DeckError(`theme.json の ${group} に知らない項目がある: ${bad.join(", ")}`, EXIT_INPUT); + for (const [k, v] of Object.entries(g)) { + if (group === "colors" && !(typeof v === "string" && HEX.test(v))) { + throw new DeckError(`theme.json の colors.${k} は 6 桁の 16 進(# なし): ${v}`, EXIT_INPUT); + } + if (group === "sizes" && !(typeof v === "number" && v > 0)) { + throw new DeckError(`theme.json の sizes.${k} は正の数(pt): ${v}`, EXIT_INPUT); + } + t[group][k] = group === "colors" ? v.toUpperCase() : v; + } + } + if ("copyright" in override) { + if (typeof override.copyright !== "string") throw new DeckError("theme.json の copyright は文字列", EXIT_INPUT); + t.copyright = override.copyright; + } + if ("logo" in override) { + if (override.logo === null) { + t.logo = null; + } else if (typeof override.logo === "string" && override.logo) { + const p = path.resolve(baseDir || process.cwd(), override.logo); + if (!fs.existsSync(p)) throw new DeckError(`theme.json の logo のファイルが無い: ${p}`, EXIT_INPUT); + t.logo = p; + } else { + throw new DeckError("theme.json の logo は PNG の相対パスか null", EXIT_INPUT); + } + } + return t; +} + +function loadTheme(theme) { + if (theme == null) return mergeTheme(null); + if (typeof theme === "object") return mergeTheme(theme, process.cwd()); + if (!fs.existsSync(theme)) return mergeTheme(null); + let data; + try { + data = JSON.parse(fs.readFileSync(theme, "utf8")); + } catch (e) { + throw new DeckError(`theme.json を JSON として読めない: ${theme}: ${e.message}`, EXIT_INPUT); + } + return mergeTheme(data, path.dirname(path.resolve(theme))); +} + +// ---- 台本(script.md)の読み込み ---- +// `## <番号>. <見出し>` がページの区切り。見出しの末尾の `(<秒>秒)` は配分秒数。 +// 区切りの中の `>` で始まる行が読み上げ。`>` だけの行は段落の区切り。ほかの行は読まない。 +const HEAD = /^##\s+(\d+)\.\s*(.*?)\s*$/; +const SECONDS = /[((]\s*(\d+)\s*秒\s*[))]\s*$/; + +function plain(s) { + return s.replace(/\*\*/g, "").replace(/`/g, ""); +} + +function parseScript(text, name) { + const pages = []; + let cur = null; + let para = []; + const flush = () => { + if (cur && para.length) cur.paragraphs.push(para.join("")); + para = []; + }; + for (const raw of text.split(/\r?\n/)) { + const m = raw.match(HEAD); + if (m) { + flush(); + let title = m[2]; + let seconds = null; + const sm = title.match(SECONDS); + if (sm) { + seconds = Number(sm[1]); + title = title.slice(0, sm.index).trim(); + } + cur = { no: Number(m[1]), title, seconds, paragraphs: [] }; + pages.push(cur); + continue; + } + if (/^#{1,2}\s/.test(raw)) { + flush(); + cur = null; // 番号の無い見出しの下は読まない + continue; + } + if (!cur) continue; + const q = raw.match(/^>\s?(.*)$/); + if (q) { + const body = q[1].trim(); + if (body) para.push(plain(body)); + else flush(); + } else { + flush(); + } + } + flush(); + if (!pages.length) throw new DeckError(`台本にページの区切り(## <番号>. <見出し>)が無い: ${name}`, EXIT_SCRIPT); + const seen = new Map(); + for (const p of pages) seen.set(p.no, (seen.get(p.no) || 0) + 1); + const dup = [...seen].filter(([, c]) => c > 1).map(([n]) => n); + if (dup.length) throw new DeckError(`台本のページの番号が重なる: ${dup.join(", ")}(${name})`, EXIT_SCRIPT); + const max = Math.max(...pages.map((p) => p.no)); + const missing = []; + for (let i = 1; i <= max; i++) if (!seen.has(i)) missing.push(i); + if (missing.length) throw new DeckError(`台本のページの番号が欠ける: ${missing.join(", ")}(${name})`, EXIT_SCRIPT); + pages.sort((a, b) => a.no - b.no); + for (const p of pages) p.reading = p.paragraphs.join("\n"); + return pages; +} + +function readScript(file) { + if (!fs.existsSync(file)) throw new DeckError(`台本が無い: ${file}`, EXIT_INPUT); + return parseScript(fs.readFileSync(file, "utf8"), file); +} + +// ---- 表のセル ---- +const H = (t, extra) => ({ text: t, options: Object.assign({ fill: { color: "header" }, color: "onHeader", bold: true, align: "center" }, extra || {}) }); +const C = (t, extra) => ({ text: t, options: Object.assign({ align: "center" }, extra || {}) }); +const B = (t, extra) => ({ text: t, options: Object.assign({ bold: true }, extra || {}) }); + +// ---- 資料 ---- +function createDeck(opts) { + const o = opts || {}; + const PptxGenJS = require(require.resolve("pptxgenjs", { paths: [o.baseDir || process.cwd(), __dirname] })); + const theme = loadTheme(o.theme); + const script = o.script ? readScript(o.script) : null; + const p = new PptxGenJS(); + p.layout = "LAYOUT_16x9"; + if (o.title) p.title = o.title; + if (o.author) p.author = o.author; + if (o.title) p.subject = o.title; + const F = theme.font; + const logo = theme.logo ? "image/png;base64," + fs.readFileSync(theme.logo).toString("base64") : null; + let pageNo = 0; // 作ったページ(章扉を含む)。ページ番号に使う + let scriptNo = 0; // 台本のページを消費したページ(章扉を除く) + let sections = 0; + + function color(name) { + if (!(name in theme.colors)) throw new DeckError(`色は theme.colors の項目名で指す: ${name}`, EXIT_INPUT); + return theme.colors[name]; + } + function size(v) { + if (typeof v === "number") return v; + if (!(v in theme.sizes)) throw new DeckError(`文字の大きさは pt か theme.sizes の項目名で指す: ${v}`, EXIT_INPUT); + return theme.sizes[v]; + } + // 色の項目名を値へ置き換える(color・fill.color・line.color・border.color) + function resolve(options) { + if (!options || typeof options !== "object") return options; + const r = Array.isArray(options) ? options.map(resolve) : { ...options }; + if (Array.isArray(r)) return r; + if (typeof r.color === "string") r.color = color(r.color); + for (const k of ["fill", "line", "border"]) { + if (r[k] && typeof r[k] === "object" && !Array.isArray(r[k]) && typeof r[k].color === "string") r[k] = { ...r[k], color: color(r[k].color) }; + if (Array.isArray(r[k])) r[k] = r[k].map((b) => (b && typeof b.color === "string" ? { ...b, color: color(b.color) } : b)); + } + if (r.fontSize !== undefined) r.fontSize = size(r.fontSize); + return r; + } + function runs(t) { + if (!Array.isArray(t)) return t; + return t.map((x) => (x && typeof x === "object" ? { ...x, options: resolve(x.options) } : x)); + } + + // withScript が false のページ(章扉)は台本の番号を進めず、ノートを持たない + function newSlide(withScript) { + const s = p.addSlide(); + pageNo++; + s.background = { color: color("background") }; + if (withScript === false) { + sections++; + return s; + } + scriptNo++; + if (script) { + const sp = script[scriptNo - 1]; + if (sp && sp.reading) s.addNotes(sp.reading); + } + return s; + } + function footer(s, cx, withNo, textColor) { + if (theme.copyright) { + s.addText(theme.copyright, { ...cx, fontFace: F, fontSize: theme.sizes.footer, color: color(textColor || "mute"), valign: "middle", margin: 0 }); + } + if (withNo) { + s.addText(String(pageNo), { ...FRAME.pageNo, fontFace: F, fontSize: theme.sizes.footer, color: color("mute"), align: "right", valign: "middle", margin: 0 }); + } + } + function putLogo(s, at) { + if (logo) s.addImage({ data: logo, ...at }); + } + + // 文字を置く。既定は本文の書体・大きさ・色(ink)。色は項目名で、大きさは pt か項目名で受ける + function text(s, t, opt) { + s.addText(runs(t), resolve(Object.assign({ fontFace: F, fontSize: theme.sizes.body, color: "ink", margin: 0, valign: "top" }, opt || {}))); + } + + function cover(title, sub, meta) { + const s = newSlide(); + const c = FRAME.cover; + s.addShape(p.ShapeType.rect, { ...c.band, fill: { color: color("accent") }, line: { type: "none" } }); + putLogo(s, c.logo); + s.addText(title, { ...c.title, fontFace: F, fontSize: theme.sizes.coverTitle, bold: true, color: color("text"), valign: "bottom", margin: 0, fit: "shrink" }); + s.addShape(p.ShapeType.line, { x: c.accent.x, y: c.accent.y, w: c.accent.w, h: 0, line: { color: color("accent"), width: c.accent.width } }); + if (sub) s.addText(sub, { ...c.sub, fontFace: F, fontSize: theme.sizes.coverSub, bold: true, color: color("ink"), valign: "top", margin: 0 }); + if (meta) s.addText(meta, { ...c.meta, fontFace: F, fontSize: theme.sizes.coverMeta, color: color("ink"), margin: 0 }); + footer(s, FRAME.copyright, false); + return s; + } + + function page(title, lead) { + const s = newSlide(); + s.addText(title, { ...FRAME.title, fontFace: F, fontSize: theme.sizes.title, bold: true, color: color("text"), valign: "middle", margin: 0, fit: "shrink" }); + putLogo(s, FRAME.logo); + s.addShape(p.ShapeType.line, { x: FRAME.rule.x, y: FRAME.rule.y, w: FRAME.rule.w, h: 0, line: { color: color("rule"), width: 1 } }); + if (lead) s.addText(lead, { ...FRAME.lead, fontFace: F, fontSize: theme.sizes.lead, bold: true, color: color("ink"), valign: "middle", margin: 0 }); + footer(s, FRAME.copyright, true); + return s; + } + + function section(title, sub) { + const s = newSlide(false); + const c = FRAME.section; + s.addShape(p.ShapeType.rect, { ...c.band, fill: { color: color("accent") }, line: { type: "none" } }); + putLogo(s, FRAME.logo); + s.addText(title, { ...c.title, fontFace: F, fontSize: theme.sizes.sectionTitle, bold: true, color: color("text"), margin: 0 }); + s.addShape(p.ShapeType.line, { x: c.accent.x, y: c.accent.y, w: c.accent.w, h: 0, line: { color: color("accent"), width: c.accent.width } }); + if (sub) s.addText(sub, { ...c.sub, fontFace: F, fontSize: theme.sizes.sectionSub, bold: true, color: color("ink"), margin: 0 }); + footer(s, c.copyright, true); + return s; + } + + function box(s, x, y, w, h, fill, line) { + s.addShape(p.ShapeType.rect, { x, y, w, h, fill: { color: color(fill) }, line: line === false ? { type: "none" } : { color: color(line || "rule"), width: 1 } }); + } + + // 箱に文字を中央寄せで載せる(図の部品) + function chip(s, x, y, w, h, t, fill, opt) { + box(s, x, y, w, h, fill, false); + text(s, t, Object.assign({ x, y, w, h, align: "center", valign: "middle", bold: true, color: "text" }, opt || {})); + } + + // 矢印(x, y から w, h だけ進む) + function arrow(s, x, y, w, h, lineColor) { + s.addShape(p.ShapeType.line, { x, y, w, h, line: { color: color(lineColor || "mute"), width: 1.5, endArrowType: "triangle" } }); + } + + function note(s, t) { + s.addText(runs(t), { ...FRAME.note, fontFace: F, fontSize: theme.sizes.note, color: color("mute"), valign: "middle", margin: 0 }); + } + + function tbl(s, rows, opt) { + const r = rows.map((row) => row.map((cell) => (cell && typeof cell === "object" ? { ...cell, options: resolve(cell.options) } : cell))); + s.addTable(r, resolve(Object.assign({ fontFace: F, fontSize: theme.sizes.table, color: "ink", valign: "middle", border: { type: "solid", color: "rule", pt: 1 }, autoPage: false, margin: [2, 4, 2, 4] }, opt || {}))); + } + + function callout(s, y, h, fill, head, body) { + const c = FRAME.callout; + box(s, c.x, y, c.w, h, fill, false); + s.addText(head, { x: c.x + c.padX, y: y + 0.05, w: c.w - 2 * c.padX, h: c.headH, fontFace: F, fontSize: theme.sizes.calloutHead, bold: true, color: color("text"), valign: "middle", margin: 0 }); + if (body) s.addText(runs(body), { x: c.x + c.padX, y: y + 0.05 + c.headH, w: c.w - 2 * c.padX, h: h - c.headH - 0.1, fontFace: F, fontSize: theme.sizes.calloutBody, color: color("ink"), valign: "top", margin: 0 }); + } + + async function save(out) { + if (!out || !/\.pptx$/i.test(out)) throw new DeckError("出力のパスは .pptx で終える", EXIT_INPUT); + if (script && script.length !== scriptNo) { + throw new DeckError(`ページ数が台本と合わない: 台本 ${script.length} ページ / 作ったページ ${scriptNo}(章扉 ${sections} を除く)`, EXIT_INPUT); + } + await p.writeFile({ fileName: out }); + return { out, pages: pageNo, sections }; + } + + return { + pptx: p, + theme, + frame: FRAME, + script, + get pageNo() { + return pageNo; + }, + get scriptNo() { + return scriptNo; + }, + color, + text, + cover, + page, + section, + box, + chip, + arrow, + note, + tbl, + callout, + H, + C, + B, + save, + }; +} + +// 生成スクリプトの入口。`node build-deck.js <出力>.pptx []` +// build(deck) でページを作り、保存まで行う。失敗は DeckError の code で終える +function run(dir, opts, build) { + const [out, themeArg] = process.argv.slice(2); + (async () => { + if (!out) throw new DeckError("usage: node build-deck.js <出力>.pptx []", EXIT_INPUT); + const theme = themeArg ? path.resolve(themeArg) : path.join(dir, "theme.json"); + if (themeArg && !fs.existsSync(theme)) throw new DeckError(`theme.json が無い: ${theme}`, EXIT_INPUT); + const deck = createDeck({ ...opts, theme, baseDir: dir }); + await build(deck); + const r = await deck.save(out); + console.log(`${r.out}: ${r.pages} ページ(章扉 ${r.sections})`); + })().catch((e) => { + console.error(e.message); + process.exit(e instanceof DeckError ? e.code : 1); + }); +} + +module.exports = { createDeck, run, parseScript, readScript, mergeTheme, DeckError, FRAME, DEFAULT_THEME, H, C, B, EXIT_INPUT, EXIT_SCRIPT }; diff --git a/docs/presentations/2026-11-03-neural-development-factory/slides/lib/script-time.js b/docs/presentations/2026-11-03-neural-development-factory/slides/lib/script-time.js new file mode 100644 index 000000000..d89484cbf --- /dev/null +++ b/docs/presentations/2026-11-03-neural-development-factory/slides/lib/script-time.js @@ -0,0 +1,80 @@ +#!/usr/bin/env node +"use strict"; +// 台本の字数と尺を、ページごとと合計で出す。 +// +// node lib/script-time.js <台本> [--wpm 350] [--limit <分>] [--json] +// +// - 字数は読み上げ(`>` の行)の文字の数。空白と強調の記号(** と `)は数えない +// - 尺の秒は 字数 ÷ --wpm(1 分あたりの字数。既定 350)× 60。合計はページごとの値の和 +// - 終了コード: 0 / 1 合計が --limit(分)を超えた・引数の失敗 / 2 台本の形の失敗 +// 台本の読み取りは deck-frame.js と同じ関数を使う(スピーカーノートと同じ文を数える)。 + +const { readScript, DeckError, EXIT_INPUT } = require("./deck-frame"); + +function count(reading) { + return [...reading.replace(/\s/g, "")].length; +} + +function measure(pages, wpm) { + const rows = pages.map((p) => { + const chars = count(p.reading); + return { no: p.no, title: p.title, chars, seconds: Math.round((chars / wpm) * 600) / 10, planned: p.seconds }; + }); + const chars = rows.reduce((a, r) => a + r.chars, 0); + const seconds = Math.round(rows.reduce((a, r) => a + r.seconds, 0) * 10) / 10; + const planned = rows.every((r) => r.planned == null) ? null : rows.reduce((a, r) => a + (r.planned || 0), 0); + return { wpm, pages: rows, total: { pages: rows.length, chars, seconds, planned } }; +} + +function mmss(sec) { + const s = Math.round(sec); + return `${Math.floor(s / 60)}分${String(s % 60).padStart(2, "0")}秒`; +} + +function table(m) { + const lines = [ + `| # | 見出し | 字数 | 尺(秒) | 配分(秒) |`, + `| ---: | --- | ---: | ---: | ---: |`, + ]; + for (const r of m.pages) lines.push(`| ${r.no} | ${r.title.replace(/\|/g, "\\|")} | ${r.chars} | ${r.seconds.toFixed(1)} | ${r.planned == null ? "—" : r.planned} |`); + const t = m.total; + lines.push(`| 合計 | ${t.pages} ページ(${m.wpm} 字/分) | ${t.chars} | ${t.seconds.toFixed(1)}(${mmss(t.seconds)}) | ${t.planned == null ? "—" : t.planned} |`); + return lines.join("\n"); +} + +function parseArgs(argv) { + const a = { file: null, wpm: 350, limit: null, json: false }; + for (let i = 0; i < argv.length; i++) { + const v = argv[i]; + if (v === "--json") a.json = true; + else if (v === "--wpm" || v === "--limit") { + const n = Number(argv[++i]); + if (!(n > 0)) throw new DeckError(`${v} は正の数`, EXIT_INPUT); + a[v.slice(2)] = n; + } else if (v.startsWith("--")) throw new DeckError(`知らない引数: ${v}`, EXIT_INPUT); + else if (!a.file) a.file = v; + else throw new DeckError(`台本は 1 つだけ渡す: ${v}`, EXIT_INPUT); + } + if (!a.file) throw new DeckError("usage: node lib/script-time.js <台本> [--wpm 350] [--limit <分>] [--json]", EXIT_INPUT); + return a; +} + +function main() { + try { + const a = parseArgs(process.argv.slice(2)); + const m = measure(readScript(a.file), a.wpm); + if (a.limit != null) m.limit = { minutes: a.limit, over: m.total.seconds > a.limit * 60 }; + console.log(a.json ? JSON.stringify(m) : table(m)); + if (m.limit && m.limit.over) { + console.error(`尺が上限を超えた: ${mmss(m.total.seconds)} > ${a.limit} 分`); + process.exit(1); + } + } catch (e) { + console.error(e.message); + process.exit(e instanceof DeckError ? e.code : 1); + } +} + +if (require.main === module) main(); + +module.exports = { count, measure, table }; diff --git a/docs/presentations/2026-11-03-neural-development-factory/slides/package-lock.json b/docs/presentations/2026-11-03-neural-development-factory/slides/package-lock.json new file mode 100644 index 000000000..7d73ee931 --- /dev/null +++ b/docs/presentations/2026-11-03-neural-development-factory/slides/package-lock.json @@ -0,0 +1,171 @@ +{ + "name": "slide-deck", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "slide-deck", + "version": "0.0.0", + "dependencies": { + "pptxgenjs": "4.0.1" + } + }, + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==", + "license": "ISC" + }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz", + "integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/pptxgenjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz", + "integrity": "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==", + "license": "MIT", + "dependencies": { + "@types/node": "^22.8.1", + "https": "^1.0.0", + "image-size": "^1.2.1", + "jszip": "^3.10.1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + } + } +} diff --git a/docs/presentations/2026-11-03-neural-development-factory/slides/package.json b/docs/presentations/2026-11-03-neural-development-factory/slides/package.json new file mode 100644 index 000000000..24fc4a06d --- /dev/null +++ b/docs/presentations/2026-11-03-neural-development-factory/slides/package.json @@ -0,0 +1,13 @@ +{ + "name": "slide-deck", + "version": "0.0.0", + "private": true, + "description": "pptxgenjs で pptx を作る生成スクリプト", + "scripts": { + "build": "node build-deck.js deck.pptx", + "time": "node lib/script-time.js ../script.md" + }, + "dependencies": { + "pptxgenjs": "4.0.1" + } +} diff --git a/docs/presentations/2026-11-03-neural-development-factory/slides/theme.json b/docs/presentations/2026-11-03-neural-development-factory/slides/theme.json new file mode 100644 index 000000000..dc8cc5047 --- /dev/null +++ b/docs/presentations/2026-11-03-neural-development-factory/slides/theme.json @@ -0,0 +1,26 @@ +{ + "colors": { + "background": "0A0E1A", + "panel1": "131A2B", + "panel2": "1E2740", + "rule": "2A3550", + "text": "FFFFFF", + "ink": "CBD5E1", + "mute": "8391A7", + "accent": "22D3EE", + "accentSoft": "113A4A", + "warn": "FF4F8B", + "caution": "FFB547", + "header": "7C5CFF", + "onHeader": "FFFFFF" + }, + "sizes": { + "title": 22, + "lead": 12.5, + "sectionTitle": 36, + "sectionSub": 15, + "coverTitle": 34, + "coverSub": 15 + }, + "copyright": "NDF — github.com/devbasex/ai-plugins" +} diff --git a/docs/presentations/README.md b/docs/presentations/README.md index 69fd164cd..e2845e9d9 100644 --- a/docs/presentations/README.md +++ b/docs/presentations/README.md @@ -28,6 +28,7 @@ | 資料 | 内容 | 想定時間 | |---|---|---| +| [2026-11-03-neural-development-factory/](2026-11-03-neural-development-factory/README.md) | AI が回すソフトウェア工場(2026-10-31 の版を更新)。NDF の開発ループ、Neural Development Factory のループの画面とオーガナイザーを足した(社外の勉強会・登壇向け) | 35〜40分 / 30ページ | | [2026-10-31-neural-development-factory/](2026-10-31-neural-development-factory/README.md) | AI が回すソフトウェア工場。Software Factory の考え方、NDF(工場のラインをプラグインに)、Neural Development Factory の構想と画面モック(社外の勉強会・登壇向け) | 30〜40分 / 31ページ | | [2026-10-17-skill-plugin-development/](2026-10-17-skill-plugin-development/README.md) | 独自プラグイン開発の薦め。NDF の機能を例に、自分用のスキルを作り、プラグインにして配り、育てることを薦める(社外の勉強会・登壇向け) | 30〜35分 / 21ページ | | [2026-08-06-ai-plugins-intro.md](2026-08-06-ai-plugins-intro.md) | ai-plugins / NDF v5.0.0 の個別機能紹介。各スキルに何が書いてあり、何を重視しているかを扱う | 15分 / 15枚 | diff --git a/docs/specifications/README.md b/docs/specifications/README.md index 21bf2906f..76ebe80d3 100644 --- a/docs/specifications/README.md +++ b/docs/specifications/README.md @@ -106,6 +106,7 @@ | [ndf-ask-guard.md](ndf-ask-guard.md) | `AskUserQuestion` を PreToolUse の hook(問い合わせの guard)で判定し、区分(`metadata.source`)の無い・許されない問いを止めて AI に決めさせる理由と境界(承認ゲートとレッドラインは宣言で外せない、壊れた宣言は丸ごと既定に戻す、relay の `question open` も同じ判定を使う、止めた問いでは待ちの通知を送らない)、宣言 `.ndf/ask.json` と記録 `ask.jsonl` の形、`ask.py decide` / `summary` の契約。区分と手順は `development-workflow` の `references/asking.md` が正 | | [ndf-factory-runs-and-runner.md](ndf-factory-runs-and-runner.md) | Neural Development Factory: リポジトリの登録(GitHub App の利用者の認可と候補の証明)・キーの持ち込み・課題の候補・実行の制御(開始の判定と実行の枠・起動・停止の知らせと見回り・取り消し・再開の要求・形態 local の渡す口・打ち切りの時間と token の期限)・ランナー(手順・翻訳・送信・状態の保存と復元・形態ごとの claude の呼び方)・runs.yaml の経路。#2186 | | [ndf-factory-runs-state-and-data.md](ndf-factory-runs-state-and-data.md) | Neural Development Factory: スプリントの実行と実行ジョブの状態の移り・処理の流れ・実行の表・置き場の値・環境変数(ndf-factory-runs-and-runner.md から分けた)。#2186 | +| [ndf-factory-screen-map.md](ndf-factory-screen-map.md) | Neural Development Factory: Web の画面の地図(経路・役割・状態・載る機能)・ナビゲーション・役割ごとの主な動線・見本の画面・ダミーで起動する方法。#2382・#2347 | | [ndf-supervise-work-step-web-tools-and-report-stop.md](ndf-supervise-work-step-web-tools-and-report-stop.md) | supervise の work のステップが WebFetch・WebSearch で外部の文書を読み、読めないときと「判断が要る / できなかった」の報告で起こし直さずに止まること | | [ndf-deps-require-groups-check.md](ndf-deps-require-groups-check.md) | 依存の宣言の検査(project-mvv.py propose / vet の deps.require と、構造チェックの require-groups をエントリポイントから関数の中の import までたどること) | | [ndf-supervise-queue-resume-and-gate-evidence.md](ndf-supervise-queue-resume-and-gate-evidence.md) | pace: normal の承認ゲートの後をキュー 1 回で流すこと、queue --resume で止まったプランを続けること、承認ゲートの記録、new sprint --absorb | @@ -119,5 +120,11 @@ | [ndf-entrypoint-contracts.md](ndf-entrypoint-contracts.md) | NDF のエントリポイントの契約(plugins/ndf/contracts/ の契約の一覧・構造チェック check-ndf-contracts.py と例外リストのラチェット・.ndf/ の設定を読む木を lib/decl.py で解くこと・本番のブランチとテストのコマンドの 1 か所化と移行の読み。#2336) | | [ndf-worker-stage-skills.md](ndf-worker-stage-skills.md) | プランの worker と ndf:worker へ、ステップの工程に要る規則の Skill だけを届ける仕組み。Skill の対応表(stage-skills.json)と規則の Skill の一覧、プランの skills・skills_add 欄と流す前の検査、本文を system プロンプトへ入れる渡し方と起動指示の「読む Skill」、進捗ログの kind: skills の行、stage-skills.py lookup。規約は supervise_lib/plan.py と agent-layers.md が正 | | [ndf-supervise-verification-mark-after-ok.md](ndf-supervise-verification-mark-after-ok.md) | まとめのプランで「リリース後テスト」の進行の印を、記録を投稿したステップが ok で終わった後に付けること(STAGES_AFTER_OK・record_stage の after_ok・release-verify の worker は進捗記録を打たない)、ほかの工程は始めに付けること、印と記録が食い違いうる経路(#2288・#2356)。プランの外の印の時点は release-verification が正 | +| [ndf-relay-multi-session-supervisor.md](ndf-relay-multi-session-supervisor.md) | 同じコンテナで最初のラッパーが統括役を兼ね、2 本目以降の claude・`relay.py spawn`・起動のブロックで名前つきのラッパーを tmux に起動し、`ls` で一覧し、`stop` / `restart` / `attach` で 1 本ずつ扱う入出力の契約・状態の置き場所・決定と理由。手順は `development-workflow` の `references/relay.md` の「複数のループを並べる(統括役)」が正 | +| [ndf-backlog-refinement-depths.md](ndf-backlog-refinement-depths.md) | 棚卸しの 3 つの深さ(差分の棚卸し・見直し・大整理)の起動の判定・見直しの持ち主・ニーズとシーズの調査・起票と付け直しと戻しについて、`upkeep.py` の副命令の契約、`.ndf/backlog/` の記録、`materials`・`depths` の設定と、そう決めた理由。手順は `backlog-refinement` の SKILL.md と `references/depths.md`・`review.md`・`research.md`・`overhaul.md` が正 | +| [ndf-context-restart-policy.md](ndf-context-restart-policy.md) | 再起動の方針(現行方式と試行方式)の適用値の解決と優先順位、工程境界の帯・容量・費用の判定、判定の記録と「引継ぎ開始」「復元完了」の印、`context-policy.py` の副命令と終了コード、`.ndf/context.json`・状態ディレクトリのファイル・環境変数、作業ごとの集計の区分。手順は `development-workflow` の `references/context-window.md` の「再起動の方針」が正 | +| [ndf-factory-dev-loops.md](ndf-factory-dev-loops.md) | Neural Development Factory: 開発ループ(集約・状態の写し方・周回の開始)・同じリポジトリで並行する実行の取り合い(課題・マイルストーンの持ち分・NDF の版・同時の数)・移行の埋め戻し・ランナーの区間と ndf-next・形態 local の利用上限・手元のランナーの同時のジョブの数・loops.yaml の経路。#2347 | +| [ndf-factory-dev-loops-data-and-contracts.md](ndf-factory-dev-loops-data-and-contracts.md) | Neural Development Factory: 開発ループの実行の表の行・移行の済みの行と埋め戻し・環境変数と、実行ジョブの指示・進捗の事象・ndf-next の受け渡しの形・手元のランナーの申告・loops.yaml の経路の契約(ndf-factory-dev-loops.md から分けた)。#2347 | +| [ndf-factory-ui-components.md](ndf-factory-ui-components.md) | Neural Development Factory: Web の画面の部品の一覧と使い分け・デザインのトークン・端末の幅・文言と書式(国際化)・API クライアントとダミー・つながりの状態・常に成り立つ条件・検査(check:source・check:bundle・E2E)。#2382 | Skill の挙動仕様はここに置かない。Skill に関する詳細は対象 Skill の `SKILL.md` を参照する。 diff --git a/docs/specifications/ndf-ask-guard.md b/docs/specifications/ndf-ask-guard.md index 6e3e8205d..23a30e4fb 100644 --- a/docs/specifications/ndf-ask-guard.md +++ b/docs/specifications/ndf-ask-guard.md @@ -14,7 +14,7 @@ Claude Code の `AskUserQuestion` を PreToolUse の hook(問い合わせの g | 4 | relay の `question open` も同じ判定で `deny` を得て、質問のシグナルファイルを作らない | | 5 | AI は A を選び、`ask.py decide --ref ae1533b36fcd --answer A --reason "…"` で代わりの判断の行を足して作業を続ける | -設計 Pull Request のマージの承認は、入力に `"metadata": {"source": "ndf-gate:1"}` を付ければ、どの宣言でも利用者へ出る。 +設計 Pull Request のマージの承認は、入力に `"metadata": {"source": "ndf-gate:1"}` を付ければ、どの宣言でも利用者へ出る。並列の計画のレーンと一時レーンでは、判定が通す問いを hook がオーガナイザーへ送り、オーガナイザーがまとめて利用者へ出す(下の「並列の計画のレーンの問い」)。 **手順・区分の当てはめ方・宣言の書き方は Skill が正である。** ここに書き写さない。 @@ -103,7 +103,7 @@ Claude Code の `AskUserQuestion` を PreToolUse の hook(問い合わせの g | --- | --- | | `plugins/ndf/scripts/lib/ask_policy.py` | 宣言の読み取りと検証(`load_ask_declaration`・`parse_declaration`)、根の決定(`project_root`)、区分の取り出しと解析(`source_of`・`parse_source`)、判定(`judge_ask`)、hook の入力からの判定(`judge_input`)、人へ回してよい区分の一覧(`passing_sources`)。標準ライブラリだけで書き、ファイルを読む以外の副作用を持たない | | `plugins/ndf/scripts/lib/ask_ledger.py` | 記録の置き場(`ask_ledger_path`)、行の組み立て(`verdict_row`・`decision_row`)、1 行の追記(`append_ask_row`)、読み出し(`read_ask_rows`)、判定の行の引き当て(`find_row`)、集計(`summarize_asks`)。標準ライブラリだけで書く | -| `plugins/ndf/scripts/hook_lib/ask_guard.py` | `ask_decision(ev)` が判定から hook の出力(`deny` の理由・`additionalContext` の案内)を作り、判定の行を追記する。追記の失敗は飲み、どの失敗も `None`(通す)を返す | +| `plugins/ndf/scripts/hook_lib/ask_guard.py` | `ask_decision(ev)` が判定から hook の出力(`deny` の理由・`additionalContext` の案内)を作り、判定の行を追記する。判定が通す問いを並列の計画のレーンか一時レーンが出したときは、`forward_to_organizer` が `parallel.py request-ask` でオーガナイザーへ送り、送れたら保留の手順を添えた `deny` を返す。追記と送りの失敗は飲み、どの失敗も `None`(通す)を返す | | `plugins/ndf/scripts/hook.py` の `_guards`・`dispatch` | `--runtime claude`・PreToolUse・`AskUserQuestion` のときだけ、締め切りの中で `ask_guard.ask_decision` を打つ(`Deadline.current` は `ask-guard`)。出力に `deny` があれば待ちの通知(`wait_notify.hook`)を打たない | | `plugins/ndf/scripts/ask.py` | 副命令 `decide`(代わりの判断の行を書く)と `summary`(数える)。出力は統一の JSON レポート(`lib/step_result.py`) | | `plugins/ndf/scripts/relay_lib/mark.py` | `question_denied(raw)` が入力を `judge_input` に掛け、`cmd_question("open")` は `deny` の問いにシグナルファイル(`question`)と時刻(`asked.json`)を書かない。差し戻しの文 `WAIT_REASON` が区分の付け方を案内する | @@ -215,8 +215,8 @@ classDiagram | # | 集約 | 条件 | 破れたときの扱い | | --- | --- | --- | --- | -| I1 | 問い合わせの判定 | 区分が `ndf-gate:1`・`ndf-gate:2` なら、どの宣言でも `deny` にならない | テストが落ちる | -| I2 | 問い合わせの判定 | 区分が `ndf-ask:redline:<番号>`(`C1`〜`C8`・`P<数>`・`R<数>`)なら、どの宣言でも `deny` にならない | テストが落ちる | +| I1 | 問い合わせの判定 | 区分が `ndf-gate:1`・`ndf-gate:2` なら、どの宣言でも `deny` にならない。例外は並列の計画のレーンか一時レーンの問いをオーガナイザーへ送れたときで、このときの `deny` は保留の案内である | テストが落ちる | +| I2 | 問い合わせの判定 | 区分が `ndf-ask:redline:<番号>`(`C1`〜`C8`・`P<数>`・`R<数>`)なら、どの宣言でも `deny` にならない。例外は I1 と同じく、オーガナイザーへ送れたときである | テストが落ちる | | I3 | 問い合わせの判定 | `mode` が `enforce` のとき、区分が無い・形に合わない・許す区分(`allow` と `extra[].name`)に無い問いは `deny`、ある問いは `allow` になる | テストが落ちる | | I4 | 問い合わせの宣言 | 宣言が無い・読めない・形が誤っているとき、`enforce`・組み込みの 4 区分・しきい値 `0` で判定し、読めなかったときはその箇所(`problem`)を判定の出力と記録に添える | テストが落ちる | | I5 | 問い合わせの判定 | `mode` が `off` なら hook は何も出さず記録も書かない。`advise` なら `deny` を出さない | テストが落ちる | @@ -236,7 +236,7 @@ classDiagram | E1 | AI が `AskUserQuestion` を呼んだ | AI(Claude Code のセッション) | `hook.py` と relay の `question open`(PreToolUse) | — | | E2 | hook が宣言を読んだ | `ask_policy.load_ask_declaration` | `judge_ask` | 無い: 既定。読めない・誤り: 既定と箇所(I4) | | E3 | hook が問い合わせを通した(`allow`・`advise`) | `ask_guard.ask_decision` | Claude Code(問いを出す)・待ちの通知・relay(シグナルファイルを作る) | — | -| E4 | hook が問い合わせを止めた(`deny`) | `ask_guard.ask_decision` | AI(理由と案内を受け取る) | 内部の失敗は通す側へ倒す(I8) | +| E4 | hook が問い合わせを止めた(`deny`) | `ask_guard.ask_decision` | AI(理由と案内を受け取る。オーガナイザーへ送ったときは保留の手順) | 内部の失敗は通す側へ倒す(I8) | | E5 | hook が判定を記録に書いた | `ask_ledger.append_ask_row` | 振り返り・Factory のランナー | 書けなくても判定は変えない | | E6 | AI が止めた理由を受け取り、自分で判断した | AI | (E7 で記録に残る) | 区分を付け替えて出し直したら、hook は区分だけを見て判定する | | E7 | AI が判断と理由を記録した | `ask.py decide` | 振り返り・Factory のランナー | 記録しないまま進むと、E5 の止めた行だけが残り `unanswered` に数えられる | @@ -267,6 +267,10 @@ classDiagram `allow` の案内は 5 と 6 で、`kind` が `allowed`(宣言が許した区分)のときだけ先頭に「区分 `` で人へ回した。区分の 当てはめが誤っていれば取り下げて自分で決める」を足す。承認ゲートとレッドラインにはこの 1 行を付けない。 +### 並列の計画のレーンの問い + +判定が通す問い(`gate`・`redline`・`allowed`)を並列の計画のレーンか一時レーン(`NDF_RELAY_NAME` が計画のレーンの名前か、役割のファイルが `helper`。`parallel_files.lane_name_in_plans` がファイルだけを読んで決める)が出したとき、hook は `parallel.py request-ask <計画名> --lane <名>` を上限 8 秒で打ってオーガナイザーへ送り、依頼の `id` が返れば保留の手順を添えた `deny` を返す。送れなければ今どおり通す。止める問い(`deny`・`advise`)は送らない。保留の手順の正本は `asking.md` の「並列の計画のレーン」にある。 + ### `ask.py` | 副命令 | 入力 | 出力(標準出力の 1 行の統一 JSON レポート。`tool: ask`) | 終了コード | diff --git a/docs/specifications/ndf-backlog-refinement-depths.md b/docs/specifications/ndf-backlog-refinement-depths.md new file mode 100644 index 000000000..7d785593e --- /dev/null +++ b/docs/specifications/ndf-backlog-refinement-depths.md @@ -0,0 +1,364 @@ +# 棚卸しを 3 つの深さで回し、ニーズとシーズの調査から課題を足す + +課題の棚卸し(`backlog-refinement`)には 3 つの深さがある。毎スプリントの差分の棚卸し(`diff`)、数回に 1 回の +見直し(`review`)、十数回に 1 回の大整理(`overhaul`)である。見直しは、プロジェクトが宣言した材料をもとに +ニーズとシーズを調べて起票する。大整理は、未完了の課題を原因の型で分けて根本原因を探し、課題を組み直す。 +見直しと大整理を起動するかは、まとめのプランの中でスクリプトが判定する。見直し・大整理・待ち時間の調査は、1 つのセッション +(見直しの持ち主)だけが行う。この文書は、`upkeep.py` の副命令の契約と受け渡しのファイル、それぞれをそう決めた理由を残す。 + +**手順は各 Skill の文書が正である。** 深さの表・見直しの 7 つの手順・大整理の 6 つの手順・報告の形をここへ書き写さない。 + +| 何を読むか | 正本 | +| --- | --- | +| 深さの表・引数 `--depth`・手順 1 の経路の深さごとの違い | `plugins/ndf/skills/backlog-refinement/SKILL.md` の「深さ」 | +| 起動の条件の表・見直しの持ち主の決まり・`.ndf/backlog/` の記録 | `plugins/ndf/skills/backlog-refinement/references/depths.md` | +| 見直しの 7 つの手順(`order` の値)・起票の決まり・報告の `metrics` | `plugins/ndf/skills/backlog-refinement/references/review.md` | +| 材料の宣言のキーと調査の項目・利用の記録を読む範囲・待ち時間の調査の決まり | `plugins/ndf/skills/backlog-refinement/references/research.md` | +| 大整理の 6 つの手順・付け直しと戻し・後回しの再評価・報告に足すキー | `plugins/ndf/skills/backlog-refinement/references/overhaul.md` | +| plan に足すキー(`depth`・`owner`・`create`・`reparent`・`policy_verdict`) | `plugins/ndf/skills/backlog-refinement/scripts/upkeep_actions.py` の冒頭 | +| 材料の宣言の形 | `plugins/ndf/skills/development-workflow/schemas/project.schema.json`(`materials`) | +| 材料の宣言の案を出す手順 | `plugins/ndf/skills/development-workflow/references/project-analysis.md` | +| 周回の打つ順に待ち時間の調査を並べること | `plugins/ndf/skills/development-workflow/references/loop.md`・`references/waiting.md` | + +## 概要 + +**例(ai-plugins の開発ループ、2026-11-01 時点の値)。** open は 185 件で、そのうち 80 件が後回しのマイルストーンにあった。 +周回ごとの棚卸しは 206〜224 秒・1.41〜1.61 ドルかかり、1 回 10 件では持ち越しが約 160 件まで溜まった。今の形では、周回のまとめは次のように動く。 + +| 時点 | 振る舞い | +| --- | --- | +| 課題を閉じた(`close` が 0) | `refine` が `/ndf:backlog-refinement --depth diff` を通す。候補は差分の経路だけで、順位の表・前倒しと後ろ倒し・マイルストーンの設定・やらない・起票・付け直しは `apply` が `wrong_depth` で退ける | +| 差分の棚卸しを終えた | `refine-record` が `depths.jsonl` に `diff` の `ran` の行を足す。持ち越し 160 件は行の `values.carry` に入る | +| 大整理の判定(`overhaul-due`) | 前の大整理から 12 回に満たず、溜まり具合の条件にも当たらない。`not_due` の行を残し、終了コード 3 で `review-due` へ飛ぶ | +| 見直しの判定(`review-due`) | 持ち越し 160 件が上限 10 件の 10 倍を超えるので、`carry` で起動する(`due` の行を残して 0) | +| 持ち主の宣言(`review-claim`) | `owner claim --what review --keepalive 5400 --pid $PPID` が宣言を取る。別のセッションの有効な宣言があれば 10 を返し、`review-skipped` が `skipped` の行を残して終わる | +| 見直し(`review`) | 後回しを除く open の 105 件を、更新の古い順に上限なしで見る。起票は出典つきで 5 件まで。MVV 判定が「従う」でない課題には `needs-decision` が付く | +| 見直しを終えた | `report --depth review` が報告の土台を組み、`depth record --depth review` が行を足す。`review-release` が宣言を手放す | +| 次の周回の待ち | conductor は `supervise.py wait` と並べて、`loop sprint` が返す `metrics.research` の 1 行を背景で打つ。調査は記録を書くだけで、課題には触らない | + +**コンテキストは 2 つで、関係は顧客 / 供給者である。** 開発ワークフロー(`ndf-workflow`)が顧客で、周回・まとめ・止まる条件・予算を持つ。 +課題の棚卸し(`ndf-issue-upkeep`)が供給者で、深さ・候補・調査の項目・付け直し・大方針を持つ。開発ループが読むのは、棚卸しの +起動の判定の終了コードと、深さの記録・調査の費用の記録だけで、深さの中身は知らない。 + +## 用語 + +棚卸しの深さ・差分の棚卸し・見直し・大整理・後回しの再評価・ニーズの調査・シーズの調査・調査の記録・調査の項目・材料の宣言・ +見直しの持ち主・大方針・柱・付け直しの記録・原因の型・寿命の窓の定義は、設定の用語集(`.ndf/glossary.json` の `document` が指す +[docs/glossary.md](../glossary.md))にある。 + +## 決定と理由 + +| 決定 | 理由 | +| --- | --- | +| 見直しと大整理は、別の Skill にせず `backlog-refinement` の深さ(`--depth review` / `overhaul`)にする | 区分・見積の段階・`apply` を 1 か所から使え、起票の上限と MVV 判定も 1 つの Skill の中で守れる。入口は `/ndf:backlog-refinement --depth overhaul` | +| `--depth` を省いたら差分の棚卸しにする | 既存の呼び出し(まとめの `refine`・人の呼び出し)が、そのまま軽い棚卸しで動く。省いたときに 5 つすべてを行うと、指示を直し忘れた呼び出しが重いまま残る | +| 差分の棚卸しではマイルストーンを設定しない。`no-milestone`・`closed-milestone` の経路は見直しだけが拾う | どのマイルストーンに入れるかは、全体を見ない回では決められない。周回の中で起票した課題は次の周回まで選ばれず、課題が尽きる前に見直しが入るので、着手は止まらない | +| 回数と溜まり具合は、深さの記録(`depths.jsonl`)から数える | 棚卸しのたびに 1 行足すので、`--loop` を使わない進め方でも同じ数え方になる。起動しなかった判定も残る | +| 起動の判定はスクリプト(`depth due`)が行う。まとめのプランでは、大整理と見直しを実行条件つきのステップとして `refine` の後ろに置く | 回数を比べるだけの判断を LLM に任せず、判定の記録の形もそろえる。大整理を先に置くのは、両方が起動したときに組み直した後の課題で順位を付けるため | +| 持ち主の宣言は、既存の `run_owner`(寿命の窓は引数)と `locks` の上に作る。寿命の窓 900 秒・生きている印 120 秒 | 同じ役割の部品を 2 つ作らない。窓を 24 時間にすると、中断した持ち主の引き取りまでが長すぎる | +| queue の work ステップの持ち主は `--pid "$PPID"`(`supervise.py`)で宣言し、`--keepalive` の見張りに `touch` させる | run ステップのシェルはすぐ終わるので、その pid で宣言すると次のステップの前に無効になる。work ステップの中には `touch` を打つ手順が無い | +| 見直し・大整理の本体の成否によらず、`release` のステップを通す | 宣言が残ると見張りが打ち切りまで `touch` し続け、続く見直しの `claim` と他のループの調査が `not_owner` で締め出される | +| 待ち時間の調査はスクリプト(`research run`)が 1 項目ずつ `claude -p` を起こす | 止める時点・空きの判定・上限・記録の形をスクリプトが持ち、LLM には 1 項目の調べと要約だけを任せる | +| 課題の一覧への書き込み(起票・付け直し)は `apply` に寄せる | 深さ・持ち主・件数の上限・出典と MVV の当たる所とラベル・前の状態の記録を 1 か所で確かめられる | +| 利用の記録は、スクリプトが宣言のコマンドを打ち、マスクと `secret_redact` を通した集計値だけを LLM へ渡す。宣言が無ければ何も読まない | 範囲の外を読まないことを、手順の文ではなくコードで守る。何を集計値とみなすかは、プロジェクトごとに機械で決められない | +| 1 回の見直しの起票は既定 5 件まで。MVV 判定が「従う」でない課題には既存の `needs-decision` を付ける | 見直しの間の 3 周回で着手するのは 6〜9 件なので、起票が着手を上回らない。順位付けと選定は `needs-decision` をすでに外すので、新しい除外は要らない | +| 見直しの候補は上限で切らず、更新の古い順に並べる | 持ち越しが約 160 件あり、10 件ずつでは追いつかない | +| 選べる課題が尽きたら、止まる前に見直しを 1 回だけ挟む(`review_first`)。答えを待つ作業(保留の周回か、オーガナイザーへ送って答えの無い設計の承認)が残るときは挟まず `waiting_only` で止まる | 調査からの起票を試してから止まる。1 回に限るのは、起票が `needs-decision` に回り続けると見直しと選定の間を回り続けるため | +| 溜まり具合の閾値は open の増加 30%・持ち越しが上限の 10 倍・型の割合 30%・3 回の連続 | 1 本のループ(4 周回)と 2026-11-01 の分類(最多の型 A が不具合の 31%)から置いた値で、最初の 3 回の見直しの `depths.jsonl` で見直す | +| 材料は `.ndf/project.json` の `materials`、頻度と上限は `.ndf/backlog.json` の `depths` に置く | 材料の宣言は解析の 3 手(measure → 答え → write)と「手で書いた値を変えない」規則を使える。頻度は `candidate_limit` と同じ棚卸しの調整の値である | +| ライブラリは `seeds.libraries` に宣言したものだけを調べ、1 行を「変化」と「競合」の 2 つの項目にする | すべての依存を調べると、費用が依存の数に比例して増える | +| 付け直しでは、最初の書き込みの前に対象の全件の前の状態を `before.json` に書き、戻しは 1 つのコマンドにする | 1 件ずつ書くと、書き込みの途中で落ちたときに前の状態の無い課題が残る | +| 調査の記録のファイル名は、英数字でない名前に元の名前のハッシュを足す | 英数字以外を `_` に置き換えるだけでは、日本語の名前どうしが同じファイル名になり、記録が上書きされる | +| 調査の費用は、結果を捨てた呼び出しも含めて呼び出しごとに `calls.jsonl` へ残す | 支払った費用を予算に数える。背景の調査は周回を締めた後にも終わるので、記録から数え直して精算する | +| 大整理の固定の入力は、2026-11-01T04:23Z 時点の open の一覧を作り直したもので、`count` の値だけを試験で縛る | 大方針(#2335)のときの分類の記録はリポジトリに無い。分類は LLM の判断で、同じ文字列は返らない | + +## 仕様 + +### 常に成り立つ条件 + +- **有効な持ち主の宣言は、同時に 1 つだけである。** 宣言は錠(`owner.json.lock`)の中でだけ作る・書き換える +- **宣言が有効なのは、次の 3 つがそろうときだけである。** 終わった時刻が無い。最後に動いた時刻から寿命の窓(`depths.owner.ttl`)の内にある。 + 同じホストなら pid が生きている(別のホストの宣言は窓だけで決める)。生きていて窓の内の宣言を、別のセッションは引き取らない +- **深さが `review` / `overhaul` の `apply` が書くのは、plan の `owner` が有効な宣言の `run` と一致するときだけである** +- **差分の棚卸しの `apply` は、前倒し・後ろ倒し・やらない・マイルストーンの設定・順位の表・退けた移動・起票・付け直しを反映しない** +- **1 回の見直しで作る課題(`kind: policy` を除く)は、`depths.review.issue_limit` 件以下である。** 起票には出典が 1 つ以上と + MVV の当たる所があり、区別のラベルが付く。`mvv_verdict` が `follow` でなければ `needs-decision` も付く +- **付け直しは、大方針の MVV 判定が `follow` で、深さが `overhaul` のときだけ行う。** 最初の書き込みの前に、対象の全件の前の状態が `before.json` にある +- **調査の記録は、出典と調べた日時がそろった結果だけである。** 調査は課題を起票・クローズせず、ラベル・マイルストーン・順位も変えない +- **利用の記録は、`scope.aggregate_only: true` の宣言の行のコマンド出力だけを、マスクを通してから読む。** 宣言が無ければ読まない +- **待っていたプランが終わった後と、並列の上限に空きが無いときには、調査は新しい項目を始めない** +- **見直しの候補に、後回しのマイルストーン(`materials.deferred_milestones`)の課題は入らない。** 差分の棚卸しの候補には入る +- **`depths.jsonl` と `causes.jsonl` は行を足すだけで、書き換えない** + +### 集約 + +| 集約 | 書き換えてよいもの | 根(置き場) | 値オブジェクト | +| --- | --- | --- | --- | +| 深さの記録 | `upkeep.py depth due` / `depth record` | `.ndf/backlog/depths.jsonl` | 深さ・事象・起動した条件・判定の値・open の前後 | +| 型の記録 | `upkeep.py causes record` | `.ndf/backlog/causes.jsonl` | 主の型・従の型 | +| 見直しの持ち主の宣言 | `upkeep.py owner`(`run_owner` と `locks` の上) | `.ndf/backlog/owner.json` | 持ち主(`run`・`pid`・`host`・`loop`)・最後に動いた時刻・引き取り元 | +| 調査の記録 | `upkeep.py research run` | `.ndf/backlog/research/<側面>--<対象>.item.json` | 出典・調べた日時・見つけたこと・費用と秒 | +| 課題の一覧(GitHub) | `upkeep.py apply`(深さが見直し・大整理なら持ち主の実行だけ)・`upkeep.py revert` | 課題 | 本文・ラベル・マイルストーン・親 | +| 付け直しの記録 | `upkeep.py apply`(`reparent`) | `.ndf/backlog/overhaul//before.json` | 課題ごとの親・マイルストーン | +| 材料の宣言 | 人(`project-decl.py write` は欠けたキーへ案を書くだけ) | `.ndf/project.json` の `materials` | 側面ごとの読む先と範囲 | + +### ドメインイベント + +| イベント | 発生元 | 受け手 | +| --- | --- | --- | +| 差分の棚卸しを終えた | まとめの `refine` → `refine-record`(`depth record --depth diff`) | 深さの記録 | +| 見直し・大整理の起動を判定した | `depth due review\|overhaul`(`due` / `not_due` の行) | まとめのプラン・深さの記録・`report --depth` の `trigger` | +| 持ち主になった / 引き取った | `owner claim`(`taken_from`) | 調査・見直し・大整理 | +| 持ち主になれず飛ばした | `<深さ>-skipped`(`depth record --event skipped`) | 深さの記録 | +| 調査の項目を 1 つ調べ終えた | `research run` | 調査の記録・`calls.jsonl`・宣言の `seen_at` と `item` | +| 待っていたプランが終わり、調査を止めた | `research run`(`--until` のファイルを見た) | 宣言の `finished_at` | +| 宣言の無い側面を飛ばした | `research plan` / `report --depth` の `skipped_aspects` | 見直しの報告・深さの記録 | +| 課題を起票した | `apply`(`create`) | `apply` の結果の `created`・見直しの報告 | +| 課題を柱の子へ付け直した | `apply`(`reparent`) | 付け直しの記録・大整理の報告 | +| 付け直しを戻した | `revert --run` | 課題の一覧 | +| 見直し・大整理を終え、持ち主を手放した | `depth record --depth review\|overhaul`・`owner release` | 深さの記録・周回の記録 | + +### `upkeep.py` の副命令 + +結果はどれも `lib/step_result.py` の形の 1 行の JSON である。終了コードは、既存の表(0 = ok / 10 = 判断が要る / 20 = 続きがある / +1 = 失敗 / 2 = 読めない / 3 = 前提が無い、または正常な否定の結果)の値を使う。 + +| 副命令 | 引数 | 成功 | 失敗・否定 | +| --- | --- | --- | --- | +| `candidates` | 既存の引数に `--depth diff\|review`(既定 `diff`) | `diff`: 経路 `diff-path`・`diff-identifier`・`sub-issue`・`commit-subject`・`manual`・`unscored`(と `--all`)。`review`: 上の経路に `no-milestone`・`closed-milestone` と、後回しを除く open の全件(`all`)を足す。上限で切らず、`updated_at` の古い順に並べる(`metrics.limit` は `null`、`metrics.depth` に深さ) | 既存 | +| `depth due review\|overhaul` | `--loop`・`--lap`・`--no-issue`・`--manual`・`--open N`(省くと GitHub から数える) | 0。`metrics.trigger` と `values`。`due` の行を足す | 3。起動しない(`not_due` の行と判定の値)。記録や設定が読めないときも 3 で、`values.reason: unreadable` の行を残す | +| `depth record` | `--depth diff\|review\|overhaul`(必須)・`--event ran\|not_due\|skipped`(既定 `ran`)・`--trigger a,b`・`--run`・`--report <統一 JSON レポート>`・`--loop`・`--lap`・`--sprint`・`--cost`・`--seconds` | 0。1 行を足す | 2 = `--trigger` の値が条件の一覧に無い | +| `causes record` | `--classes <[{number, type, sub}]>`(必須)・`--depth review\|overhaul`(既定 `review`) | 0。行を足す | 2 = 配列でない・型が `cause_types` に無い | +| `owner claim` | `--what research\|review\|overhaul`(必須)・`--loop`・`--pid`(省くと呼び出し元)・`--keepalive <秒>` | 0(`run`・`taken_from`・`what`、見張りを起こしたら `keepalive_pid`) | 10 = 他の有効な持ち主がいる(`reason: not_owner`・`owner`)/ 3 = 錠を 5 秒で取れない | +| `owner keepalive` | `--run`・`--pid`・`--seconds`(すべて必須) | 0(止まった理由 `released` / `owner_gone` / `expired`) | — | +| `owner touch` | `--run`(必須)・`--item` | 0 | 10 = 宣言が別の実行のもの(引き取られた) | +| `owner release` | `--run`、または `--pid` と `--what` の組 | 0(`--pid --what` は、宣言の pid・ホスト・`what` が一致するときだけ手放す。手放し済みでも 0) | 10 = 別の持ち主の宣言 / 3 = どちらの組も無い | +| `owner status` | — | 0(`owner`・`alive`・`seen_at`・`path`・`error`) | — | +| `research plan` | `--since <前の見直しの at>` | 0(`items`・`todo`・`fresh`・`skipped_aspects`) | 2 = 材料の宣言が読めない | +| `research run` | `--until <完了のファイル>`・`--run <持ち主の run>`・`--since`・`--queue .plans.json`・`--max`(既定 6)・`--max-items`(既定 3)・`--lap`・`--loop`・`--pid` | 0(`run`・`taken_from`・`items`・`cost`・`seconds`・`lap`・`stopped`)。空きが無い・項目が無いときも 0 で `items` が空 | 10 = 他の持ち主がいる(`not_owner`)。項目の失敗はその項目を書かずに次へ進み、3 回続いたら止まる(終了コードは 0、理由は `stopped`) | +| `apply` | plan に `depth`・`owner`・`create`・`reparent`・`policy_verdict` を足す(キーは `upkeep_actions.py` の冒頭) | 既存の `metrics` に `depth`・`wrong_depth`・`created`・`not_created`・`rejected_create`・`create_failed`・`reparented`・`not_reparented`・`reparent_failed`・`revert_command`・`reason` を足す | 3 = 深さが見直し・大整理で持ち主が一致しない(1 件も書かない)/ 2 = `depth` が 3 つのどれでもない / 1 = 起票の失敗、または付け直しの途中の失敗(`revert_command` を必ず載せる) | +| `revert` | `--run`(必須) | 0(`reverted`) | 1 = 戻せなかった課題(`items` と `failed`)/ 3 = `before.json` が無い・読めない・記録のリポジトリと書き込み先が合わない | +| `overhaul snapshot` | `--out `(必須) | 0(`path`・`open`)。`snapshot.json` を書く | 既存(GitHub の読み取り) | +| `overhaul count` | `--snapshot`・`--classes`(必須)・`--out` | 0(`issues`・`unclassified`・`by_type`・`defects`・`defect_share`・`components`・`milestones`・`months`)。`--out` があれば `counts.json` も書く | 2 = 入力が読めない・形が違う | +| `overhaul measure` | `--out` | 0(`items` に宣言ごとの値、`metrics.declared`)。宣言が無ければ飛ばしたと要約に書く | — | +| `report` | 既存の引数に `--depth diff\|review\|overhaul`・`--run`(省くと、宣言の `what` が同じ深さならその `run`)・`--order prune,rank,...` | `--depth` を渡したときだけ `depth`・`run`・`trigger`(最後の `ran` より後の `due` の行)・`order`・`open_before`・`open_after`(常に `null`。手で埋める)・`closed`・`created`・`not_created`・`skipped_aspects`・`research`(`items`・`researched`・`sources`)・`root_cause`・`revert_command` を足す | 既存 | + +- **起票(`create`)は、`kind`・題名・出典・`mvv_basis` のどれかが欠けたら `rejected_create` に並べて作らない。** 本文には `apply` が + 「## 出典」と「## MVV の当たる所」の節を足す。ラベルには `depths.labels` の区別のラベルを足す。打ち直しのときは、記録(`ledger.json`)にある起票を 2 度作らない +- **付け直しの対象は、大方針を同じ plan で起票するならその `mvv_verdict`、前の回に起票したなら `policy_verdict` で決まる。** どちらも + `follow` でないときは `not_reparented` に並べる +- **`revert` は、記録したリポジトリへ書き戻す。** 別のリポジトリを整理した後に、今のリポジトリの同じ番号を書き換えない + +### `project-mvv.py vet --kind issue` + +`--kind` に `issue` を足した。課題の本文(`--body F`)を、プロジェクト MVV の本文と照合する。従うなら 0、 +反する疑いか判定できないなら 10 を返す。見直しのニーズ・シーズと、大整理の大方針がこれにかける(`mvv_verdict` に結果を書く)。 + +### `project-decl.py measure` の `materials` + +測る項目に `materials` を足した(`project_lib/measure_materials.py`。解析器の版 `ANALYZER` は 3)。候補は `question` の項目で返す。 +根拠にするのは README の冒頭 8 行・直接の依存(15 件まで、`alternatives` は空)・解析の SDK の依存(アクセス解析・エラーの監視・製品の分析)である。 +`product` と `needs.usage` の読むコマンド・範囲は人が書く。`write` は手で書いた値を変えない(既存の規則)。 + +### まとめのプランの深さのステップ + +`supervise_lib/sprint_close_steps.py` の `refine_steps` は、`refine`(差分の棚卸し。キーは +[棚卸しを工程表の行にする仕様](ndf-backlog-refinement-stage.md) の「スプリントの終わりのプランの `refine` ステップ」)の後ろに、次のステップを足す。 + +| `id` | 種類 | 中身 | 遷移 | +| --- | --- | --- | --- | +| `refine-record` | run | `depth record --depth diff --sprint <名前>` | → `overhaul-due` | +| `<深さ>-due` | run | `depth due <深さ>` | 3 なら `skip_to` で後へ。0 なら → `<深さ>-claim` | +| `<深さ>-claim` | run | `owner claim --what <深さ> --keepalive 5400 --pid "$PPID"` | 10 なら → `<深さ>-skipped`。0 なら → `<深さ>` | +| `<深さ>` | work(`full: true`、`stage` は `棚卸し`、`timeout` 5400) | `/ndf:backlog-refinement --depth <深さ>` を無人で通す。持ち主の実行は `owner status`、起動した条件は `depths.jsonl` から読む。最後に `depth record` と `owner release` を打つ | 成否によらず(`on_fail` も)→ `<深さ>-release` | +| `<深さ>-release` | run | `owner release --what <深さ> --pid "$PPID"` | 0 でも 10 でも後へ | +| `<深さ>-skipped` | run | `depth record --depth <深さ> --event skipped` | 後へ | + +深さは `overhaul` → `review` の順で、`overhaul` の「後」は `review-due`、`review` の「後」は `end` である。 + +### 開発ループ + +- **`loop select` の `review_first`** は [開発ループの仕様](ndf-workflow-development-loop.md) の選定の手順 6 にある。挟んだ周回は、ループの記録の `review_first`(その時点の周回の数)に残る +- **`loop sprint` は `metrics.research` を返す。** 中身は `upkeep.py research run --root <メインディレクトリ> --until --queue .plans.json --max 3 --lap --loop <名>` + の 1 行で、`--max` はまとめの queue の並列の上限 `QUEUE_MAX`(`lib/sprint_loop_gate.py`)と同じ値である。conductor は `wait` と並べて背景で打つ +- **周回を締めるとき(`close_lap` → `backlog_summary`)**、周回が始まってからの `depths.jsonl` の行を `lap.depths`(`depth`・`event`・`trigger`・`cost`・`seconds`)に写す。 + `calls.jsonl` のうちこのループのその周回の行を集め、`lap.research`(成功した項目の `items`・`cost`・`seconds`)に入れる。すでに締めた周回も数え直す +- **予算の消費(`consumed`)の費用には、`lap.research.cost` も足す** +- **`loop status` は「見直しの持ち主」の行と `metrics.review_owner` を出す**(終わった宣言・宣言なしは「なし」) + +## データ・設定 + +すべて、メインディレクトリの `.ndf/backlog/` に置く(`upkeep_depth.backlog_dir`)。複数の worktree とセッションが同じ場所を見るので、 +持ち主の取り合いはここで決まる。別の clone のセッションとは取り合えない。 + +### `depths.jsonl` + +| 列 | 意味 | +| --- | --- | +| `at` | UTC の時刻 | +| `depth` | `diff` / `review` / `overhaul` | +| `event` | `ran`(行った)/ `due`(起動を判定した)/ `not_due`(起動しない)/ `skipped`(持ち主になれず飛ばした) | +| `trigger` | 起動した条件の配列(`count`・`open_growth`・`carry`・`no_issue`・`manual`・`created_over_closed`・`open_rising`・`cause_share`)。`not_due` は空 | +| `values` | 判定の行は判定に使った値(`count`・`open`・`open_growth`・`carry`・`cause_share: {type, share}`、読めなければ `reason`・`detail`)。`diff` の `ran` の行は `carry`(`candidates.json` の `deferred` の件数) | +| `loop`・`lap`・`sprint` | 空は `--loop` の外 | +| `run` | 持ち主の実行。差分の棚卸しと判定の行は空 | +| `open_before`・`open_after`・`closed`・`created` | `--report` の `metrics` から写す。`diff` の `ran` は `candidates.json` の `open` と `apply.json` の `closed` から写す | +| `skipped_aspects`・`order`・`research` | `--report` の `metrics` にあれば写す | +| `cost`・`seconds`・`report` | 引数の値 | + +回数は「最後のその深さの `ran` の行より後にある、`diff` の `ran` の行の数」で数える。`open_growth` は、前の見直しの `ran` の行の `open_after` と比べる。 +`carry` は、最後の `diff` の `ran` の行の `values.carry` と、候補の上限 × `carry_factor` を比べる。`created_over_closed` / `open_rising` は、 +前の大整理より後の見直しの `ran` の行の、最後の `streak` 件を見る。 + +### `causes.jsonl` + +`{number, type, sub, at, depth}` の行。同じ番号の行が複数あれば最後の行を採る。`cause_share` は、型の記録のうち open の課題で、 +不具合の型(`defect: false` を除く)の中で最も多い型の割合を数える。 + +### `owner.json`(錠は `owner.json.lock`) + +`version: 1`・`kind: review-owner`・`run`(UUID)・`pid`・`host`・`loop`・`what`(`research` / `review` / `overhaul`)・`item`(今の調査の項目)・ +`started_at`・`seen_at`・`finished_at`・`taken_from`(`{run, loop, seen_at}`。終わっていない宣言を引き取ったときだけ)。 +有効さの判定は `lib/run_owner.py` の `alive`(`window` に `depths.owner.ttl`)と `owner_pid_alive` が行う([実行をまたいで残る状態の仕様](ndf-run-state-owner-and-sweep.md))。 + +### `research/` + +| ファイル | 中身 | +| --- | --- | +| `<側面>--<対象>.item.json` | `id`(`<側面>:<対象>`)・`checked_at`・`sources`(`{kind: url\|path\|command, value}` が 1 つ以上)・`findings`(`{title, summary, kind: needs\|seeds\|prune\|quality}`)・`cost`・`seconds`・`lap`・`by: {run, loop}`。対象の名前が英数字・`_.-` だけでなければ、名前の SHA-256 の先頭 10 桁を足す。足す前の版の名前の記録は、`id` が一致するときだけ読む(`read_research_item`) | +| `calls.jsonl` | 呼び出しごとの `{id, at, ok, cost, seconds, lap, by}`。結果を捨てた呼び出しも残す | + +側面は `needs.usage`・`needs.market`・`seeds.library`・`seeds.alternatives`・`seeds.environment`・`method.unused`・`method.hotspot`・`method.failures` の 8 つである。 +`materials` が無い(`{"unknown": ...}` を含む)と、8 つすべてを `材料なし(未宣言)` で飛ばす。Web を読む側面(`needs.market`・`seeds.*`)には +`WebFetch,WebSearch` を、手元を読む側面には `Read,Grep,Glob` を渡す。 + +### `overhaul//` + +| ファイル | 中身 | +| --- | --- | +| `before.json` | `[{number, repo, parent, milestone, at}]`。同じ `run` の打ち直しでは、最初に書いた前の状態を残し、無い番号だけを足す。別のリポジトリの行があれば書かずに止める | +| `snapshot.json` | `{at, open: [{number, title, labels, milestone, body(先頭 2500 字), created_at}], closed: [{number, created_at, closed_at}]}`(`overhaul snapshot --out` の置き場) | +| `counts.json`・`measures.json` | `overhaul count` / `measure` の `--out` に渡したときの値 | + +受け渡しのファイル(`before-json`・`snapshot-json`・`counts-json`・`measures-json`・`research-item-json`・`research-calls-jsonl`)は +`plugins/ndf/contracts/files/` に宣言してある。 + +### 材料の宣言(`.ndf/project.json` の `materials`) + +型は `project_lib/model.py` の `Materials` で、この変更で足した。 + +```mermaid +classDiagram + class Materials { + product: Product? + needs: Needs? + seeds: Seeds? + deferred_milestones: list~str~ + cause_types: list~CauseType~ + code_measures: list~CodeMeasure~ + failures: list~str~ + } + class Product { kind; users? } + class Needs { usage: list~Usage~; market: list~Market~ } + class Usage { name; command; scope: UsageScope } + class UsageScope { aggregate_only: StrictBool; days?; mask: list~str~ } + class Market { name; url?; viewpoints } + class Seeds { libraries: list~Library~; environment: list~Environment~ } + class Library { name; ecosystem?; alternatives; viewpoints } + class Environment { name; url? } + class CauseType { id; name; defect = true } + class CodeMeasure { name; command; types } + Materials --> Product + Materials --> Needs + Materials --> Seeds + Materials --> CauseType + Materials --> CodeMeasure + Needs --> Usage + Needs --> Market + Usage --> UsageScope + Seeds --> Library + Seeds --> Environment +``` + +`cause_types` が無ければ、既定の A〜I(`upkeep_depth.DEFAULT_CAUSE_TYPES`。G 機能と方針・H 文書は `defect: false`)を使う。利用の記録のコマンドには、 +期間 `scope.days` を環境変数 `NDF_USAGE_DAYS` で渡す。出力は `scope.mask` の正規表現に当たる所を `***` にし、`secret_redact` で 400 行・2 万字までに伏せて切る。 + +### 棚卸しの設定(`.ndf/backlog.json` の `depths`) + +キーごとに既定(`upkeep_depth.DEFAULTS`)へ重ねる。知らないキーやオブジェクトでない値は、終了コード 2 になる。 + +| キー | 既定 | 読むところ | +| --- | --- | --- | +| `review.every` / `overhaul.every` | 3 / 12 | `depth due` の `count` | +| `review.open_growth` | 0.3 | `depth due` の `open_growth` | +| `review.carry_factor` | 10 | `depth due` の `carry`(候補の上限との積) | +| `review.issue_limit` | 5 | `apply` の起票の上限 | +| `review.usd` / `review.seconds` | 6 / 1800 | 既定に置いた値。今のスクリプトはこの値で止めない(見直しのステップの打ち切りは 5400 秒) | +| `overhaul.streak` | 3 | `depth due` の `created_over_closed`・`open_rising` | +| `overhaul.cause_share` | 0.3 | `depth due` の `cause_share` | +| `research.item_usd` / `research.item_seconds` | 1 / 900 | 1 項目の上限。超えた結果は書かない | +| `research.grace` | 300 | 完了のファイルが出た後、調べている項目を待つ秒数 | +| `owner.ttl` / `owner.beat` | 900 / 120 | 寿命の窓と、生きている印の間隔(秒) | +| `labels` | `{needs: "needs", seeds: "seeds", policy: "policy"}` | 起票の区別のラベル | + +## エラー処理 + +| 場面 | 振る舞い | +| --- | --- | +| `depth due` が設定・記録・open の件数を読めない | 起動せず 3 を返し、`not_due` の行に `reason: unreadable` を残す。プランは飛ぶ | +| 持ち主になれない | `claim` は 10 を返す。まとめのプランは `skipped` の行を残して先へ進み、`research run` は何もせずに終える | +| 持ち主が中断した | 次の `claim` が、寿命の窓を過ぎた宣言、または同じホストで pid の止まった宣言を引き取り、`taken_from` を書く。調査は記録の無い項目から続く | +| 調査の途中で持ち主でなくなった | `touch` が理由を返したら、新しい項目を始めずに終える | +| 調査の項目が失敗した・上限を超えた・出典が無い | 項目を書かずに次へ進む(費用は `calls.jsonl` に残す)。3 回続いたら止まる | +| 待っていたプランが終わった | 新しい項目を始めない。調べている項目は `research.grace` 秒を過ぎたら止め、書かない | +| 起票が失敗した | `create_failed` に並べ、付け直しへ進まずに `stopped`・1 で返す | +| 前の状態を書けない | 1 件も付け直さず、`reparent_failed` と理由を返す | +| 付け直しの途中で失敗した | そこで止め、`revert_command`(`upkeep.py revert --run `)を結果と `next` に載せて `stopped`・1 で返す | +| 戻しの一部が失敗した | 戻せた番号を `reverted`、戻せなかった課題を `items` に載せて 1 を返す | +| 材料の宣言が無い・側面の宣言が無い | 止まらずに、その側面を `skipped_aspects` に「材料なし(未宣言)」で載せる。見直しは既定の回数で起動する | + +## テスト観点 + +| 観点 | 確かめ方 | +| --- | --- | +| 回数は前のその深さの `ran` から数え、溜まり具合(open の増加・持ち越し・課題なし・人の呼び出し)でも見直しが起動する | `plugins/ndf/skills/backlog-refinement/tests/test_upkeep_depths.py`(`test_review_fires_on_count_and_counts_from_the_last_review` / `test_review_fires_on_growth_carry_no_issue_and_manual`) | +| 大整理は回数・3 回の連続・型の割合で起動する | 同上の `test_overhaul_fires_on_count_streak_and_cause_share` | +| 起動しない判定は `not_due` の行を残して 3 を返す。宣言の無いリポジトリでは回数だけで起動する | 同上の `test_depth_due_records_not_due_and_returns_skip_code` / `test_depth_due_without_declarations_counts_only` | +| `cause_types` に無い型を型の記録へ足さない | 同上の `test_causes_record_rejects_unknown_types` | +| 2 つのプロセスが同時に `claim` すると、片方だけが持ち主になる | 同上の `test_only_one_of_two_concurrent_claims_wins` | +| pid の止まった宣言と窓を過ぎた宣言は引き取り、`taken_from` を書く。別のホストは窓だけで決める | 同上の `test_dead_or_expired_owner_is_taken_over_with_taken_from` / `test_other_host_owner_is_judged_by_the_window_only` | +| `release --pid --what` は自分の宣言だけを手放す。見張りは手放す・持ち主が止まるまで `touch` し、切り離して起こされる | 同上の `test_release_by_pid_and_what_only_releases_its_own_claim` / `test_keepalive_touches_until_released_or_owner_gone` / `test_claim_with_keepalive_spawns_a_detached_watcher` | +| 調査の項目は宣言からだけ並ぶ。出典のある結果だけを書き、利用の記録はマスクを通す | 同上の `test_plan_lists_items_from_the_declaration_only` / `test_run_writes_only_sourced_items_and_masks_usage` / `test_run_discards_unsourced_or_over_budget_results` | +| 完了のファイルの後と空きが無いときは項目を始めず、猶予を過ぎた呼び出しを止める。他の持ち主がいれば何もしない | 同上の `test_run_starts_nothing_after_the_plan_ends_or_without_a_slot` / `test_run_stops_a_call_after_the_grace_once_the_plan_ends` / `test_run_does_nothing_when_another_owner_is_alive` | +| 見直しの持ち主は宣言を手放さずに古い項目を調べる | 同上の `test_review_owner_researches_stale_items_without_releasing` | +| 名前の違う項目は違うファイルになり、日本語の名前どうしが上書きし合わない。前の版の記録は `id` が一致するときだけ読む | 同上の `test_item_paths_differ_for_names_that_differ` / `test_research_records_of_japanese_names_do_not_overwrite_each_other` / `test_old_record_of_a_non_ascii_name_is_read_only_when_its_id_matches` | +| `overhaul count` が型・部品・マイルストーン・月を数え、2026-11-01 の固定の入力(`tests/fixtures/overhaul-20261101.json`)を読む | 同上の `test_count_types_components_milestones_and_months` / `test_count_reads_the_20261101_snapshot` | +| 性質の違う 4 つのプロジェクト(novel・carul・bi-tools・ai-plugins)の宣言から、項目・飛ばした側面・報告が出る | `tests/test_upkeep_depth_report.py`(`test_four_declarations_give_items_skipped_aspects_and_the_report`、入力は `tests/fixtures/materials-four-projects.json`) | +| `report` は `--depth` が無ければ出力を変えない。`--depth` があれば apply の記録から起票・閉じた・大方針・戻すコマンドを読む | 同上の `test_report_without_depth_keeps_its_output` / `test_report_depth_reads_apply_created_closed_root_cause_and_revert` | +| 差分の棚卸しは見直しの作業を反映しない。見直しの `apply` は持ち主が要る | `tests/test_upkeep_script.py`(`test_diff_depth_does_not_reflect_review_work` / `test_review_apply_needs_the_owner`) | +| 起票は上限まで、ラベルと `needs-decision` つき | 同上の `test_review_creates_up_to_the_limit_with_labels_and_decision` | +| 付け直しの前に前の状態を書き、戻せる。途中で失敗したら戻すコマンドを返す。大方針が従うまで付け直さない | 同上の `test_reparent_writes_before_state_and_reverts` / `test_reparent_failure_reports_the_revert_command` / `test_reparent_waits_for_a_following_policy` | +| 選べる課題が尽きた最初の選定は `review_first`、見直しの後も 0 件なら `no_issue` で止まる。答えを待つ作業が残れば `waiting_only` で止まる | `plugins/ndf/scripts/tests/test_sprint_loop.py`(`test_no_candidates_is_no_issue_and_all_excluded_is_outside_mvv`) | +| 周回の記録に深さの行と調査の費用が入り、調査の費用が予算に数えられる | 同上の `test_lap_records_depths_and_research_and_counts_research_in_the_budget` | +| まとめのプランで、`refine` の次が `refine-record`、大整理 → 見直しの判定は 3 で飛び、持ち主になれないと `skipped`、本体の成否によらず `release` を通る | `plugins/ndf/scripts/tests/test_supervise_pace.py`(`test_close_runs_spec_retro_and_close_once_each_in_order`)・`test_supervise_pace_verify.py` | +| `vet --kind issue` が課題の本文をプロジェクト MVV と照合する | `plugins/ndf/scripts/tests/test_project_mvv.py`(`test_vet_issue_judges_an_issue_body_against_the_project_mvv`) | +| `measure` が直接の依存から材料の案を問いで出す | `plugins/ndf/scripts/tests/test_project_decl.py`(`test_measure_proposes_materials_from_direct_dependencies_as_a_question`) | +| 毎スプリントの棚卸しの所要(秒・ドル)が、206〜224 秒・1.41〜1.61 ドルより短い | 手動確認(リリース後テスト)。変更の後の周回の `depths.jsonl` の `diff` の行の `cost`・`seconds` と比べる | +| 大整理の分類・柱・付け直しの一覧が #2335 と同じ形になる | 手動確認。固定の入力で大整理の手順 1 をやり直し、件数の差を報告に書く | + +## 運用 + +- **頻度と閾値(見直し 3・大整理 12・open の増加 30%・型の割合 30%)は、最初の 3 回の見直しの `depths.jsonl` で見直す** +- **持ち主の部品は、#2342 が持ち主の部品(`lib/lease.py`)を入れたら、その鍵の 1 つへ移す** +- **`.ndf/backlog/` はコミットしない。** ループの記録(`.ndf/loop/`)と同じく、メインディレクトリの手元の記録である + +## 関連リンク + +- [#2315](https://github.com/devbasex/ai-plugins/issues/2315) — 要求と受け入れ条件(2026-11-01 の利用者の依頼。2026-11-02 の承認ゲート 1 で、置き場・利用の記録の既定・試すプロジェクトを利用者が決めた) +- [#2335](https://github.com/devbasex/ai-plugins/issues/2335) — 2026-11-01 の大整理で立てた大方針(大整理の手順の元になった実例) +- [#2342](https://github.com/devbasex/ai-plugins/issues/2342) — 持ち主の部品 `lib/lease.py`(持ち主の宣言の移り先) +- [棚卸しを工程表の行にし、1 回の件数と書き込む先に上限を置く](ndf-backlog-refinement-stage.md) — `refine` ステップ・候補の上限・書き込む先の照合 +- [開発ループ(`--loop`)](ndf-workflow-development-loop.md) — 選定の `review_first`・周回の記録・予算 +- [課題を根本原因の場所で直す区分(ルートコーズ)](ndf-issue-upkeep-root-cause.md) — 大整理が課題の集まりへ広げた区分 +- [実行をまたいで残る状態: 持ち主と寿命を記録し、終わった実行の残骸を片付ける](ndf-run-state-owner-and-sweep.md) — `run_owner` の寿命の窓と pid の判定 +- [スプリント課題を最終工程の記録を確かめてから閉じる](ndf-sprint-close-final-stage.md) — まとめのプランの並び diff --git a/docs/specifications/ndf-context-restart-policy.md b/docs/specifications/ndf-context-restart-policy.md new file mode 100644 index 000000000..77fd81c7a --- /dev/null +++ b/docs/specifications/ndf-context-restart-policy.md @@ -0,0 +1,381 @@ +# 再起動の方針を選べるようにし、工程境界で継続か再起動かを容量と費用で決める + +conductor が工程境界で会話を切るかを決める規則の組(再起動の方針)を、現行方式と試行方式の 2 つから選べるようにする。 +既定は現行方式で、文脈が 200,000 を超えたら次の工程を止め、4 つのカットポイントでは文脈の量に関わらず `ndf-next` を出す。 +試行方式を選ぶと、カットポイントは「再起動を判断できる境界」になり、hook が帯・容量・費用の見込みで継続か拒否かを決める。 +どちらの方針でも、工程の起動 1 回ごとの判定と、引継ぎ・復元の区間の印を記録に残し、作業ごとに費用・秒・再起動を集計できる。 +この文書は、適用値の解決・判定・記録・集計の契約と、それぞれをそう決めた理由を残す。 + +**規約は Skill の文書が正である。** 方針の選び方・帯の表・費用の式・印を打つ時点をここへ書き写さない。 + +| 何を読むか | 正本 | +| --- | --- | +| 2 つの方針、選び方と優先順位、試行方式の帯の表、自分で切ってよい理由、どちらの方針でも変わらないこと、Codex での扱い、判定と再起動の記録、statusline の色と hook の止める値の意味の違い | `plugins/ndf/skills/development-workflow/references/context-window.md` の「再起動の方針」「判定と再起動を記録する」 | +| 現行方式の hook の止め方(1 度だけ通す・ラッパーの下は止め続ける) | `plugins/ndf/skills/development-workflow/references/context-window.md` の「上限を超えたら hook が止める」 | +| 判断帯の費用の式と記号、倍率の正本 | `plugins/ndf/skills/development-workflow/references/work-vessels.md` の「会話を替えて続けるか(試行方式の判断帯)」「倍率の正本」 | +| 「引継ぎ開始」「復元完了」の印を打つ手順、`sprint-state.py next --reason` | `plugins/ndf/skills/development-workflow/references/relay.md` | +| 承認待ち・質問待ちを引継ぎ文書に保留として残す規則 | `plugins/ndf/skills/development-workflow/references/handoff.md` | +| statusline の試行方式の色 | `plugins/ndf/skills/statusline/SKILL.md` | +| skill-stats の「割る候補」の上限 | `plugins/ndf/skills/skill-stats/SKILL.md` | +| `context-policy.py` の副命令と終了コードの一覧 | `plugins/ndf/scripts/context-policy.py` の docstring | + +## 概要 + +**例(`.ndf/context.json` に `{"version": 1, "policy": "trial"}` を置いたリポジトリで `#829 #830` を進める)。** + +| 時点 | 振る舞い | +| --- | --- | +| セッションを始めた | SessionStart の `context-policy.py publish` が適用値(試行方式・判断開始の値 200,000・運用上限 400,000・余白 50,000)を `sessions/<セッション>.applied.json` へ書く。statusline は窓の大きさ(例: 1,000,000)を `sessions/<セッション>.window.json` へ書く | +| 文脈 150,000 で設計を終え、カットポイントに着いた | conductor は `ndf-next` を出さずに次の工程(`/ndf:implementation-plan #829`)を起動する。hook は継続帯として通し、判定「継続」・`basis: 判断開始の値以下` を 1 行記録する | +| 文脈 260,000 で次の工程を起動した | 判断帯。較正した見込みがあれば続ける費用と再起動の費用を比べ、再起動のほうが安ければ理由「費用」で拒否する。見込みが無ければ継続(`basis: 見込みなし`、`missing` に `estimate`) | +| 文脈 410,000 で次の工程を起動した | 上限超。理由「運用上限」で拒否する。ラッパーの下では判定が変わるまで止め続け、外では同じ起動の 2 度目を 1 度だけ通す | +| 拒否を受けて切る | conductor は引継ぎ文書を書き始める前に `context-policy.py record --event 引継ぎ開始 --work "#829 #830"` を打ち、`sprint-state.py next` が直前の拒否の理由を引き継いで判定「再起動」を記録し、見込みを較正し直す | +| 再開した | 新しいセッションの conductor は状態を確かめ終えた直後に `record --event 復元完了` を打つ。`context-policy.py report --work "#829 #830"` で区分ごとの token・秒・費用・再起動の回数が見える | + +設定を置かない利用者は現行方式のままで、200,001 で拒否・200,000 以下で通す判定と拒否の文面は変わらない。 + +**200,000 / 400,000 / 50,000 は比較実験の初期値で、品質が落ちる境目の実測ではない。** 値は設定で変えられる。 + +**コンテキストは 2 つで、関係は公開された言語である。** 開発ワークフロー(`ndf-workflow`)が方針・判定・記録を持ち、 +ラッパー(`ndf-relay`)へは `ndf-next` のブロックだけを渡す。ラッパーは方針を知らず、hook はラッパーの直接の子かどうか +(`relay_notice`)だけを今のまま使う。 + +## 用語 + +再起動の方針・現行方式・試行方式・判断開始の値・運用上限・容量の余白・再起動の理由・境界の判定・適用値・作業の識別子・ +費用の見込み・引継ぎの区間・復元の区間の定義は、設定の用語集(`.ndf/glossary.json` の `document` が指す +[docs/glossary.md](../glossary.md))にある。復元の区間を測るときの終わりは「復元完了」の印である。 + +## 決定と理由 + +| 決定 | 理由 | +| --- | --- | +| 既定は現行方式で、試行方式は設定か環境変数で選んだときだけ働く。モデル名と窓の大きさは方針を選ばない | 選ばない利用者の判定を変えずに、2 つの方式を同じ物差しで比べてから既定を決めるため。1M の窓のモデル名だけで緩和すると未検証の環境へ広がる | +| 方針は `.ndf/context.json`(置き場 `worktree`、チーム)と環境変数 `NDF_CONTEXT_POLICY`(個人)で受ける | 比較はリポジトリ単位で回すためチームで条件を揃える形が要り、個人はチームの設定を書き換えずに試せる。`pace.json` へ足すと進め方の許可と意味が混ざる | +| 優先順位は CLI 引数 > 環境変数 > 設定 > 既定。`NDF_CONTEXT_LIMIT` は適用中の方針の運用上限を上書きする | 現行方式では今と同じ意味のまま、試行方式でも同じ変数で上限を変えられる。判断開始の値が上回れば同じ値へ下げ、判断開始の値 ≤ 運用上限を保つ | +| 容量の余白は窓に対する比でなく固定の 50,000 | 余白で受けたいのは次の工程の起動・Tool の結果・引継ぎ文書の作成で、窓でなく作業の量で決まる。比にすると 1M の窓で大きすぎ、200K の窓で足りない | +| 容量と費用の判定は試行方式だけに当てる | 現行方式に当てると窓 200K のモデルで 150,000 から止まり、既定の振る舞いが変わる | +| 判断帯は LLM を呼ばず、次の工程までの費用を記録から較正した中央値で比べる | 判定のたびの LLM 呼び出しを無くす。`work-vessels.md` の委譲の式は会話全体を替えるときの引継ぎと復元の費用を持たないため、別の式にした | +| 見込みの標本が 3 未満なら判断帯は継続にする | 品質の側は運用上限が守る。標本が無いときに再起動へ寄せると、試行方式が初回から現行方式と同じになり比べる材料が溜まらない | +| hook は較正したファイルを読むだけにし、較正は `sprint-state.py next`(再起動の記録のたび)と手で打つ `calibrate` が行う | 見込みには会話の記録の全体の走査が要り、hook で行うと起動のたびに遅くなる。SessionStart で行うと起動のたびに数十の記録を読む | +| 作業の識別子は課題番号の昇順・空白区切り(`#829 #830`) | hook は起動の引数から、`sprint-state.py next` はスプリントの課題から同じ形で作れる。引継ぎ文書の名は最初の再起動まで無く、ラッパーのセッションの組はラッパーを使わない利用者に無い | +| statusline とは状態ディレクトリのファイルで窓と適用値を受け渡す | statusline は `~/.claude/ndf-statusline.sh` に写して動き、プラグインのライブラリを読めない。窓は statusline の入力にしか無い。statusline が設定を読むと優先順位の解決が 2 か所になる | +| 状態ディレクトリは `CLAUDE_PLUGIN_DATA` に依らず `NDF_CONTEXT_DIR` → `XDG_STATE_HOME` → `~/.local/state` で決める | `CLAUDE_PLUGIN_DATA` は statusline に渡らず、書き手と読み手で置き場が分かれる | +| 現行方式の statusline の色(500K / 窓 200K 以下は 150K)とサブエージェントの行は変えない。試行方式だけ判断開始の値で黄・実効の上限で赤にする | 現行方式の色は窓に対して危険な水準の表示で、hook の止める上限とは意味が違う。試行方式は試す値が見えないと比較の材料にならない | +| 換算の単価の正本を配布する `plugins/ndf/scripts/lib/token_rates.py` へ移し、`scripts/token-usage.py` は import する | 配布しない `scripts/token-usage.py` の単価を hook は読めず、写すと正本が 2 つになる | +| 試行方式では conductor はカットポイントで `ndf-next` を出さず、次の工程を起動して hook に判定させる。自分で切るのは独立性と手動だけ | conductor は自分の文脈の量を読めない。次の工程の起動は必ず hook を通るため、判断が 1 か所に集まる | +| 集計の区分は時刻で排他に割り当て、同じ `message.id` は 1 回、帳簿にあるセッションの会話の記録は読まない | 会話の記録は 1 回の応答を複数の行に分けて同じ `usage` を持ち、`claude -p` のセッションは帳簿と会話の記録の両方に現れる | +| 「割る候補」の上限はセッションの判定の記録の運用上限で決め、`--window-limit` を渡したときだけ全記録へ当てる | 測る側と止める側の上限を食い違わせない。今の `--window-limit` の使い方は保つ | + +## 仕様 + +### 常に成り立つ条件 + +- 方針を指定しないとき、適用値は現行方式(判断開始の値 = 運用上限 = 200,000)で、hook の判定と拒否の文面は方針の導入前と同じである +- 適用値を解決する関数は `lib/context_policy.py` の `resolve_policy` 1 つで、hook・`publish`・`resolve`・`sprint-state.py next`・`record` が使う。statusline は `publish` と hook が書いた受け渡しのファイルを読み、skill-stats は判定の記録に残った運用上限を読む +- `NDF_CONTEXT_GUARD=0` なら、方針に関わらず hook は判定も記録もしない +- `agent_id` を持つ呼び出し(サブエージェント)と、`transcript_path` に `/subagents/` を含む呼び出しは判定も記録もしない。supervisor のスイッチポイント(`guard_supervisor_cut`)は方針に依らない +- 判定(`decide_boundary`・`estimate_cost`)は LLM もネットワークも子プロセスも使わない純粋関数である。hook が読むのは会話の記録の末尾 200 行・`.ndf/context.json`・見込みと窓の小さな JSON と `git rev-parse` 1 回だけである +- 取れない値を推測で埋めない。文脈の量が取れなければ止めない。窓が取れなければ容量を見ない。見込みが出せなければ判断帯は継続にする。取れなかった値は `missing` に名前で残る +- 記録の書き込みに失敗しても判定(止める / 通す)は変わらない +- ラッパーの直接の子では、拒否の理由(運用上限・容量・費用)に関わらず判定が変わるまで止め続ける。外では同じ起動の 2 度目を 1 度だけ通す +- 判定の記録は追記だけで、既存の行を書き換えない。方針の列(`policy`)の無い行は現行方式(200,000)として読み、その数を出力に示す +- 集計で 1 回の API 呼び出しを 2 つの区分へ数えない +- 取れなかった品質の値と自動圧縮の回数を 0 にせず「不明」と出す +- 承認ゲートの前に `ndf-next` を出さない、動いているプランと supervisor の報告を待ってから引継ぎ文書を作る、承認待ち・質問待ちを保留として残す規則は、どちらの方針でも変わらない。拒否の文面(ラッパーの下)も「承認待ちと質問待ちは『次にやること』に保留として残す」を含む + +### 適用値の解決(`resolve_policy`) + +既定 < `.ndf/context.json` < 環境変数 < CLI 引数の順に重ねて `AppliedPolicy` を返す。 + +| 段 | 方針 | 判断開始の値 / 運用上限 | 余白 | +| --- | --- | --- | --- | +| 既定 | `current` | 200,000 / 200,000 | 50,000 | +| 設定 | `policy` | 試行方式なら `trial.decide_from` / `trial.limit`(無ければ 200,000 / 400,000) | `margin`(無ければ 50,000) | +| 環境変数 | `NDF_CONTEXT_POLICY` が `current` / `trial` なら上書き。ほかの値は無視して `sources.ignored` に `NDF_CONTEXT_POLICY=<値>` を残す | `NDF_CONTEXT_LIMIT`(正の整数)が運用上限を上書き | — | +| CLI 引数 | `--policy`(`context-policy.py resolve`) | `limit`(skill-stats の `--window-limit` は解決を通さず `record_limits` が全記録へ当てる) | — | + +- 現行方式では判断開始の値を運用上限と同じ値にする。試行方式で判断開始の値が運用上限を上回れば、判断開始の値を運用上限へ下げる(出所も運用上限の出所に揃える) +- `sources` は `policy`・`decide_from`・`limit`・`margin` ごとの出所(`default` / `config` / `env` / `cli`)を持つ。`margin` の出所が `config` になるのは試行方式のときだけである +- 設定が壊れていれば設定全体を読まず、`config_error` に理由を持たせた上で残りの段(環境変数・CLI 引数)を重ねる + +### 判定(`decide_boundary`) + +C は会話の記録の最後の assistant 呼び出しの `input_tokens + cache_read_input_tokens + cache_creation_input_tokens`。 + +| 条件(上から順に見る) | 帯 | 判定 | 理由 / 根拠 | 実効の上限 | +| --- | --- | --- | --- | --- | +| C が取れない | `null` | 継続 | `basis: 値なし`、`missing: ["context"]` | 運用上限 | +| 現行方式で C > 運用上限 | 上限超 | 拒否 | 運用上限 | 運用上限 | +| 現行方式で C ≤ 運用上限 | 継続帯 | 継続 | `basis: 判断開始の値以下` | 運用上限 | +| 試行方式で窓が取れ、窓 − 余白 < 運用上限、C > 窓 − 余白 | 容量超 | 拒否 | 容量 | 窓 − 余白 | +| 試行方式で C > 運用上限 | 上限超 | 拒否 | 運用上限 | 運用上限か 窓 − 余白 の小さいほう | +| 試行方式で C ≤ 判断開始の値 | 継続帯 | 継続 | `basis: 判断開始の値以下` | 同上 | +| 試行方式の判断帯で見込みが無い | 判断帯 | 継続 | `basis: 見込みなし`、`missing` に `estimate` | 同上 | +| 試行方式の判断帯で 再起動 < 続ける | 判断帯 | 拒否 | 費用 | 同上 | +| 試行方式の判断帯で 再起動 ≥ 続ける | 判断帯 | 継続 | `basis: 費用` | 同上 | + +試行方式で窓が取れなければ `missing` に `window` が入る。初期値での境界は、200,000 が継続帯、200,001 と 400,000 が +判断帯、400,001 が上限超である。 + +### 費用の見込み(`estimate_cost`) + +式と記号は `work-vessels.md` の「会話を替えて続けるか」が正である(続ける `k·r·C + m·w·C`、再起動 +`h·r·C + 5·H + w·Q + q·r·Q + k·r·Q + m·w·Q`)。単位は入力の単価を 1 とした換算で、r は `token_rates.read_rate(model)`、 +w は `WEIGHTS["w1h"]`(2.0)、H の係数は `WEIGHTS["out"]`(5.0)である。 + +- hook は試行方式の判断帯(判断開始の値 < C ≤ 運用上限)のときだけ見込みのファイルを読む +- k と m は工程(`stage`)ごとの中央値で、その工程の標本が 3 未満なら全工程(`*`)の値を使う。それも足りなければ見込みを出さない +- 復元の標本(`samples.restore`)と引継ぎの標本(`samples.handoff`)のどちらかが 3 未満か、Q・q・h・H のどれかが `null` なら見込みを出さない +- 記録へは `{"continue": <整数に丸めた値>, "restart": <同>}` で残す + +### 較正(`calibrate`) + +直近 `--days`(既定 30)日の判定の記録と会話の記録から中央値を作り、見込みのファイルを上書きする。 + +| 値 | 標本 | +| --- | --- | +| `restore_context` / `restore_calls` | 最初の非メタの user の入力が `.ndf/handoff/` を含むセッションで、最初の「復元完了」の印までの最後の呼び出しの文脈の量と、そこまでの呼び出し数 | +| `handoff_calls` / `handoff_output` | 最後の「引継ぎ開始」の印から後の呼び出し数と出力 token の合計 | +| `stage_calls` / `stage_expiries` | 判定「継続」の行で区切った工程ごとの呼び出し数と、間隔が 3,600 秒を超えた回数(1 時間のキャッシュが切れたとみなす)。最後の区間と空の区間は除く | +| `samples` | `restore`・`handoff`・`stage:<工程>` ごとの標本数 | + +`restore_*`・`handoff_*` は標本が 3 未満なら `null`。`session_id` の無い印は、`at` を時間の範囲に含むセッションへ割り当てる。 + +### hook の判定と記録(`token_guard.guard_context`) + +1. `NDF_CONTEXT_GUARD=0`・`agent_id` あり・サブエージェントの記録・工程の起動でない(工程 Skill・フェーズの Agent・プランの起動のどれでもない)なら何もしない +2. 会話の記録の末尾から C とモデルを読み、`resolve_policy(git の根)` で適用値を得る。試行方式なら窓のファイルを、判断帯なら見込みのファイルも読む +3. `decide_boundary` で判定し、1 度だけ通す印(`guards/` の `context-<セッション>.json`)とラッパーの下の止め続けを当てる +4. 判定の記録へ 1 行を追記し、適用値の受け渡しのファイルを更新する(中身が同じなら書かない) +5. 通すときは、設定が壊れていればそのセッションで 1 度だけ(`guards/` の `context-config-<セッション>.json` で覚える)`additionalContext` で知らせる + +工程の列(`stage`)は起動の鍵から作る: `skill:<名>` / `agent:` / `plan`。作業の識別子は起動の引数か +`description` の課題番号から作る(日付・版数・小数は除く)。 + +### カットポイントと再起動の記録 + +| 方針 | カットポイントで conductor がすること | 記録 | +| --- | --- | --- | +| 現行方式 | 引継ぎ文書を書き始める前に `record --event 引継ぎ開始` を打ち、`sprint-state.py next` で `ndf-next` を出す | 印の後、`next` が判定「再起動」を書く | +| 試行方式 | `ndf-next` を出さずに次の工程を起動する。拒否されたときと、独立性・手動で自分で切るとき(`next --reason 独立性\|手動`)だけ、印を打ってから `next` で切る | hook が「継続」か「拒否」を書き、切ったら `next` が「再起動」を書く | + +`sprint-state.py next` の理由は、`--reason` を渡せばその値、渡さなければ直前 1,800 秒以内の同じ作業の識別子の拒否の理由、 +それも無ければ現行方式は「カットポイント」・試行方式は「手動」である。記録の後に較正し直す。記録と較正に失敗しても +`ndf-next` は出す。 + +Codex には hook が無く、conductor は文脈の量を読めないため、試行方式を選んでいても現行方式のカットポイントで切る。 +印と再起動の記録は Bash で打つため、`--runtime codex` で同じ形に残せる。 + +### 作業ごとの集計(`report`) + +| 区分 | どの呼び出しか | +| --- | --- | +| `restore`(復元の区間) | 最初の非メタの user の入力が `.ndf/handoff/` を含むセッションで、最初の行から最初の「復元完了」の印まで | +| `handoff`(引継ぎの区間) | 最後の「引継ぎ開始」の印から、そのセッションの最後の行まで | +| `conductor` | 作業に結ばれたセッションの会話の記録の、上の 2 つに入らない呼び出し | +| `ndf`(NDF が起動した分) | そのセッションの `subagents/*.jsonl` と、使用量の帳簿の行(`parent_session_id` が一致するか、親が無くセッションの時間の範囲にあるもの) | + +- 作業の識別子が空の行は、同じセッションの前の行の値で補う。同じセッションで作業が替わったら、その作業の行が続いた時間の区間の呼び出しだけを数える +- 同じ `message.id` は 1 回だけ数え、最後の `usage` を採り、時刻は最初の行のものを使う。使用量の帳簿に `session_id` があるセッションとサブエージェントの会話の記録は読まず、帳簿の行で数える +- 秒は呼び出しの間隔の合計で、1 つの間隔を 1,800 秒で打ち切る(利用者の返答待ちを入れない) +- 自動圧縮は会話の記録の `type: system`・`subtype: compact_boundary` で `compactMetadata.trigger` が `auto` の行を数える。記録が読めなければ「不明」 +- 印が無い区間は判定の位置から推して埋めず、その呼び出しを `conductor` に数え、`unknown_parts` に印の無かったセッションの数を出す(`restore` は `ndf-next` で始まったのに「復元完了」が無いセッション、`handoff` は再起動を記録したのに「引継ぎ開始」が無いセッション) +- 品質の値: `rework` は同じ `stage` の判定「継続」が 2 度以上ある工程の数(判定が無ければ「不明」)。`review_fixes` と `missed_requirements` は判定の記録から取れないため常に「不明」 + +### statusline + +- 入力の `context_window.context_window_size` とモデル ID を `sessions/<セッション>.window.json` へ書く(値が変わったときだけ)。セッションは入力の `session_id`、無ければ `transcript_path` のファイル名から作る +- `sessions/<セッション>.applied.json` の `policy` が `trial` なら、メインの使用量を判断開始の値を超えたら黄、実効の上限(運用上限か 窓 − 余白 の小さいほう)を超えたら赤にする。それ以外とサブエージェントの行は方針の導入前と同じ + +### skill-stats の「割る候補」(`context_decisions.record_limits`) + +`--window-limit` を渡せば全記録の上限にする。渡さなければ、判定の記録のうちそのセッションの最後の行の `limit` +(`policy` の列が無い行なら 200,000)を上限にし、判定の記録が無いセッションは 200,000 にする。JSON の `meta` に +`window_limit`・`window_limit_source`(`cli` / `records`)・`records_without_decisions` を出す。 + +## データ・設定 + +### `context-policy.py` + +| 副命令 | 入力 | 出力 | 終了コード | +| --- | --- | --- | --- | +| `resolve [--root ] [--policy current\|trial]` | 作業ディレクトリ・環境変数 | 統一 JSON(`tool: context-policy`、`status: ok`、`items` に適用値 1 件、`metrics` に `policy`・`limit`)。設定が壊れていても `ok` で、`summary` と `config_error` に理由が載る | 0 / 2 引数の誤り | +| `publish` | 標準入力の JSON(SessionStart の `session_id`・`cwd`) | `session_id` があれば適用値の受け渡しのファイルを書く。設定が壊れていれば 1 行を標準出力へ出す | 常に 0 | +| `record --verdict 再起動 --reason <理由> --work <識別子> [--runtime claude\|codex] [--context ]` | 理由(6 つ)と作業の識別子 | 1 行(`source: record`・`stage: cut`)を追記し、統一 JSON で返す | 0 / 1 書けない / 2 `--verdict` が `再起動` でない・理由が 6 つ以外・`--verdict` と `--event` が両方か両方なし | +| `record --event 引継ぎ開始\|復元完了 --work <識別子> [--runtime claude\|codex]` | 印と作業の識別子 | 印の 1 行(`event` が印、`verdict`・`stage`・`band` は `null`)を追記し、統一 JSON で返す | 0 / 1 書けない / 2 印が 2 つ以外 | +| `calibrate [--days 30]` | 判定の記録・会話の記録 | 見込みのファイルを書き、`metrics.samples` に標本数を返す | 0 / 1 書けない | +| `report --work <識別子> \| --all [--since YYYY-MM-DD] [--format json\|table]` | 判定の記録・会話の記録・使用量の帳簿 | 作業ごとに `work`・`sessions`・`parts`(4 区分ごとの `inp`・`out`・`read`・`w5`・`w1h`・`calls`・`seconds`・`cost`)・`unknown_parts`・`restarts`(理由ごと)・`restart_count`・`auto_compacts`・`quality`・`no_policy_rows`。`table` は Markdown の表と作業ごとの 1 行を出す | 0(0 件でも 0)/ 2 `--work` と `--all` が両方か両方なし・`--since` の形の誤り | + +`record` のセッション ID は `--runtime claude` のときだけ `CLAUDE_CODE_SESSION_ID` から取り、無ければ `null` にする。 + +### 状態ディレクトリ + +`NDF_CONTEXT_DIR` → `${XDG_STATE_HOME}/ndf/context` → `~/.local/state/ndf/context` の順に決める(`context_decisions.context_state_dir`)。 +`__` は `usage_ledger.repo_key` で作り、git の外は `_local` にする。 + +| ファイル | 中身 | 書く | 読む | 保持 | +| --- | --- | --- | --- | --- | +| `decisions/__.decisions.jsonl` | 判定の記録(1 行 1 件、追記だけ) | hook・`record`・`sprint-state.py next` | `report`・`calibrate`・skill-stats・`sprint-state.py next` | 消さない | +| `estimates/__.cost-estimates.json` | 費用の見込み(`Estimates` の形と `at`) | `calibrate`・`sprint-state.py next` | hook | 上書き | +| `sessions/<セッション>.window.json` | `{"window": , "model": "", "at": ""}` | statusline | hook | 適用値のファイルを書くときに 7 日より古い `sessions/*.json` を消す | +| `sessions/<セッション>.applied.json` | 適用値(`AppliedPolicy` の JSON) | `publish`・hook | statusline | 同上 | + +### 判定の記録の 1 行 + +| 列 | 値 | 空の値の意味 | +| --- | --- | --- | +| `schema` | 1 | — | +| `at` | UTC の ISO 8601 | — | +| `runtime` | `claude` / `codex` | — | +| `source` | `hook` / `record` / `sprint-state` | — | +| `session_id` / `transcript` | 文字列 | `null`: 書き手がセッションを知らない | +| `model` | 文字列 | `null`: 記録の末尾にモデルが無い・hook 以外 | +| `work` | 作業の識別子 | `""`: 課題番号が取れない(集計は同じセッションの前の行で補う) | +| `event` | `判定` / `引継ぎ開始` / `復元完了` | — | +| `stage` | `skill:<名>` / `agent:<先頭語>` / `plan` / `cut` | `null`: 印 | +| `policy` / `decide_from` / `limit` / `margin` / `sources` / `config_error` | 適用値 | `config_error` の `null`: 設定は正しいか無い | +| `context` / `window` / `effective_limit` | 整数 | `null`: 取れない・判定しない | +| `band` | `継続帯` / `判断帯` / `上限超` / `容量超` | `null`: 判定しない(`record`・`sprint-state`)か C が取れない | +| `verdict` | `継続` / `拒否` / `再起動` | `null`: 印 | +| `reason` | `容量` / `運用上限` / `費用` / `独立性` / `手動` / `カットポイント` | `null`: 継続 | +| `basis` | `判断開始の値以下` / `費用` / `見込みなし` / `値なし` | `null`: 継続でない | +| `estimate` | `{"continue": , "restart": }` | `null`: 見込みを使っていない | +| `missing` | 取れなかった値の名前(`context` / `window` / `estimate`) | `[]` | +| `relay` / `passed_once` | 真偽(ラッパーの下だったか / 1 度だけ通したか) | — | + +### 型 + +`lib/context_policy.py` が持つ。`AppliedPolicy`・`Verdict`・`Estimate` は `dataclass(frozen=True)`、`Estimates` は見込みのファイルの中身をそのまま持つ。 + +```mermaid +classDiagram + class AppliedPolicy { + +policy: str + +decide_from: int + +limit: int + +margin: int + +sources: dict + +config_error: str|None + } + class Verdict { + +band: str|None + +verdict: str + +reason: str|None + +basis: str|None + +missing: tuple + +effective_limit: int|None + +estimate: Estimate|None + } + class Estimate { + +continue_cost: float + +restart_cost: float + } + class Estimates { + +restore_context: float|None + +restore_calls: float|None + +handoff_calls: float|None + +handoff_output: float|None + +stage_calls: dict + +stage_expiries: dict + +samples: dict + } + Verdict --> Estimate + Estimate ..> Estimates : estimate_cost が中央値から作る +``` + +### `.ndf/context.json` + +置き場は `worktree`(チームの設定。契約は `plugins/ndf/contracts/settings/context.json.json`)。 + +```json +{"version": 1, "policy": "trial", "trial": {"decide_from": 200000, "limit": 400000}, "margin": 50000} +``` + +| キー | 型 | 無いとき | 壊れているとき | +| --- | --- | --- | --- | +| `version` | 1 | 1 とみなす | 1 以外はファイル全体を壊れたものとする | +| `policy` | `"current"` / `"trial"` | `"current"` | 同上 | +| `trial` | オブジェクト | 下の既定 | オブジェクトでなければ同上 | +| `trial.decide_from` / `trial.limit` | 正の整数 | 200000 / 400000 | 同上。`decide_from > limit` も同じ | +| `margin` | 0 以上の整数 | 50000 | 同上 | + +現行方式の運用上限は設定に持たず、`NDF_CONTEXT_LIMIT` だけで変える。 + +### 環境変数と CLI 引数 + +| 名前 | 意味 | +| --- | --- | +| `NDF_CONTEXT_POLICY` | `current` / `trial`。設定より強い。ほかの値は無視して `sources.ignored` に残す | +| `NDF_CONTEXT_LIMIT` | 適用中の方針の運用上限。試行方式で判断開始の値がこれを上回れば同じ値へ下げる(既定と互換の扱いは [ndf-token-waits-and-context-cut.md](ndf-token-waits-and-context-cut.md) の「環境変数」) | +| `NDF_CONTEXT_GUARD=0` | 判定も記録もしない | +| `NDF_CONTEXT_DIR` | 状態ディレクトリ | +| `CLAUDE_CODE_SESSION_ID` | `record` と `sprint-state.py next` が行の `session_id` に使う | +| `skill-stats --window-limit ` | 渡したときだけ全記録の「割る候補」の上限にする | +| `sprint-state.py next --reason <理由>` | 再起動の理由(6 つ) | + +### hook の出力 + +| 場面 | 出力 | +| --- | --- | +| 現行方式で上限超 | 方針の導入前と同じ拒否の文面(ラッパーの下と外の 2 通り)。「上限 {limit} を超えた」 | +| 試行方式で上限超 | 「上限 {limit}(再起動の方針: 試行方式、理由: 運用上限)を超えた」を入れた同じ形の拒否 | +| 試行方式で容量超 | 「容量(窓 {window} − 余白 {margin} = {effective})を超えた(再起動の方針: 試行方式、理由: 容量)」 | +| 試行方式の判断帯で再起動が安い | 「判断開始の値 {decide_from} を超え、再起動のほうが安い(再起動の方針: 試行方式、理由: 費用(続ける {continue} / 再起動 {restart}、入力の単価に換算))」 | +| 設定が壊れていて通す | そのセッションで 1 度だけ `additionalContext` に「{理由}。再起動の方針の設定を読まずに現行方式の既定で判定している」 | + +SessionStart には `context-policy.py publish` が登録されている(`plugins/ndf/hooks/claude.json`)。 + +## エラー処理 + +| 場面 | 扱い | +| --- | --- | +| `.ndf/context.json` が壊れている・読めない | 設定を読まずに現行方式の既定で解決し、`config_error` に理由を持つ。`publish` が 1 行、hook が 1 度だけ知らせる | +| 判定の記録・受け渡しのファイルに書けない | 判定は変えず、そのまま通す / 止める。`record` は終了コード 1 | +| 見込みのファイルが無い・壊れている・標本不足 | 見込みを出さず、判断帯は継続 | +| 窓のファイルが無い・形が違う | 窓を使わず `missing` に `window` | +| `sprint-state.py next` の記録・較正の失敗 | 投げずに `ndf-next` を出す | +| `publish` の内部の例外 | 標準エラーへ 1 行を出し、終了コード 0 | +| 使用量の帳簿が読めない(`report`) | NDF が起動した分は会話の記録の分だけで数える | +| 判定の記録の壊れた行 | 飛ばす | + +## テスト観点 + +| 観点 | 確かめ方 | +| --- | --- | +| 設定が無ければ現行方式・200,000 で、拒否の文面が導入前と同じ | `plugins/ndf/scripts/tests/test_context_policy.py`(`test_without_setting_the_policy_is_current_with_200k`)、`test_context_policy_hook.py`(`test_default_policy_keeps_todays_text`)、既存の `test_token_guard*.py` | +| 優先順位(CLI 引数 > 環境変数 > 設定 > 既定)と出所、知らない方針の値の無視 | `test_context_policy.py`(`test_priority_cli_over_env_over_setting` / `test_unknown_env_policy_is_ignored_and_recorded`)、`test_context_policy_hook.py`(`test_resolve_cli_shows_sources`) | +| `NDF_CONTEXT_LIMIT` が現行方式で今の意味のまま、試行方式で判断開始の値も下げる | `test_context_policy.py`(`test_env_limit_keeps_current_meaning` / `test_env_limit_lowers_decide_from_in_trial`)、`test_context_policy_hook.py`(`test_trial_limit_from_env`) | +| 壊れた設定で現行方式になり、1 度だけ知らせる | `test_context_policy.py`(`test_broken_setting_falls_back_to_current`)、`test_context_policy_hook.py`(`test_broken_setting_is_told_once`) | +| 窓とモデル名が方針を選ばない | `test_context_policy.py`(`test_window_and_model_do_not_choose_the_policy`) | +| 帯の境界の値(200,000 / 200,001 / 400,000 / 400,001)と、仕様の初期値と定数が同じ | `test_context_policy.py`(`test_trial_band_edges` / `test_current_band_edges` / `test_initial_values_are_the_constants`) | +| 小さい窓で理由「容量」の拒否、窓が無ければ `missing` に残す、C が取れなければ通す | `test_context_policy.py`(`test_small_window_denies_by_capacity` / `test_missing_values_are_not_filled`)、`test_context_policy_hook.py`(`test_trial_small_window_denies_by_capacity` / `test_trial_without_window_records_missing` / `test_unreadable_context_passes_and_records`) | +| 費用の式・標本 3 未満で見込みなし・全工程の値への寄せ・判定が外部を触らない | `test_context_policy.py`(`test_cost_follows_the_formula` / `test_cost_needs_three_samples` / `test_cost_falls_back_to_all_stages` / `test_cost_decides_the_judgement_band` / `test_decide_and_cost_do_not_touch_the_outside`) | +| 試行方式で判断開始の値未満ならカットポイントの次の工程を通す | `test_context_policy_hook.py`(`test_trial_passes_cut_point_below_decide_from`) | +| ラッパーの下で費用の拒否が続く間は止め続ける | `test_context_policy_hook.py`(`test_trial_cost_denial_keeps_denying_under_relay`) | +| `NDF_CONTEXT_GUARD=0` とサブエージェントは判定も記録もしない | `test_context_policy_hook.py`(`test_guard_off_and_subagent_neither_judge_nor_record`) | +| 起動 1 回で 1 行が増え、必要な列を持つ。書けなくても判定は同じ | `test_context_policy_hook.py`(`test_one_row_per_launch_with_the_columns` / `test_unwritable_record_does_not_change_the_verdict`) | +| hook・`publish`(statusline が読む)・skill-stats が同じ設定から同じ値を読み、設定を消すと 3 つとも現行方式へ戻る。statusline が窓を書く | `test_context_policy_hook.py`(`test_three_readers_follow_the_same_setting` / `test_statusline_writes_the_window`) | +| 印と再起動の記録、知らない印・理由は終了コード 2 | `test_context_policy_hook.py`(`test_record_marks_and_restart`) | +| `sprint-state.py next` が再起動を記録し、見込みを較正する | `test_context_policy_hook.py`(`test_sprint_state_next_records_restart`) | +| 同じ `message.id` と帳簿にあるセッションを 1 回だけ数える。最後の `usage` を採る | `test_context_policy_hook.py`(`test_same_message_and_ledger_are_counted_once` / `test_subagent_in_the_ledger_is_not_read_from_its_transcript` / `test_same_message_takes_the_last_usage`) | +| 同じセッションで作業が替わったら、その作業の区間だけを数える | `test_context_policy_hook.py`(`test_work_report_counts_only_the_calls_while_the_work_is_current`) | +| 印の位置で復元・引継ぎの区間が区切られ、印が無ければ「不明」と数える | `test_context_policy_hook.py`(`test_marks_split_restore_and_handoff` / `test_missing_marks_are_unknown_and_counted`) | +| 方針の列の無い行を 200,000 で読み、その数を出す。`--window-limit` が記録より強い | `test_context_policy_hook.py`(`test_rows_without_policy_are_read_as_current` / `test_skill_stats_window_limit_overrides_records`) | +| 較正の標本の下限と、工程・印・キャッシュ切れ・期間の扱い | `test_context_policy_hook.py`(`test_calibrate_needs_three_samples` / `test_calibrate_characterizes_*`) | +| テストが利用者の状態ディレクトリへ書かない | `plugins/ndf/scripts/tests/conftest.py` が `NDF_CONTEXT_DIR` と `XDG_STATE_HOME` を一時ディレクトリへ向ける | +| 承認ゲートを越えない・保留を残す規則が拒否の文面と引継ぎの手順から辿れる | 文書と拒否の文面を `grep -n` で読んで確かめる(文言を照合するテストは書かない) | + +## 運用 + +- **初期値(200,000 / 400,000 / 50,000)は比較実験の値である。** 試行方式を選んだ conductor で `report` を取り、現行方式と比べてから既定を変えるかを決める。既定を変えるのは別の課題として扱う +- **品質の値のうちレビュー後の修正量と要件の漏れは `report` が常に「不明」を出す。** 比較するときは出所を別に決める +- **作業の識別子は起動の引数とスプリントの課題の組から作るため、両者が食い違うと同じ作業が 2 つに分かれる。** `report --all` で分かれた作業を見る + +## 関連リンク + +- [#2262](https://github.com/devbasex/ai-plugins/issues/2262) — 要求と決定(2026-11-02 利用者が設計と初期値を承認) +- [待つ間の問い合わせを止め、conductor の会話をカットポイントで切る](ndf-token-waits-and-context-cut.md) — hook の判定の全体、現行方式の止め方、環境変数の一覧 +- [会話の記録から context window を 3 層で測る](ndf-context-window-metrics.md) — skill-stats の「割る候補」の目印 +- [#827](https://github.com/devbasex/ai-plugins/issues/827) — conductor の文脈・持ち越し量の実測 +- [#1785](https://github.com/devbasex/ai-plugins/issues/1785) — 引継ぎ文書の肥大化(復元の費用に効く条件) diff --git a/docs/specifications/ndf-factory-contracts-and-progress.md b/docs/specifications/ndf-factory-contracts-and-progress.md index 26c2313c1..c891b6116 100644 --- a/docs/specifications/ndf-factory-contracts-and-progress.md +++ b/docs/specifications/ndf-factory-contracts-and-progress.md @@ -158,7 +158,7 @@ graph TD | `pr.updated` | `repo`(`owner/name`)、`number`(1 以上の整数)、`state`(`open` / `closed` / `merged`) | | `ci.updated` | `repo`(`owner/name`)、`pr_number`(1 以上の整数)、`name`(文字列)、`conclusion`(文字列か `null`。`null` は「まだ終わっていない」) | | `issue.requested` | `repo`(`owner/name`。起票先)、`title`(1〜256 字。GitHub の題の上限)、`body`(1〜50,000 字。4 バイトの文字だけでも 256 KiB に収まる長さ)。ラベルと相手の課題の番号は相手先の流儀に属するため持たない | -| `section.started`・`loop.lap_closed`・`loop.stopped` | #2347 が足した区間の起動・周回の締め・ループの止まる条件。鍵は [#2347 の設計の「入出力の契約」](../../issues/issue-2347-design-contracts.md#入出力の契約) にある | +| `section.started`・`loop.lap_closed`・`loop.stopped` | #2347 が足した区間の起動・周回の締め・ループの止まる条件。鍵は [開発ループの確定仕様の「進捗の事象」](ndf-factory-dev-loops-data-and-contracts.md#進捗の事象progress-eventschemajson) にある | | `issue.request_resolved` | `request_key`(依頼の冪等の鍵)、`outcome`(`filed` / `drafted`)、`issue`(`filed` のときだけ必須で `drafted` では持たない。`repo`(`owner/name`)・`number`(1 以上の整数)・`url`(URI))、`reason`(`drafted` のときだけ必須。[起票の依頼と起票の代行](ndf-factory-issue-requests.md) の理由の表のどれか)、`duplicate_of`(同じ中身の最初の依頼の鍵か `null`) | **大きさの上限は 256 KiB(262,144 バイト、`MAX_EVENT_BYTES`)で、定義の外で見る。** `runner`・`api` の経路は要求の本文の UTF-8 のバイト数、`github` の経路と `appendControlEvent` は作った事象を JSON にしたバイト数で見る。JSON Schema の `maxLength` は字の数でバイトを数えられないためである。 diff --git a/docs/specifications/ndf-factory-dev-loops-data-and-contracts.md b/docs/specifications/ndf-factory-dev-loops-data-and-contracts.md new file mode 100644 index 000000000..de53d6c3d --- /dev/null +++ b/docs/specifications/ndf-factory-dev-loops-data-and-contracts.md @@ -0,0 +1,172 @@ +# Neural Development Factory: 開発ループのデータと契約 + +[開発ループと並行する実行の調整](ndf-factory-dev-loops.md) の実行の表の行・移行の埋め戻し・環境変数と、実行ジョブの指示・進捗の事象・NDF の受け渡しの形・手元のランナーの申告・画面向けの経路の契約を定める(ndf-factory-dev-loops.md から分けた)。機械可読な正は `factory/contracts/` にある。 + +## データ・設定 + +### 実行の表の行 + +実行の表(`RUNS_TABLE_NAME`)の行である。時系列の扱いは既存と同じ「今の値の上書き」で、周回ごとの課題・結果・版・費用は進捗の記録の `loop.lap_closed` が追記で持ち、ループの行の和はそこから作り直せる。 + +```mermaid +erDiagram + "リポジトリの登録" ||--o{ "開発ループ" : "持つ" + "開発ループ" ||--o{ "スプリントの実行" : "周回ごとに起こす" + "開発ループ" ||--|{ "マイルストーンの持ち分" : "占める" + "スプリントの実行" ||--o{ "課題の持ち分" : "占める" + "手元のランナーの枠" ||--o{ "実行ジョブ" : "数える" +``` + +| 行 | `pk` | `sk` | `gsi1pk` / `gsi1sk` | +| --- | --- | --- | --- | +| 開発ループ | `ACCOUNT#` | `LOOP#` | `WAITING_LOOPS` / `#`(`between_laps` で `waitReason` が `user_start` でない間だけ) | +| ループの実行(周回の開始の書き込みで足す。列は `sprintRunId`) | `ACCOUNT#` | `LOOPRUN##` | — | +| マイルストーンの持ち分 | `ACCOUNT#` | `MILESTONE##<題の sha256 の先頭 32 字>` | — | +| 課題の持ち分 | `ACCOUNT#` | `ISSUE##<番号>` | — | +| 手元のランナーの枠 | `ACCOUNT#` | `RUNNER#` | — | +| 移行の済み(表に 1 つ) | `MIGRATION` | `MIGRATED#2347` | — | + +| 行 | 列 | 型 | 空 | 意味 | +| --- | --- | --- | --- | --- | +| ループ | `loopId` | 文字列 | 不可 | 32 字(実行と同じ作り方) | +| ループ | `repositoryId`・`name` | 数・文字列 | 不可 | 属する登録と、NDF のループ名(`^[0-9A-Za-z][0-9A-Za-z_-]{0,63}$`) | +| ループ | `milestones` | 文字列の並び | 不可 | 1〜10 件。題に `,` を含まない(NDF の `--milestones` の区切り) | +| ループ | `budget` | `{laps, usd, hours}` | 各値は可 | 空は「宣言しない(NDF の `.ndf/pace.json` の既定)」 | +| ループ | `state` | 文字列 | 不可 | `running` / `between_laps` / `awaiting_approval` / `stopped` / `cancelled`。後ろの 2 つが終わりの状態 | +| ループ | `waitReason` | 文字列 | 可 | `between_laps` の理由: `slots`(枠)・`legacy_version`・`user_start`(`local`)。空は「起こす途中」 | +| ループ | `currentRunId` | 文字列 | 可 | 終わりの状態でない実行(I44)。空は「動く実行が無い」 | +| ループ | `lapRuns` | 数 | 不可 | 起こした実行の数。周回の開始の条件(I44)と、実行の `lap` | +| ループ | `lapsClosed`・`closedCostUsd`・`closedSeconds` | 数 | 不可 | `loop.lap_closed` の和 | +| ループ | `stopCondition`・`stopDetail` | 文字列・JSON | 可 | 止まった理由。NDF の条件か、Factory の `run_failed`・`run_stopped`・`limits_exceeded`・`launch_refused` | +| ループ | `pending` | `{number, reason, lap}` の並び | 不可 | 最後に届いた人の判断待ちの写し | +| ループ | `answers` | `{number, decision, pr}` の並び | 不可 | 画面の答え。次のジョブの `LOOP_ANSWERS` に載り、`pending` から消えた番号は外す | +| ループ | `cancelRequested`・`createdBy`・`createdAt`・`updatedAt`・`lastRunAt` | — | `lastRunAt` だけ可 | 取り消しの要求と時刻(`lastRunAt` は最後の実行の `updatedAt`) | +| 持ち分 | `loopId`・`title` / `sprintRunId` | 文字列 | 不可 | 占めるループと題 / 占める実行 | +| ランナーの枠 | `count` | 数 | 不可 | `running` の `local` のジョブの数(I50) | +| 実行 | `loopId`・`lap` | 文字列・数 | 可 | 空は手動の実行 | +| 実行 | `legacy` | 真偽 | 可 | 古い版として `legacyCount` に数えた実行。作成と移行の埋め戻しで書き、終わりへの移りで真のときだけ `legacyCount` を減らす。空は「埋め戻していない既存の実行」 | +| 実行 | `finishReason` | 文字列 | 可 | `job.finished` の `reason`(`section_limit` / `usage_limit`) | +| 実行 | `resumeAt` | 文字列 | 可 | `awaiting_reset` の間だけ。リセット時刻(読めなければ `USAGE_LIMIT_RETRY_SECONDS` 後) | +| ジョブ | `runnerId` | 文字列 | 可 | `local` で受け取ったランナー。手元のランナーの枠を減らす先 | +| リポジトリの枠 | `legacyCount` | 数 | 不可 | 並行を許す版の下限より古い版の、終わりの状態でない実行の数(I48)。無いのは 0 | +| 登録 | `maxConcurrentRuns` | 数 | 可 | 空は「宣言しない(プランの値)」 | + +実行の状態には `awaiting_reset` がある(終わりの状態でない)。`maxConcurrentRuns`・`loopId` などの無い行は空として読む。 + +| 機能 | ループ | 実行 | ジョブ | 枠・持ち分・ランナーの枠 | 登録 | 進捗の記録 | +| --- | --- | --- | --- | --- | --- | --- | +| 始める | C | C | C | C・U | R | — | +| 止める | U | U | — | U・D | — | R | +| 予算 | U | — | — | — | — | — | +| 次の周回 | U | C | C | U・C | R | — | +| 一覧と詳細 | R | R | — | — | — | R | +| 答える | U | — | — | — | — | — | +| 手動の開始 | R | C | C | C・U | R | — | +| 宣言 | — | — | — | — | U | — | +| 停止の後の処理 | U | U | U | U・D | — | R・C | +| 見回り(再開・待ち) | U | U | C | U・C | R | — | +| 受け取り(`local`) | — | U | U | U | — | — | + +### 移行の済みの行と埋め戻し + +並行の開始の材料(課題の持ち分・`legacyCount`)は、既存の終わりの状態でない実行から埋め戻してから使う。どの時点で止まっても I47・I48 を破らない。 + +1. 表に移行の済みの行が無い間、開始はすべての版を古い版として扱い、リポジトリの枠の `count` が 0 のときだけ書く。作成する実行は版によらず `legacy` を真にして `legacyCount` を 1 増やす(偽を書くと、この間に始めた古い版の実行が移行の後に数えられず I48 を破る) +2. 埋め戻しのコマンド(`factory/api/src/runs/backfill.ts`。何度流しても同じ結果になる)が、すべてのアカウントの `legacy` の空の終わりの状態でない実行を 1 本ずつ扱う。実行の課題の持ち分の行を強い整合で読んで他の実行が持つ課題を除き、1 回のトランザクションで実行の `legacy` を真に(条件は `attribute_not_exists(legacy)` と終わりの状態でないこと)・`legacyCount` を 1 増やし・残った課題の持ち分を `attribute_not_exists` で書く。落ちたら(`TransactionCanceledException`)その実行を読み直してやり直す +3. `legacy` の空の終わりの状態でない実行が 0 件になったら、同じコマンドが移行の済みの行を書く。開始はこの行がある間だけ `NDF_PARALLEL_MIN_VERSION` を読み、下限より古い版なら `legacy` を真にして `legacyCount` を 1 増やし、新しい版なら偽にする + +- 開始と周回の開始は、移行の済みの行をトランザクションの前に強い整合で 1 回読むだけにし、行の照合(`attribute_exists`)をトランザクションへ入れない。全アカウントで共有の 1 項目を照合に入れると、同時の開始どうしが `TransactionConflict` で取り消し合うためである。行は一度書けば消さないため、読んで「ある」なら以後もあり、「無い」なら 1 の保守側の扱いになる +- 移行の済みの行はアカウントごとに置かず表に 1 つにする。埋め戻しの後に作られたアカウントの実行はすべて作成時に `legacy` を書いており、埋め戻すものが無いためである +- 既存の実行を古い版と見なすのは、その実行の NDF の版を照合していないためである。無い値を 0 と読むのは 1 の間の `legacyCount` だけである + +### 環境変数 + +`factory/.env.example` に名前だけを置く。 + +| 名前 | 使う側 | 意味 | +| --- | --- | --- | +| `NDF_PARALLEL_MIN_VERSION` | 制御 | 並行を許す NDF の版の下限。無ければ並行を許さない | +| `LOCAL_LOOP_AUTOSTART` | 制御 | `true` のとき `local` のループも自動で次の周回を起こす。既定 `false` | +| `USAGE_LIMIT_RETRY_SECONDS` | 制御 | リセット時刻を読めないときの待ち。既定 900 | +| `RUNNER_SECTION_LIMIT` | ランナー | 区間の上限。既定 20 | + +取り込むリポジトリの選定の規則・予算の既定・止まる条件の閾値は、ランナーの中の NDF がそのリポジトリの `.ndf/pace.json` から読む。Factory が持つのは利用者が画面で入れた値(マイルストーン・予算・同時の数の宣言)と、上の設定だけである。 + +## 外部連携 + +### 実行ジョブの指示(`run-job-instruction.schema.json`) + +| 入力 | 型・必須 | 意味 | +| --- | --- | --- | +| `START` | `new` / `resume` / `loop`・必須 | `loop` は開発ループの周回を新しく始める | +| `ISSUES` | 番号の並び・`new` で必須 | `loop` では持たない | +| `APPROVAL_REQUEST_ID` | 文字列・任意 | 無い `resume` は答えを書かずに続ける(利用上限の後の再開) | +| `LOOP_NAME` | `^[0-9A-Za-z][0-9A-Za-z_-]{0,63}$`・`loop` で必須 | NDF のループ名。ループの実行の `resume` にも載せる | +| `LOOP_MILESTONES` | 文字列・`loop` で必須 | マイルストーンの題を `,` で結ぶ | +| `LOOP_BUDGET` | `^(laps=\d+(\.\d+)?)?(,?usd=\d+(\.\d+)?)?(,?hours=\d+(\.\d+)?)?$`・任意 | 宣言した予算だけを並べる。無ければ NDF の既定 | +| `EXCLUDE_ISSUES` | 番号の並び・任意 | 同じリポジトリの課題の持ち分。無ければ外さない | +| `LOOP_ANSWERS` | `<番号>=(approved\|rejected)(#)?` を `,` で結ぶ・任意 | 人の判断待ちへの答え | + +- `START=loop` は `LOOP_NAME`・`LOOP_MILESTONES` を要り、`ISSUES` を許さない。`LOOP_*`・`EXCLUDE_ISSUES` は `loop` と、ループの実行の `resume` のほかでは許さない +- RunTask の `overrides` の 8,192 字に収めるため、マイルストーンは 10 件・各 100 字まで(ループの開始の入力で断る) + +### 進捗の事象(`progress-event.schema.json`) + +`schema_version` は 1 である。冪等の鍵は `:` で、`loop.*` はジョブの終わりに `job.finished` より前に送り、実行の制御は停止の後に同じジョブの事象として読む。 + +| 種類 | 出所 | `payload` | 受け手 | +| --- | --- | --- | --- | +| `section.started` | ランナー | `section`(1 から)・`command`(2,000 字で切る) | 画面の詳細 | +| `loop.lap_closed` | ランナー(`loop.json` の締めた周回) | `loop`・`lap`・`issues`(番号の並び)・`result`(`完了` / `失敗` / `人へ戻した`)・`version`(開発版と本番の版、無ければ `null`)・`cost_usd`・`seconds`・`pending`(`{number, reason, lap}` の並び) | 実行の制御(停止の後に読む)・画面の詳細 | +| `loop.stopped` | ランナー(`loop.json` の `stop`) | `loop`・`condition`(止まる条件の 10 個)・`detail`(`stop` の残りの値の JSON。4,000 字まで)・`pending` | 実行の制御・画面 | +| `job.started` | ランナー | `start` に `loop` を持てる | — | +| `job.finished` | ランナー | `reason`(`section_limit` / `usage_limit`、任意)・`resets_at`(ISO 8601、`usage_limit` のときだけ。読めなければ `null`) | 実行の制御 | +| `gate.reached` | ランナー | `loop`(`{name, lap}`、ループの実行だけ) | 承認の依頼の一覧 | + +利用上限の事象はリセット時刻だけを持ち、アカウントの名前を載せない。 + +### NDF の受け渡しの形(`plugins/ndf/contracts/forms/ndf-next-block.json`) + +| 鍵 | 中身 | +| --- | --- | +| `name` | `ndf-next-block` | +| `summary` | 応答の文の最後に置く、次の区間で起動するコマンド | +| `rule` | CommonMark の 3 つのバッククォートの囲みで、情報文字列を前後の空白を除いて `ndf-next` と比べる。ほかの囲みの中の囲みは拾わない。中身は前後の改行を除く。囲みが 2 つ以上なら起動しない。`- 結果:` の行の値が `判断が要る` なら起動しない | +| `writers` / `readers` | `sprint-state.py`(`ndf_next`)・conductor の Skill の本文 / `relay.py`(`relay_lib/mark.py`) | +| `cases` | `{text, blocks, launch}` の並び。囲みが 1 つ・無し・2 つ・別の囲みの中・`~~~` の囲み・情報文字列の前後の空白・`判断が要る`・`完了` を含む | + +- 規則の持ち主は NDF で、NDF の変更が例を直す。ランナーは実行時に NDF をマーケットプレイスから入れ、`plugins/` を参照しないため、規則を写す。写した規則が古くなるのを、NDF の pytest(`next_blocks`・`_mark_action`)と Factory の vitest(`next-block.ts`)が同じ例の組を読むことで防ぐ(I55) +- `scripts/contracts_lib/model.py` は `forms/*.json` を読み、上の鍵の型を確かめる(知らない鍵は断る)。Factory はテストだけが `../plugins/ndf/contracts/forms/ndf-next-block.json` を読む。区分 `forms/` は [NDF の入口の契約](ndf-entrypoint-contracts.md) の一覧にある + +### 手元のランナーの申告(`local-runner-declaration.schema.json`) + +| 鍵 | 型・必須 | 意味 | +| --- | --- | --- | +| `runner_id` | 32 字の英数字・必須 | 登録のたびに発行する | +| `max_jobs` | 1〜16 の整数・任意(無ければ 1) | 同時のジョブの数 | +| `capacity_hint` | 1 以上の整数・任意 | 目安。表示だけに使う | + +手元のランナーの登録と心拍の本文はこの形を含む。 + +### 画面向けの経路(`openapi/loops.yaml`・`openapi/runs.yaml`) + +どの経路も Cognito の access token を要り、`resolveFromRequest` でアカウントを解決する。ループの経路はループの `accountId` が一致しなければ 403、無ければ 404 `loop_not_found`。エラーの本文は `{"error": {"code", "message", "details"?}}`。 + +| 経路 | 誰が | 入力 | 成功 | 失敗 | +| --- | --- | --- | --- | --- | +| `POST /accounts/{a}/repositories/{r}/loops` | 席を持つ人 | `{name, milestones, budget?: {laps?, usd?, hours?}}` | 201 `{loop_id, state}`(`running` か `between_laps`) | 400 `invalid_request`(名前・題の形、1〜10 件、予算が正の数でない)・403・404 `repository_not_found`・409 `milestone_in_use`(`details.milestones`・`details.loop_name`)・409 `loop_name_in_use`(同じ登録の終わりの状態でないループ) | +| `GET /accounts/{a}/repositories/{r}/loops` | 席を持つ人 | — | 200 `{loops: [{loop_id, name, milestones, state, wait_reason, laps_closed, cost_usd, budget, budget_remaining, last_run_at, stop_condition, stop_detail}]}`(新しい順 50 件) | 400・403 | +| `GET /accounts/{a}/loops/{id}` | 席を持つ人 | — | 200 `{loop, laps: [{lap, issues, result, version, cost_usd, seconds}], runs: [{sprint_run_id, lap, state, failure_reason, finish_reason, resume_at, cost_usd, sections}], pending: [{number, reason, lap, answer}]}` | 403・404 | +| `PATCH /accounts/{a}/loops/{id}` | 席を持つ人 | `{budget}` | 200 ループの 1 行 | 400・403・404・409 `already_finished` | +| `POST /accounts/{a}/loops/{id}/cancel` | 席を持つ人 | — | 200 `{state}`(`cancelled` か、実行の停止を待つ `running`) | 403・404・409 `already_finished` | +| `POST /accounts/{a}/loops/{id}/next-lap` | 席を持つ人 | — | 202 `{state}` | 403・404・409 `not_awaiting_user_start` | +| `POST /accounts/{a}/loops/{id}/answers` | 席を持つ人 | `{issue, decision: approved \| rejected, pr?}` | 200 `{pending}` | 400・403・404・409 `not_pending`(`pending` に無い番号) | +| `PATCH /accounts/{a}/repositories/{r}`(`runs.yaml`) | 持ち主 | `{max_concurrent_runs: 1〜100 の整数か null}` | 200 登録の 1 行 | 400・403・404 | +| `POST /accounts/{a}/sprint-runs`(`runs.yaml`) | 席を持つ人 | 既存のまま | 既存のまま | 既存の失敗に加えて 409 `issue_in_use`(`details.numbers`)・409 `issue_in_loop`(`details.numbers`・`details.loop_name`)・409 `parallel_unsupported_version`、`limits_exceeded` の `reasons` に `concurrent_repo_runs` | + +## 関連リンク + +- [開発ループと並行する実行の調整](ndf-factory-dev-loops.md) +- [スプリントの実行と実行ジョブの状態・実行の表](ndf-factory-runs-state-and-data.md) +- [契約の定義と進捗の受け口](ndf-factory-contracts-and-progress.md) +- [NDF の入口の契約](ndf-entrypoint-contracts.md) diff --git a/docs/specifications/ndf-factory-dev-loops.md b/docs/specifications/ndf-factory-dev-loops.md new file mode 100644 index 000000000..3dc2bb1c3 --- /dev/null +++ b/docs/specifications/ndf-factory-dev-loops.md @@ -0,0 +1,406 @@ +# Neural Development Factory: 開発ループと並行する実行の調整・ジョブの中の区間・利用上限・手元のランナーの同時のジョブの数 + +Neural Development Factory の利用者は、リポジトリごとに方針の違う開発ループを Web から始め・止め・予算を変え、一覧と詳細で周回と止まった理由を見る。同じリポジトリで並行する実行どうしの取り合い(課題・マイルストーン・NDF の版・同時の数)と、手元のランナーの同時のジョブの数は機械が守る。ランナーは 1 つの実行ジョブの中で `ndf-next` の区間をつなぎ、手元の relay.py と同じく続ける。 + +## 概要 + +- **実行をまたぐ制御は API と見回りに、ジョブの中の区切りはランナーに置く。** 次の周回は前の実行の終わり(停止の知らせ)と 5 分ごとの見回りで起こし、常駐のプロセスを持たない。常駐を置くと、ループが 1 本も動かないアカウントにも費用が掛かるためである +- **周回 1 回がスプリントの実行 1 本である。** ランナーは周回が締まった区間の後に次の区間を起動せず、次の周回は実行の制御が新しい実行で起こす。並行する実行の取り合いは実行を起こす時点でしか照らせないためである +- **課題の選定と周回の締めはランナーの中の NDF(`/ndf:development-workflow --loop`・`sprint-state.py loop`)が行う。** Factory は選定の規則を持たない +- 既存の手動のスプリントの実行(`loopId` が空)の振る舞いは変わらない + +## 用語 + +定義は [用語集](../glossary.md) が正である(文脈 `ndf-factory`・`ndf-workflow`)。この文書で使う語は「開発ループ」「周回」「マイルストーンの持ち分」「課題の持ち分」「手元のランナーの枠」「区間」「区間の上限」「同時のジョブの数」「人の判断待ち」「リセット時刻」である。 + +## コンテキスト + +| コンテキスト | 何の語が 1 つの意味に決まるか | +| --- | --- | +| Neural Development Factory(`ndf-factory`) | 開発ループ(集約)・スプリントの実行・実行の枠・マイルストーンの持ち分・課題の持ち分・手元のランナーの枠・区間・区間の上限・同時のジョブの数 | +| NDF の開発ワークフロー(`ndf-workflow`) | 開発ループ(`loop.json`)・周回・ループの予算・止まる条件・人の判断待ち・`ndf-next`・リセット時刻 | + +`ndf-workflow` が上流、`ndf-factory` が下流の腐敗防止層である。Factory の開発ループは NDF のループの記録(`.ndf/loop/<名>/loop.json`)を読まず、ランナーが翻訳した進捗の事象(`loop.lap_closed`・`loop.stopped`)だけを読む。翻訳はランナーの `factory/runner/src/loop-file.ts` と `translate.ts` だけが持つ。`ndf-next` のブロックの読み方は NDF の契約の一覧が公開された言語として宣言し、ランナーは形を写す。 + +## 構成要素 + +| 要素 | 場所 | 責務 | +| --- | --- | --- | +| ループの制御 | `factory/api/src/runs/loops.ts`(`LoopControl`) | 始める・止める・予算・答え・次の周回の操作、`startNextLap`(周回の開始の判定とトランザクション)、`wakeWaitingLoops`(待っているループを古い順に起こす)、停止の後の処理が使うループの状態の写し | +| 実行の制御 | `factory/api/src/runs/control.ts`(`RunControl`) | 手動の開始の課題の持ち分・マイルストーンの照合・版の照合・リポジトリの同時の数。停止の後の処理で `loop.*` と `job.finished` の理由を読み、ループの写しと `awaiting_reset` を同じトランザクションへ入れる。見回りの再開の時刻と待っているループ。`claimLocalRunJob` の手元のランナーの枠 | +| 実行の表 | `factory/api/src/runs/table.ts`・`dynamo-table.ts`・`memory-table.ts` | ループの行・持ち分の行・手元のランナーの枠の行と、3 集約をまたぐ条件つきの書き込み | +| 移行の埋め戻し | `factory/api/src/runs/backfill.ts` | 既存の終わりの状態でない実行を古い版として `legacyCount` と課題の持ち分へ写し、移行の済みの行を書く | +| 上限の判定 | `factory/api/src/accounts/limits.ts`(`judgeLimits`) | 理由 `concurrent_repo_runs`(リポジトリの枠の数と登録の宣言) | +| 取り込み | `factory/api/src/repositories/service.ts` | 登録の `maxConcurrentRuns` の宣言 | +| GitHub の口 | `factory/api/src/github/port.ts`・`app.ts` | 課題の確かめの応答にマイルストーンの題を返す | +| 進捗の記録の読み出し | `factory/api/src/progress/read.ts`(`ProgressReader`) | ジョブの `loop.lap_closed`・`loop.stopped` と、ループの詳細の周回の行を読む | +| 画面向けの経路 | `factory/api/src/routes/factory.ts` | ループの 7 経路・登録の宣言の経路・開始の 409 | +| ランナー向けの経路 | `factory/api/src/runs/runner-api.ts` | ループの実行の状態の置き場の鍵を `loops//ndf-state.tar.gz` にする | +| ランナーの区間 | `factory/runner/src/sections.ts` | 区間をつなぐ繰り返し・区間の上限・周回の締めの検出・利用上限の検出 | +| `ndf-next` の読み方 | `factory/runner/src/next-block.ts` | 応答の文からブロックと「起動するか」を読む | +| ループの記録の翻訳 | `factory/runner/src/loop-file.ts` | `loop.json` を読み、`loop.lap_closed`・`loop.stopped` と答えの書き込みの要否を作る | +| 同時のジョブの数の目安 | `factory/runner/src/capacity.ts` | CPU の数とメモリから目安を出し、申告と比べる | +| ランナーの手順 | `factory/runner/src/main.ts`・`runtime.ts`・`state.ts`・`translate.ts` | `START=loop` の手順、答えの書き込み、`NDF_LOOP_EXCLUDE` の受け渡し、状態の `.ndf/handoff/loop-*`、`base_conflict` の翻訳 | +| 契約 | `factory/contracts/schemas/*.json`・`openapi/runs.yaml`・`openapi/loops.yaml`・`schemas/local-runner-declaration.schema.json` | [開発ループのデータと契約](ndf-factory-dev-loops-data-and-contracts.md) の「外部連携」 | +| 画面 | `factory/web/src/pages/LoopsPage.tsx`・`LoopDetailPage.tsx`・`RepositoriesPage.tsx` | ループの一覧と始める・詳細と操作・同時の実行の数の宣言 | +| NDF の受け渡しの形 | `plugins/ndf/contracts/forms/ndf-next-block.json`・`scripts/contracts_lib/model.py` | `ndf-next` のブロックの読み方と例の組。構造チェックが形を確かめる | +| NDF のループの除外 | `plugins/ndf/scripts/lib/sprint_loop_select.py`・`plugins/ndf/contracts/env/NDF_LOOP_EXCLUDE.json` | `loop select` が環境変数の課題をその周回の除外へ足す | + +```mermaid +graph TD + subgraph sg1["画面(factory/web)"] + LP[ループの一覧] + LD[ループの詳細] + end + subgraph sg2["API(factory/api)"] + RT[画面向けの経路] + REPO[取り込み] + GH[GitHub の口] + LC[ループの制御] + RC[実行の制御] + LIM[上限の判定] + TBL[実行の表] + READ[進捗の記録の読み出し] + RAPI[ランナー向けの経路] + SW[停止の知らせと見回り] + end + subgraph sg3["実行ジョブ(ECS か手元)"] + MAIN[ランナーの手順] + SEC[ランナーの区間] + NB[ndf-next の読み方] + LF[ループの記録の翻訳] + CAP[同時のジョブの数の目安] + end + NDF["NDF(sprint-state.py loop・契約の一覧)"] + ING[進捗の受け口] + LP --> RT + LD --> RT + RT --> LC + RT --> REPO + RT --> RC + RC --> GH + LC --> RC + LC --> TBL + RC --> LIM + RC --> TBL + RC --> READ + SW --> RC + RC --> LC + MAIN --> SEC + SEC --> NB + SEC --> LF + MAIN --> NDF + LF --> NDF + MAIN --> RAPI + MAIN --> ING + CAP -.->|手元のランナーの登録と心拍| RC + READ --> ING +``` + +ループの制御は既存の API の handler と、停止の知らせと見回りの Lambda の中で動く。手元のランナーは利用者の環境で、`max_jobs` の数までジョブを別の作業場所で並べて動かす。 + +## 仕様 + +### 集約 + +| 集約 | 持ち主(書き換えてよいもの) | 根 | 値オブジェクト | +| --- | --- | --- | --- | +| 開発ループ | ループの制御(`LoopControl`) | ループ | 名前・選定の対象のマイルストーン・予算・状態・待ちの理由・止まった理由・人の判断待ちの写し・答え・締めた周回の和 | +| スプリントの実行 | 実行の制御 | スプリントの実行(エンティティ: 実行ジョブ) | 属するループ・周回の番号・終わりの理由・再開の時刻 | +| 実行の枠 | 実行の制御 | アカウントの枠 | リポジトリの枠の古い版の数・マイルストーンの持ち分・課題の持ち分・手元のランナーの枠 | +| リポジトリの登録 | 取り込み | 登録 | 同時の実行の数の宣言 | + +- ほかの集約は識別子で参照する。ループは登録を `repositoryId`、今の実行を `currentRunId` で指し、実行はループを `loopId` で指す +- 1 つの処理が書き換える集約は 1 つで、例外は「開発ループ・スプリントの実行・実行の枠」の 3 つを 1 つのトランザクションで書く周回の開始・実行の終わりへの移り・ループの取り消しである。ループの「今の実行」と実行の作成を別のトランザクションにすると、間で止まったときに見回りがもう 1 本を起こすためである。実行の制御は停止の後の処理でループの写しを同じトランザクションへ入れるが、書く中身はループの制御の関数が組む +- 持ち分と手元のランナーの枠は実行の枠の行である。取り合いの条件を、実行と同じトランザクションの条件つきの書き込みで守るためである(読んでから書くと同時の 2 本がどちらも通る) +- DynamoDB のトランザクションは 100 件まで書け、最も多い手動の開始(実行 1・ジョブ 1・枠 2・課題の持ち分 20・マイルストーンの持ち分の照合が題ごとに 1 件で多くて 20 の 44 件)でも足りる + +### 常に成り立つ条件 + +| # | 集約 | 条件 | 破れそうなときの扱い | +| --- | --- | --- | --- | +| I44 | 開発ループ | 1 つのループで終わりの状態でないスプリントの実行は 1 つまで(`currentRunId`) | 周回の開始は `currentRunId` が空で `lapRuns` が読んだ値のときだけ書く。条件で落ちれば何もしない | +| I45 | 実行の枠 | 同じリポジトリの終わりの状態でない 2 つのループの選定の対象のマイルストーンは重ならない | 持ち分の行を `attribute_not_exists` で書き、落ちれば 409 `milestone_in_use` | +| I46 | 実行の枠 | 枠の数は終わりの状態でない実行の数に等しい。`between_laps` のループは枠の行を持たない | ループは枠の行を書かない。枠は実行の作成と終わりへの移りでだけ増減する | +| I47 | 実行の枠 | 同じリポジトリで、1 つの課題を持つ終わりの状態でない実行は 1 つまで | 手動の開始が課題の持ち分の `attribute_not_exists` の書き込みと、課題のマイルストーンの持ち分の `attribute_not_exists` の照合を同じトランザクションで行う。前者で落ちれば 409 `issue_in_use`、後者で落ちれば 409 `issue_in_loop`。ループの周回へは、強い整合で読んだ持ち分の課題を `EXCLUDE_ISSUES` で渡す | +| I48 | 実行の枠 | 並行を許す版の下限より古い NDF の版の実行がリポジトリにあれば、その実行は唯一である | リポジトリの枠の `legacyCount` と `count` を条件にし、落ちれば 409 `parallel_unsupported_version`(ループは待つ)。移行の済みの行が無い間は、すべての版を古い版として扱う | +| I49 | 実行の枠 | リポジトリの終わりの状態でない実行の数は、プランの同時の数と登録の宣言の小さい方以下 | `judgeLimits` が `concurrent_repo_runs` を理由に断る(ループは待つ) | +| I50 | 実行の枠 | 1 つの手元のランナーが `running` で持つ `local` のジョブの数は、そのランナーが申告した `max_jobs` 以下 | 受け取りが手元のランナーの枠を条件に書き、落ちれば `null`(渡さない) | +| I51 | スプリントの実行 | 1 つの実行ジョブが起動する区間は区間の上限以下 | ランナーが次の区間を起動せず `job.finished`(`stopped`・`section_limit`)で終える | +| I52 | 開発ループ | `local` のループの次の周回は、利用者の「次の周回を始める」の操作か、設定 `LOCAL_LOOP_AUTOSTART=true` のときだけ起こる | 自動の起動は `waitReason` が `user_start` のループを飛ばす | +| I53 | 開発ループ | ループの状態は、属する実行の終わり方と `loop.stopped` の条件から、下の「ループの状態の写し方」の表のとおりにだけ決まる | 表に無い組は `stopped`(`run_failed`)にする | +| I54 | スプリントの実行 | `usage_limit` で終わるジョブと `awaiting_reset` の実行は、形態 `local` にだけある | ランナーは `local` 以外で利用上限を検出しない。制御は `local` 以外の `usage_limit` を `failed` として扱う | +| I55 | ランナー | ランナーと NDF の relay.py は、同じ応答の文から同じ `ndf-next` のブロックと同じ「起動するか」を読む | 契約の一覧の例の組を両方のテストが読み、どちらかが違えば落ちる | + +I8・I9・I32〜I43 は [スプリントの実行と実行ジョブの状態](ndf-factory-runs-state-and-data.md) と [リポジトリの登録・実行の制御・ランナー](ndf-factory-runs-and-runner.md) のまま変わらない。 + +### ドメインイベント + +| # | イベント | 発生元 | 受け手 | +| --- | --- | --- | --- | +| E1 | 開発ループを始めた | 実行の制御(画面の操作) | 実行の制御(E2) | +| E2 | 周回の実行を起こした(`START=loop`) | 実行の制御(`startNextLap`) | ランナー・実行の枠 | +| E3 | 周回の課題を選んだ | ランナーの中の NDF(`loop select`) | ランナー(`phase.changed` の課題で画面へ) | +| E4 | ジョブの中で次の区間を起動した(`section.started`) | ランナー | 進捗の受け口 → 画面の詳細 | +| E5 | 承認ゲート 2 で止まった | ランナー(`gate.reached`・`loop.stopped`(`gate2_returned`)) | 承認の依頼・実行の制御(ループを `awaiting_approval`) | +| E6 | 人の判断待ちの課題ができた | ランナー(`loop.lap_closed`・`loop.stopped` の `pending`) | 実行の制御(ループの写し)→ 画面の詳細・知らせ | +| E7 | ループが止まった(`loop.stopped`) | ランナー | 実行の制御(停止の後の処理で読む) | +| E8 | 周回の実行が終わった | 実行の制御(停止の後の処理) | 実行の制御(`startNextLap`・待っているループを起こす) | +| E9 | 取り込みで衝突を解けなかった(`attention`・`base_conflict`) | ランナー(NDF の `progress.jsonl` の行の翻訳) | 画面・知らせ | +| E10 | 次の周回を始める操作を待つ(`local`) | 実行の制御(E8 の後) | 画面(一覧の待ちの理由)・知らせ | +| E11 | 利用上限でジョブを終えた(`usage_limit`) | ランナー | 実行の制御(実行を `awaiting_reset`) | +| E12 | リセット時刻の後に再開を起こした | 実行の制御(見回り) | ランナー(`START=resume`) | +| E13 | 利用者がループを止めた(`cancelled`) | 実行の制御(画面の操作) | 実行の制御(`cancelSprintRun` と同じ形で今の実行を止める) | +| E14 | 手元のランナーが同時のジョブの数を申告した | 手元のランナー(登録と心拍) | 実行の制御(`claimLocalRunJob` の条件) | + +### ループの状態 + +```mermaid +stateDiagram-v2 + [*] --> running: 始めた(最初の周回が通った) + [*] --> between_laps: 始めた(枠・版で待つ) + running --> between_laps: 実行が done で loop.stopped が無い + running --> awaiting_approval: 実行が awaiting_approval + running --> stopped: loop.stopped(gate2_returned 以外)・実行が failed / stopped + running --> cancelled: 止めた(実行の停止の後) + awaiting_approval --> running: 答え(承認か差し戻し)で再開した + awaiting_approval --> cancelled: 止めた + between_laps --> running: 周回の開始が通った + between_laps --> stopped: 周回の開始が断られた(月の token・キー・導入) + between_laps --> cancelled: 止めた + stopped --> [*] + cancelled --> [*] +``` + +利用上限の `awaiting_reset` の間、ループは `running` のままである(周回は進行中)。 + +**ループの状態の写し方**(I53): + +| 実行の終わり方 | `loop.stopped` | ループ | 止まった理由 | +| --- | --- | --- | --- | +| `awaiting_approval` | `gate2_returned` か無し | `awaiting_approval` | — | +| `awaiting_reset` | — | `running` のまま | — | +| `done` | 無し | `between_laps` → `startNextLap` | — | +| `done` か `stopped` | `gate2_returned` 以外の条件 | `stopped` | 条件と `detail` | +| `failed` | 無し | `stopped` | `run_failed`(実行の `failureReason` を `detail` に) | +| `stopped` | 無し | `stopped` | `run_stopped`(`finishReason` を `detail` に) | +| `cancelled` | — | `cancelled` | — | + +- `loop.stopped` の条件は NDF の止まる条件の 10 個(`gate2_returned`・`user_stop`・`mvv_changed`・`same_failure`・`budget_laps`・`budget_usd`・`budget_hours`・`no_issue`・`outside_mvv`・`refused`)である。`gate2_returned` だけが周回の途中で止まる条件で、`awaiting_approval` へ写し、答えで `resume` が続く +- NDF が周回を失敗で締めた場合(`loop lap --failed`)はジョブが `done` で終わり、`same_failure` の数え方は NDF に残る。ジョブそのものが落ちた(`failed`)ときは NDF が周回を締めておらず、起こし続けると同じ失敗を数えないまま繰り返すため、ループを止める +- **止まった・取り消したループは終わりで、戻る遷移を持たない。** 続けたい利用者は同じマイルストーンで新しいループを始める(持ち分は止まった時点で消えている)。戻る遷移を持つと、どの条件が戻れるかの表が要るためである +- **ループを止めるときに NDF の `loop stop` を打たない。** 動いている実行は今の取り消し(`cancelSprintRun`)で止まり、ランナーは `SIGTERM` で何も送らずに終わる。取り消したループには次のジョブが起きず、止める印を書いても読む者がいないため、知らせはループの `cancelled` そのものとする +- 予算の変更は終わりの状態でないループにだけ効き、次のジョブの指示から NDF の `--budget-*` へ渡る + +### 周回の開始(`startNextLap`) + +```mermaid +sequenceDiagram + participant S as 呼ぶ側(始める・停止の後・見回り・操作) + participant L as ループの制御 + participant C as 実行の制御 + participant T as 実行の表 + S->>L: startNextLap(loopId) + L->>T: ループを読む(between_laps・currentRunId 空) + alt local で自動が許されず、操作でない + L->>T: waitReason = user_start + else + L->>C: 導入・キー・NDF の版を決める + C->>T: 枠・リポジトリの枠・課題の持ち分を読む + C->>C: judgeLimits(concurrent_* / monthly_tokens)と版の照合 + alt 同時の数か古い版で断られた + L->>T: waitReason = slots / legacy_version(GSI へ) + else 月の token・キー・導入で断られた + L->>T: stopped(limits_exceeded / launch_refused)・持ち分を消す + else 許された + L->>T: 1 回のトランザクション: ループ(条件 lapRuns・currentRunId 空)・実行・ジョブ・枠 + alt 条件で落ちた + L->>L: 読み直して 3 回まで。ループが変わっていれば何もしない + else + L->>C: launch(ECS か渡す口) + end + end + end +``` + +- **`EXCLUDE_ISSUES` は、トランザクションの直前に強い整合(`ConsistentRead`)で読んだ同じリポジトリの課題の持ち分の番号である。** 読みと手動の開始の間は、マイルストーンの持ち分の行で直列になる。ループの作成がマイルストーンの持ち分を書いた後に通る手動の開始はその行の照合で落ち、作成より前に通った手動の開始の持ち分はこの読みに必ず現れる +- **起こすのは 4 か所だけである**: 始めた直後・停止の後の処理(そのループ)・停止の後の処理で枠が減ったとき(同じアカウントの待っているループを `wakeWaitingLoops` が古い順に 1 本ずつ)・5 分ごとの見回り(`WAITING_LOOPS` をすべて)。`local` の `user_start` は操作だけが起こす(I52) +- **`local` の次の周回と人の判断待ちは、承認の依頼にしない。** 承認の依頼の種類はゲート 1・2 に限られ、サービスの側で承認を足さないためである。`local` のループは `between_laps`(`waitReason: user_start`)で待ち、一覧の行と詳細に「次の周回を始める」を出す。人の判断待ちの答えはループの行に持ち、次のジョブが `loop answer` で書く(今の実行を再開しない) + +### 課題とマイルストーンの取り合い + +- 手動の開始は課題ごとに、ループの開始はマイルストーンごとに持ち分の行を `attribute_not_exists` で書き、実行・ループと同じトランザクションで消す。ループの周回が選ぶ課題は持ち分の行を書かない。ループどうしはマイルストーンで分かれ、手動の実行とは `EXCLUDE_ISSUES` と `issue_in_loop` で分かれる +- 手動の開始は、動いているループのマイルストーンに属する課題を断る(409 `issue_in_loop`)。マイルストーンをループに持たせた以上、外から課題を差すと周回の選定と同じ課題を取り合うためである +- 手動の開始のトランザクションは、課題の属するマイルストーンの持ち分の行の照合(`ConditionCheck`)を、課題ごとでなく題ごとに重複を除いて 1 件にする。DynamoDB のトランザクションは同じ項目への 2 つの操作を `ValidationException` で拒むためである。マイルストーンの無い課題は照合の行を持たない +- **開始の確かめの順**(既存の 8 段のうち): 3 の課題の確かめでマイルストーンの題を引き、`issue_in_loop` を 3 の直後に断る。`issue_in_use` と `parallel_unsupported_version` は 8 のトランザクションの条件で落ち、読み直して理由を返す。8 のトランザクションは題ごとのマイルストーンの持ち分の照合も持ち、3 の直後の照合を通った後に作られたループとの取り合いで落ちれば 409 `issue_in_loop` を返す +- **ループの開始**は、登録の確かめ・名前の重なり・マイルストーンの持ち分と、ループの行を 1 回のトランザクションで書き、続けて `startNextLap` を呼ぶ。最初の周回が枠で待つときも 201(`between_laps`)を返す +- 選定から外す課題は、NDF の `loop select` が環境変数 `NDF_LOOP_EXCLUDE` の番号をその周回の除外(`excluded`、理由「別の実行が持つ課題」)へ足して受ける。`loop exclude` は選定の後にしか打てず、ランナーはその間に割り込めないためである。手元の relay.py で複数のループを並べるときも同じ口を使える + +### NDF の版の照合 + +- リポジトリの枠の `legacyCount` に、並行を許す版の下限より古い版の終わりの状態でない実行の数を持つ。古い版の実行は `count` が 0 のとき、新しい版の実行は `legacyCount` が 0 のときだけ書く +- 比べるのは版の `@` より前を semver として読んだ値で、開発版の接尾辞は semver の前置きの版として小さく読む。下限は設定 `NDF_PARALLEL_MIN_VERSION` が持ち(コードに埋め込まない)、無ければすべての版を古い版として扱い、同じリポジトリの実行を 1 本に絞る +- 実行の行を読んで版を数えない。読んでから書く間に別の開始が通るためである + +### 停止の後の処理(`finalize`) + +既存の順(`job.finished` を読む → 実行と枠を移す → 最後にジョブを終わりへ)は変わらず、実行と枠を移すトランザクションに次が入る。 + +| 書くもの | いつ | +| --- | --- | +| 実行を `awaiting_reset`(`resumeAt`)にする | `job.finished` が `stopped`・`usage_limit` で、形態が `local`(I54。ほかの形態では `failed`) | +| 実行の `finishReason` | `job.finished` に `reason` がある | +| 課題の持ち分を消す(`sprintRunId` がその実行の行だけ)・`legacyCount` を減らす(実行の `legacy` が真のときだけ) | 実行が終わりの状態へ移る。実行を移す書き込みに読んだ時の `legacy` の値を条件として入れ(真・偽・`attribute_not_exists(legacy)`)、落ちたら実行と持ち分を読み直してやり直す。読んだ後に埋め戻しが `legacy` を真にしたとき、減算と削除を漏らさないためである | +| 手元のランナーの枠を減らす | ジョブを終わりへ移す最後の書き込み(`runnerId` がある) | +| ループの写し(上の表)・`lapsClosed` などの和・`pending`・持ち分の削除 | 実行が `loopId` を持つ。`loop.lap_closed` と `loop.stopped` をジョブの冪等の鍵の範囲で読む | + +トランザクションが通った後に、ループが `between_laps` なら `startNextLap`、枠が減ったなら `wakeWaitingLoops` を呼ぶ。どちらも落ちてよく、次の見回りが同じ結果で起こし直す。 + +### 利用上限(形態 `local`) + +- `local` で利用上限に当たったジョブは `job.finished`(`stopped`・`usage_limit`・`resets_at`)で終え、実行は `awaiting_reset` になる。`awaiting_reset` は終わりの状態でなく、枠を数え、承認待ちと同じく今のジョブは空である +- 見回りがリセット時刻を過ぎた実行に `START=resume` のジョブを作る。`APPROVAL_REQUEST_ID` の無い `resume` は答えを書かずに続け、ランナーは conductor へ「前回の続きから進めてください。」だけを渡す。新しい `START` の値を足さないのは、状態の復元と conductor の起動が `resume` と同じで、違いが答えの有無だけのためである +- リセット時刻を読めないときの待ちは `USAGE_LIMIT_RETRY_SECONDS`(既定 900 秒)である。NDF の `supervise_lib/claude.py` の `LIMIT_RETRY` と同じ値にし、同じ利用者の機械で同じ上限に当たったときの振る舞いを揃える +- 検出は NDF と同じ文言の規則(`usage limit reached|`・`resets <時刻> (<時間帯>)`)と、`result` の `is_error` で読む +- `managed`・`byok` では同じ文が来ても今の `failed` の扱いになる。アカウントの切り替えは手元で行い、サービスはアカウントを知らない +- リセット時刻の後の自動の再開は、`local` でサービスがジョブを自動で起こす唯一の例外である。次の周回の自動の起動は `LOCAL_LOOP_AUTOSTART`(既定 `false`)が許すときだけである + +### ランナーの区間 + +`START` の 3 つ(`new`・`resume`・`loop`)に共通である。 + +```mermaid +sequenceDiagram + participant M as ランナーの手順 + participant S as ランナーの区間 + participant Cl as claude -p + participant N as NDF(loop.json) + participant I as 進捗の受け口 + M->>S: 最初の指示(new・resume・loop) + loop 区間ごと + S->>I: section.started(番号・コマンド) + S->>Cl: claude -p <指示> + Cl-->>S: 最後の result + alt local で利用上限 + S->>M: usage_limit(リセット時刻) + else ループの周回が締まった・止まる条件が書かれた + S->>M: 終える + else ndf-next が 1 つで結果が判断が要るでない + alt 区間の上限に達している + S->>M: section_limit + else + S->>S: ブロックの中身を次の指示にする + end + else + S->>M: 終える(今の判定のまま) + end + end + M->>N: loop.json を読む(ループの実行だけ) + M->>I: loop.lap_closed・loop.stopped + M->>M: 状態を保存(gate・usage_limit・ループの実行) + M->>I: gate.reached(あれば)・job.finished(結果・理由・リセット時刻) +``` + +- 区間の上限は `RUNNER_SECTION_LIMIT`(既定 20)で、最初の起動を数える。20 回目の `claude -p` の後に次を起動しない。既定 20 は、手元の relay の記録 14 本(2026-10-25〜11-02)の区間の数の最大 9 の 2 倍を超え、通常のジョブを打ち切らない値である +- 周回の締めの判定は区間ごとに行う。ジョブの始めの `loop.json` の締めた周回の数より増えたか、`stop` がジョブの始めより後に書かれていれば、`ndf-next` があっても次の区間を起動しない +- 使用量(`usage.recorded`)は区間ごとに送る。打ち切りの時間はジョブの全体に掛かり、区間で延びない +- `progress.jsonl` の `attention` の行のうち `step` が `base-sync` のものは、翻訳が `reason` を `base_conflict` へ写す。ほかの行の理由は変えない。Factory の語へ変えるのは腐敗防止層の仕事で、NDF の側の理由の語は増やさない + +### `START=loop` のランナーの手順 + +既存の手順 1〜4 と 8 は変わらず、5〜7 が次になる。`START=resume` でも `LOOP_NAME` があれば 5・6 の鍵と書き込みは同じである。 + +| 手順 | 中身 | +| --- | --- | +| 5. 状態の復元 | `POST /run-jobs/state`(`get`)。鍵はループの `loops//ndf-state.tar.gz`。404 は最初の周回として空から始める。戻すのは `.ndf/tmp`・`.ndf/loop`・`.ndf/handoff/loop-.md`(追跡するファイルは断る) | +| 6. 答えの書き込み | `LOOP_ANSWERS` の各組のうち、`loop.json` の人の判断待ちに答えの無い番号だけ `sprint-state.py loop answer <名> <番号> --approved \| --rejected [--pr <番号>]` を打つ | +| 7. conductor | `/ndf:development-workflow --loop --milestones [--budget-laps …]`。子の環境に `NDF_LOOP_EXCLUDE=` を置く | + +### 手元のランナーの同時のジョブの数 + +- `claimLocalRunJob(accountId, {runnerId, maxJobs})` は、そのランナーの手元のランナーの枠が `maxJobs` 未満のときだけ、ジョブを `running` にする書き込みと枠を 1 増やす書き込みを 1 回のトランザクションで行う(I50)。引数の 2 つ目が無い呼び出しは `max_jobs` 1・ランナーの識別子なしとして扱う +- 申告の値の保存は手元のランナーの登録の集約に属し、この仕様は受け取りの条件と申告の形と目安の関数を持つ +- 目安は `max(1, min(floor(CPU の数 × 3 / 4), floor(メモリの GiB / 5)))` である(2026-09-25 の実測、8 コア・30 GB で全体テスト 6 本から 1 本あたり 4/3 コア・5 GiB)。申告が目安を超えれば手元のランナーが標準エラーへ 1 行の警告を出し、申告はそのまま送る。手元の機械の資源を知るのは手元だけで、サービスは申告を切り詰めない + +### 予算の残りと費用 + +一覧の「費用」「周回の数」「予算の残り」は `loop.lap_closed` の和(ループの行の `lapsClosed`・`closedCostUsd`・`closedSeconds`)で出す。止まる条件を判定する NDF は締めた周回の費用と秒の和を予算と比べるため、`usage.recorded` の和で出すと画面の残りと止まる判定が食い違う。今の周回の分は、詳細の画面で実行ごとの `usage.recorded` の和として別に出す。 + +### 画面 + +| 画面 | 経路 | 何をする場所か | +| --- | --- | --- | +| ループ | `/accounts/:accountId/repositories/:repositoryId/loops` | リポジトリのループの一覧・始める欄 | +| ループの詳細 | `/accounts/:accountId/loops/:loopId` | 周回・実行と区間・人の判断待ちと答え・予算の変更・止める・次の周回を始める | +| リポジトリ | 既存 | 行に「ループ」への移りと、持ち主に同時の実行の数の宣言の欄 | + +経路と役割の一覧は [画面の地図](ndf-factory-screen-map.md) にある。 + +| 画面 | 項目 | 型 | 必須 | 上限 | 満たさないときの表示 | +| --- | --- | --- | --- | --- | --- | +| ループ | 名前 | 文字列 | 必須 | 64 字・英数字と `-` `_` | 「英数字と - _ で 64 字までにしてください」 | +| ループ | マイルストーン | 開いているマイルストーンの複数選択 | 必須 | 10 件 | 「マイルストーンを 1 つ以上選んでください」 | +| ループ・詳細 | 予算(周回の数・費用(USD)・時間) | 正の数 | 任意 | — | 「0 より大きい数を入れてください」 | +| 詳細 | 答え | 承認・差し戻し、設計 PR の番号(承認のとき任意) | 必須 | — | — | +| リポジトリ | 同時の実行の数 | 整数か空 | 任意 | 1〜100 | 「1 から 100 の整数を入れてください」 | + +| 状態 | 出すもの | +| --- | --- | +| 読み込み中・403・5xx | 既存の画面と同じ | +| ループが 0 件 | 「まだループがありません」と始める欄 | +| `between_laps` | 待ちの理由: `slots`「同時に動かせる実行の空きを待っています」・`legacy_version`「並行できない版の実行が終わるのを待っています」・`user_start`「次の周回を始める」のボタン | +| `stopped` | 止まった理由(条件の名前と `detail` の要点)。`budget_*` は「新しいループで続けられます」 | +| 実行が `awaiting_reset` | 「利用上限のため <時刻(利用者の時間帯)> に再開します」 | +| 409 `milestone_in_use` | 「<題> はループ <名前> が選んでいます」 | +| 人の判断待ちが 0 件 | 節を出さない | + +権限: 一覧・詳細・操作は席を持つ人、同時の実行の数の欄は持ち主だけに出す(API も 403)。 + +## データ・設定 + +実行の表の行・移行の埋め戻し・環境変数と、外部との契約(実行ジョブの指示・進捗の事象・NDF の受け渡しの形・手元のランナーの申告・画面向けの経路)は [開発ループのデータと契約](ndf-factory-dev-loops-data-and-contracts.md) にある。 + +## エラー処理 + +- 周回の開始・ループの写し・終わりへの移りは条件つきの 1 回のトランザクションで書き、実行の制御のどの処理が途中で止まっても、見回りか知らせの再送で同じ結果へやり直せる(1 つの周回の実行を 2 本起こさない) +- 周回の開始の条件で落ちたときは読み直して 3 回までやり直し、ループが変わっていれば何もしない +- 同時の数か古い版で断られた周回はループを `between_laps`(`slots` / `legacy_version`)で待たせ、月の token・キー・導入で断られた周回はループを `stopped`(`limits_exceeded` / `launch_refused`)にして持ち分を消す +- `loop.stopped` が届かずにジョブが終わったループは、実行の終わり方から `run_failed` か `run_stopped` で `stopped` になる + +## セキュリティ + +- サービスは `local` のアカウントと認証を知らない。利用上限の事象はリセット時刻だけを持つ +- ループの操作は、そのリポジトリの登録を持つアカウントだけが行える。ループの経路は `resolveFromRequest` の後にループの `accountId` と登録を照らし、他人は 403、無ければ 404 になる +- 同時の実行の数の宣言は持ち主だけが変えられる + +## テスト観点 + +- ループを始め・止め・予算を変える経路と、周回のジョブの指示が `START=loop`・`LOOP_NAME` を持つこと。`factory/api/src/` に選定の規則が無いこと +- 「ループの状態の写し方」の表の各行の組で、ループの状態と止まった理由が表のとおりになり、10 個の条件のどれも `loop.stopped` として受かること +- 枠が満ちたアカウントでループが `between_laps`(`slots`)で待ち、枠の行の数が実行の数と等しく、別の実行が終わると待っていたループの周回が起きること +- 重なるマイルストーンの 2 本目が 409 `milestone_in_use`、重ならない 2 本目が 201 で、同時の 2 つの開始で片方だけが通ること +- 動いている実行の課題を指す手動の開始が 409 `issue_in_use`、ループのマイルストーンの課題が 409 `issue_in_loop` になり、周回の指示の `EXCLUDE_ISSUES` に持ち分の課題が並び、NDF の `loop select` が `NDF_LOOP_EXCLUDE` の課題をその周回の除外にすること +- 下限より古い版の実行があるリポジトリで 2 本目が 409 `parallel_unsupported_version` になり、下限が設定に無ければ 2 本目が断られ、下限以上の版どうしは並ぶこと +- 移行の済みの行が無い間は下限以上の版でも 2 本目が断られ、埋め戻しの後に既存の実行の課題を指す手動の開始が 409 `issue_in_use` になること。埋め戻しを 2 回流しても `legacyCount` が増えず、埋め戻す前に終わった実行で `legacyCount` が負にならないこと +- `step` が `base-sync` の `attention` の行だけが `reason: base_conflict` の事象になること +- プランの値 3 で宣言 1 のリポジトリで 2 本目が `concurrent_repo_runs` で断られ、宣言が無ければプランの値で断られること +- `max_jobs` 1 のランナーへ 2 本目を渡さず、1 本目が終わると渡し、申告が無ければ 1 として扱うこと。目安が 8 コア・30 GiB で 6、2 コア・4 GiB で 1 になり、目安を超える申告で警告の 1 行が出て申告は変わらないこと +- 答えると次のジョブの `LOOP_ANSWERS` に載り、ランナーが `loop answer` を打ち、答えの済んだ番号は打たないこと。ループの実行の `gate.reached` が `loop` を持つこと +- `local` の周回の後にループが `user_start` で待ち、`next-lap` で起き、`LOCAL_LOOP_AUTOSTART=true` なら待たず、`managed`・`byok` は待たないこと +- 偽の `claude` が `ndf-next` を 1 つ返すと次の区間が起き、`判断が要る`・ブロック無し・2 つでは起きず、区間ごとに `section.started` が届くこと。21 回目を起動せず `section_limit` で終わり、打ち切りの時間は区間の合計に掛かること +- 契約の一覧の例の組を NDF の pytest と Factory の vitest が読んで同じ答えを返し、`check-service-boundary.py` が通ること +- `local` の偽の `claude` が上限の文を返すと `usage_limit` で終わって実行が `awaiting_reset` になり、時刻の後の見回りで `APPROVAL_REQUEST_ID` の無い `resume` が積まれ、時刻が読めなければ 900 秒後になること。`managed`・`byok` では `failed` になること +- 契約の検査が新しい欄と種類を通して生成した型に現れ、`loop` に `ISSUES` を持つ指示が断られること +- 手動の開始・一覧・取り消し・再開の既存のテストが変えずに通ること +- 周回の開始のトランザクションの後で止めても、2 つの呼び手が同時に `startNextLap` を呼んでも、実行が 1 本だけになること + +## 関連リンク + +- [サービスの境界と全体の契約](ndf-factory-service-boundary.md) +- [リポジトリの登録・実行の制御・ランナー](ndf-factory-runs-and-runner.md) +- [スプリントの実行と実行ジョブの状態・実行の表](ndf-factory-runs-state-and-data.md) +- [契約の定義と進捗の受け口](ndf-factory-contracts-and-progress.md) +- [認証とアカウント・席・プラン](ndf-factory-accounts-and-plans.md) +- [画面の地図](ndf-factory-screen-map.md) +- [開発ループのデータと契約](ndf-factory-dev-loops-data-and-contracts.md) +- [NDF の入口の契約](ndf-entrypoint-contracts.md) diff --git a/docs/specifications/ndf-factory-runs-state-and-data.md b/docs/specifications/ndf-factory-runs-state-and-data.md index f816d0635..ddcc163aa 100644 --- a/docs/specifications/ndf-factory-runs-state-and-data.md +++ b/docs/specifications/ndf-factory-runs-state-and-data.md @@ -108,7 +108,7 @@ erDiagram | スプリントの実行 | `RUN#` | `RUN` | `ACCOUNT#` / `RUN#` | | 実行ジョブ | `JOB#` | `JOB` | `ACTIVE_JOBS` / `#`(終わりの状態でない間だけ。終わりへ移すときに外す) | -列の名前は `factory/api/src/runs/model.ts` の型の名前のまま(camelCase)である。#2347 が足した行(開発ループ・課題とマイルストーンの持ち分・手元のランナーの枠・移行の済みの行)と列(実行の枠の `legacyCount`、実行の `legacy`・`resumeAt`・`loopId` など)は [#2347 の設計の「データ構造」](../../issues/issue-2347-design.md#データ構造) にある。 +列の名前は `factory/api/src/runs/model.ts` の型の名前のまま(camelCase)である。#2347 が足した行(開発ループ・課題とマイルストーンの持ち分・手元のランナーの枠・移行の済みの行)と列(実行の枠の `legacyCount`、実行の `legacy`・`resumeAt`・`loopId` など)は [開発ループの確定仕様の「実行の表の行」](ndf-factory-dev-loops-data-and-contracts.md#実行の表の行) にある。 | 行 | 列 | 型 | 空 | 意味 | | --- | --- | --- | --- | --- | diff --git a/docs/specifications/ndf-factory-ui-components.md b/docs/specifications/ndf-factory-ui-components.md index 697b3df05..02c189884 100644 --- a/docs/specifications/ndf-factory-ui-components.md +++ b/docs/specifications/ndf-factory-ui-components.md @@ -76,11 +76,63 @@ Neural Development Factory の Web の画面を組む部品の一覧と使い分 | `non-ascii` | ASCII の外の文字を含む文字列・テンプレート(JSX の外も) | | `dummy-import` | `src/main.tsx` 以外から `dummy/` を import する | +- API が 400・409・422 で返す `message` は API が日本語で持ち、画面はそのまま出す。翻訳のファイルの対象ではない + ## API クライアントとダミー - 画面は `useServices()` の `api`(`Api` のインタフェース)だけを通してデータを読む。足す操作は配列で受け渡す(1 件のときも要素 1 個の配列) +- 画面は API の応答の形(`factory/contracts/openapi/`)をそのまま受け、自分の語へ翻訳する層を置かない - ダミーの実装は `src/dummy/` にあり、`Api`・`Auth`・つながりの元のダミーとダミーデータを持つ。操作を足したらダミーにも同じ名前の関数を足す -- 起動の組み立て(`src/main.tsx`)が Vite の mode `dummy` のときだけダミーを動的に読む。本番のビルドでは分岐ごと落ち、`npm run check:bundle -w web` が成果物にダミーデータの印 `ndf-factory-dummy-data` が無いことを確かめる +- 起動の組み立て(`src/main.tsx`)が Vite の mode `dummy` のときだけダミーを動的に読む。本番のビルドでは `import.meta.env.MODE` が `"production"` に置き換わって分岐ごと落ち、`npm run check:bundle -w web` が成果物にダミーデータの印 `ndf-factory-dummy-data` が無いことを確かめる(終了コードは 0 無し・1 印を見つけた・2 `dist/` が無い) +- ダミーデータに秘密と実在の個人情報を書かない。利用者の ID は `00000000-…` の形、名前は架空のものにする + +| 操作 | 入力 | 出力 | 失敗の形 | +| --- | --- | --- | --- | +| `sprintRunDetails(accountId, sprintRunIds)` | アカウントの ID と実行の ID の配列 | `SprintRunSummary[]`。見つからない ID の要素は返さない | `ApiFailure`(401・403・5xx) | + +`sprintRunDetails` の本物の実装は、詳細の経路がまだ無いため `GET /accounts/{a}/sprint-runs` の新しい順 50 件から ID で選ぶ。それより古い実行は見つからない扱いになる。詳細の経路ができたら本物の実装だけを差し替え、画面は変えない。 + +## つながりの状態 + +`src/connection.ts` の `ConnectionSource`(`current()` と `subscribe(listener)`)が `ConnectionState` を渡し、枠の `ConnectionStatus` と承認の操作の `ApprovalActions` が `useSyncExternalStore` で読む。 + +| 値 | 型 | 意味 | +| --- | --- | --- | +| `online` | 真偽 | 今サーバーへつながっているか | +| `lastReceivedAt` | UTC の ISO 8601 か `null` | 最後に受け取った時刻 | +| `pendingOperations` | 数 | つながってから確定する操作の数 | + +- 3 つは独立した値で、オフラインで確定待ちがある場面も表せる。`ConnectionStatus` はオンライン・オフライン(最後に受け取った時刻つき)・確定待ちの操作がある、を見分けて出す +- ブラウザの実装は `navigator.onLine` と `online` / `offline` の事象だけを読み、`lastReceivedAt` は `null`、`pendingOperations` は 0 を返す。時刻と確定待ちの数を作る実装は、同じ `ConnectionSource` を差し替えて足す +- `App` は `services` に `connection` が無いときブラウザの実装を、`settings` が無いときブラウザから決めた言語と地域の設定を、`resources` が無いとき日本語の翻訳のファイルを使う + +## 常に成り立つ条件 + +| 条件 | 破れたときに落ちるもの | +| --- | --- | +| 画面のコード(テスト・ダミー・翻訳のファイルを除く)に表示の文言が無い | `check:source`(`npm run check`) | +| 画面のコードが使う翻訳のキーは日本語の翻訳のファイルにある | 型検査 | +| ブラウザの言語に翻訳のファイルが無いとき、文言は日本語で出る | Vitest | +| 日時は UTC の ISO 8601 で受け、設定の時刻帯で整形する | Vitest | +| 金額は通貨と組(`Money`)でだけ整形の関数へ渡せる | 型検査 | +| 部品の見た目の値はトークンの名前で引く | トークンの写しのテスト | +| `src/ui/` の部品は「部品の一覧」に 1 つずつ載る | `docs.test.ts` | +| ログインの後の画面は 360・768・1280 px のどの幅でも横にはみ出さない | E2E | +| 画面は `Api` だけを通してデータを読み、ダミーを import するのは `src/main.tsx` だけ | `check:source` の `dummy-import` | +| 本番のビルドの成果物にダミーの実装とダミーデータが入らない | `check:bundle` | +| 0 件は `EmptyState`、失敗は `ErrorState` を出し、画面は白くならない | 部品のテスト | + +## 設計の判断 + +- **国際化は i18next と react-i18next で行う。** 日本語の翻訳のファイルを `CustomTypeOptions` の `resources` に渡してキーの型を作り、埋め込み・複数形・既定の言語への戻りを i18next に任せる。言語の判定は追加のパッケージを使わず、`navigator.languages` から翻訳のある言語を選ぶ +- **日時・数・金額は `Intl` で整形する。** 必要な整形を `Intl.DateTimeFormat`(`timeZone` を渡す)と `Intl.NumberFormat`(`style: "currency"`)が持ち、日時のライブラリを足すと依存の費用だけが増えるためである +- **直に書いた文言の検査は `oxc-parser` で源を読む自前のスクリプト(`web/scripts/check-source.ts`)にする。** ESLint の TypeScript の構文解析(`typescript-eslint`)は `factory/` の TypeScript 7 と組めず、`oxc-parser` は TypeScript に依存しないためである +- **ダミーと本物の切り替えは Vite の mode `dummy` で `src/main.tsx` の 1 か所が行う。** 画面の設定へ環境変数を足すと画面へ埋め込む変数が増え、mode なら本番のビルドで値が定数になって分岐ごと落とせるためである +- **経路は `screens.ts` の画面の地図から作り、確定仕様の表と識別子の集合を照らす。** 役割ごとの動線と機能との対応はコードに持たない情報のため、文書を生成せず、経路と部品の名前の集合だけをテストで照らす +- **トークンは Design Tokens Format Module の JSON に置き、起動時に CSS の変数へ写す。** ビルドの手順(Style Dictionary など)を足さず、Tailwind CSS や CSS-in-JS のように正を設定ファイルへ移したり実行時の依存を足したりしないためである +- **配置は Playwright の Chromium で測り、E2E は `npm run check` の外の CI の手順にする。** jsdom は配置を計算せず、ブラウザの導入が要るためである。iOS の Safari と Android の Chrome での描かれ方は、リリース後テストで人が見る +- **境は 768 px と 1024 px の 2 つにし、同じ部品が CSS だけで形を変える。** 画面のコードが幅を知らずに済むためである +- **`NavMenu` は `