From 2038aa273517a87305c6a2913ebef6e4d34d68da Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 4 Nov 2026 20:48:53 +0000 Subject: [PATCH 1/7] =?UTF-8?q?Docs:=20#2474=20=E3=81=AE=E8=A6=81=E6=B1=82?= =?UTF-8?q?=E3=81=A8=E5=8F=97=E3=81=91=E5=85=A5=E3=82=8C=E6=9D=A1=E4=BB=B6?= =?UTF-8?q?=E3=81=AE=E5=86=99=E3=81=97=E3=82=92=E7=BD=AE=E3=81=8F=EF=BC=88?= =?UTF-8?q?=E5=91=A8=E5=9B=9E=E3=81=AE=E9=80=94=E4=B8=AD=E3=81=AE=E6=AD=A2?= =?UTF-8?q?=E3=82=81=E3=82=8B=E6=8C=87=E7=A4=BA=E3=82=92=E5=86=8D=E9=96=8B?= =?UTF-8?q?=E3=81=A7=E8=A7=A3=E3=81=8D=E3=80=81=E6=AD=A2=E3=82=81=E3=81=A6?= =?UTF-8?q?=E3=81=84=E3=81=9F=E6=99=82=E9=96=93=E3=82=92=E7=B5=8C=E9=81=8E?= =?UTF-8?q?=E6=99=82=E9=96=93=E3=81=8B=E3=82=89=E5=A4=96=E3=81=99=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- issues/issue-2474-requirements.md | 153 ++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 issues/issue-2474-requirements.md diff --git a/issues/issue-2474-requirements.md b/issues/issue-2474-requirements.md new file mode 100644 index 000000000..021fbe7af --- /dev/null +++ b/issues/issue-2474-requirements.md @@ -0,0 +1,153 @@ +# #2474: 開発ループ: 周回の途中で止める指示を置くと、再開で解けず、止めていた時間が経過時間に入る + +正は課題の本文(#2474)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。 + +## 依頼(原文) + +> ## 何が起きたか +> +> 開発ループ lfactory2 で、周回の途中に `loop stop lfactory2` を打って止める指示(`.ndf/loop/lfactory2/stop`)を置き、セッションを切った(2026-11-02 22:08 JST)。2026-11-04 15:01 JST に再開した。 +> +> 1. `loop start lfactory2` は `in_lap` を返し、止める指示のファイルを消さなかった(`stop` は止まった記録が無いため、再開の扱い `_resume_user_stop` を通らない) +> 2. 周回 4 を `loop lap lfactory2 --failed` で締めると、境の判定が残ったファイルを読み、`user_stop` で止まった。本当に止めたい理由(プロジェクト MVV が版 4 → 5 に改訂された `mvv_changed`)は出なかった +> 3. 周回 4 の秒に、止めていた約 41 時間が入った(消費の hours が 6.19 → 47.14)。確定仕様の「止まっていた時間は経過時間に入れない」に反し、予算 72 時間の 6 割を止めていた時間で使った +> +> ## 期待 +> +> - 周回の途中の止める指示は、`loop start` の再開で解ける(周回の境の止まりと同じ) +> - 止める指示を置いてから再開までの時間は、経過時間に数えない +> +> ## 再現 +> +> 1. 周回を組んだ状態(進行中)で `sprint-state.py loop stop <名>` を打つ +> 2. 時間を置いて `loop start <名>`(`in_lap` が返る) +> 3. `loop lap <名> --failed` → `user_stop` で止まり、周回の秒に止めていた時間が入る +> +> ## 範囲 +> +> `plugins/ndf/scripts/lib/sprint_loop.py`(`cmd_start` の in_lap の経路)・`sprint_loop_record.py`(`boundary_stop`・周回の秒)。 + +(書き手: 起票者、日付: 2026-11-04。開発ループを回していた AI が、起きたことの記録として書いた本文である。利用者の指示の原文は含まない。「止める指示を置いてセッションを切り、再開した」という操作は利用者のものだが、その操作に添えた利用者の文は本文に引用されていない) + +## 起票者の推定 + +題・本文のうち、利用者の指示の原文に無く起票者が推して足した条件。承認ゲート 1 で利用者が確かめるまで、利用者の決定として扱わない。 + +| # | 条件 | 推した出所 | この要求での扱い | +| --- | --- | --- | --- | +| S1 | 周回の途中に置いた止める指示は、`loop start` の再開で解ける(周回の境で止まったときと同じ) | 本文の「期待」の 1 行目。確定仕様 `docs/specifications/ndf-workflow-development-loop.md` の再開の表(`user_stop` はファイルを消して解ける)からの類推 | 受け入れ条件 1・2・3 | +| S2 | 止める指示を置いてから再開までの時間は、ループの予算の経過時間に数えない | 本文の「期待」の 2 行目。確定仕様の不変条件 I8(止まっていた時間は経過時間に入らない) | 受け入れ条件 4・5、前提 1 | +| S3 | 直す場所は `cmd_start` の周回の途中の経路と、境の判定・周回の秒 | 本文の「範囲」 | 直す場所は設計が決める。受け入れ条件は場所を指定しない | +| S4 | 「周回の秒」に止めていた時間が入った | 本文の「何が起きたか」の 3 | 採らない。記録を見ると、周回 4 の秒(計画の行の秒の和)は 0 で、止めていた時間が入ったのはループの予算の経過時間(消費の hours)である(下の「目的」の実測) | + +## 目的 + +周回の途中で開発ループを止めて後から再開したとき、止める指示が再開で解け、止めていた時間がループの予算を使わない状態にする。利用者がセッションを切ってループを休ませても、再開した後に余分な止まりが 1 回挟まらず、予算の残りが休ませる前と同じに保たれる。 + +実際に起きた値(`.ndf/loop/lfactory2/loop.json` の記録、時刻は UTC): + +| 項目 | 値 | +| --- | --- | +| 周回 4 の開始 | 2026-11-02 13:04:20 | +| 止める指示のファイルの中身(置いた時刻) | 2026-11-02 13:08:53 | +| 周回 4 を締めた時刻(`user_stop` の止まりの時刻) | 2026-11-04 06:01:43 | +| 周回 4 の秒(計画の行の秒の和) | 0 | +| 消費の hours(周回 4 の前 → 後) | 6.19 → 47.14 | + +周回 4 の区間(開始から締めるまで、約 40.95 時間)がそのまま経過時間に入った。止める指示の時刻から締めた時刻までの約 40.88 時間は、止まりの記録(`stops`)のどの行にも重ならないため引かれなかった(`user_stop` の行は締めた時刻 06:01:43 に始まり 06:02:11 に解けている)。 + +## 前提 + +- 前提 1: 止まっていた区間の始まりは、止める指示のファイルに書かれた時刻(`loop stop` を打った時刻)とする。止める指示を置いた後もセッションが周回を進めていた場合、その時間も経過時間から外れる。セッションがいつ止まったかを記録する仕組みが今は無く、止める指示の時刻が手に入る最も近い値であるため、この扱いを採る +- 前提 2: 止める指示のファイルの中身が時刻として読めない(空・壊れている)ときは、ファイルの更新時刻を区間の始まりとする +- 前提 3: 止まっていた区間の終わりは、`loop start` で再開した時刻とする +- 前提 4: 周回の途中でない(進行中の周回が無い)ときに `loop start` が止める指示を見つけた場合も、止める指示を解く。`loop start` を打つこと自体が、利用者が再開を求めた印であるため。この場合、止まっていた区間は周回の区間の外にあるため、経過時間は変わらない +- 前提 5: 既に止まっている記録(`status` が `stopped`)の再開は、今の再開の表のとおりに動く。この課題では変えない + +## 対象範囲 + +含む: +- `loop start` が、止まっていない記録(進行中の周回がある・無いの両方)で止める指示を見つけたときに、止める指示を解く振る舞い +- 周回の途中で止める指示を置いてから再開するまでの区間を、止まりの記録へ残し、ループの予算の経過時間から外すこと +- 確定仕様 `docs/specifications/ndf-workflow-development-loop.md` の、再開の表と不変条件 I3・I8 の記述を振る舞いに合わせること + +含まない: +- 既存のループの記録(lfactory2 など)の値を直すこと。lfactory2 は `mvv_changed` で止まっており再開しない +- 周回の秒(計画の行の秒の和)の数え方。止めていた時間は入っていない(起票者の推定 S4) +- 止める指示が周回の途中で効く(周回の途中で止まる)ようにすること。止める指示は今のとおり次の周回の境で効く +- 境の判定の順(`user_stop` を `mvv_changed` より先に見ること)を変えること。止める指示が再開で解ければ、次の境で `mvv_changed` が出る +- セッションが止まった時刻を記録する仕組み(前提 1) + +## ドメインイベント + +| # | イベント | 引き金 | 失敗したとき | 順序の前提 | +| --- | --- | --- | --- | --- | +| E1 | 周回を始めた | `loop sprint` | 周回を組めなければ `refused` で止まる(今のとおり) | — | +| E2 | 止める指示を置いた | 利用者の `loop stop` | ループの記録が無ければ断る(今のとおり) | — | +| E3 | セッションが終わった | 利用者がセッションを切る・端末が落ちる | 記録は残らない(前提 1) | E2 の後とは限らない | +| E4 | 再開を求めた | 利用者の `loop start` | 使ってよい条件に当たらなければ断る(今のとおり) | E2 | +| E5 | 止める指示を解いた | E4 で止める指示のファイルがある | ファイルを消せなければ再開を断り、記録を書き換えない | E4 | +| E6 | 止まっていた区間を記録した | E5 で進行中の周回がある | 区間の始まりが読めなければ前提 2 の時刻を使う | E5 | +| E7 | 周回を締めた | `loop lap` | 今のとおり | E1 | +| E8 | 境で止まる条件を判定した | E7 の後・選定の前 | 今のとおり | E5 が先に起きていれば `user_stop` は当たらない | + +## 用語 + +| 用語 | 意味 | +| --- | --- | +| 開発ループ | スプリントを 1 つ終えるたびに次のスプリントを選んで続けて始める進め方(用語集) | +| 周回 | 開発ループの 1 回分(用語集) | +| ループの予算 | 開発ループが使ってよい量の上限。周回の数・費用・経過時間で数える(用語集)。この文書の「経過時間」は、確定仕様の I8 の「動いていた時間」(終わった周回の区間の和から、止まりの記録と重なる部分を引いたもの。結果の `consumed.hours`)を指す | +| 止まる条件 | 開発ループを止める、機械で判定できる条件(用語集)。止める指示は `user_stop` に当たる | +| ループの記録 | `.ndf/loop/<名>/loop.json`(用語集)。止まりの記録は、その中の解けた止まりの一覧(`stops`)を指す | + +止める指示は、`loop stop` が置くファイル `.ndf/loop/<名>/stop`(中身は置いた時刻)を指す。 + +## 受け入れ条件 + +- [ ] 1. 前提: 進行中の周回があり、止める指示のファイルがある。操作: `loop start <名>`。結果: 終了コード 0、`status` が `in_lap`、止める指示のファイルが消えている、結果に止める指示を解いたことが出る +- [ ] 2. 前提: 1 の再開の後。操作: その周回を `loop lap <名>`(`--failed` の有無を問わない)で締める。結果: 境の判定で `user_stop` に当たらない。ほかの止まる条件(`mvv_changed` など)に当たればそれで止まり、当たらなければ次の周回へ進む +- [ ] 3. 前提: 進行中の周回が無く、止まっていない記録に止める指示のファイルがある。操作: `loop start <名>`。結果: 終了コード 0、`status` が `resumed`、止める指示のファイルが消えている。続く `loop select` が `user_stop` で止まらない +- [ ] 4. 前提: 周回を始めた時刻 T0、止める指示を置いた時刻 T1、再開した時刻 T2、周回を締めた時刻 T3(T0 < T1 < T2 < T3)。結果: 締めた後の経過時間(`consumed.hours`)の、この周回の分が (T1 − T0) + (T3 − T2) になる。T2 − T1 は入らない +- [ ] 5. 4 の止まっていた区間(T1〜T2)が、ループの記録の止まりの記録に、周回の番号・始まり T1・終わり T2 つきで 1 行残る。`laps.md` の周回の行の止まりの欄に `user_stop` が出る +- [ ] 6. 止める指示のファイルの中身が時刻として読めないとき、1 と 4 の振る舞いは、ファイルの更新時刻を T1 として成り立つ +- [ ] 7. 止める指示のファイルが無いときの `loop start` の結果(`in_lap` / `resumed`)と、ループの記録の中身は変わらない +- [ ] 8. 既に止まっている記録(`stopped`)の再開は、止まった理由ごとに今と同じ結果になる。既存のテスト(`plugins/ndf/scripts/tests/test_sprint_loop.py`)が通る +- [ ] 9. 確定仕様 `docs/specifications/ndf-workflow-development-loop.md` の再開の表と I3・I8 が、受け入れ条件 1〜5 の振る舞いを書いている + +## 影響 + +| 対象 | 影響 | +| --- | --- | +| 公開インタフェース | `sprint-state.py loop start` の結果に、止める指示を解いたことを示す値が加わる。既存の値は変わらない(互換あり) | +| データ | ループの記録の止まりの記録に、周回の途中で止める指示を解いた行(`user_stop`)が加わる。形は既存の行と同じ。移行は無い | +| 既存の振る舞い | 止まっていない記録の `loop start` が、止める指示のファイルを消すようになる。止まっていた区間が経過時間から外れ、ループの予算の `hours` が今より少なく数えられる | + +## 検証手段 + +| 項目 | 手段 | +| --- | --- | +| テスト | `uv run --frozen --project . --all-extras pytest plugins/ndf/scripts/tests/test_sprint_loop.py -q`(受け入れ条件 1〜8。時刻は `Env.now` の差し替えで進める) | +| テスト(全体) | `uv run --frozen --project . --all-extras pytest . -q -n 4` | +| 静的解析 | `ruff format --check` と `ruff check`(継続的統合の `lint.yml` と同じ) | +| 手動確認 | 次に開発ループを回すとき、周回の途中で `loop stop` → セッションを切る → `loop start` を通し、止める指示が消え、`laps.md` の経過時間に休ませた時間が入らないことを見る(リリース後テスト) | + +## 前提とする取り決め + +| 項目 | 参照先 / 決めたこと | +| --- | --- | +| プロジェクト構造 | 開発ループのコードは `plugins/ndf/scripts/lib/sprint_loop*.py`。外の世界は `Env` を通して呼び、テストで差し替える(`sprint_loop_record.py` の先頭の説明) | +| コーディング規約 | `AGENTS.md` の「最小限のコード実装」。`.md` の文言を照合するテストは書かない | +| テスト戦略 | 単体テストで `cmd_start`・`cmd_lap`・経過時間の計算を、差し替えた `Env` と時刻で確かめる(既存の `test_stop_file_works_at_the_next_boundary_and_resume_removes_it` と同じ階層) | + +## 境界 + +| 区分 | 内容 | +| --- | --- | +| 常に行う | 既存テストの実行、確定仕様の該当箇所の更新 | +| 確認してから行う | `loop start` の結果の形の変更(承認ゲート 1・2 で見る) | +| 行わない | 既存のループの記録の書き換え、境の判定の順の変更、止める指示を周回の途中で効かせること | + +## 未決 + +無し(決められない点は前提 1〜5 に置いた。前提 1 の扱いは承認ゲート 1 で利用者が確かめる) From f465a94626b00fad8696209b362de775f20fa31b Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 4 Nov 2026 20:52:18 +0000 Subject: [PATCH 2/7] =?UTF-8?q?Docs:=20#2474=20=E3=81=AE=E8=A8=AD=E8=A8=88?= =?UTF-8?q?=E3=82=92=E6=9B=B8=E3=81=8F=EF=BC=88=E5=91=A8=E5=9B=9E=E3=81=AE?= =?UTF-8?q?=E9=80=94=E4=B8=AD=E3=81=AB=E7=BD=AE=E3=81=84=E3=81=9F=E6=AD=A2?= =?UTF-8?q?=E3=82=81=E3=82=8B=E6=8C=87=E7=A4=BA=E3=82=92=E5=86=8D=E9=96=8B?= =?UTF-8?q?=E3=81=A7=E8=A7=A3=E3=81=8D=E3=80=81=E6=AD=A2=E3=82=81=E3=81=A6?= =?UTF-8?q?=E3=81=84=E3=81=9F=E6=99=82=E9=96=93=E3=82=92=E4=BA=88=E7=AE=97?= =?UTF-8?q?=E3=81=AE=E7=B5=8C=E9=81=8E=E6=99=82=E9=96=93=E3=81=8B=E3=82=89?= =?UTF-8?q?=E5=A4=96=E3=81=99=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- issues/issue-2474-design.md | 300 ++++++++++++++++++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 issues/issue-2474-design.md diff --git a/issues/issue-2474-design.md b/issues/issue-2474-design.md new file mode 100644 index 000000000..ec725a1eb --- /dev/null +++ b/issues/issue-2474-design.md @@ -0,0 +1,300 @@ +# 開発ループ: 周回の途中に置いた止める指示を再開で解き、止めていた時間を予算の経過時間から外す(#2474) + +## 目的 + +- **今どうなっているか**: 周回の途中で `loop stop` を打ってセッションを切り、後から `loop start` で再開すると、止める指示のファイルが残る。周回を締めた境でそのファイルを読み、`user_stop` で止まり直す。止めていた時間は周回の区間に入ったまま、止まりの記録のどの行にも重ならないため、ループの予算の経過時間に数えられる(lfactory2 の周回 4 で約 41 時間、予算 72 時間の約 6 割) +- **誰に効くか**: 開発ループを使う利用者。周回の途中でループを休ませてセッションを切る使い方をする人 +- **変えると何が成り立つか**: `loop start` の再開で止める指示が解ける。休ませた区間は止まりの記録に 1 行残り、経過時間から外れる。再開の後に余分な止まりが挟まらず、予算の残りが休ませる前と同じに保たれる + +## 適用範囲 + +- **働く範囲**: NDF の `sprint-state.py loop` の副命令(`start`・`lap`・`select`)。開発ループを使う配布先のリポジトリでも同じように働く +- **プロジェクトごとに違うもの**: 無し(足す設定・引数は無い。止める指示のファイルの置き場と中身は今のまま) +- **当たるモード**: 開発ループで回すすべてのモード(`pace: auto`) + +## あるべき姿の根拠 + +| 根拠 | 種類 | 何を示すか | +| --- | --- | --- | +| `.ndf/loop/lfactory2/loop.json` の記録(要求の「目的」の表): 周回 4 の開始 2026-11-02 13:04:20、止める指示 13:08:53、締めた時刻 2026-11-04 06:01:43、消費の hours 6.19 → 47.14、`user_stop` の行は 06:01:43〜06:02:11 | 実測 | 止めていた区間が止まりの記録に無く、経過時間から引かれていない | +| 確定仕様 `docs/specifications/ndf-workflow-development-loop.md` の I8(止まっていた時間は経過時間に入らない)と再開の表(`user_stop` はファイルを消して解ける) | 既存の規則 | 止める指示は再開で解けること、止まっていた区間を経過時間から外すことが、すでに決まった形である | +| `plugins/ndf/scripts/lib/sprint_loop.py` の `cmd_start`(止まっていない記録では `in_lap` / `resumed` を返すだけで、止める指示のファイルを見ない) | 実測(コードの読み) | 止める指示を消すのは、止まった記録の再開(`_resume_user_stop`)の経路だけである | + +要求と受け入れ条件は #2474 の本文にある(コピーは `issues/issue-2474-requirements.md`)。この文書は「どう作るか」だけを扱う。 + +この文書での呼び方は次のとおりにする。 + +| 呼び方 | 意味 | +| --- | --- | +| 止める指示 | `loop stop` が置くファイル `.ndf/loop/<名>/stop`。中身は置いた時刻 | +| 止まりの記録 | ループの記録(`loop.json`)の `stops`。解けた止まりの一覧(追記だけ) | +| 経過時間 | 確定仕様の I8 の「動いていた時間」。終わった周回の区間の和から、止まりの記録と重なる部分を引いたもの(結果の `consumed.hours`) | +| 止まっていない記録 | ループの記録の `status` が `running` のもの(`stopped` でないもの) | +| 休ませた区間 | 止める指示を置いた時刻 T1 から、`loop start` で再開した時刻 T2 まで | + +## ドメインモデル + +### コンテキスト + +| コンテキスト | 何の語が 1 つの意味に決まるか | +| --- | --- | +| NDF の開発ワークフロー(`ndf-workflow`) | 開発ループ・周回・ループの予算・止まる条件・ループの記録・止める指示 | + +1 つのコンテキストに収まる。ほかのコンテキストとの関係は変えない。 + +### 集約 + +| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | +| --- | --- | --- | --- | --- | +| 開発ループ | `sprint_loop.py` の副命令(記録の読み書きは `sprint_loop_record.py` の関数を通す) | ループの記録(`loop.json`) | 周回(`laps[]`) | 止まり(`stop` / `stops[]` の 1 行)・予算・消費 | + +止める指示のファイルは集約の外にある入力である。置くのは利用者の `loop stop`、消すのは `loop start` だけにする。 +この変更で書き換えるのは開発ループの集約だけで、1 回の `loop start` が書き換える集約も 1 つである。 + +### 不変条件 + +| # | 集約 | 条件 | 破れたときの扱い | +| --- | --- | --- | --- | +| I1 | 開発ループ | 止まっていない記録の再開と、`user_stop` で止まった記録の再開では、`loop start` が終了コード 0 を返した後に止める指示のファイルが無い | テストが落ちる | +| I2 | 開発ループ | 止まっていない記録で進行中の周回があるときに止める指示を解いたら、止まりの記録に `condition: user_stop`・その周回の番号・`at` = T1・`resumed_at` = T2 の行がちょうど 1 行足される。進行中の周回が無いときは行を足さない | テストが落ちる | +| I3 | 開発ループ | 休ませた区間(T1〜T2)は、その周回の経過時間に入らない。周回の分は (T1 − T0) + (T3 − T2) になる | テストが落ちる | +| I4 | 開発ループ | T1 は T2 を超えない。止める指示の中身が時刻として読めないときは、ファイルの更新時刻を T1 にする | テストが落ちる | +| I5 | 開発ループ | 止める指示を消せなかったら、`loop start` は断り、ループの記録を書き換えない | テストが落ちる | +| I6 | 開発ループ | 止める指示のファイルが無いときの `loop start` の結果とループの記録は、今と同じである | 既存のテストが落ちる | + +### ドメインイベント + +| # | イベント | 発生元 | 受け手 | +| --- | --- | --- | --- | +| E2 | 止める指示を置いた | 利用者の `loop stop` | `loop start`(E5)・周回の境の判定(E8) | +| E4 | 再開を求めた | 利用者の `loop start` | `cmd_start` | +| E5 | 止める指示を解いた | `cmd_start`(止まっていない記録で止める指示がある) | 結果の `released_stop`・周回の境の判定(`user_stop` に当たらなくなる) | +| E6 | 休ませた区間を記録した | `cmd_start`(E5 で進行中の周回がある) | 経過時間の計算(`active_seconds`)・`laps.md` の描画 | +| E8 | 境で止まる条件を判定した | `loop lap` の後・`loop select` の前(`boundary_stop`) | 止まる条件の判定。E5 が先に起きていれば `user_stop` は当たらない | + +要求の E1(周回を始めた)・E3(セッションが終わった)・E7(周回を締めた)は振る舞いを変えないため、この表に載せない。 +E3 は記録が残らないため受け手を持たない(要求の前提 1)。 + +### 用語 + +| 用語 | 意味 | 用語集への反映 | +| --- | --- | --- | +| 開発ループ | スプリントを 1 つ終えるたびに次のスプリントを選んで続けて始める進め方 | 無し(既存の語をそのまま使う) | +| 周回 | 開発ループの 1 回分 | 無し | +| ループの予算 | 開発ループが使ってよい量の上限。周回の数・費用・経過時間で数える | 無し | +| 止まる条件 | 開発ループを止める、機械で判定できる条件 | 無し | +| ループの記録 | `.ndf/loop/<名>/loop.json` | 無し | + +新しい語は足さない。「止める指示」「休ませた区間」はこの文書の中の呼び方で、用語集へは足さない。 + +## 機能一覧 + +| # | 機能 | 誰が使うか | +| --- | --- | --- | +| F1 | 周回の途中でループを休ませた後、`loop start` で再開すると止める指示が解ける | 開発ループを回す利用者と conductor | +| F2 | 周回の外(進行中の周回が無い)で置いた止める指示も、`loop start` で解ける | 同上 | +| F3 | 休ませた区間が止まりの記録と `laps.md` に残り、ループの予算の経過時間から外れる | 同上 | + +## 構成要素 + +| 要素 | 責務 | 変える / 変えない | +| --- | --- | --- | +| `cmd_start`(`plugins/ndf/scripts/lib/sprint_loop.py`) | 止まっていない記録の経路で、止める指示があれば解く関数を呼び、結果に `released_stop` を載せる | 変える | +| `release_stop_request`(新設。`sprint_loop.py`) | 止める指示の時刻 T1 を読み、ファイルを消し、進行中の周回があれば止まりの記録へ 1 行足す。消せなければ断る結果を返す | 作る | +| `stop_request_time`(新設。`plugins/ndf/scripts/lib/sprint_loop_record.py`) | 止める指示のファイルから T1 を読む(中身 → 読めなければ更新時刻)。T2 を超えれば T2 に揃える | 作る | +| `lap_rows`(`sprint_loop_record.py`) | `laps.md` の周回の行の「止まった理由」に、その周回の止まりを起きた順にすべて並べる | 変える | +| `active_seconds`(`sprint_loop_record.py`) | 止まりの記録の `at`〜`resumed_at` と周回の区間の重なりを引く | 変えない(足した行をそのまま読む) | +| `boundary_stop`(`sprint_loop_record.py`) | 周回の境で止まる条件を判定する(順も変えない) | 変えない | +| `resume_stopped` / `_resume_user_stop`(`sprint_loop.py`) | 止まった記録の再開 | 変えない(要求の前提 5) | +| 確定仕様 `docs/specifications/ndf-workflow-development-loop.md` | 再開の表・I3・I8・`start` の契約・ループの記録の置き場の表を振る舞いに合わせる | 変える | + +```mermaid +graph TD + U[利用者の
loop start] --> S[cmd_start] + S -->|止まった記録| R[resume_stopped
今のまま] + S -->|止まっていない記録| L[release_stop_request] + L --> T[stop_request_time] + L -->|周回の途中なら
1 行足す| J[(ループの記録の
止まりの記録)] + L -->|消す| F[(止める指示の
ファイル)] + J --> A[active_seconds
経過時間] + J --> M[lap_rows
laps.md] +``` + +図に含めない要素: 周回の境の判定(`boundary_stop`。変えず、止める指示が無いことを読むだけ)と確定仕様(文書)。 + +### 値の集合へ値を足す影響 + +止まりの記録(`stops`)へ、周回の途中で解いた `user_stop` の行が加わる。行の形(`condition`・`values`・`lap`・`at`・`resumed_at`)は +既存の行と同じである。止まりの記録を読む関数を `grep -rn "stops" plugins/ndf/scripts --include=*.py` で集め、テストを除いた +当たりを 1 つずつ判定した(2026-11-04。`merged_lib/merge.py` と `test_strategy.py` の当たりは別のオブジェクトの `stops` 属性で、 +ループの記録を読まない)。 + +| 読む関数 | 新しい行への当てはまり | 扱い | +| --- | --- | --- | +| `active_seconds` | 当てはまる。`at`〜`resumed_at` と周回の区間の重なりを引くのが、この変更の狙いそのものである | 変えない | +| `failure_streak` | 当てはまる。`same_failure` の行だけを区切りに使い、`user_stop` の行は読み飛ばす | 変えない | +| `lap_rows` | 当てはまらない。周回ごとに止まりを 1 つだけ(後の行で上書き)持つため、休ませた後に同じ周回が別の理由で止まると `user_stop` が消える | 変える(決定 4) | +| `resume_record` | 書く側。止まった記録の再開で行を足す | 変えない | + +## 入出力の契約 + +`sprint-state.py loop start` の結果に 1 つ値を足す。既存の値は変えない。 + +| 場合 | 終了コード | `metrics.status` | 足す値 | +| --- | --- | --- | --- | +| 止まっていない記録・進行中の周回あり・止める指示あり | 0 | `in_lap` | `released_stop: {at, source, lap}`。`summary` に「止める指示を解いた」を足す | +| 止まっていない記録・進行中の周回なし・止める指示あり | 0 | `resumed` | `released_stop: {at, source, lap: null}`。同上 | +| 止める指示が無い | 今のまま | 今のまま | 足さない(キー自体を出さない) | +| 止める指示を消せない | 1 | `refused` | `items` に `{path, reason}`。ループの記録は書かない | +| 止まった記録(`stopped`) | 今のまま | 今のまま | 足さない(要求の前提 5) | + +`released_stop` の値: + +| キー | 型 | 意味 | +| --- | --- | --- | +| `at` | 文字列(ISO 8601、UTC) | 休ませた区間の始まり T1 | +| `source` | `content` / `mtime` | T1 を止める指示の中身から読んだか、更新時刻から読んだか | +| `lap` | 整数 / `null` | 止まりの記録へ行を足した周回の番号。進行中の周回が無ければ `null` | + +止まりの記録へ足す行(形は既存の行と同じ): + +```json +{"condition": "user_stop", "values": {"at": "<止める指示の中身のまま>"}, "lap": 4, "at": "", "resumed_at": ""} +``` + +`values.at` は周回の境の判定(`boundary_stop`)が `user_stop` に書く値と同じく、ファイルの中身をそのまま残す。 +中身が読めなかった場合も、空・壊れた中身を `values.at` に残し、`at` に更新時刻を入れる。 + +## 処理の流れ + +```mermaid +sequenceDiagram + participant U as 利用者 + participant S as cmd_start + participant F as 止める指示 + participant R as ループの記録 + U->>S: loop start 名 + S->>R: 読む(排他の中) + alt status が stopped + S->>S: resume_stopped(今のまま) + else 止まっていない記録 + S->>F: ある? + alt 無い + S-->>U: in_lap / resumed(今のまま) + else ある + S->>F: T1 を読む(中身 → 更新時刻) + S->>F: 消す + alt 消せない + S-->>U: 終了コード 1(記録を書かない) + else 消せた + opt 進行中の周回がある + S->>R: 止まりの記録へ 1 行(T1〜T2) + end + S->>R: 書く + S-->>U: in_lap / resumed と released_stop + end + end + end +``` + +`cmd_start` の中の順は、予算の上書き(`apply_budget`)→ 止める指示を解く → 記録を書く、にする。 +止める指示を消すのは記録を書く前である(決定 3)。続く `loop lap` の後の境の判定は今のまま `boundary_stop` を通り、 +止める指示が無いため `user_stop` に当たらず、`mvv_changed` などの次の条件を見る。 + +## 決定の記録 + +### 決定 1: 再開を求めた印を 1 か所で受けるため、止まっていない記録の止める指示を `loop start` で解く + +止まった記録の `user_stop` は、すでに `loop start` で解ける。止まっていない記録で止める指示を残すと、同じ利用者の操作 +(`loop stop` → `loop start`)が、周回の境の前か後かで違う結果になる。`loop start` を打つこと自体を再開の印として扱い、 +どちらの経路でも止める指示を消す。 + +周回の境の判定(`boundary_stop`)の側で「再開より前に置かれた止める指示を読み飛ばす」形は採らない。再開の時刻を別に +持つ必要があり、止める指示のファイルも残り続けるため、`loop status` と利用者の目に止める指示が残って見える。 + +根拠: Value 7 / Vision(MVV 版 5) + +### 決定 2: 休ませた区間を経過時間から外すため、止まりの記録へ既存と同じ形の行を足す + +経過時間の計算(`active_seconds`)は、止まりの記録の `at`〜`resumed_at` と周回の区間の重なりをすでに引く。休ませた区間を +同じ形の 1 行として足せば、計算の側を変えずに I8 が成り立つ。`laps.md` の止まりの欄にも同じ経路で出る。 + +周回に「休ませた秒」の欄を新しく足す形は採らない。経過時間の計算と `laps.md` の両方に読み手を足すことになり、止まりの +記録と同じ意味の値を 2 か所に持つ。 + +根拠: Value 7 / Value 8(MVV 版 5) + +### 決定 3: 消せなかったときに記録だけが進まないよう、止める指示を消してから記録を書く + +止める指示を先に消し、消せなければ記録を書かずに終了コード 1 で断る(要求の E5)。記録を先に書いてから消す順では、 +消せなかったときに止まりの記録の行だけが残り、次の境で同じ止める指示が `user_stop` として効き、同じ区間が 2 回扱われる。 + +消した後に記録の書き込みが失敗した場合は、例外がそのまま上がる(今の `write_record` の失敗と同じ扱い)。このとき +休ませた区間は経過時間から外れないが、止める指示は解けており、再開の後に余分な止まりは挟まらない。 + +根拠: Value 1(MVV 版 5) + +### 決定 4: 休ませた事実が後の止まりで消えないよう、`laps.md` の止まりの欄に周回の止まりをすべて並べる + +今の `lap_rows` は周回ごとに止まりを 1 つだけ持ち、後の行で上書きする。休ませた周回が締めた後の境で `mvv_changed` などで +止まると、`user_stop` が欄から消える。止まりの記録の順(起きた順)に、条件を `、` で区切って並べる。止まりが 1 つの周回の +欄は今と同じ文字列になる。 + +`laps.md` へ休ませた区間だけの別の表を足す形は採らない。周回の一覧の 1 つの欄で足りる。 + +根拠: Value 8(MVV 版 5) + +### 決定 5: 時刻の誤りで経過時間を負に数えないよう、T1 を T2 以下に揃える + +止める指示の中身が未来の時刻(手で書いた・時計のずれ)だと、`at` が `resumed_at` より後の行になる。重なりの計算は 0 を +返すが、記録として読めない行が残る。T1 が T2 を超えたら T2 にそろえる(区間の長さは 0)。T1 が周回の開始 T0 より前の +とき(周回を組む前に置いた止める指示)は揃えない。重なりの計算が周回の区間で切るため、引きすぎない。 + +根拠: Value 1(MVV 版 5) + +## テスト設計 + +置き場は `plugins/ndf/scripts/tests/test_sprint_loop.py`。時刻は `Env.now` の差し替えで進める。 + +| 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | +| --- | --- | --- | +| 受け入れ条件 1・I1 | 進行中の周回があり止める指示があるとき、`loop start` が 0・`in_lap`・ファイルなし・`released_stop` ありを返す | 止まっていない記録の経路で止める指示を見ないように戻すと落ちる | +| 受け入れ条件 2 | 1 の後に `loop lap`(`--failed` あり・なし)を締めると、境の判定が `user_stop` に当たらない。MVV を改訂しておけば `mvv_changed` で止まる | ファイルを消さずに `released_stop` だけを返すと落ちる | +| 受け入れ条件 3・I2 の後半 | 進行中の周回が無いとき、`loop start` が 0・`resumed`・ファイルなしを返し、止まりの記録に行が増えず、続く `loop select` が `user_stop` で止まらない | 周回が無いときにも行を足すと落ちる | +| 受け入れ条件 4・I3 | T0 < T1 < T2 < T3 の順に時刻を進めると、`consumed.hours` の周回の分が (T1 − T0) + (T3 − T2) になる | 行の `at` に T2 を書く・`resumed_at` を書かないと落ちる | +| 受け入れ条件 5・I2 の前半 | 止まりの記録に `user_stop`・周回の番号・`at` = T1・`resumed_at` = T2 の行がちょうど 1 行あり、`laps.md` の周回の行の止まりの欄に `user_stop` が出る | 行を 2 回足す・周回の番号を書かないと落ちる | +| 決定 4 | 休ませた周回がその後 `mvv_changed` で止まっても、`laps.md` の欄に `user_stop` と `mvv_changed` の両方が出る | 後の止まりで上書きする今の形に戻すと落ちる | +| 受け入れ条件 6・I4 | 止める指示の中身が空・時刻でないとき、ファイルの更新時刻が T1 になり、`source` が `mtime` になる。中身が T2 より後の時刻なら `at` が T2 になる | 中身が読めないときに T2 を使う・揃えずに未来の時刻を残すと落ちる | +| I5 | 止める指示を消せない(消す関数を失敗させる)とき、終了コード 1 で、ループの記録の中身が呼ぶ前と同じ | 記録を先に書いてから消す順にすると落ちる | +| 受け入れ条件 7・I6 | 止める指示が無いとき、`loop start` の結果に `released_stop` が無く、`in_lap` / `resumed` とループの記録が今と同じ | ファイルの有無を見ずに行を足すと落ちる | +| 受け入れ条件 8 | 既存の `test_resume_stopped_*` と `test_stop_file_works_at_the_next_boundary_and_resume_removes_it` が通る | 止まった記録の再開の経路を変えると落ちる | +| 受け入れ条件 9 | 確定仕様の再開の表・I3・I8・`start` の契約が振る舞いを書いている。文言を照合するテストは書かず、コードレビューで読む | — | + +## 確定仕様の直し方 + +`docs/specifications/ndf-workflow-development-loop.md` を次のとおりに直す(受け入れ条件 9)。 + +| 箇所 | 直す内容 | +| --- | --- | +| 常に成り立つ条件の I3 | 止める指示は周回の境でだけ効く、を残し、「止まっていない記録の `loop start` は止める指示を消し、進行中の周回があれば休ませた区間(止める指示の時刻〜再開)を `stops` へ `user_stop` の行として足す」を加える | +| 常に成り立つ条件の I8 | 引く区間に、周回の途中で解いた止める指示の行(I3)が含まれることを書く | +| 副命令の表の `start` | 出力に `released_stop`(`{at, source, lap}`。止める指示を解いたときだけ)を足す。止める指示を消せないときの 1 を足す | +| 再開の表 | 「止まっていない記録に止める指示がある」の行を足す(ファイルを消し、周回の途中なら区間を `stops` へ足す。`in_lap` / `resumed` のまま) | +| ループの記録の置き場の表 | `stop` の行を「`loop stop` が置き、`loop start` で消す」にする | +| `stop` / `stops` の形の段落 | 周回の途中で解いた行も同じ形であることを書く | +| テストの表 | 受け入れ条件 1〜6 の縛りを 1 行足す | + +## 設計の結果 + +| 課題 | 扱い | 取り込み先 | 触るファイル | +| --- | --- | --- | --- | +| #2474 | 実装する | — | `plugins/ndf/scripts/lib/sprint_loop.py`、`plugins/ndf/scripts/lib/sprint_loop_record.py`、`plugins/ndf/scripts/tests/test_sprint_loop.py`、`docs/specifications/ndf-workflow-development-loop.md` | + +## 未確認のまま残ること + +| 項目 | 内容 | +| --- | --- | +| 止める指示を置いた後もセッションが周回を進めていた時間 | 要求の前提 1 のとおり、その時間も経過時間から外れる。セッションが止まった時刻を記録する仕組みが無いため、実際に動いていた時間との差は測れない。承認ゲート 1 で利用者が前提 1 を確かめる | +| 消した後の記録の書き込みの失敗 | 決定 3 のとおり、休ませた区間が経過時間に残る。発生の頻度は未確認(今の `write_record` の失敗と同じ扱い) | +| `loop stop` と `loop start` の取り合い | `loop stop` は排他を取らない。`loop start` が止める指示を読んで消す間に別の `loop stop` が書くと、後の指示も消える。同時に打つ使い方は確認していない | +| リリース後の実機確認 | 次に開発ループを回すとき、周回の途中で `loop stop` → セッションを切る → `loop start` を通し、`laps.md` の経過時間に休ませた時間が入らないことを見る(要求の検証手段) | From 981917d2b325d3d2e0ccc07a534ca47ee9b86ea7 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 4 Nov 2026 20:54:46 +0000 Subject: [PATCH 3/7] =?UTF-8?q?Docs:=20#2474=20=E3=81=AE=E8=A8=AD=E8=A8=88?= =?UTF-8?q?=E6=96=87=E6=9B=B8=E3=82=92=E7=B5=84=E3=81=BF=E7=9B=B4=E3=81=99?= =?UTF-8?q?=EF=BC=88=E6=9C=80=E9=95=B7=E6=96=87=E3=82=92=E5=88=86=E3=81=91?= =?UTF-8?q?=E3=80=81=E7=9B=AE=E7=9A=84=E3=81=AE=E7=AB=A0=E3=81=AE=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6=E5=90=8D=E3=81=AB=E8=AA=AC=E6=98=8E=E3=82=92=E6=B7=BB?= =?UTF-8?q?=E3=81=88=E3=82=8B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 章の順は設計文書の雛形どおりで並べ替えは無い。内容は足していない。 Co-Authored-By: Claude Opus 5.5 --- issues/issue-2474-design.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/issues/issue-2474-design.md b/issues/issue-2474-design.md index ec725a1eb..8cda1b3de 100644 --- a/issues/issue-2474-design.md +++ b/issues/issue-2474-design.md @@ -2,7 +2,7 @@ ## 目的 -- **今どうなっているか**: 周回の途中で `loop stop` を打ってセッションを切り、後から `loop start` で再開すると、止める指示のファイルが残る。周回を締めた境でそのファイルを読み、`user_stop` で止まり直す。止めていた時間は周回の区間に入ったまま、止まりの記録のどの行にも重ならないため、ループの予算の経過時間に数えられる(lfactory2 の周回 4 で約 41 時間、予算 72 時間の約 6 割) +- **今どうなっているか**: 周回の途中で `loop stop` を打ってセッションを切り、後から `loop start` で再開すると、止める指示のファイルが残る。周回を締めた境でそのファイルを読み、利用者が止めたという条件(`user_stop`)で止まり直す。止めていた時間は周回の区間に入ったまま、止まりの記録のどの行にも重ならないため、ループの予算の経過時間に数えられる(lfactory2 の周回 4 で約 41 時間、予算 72 時間の約 6 割) - **誰に効くか**: 開発ループを使う利用者。周回の途中でループを休ませてセッションを切る使い方をする人 - **変えると何が成り立つか**: `loop start` の再開で止める指示が解ける。休ませた区間は止まりの記録に 1 行残り、経過時間から外れる。再開の後に余分な止まりが挟まらず、予算の残りが休ませる前と同じに保たれる @@ -125,9 +125,9 @@ graph TD ### 値の集合へ値を足す影響 止まりの記録(`stops`)へ、周回の途中で解いた `user_stop` の行が加わる。行の形(`condition`・`values`・`lap`・`at`・`resumed_at`)は -既存の行と同じである。止まりの記録を読む関数を `grep -rn "stops" plugins/ndf/scripts --include=*.py` で集め、テストを除いた -当たりを 1 つずつ判定した(2026-11-04。`merged_lib/merge.py` と `test_strategy.py` の当たりは別のオブジェクトの `stops` 属性で、 -ループの記録を読まない)。 +既存の行と同じである。止まりの記録を読む関数は `grep -rn "stops" plugins/ndf/scripts --include=*.py` で集めた(2026-11-04)。 +テストを除いた当たりを 1 つずつ判定した結果が次の表である。`merged_lib/merge.py` と `test_strategy.py` の当たりは +別のオブジェクトの `stops` 属性で、ループの記録を読まない。 | 読む関数 | 新しい行への当てはまり | 扱い | | --- | --- | --- | From 4882b6e75636ecdb156fc1cc25a4851c0f024e78 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 4 Nov 2026 21:06:41 +0000 Subject: [PATCH 4/7] =?UTF-8?q?docs(design):=20=E5=91=A8=E5=9B=9E=E3=81=AE?= =?UTF-8?q?=E9=80=94=E4=B8=AD=E3=81=A7=E6=AD=A2=E3=81=BE=E3=81=A3=E3=81=9F?= =?UTF-8?q?=E8=A8=98=E9=8C=B2=E3=81=AE=E5=86=8D=E9=96=8B=E3=81=AB=E6=AD=A2?= =?UTF-8?q?=E3=82=81=E3=82=8B=E6=8C=87=E7=A4=BA=E3=81=8C=E6=AE=8B=E3=82=8B?= =?UTF-8?q?=E3=81=93=E3=81=A8=E3=82=92=E6=98=8E=E8=A8=98=EF=BC=88#2474?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- issues/issue-2474-design.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/issues/issue-2474-design.md b/issues/issue-2474-design.md index 8cda1b3de..0bd57790b 100644 --- a/issues/issue-2474-design.md +++ b/issues/issue-2474-design.md @@ -105,7 +105,7 @@ E3 は記録が残らないため受け手を持たない(要求の前提 1) | `lap_rows`(`sprint_loop_record.py`) | `laps.md` の周回の行の「止まった理由」に、その周回の止まりを起きた順にすべて並べる | 変える | | `active_seconds`(`sprint_loop_record.py`) | 止まりの記録の `at`〜`resumed_at` と周回の区間の重なりを引く | 変えない(足した行をそのまま読む) | | `boundary_stop`(`sprint_loop_record.py`) | 周回の境で止まる条件を判定する(順も変えない) | 変えない | -| `resume_stopped` / `_resume_user_stop`(`sprint_loop.py`) | 止まった記録の再開 | 変えない(要求の前提 5) | +| `resume_stopped` / `_resume_user_stop`(`sprint_loop.py`) | 止まった記録の再開 | 変えない(要求の前提 5)。承認ゲート 2 の差し戻し(`gate2_returned`)と周回を組めなかった止まり(`refused`)の再開は、止める指示のファイルを消さない(「未確認のまま残ること」) | | 確定仕様 `docs/specifications/ndf-workflow-development-loop.md` | 再開の表・I3・I8・`start` の契約・ループの記録の置き場の表を振る舞いに合わせる | 変える | ```mermaid @@ -209,6 +209,9 @@ sequenceDiagram (`loop stop` → `loop start`)が、周回の境の前か後かで違う結果になる。`loop start` を打つこと自体を再開の印として扱い、 どちらの経路でも止める指示を消す。 +この決定が揃えるのは、止まっていない記録の再開と `user_stop` で止まった記録の再開の 2 つである。周回の途中で +`gate2_returned`・`refused` で止まった記録の再開は、要求の前提 5 のとおり変えないため、止める指示が残る(「未確認のまま残ること」)。 + 周回の境の判定(`boundary_stop`)の側で「再開より前に置かれた止める指示を読み飛ばす」形は採らない。再開の時刻を別に 持つ必要があり、止める指示のファイルも残り続けるため、`loop status` と利用者の目に止める指示が残って見える。 @@ -297,4 +300,5 @@ sequenceDiagram | 止める指示を置いた後もセッションが周回を進めていた時間 | 要求の前提 1 のとおり、その時間も経過時間から外れる。セッションが止まった時刻を記録する仕組みが無いため、実際に動いていた時間との差は測れない。承認ゲート 1 で利用者が前提 1 を確かめる | | 消した後の記録の書き込みの失敗 | 決定 3 のとおり、休ませた区間が経過時間に残る。発生の頻度は未確認(今の `write_record` の失敗と同じ扱い) | | `loop stop` と `loop start` の取り合い | `loop stop` は排他を取らない。`loop start` が止める指示を読んで消す間に別の `loop stop` が書くと、後の指示も消える。同時に打つ使い方は確認していない | +| 周回の途中で止まった記録の再開に残る止める指示 | 周回の途中で `loop stop` を置いた後に、承認ゲート 2 の差し戻し(`gate2_returned`)か周回を組めなかった止まり(`refused`)で止まると、その記録の再開(`resume_stopped`)は止める指示のファイルを消さない。再開の後の周回の境で `user_stop` が出直し、止める指示を置いた時刻 T1 から止まった時刻までが経過時間に残る。#2474 と同じ症状がこの経路に残る。直すには止まった記録の再開を変える必要があり、要求の前提 5 と受け入れ条件 8 に触れるため、この設計では直さない。直すかは課題 #2474 の本文で決める | | リリース後の実機確認 | 次に開発ループを回すとき、周回の途中で `loop stop` → セッションを切る → `loop start` を通し、`laps.md` の経過時間に休ませた時間が入らないことを見る(要求の検証手段) | From ee673450f46a422a475f069f6030c8f9055c6410 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Thu, 5 Nov 2026 00:58:50 +0000 Subject: [PATCH 5/7] =?UTF-8?q?Docs:=20#2474=20=E3=81=AE=E8=A8=AD=E8=A8=88?= =?UTF-8?q?=E3=82=92=E7=B5=84=E3=81=BF=E7=9B=B4=E3=81=99=EF=BC=88=E6=AD=A2?= =?UTF-8?q?=E3=82=81=E3=82=8B=E6=8C=87=E7=A4=BA=E3=82=92=E4=BA=BA=E3=81=8C?= =?UTF-8?q?=E9=80=81=E3=81=A3=E3=81=9F=E5=81=9C=E6=AD=A2=E3=81=A0=E3=81=91?= =?UTF-8?q?=E3=81=8B=E3=82=89=E7=BD=AE=E3=81=8F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-11-05 の利用者の判断(直して出し直す)に合わせ、要求の写しを作り直し、設計を置き換えた。 - 会話の記録から、lfactory2 の止める指示は conductor が別のセッションの「終了して」を読み替えて置いたものと確かめた - conductor の手順(loop.md)で止める指示の発生元を人が送った入力に限り、終える依頼では置かない - loop start で止める指示を消す処理と、経過時間・周回一覧・時刻の補正の変更を外し、コードは変えない Co-Authored-By: Claude Opus 5.5 --- issues/issue-2474-design.md | 284 +++++++++++------------------- issues/issue-2474-requirements.md | 116 ++++++------ 2 files changed, 153 insertions(+), 247 deletions(-) diff --git a/issues/issue-2474-design.md b/issues/issue-2474-design.md index 0bd57790b..8a6b2a823 100644 --- a/issues/issue-2474-design.md +++ b/issues/issue-2474-design.md @@ -1,24 +1,25 @@ -# 開発ループ: 周回の途中に置いた止める指示を再開で解き、止めていた時間を予算の経過時間から外す(#2474) +# 開発ループ: 止める指示を人が送った停止だけから置く(#2474) ## 目的 -- **今どうなっているか**: 周回の途中で `loop stop` を打ってセッションを切り、後から `loop start` で再開すると、止める指示のファイルが残る。周回を締めた境でそのファイルを読み、利用者が止めたという条件(`user_stop`)で止まり直す。止めていた時間は周回の区間に入ったまま、止まりの記録のどの行にも重ならないため、ループの予算の経過時間に数えられる(lfactory2 の周回 4 で約 41 時間、予算 72 時間の約 6 割) -- **誰に効くか**: 開発ループを使う利用者。周回の途中でループを休ませてセッションを切る使い方をする人 -- **変えると何が成り立つか**: `loop start` の再開で止める指示が解ける。休ませた区間は止まりの記録に 1 行残り、経過時間から外れる。再開の後に余分な止まりが挟まらず、予算の残りが休ませる前と同じに保たれる +- **今どうなっているか**: conductor の手順は、止める指示(`loop stop` が置くファイル)を誰の何を受けて置くかを決めていない。lfactory2 では、conductor が別のセッションからの「引継ぎ文書を書いて終了して」というメッセージを「ループを止めること」と読み替えて `loop stop` を打った。再開した後の周回の境で、誰も求めていない利用者の停止(`user_stop`)で止まり、本当に止まるべき理由(プロジェクト MVV の改訂)が出なかった +- **誰に効くか**: 開発ループを回す利用者。複数のループを並べ、別のセッションから conductor へ依頼を送る使い方をする人 +- **変えると何が成り立つか**: 止める指示は、人がそのセッションへ送った停止からだけ置かれる。セッションを終える依頼・別のセッションのメッセージ・画面の文字・サジェストからは置かれない。誤って置いた止める指示は、置いた conductor が消す ## 適用範囲 -- **働く範囲**: NDF の `sprint-state.py loop` の副命令(`start`・`lap`・`select`)。開発ループを使う配布先のリポジトリでも同じように働く -- **プロジェクトごとに違うもの**: 無し(足す設定・引数は無い。止める指示のファイルの置き場と中身は今のまま) -- **当たるモード**: 開発ループで回すすべてのモード(`pace: auto`) +- **働く範囲**: NDF の `development-workflow` の開発ループの手順(conductor が読む `references/loop.md`)。開発ループを使う配布先のリポジトリでも同じように働く +- **プロジェクトごとに違うもの**: 無し(足す設定・引数は無い) +- **当たるモード**: 開発ループで回すすべてのモード ## あるべき姿の根拠 | 根拠 | 種類 | 何を示すか | | --- | --- | --- | -| `.ndf/loop/lfactory2/loop.json` の記録(要求の「目的」の表): 周回 4 の開始 2026-11-02 13:04:20、止める指示 13:08:53、締めた時刻 2026-11-04 06:01:43、消費の hours 6.19 → 47.14、`user_stop` の行は 06:01:43〜06:02:11 | 実測 | 止めていた区間が止まりの記録に無く、経過時間から引かれていない | -| 確定仕様 `docs/specifications/ndf-workflow-development-loop.md` の I8(止まっていた時間は経過時間に入らない)と再開の表(`user_stop` はファイルを消して解ける) | 既存の規則 | 止める指示は再開で解けること、止まっていた区間を経過時間から外すことが、すでに決まった形である | -| `plugins/ndf/scripts/lib/sprint_loop.py` の `cmd_start`(止まっていない記録では `in_lap` / `resumed` を返すだけで、止める指示のファイルを見ない) | 実測(コードの読み) | 止める指示を消すのは、止まった記録の再開(`_resume_user_stop`)の経路だけである | +| 会話の記録 `37e6fcce`(lflow)・`f3bae19d`(lfactory2)の 2026-11-02 13:07〜13:09 UTC(要求の「目的」の表) | 実測 | 止める指示を置いたのは conductor 自身の読み替えで、どの段にも人がループを止めることを送った記録が無い | +| 2026-11-05 の利用者の判断(課題 #2474 の「人の判断待ち」の節) | 利用者の決定 | 送信されていない書きかけの指示は指示として扱わず、止める指示は人が実際に送ったものだけを有効にする | +| `git grep -n "STOP_FILE" -- plugins/ndf/scripts/lib`(要求の前提 1) | 実測(コードの検索) | 止める指示のファイルを書くのは `loop stop`(`cmd_stop`)だけで、発生元を絞る場所は `loop stop` を打つ側にある | +| 確定仕様 `docs/specifications/ndf-workflow-development-loop.md` の I3・I8 | 既存の規則 | 人が送った停止は次の境で効き、境で止まった後の区間はすでに経過時間から外れる | 要求と受け入れ条件は #2474 の本文にある(コピーは `issues/issue-2474-requirements.md`)。この文書は「どう作るか」だけを扱う。 @@ -27,10 +28,8 @@ | 呼び方 | 意味 | | --- | --- | | 止める指示 | `loop stop` が置くファイル `.ndf/loop/<名>/stop`。中身は置いた時刻 | -| 止まりの記録 | ループの記録(`loop.json`)の `stops`。解けた止まりの一覧(追記だけ) | -| 経過時間 | 確定仕様の I8 の「動いていた時間」。終わった周回の区間の和から、止まりの記録と重なる部分を引いたもの(結果の `consumed.hours`) | -| 止まっていない記録 | ループの記録の `status` が `running` のもの(`stopped` でないもの) | -| 休ませた区間 | 止める指示を置いた時刻 T1 から、`loop start` で再開した時刻 T2 まで | +| 人が送った入力 | 人がそのセッションの入力欄から送信した入力と、人が端末で打ったコマンド | +| 人が送っていない文 | 別のセッションからのメッセージ(「利用者の指示」と書かれていても含む)・引継ぎ文書・画面に出た文字・送信されていないサジェスト・conductor 自身の読み替え | ## ドメインモデル @@ -38,42 +37,37 @@ | コンテキスト | 何の語が 1 つの意味に決まるか | | --- | --- | -| NDF の開発ワークフロー(`ndf-workflow`) | 開発ループ・周回・ループの予算・止まる条件・ループの記録・止める指示 | +| NDF の開発ワークフロー(`ndf-workflow`) | 開発ループ・周回・止まる条件・ループの記録・止める指示 | 1 つのコンテキストに収まる。ほかのコンテキストとの関係は変えない。 ### 集約 -| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | -| --- | --- | --- | --- | --- | -| 開発ループ | `sprint_loop.py` の副命令(記録の読み書きは `sprint_loop_record.py` の関数を通す) | ループの記録(`loop.json`) | 周回(`laps[]`) | 止まり(`stop` / `stops[]` の 1 行)・予算・消費 | +| 集約 | 持ち主(書き換えてよいもの) | 根 | この変更での扱い | +| --- | --- | --- | --- | +| 開発ループ | `sprint_loop.py` の副命令 | ループの記録(`loop.json`) | 変えない | -止める指示のファイルは集約の外にある入力である。置くのは利用者の `loop stop`、消すのは `loop start` だけにする。 -この変更で書き換えるのは開発ループの集約だけで、1 回の `loop start` が書き換える集約も 1 つである。 +止める指示のファイルは集約の外にある入力である。この変更は、その入力を置く側(conductor の手順)の決まりだけを変える。 +書き換える集約は無い。 ### 不変条件 | # | 集約 | 条件 | 破れたときの扱い | | --- | --- | --- | --- | -| I1 | 開発ループ | 止まっていない記録の再開と、`user_stop` で止まった記録の再開では、`loop start` が終了コード 0 を返した後に止める指示のファイルが無い | テストが落ちる | -| I2 | 開発ループ | 止まっていない記録で進行中の周回があるときに止める指示を解いたら、止まりの記録に `condition: user_stop`・その周回の番号・`at` = T1・`resumed_at` = T2 の行がちょうど 1 行足される。進行中の周回が無いときは行を足さない | テストが落ちる | -| I3 | 開発ループ | 休ませた区間(T1〜T2)は、その周回の経過時間に入らない。周回の分は (T1 − T0) + (T3 − T2) になる | テストが落ちる | -| I4 | 開発ループ | T1 は T2 を超えない。止める指示の中身が時刻として読めないときは、ファイルの更新時刻を T1 にする | テストが落ちる | -| I5 | 開発ループ | 止める指示を消せなかったら、`loop start` は断り、ループの記録を書き換えない | テストが落ちる | -| I6 | 開発ループ | 止める指示のファイルが無いときの `loop start` の結果とループの記録は、今と同じである | 既存のテストが落ちる | +| I1 | 開発ループ(の入力) | 止める指示は、人が送った入力がループを止めることを求めたときにだけ置かれる | 手順書の決まりで守る。機械では検査しない(決定 1)。破れたら置いた conductor がファイルを消す(I2) | +| I2 | 開発ループ(の入力) | conductor が消してよい止める指示は、自分が人の送っていない文から置いたものだけである | 手順書の決まりで守る | +| I3 | 開発ループ | `loop start`・周回の境の判定・経過時間の計算の振る舞いは今と同じである | 既存のテストが落ちる | ### ドメインイベント | # | イベント | 発生元 | 受け手 | | --- | --- | --- | --- | -| E2 | 止める指示を置いた | 利用者の `loop stop` | `loop start`(E5)・周回の境の判定(E8) | -| E4 | 再開を求めた | 利用者の `loop start` | `cmd_start` | -| E5 | 止める指示を解いた | `cmd_start`(止まっていない記録で止める指示がある) | 結果の `released_stop`・周回の境の判定(`user_stop` に当たらなくなる) | -| E6 | 休ませた区間を記録した | `cmd_start`(E5 で進行中の周回がある) | 経過時間の計算(`active_seconds`)・`laps.md` の描画 | -| E8 | 境で止まる条件を判定した | `loop lap` の後・`loop select` の前(`boundary_stop`) | 止まる条件の判定。E5 が先に起きていれば `user_stop` は当たらない | +| E1 | 人がループを止めることを送った | 人の入力・人が端末で打つ `loop stop` | conductor | +| E2 | 止める指示を置いた | E1 を受けた conductor の `loop stop` | 周回の境の判定(`user_stop`) | +| E3 | セッションを終える依頼を受けた | 人の入力・別のセッションのメッセージ | conductor。引継ぎ文書を書いて終える。E2 を起こさない | +| E4 | 人が送っていない文から置いた止める指示に気づいた | 止める指示を置いた conductor | 止める指示のファイル(消す)・引継ぎ文書(消したことを書く) | -要求の E1(周回を始めた)・E3(セッションが終わった)・E7(周回を締めた)は振る舞いを変えないため、この表に載せない。 -E3 は記録が残らないため受け手を持たない(要求の前提 1)。 +要求の E5(境で止まる条件を判定した)は振る舞いを変えないため、この表に載せない。 ### 用語 @@ -81,224 +75,142 @@ E3 は記録が残らないため受け手を持たない(要求の前提 1) | --- | --- | --- | | 開発ループ | スプリントを 1 つ終えるたびに次のスプリントを選んで続けて始める進め方 | 無し(既存の語をそのまま使う) | | 周回 | 開発ループの 1 回分 | 無し | -| ループの予算 | 開発ループが使ってよい量の上限。周回の数・費用・経過時間で数える | 無し | | 止まる条件 | 開発ループを止める、機械で判定できる条件 | 無し | | ループの記録 | `.ndf/loop/<名>/loop.json` | 無し | -新しい語は足さない。「止める指示」「休ませた区間」はこの文書の中の呼び方で、用語集へは足さない。 +新しい語は足さない。「止める指示」「人が送った入力」「人が送っていない文」はこの文書の中の呼び方で、用語集へは足さない。 ## 機能一覧 | # | 機能 | 誰が使うか | | --- | --- | --- | -| F1 | 周回の途中でループを休ませた後、`loop start` で再開すると止める指示が解ける | 開発ループを回す利用者と conductor | -| F2 | 周回の外(進行中の周回が無い)で置いた止める指示も、`loop start` で解ける | 同上 | -| F3 | 休ませた区間が止まりの記録と `laps.md` に残り、ループの予算の経過時間から外れる | 同上 | +| F1 | conductor は、人が送った入力がループを止めることを求めたときだけ `loop stop` を打つ | conductor | +| F2 | セッションを終える依頼では、`loop stop` を打たずに引継ぎ文書を書いて終える | conductor | +| F3 | 人が送っていない文から置いた止める指示に気づいたら、ファイルを消して無視し、引継ぎ文書に書く | conductor | ## 構成要素 | 要素 | 責務 | 変える / 変えない | | --- | --- | --- | -| `cmd_start`(`plugins/ndf/scripts/lib/sprint_loop.py`) | 止まっていない記録の経路で、止める指示があれば解く関数を呼び、結果に `released_stop` を載せる | 変える | -| `release_stop_request`(新設。`sprint_loop.py`) | 止める指示の時刻 T1 を読み、ファイルを消し、進行中の周回があれば止まりの記録へ 1 行足す。消せなければ断る結果を返す | 作る | -| `stop_request_time`(新設。`plugins/ndf/scripts/lib/sprint_loop_record.py`) | 止める指示のファイルから T1 を読む(中身 → 読めなければ更新時刻)。T2 を超えれば T2 に揃える | 作る | -| `lap_rows`(`sprint_loop_record.py`) | `laps.md` の周回の行の「止まった理由」に、その周回の止まりを起きた順にすべて並べる | 変える | -| `active_seconds`(`sprint_loop_record.py`) | 止まりの記録の `at`〜`resumed_at` と周回の区間の重なりを引く | 変えない(足した行をそのまま読む) | -| `boundary_stop`(`sprint_loop_record.py`) | 周回の境で止まる条件を判定する(順も変えない) | 変えない | -| `resume_stopped` / `_resume_user_stop`(`sprint_loop.py`) | 止まった記録の再開 | 変えない(要求の前提 5)。承認ゲート 2 の差し戻し(`gate2_returned`)と周回を組めなかった止まり(`refused`)の再開は、止める指示のファイルを消さない(「未確認のまま残ること」) | -| 確定仕様 `docs/specifications/ndf-workflow-development-loop.md` | 再開の表・I3・I8・`start` の契約・ループの記録の置き場の表を振る舞いに合わせる | 変える | - -```mermaid -graph TD - U[利用者の
loop start] --> S[cmd_start] - S -->|止まった記録| R[resume_stopped
今のまま] - S -->|止まっていない記録| L[release_stop_request] - L --> T[stop_request_time] - L -->|周回の途中なら
1 行足す| J[(ループの記録の
止まりの記録)] - L -->|消す| F[(止める指示の
ファイル)] - J --> A[active_seconds
経過時間] - J --> M[lap_rows
laps.md] -``` - -図に含めない要素: 周回の境の判定(`boundary_stop`。変えず、止める指示が無いことを読むだけ)と確定仕様(文書)。 +| `plugins/ndf/skills/development-workflow/references/loop.md` の「止まる条件」の節 | 止める指示を置く発生元と、置かない場面と、誤って置いたときの扱いを conductor へ示す | 変える(節の中に小見出し「止める指示を置く」を足す) | +| 確定仕様 `docs/specifications/ndf-workflow-development-loop.md` | 止める指示の発生元を確定の振る舞いとして残す | 変える | +| `cmd_stop` / `cmd_start` / `boundary_stop` / `active_seconds`(`plugins/ndf/scripts/lib/sprint_loop*.py`) | 止める指示の読み書き・再開・境の判定・経過時間 | 変えない(決定 2・3) | +| `references/parallel.md` の「レーンの画面を見るとき」 | 画面の入力欄の文字を判断に使わない | 変えない。`loop.md` の新しい小見出しからリンクする | ### 値の集合へ値を足す影響 -止まりの記録(`stops`)へ、周回の途中で解いた `user_stop` の行が加わる。行の形(`condition`・`values`・`lap`・`at`・`resumed_at`)は -既存の行と同じである。止まりの記録を読む関数は `grep -rn "stops" plugins/ndf/scripts --include=*.py` で集めた(2026-11-04)。 -テストを除いた当たりを 1 つずつ判定した結果が次の表である。`merged_lib/merge.py` と `test_strategy.py` の当たりは -別のオブジェクトの `stops` 属性で、ループの記録を読まない。 - -| 読む関数 | 新しい行への当てはまり | 扱い | -| --- | --- | --- | -| `active_seconds` | 当てはまる。`at`〜`resumed_at` と周回の区間の重なりを引くのが、この変更の狙いそのものである | 変えない | -| `failure_streak` | 当てはまる。`same_failure` の行だけを区切りに使い、`user_stop` の行は読み飛ばす | 変えない | -| `lap_rows` | 当てはまらない。周回ごとに止まりを 1 つだけ(後の行で上書き)持つため、休ませた後に同じ周回が別の理由で止まると `user_stop` が消える | 変える(決定 4) | -| `resume_record` | 書く側。止まった記録の再開で行を足す | 変えない | +値の集合へ値を足さない(止まる条件・結果の `status`・ループの記録の形は変えない)。 ## 入出力の契約 -`sprint-state.py loop start` の結果に 1 つ値を足す。既存の値は変えない。 - -| 場合 | 終了コード | `metrics.status` | 足す値 | -| --- | --- | --- | --- | -| 止まっていない記録・進行中の周回あり・止める指示あり | 0 | `in_lap` | `released_stop: {at, source, lap}`。`summary` に「止める指示を解いた」を足す | -| 止まっていない記録・進行中の周回なし・止める指示あり | 0 | `resumed` | `released_stop: {at, source, lap: null}`。同上 | -| 止める指示が無い | 今のまま | 今のまま | 足さない(キー自体を出さない) | -| 止める指示を消せない | 1 | `refused` | `items` に `{path, reason}`。ループの記録は書かない | -| 止まった記録(`stopped`) | 今のまま | 今のまま | 足さない(要求の前提 5) | - -`released_stop` の値: +`sprint-state.py loop` の副命令の引数と結果は変えない。 -| キー | 型 | 意味 | -| --- | --- | --- | -| `at` | 文字列(ISO 8601、UTC) | 休ませた区間の始まり T1 | -| `source` | `content` / `mtime` | T1 を止める指示の中身から読んだか、更新時刻から読んだか | -| `lap` | 整数 / `null` | 止まりの記録へ行を足した周回の番号。進行中の周回が無ければ `null` | - -止まりの記録へ足す行(形は既存の行と同じ): +`loop.md` へ足す小見出しの中身(実装でこの内容を書く。文面は実装で整える): -```json -{"condition": "user_stop", "values": {"at": "<止める指示の中身のまま>"}, "lap": 4, "at": "", "resumed_at": ""} -``` - -`values.at` は周回の境の判定(`boundary_stop`)が `user_stop` に書く値と同じく、ファイルの中身をそのまま残す。 -中身が読めなかった場合も、空・壊れた中身を `values.at` に残し、`at` に更新時刻を入れる。 +| 項目 | 書く内容 | +| --- | --- | +| 置く場面 | 人が自分のセッションへ送った入力が、ループを止めることを求めたときだけ `$SS loop stop <名>` を打つ | +| 発生元にしないもの | 別のセッションからのメッセージ(「利用者の指示」と書かれていても含む)・引継ぎ文書・画面に出た文字(`parallel.md` の「レーンの画面を見るとき」へリンク)・送信されていないサジェスト・自分の読み替え。これらが止めることを求めていたら、人に確かめるまで置かない | +| 置かない場面 | セッションを終える依頼(引き継いで終了する・新しい版で起動し直す)。引継ぎ文書を書いて終える。周回は進行中のまま残り、次の起動の `loop start` で続く | +| 誤って置いたとき | 人が送っていない文から置いたと気づいたら、`.ndf/loop/<名>/stop` を消し、消したことと理由を引継ぎ文書に書く。人が置いた止める指示と、置いた経緯が分からない止める指示は消さない | +| 理由(1 文) | 止める指示は再開の後も残り、次の境で `user_stop` として効く。誰も求めていない停止で止まると、本当に止まるべき理由が出ない | ## 処理の流れ +conductor が止めることを求める文を受けたときの判断を、次の流れにする。 + ```mermaid -sequenceDiagram - participant U as 利用者 - participant S as cmd_start - participant F as 止める指示 - participant R as ループの記録 - U->>S: loop start 名 - S->>R: 読む(排他の中) - alt status が stopped - S->>S: resume_stopped(今のまま) - else 止まっていない記録 - S->>F: ある? - alt 無い - S-->>U: in_lap / resumed(今のまま) - else ある - S->>F: T1 を読む(中身 → 更新時刻) - S->>F: 消す - alt 消せない - S-->>U: 終了コード 1(記録を書かない) - else 消せた - opt 進行中の周回がある - S->>R: 止まりの記録へ 1 行(T1〜T2) - end - S->>R: 書く - S-->>U: in_lap / resumed と released_stop - end - end - end +flowchart TD + A[止める・終えることを
求める文を受けた] --> B{人が自分のセッションへ
送った入力か} + B -->|いいえ| C[止める指示を置かない
人に確かめる] + B -->|はい| D{ループを止めることを
求めているか} + D -->|はい| E[loop stop を打つ] + D -->|セッションを
終えるだけ| F[引継ぎ文書を書いて終える
loop stop を打たない] ``` -`cmd_start` の中の順は、予算の上書き(`apply_budget`)→ 止める指示を解く → 記録を書く、にする。 -止める指示を消すのは記録を書く前である(決定 3)。続く `loop lap` の後の境の判定は今のまま `boundary_stop` を通り、 -止める指示が無いため `user_stop` に当たらず、`mvv_changed` などの次の条件を見る。 - -## 決定の記録 +人が送っていない文から止める指示をすでに置いていたと気づいたときは、ファイルを消して引継ぎ文書に書く(F3)。 -### 決定 1: 再開を求めた印を 1 か所で受けるため、止まっていない記録の止める指示を `loop start` で解く +図は `loop.md` の新しい小見出しが conductor へ示す判断である。図に含めない要素: 確定仕様(文書)、変えない開発ループのコード、 +リンク先の `parallel.md`。 -止まった記録の `user_stop` は、すでに `loop start` で解ける。止まっていない記録で止める指示を残すと、同じ利用者の操作 -(`loop stop` → `loop start`)が、周回の境の前か後かで違う結果になる。`loop start` を打つこと自体を再開の印として扱い、 -どちらの経路でも止める指示を消す。 +## 決定の記録 -この決定が揃えるのは、止まっていない記録の再開と `user_stop` で止まった記録の再開の 2 つである。周回の途中で -`gate2_returned`・`refused` で止まった記録の再開は、要求の前提 5 のとおり変えないため、止める指示が残る(「未確認のまま残ること」)。 +### 決定 1: 発生元の限定は conductor の手順で決め、コマンドや hook の側で入力元を判定しない -周回の境の判定(`boundary_stop`)の側で「再開より前に置かれた止める指示を読み飛ばす」形は採らない。再開の時刻を別に -持つ必要があり、止める指示のファイルも残り続けるため、`loop status` と利用者の目に止める指示が残って見える。 +止める指示を置くのは `loop stop` だけで、`loop stop` を打つのは conductor か人である。人が送った入力かを機械で +見分けるには、その入力の出所を知る値が要る。hook が受ける入力の項目に入力元は無い。Claude Code の会話の記録には入力元の値 +(`promptSource`・`turnOrigin`)があるが公開の仕様ではなく、Codex には無い。lfactory2 の起点の入力も記録上は `typed` で、 +サジェストを採った入力と区別できなかった(要求の「目的」の表)。 -根拠: Value 7 / Vision(MVV 版 5) +hook で `loop stop` の呼び出しを会話の記録の入力元で止める形は採らない。内部の値に依存し、ランタイムによって働かない。 -### 決定 2: 休ませた区間を経過時間から外すため、止まりの記録へ既存と同じ形の行を足す +根拠: Value 6 / Value 5(MVV 版 5) -経過時間の計算(`active_seconds`)は、止まりの記録の `at`〜`resumed_at` と周回の区間の重なりをすでに引く。休ませた区間を -同じ形の 1 行として足せば、計算の側を変えずに I8 が成り立つ。`laps.md` の止まりの欄にも同じ経路で出る。 +### 決定 2: `loop start` で止める指示を消さない -周回に「休ませた秒」の欄を新しく足す形は採らない。経過時間の計算と `laps.md` の両方に読み手を足すことになり、止まりの -記録と同じ意味の値を 2 か所に持つ。 +人が送った停止は、次の周回の境で効くのが正しい振る舞いである(確定仕様の I3)。自動の再起動(カットポイントと利用上限での +切り替え)も周回の途中で `loop start` を打つため、`loop start` で止める指示を消すと、人が送った停止が境の前に消える。 +発生元を人が送った停止に限れば、再開の後に止める指示が効くのは人の意図どおりである。 -根拠: Value 7 / Value 8(MVV 版 5) +根拠: 共通原則の上位の原則(人の判断を守る) / Value 7(MVV 版 5) -### 決定 3: 消せなかったときに記録だけが進まないよう、止める指示を消してから記録を書く +### 決定 3: 経過時間の計算を変えない -止める指示を先に消し、消せなければ記録を書かずに終了コード 1 で断る(要求の E5)。記録を先に書いてから消す順では、 -消せなかったときに止まりの記録の行だけが残り、次の境で同じ止める指示が `user_stop` として効き、同じ区間が 2 回扱われる。 +人が送った停止では、conductor は境まで周回を続けるため、止める指示を置いてから境までは動いていた時間である。境で止まった +後の区間は、確定仕様の I8 ですでに経過時間から外れる。外すべき区間は止める指示からは生じない。 -消した後に記録の書き込みが失敗した場合は、例外がそのまま上がる(今の `write_record` の失敗と同じ扱い)。このとき -休ませた区間は経過時間から外れないが、止める指示は解けており、再開の後に余分な止まりは挟まらない。 +lfactory2 の周回 4 に入った約 41 時間は、周回の途中でセッションを終えて 2 日後に再開した区間である。セッションが止まった +時刻を記録する仕組みが無いため、この変更では扱わない(「未確認のまま残ること」)。 -根拠: Value 1(MVV 版 5) +根拠: Value 3 / Value 4(MVV 版 5) -### 決定 4: 休ませた事実が後の止まりで消えないよう、`laps.md` の止まりの欄に周回の止まりをすべて並べる +### 決定 4: セッションを終える依頼を止める指示に読み替えない -今の `lap_rows` は周回ごとに止まりを 1 つだけ持ち、後の行で上書きする。休ませた周回が締めた後の境で `mvv_changed` などで -止まると、`user_stop` が欄から消える。止まりの記録の順(起きた順)に、条件を `、` で区切って並べる。止まりが 1 つの周回の -欄は今と同じ文字列になる。 +セッションを終える依頼の目的は、周回を進行中のまま残し、次の起動で続けることである。止める指示を置くと、次の起動で続けた +周回が境で `user_stop` で止まり、依頼の目的と逆になる。lfactory2 で起きたのはこの読み替えである。 -`laps.md` へ休ませた区間だけの別の表を足す形は採らない。周回の一覧の 1 つの欄で足りる。 +根拠: Mission / Value 2(MVV 版 5) -根拠: Value 8(MVV 版 5) +### 決定 5: 誤って置いた止める指示は、置いた conductor だけが消す -### 決定 5: 時刻の誤りで経過時間を負に数えないよう、T1 を T2 以下に揃える +止める指示のファイルの中身は置いた時刻だけで、誰が何を受けて置いたかを持たない。後から読んだ conductor には、人が置いた +ものと見分けられない。消してよいのは、自分が人の送っていない文から置いたと分かっている conductor だけにする。経緯が +分からない止める指示は消さず、境で `user_stop` として止まり、人が `loop start` で解く(今の再開の表のとおり)。 -止める指示の中身が未来の時刻(手で書いた・時計のずれ)だと、`at` が `resumed_at` より後の行になる。重なりの計算は 0 を -返すが、記録として読めない行が残る。T1 が T2 を超えたら T2 にそろえる(区間の長さは 0)。T1 が周回の開始 T0 より前の -とき(周回を組む前に置いた止める指示)は揃えない。重なりの計算が周回の区間で切るため、引きすぎない。 +止める指示のファイルに置いた理由を書き足す形は採らない。書く内容を検証できず、決定 1 と同じく人が送ったかを判定できない。 -根拠: Value 1(MVV 版 5) +根拠: 共通原則の上位の原則(人の判断を守る) / Value 7(MVV 版 5) ## テスト設計 -置き場は `plugins/ndf/scripts/tests/test_sprint_loop.py`。時刻は `Env.now` の差し替えで進める。 - | 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | | --- | --- | --- | -| 受け入れ条件 1・I1 | 進行中の周回があり止める指示があるとき、`loop start` が 0・`in_lap`・ファイルなし・`released_stop` ありを返す | 止まっていない記録の経路で止める指示を見ないように戻すと落ちる | -| 受け入れ条件 2 | 1 の後に `loop lap`(`--failed` あり・なし)を締めると、境の判定が `user_stop` に当たらない。MVV を改訂しておけば `mvv_changed` で止まる | ファイルを消さずに `released_stop` だけを返すと落ちる | -| 受け入れ条件 3・I2 の後半 | 進行中の周回が無いとき、`loop start` が 0・`resumed`・ファイルなしを返し、止まりの記録に行が増えず、続く `loop select` が `user_stop` で止まらない | 周回が無いときにも行を足すと落ちる | -| 受け入れ条件 4・I3 | T0 < T1 < T2 < T3 の順に時刻を進めると、`consumed.hours` の周回の分が (T1 − T0) + (T3 − T2) になる | 行の `at` に T2 を書く・`resumed_at` を書かないと落ちる | -| 受け入れ条件 5・I2 の前半 | 止まりの記録に `user_stop`・周回の番号・`at` = T1・`resumed_at` = T2 の行がちょうど 1 行あり、`laps.md` の周回の行の止まりの欄に `user_stop` が出る | 行を 2 回足す・周回の番号を書かないと落ちる | -| 決定 4 | 休ませた周回がその後 `mvv_changed` で止まっても、`laps.md` の欄に `user_stop` と `mvv_changed` の両方が出る | 後の止まりで上書きする今の形に戻すと落ちる | -| 受け入れ条件 6・I4 | 止める指示の中身が空・時刻でないとき、ファイルの更新時刻が T1 になり、`source` が `mtime` になる。中身が T2 より後の時刻なら `at` が T2 になる | 中身が読めないときに T2 を使う・揃えずに未来の時刻を残すと落ちる | -| I5 | 止める指示を消せない(消す関数を失敗させる)とき、終了コード 1 で、ループの記録の中身が呼ぶ前と同じ | 記録を先に書いてから消す順にすると落ちる | -| 受け入れ条件 7・I6 | 止める指示が無いとき、`loop start` の結果に `released_stop` が無く、`in_lap` / `resumed` とループの記録が今と同じ | ファイルの有無を見ずに行を足すと落ちる | -| 受け入れ条件 8 | 既存の `test_resume_stopped_*` と `test_stop_file_works_at_the_next_boundary_and_resume_removes_it` が通る | 止まった記録の再開の経路を変えると落ちる | -| 受け入れ条件 9 | 確定仕様の再開の表・I3・I8・`start` の契約が振る舞いを書いている。文言を照合するテストは書かず、コードレビューで読む | — | +| 受け入れ条件 1〜4・I1・I2 | `loop.md` の手順。文言を照合するテストは書かず、設計と実装の Pull Request のレビューで読む | — | +| 受け入れ条件 5 | 確定仕様の記述。レビューで読む | — | +| 受け入れ条件 6・I3 | 既存の `plugins/ndf/scripts/tests/test_sprint_loop.py` が通る | 開発ループのコードを変えると落ちる | +| 手順書の参照 | `loop.md` から `parallel.md` の見出しへのリンクを、リンクの検査(`python3 scripts/check-markdown-links.py --root .`。継続的統合の `runtime-plugin-validate.yml` と同じ)で確かめる | 見出しの名前を違えると落ちる | ## 確定仕様の直し方 -`docs/specifications/ndf-workflow-development-loop.md` を次のとおりに直す(受け入れ条件 9)。 +`docs/specifications/ndf-workflow-development-loop.md` を次のとおりに直す(受け入れ条件 5)。 | 箇所 | 直す内容 | | --- | --- | -| 常に成り立つ条件の I3 | 止める指示は周回の境でだけ効く、を残し、「止まっていない記録の `loop start` は止める指示を消し、進行中の周回があれば休ませた区間(止める指示の時刻〜再開)を `stops` へ `user_stop` の行として足す」を加える | -| 常に成り立つ条件の I8 | 引く区間に、周回の途中で解いた止める指示の行(I3)が含まれることを書く | -| 副命令の表の `start` | 出力に `released_stop`(`{at, source, lap}`。止める指示を解いたときだけ)を足す。止める指示を消せないときの 1 を足す | -| 再開の表 | 「止まっていない記録に止める指示がある」の行を足す(ファイルを消し、周回の途中なら区間を `stops` へ足す。`in_lap` / `resumed` のまま) | -| ループの記録の置き場の表 | `stop` の行を「`loop stop` が置き、`loop start` で消す」にする | -| `stop` / `stops` の形の段落 | 周回の途中で解いた行も同じ形であることを書く | -| テストの表 | 受け入れ条件 1〜6 の縛りを 1 行足す | +| 冒頭の「利用者の止める指示では…止まる」の行 | 止める指示は人が送った停止からだけ置かれることを添える | +| 常に成り立つ条件の I3 | 止める指示を置くのは、人が送った入力がループを止めることを求めたときだけで、セッションを終える依頼・別のセッションのメッセージからは置かないことを加える | +| ループの記録の置き場の表の `stop` の行 | 「人が送った停止を受けて `loop stop` が置き、`user_stop` の再開で消す」にする | ## 設計の結果 | 課題 | 扱い | 取り込み先 | 触るファイル | | --- | --- | --- | --- | -| #2474 | 実装する | — | `plugins/ndf/scripts/lib/sprint_loop.py`、`plugins/ndf/scripts/lib/sprint_loop_record.py`、`plugins/ndf/scripts/tests/test_sprint_loop.py`、`docs/specifications/ndf-workflow-development-loop.md` | +| #2474 | 実装する | — | `plugins/ndf/skills/development-workflow/references/loop.md`、`docs/specifications/ndf-workflow-development-loop.md` | ## 未確認のまま残ること | 項目 | 内容 | | --- | --- | -| 止める指示を置いた後もセッションが周回を進めていた時間 | 要求の前提 1 のとおり、その時間も経過時間から外れる。セッションが止まった時刻を記録する仕組みが無いため、実際に動いていた時間との差は測れない。承認ゲート 1 で利用者が前提 1 を確かめる | -| 消した後の記録の書き込みの失敗 | 決定 3 のとおり、休ませた区間が経過時間に残る。発生の頻度は未確認(今の `write_record` の失敗と同じ扱い) | -| `loop stop` と `loop start` の取り合い | `loop stop` は排他を取らない。`loop start` が止める指示を読んで消す間に別の `loop stop` が書くと、後の指示も消える。同時に打つ使い方は確認していない | -| 周回の途中で止まった記録の再開に残る止める指示 | 周回の途中で `loop stop` を置いた後に、承認ゲート 2 の差し戻し(`gate2_returned`)か周回を組めなかった止まり(`refused`)で止まると、その記録の再開(`resume_stopped`)は止める指示のファイルを消さない。再開の後の周回の境で `user_stop` が出直し、止める指示を置いた時刻 T1 から止まった時刻までが経過時間に残る。#2474 と同じ症状がこの経路に残る。直すには止まった記録の再開を変える必要があり、要求の前提 5 と受け入れ条件 8 に触れるため、この設計では直さない。直すかは課題 #2474 の本文で決める | -| リリース後の実機確認 | 次に開発ループを回すとき、周回の途中で `loop stop` → セッションを切る → `loop start` を通し、`laps.md` の経過時間に休ませた時間が入らないことを見る(要求の検証手段) | +| lflow の起点の入力がサジェストだったか | 会話の記録の入力元の値は `typed` で、サジェストを採った入力の値(`suggestion_accepted`)ではない。利用者の判断(2026-11-05)ではサジェストを指示と読んだものである。どちらであっても、ループを止めることは誰も送っていない(要求の「目的」の表) | +| 手順書の決まりが守られるか | 決定 1 のとおり機械では検査しない。守られたかは、次に別のセッションから終える依頼を受けた conductor の振る舞いで見る(リリース後テスト) | +| 経緯が分からない止める指示 | 再開した conductor が、人の送っていない文から置かれた止める指示を見分ける手段は無い(決定 5)。lfactory2 と同じ場合は、境で `user_stop` で止まり、人が `loop start` で解く | +| 周回の途中でセッションを終えた区間 | セッションを終えてから再開までの区間は経過時間に入る(lfactory2 の約 41 時間)。セッションが止まった時刻を記録する仕組みが無く、この変更では扱わない | diff --git a/issues/issue-2474-requirements.md b/issues/issue-2474-requirements.md index 021fbe7af..3ae993601 100644 --- a/issues/issue-2474-requirements.md +++ b/issues/issue-2474-requirements.md @@ -1,4 +1,4 @@ -# #2474: 開発ループ: 周回の途中で止める指示を置くと、再開で解けず、止めていた時間が経過時間に入る +# #2474: 開発ループ: 止める指示を人が送った停止だけから置く(人が送っていない文から置かれた止める指示が、再開後の境で user_stop を起こした) 正は課題の本文(#2474)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。 @@ -31,65 +31,62 @@ ## 起票者の推定 -題・本文のうち、利用者の指示の原文に無く起票者が推して足した条件。承認ゲート 1 で利用者が確かめるまで、利用者の決定として扱わない。 +題・本文のうち、利用者の指示の原文に無く起票者が推して足した条件。2026-11-05 の利用者の判断(下の「人の判断待ち」の節)で扱いを決めた。 | # | 条件 | 推した出所 | この要求での扱い | | --- | --- | --- | --- | -| S1 | 周回の途中に置いた止める指示は、`loop start` の再開で解ける(周回の境で止まったときと同じ) | 本文の「期待」の 1 行目。確定仕様 `docs/specifications/ndf-workflow-development-loop.md` の再開の表(`user_stop` はファイルを消して解ける)からの類推 | 受け入れ条件 1・2・3 | -| S2 | 止める指示を置いてから再開までの時間は、ループの予算の経過時間に数えない | 本文の「期待」の 2 行目。確定仕様の不変条件 I8(止まっていた時間は経過時間に入らない) | 受け入れ条件 4・5、前提 1 | -| S3 | 直す場所は `cmd_start` の周回の途中の経路と、境の判定・周回の秒 | 本文の「範囲」 | 直す場所は設計が決める。受け入れ条件は場所を指定しない | -| S4 | 「周回の秒」に止めていた時間が入った | 本文の「何が起きたか」の 3 | 採らない。記録を見ると、周回 4 の秒(計画の行の秒の和)は 0 で、止めていた時間が入ったのはループの予算の経過時間(消費の hours)である(下の「目的」の実測) | +| S1 | lfactory2 の止める指示は「利用者の指示」で置かれた | 本文の「何が起きたか」の 1 行目。止める指示を置いた conductor が引継ぎ文書に「利用者の指示で `loop stop` を打った」と書いていた | 採らない。記録では、人はループを止めることを送っていない(下の「目的」の表) | +| S2 | 周回の途中に置いた止める指示は、`loop start` の再開で解ける | 本文の「期待」の 1 行目 | 採らない。人が送った停止は次の周回の境で効くのが正しい振る舞いである。自動の再起動も周回の途中で `loop start` を打つため、再開で消すと人が送った停止が境の前に消える(前提 4) | +| S3 | 止める指示を置いてから再開までの時間を、ループの予算の経過時間に数えない | 本文の「期待」の 2 行目 | 採らない。人が送った停止では conductor が境まで周回を続けるため、止める指示から境までは実際に動いていた時間である。境で止まった後の区間は、確定仕様の I8 ですでに経過時間から外れる(前提 3) | +| S4 | 直す場所は `cmd_start` の周回の途中の経路と、境の判定・周回の秒 | 本文の「範囲」 | 採らない。直すのは止める指示を置く経路(conductor の手順)である | +| S5 | 「周回の秒」に止めていた時間が入った | 本文の「何が起きたか」の 3 | 採らない。周回 4 の秒(計画の行の秒の和)は 0 で、時間が入ったのはループの予算の経過時間(消費の hours)である | ## 目的 -周回の途中で開発ループを止めて後から再開したとき、止める指示が再開で解け、止めていた時間がループの予算を使わない状態にする。利用者がセッションを切ってループを休ませても、再開した後に余分な止まりが 1 回挟まらず、予算の残りが休ませる前と同じに保たれる。 +止める指示(`.ndf/loop/<名>/stop`)が、人が実際に送った停止からだけ置かれる状態にする。人が送っていない文(送信されていないサジェスト・入力欄に残った文字・人の指示を名乗る別のセッションのメッセージ)から止める指示が置かれると、ループは誰も求めていない利用者の停止(`user_stop`)で止まり、本当に止まるべき理由が出ない。 -実際に起きた値(`.ndf/loop/lfactory2/loop.json` の記録、時刻は UTC): +記録で確かめた経路(2026-11-02、時刻は UTC。会話の記録 `37e6fcce`・`f3bae19d` とループの記録 `.ndf/loop/lfactory2/loop.json`): -| 項目 | 値 | -| --- | --- | -| 周回 4 の開始 | 2026-11-02 13:04:20 | -| 止める指示のファイルの中身(置いた時刻) | 2026-11-02 13:08:53 | -| 周回 4 を締めた時刻(`user_stop` の止まりの時刻) | 2026-11-04 06:01:43 | -| 周回 4 の秒(計画の行の秒の和) | 0 | -| 消費の hours(周回 4 の前 → 後) | 6.19 → 47.14 | +| 時刻 | 誰が | 何を読んで | 何をした | +| --- | --- | --- | --- | +| 13:07:22 | ループ lflow の conductor のセッション | 人の入力として記録された「3本に引継ぎメモを作って終了する指示を送ってください。」。記録の入力元の値は `typed` で、サジェストを採った入力の値(`suggestion_accepted`)ではない。利用者は 2026-11-05 に、この入力をサジェストを指示と読んだものと判断した | 3 本のループの conductor へ、別のセッションへのメッセージで「【利用者の指示・lflow 経由】引継ぎ文書を最新にして、このセッションを終了してください」を送った | +| 13:07:39 | ループ lfactory2 の conductor のセッション(`f3bae19d`) | 上のメッセージ(入力元は別のセッション) | パイプラインを止め、引継ぎ文書を書いた | +| 13:08:52 | 同じ | 同じメッセージ。ループを止めることは書かれていない | 「利用者の指示はループを止めること」と自分で読み替えて `loop stop lfactory2` を打ち、引継ぎ文書に「利用者の指示で打った」と書いた | +| 2026-11-04 06:01:43 | 再開した conductor | 残っていた止める指示 | 周回 4 を締めた境で `user_stop` で止まった。プロジェクト MVV の改訂(`mvv_changed`)は出なかった | -周回 4 の区間(開始から締めるまで、約 40.95 時間)がそのまま経過時間に入った。止める指示の時刻から締めた時刻までの約 40.88 時間は、止まりの記録(`stops`)のどの行にも重ならないため引かれなかった(`user_stop` の行は締めた時刻 06:01:43 に始まり 06:02:11 に解けている)。 +どの段にも、人がループを止めることを送った記録は無い。止める指示は、conductor が別のセッションのメッセージを自分で読み替えて置いた。周回 4 の経過時間に入った約 41 時間(消費の hours 6.19 → 47.14)は、周回の途中でセッションを終えて 2 日後に再開した区間であり、止める指示が作った区間ではない。 ## 前提 -- 前提 1: 止まっていた区間の始まりは、止める指示のファイルに書かれた時刻(`loop stop` を打った時刻)とする。止める指示を置いた後もセッションが周回を進めていた場合、その時間も経過時間から外れる。セッションがいつ止まったかを記録する仕組みが今は無く、止める指示の時刻が手に入る最も近い値であるため、この扱いを採る -- 前提 2: 止める指示のファイルの中身が時刻として読めない(空・壊れている)ときは、ファイルの更新時刻を区間の始まりとする -- 前提 3: 止まっていた区間の終わりは、`loop start` で再開した時刻とする -- 前提 4: 周回の途中でない(進行中の周回が無い)ときに `loop start` が止める指示を見つけた場合も、止める指示を解く。`loop start` を打つこと自体が、利用者が再開を求めた印であるため。この場合、止まっていた区間は周回の区間の外にあるため、経過時間は変わらない -- 前提 5: 既に止まっている記録(`status` が `stopped`)の再開は、今の再開の表のとおりに動く。この課題では変えない +- 前提 1: 止める指示のファイルを書くのは `sprint-state.py loop stop`(`cmd_stop`)だけである。`git grep -n "STOP_FILE" -- plugins/ndf/scripts/lib` の当たりは、定数の定義と読み込みを除くと、`cmd_stop` が書き、周回の境の判定(`boundary_stop`)が読み、止まった記録の再開(`_resume_user_stop`)が消す 3 か所である +- 前提 2: 入力を人が送ったかを、コマンドや hook の側で見分ける値は無い。hook が受ける入力の項目に入力元は無く、Claude Code の会話の記録にある入力元の値(`promptSource`・`turnOrigin`)は公開の仕様ではなく、Codex には無い +- 前提 3: 人が送った停止は、次の周回の境で効く(確定仕様の I3)。conductor は境まで周回を続けるため、止める指示が置かれてから境までは動いていた時間である。境で止まった後の区間は確定仕様の I8 で経過時間から外れる +- 前提 4: 自動の再起動(カットポイントと利用上限での切り替え)も、周回の途中で `loop start` を打つ ## 対象範囲 含む: -- `loop start` が、止まっていない記録(進行中の周回がある・無いの両方)で止める指示を見つけたときに、止める指示を解く振る舞い -- 周回の途中で止める指示を置いてから再開するまでの区間を、止まりの記録へ残し、ループの予算の経過時間から外すこと -- 確定仕様 `docs/specifications/ndf-workflow-development-loop.md` の、再開の表と不変条件 I3・I8 の記述を振る舞いに合わせること +- conductor の手順(`development-workflow` の `references/loop.md`)に、止める指示を置く発生元を人が送った入力に限ることを書く +- セッションを終える依頼(引き継いで終了する)では止める指示を置かないことを書く +- 人が送っていない文から止める指示を置いたと気づいたときに、ファイルを消して無視することを書く +- 確定仕様 `docs/specifications/ndf-workflow-development-loop.md` の、止める指示の発生元の記述 含まない: -- 既存のループの記録(lfactory2 など)の値を直すこと。lfactory2 は `mvv_changed` で止まっており再開しない -- 周回の秒(計画の行の秒の和)の数え方。止めていた時間は入っていない(起票者の推定 S4) -- 止める指示が周回の途中で効く(周回の途中で止まる)ようにすること。止める指示は今のとおり次の周回の境で効く -- 境の判定の順(`user_stop` を `mvv_changed` より先に見ること)を変えること。止める指示が再開で解ければ、次の境で `mvv_changed` が出る -- セッションが止まった時刻を記録する仕組み(前提 1) +- `loop start` と経過時間の計算を変えること(前提 3・4) +- 中継が tmux に起動する claude のサジェストを止めること(#2504) +- 画面の入力欄の文字を判断に使わない決まり(PR #2502 で develop に入った) +- 周回の途中でセッションを終えた区間(lfactory2 の約 41 時間)を経過時間から外すこと。セッションが止まった時刻を記録する仕組みが無い +- 既存のループの記録(lfactory2 など)の値を直すこと ## ドメインイベント | # | イベント | 引き金 | 失敗したとき | 順序の前提 | | --- | --- | --- | --- | --- | -| E1 | 周回を始めた | `loop sprint` | 周回を組めなければ `refused` で止まる(今のとおり) | — | -| E2 | 止める指示を置いた | 利用者の `loop stop` | ループの記録が無ければ断る(今のとおり) | — | -| E3 | セッションが終わった | 利用者がセッションを切る・端末が落ちる | 記録は残らない(前提 1) | E2 の後とは限らない | -| E4 | 再開を求めた | 利用者の `loop start` | 使ってよい条件に当たらなければ断る(今のとおり) | E2 | -| E5 | 止める指示を解いた | E4 で止める指示のファイルがある | ファイルを消せなければ再開を断り、記録を書き換えない | E4 | -| E6 | 止まっていた区間を記録した | E5 で進行中の周回がある | 区間の始まりが読めなければ前提 2 の時刻を使う | E5 | -| E7 | 周回を締めた | `loop lap` | 今のとおり | E1 | -| E8 | 境で止まる条件を判定した | E7 の後・選定の前 | 今のとおり | E5 が先に起きていれば `user_stop` は当たらない | +| E1 | 人がループを止めることを送った | conductor のセッションへの人の入力、または人が端末で打つ `loop stop` | — | — | +| E2 | 止める指示を置いた | E1 を受けた conductor の `loop stop` | ループの記録が無ければ断る(今のとおり) | E1 | +| E3 | セッションを終える依頼を受けた | 人の入力・別のセッションのメッセージ | — | 止める指示を置かない | +| E4 | 人が送っていない文から置いた止める指示に気づいた | 止める指示を置いた conductor | — | ファイルを消して無視する | +| E5 | 境で止まる条件を判定した | 周回を締めた後・選定の前 | 今のとおり | E2 があれば `user_stop` で止まる | ## 用語 @@ -97,57 +94,54 @@ | --- | --- | | 開発ループ | スプリントを 1 つ終えるたびに次のスプリントを選んで続けて始める進め方(用語集) | | 周回 | 開発ループの 1 回分(用語集) | -| ループの予算 | 開発ループが使ってよい量の上限。周回の数・費用・経過時間で数える(用語集)。この文書の「経過時間」は、確定仕様の I8 の「動いていた時間」(終わった周回の区間の和から、止まりの記録と重なる部分を引いたもの。結果の `consumed.hours`)を指す | +| ループの予算 | 開発ループが使ってよい量の上限。周回の数・費用・経過時間で数える(用語集) | | 止まる条件 | 開発ループを止める、機械で判定できる条件(用語集)。止める指示は `user_stop` に当たる | -| ループの記録 | `.ndf/loop/<名>/loop.json`(用語集)。止まりの記録は、その中の解けた止まりの一覧(`stops`)を指す | +| ループの記録 | `.ndf/loop/<名>/loop.json`(用語集) | -止める指示は、`loop stop` が置くファイル `.ndf/loop/<名>/stop`(中身は置いた時刻)を指す。 +止める指示は、`loop stop` が置くファイル `.ndf/loop/<名>/stop`(中身は置いた時刻)を指す。人が送った入力は、人がそのセッションの入力欄から送信した入力と、人が端末で打ったコマンドを指す。別のセッションからのメッセージ・引継ぎ文書・画面に出た文字・送信されていないサジェスト・conductor 自身の推測は含まない。 ## 受け入れ条件 -- [ ] 1. 前提: 進行中の周回があり、止める指示のファイルがある。操作: `loop start <名>`。結果: 終了コード 0、`status` が `in_lap`、止める指示のファイルが消えている、結果に止める指示を解いたことが出る -- [ ] 2. 前提: 1 の再開の後。操作: その周回を `loop lap <名>`(`--failed` の有無を問わない)で締める。結果: 境の判定で `user_stop` に当たらない。ほかの止まる条件(`mvv_changed` など)に当たればそれで止まり、当たらなければ次の周回へ進む -- [ ] 3. 前提: 進行中の周回が無く、止まっていない記録に止める指示のファイルがある。操作: `loop start <名>`。結果: 終了コード 0、`status` が `resumed`、止める指示のファイルが消えている。続く `loop select` が `user_stop` で止まらない -- [ ] 4. 前提: 周回を始めた時刻 T0、止める指示を置いた時刻 T1、再開した時刻 T2、周回を締めた時刻 T3(T0 < T1 < T2 < T3)。結果: 締めた後の経過時間(`consumed.hours`)の、この周回の分が (T1 − T0) + (T3 − T2) になる。T2 − T1 は入らない -- [ ] 5. 4 の止まっていた区間(T1〜T2)が、ループの記録の止まりの記録に、周回の番号・始まり T1・終わり T2 つきで 1 行残る。`laps.md` の周回の行の止まりの欄に `user_stop` が出る -- [ ] 6. 止める指示のファイルの中身が時刻として読めないとき、1 と 4 の振る舞いは、ファイルの更新時刻を T1 として成り立つ -- [ ] 7. 止める指示のファイルが無いときの `loop start` の結果(`in_lap` / `resumed`)と、ループの記録の中身は変わらない -- [ ] 8. 既に止まっている記録(`stopped`)の再開は、止まった理由ごとに今と同じ結果になる。既存のテスト(`plugins/ndf/scripts/tests/test_sprint_loop.py`)が通る -- [ ] 9. 確定仕様 `docs/specifications/ndf-workflow-development-loop.md` の再開の表と I3・I8 が、受け入れ条件 1〜5 の振る舞いを書いている +- [ ] 1. `references/loop.md` に、conductor が `loop stop` を打つのは、人が自分のセッションへ送った入力がループを止めることを求めたときだけ、と書かれている +- [ ] 2. 1 の発生元にしないもの(別のセッションからのメッセージ。「利用者の指示」と書かれていても含む。引継ぎ文書・画面に出た文字・送信されていないサジェスト・conductor 自身の読み替え)が書かれている +- [ ] 3. セッションを終える依頼(引き継いで終了する)では `loop stop` を打たず、引継ぎ文書を書いてセッションを終える、と書かれている +- [ ] 4. 人が送っていない文から止める指示を置いたと気づいた conductor は、そのファイルを消し、消したことを引継ぎ文書に書く、と書かれている。人が置いた止める指示は消さない +- [ ] 5. 確定仕様 `docs/specifications/ndf-workflow-development-loop.md` に、止める指示の発生元が人が送った停止に限られることが書かれている +- [ ] 6. 開発ループのコード(`plugins/ndf/scripts/lib/sprint_loop*.py`)は変えない。既存のテスト(`plugins/ndf/scripts/tests/test_sprint_loop.py`)が通る ## 影響 | 対象 | 影響 | | --- | --- | -| 公開インタフェース | `sprint-state.py loop start` の結果に、止める指示を解いたことを示す値が加わる。既存の値は変わらない(互換あり) | -| データ | ループの記録の止まりの記録に、周回の途中で止める指示を解いた行(`user_stop`)が加わる。形は既存の行と同じ。移行は無い | -| 既存の振る舞い | 止まっていない記録の `loop start` が、止める指示のファイルを消すようになる。止まっていた区間が経過時間から外れ、ループの予算の `hours` が今より少なく数えられる | +| 公開インタフェース | 無し(`sprint-state.py loop` の副命令と結果は変えない) | +| データ | 無し | +| 既存の振る舞い | conductor が `loop stop` を打つ場面が、人が送った停止に限られる | ## 検証手段 | 項目 | 手段 | | --- | --- | -| テスト | `uv run --frozen --project . --all-extras pytest plugins/ndf/scripts/tests/test_sprint_loop.py -q`(受け入れ条件 1〜8。時刻は `Env.now` の差し替えで進める) | -| テスト(全体) | `uv run --frozen --project . --all-extras pytest . -q -n 4` | -| 静的解析 | `ruff format --check` と `ruff check`(継続的統合の `lint.yml` と同じ) | -| 手動確認 | 次に開発ループを回すとき、周回の途中で `loop stop` → セッションを切る → `loop start` を通し、止める指示が消え、`laps.md` の経過時間に休ませた時間が入らないことを見る(リリース後テスト) | +| 文書のレビュー | 受け入れ条件 1〜5 を設計と実装の Pull Request のレビューで読む(`.md` の文言を照合するテストは書かない) | +| テスト | `uv run --frozen --project . --all-extras pytest plugins/ndf/scripts/tests/test_sprint_loop.py -q`(受け入れ条件 6) | +| リンクの検査 | `python3 scripts/check-markdown-links.py --root .`(手順書から足したリンクの参照切れ) | +| 手動確認 | 次に別のセッションからセッションを終える依頼を受けた conductor が、`loop stop` を打たずに引継ぎ文書を書いて終えることを見る(リリース後テスト) | ## 前提とする取り決め | 項目 | 参照先 / 決めたこと | | --- | --- | -| プロジェクト構造 | 開発ループのコードは `plugins/ndf/scripts/lib/sprint_loop*.py`。外の世界は `Env` を通して呼び、テストで差し替える(`sprint_loop_record.py` の先頭の説明) | +| 手順書の置き場 | conductor の手順は `plugins/ndf/skills/development-workflow/references/loop.md` | | コーディング規約 | `AGENTS.md` の「最小限のコード実装」。`.md` の文言を照合するテストは書かない | -| テスト戦略 | 単体テストで `cmd_start`・`cmd_lap`・経過時間の計算を、差し替えた `Env` と時刻で確かめる(既存の `test_stop_file_works_at_the_next_boundary_and_resume_removes_it` と同じ階層) | +| 関連の決まり | 画面の入力欄の文字を判断に使わない(`references/parallel.md` の「レーンの画面を見るとき」) | ## 境界 | 区分 | 内容 | | --- | --- | | 常に行う | 既存テストの実行、確定仕様の該当箇所の更新 | -| 確認してから行う | `loop start` の結果の形の変更(承認ゲート 1・2 で見る) | -| 行わない | 既存のループの記録の書き換え、境の判定の順の変更、止める指示を周回の途中で効かせること | +| 確認してから行う | 無し | +| 行わない | 開発ループのコードの変更、既存のループの記録の書き換え | ## 未決 -無し(決められない点は前提 1〜5 に置いた。前提 1 の扱いは承認ゲート 1 で利用者が確かめる) +無し(人が送ったかを機械で見分けない理由は前提 2 に置いた) From eaf2e874e877a2a4ba0d3c268be0ebe927b476c4 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Thu, 5 Nov 2026 01:01:19 +0000 Subject: [PATCH 6/7] =?UTF-8?q?Docs:=20#2474=20=E3=81=AE=E8=A8=AD=E8=A8=88?= =?UTF-8?q?=E6=96=87=E6=9B=B8=E3=82=92=E7=B5=84=E3=81=BF=E7=9B=B4=E3=81=99?= =?UTF-8?q?=EF=BC=88=E7=A2=BA=E5=AE=9A=E4=BB=95=E6=A7=98=E3=81=AE=E7=9B=B4?= =?UTF-8?q?=E3=81=97=E6=96=B9=E3=82=92=E4=B8=AD=E8=BA=AB=E3=81=AE=E7=AB=A0?= =?UTF-8?q?=E3=81=AE=E6=9C=AB=E5=B0=BE=E3=81=B8=E7=A7=BB=E3=81=97=E3=80=81?= =?UTF-8?q?=E6=A0=B9=E6=8B=A0=E3=81=AE=20Value=20=E3=81=AB=E9=A1=8C?= =?UTF-8?q?=E3=82=92=E6=B7=BB=E3=81=88=E3=82=8B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 「確定仕様の直し方」を「処理の流れ」の直後へ移し、何をどう書くかの章を決定の記録の前にまとめた - 決定の根拠の Value の番号に題を添えた。「どの段にも」を「どちらの会話の記録にも」に置き換えた - 内容は足していない(行数 216 → 216) Co-Authored-By: Claude Opus 5.5 --- issues/issue-2474-design.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/issues/issue-2474-design.md b/issues/issue-2474-design.md index 8a6b2a823..5a9f7e45b 100644 --- a/issues/issue-2474-design.md +++ b/issues/issue-2474-design.md @@ -16,7 +16,7 @@ | 根拠 | 種類 | 何を示すか | | --- | --- | --- | -| 会話の記録 `37e6fcce`(lflow)・`f3bae19d`(lfactory2)の 2026-11-02 13:07〜13:09 UTC(要求の「目的」の表) | 実測 | 止める指示を置いたのは conductor 自身の読み替えで、どの段にも人がループを止めることを送った記録が無い | +| 会話の記録 `37e6fcce`(lflow)・`f3bae19d`(lfactory2)の 2026-11-02 13:07〜13:09 UTC(要求の「目的」の表) | 実測 | 止める指示を置いたのは conductor 自身の読み替えで、どちらの会話の記録にも人がループを止めることを送った記録が無い | | 2026-11-05 の利用者の判断(課題 #2474 の「人の判断待ち」の節) | 利用者の決定 | 送信されていない書きかけの指示は指示として扱わず、止める指示は人が実際に送ったものだけを有効にする | | `git grep -n "STOP_FILE" -- plugins/ndf/scripts/lib`(要求の前提 1) | 実測(コードの検索) | 止める指示のファイルを書くのは `loop stop`(`cmd_stop`)だけで、発生元を絞る場所は `loop stop` を打つ側にある | | 確定仕様 `docs/specifications/ndf-workflow-development-loop.md` の I3・I8 | 既存の規則 | 人が送った停止は次の境で効き、境で止まった後の区間はすでに経過時間から外れる | @@ -133,6 +133,16 @@ flowchart TD 図は `loop.md` の新しい小見出しが conductor へ示す判断である。図に含めない要素: 確定仕様(文書)、変えない開発ループのコード、 リンク先の `parallel.md`。 +## 確定仕様の直し方 + +`docs/specifications/ndf-workflow-development-loop.md` を次のとおりに直す(受け入れ条件 5)。 + +| 箇所 | 直す内容 | +| --- | --- | +| 冒頭の「利用者の止める指示では…止まる」の行 | 止める指示は人が送った停止からだけ置かれることを添える | +| 常に成り立つ条件の I3 | 止める指示を置くのは、人が送った入力がループを止めることを求めたときだけで、セッションを終える依頼・別のセッションのメッセージからは置かないことを加える | +| ループの記録の置き場の表の `stop` の行 | 「人が送った停止を受けて `loop stop` が置き、`user_stop` の再開で消す」にする | + ## 決定の記録 ### 決定 1: 発生元の限定は conductor の手順で決め、コマンドや hook の側で入力元を判定しない @@ -144,7 +154,7 @@ flowchart TD hook で `loop stop` の呼び出しを会話の記録の入力元で止める形は採らない。内部の値に依存し、ランタイムによって働かない。 -根拠: Value 6 / Value 5(MVV 版 5) +根拠: Value 6(どのプロジェクトでも使える形を保つ) / Value 5(決まった手順はスクリプトに、判断と中身は AI に)(MVV 版 5) ### 決定 2: `loop start` で止める指示を消さない @@ -152,7 +162,7 @@ hook で `loop stop` の呼び出しを会話の記録の入力元で止める 切り替え)も周回の途中で `loop start` を打つため、`loop start` で止める指示を消すと、人が送った停止が境の前に消える。 発生元を人が送った停止に限れば、再開の後に止める指示が効くのは人の意図どおりである。 -根拠: 共通原則の上位の原則(人の判断を守る) / Value 7(MVV 版 5) +根拠: 共通原則の上位の原則(人の判断を守る) / Value 7(根本原因の場所で直す)(MVV 版 5) ### 決定 3: 経過時間の計算を変えない @@ -162,14 +172,14 @@ hook で `loop stop` の呼び出しを会話の記録の入力元で止める lfactory2 の周回 4 に入った約 41 時間は、周回の途中でセッションを終えて 2 日後に再開した区間である。セッションが止まった 時刻を記録する仕組みが無いため、この変更では扱わない(「未確認のまま残ること」)。 -根拠: Value 3 / Value 4(MVV 版 5) +根拠: Value 3(やめる・縮めるを既定にする) / Value 4(外の利用で測って決める)(MVV 版 5) ### 決定 4: セッションを終える依頼を止める指示に読み替えない セッションを終える依頼の目的は、周回を進行中のまま残し、次の起動で続けることである。止める指示を置くと、次の起動で続けた 周回が境で `user_stop` で止まり、依頼の目的と逆になる。lfactory2 で起きたのはこの読み替えである。 -根拠: Mission / Value 2(MVV 版 5) +根拠: Mission / Value 2(人が判断できる量に収める)(MVV 版 5) ### 決定 5: 誤って置いた止める指示は、置いた conductor だけが消す @@ -179,7 +189,7 @@ lfactory2 の周回 4 に入った約 41 時間は、周回の途中でセッシ 止める指示のファイルに置いた理由を書き足す形は採らない。書く内容を検証できず、決定 1 と同じく人が送ったかを判定できない。 -根拠: 共通原則の上位の原則(人の判断を守る) / Value 7(MVV 版 5) +根拠: 共通原則の上位の原則(人の判断を守る) / Value 7(根本原因の場所で直す)(MVV 版 5) ## テスト設計 @@ -190,16 +200,6 @@ lfactory2 の周回 4 に入った約 41 時間は、周回の途中でセッシ | 受け入れ条件 6・I3 | 既存の `plugins/ndf/scripts/tests/test_sprint_loop.py` が通る | 開発ループのコードを変えると落ちる | | 手順書の参照 | `loop.md` から `parallel.md` の見出しへのリンクを、リンクの検査(`python3 scripts/check-markdown-links.py --root .`。継続的統合の `runtime-plugin-validate.yml` と同じ)で確かめる | 見出しの名前を違えると落ちる | -## 確定仕様の直し方 - -`docs/specifications/ndf-workflow-development-loop.md` を次のとおりに直す(受け入れ条件 5)。 - -| 箇所 | 直す内容 | -| --- | --- | -| 冒頭の「利用者の止める指示では…止まる」の行 | 止める指示は人が送った停止からだけ置かれることを添える | -| 常に成り立つ条件の I3 | 止める指示を置くのは、人が送った入力がループを止めることを求めたときだけで、セッションを終える依頼・別のセッションのメッセージからは置かないことを加える | -| ループの記録の置き場の表の `stop` の行 | 「人が送った停止を受けて `loop stop` が置き、`user_stop` の再開で消す」にする | - ## 設計の結果 | 課題 | 扱い | 取り込み先 | 触るファイル | From 8c5982fe95caca028e6a2698391b33ba4b8ad33a Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Thu, 5 Nov 2026 01:06:37 +0000 Subject: [PATCH 7/7] =?UTF-8?q?Docs:=20#2474=20=E3=81=AE=E5=87=A6=E7=90=86?= =?UTF-8?q?=E3=81=AE=E6=B5=81=E3=82=8C=E3=81=A7=E3=80=81=E3=82=BB=E3=83=83?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=92=E7=B5=82=E3=81=88=E3=82=8B?= =?UTF-8?q?=E3=81=A0=E3=81=91=E3=81=AE=E4=BE=9D=E9=A0=BC=E3=82=92=E9=80=81?= =?UTF-8?q?=E4=BF=A1=E5=85=83=E3=81=AB=E3=82=88=E3=82=89=E3=81=9A=E5=BC=95?= =?UTF-8?q?=E7=B6=99=E3=81=8E=E3=81=A7=E7=B5=82=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 送信元の確認を、ループを止めることを求める文だけに掛ける。要求の E3・受け入れ条件 3 と設計の E3・F2 に揃える。 Co-Authored-By: Claude Opus 5.5 --- issues/issue-2474-design.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/issues/issue-2474-design.md b/issues/issue-2474-design.md index 5a9f7e45b..206748ffe 100644 --- a/issues/issue-2474-design.md +++ b/issues/issue-2474-design.md @@ -121,13 +121,15 @@ conductor が止めることを求める文を受けたときの判断を、次 ```mermaid flowchart TD - A[止める・終えることを
求める文を受けた] --> B{人が自分のセッションへ
送った入力か} - B -->|いいえ| C[止める指示を置かない
人に確かめる] - B -->|はい| D{ループを止めることを
求めているか} - D -->|はい| E[loop stop を打つ] + A[止める・終えることを
求める文を受けた] --> D{ループを止めることを
求めているか} D -->|セッションを
終えるだけ| F[引継ぎ文書を書いて終える
loop stop を打たない] + D -->|はい| B{人が自分のセッションへ
送った入力か} + B -->|はい| E[loop stop を打つ] + B -->|いいえ| C[止める指示を置かない
人に確かめる] ``` +セッションを終えるだけの依頼は、送信元によらず引継ぎ文書を書いて終える(E3・F2)。送信元を確かめるのは、ループを止めることを求める文だけである。 + 人が送っていない文から止める指示をすでに置いていたと気づいたときは、ファイルを消して引継ぎ文書に書く(F3)。 図は `loop.md` の新しい小見出しが conductor へ示す判断である。図に含めない要素: 確定仕様(文書)、変えない開発ループのコード、