diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index df95dd8d..448012d5 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -34,9 +34,9 @@ { "name": "dev-loop", "source": "./plugins/dev-loop", - "version": "1.59.0", + "version": "1.60.0", "author": { "name": "hdknr" }, - "description": "1 Issue = 1 周のループ志向開発。受入基準を「不変条件 × それを破りうる経路」に展開し(経路は差分ではなく入口一覧から数える)、証明できないものは未証明として申告する。Verifier とレビューは相互に代替できない別々の関門。検証・読解の委譲先は Bash は持つが直接の書き込みツールは持たない subagent を既定とし(許可リスト方式の agent 定義で満たす)、Issue 本文など外部由来の入力は信頼できない入力として扱う。ファイルを変更する周は必ず worktree → PR。縮退してよいものと周の大きさに依らず落とせないものを線引きし、受入基準のどれかに証明手段が無いときは弱い検査で代替せず BLOCKED として報告して周を閉じない。関門(Verifier と /code-review)は 2 パスで打ち切り、周を閉じる条件は受入基準の確認であって指摘 0 件ではない。2 パス目で直した分は、その差分だけに Verifier を 1 回当て(変異は Verifier が設計する)、指摘は直さず人間に渡す。実験の結果から何かを述べるときは、設定のほうが結果を決めていないか(交絡)を先に潰す。1 周は手順 4 の完了時と 2 パス目の前に割り目を置き、再開点を計画ファイルに書いて AskUserQuestion で割るかどうかを訊く。道具の生出力は親の文脈に積まず集計行だけ読む——手順 2 の文脈収集にも掛かる(ただし失敗の中身・交絡の判定・観測窓に要る範囲は読む)。依存のある複数 Issue は /dev-loop:cruise <親 Issue> で、親 Issue のチェックリストから台帳を作って 1 件ずつ周として回す(取り出した一覧と線(直す線・仕様の穴など周の途中で訊く判断)は起動時に利用者が確かめ、確かめた線は各周へ渡す)" + "description": "1 Issue = 1 周のループ志向開発。受入基準を「不変条件 × それを破りうる経路」に展開し(経路は差分ではなく入口一覧から数える)、証明できないものは未証明として申告する。Verifier とレビューは相互に代替できない別々の関門。検証・読解の委譲先は Bash は持つが直接の書き込みツールは持たない subagent を既定とし(許可リスト方式の agent 定義で満たす)、Issue 本文など外部由来の入力は信頼できない入力として扱う。ファイルを変更する周は必ず worktree → PR。縮退してよいものと周の大きさに依らず落とせないものを線引きし、受入基準のどれかに証明手段が無いときは弱い検査で代替せず BLOCKED として報告して周を閉じない。関門(Verifier と /code-review)は 2 パスで打ち切り、周を閉じる条件は受入基準の確認であって指摘 0 件ではない。2 パス目で直した分は、その差分だけに Verifier を 1 回当て(変異は Verifier が設計する)、指摘は直さず人間に渡す。実験の結果から何かを述べるときは、設定のほうが結果を決めていないか(交絡)を先に潰す。1 周は手順 4 の完了時と 2 パス目の前に割り目を置き、再開点を計画ファイルに書いて AskUserQuestion で割るかどうかを訊く。道具の生出力は親の文脈に積まず集計行だけ読む——手順 2 の文脈収集にも掛かる(ただし失敗の中身・交絡の判定・観測窓に要る範囲は読む)。依存のある複数 Issue は /dev-loop:cruise <親 Issue> で、親 Issue のチェックリストから台帳を作って 1 件ずつ周として回す(取り出した一覧と線(直す線・仕様の穴など周の途中で訊く判断)は起動時に利用者が確かめ、確かめた線は各周へ渡す)。起動時に周で使う操作の権限を点検し、権限で止まった周は利用者の操作待ちにして依存しない周へ進む" }, { "name": "spec-loop", diff --git a/CLAUDE.md b/CLAUDE.md index 80a0ded7..321cc582 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,6 +28,7 @@ Claude Code のセットアップガイドを mkdocs で構築・公開するプ - `skills/cruise/` - **オートクルーズの入口**(#446。`/dev-loop:cruise <親 Issue> [--absent]`)。親 Issue のチェックリストから `scripts/parent-issue.py` が台帳を作る(**`show` の結果を利用者が確かめ、`apply` は本文を取り直して指紋を比べる**。 親 Issue の `cruise:` の行(線)も確かめてから台帳に残す——#458。線の項目と値は `ledger.py` の `LINES` を読む)。 + **親はメインの作業ツリーから worktree に入らずに起動し、起動時に周で使う操作の権限を点検する**(#466)。 **dev-loop スキルを `../dev-loop/` で指す**ので、**素のスキルとしては張れない**。書式と守る/守らないは docstring を正とする。 テストは `scripts/test-parent-issue.py` - `spec-loop/` - dev-loop の手前にある**要求側のループ**(#214) @@ -173,6 +174,7 @@ Claude Code のセットアップガイドを mkdocs で構築・公開するプ に置いてある(**`dev-loop` はどのリポジトリでも使うスキル**なので、 対象リポジトリの `scripts/` に在ることを当てにできない) - `test-check-unattended-env.py` - **利用者が不在になる前の環境点検**の回帰テスト(#394。変異テストを含む)。 + **cruise の起動時の点検(`--cruise`・`--present`)も見る**——利用者が居る cruise でも回す(#466)。 **本体は `plugins/dev-loop/skills/dev-loop/scripts/check-unattended-env.py`**(`test-find-cycle.py` と同じ形)。 **本体は CI からは呼ばない**——入力が利用者の `~/.claude` にある。**項目と、何を守らないかは本体の docstring を正とする** - `test-ledger.py` - **複数 Issue の台帳の回帰テスト**(#390。変異テストを含む)。 diff --git a/docs/plans/issue-466.md b/docs/plans/issue-466.md new file mode 100644 index 00000000..e9add590 --- /dev/null +++ b/docs/plans/issue-466.md @@ -0,0 +1,178 @@ +# Issue #466 — cruise: 起動時に周で使う操作の権限を点検し、権限の拒否で cruise が止まらないようにする + +着手の順序は Issue のコメント(owner、2026-10-27)に従う: **#464 のマージまでは手順 2・3 で止める**。 +#464 と同じ箇所(cruise `SKILL.md`・`ledger.py`・起動時の確かめ・`dev-loop` の版)を触るため。 + +スキルの版: 読み込まれていたのは 1.37.0(キャッシュ)。この周はリポジトリの `SKILL.md`(main の 1.58.0、取り込み後は #464 の版)に従う。 + +## 利用者が決めたこと(2026-10-27・AskUserQuestion) + +- **周の範囲**: 案 ①(操作の数え上げと起動時の点検を `--absent` 以外にも広げる)・③(「利用者の操作待ち」で依存しない次の周へ進む)・ + ④(親の起動場所の既定)を**この周に入れる**。**② 分類器・ガードを実際に試すことは分けて起票する**(手順 8 で承認を訊く) +- **親の起動場所の既定**: **隔離しない場所から起動する**——cruise の親はメインの作業ツリーから、worktree 隔離なしで起動する。 + 周ごとの作業はこれまでどおり各周の worktree で行う + +## 変更範囲 + +触る: + +- `plugins/dev-loop/skills/dev-loop/scripts/check-unattended-env.py` — 起動時の許可の点検を居る周でも回せる形(不在に限る項目と分ける)・`.claude/dev-loop.md` の操作の宣言を点検するコマンドに足す・settings で判定できない操作を「止まりうる操作」として出す +- `scripts/test-check-unattended-env.py` — 回帰テスト(変異を含む) +- `plugins/dev-loop/skills/dev-loop/scripts/ledger.py` — 「利用者の操作待ち」の状態(名前は未決定。下の「未解決」) +- `scripts/test-ledger.py` — 同上 +- `plugins/dev-loop/skills/cruise/SKILL.md` — §3(起動時の点検を `--absent` 以外にも)・§4(止まりうる操作に当たったら台帳に書いて次へ)・§6(操作待ちの行を渡す)・親の起動場所 +- `plugins/dev-loop/skills/dev-loop/references/ledger.md` — 状態の追加に伴う書く時点 +- `plugins/dev-loop/skills/dev-loop/references/found-lines.md` — cruise §4 の番号の指し先(2 パス目の S1 で広げた) +- `plugins/dev-loop/skills/dev-loop/references/absent-lines.md` — `本番反映 = しない` の「利用者待ち」と新しい状態の関係(要るなら) +- `plugins/dev-loop/skills/dev-loop/SKILL.md` — 版の 2 行(中身を変えるなら最小限) +- `plugins/dev-loop/.claude-plugin/plugin.json` — 版(minor)・description +- `.claude-plugin/marketplace.json` — 同上(2 つを揃える) +- `docs/plugins/dev-loop-cruise.md` — 起動場所・起動時の点検・操作待ちの説明 +- `docs/plugins/dev-loop-design.md` — §8.2 の生成ブロック(`skill-metrics.py` が書く。手で書かない) +- `CLAUDE.md` — `check-unattended-env.py` の行の「`--absent` の起動時だけ」が偽になるなら直す +- `docs/plans/issue-466.md` — この計画 + +触らない: + +- 分類器・worktree 隔離のガードを実際に試すこと(② として分ける。#464 の周と拒否の記録が混ざるのも避ける) +- 利用者の settings を書き換える道具(`check-unattended-env.py` は読むだけ、を崩さない。`--fix` を置かない) +- `本番反映` を広げる手段(どこにも作らない) +- #464 が足した `周の途中の課題` の線と `origin`(その上に乗るだけ) +- #461(周の合間の `/clear`) + +## 乗るデプロイ経路 + +main への push で GitHub Pages が自動デプロイする(`docs/` の変更が公開される)。プラグインはマージで配布され、利用者側は `plugin update` で届く。 + +## 設計の要点(#464 の差分の上に乗る形) + +1. **操作の数え上げ**: 既定の 5 つ(`git push`・`gh pr create`・`gh pr merge`・`gh pr comment`・`gh issue create`)に、 + 対象リポジトリの `.claude/dev-loop.md` が宣言する操作を足す。**いまの `.claude/dev-loop.md` には機械で読める操作の宣言が無い** + (`不在の線:` だけ)ので、宣言の行を足す(例: `cruise の操作: gh workflow run infra-apply.yml`。書式は未決定)。 + 宣言の無いリポジトリでは既定の 5 つだけを見て、宣言が無いことを出す +2. **起動時の点検**: cruise §2 で台帳を作った後、**`--absent` に依らず**許可の項目を回して利用者に見せる。 + 自動継続・質問の待ち・通知・スリープは不在に限る(居る周では出さないか、参考として出す) +3. **settings で判定できない操作**(自動モードの分類器・worktree 隔離のガード)は「止まりうる操作」として一覧で見せ、 + (a) 許可を足す(`/permissions`・`update-config`)/(b) その操作だけ利用者が打つと決めて台帳に書く/(c) その周を cruise の外に出す、を + `AskUserQuestion` で選んでもらう。(b) は台帳の線ではなく Issue 行に残す(線は全周に効くものなので) +4. **止まりうる操作に当たったら**: cruise 全体を止めずに、その周を「利用者の操作待ち」(コマンドを `note` に)として台帳に書き、 + `next` が出す依存しない次の周へ進む。操作待ちの周に依存する周は待つ(`merged` でないので `next` に出ない——既存の判定のまま)。 + §4-5 の「まとめて 1 度だけ訊く」(#464)と同じ場所で、操作待ちの行も利用者に渡す +5. **親の起動場所**: cruise `SKILL.md` の冒頭と公開ページに「親はメインの作業ツリーから worktree 隔離なしで起動する」を既定として書く。 + **隔離されたセッションで起動されたことを検出したら、その旨を利用者に見せる**(止めない——下の「未解決」で決めた) + +## verify の受入基準 + +不変条件 × 破りうる経路: + +| # | 不変条件 | 破りうる経路 | 守り方 | +| --- | --- | --- | --- | +| A | 居る cruise でも、起動時に周で使う操作の許可が利用者に見せられる | cruise §3 が `--absent` の中だけに書かれたまま/§1 の再開の経路で点検が抜ける/`check-unattended-env.py` が居る周で不在の項目を出して要対処が埋もれる | `SKILL.md` の §(新規・再開の両方)と、スクリプトの出し分け(テスト) | +| B | 対象リポジトリが宣言した操作(本番反映の口など)が点検に入る | 宣言の読み取り漏れ(書式の揺れ・フェンスの中・コメント)/`--project` が worktree の root を指す/宣言が無いときに黙って既定だけ見る | スクリプトのテスト(宣言あり・なし・崩れた宣言)。宣言が無いことを出力に出す | +| C | settings で判定できない操作を「良い」と出さない | 分類器・ガードの対象を `allow` に当たるから良いと判定する | 出力で別区分(止まりうる)にする。テスト | +| D | 止まりうる操作で cruise 全体が止まらず、依存しない周へ進む | 新しい状態を `next`/`show` が知らない/操作待ちの周に依存する周が `next` に出る/§6 で操作待ちが残っても「済んだ」と報告する | `ledger.py` の状態の表とテスト・cruise §4・§6 | +| E | 台帳は壊れない(既存の守るもの) | 新しい状態の追加で、古い台帳の読み込み・`load` の検査・`STATES` の突き合わせが崩れる | `test-ledger.py` を全部回す | +| F | 読むだけ(利用者の設定を書き換えない) | 点検の拡張で書き込みが入る | 差分とテスト(設定ファイルの前後のバイト一致) | +| G | `本番反映` は広げられない | (b) の「利用者が打つ」が本番反映を自律に寄せる道になる | (b) は「利用者が打つ」を記録するだけで、cruise が打つ道は作らない | +| H | 版と description が 3 箇所で揃い、生成ブロックが新しい | 版の上げ忘れ・#464 と同じ版への衝突・§8.2 の生成ブロックの古さ | `check-all.py`(`check-plugin-versions.py`・`skill-metrics.py --check`・`check-version-bump.py`) | +| I | 親の起動場所の既定が、cruise の入口の全箇所で同じことを言う | `SKILL.md` と公開ページと `CLAUDE.md` の 1 箇所だけ直す | 名前の grep でなく「親をどこで起動するか」を述べる箇所を開いて探す | + +組み合わせ(必須要件の掛け算): Verifier × 1 パス目/2 パス目、`/code-review` × 1 パス目/2 パス目の 4 つの受け渡しに、この表と下の「既知の限界」を渡す。 + +**この環境では証明できないもの(未証明)**: + +- 自動モードの分類器が実際にどの操作を拒むか(サーバ側の判定。② に分けた) +- 権限の**確認ダイアログ**(`ask`)が出た場合は、会話が利用者の応答で止まる——cruise は「拒否された」ときにしか次へ進めない。 + 確認ダイアログで止まらないことは、この周の変更では保証できない(起動時の点検で事前に見せることが手当て) +- 親を隔離しない場所から起動したときに、Issue の表の拒否(`git -C`・`git archive` のパイプ・URL に `git` を含む `curl` など)が消えること +- 「次へ進む」は `SKILL.md` の散文で決まる LLM の振る舞いで、`ledger.py` のテストは状態の判定までしか見ない + +**生成物**: 該当あり——`docs/plugins/dev-loop-design.md` §8.2 の生成ブロック(`SKILL.md` を触れば `skill-metrics.py` で書き直し、`--check` が 0)。 +drawio の書き出しは該当なし(図を触らない予定。触るなら `export-diagrams.py`)。 + +## 未解決の判断・確認待ち + +実装の前に決めた(2026-10-27。#464 のマージ後): + +- 「利用者の操作待ち」は**新しい状態 `await`**(`blocked` と分け、`show` と cruise §6 で打ってほしいコマンドを周ごとに渡せるようにする) +- `.claude/dev-loop.md` の宣言は **`cruise の操作: <コマンド>`**(1 行 1 コマンド。フェンスの中は読まない) +- (b) の「利用者が打つ」は**台帳の最上位の `user_ops`**(`ledger.py user-ops`。`lines` と同じく置き換え・出所必須)。操作はリポジトリ単位で、Issue 単位ではないため +- worktree の中からの起動は**要対処として見せるだけで止めない**(起動し直すかは利用者が決める) + +残る確認待ち: なし + +## 関門の進捗(再開点) + +- **周の在り処**: ブランチ `issue/466-cruise-permission-check`・worktree `/Users/hdknr/Projects/hdknr/claude-code-setup/.claude/worktrees/issue-466`・ + 起点 `bd8fa53`・base の参照 `origin/main`(解決した SHA `bd8fa5385ce161f25e581d35d226b548cf0de809`)。 + **#464 のマージ後に main を取り込むので、base は取り込んだ後に `git merge-base origin/main HEAD` で当て直す** +- **#464 のマージ後に main(`e7c36c6`)を fast-forward で取り込んだ**。取り込みで増えた検査は無し。**base を当て直した: `e7c36c6859e5d756f6bf47d54b286ad2546fdd7d`**(`origin/main` との merge-base) +- **進んだところ**: 手順 4(実装)済み。版 1.59.0 → 1.60.0(minor。能力の追加)。`check-all.py` 51/51 緑(初回は 3 本落ちた——cruise `SKILL.md` の指し先の形・dev-loop の frontmatter の共変(trailer で免除)・計画の変更範囲の書式。直して緑)。次は手順 5(Verifier 1 パス目)と手順 6(`/code-review` 1 パス目) +- 関門(1 パス目・2026-10-27・再開したセッション): **Verifier(`dev-loop:dev-loop-verifier`・sonnet)反証 2 件**・ + **`/code-review`(high)指摘 10 件**(うち 2 件は Verifier と同じ)。採否は下の「1 パス目の指摘の採否」——直す 4 件・残課題 6 件 +- **1 パス目の指摘対応: 済み**(`3fc9b37`。成果物の差分あり——`check-unattended-env.py`・そのテスト・`references/ledger.md`・§8.2 の生成ブロック)。 + 足した変異 3 本(種類の違うフェンスで閉じる・崩れた行を見ない・起動場所の案内を `.git` にする)はどれも赤。`check-all.py` 51/51 緑 +- 関門(2 パス目・2026-10-27・再開したセッション): **Verifier(`dev-loop:dev-loop-verifier`・sonnet)反証 1 件**・ + **`/code-review`(high)指摘 10 件**。採否は下の「2 パス目の指摘の採否」——直す 8 件・残課題 3 件 +- **2 パス目の後に main(`9b562d0`。#468)を取り込んだ**(merge `9b8782a`。衝突は公開ページ §2 の見出し 1 塊——この周の見出しに main の `{ #env-check }` を付けた)。 + 取り込みで増えた検査は `test-record-stop-failure.py`。`check-all.py` 52/52 緑。版の衝突なし(main は 1.59.0 のまま)。 + **base を当て直した: `9b562d0f72947fe5f0f140ec5a835ca564ecafa5`** +- **2 パス目の指摘対応: 済み**(`92891c7`。直す 8 件。変異 3 本を足した。`check-all.py` 52/52 緑) +- **狭いパス: 済み**(起点 `9b8782a`・終点 `92891c7`。Verifier(`dev-loop:dev-loop-verifier`・sonnet)反証 1 件・主張の不一致 1 件・素通りした変異 2 件——下の「狭いパスの指摘」。直さず人間に渡す) +- **関門はここで打ち切り。** 次は PR(手順 6)。マージの前に起票を訊く(手順 7・8) +- 手順 5 の反証と差し戻し: 1 パス目に 2 件(V1・V2)→ 手順 4 に差し戻して直した +- 交絡を潰した手順: 実験なし +- 割り目の選択(手順 4・2026-10-27・AskUserQuestion): **ここで割る**——worktree の中で `/clear` して `/dev-loop:dev-loop 466`、手順 5 から +- 割り目の選択(手順 6 の 2 パス目の前・2026-10-27・AskUserQuestion): **ここで割る**——worktree の中で `/clear` して `/dev-loop:dev-loop 466`、 + **Verifier 2 パス目と `/code-review` 2 パス目から**(base は `e7c36c6859e5d756f6bf47d54b286ad2546fdd7d` のまま。 + 2 パス目の差分を指摘対応に絞るなら、指摘対応は `e1bcb9f..3fc9b37`——既存コードの改変を含むので周辺も対象に入れる) + +## 既知の限界・決着済みの論点 + +| 論点 | 決着 | 理由 | +| --- | --- | --- | +| ② 分類器・ガードを実際に試す | この周に入れない(分けて起票) | 利用者の決定(2026-10-27)。#464 の周と同じ時間帯に拒否を出すと記録が混ざる | +| 親の起動場所 | 隔離しない場所から起動するのを既定にする | 利用者の決定(2026-10-27) | +| 確認ダイアログでの停止 | この周では防げない(未証明に回す) | 会話が利用者の応答で止まるのはハーネスの挙動で、スキルから避けられない | + +### 1 パス目の指摘の採否(Verifier 2 件・`/code-review` 10 件。V1=R5・V2=R4 は同じ指摘) + +| # | 指摘 | 実物で失敗を示せるか | 破る受入基準 | 採否 | +| --- | --- | --- | --- | --- | +| V1/R5 | フェンスの開閉を ``` と `~~~` の種類・長さを見ずに反転させる。異種のフェンスの入れ子で、外の宣言を落とし中を読む | 示せる(Verifier が一時 project で `--cruise --present`、出力 `2 件: gh workflow run a.yml, gh mixedfence`。`check-unattended-env.py:656`) | B(フェンスの中) | 直す | +| V2/R4 | 全角コロン・字下げ・箇条書きの `cruise の操作` の行を黙って読み飛ばす(他に有効な行があれば「良い」) | 示せる(Verifier の同じ実行で 3 行とも出力に現れず警告なし) | B(書式の揺れ・崩れた宣言) | 直す | +| R1 | 起動場所のテストが `str(repo) in detail` で、案内(→ の行)を判別していない | 示せる(`/code-review` が案内を無関係な文・`.git` に変える変異で緑のまま。scratchpad の `mut/drive.py`) | I(起動場所の案内) | 直す | +| R3 | `show` が `await` を進行中に数えるのに、`references/ledger.md` の再開の手順 2・5 が `await` を知らず、cruise §6 と食い違う | 示せる(`references/ledger.md` の手順 2「進行中は実物で確かめて続ける」と cruise §6 の「await だけなら終える」) | D(操作待ちの扱い) | 直す | +| R2 | 起動時の (c)「その周を cruise の外に出す」は周の番号が要るが、操作から周への対応が無い | 示せない(文面からの推論。起きた記録は無い) | 無し | 残課題 | +| R6 | 宣言した完全なコマンドにも ` <引数>` を付けて照合するので、完全な形の allow の規則が当たらない | 示せる(`rule_matches` の式) | 無し(保守的な向き——「良い」と誤判定しない。C は守られる) | 残課題 | +| R7 | サブモジュール・bare の worktree で起動場所の案内が git ディレクトリを指す | 示せない(走らせていない) | 無し | 残課題 | +| R8 | `user-ops` に空白を含むコマンドを引用せずに渡すと単語ごとに記録される | 示せない(この周で起きた記録は無い) | 無し | 残課題 | +| R9 | `--present` を `--cruise` 無しでも受け付け、不在の点検の要対処を消せる | 示せない(誤って回した記録は無い) | 無し(受入基準は cruise の点検) | 残課題 | +| R10 | `show` が空の `user_ops` と未設定を区別しない | 示せない | 無し | 残課題 | + +### 2 パス目の指摘の採否(Verifier 1 件・`/code-review` 10 件。2026-10-27) + +| # | 指摘 | 実物で失敗を示せるか | 破る受入基準 | 採否 | +| --- | --- | --- | --- | --- | +| V3 | UTF-8 BOM 付きの `.claude/dev-loop.md` で先頭行の宣言を黙って落とし、「行が無い」と出す | 示せる(Verifier が `printf '\xef\xbb\xbf…'` のファイルで `--cruise --present`) | B(書式の揺れ・宣言が無いときに黙って既定だけ) | 直す | +| S1 | `references/found-lines.md` の「訊く場所は §4 の 5 だけ」が、§4 に 5 を挿したため操作待ちの項を指す | 示せる(`found-lines.md:86` と cruise §4 の番号) | D(§4 のまとめて訊く場所で操作待ちを渡す) | 直す | +| S2 | (c) で `dropped` にした周に依存する `todo` の周に、終わる条件も渡し方も無い | 示せる(`ledger.py` の `ready()` は依存が `merged` であることを要る・cruise §6 の終わる条件は `await`/`blocked` とその依存だけ) | D(§6 で残った周を「済んだ」と報告しない) | 直す | +| S3 | 再開のたびに止まりうる操作を `AskUserQuestion` で訊く——`--absent` の再開では利用者が居ない | 示せる(cruise §1 の再開で §3 を回し直す・§3 の質問は `--absent` でも出る) | D(cruise 全体が止まらない) | 直す | +| S4 | コロンの無い散文・字下げのコード例の `cruise の操作…` を崩れた宣言として要対処にする | 示せる(`/code-review` の probe) | 無し(過検出で安全側。B は守られる) | 残課題 | +| S5 | HTML コメントの中の宣言を読む(複数行)/黙って落とす(1 行) | 示せる(probe で `` → 「良い」) | B(経路に「コメント」と挙げてある) | 直す | +| S6 | 情報文字列にバッククォートを含む行をフェンスとして開き、後ろの宣言を全部落とす | 示せる(probe で `` ```x` `` の後の宣言が `commands=[]`) | B(フェンス) | 直す | +| S7 | §4-4 の `merge-pr.py` は `gh pr merge` を子プロセスで呼ぶので、`user_ops` の (b) を素通りし、点検も実際に Bash に渡すコマンドを見ていない | 示せる(cruise §4-4・§4-5 の文面と `merge-pr.py` の `gh pr merge`) | A・G((b) を選んだ操作を cruise が打つ) | 直す | +| S8 | §4-5 が拒否を即 `await` にし、`guard-rejections.md` の「割って打ち直す」を経ない | 示せる(文面) | 無し(周を余計に待たせるが、cruise 全体は止まらない) | 残課題 | +| S9 | `references/ledger.md` の手順 5 は `blocked` を進行中に数え、cruise §6 は `blocked`+`await` だけなら終えると言う | 示せる(`references/ledger.md:76` と cruise §6) | D | 直す | +| S10 | 公開ページ §0 の版の段落が 1.59.0 で止まり、`--cruise --present` が 1.60.0 からだと書いていない | 示せる(`docs/plugins/dev-loop-cruise.md:44`) | 無し | 残課題 | + +### 狭いパスの指摘(Verifier・`git diff 9b8782a 92891c7`。2026-10-27)——直さず、採否は人間が決める + +| # | 指摘 | 実物で失敗を示せるか | 破る受入基準 | 採否 | +| --- | --- | --- | --- | --- | +| N1 | S5 の `uncommented()` がインラインコードの中の `cruise の操作: gh x` で `['gh x']` → `[]` | 示せる | B(テストの欠け) | 人間が決める | +| N4 | 素通りした変異 M6(コメントの前の文を捨てる)——陽性対照: `cruise の操作: gh d ` で `['gh d']` → `[]` | 示せる | B(テストの欠け) | 人間が決める | + +- この周の起票: 0 件 diff --git a/docs/plugins/dev-loop-cruise.md b/docs/plugins/dev-loop-cruise.md index cc6c054c..3781d6bb 100644 --- a/docs/plugins/dev-loop-cruise.md +++ b/docs/plugins/dev-loop-cruise.md @@ -28,7 +28,7 @@ dev-loop は「1 Issue = 1 周」のスキルなので、オートクルーズ | 積んだ周(stacked) | `references/stacked.md` | 未マージの下の周の上で、上の周を先に始める | | マージ | `scripts/merge-pr.py` | CI 待ち → squash → worktree の撤去 → Issue が閉じたかの確認 | | 不在の線 | `references/absent-lines.md` | 利用者がいないとき、止まる代わりに進んでよい範囲 | -| 環境点検 | `scripts/check-unattended-env.py` | 席を外す前に、許可・自動継続・通知などを読むだけで点検する | +| 環境点検 | `scripts/check-unattended-env.py` | 起動時に、周で使う操作の許可・止まりうる操作・親の起動場所を読むだけで点検する。席を外すなら自動継続・通知なども | | 直す線 | `references/fix-line.md` | 関門の指摘をどこまで周の中で直すか | **スキルの base ディレクトリ**は、プラグインで入れていれば @@ -61,25 +61,39 @@ PR を通る場所なので、許可の範囲が利用者の目を通ります 不在の線: 実機確認 = Refs でマージ 不在の線: マージ = 関門を通ればマージ 直す線: 既定 +cruise の操作: gh workflow run infra-apply.yml ``` - **書かなければ、どの線も「止まる」です。** マージも宣言が無ければしません(PR を出して止まります)。 必要な線だけを書きます。 - **本番反映(デプロイ・migrate・本番の設定値)は広げられません。** main への push で自動デプロイする リポジトリでは、マージ自体が本番反映に数えられるので、`マージ` の線も効きません。 +- **`cruise の操作:` は、周で打つ操作のうち既定の点検(`git push`・`gh pr create`・`gh pr merge`・`gh pr comment`・`gh issue create`)に + 無いもの**を 1 行 1 コマンドで書きます(本番反映の口など)。起動時の点検がその許可を見ます。**利用者が居る cruise でも書いておきます。** - 値の一覧と、それぞれの線で何が起きるかは [`references/absent-lines.md`](https://github.com/hdknr/claude-code-setup/blob/main/plugins/dev-loop/skills/dev-loop/references/absent-lines.md) を、`直す線` は [`references/fix-line.md`](https://github.com/hdknr/claude-code-setup/blob/main/plugins/dev-loop/skills/dev-loop/references/fix-line.md) を正とします。 -## 2. 環境を点検する(席を外す前に 1 度) { #env-check } +## 2. 環境を点検する(起動時に 1 度) { #env-check } ```bash -python3 <スキルの base ディレクトリ>/scripts/check-unattended-env.py +python3 <スキルの base ディレクトリ>/scripts/check-unattended-env.py --cruise # 席を外すとき +python3 <スキルの base ディレクトリ>/scripts/check-unattended-env.py --cruise --present # 居るとき ``` -許可ルール(`git push`・`gh pr create`・`gh pr merge` など)、利用上限での自動継続、質問の待ち、通知、スリープを読み、 +**cruise は台帳を作ったあと、`--absent` に依らずこれを回して結果を見せます。** 利用者が居ても、権限の拒否や確認で +周が止まり、操作を待つことになるためです([#466](https://github.com/hdknr/claude-code-setup/issues/466))。 + +- **許可ルール**——既定の操作と `cruise の操作:` で宣言した操作が `allow` に当たるか。 +- **止まりうる操作**——自動モードの分類器と worktree 隔離のガードは settings に現れないので、許可があっても止まりえます。 + 起動時に操作ごとに選びます: 許可を足す/**その操作だけ自分で打つ**(台帳に残り、周はその操作の手前で「操作待ち」になって + 依存しない次の周へ進みます)/その周を cruise の外に出す。 +- **親の起動場所**——**cruise の親は、メインの作業ツリーから worktree に入らずに起動するのが既定です。** + worktree の中から起動すると、別の worktree・別のリポジトリへの操作がガードに拒まれます。 +- **席を外すとき**(`--present` なし)は、利用上限での自動継続・質問の待ち・通知・スリープも読みます。 + 足りないものと直し方を出します。**直すのは利用者です**(スクリプトは書き換えません)。 通知は、設定が正しくても届かない形があります。iPhone を**いまのセッション**につなぎ(`/remote-control`)、 @@ -227,6 +241,8 @@ Claude Code が終わって消えた待ちがあります。一方で、張っ ## 6. 戻ってきたら見るもの - **台帳**——`ledger.py show issue-<親>` で、どれがマージ済みで、どれが止まっているか(`note` に理由)。 +- **利用者の操作待ち**——台帳の「利用者の操作待ち」に、打ってほしいコマンドが周ごとに載っています。 + 打ったあとで同じ引数で起動し直すと、cruise が実物を確かめてからその周の続きを回します。 - **各 PR のコメント**——関門の残課題、名指しの未証明、訊かずに進めた箇所が書かれています。 `マージ` の線でマージした PR も、戻ってから PR コメントで確かめる前提です。 - **`要確認` ラベルの Issue**(ラベルが無いリポジトリでは題の先頭が `要確認:`)——`仕様の穴` の線で、 diff --git a/docs/plugins/dev-loop-design.md b/docs/plugins/dev-loop-design.md index 5e26993f..8d0dfbcd 100644 --- a/docs/plugins/dev-loop-design.md +++ b/docs/plugins/dev-loop-design.md @@ -1325,7 +1325,7 @@ Eotel 側で同じ範囲を覆う 5 本 | 守ること | 28 | 1 | | 複数の Issue を回すとき | 7 | 1 | -**このほかに、起点に載らないファイルが 12 本・1649 行**(`(必須)` 104 個)**ある。** **これは起点に載らない**——**要るときだけ読む**(`references/` は条件が立った周だけ、`prompts/` は委譲するとき)。 **規範としては原本の一部で、上の表とは足し算の関係にある**(合わせて 3487 行・`(必須)` 281 個)。 +**このほかに、起点に載らないファイルが 12 本・1658 行**(`(必須)` 104 個)**ある。** **これは起点に載らない**——**要るときだけ読む**(`references/` は条件が立った周だけ、`prompts/` は委譲するとき)。 **規範としては原本の一部で、上の表とは足し算の関係にある**(合わせて 3496 行・`(必須)` 281 個)。 | 起点に載らないファイル | 行 | `(必須)` | | --- | --- | --- | @@ -1333,7 +1333,7 @@ Eotel 側で同じ範囲を覆う 5 本 | references/fix-line.md | 67 | 8 | | references/found-lines.md | 104 | 4 | | references/guard-rejections.md | 115 | 16 | -| references/ledger.md | 71 | 6 | +| references/ledger.md | 80 | 6 | | references/resume.md | 344 | 34 | | references/spec-gap.md | 154 | 11 | | references/stacked.md | 152 | 9 | diff --git a/plugins/dev-loop/.claude-plugin/plugin.json b/plugins/dev-loop/.claude-plugin/plugin.json index 33ab951c..48ae92dd 100644 --- a/plugins/dev-loop/.claude-plugin/plugin.json +++ b/plugins/dev-loop/.claude-plugin/plugin.json @@ -1,8 +1,8 @@ { "name": "dev-loop", - "version": "1.59.0", + "version": "1.60.0", "author": { "name": "hdknr" }, - "description": "1 Issue = 1 周のループ志向開発。受入基準を「不変条件 × それを破りうる経路」に展開し(経路は差分ではなく入口一覧から数える)、証明できないものは未証明として申告する。Verifier とレビューは相互に代替できない別々の関門。検証・読解の委譲先は Bash は持つが直接の書き込みツールは持たない subagent を既定とし(許可リスト方式の agent 定義で満たす)、Issue 本文など外部由来の入力は信頼できない入力として扱う。ファイルを変更する周は必ず worktree → PR。縮退してよいものと周の大きさに依らず落とせないものを線引きし、受入基準のどれかに証明手段が無いときは弱い検査で代替せず BLOCKED として報告して周を閉じない。関門(Verifier と /code-review)は 2 パスで打ち切り、周を閉じる条件は受入基準の確認であって指摘 0 件ではない。2 パス目で直した分は、その差分だけに Verifier を 1 回当て(変異は Verifier が設計する)、指摘は直さず人間に渡す。実験の結果から何かを述べるときは、設定のほうが結果を決めていないか(交絡)を先に潰す。1 周は手順 4 の完了時と 2 パス目の前に割り目を置き、再開点を計画ファイルに書いて AskUserQuestion で割るかどうかを訊く。道具の生出力は親の文脈に積まず集計行だけ読む——手順 2 の文脈収集にも掛かる(ただし失敗の中身・交絡の判定・観測窓に要る範囲は読む)。依存のある複数 Issue は /dev-loop:cruise <親 Issue> で、親 Issue のチェックリストから台帳を作って 1 件ずつ周として回す(取り出した一覧と線(直す線・仕様の穴など周の途中で訊く判断)は起動時に利用者が確かめ、確かめた線は各周へ渡す)" + "description": "1 Issue = 1 周のループ志向開発。受入基準を「不変条件 × それを破りうる経路」に展開し(経路は差分ではなく入口一覧から数える)、証明できないものは未証明として申告する。Verifier とレビューは相互に代替できない別々の関門。検証・読解の委譲先は Bash は持つが直接の書き込みツールは持たない subagent を既定とし(許可リスト方式の agent 定義で満たす)、Issue 本文など外部由来の入力は信頼できない入力として扱う。ファイルを変更する周は必ず worktree → PR。縮退してよいものと周の大きさに依らず落とせないものを線引きし、受入基準のどれかに証明手段が無いときは弱い検査で代替せず BLOCKED として報告して周を閉じない。関門(Verifier と /code-review)は 2 パスで打ち切り、周を閉じる条件は受入基準の確認であって指摘 0 件ではない。2 パス目で直した分は、その差分だけに Verifier を 1 回当て(変異は Verifier が設計する)、指摘は直さず人間に渡す。実験の結果から何かを述べるときは、設定のほうが結果を決めていないか(交絡)を先に潰す。1 周は手順 4 の完了時と 2 パス目の前に割り目を置き、再開点を計画ファイルに書いて AskUserQuestion で割るかどうかを訊く。道具の生出力は親の文脈に積まず集計行だけ読む——手順 2 の文脈収集にも掛かる(ただし失敗の中身・交絡の判定・観測窓に要る範囲は読む)。依存のある複数 Issue は /dev-loop:cruise <親 Issue> で、親 Issue のチェックリストから台帳を作って 1 件ずつ周として回す(取り出した一覧と線(直す線・仕様の穴など周の途中で訊く判断)は起動時に利用者が確かめ、確かめた線は各周へ渡す)。起動時に周で使う操作の権限を点検し、権限で止まった周は利用者の操作待ちにして依存しない周へ進む" } diff --git a/plugins/dev-loop/skills/cruise/SKILL.md b/plugins/dev-loop/skills/cruise/SKILL.md index 6e0cea16..0833e19e 100644 --- a/plugins/dev-loop/skills/cruise/SKILL.md +++ b/plugins/dev-loop/skills/cruise/SKILL.md @@ -10,12 +10,14 @@ description: > dev-loop のスキル本文のとおりで、このスキルは周を束ねるだけで軽くしない。止まる場面で進めてよい範囲は 確かめた線と対象リポジトリの .claude/dev-loop.md の宣言だけで決まり、--absent は線を広げない。 推奨で決めた仕様の穴は確認の Issue として残し、終えるときに渡す。 + 起動時に周で使う操作の権限を点検し(親はメインの作業ツリーから worktree に入らずに起動する)、 + 権限で止まった周は利用者の操作待ちとして台帳に書き、依存しない次の周へ進む。 --- # cruise スキル - -> **このスキルの版: 1.59.0**(プラグイン `dev-loop`)。 + +> **このスキルの版: 1.60.0**(プラグイン `dev-loop`)。 > 手元で読まれている版がリポジトリの最新より古いなら、 > `/plugin marketplace update` → `/plugin update dev-loop@claude-code-setup` の順に実行し、 > Claude Code を再起動する([#63](https://github.com/hdknr/claude-code-setup/issues/63))。 @@ -27,6 +29,11 @@ description: > **呼び出し形**: `/dev-loop:cruise <親 Issue> [--absent]`。**素のスキルとしては置けない**—— 同じプラグインの dev-loop スキルを `../dev-loop/` で指しているため。 +**親は、対象リポジトリのメインの作業ツリーから、worktree に入らずに起動する(既定。#466)。** +worktree 隔離のセッションの親は、**別の worktree・別のリポジトリへの操作**(`git -C`・別の worktree を読む Verifier など)を +ガードに拒まれ、利用者の操作を待って止まる。**周ごとの作業はこれまでどおり各周の worktree で行う**(dev-loop の手順 4)。 +**worktree の中から起動されていたら、下の 3 の点検がそう言う**——利用者に見せ、起動し直すかを決めてもらう(止めはしない)。 + 使い方の案内(利用者向け)は公開ページを参照: @@ -56,6 +63,8 @@ cruise: 直す線 = 既定 `apply` は台帳が在れば作らずに 3 を返し、**親 Issue にあって台帳に無い Issue を名指しする**ので、それを利用者に渡す。 **足すかは利用者が決める**(足すなら `ledger.py add`。台帳に勝手に足さない)。**不在なら、6 で終えるときに渡す。** `show` が 3 か 4 を返したら、理由を利用者に渡して台帳のまま続ける——**台帳はもう確かめてある。** + **再開でも下の 3 の起動時の点検を回す**——設定は起動の間に変わりうる。**台帳の `user_ops` にある操作は訊き直さない。** + **`--absent` の再開では止まりうる操作を訊かない**(下の 3 の 2)。 **`apply` が「本文の線が台帳の確かめた線と違う」と名指ししたら、利用者に見せる**——台帳の線は変わっていない。 **確かめ直すと言われたら、`ledger.py lines` で書き直す**(置き換えなので、残す項目も全部渡す。出所は「利用者の確認(再開時)」と日時)。 **不在なら、台帳の線のまま続け、6 で終えるときに渡す。** @@ -93,13 +102,39 @@ python3 <このスキルの base ディレクトリ>/scripts/parent-issue.py sho 出所は「利用者の確認(起動時)」と日時。**各周は台帳から読み、周ごとには訊かない。** 答えが返らなければ書かない (`直す線` は各周が `../dev-loop/SKILL.md` 手順 3 のとおり訊き、答えが返らなければ `既定`。`周の途中の課題` は `外に出す`)。 -## 3. 不在で回すとき(`--absent`) +## 3. 起動時の点検と、不在で回すとき(`--absent`) + +**台帳を作ったら(再開なら読んだら)、`--absent` に依らず、周で使う操作の権限を点検する(#466)**—— +利用者が居る cruise でも、**権限の拒否・確認で周が止まり、利用者の操作を待つ**: + +スクリプトは `../dev-loop/scripts/check-unattended-env.py`(このスキルの base ディレクトリからの相対)で、引数は: + +```bash +check-unattended-env.py --cruise --present # 居る cruise +check-unattended-env.py --cruise # --absent +``` + +**読むだけで、設定を書き換えない。** 何を見るか(周で使う操作の宣言 `cruise の操作:`・止まりうる操作・親の起動場所)は +**そのスクリプトの docstring を正とする**。 + +1. **結果を利用者に見せる。** 要対処(`allow` に無い・`ask`・`deny`・worktree の中からの起動)と判定できない項目を名指しする。 + **宣言が無いと言われたら、その旨も見せる**——本番反映の口(workflow の dispatch・apply・run-task)は既定の操作に入っていない。 +2. **止まりうる操作**(自動モードの分類器と worktree 隔離のガードは settings に現れない)**を、`AskUserQuestion` で操作ごとに選んでもらう**: + (a) 許可を足す(利用者が `/permissions` か `update-config` で。**このスキルは settings を書かない**)/ + (b) **その操作だけ利用者が打つ**——`ledger.py user-ops issue-<親> <コマンド> … --source <出所>` で台帳に残す + (置き換えなので、残すものを全部渡す。出所は「利用者の確認(起動時)」と日時)/ + (c) その周を cruise の外に出す——`ledger.py set issue-<親> <番号> state=dropped note=<理由>`。 + **答えが返らなければ台帳に書かない**(周の途中で当たれば、下の 4 のとおり操作待ちにして次へ進む)。 + **`--absent` の新規の起動でも、ここは訊く**——起動した時点では利用者がまだ居る。 + **`--absent` の再開(5 のタイマーで起動し直したときなど)では訊かない**——利用者が居ない。点検の結果は 6 で終えるときに渡す。 + **スクリプト越しに打つ操作も同じ操作として扱う**——`../dev-loop/scripts/merge-pr.py` は中で `gh pr merge` を打つが、 + 権限に見えるのは `python3 <スクリプト>` のほうである。**4 で使うスクリプトの呼び出しも、止まりうる操作に並べて訊く。** - **`--absent` は「不在の周」の宣言である**——`../dev-loop/references/absent-lines.md` の「不在」に当たる。 **進めてよい範囲は、台帳の確かめた線と対象リポジトリの `.claude/dev-loop.md` の宣言だけで決まる。`--absent` は線を広げない。** 宣言の書き方と、宣言が無いときにどこで止まるかは、そのファイルの「線と既定」を正とする。 -- **台帳を作ったら、利用者が居るうちに環境を点検する**(`../dev-loop/scripts/check-unattended-env.py`。 - 読むだけ。結果を利用者に見せる)。**通知の試し方は `../dev-loop/SKILL.md` の「待ち時間・定時処理は別スキル」を正とする。** +- **不在に限る項目(自動継続・質問の待ち・通知・スリープ)も、上の点検(`--present` を付けない形)が利用者の居るうちに出す。** + **通知の試し方は `../dev-loop/SKILL.md` の「待ち時間・定時処理は別スキル」を正とする。** - **各周の計画ファイルに、不在の周であることと出所(`/dev-loop:cruise <親> --absent` の起動と日時)を書く。** - **確かめた後は訊かない**——周の途中で訊く場面の扱いは、そのファイルの「訊く場面」を正とする。 @@ -118,7 +153,17 @@ python3 <このスキルの base ディレクトリ>/scripts/parent-issue.py sho **`--absent` の起動で、利用者が積んでよいと言っていなければ積まない。** 4. **マージは dev-loop の `SKILL.md` 手順 7 のとおり**(`../dev-loop/scripts/merge-pr.py` に任せてよい)。 **不在の周は、`マージ` の線(台帳の確かめた線か宣言)が無ければマージせずに止まる**(`../dev-loop/references/absent-lines.md` の「`マージ = 関門を通ればマージ`」)。 -5. **周を 1 つ閉じたら、`ledger.py show issue-<親>` を読み直してから次へ。** +5. **止まりうる操作に当たったら、cruise 全体を止めずに、その周を「利用者の操作待ち」にして次へ進む(#466)。** + 当たるのは、**権限に拒否された**操作と、**台帳の `user_ops` にある**操作(打たない)。 + **`user_ops` の操作を中で打つスクリプトも呼ばない**——`gh pr merge` が `user_ops` にあれば、4 の `merge-pr.py` も呼ばずに操作待ちにする。 + `ledger.py set issue-<親> <番号> state=await note=<打ってほしいコマンドと、打った後に周がすること>` と書き、 + **利用者に打ってほしいコマンドを渡してから**、`ledger.py next` が出す依存しない周へ進む。 + **操作待ちの周に依存する周は始めない**(`next` が出さない)。**打ったと言われたら実物で確かめてから** + (`gh pr view`・`gh run list` など)、その周の続きを回す——**報告を確かめの代わりにしない**(`../dev-loop/references/ledger.md`)。 + - **権限の確認ダイアログは、ここに当たらない**——ダイアログが出ている間は、利用者が応えるまで会話そのものが止まる。 + **防げるのは上の 3 の点検で事前に許可を足してもらうことだけである。** + - **`本番反映` は広げない**——操作待ちは「利用者が打つ」を記録するだけで、cruise が代わりに打つ道ではない。 +6. **周を 1 つ閉じたら、`ledger.py show issue-<親>` を読み直してから次へ。** **台帳の線が `周の途中の課題 = 台帳に足して直す` なら、閉じた周が足した行も台帳に入っている**(出所の列)。 **利用者が居る cruise は、ここで「利用者の判断待ち」の行をまとめて 1 度だけ訊く**——訊く場所はここだけで、 答えの書き方は `../dev-loop/references/found-lines.md` の「利用者の判断をまとめて訊く」を正とする。**`--absent` なら訊かない。** @@ -132,6 +177,10 @@ python3 <このスキルの base ディレクトリ>/scripts/parent-issue.py sho ## 6. 終える `ledger.py show issue-<親>` が、すべてマージ済みか取り下げと出したら終える。 +**着手できる周が無く、残りが操作待ち(`await`)・止まっている(`blocked`)・それらか取り下げ(`dropped`)に依存する周だけになったら、そこでも終える** +(`../dev-loop/references/ledger.md` の「どう再開するか」の 5)。 +**取り下げた周に依存する周が残っていれば、名指しして渡す**——そのままでは着手できない(依存を外すか取り下げるかは利用者が決める)。 +**操作待ちの行(`show` の「利用者の操作待ち」)が残っていれば、打ってほしいコマンドを周ごとに渡す**——「済んだ」と報告しない。 **止まっている Issue が残っていれば、その `note` を利用者に渡して終える**——「済んだ」と報告しない。 **周の途中で足した行**(`show` の「周の途中で足した行のうち済んでいないもの」)**が残っていれば、出所ごとに渡す**—— 「なし」でなければ「済んだ」と報告しない(`../dev-loop/references/found-lines.md`)。 diff --git a/plugins/dev-loop/skills/dev-loop/SKILL.md b/plugins/dev-loop/skills/dev-loop/SKILL.md index 54d9448c..9aac5988 100644 --- a/plugins/dev-loop/skills/dev-loop/SKILL.md +++ b/plugins/dev-loop/skills/dev-loop/SKILL.md @@ -20,8 +20,8 @@ description: > # dev-loop スキル - -> **このスキルの版: 1.59.0**(プラグイン `dev-loop`)。 + +> **このスキルの版: 1.60.0**(プラグイン `dev-loop`)。 > 手元で読まれている版がリポジトリの最新より古いなら、 > `/plugin marketplace update` → `/plugin update dev-loop@claude-code-setup` の順に実行し、 > Claude Code を再起動する([#63](https://github.com/hdknr/claude-code-setup/issues/63))。 diff --git a/plugins/dev-loop/skills/dev-loop/references/found-lines.md b/plugins/dev-loop/skills/dev-loop/references/found-lines.md index 1c156b9d..d3e9aed9 100644 --- a/plugins/dev-loop/skills/dev-loop/references/found-lines.md +++ b/plugins/dev-loop/skills/dev-loop/references/found-lines.md @@ -83,7 +83,7 @@ tfs-stocks の cruise(台帳 5 件)では、狭いパスの指摘・テス ## 利用者の判断をまとめて訊く -**周ごとにばらばらに訊かない(必須)。** 訊く場所は **cruise の周と周の合間**(cruise の `SKILL.md` §4 の 5)だけである: +**周ごとにばらばらに訊かない(必須)。** 訊く場所は **cruise の周と周の合間**(cruise の `SKILL.md` §4 の 6)だけである: - **利用者が居る cruise**——台帳の `blocked` のうち `note` が「利用者の判断待ち」の行を、**1 度の `AskUserQuestion` にまとめて訊く**。 答えで直すことになれば `state=todo`(`note` に答えと日時)、直さないなら `state=dropped`(`note` に理由)にし、 diff --git a/plugins/dev-loop/skills/dev-loop/references/ledger.md b/plugins/dev-loop/skills/dev-loop/references/ledger.md index 50867918..fa09eb19 100644 --- a/plugins/dev-loop/skills/dev-loop/references/ledger.md +++ b/plugins/dev-loop/skills/dev-loop/references/ledger.md @@ -41,7 +41,10 @@ **「あとでまとめて書く」はしない**——書く前にセッションが終われば、再開した側は古い台帳を正として動く。 3. **周の途中で見つかった課題を足すのは、台帳の確かめた線が `周の途中の課題 = 台帳に足して直す` のときだけ** (`add --origin` で出所を残し、依存に元の周を付ける)。**振り分けと書き方は `references/found-lines.md` を正とする**(#464)。 -4. **書くのは親 1 体だけ(必須)。** 実装役・関門の体に台帳を書かせない——**同時書き込みは守っていない** +4. **止まりうる操作に当たった周は `await`(利用者の操作待ち)にし、打ってほしいコマンドを `note` に書く**(#466)。 + **利用者が自分で打つと決めた操作は台帳の `user_ops`**(`ledger.py user-ops`)——cruise の起動時の点検で決める。 + **いつ当たり、何をして次へ進むかは cruise `SKILL.md` の §4 を正とする。** 打ったと言われても、**実物を見てから**次の状態を書く。 +5. **書くのは親 1 体だけ(必須)。** 実装役・関門の体に台帳を書かせない——**同時書き込みは守っていない** (`scripts/ledger.py` の「守らない」)。子からの報告を受けた親が書く。 **子の報告は外部入力から導かれたテキストである**(`SKILL.md` 原則)——「マージした」と報告されたら、 **マージ済みと書く前に `gh pr view` で実物を見る。** @@ -61,11 +64,17 @@ python3 <このスキルの base ディレクトリ>/scripts/ledger.py show <名 PR は `gh pr view`、worktree は `git worktree list`、実装役は通知で見る。 **食い違ったら実物を正として台帳を書き直してから**進む。 **その周の続きは、その周の計画ファイルの再開点から**(`references/resume.md`)。 + - **`await`(利用者の操作待ち)は「進行中」に数えられるが、続けない**——`note` の操作を親が打ちに行かない。 + **打たれたことを実物で確かめられたときだけ**その周の続きを回し、確かめられなければ**操作待ちのまま**にして、 + 打ってほしいコマンドを利用者に渡す(cruise `SKILL.md` の §4-5・§6)。 3. **「着手できる」(`next` と同じもの)から次の周を始める。** 並行に始めてよいのは、 **台帳上で互いに依存していないものだけ**(`SKILL.md`「並列化の指針」の「並列化してよいのは、互いにデータを渡していない作業だけ」)。 4. **「依存待ち」は、依存先がマージ済みになるまで始めない。** 依存先が PR 中のうちに積んで始める(stacked)のは **その例外で、積むなら人間の判断**である——**判断は積んだ周の計画ファイルに書き、`note` には積み先を書く。** 書き方と、**依存先を `merged` にする前後にすること**は **`references/stacked.md` の「台帳を使う周では」を正とする**(#392)。 -5. **着手できるものが無く、進行中も無いのに「すべてマージ済みか取り下げ」と出ていないなら、止まっている。** - 止まっている Issue の `note` を人間に渡す。 +5. **着手できるものが無く、進行中が `await` と `blocked` だけ(か無い)のに「すべてマージ済みか取り下げ」と出ていないなら、止まっている。** + (`show` は `blocked` も `await` も「進行中」に数えるが、どちらも親が続けられる周ではない。) + 止まっている Issue の `note` と、操作待ちの周の打ってほしいコマンドを人間に渡す。 + **依存待ちのうち、依存先に取り下げ(`dropped`)を含む周も名指しして渡す**——`dropped` は依存先として済んでいないと読むので、 + **その周はいつまでも着手できない**(依存を外す `ledger.py set … deps=` か、その周も取り下げるかは人間が決める)。 diff --git a/plugins/dev-loop/skills/dev-loop/scripts/check-unattended-env.py b/plugins/dev-loop/skills/dev-loop/scripts/check-unattended-env.py index 41d0d487..04e7d74c 100644 --- a/plugins/dev-loop/skills/dev-loop/scripts/check-unattended-env.py +++ b/plugins/dev-loop/skills/dev-loop/scripts/check-unattended-env.py @@ -3,6 +3,7 @@ python3 <このスキルの base ディレクトリ>/scripts/check-unattended-env.py [--project ] [--command '<コマンド>' ...] [--flag-settings ] [--managed-settings ] + python3 <このスキルの base ディレクトリ>/scripts/check-unattended-env.py --cruise [--present] [...] python3 <このスキルの base ディレクトリ>/scripts/check-unattended-env.py --push-result '<返り値>' **直さない。読むだけ**——何が足りないかと、どう直すかを出す。**利用者の設定を書き換える道具は、 @@ -33,6 +34,26 @@ ——待ち受けは新しいセッションを起こすので、そこから送った 1 行は作業中のセッションに届かない - **蓋を閉じる・電源が変わる**——`pmset` の値は電源ごとで、蓋を閉じればスリープする +## cruise の起動時(`--cruise`・`--present`。#466) + +**cruise は `--absent` に依らず、台帳を作ったら 1 度これを回す**(cruise `SKILL.md` §3)—— +利用者が居る cruise でも、**権限の拒否・確認で周が止まり、利用者の操作を待つ**(#466 の実測)。 + +- `--cruise` は 3 つを足す: + - **周で使う操作の宣言**——`--project` の `.claude/dev-loop.md` の `cruise の操作: <コマンド>` の行(1 行 1 コマンド。 + コードフェンスの中は読まない。フェンスは開いた記号と同じ種類・同じ長さ以上の行でだけ閉じる)を、点検するコマンド(既定か `--command`)に足す。**本番反映の口** + (`gh workflow run …`・`aws ecs run-task …` など)は既定に無いので、ここで宣言させる。 + 宣言が無ければ「人が見る」で、既定だけを見たと出す。読めなければ「判定できない」、値の無い行は「要対処」。 + **宣言の形に届かなかった行**(字下げ・箇条書き・引用・全角コロン)も読まずに「要対処」 + - **止まりうる操作**(人が見る)——**自動モードの分類器と worktree 隔離のガードは settings に現れない**ので、 + 上で「良い」と出た操作も止まりうる。点検したコマンドを全部並べ、(a) 許可を足す・(b) その操作だけ利用者が打つ + (`ledger.py user-ops`)・(c) その周を cruise の外に出す、の選び方を添える。**選ばせるのは cruise の `SKILL.md`** + - **親の起動場所**——cwd が worktree の中(`--git-dir` ≠ `--git-common-dir`)なら要対処。**cruise の親は + メインの作業ツリーから、worktree に入らずに起動するのが既定**(#466)——隔離のガードが別の worktree・別のリポジトリへの + 操作を拒むので +- `--present` は**利用者が居る cruise**——不在に限る項目(自動継続・質問の待ち・通知・スリープ・人が見る 3 つ)を出さない。 + 許可の要対処の文言も「確認で周が止まる」にする(答える人は居るが、そこで止まる) + ## 通知の試験(`--push-result`) python3 check-unattended-env.py --push-result '' @@ -132,6 +153,11 @@ - **送信の要求を「届いた」と言わない**——`Mobile push requested` は人が見る(iPhone に届いたか) - **知らない返り値を「良い」に混ぜない**——判定できない - **何も書かない**——設定ファイルにも、どこにも +- **`--cruise` で、宣言した操作を点検から落とさない**——既定(か `--command`)に足す。フェンスの中の行は宣言にしない。 + **種類の違うフェンスの記号では閉じない**。**宣言の形に届かなかった行を黙って読み飛ばさない**(要対処) +- **`--cruise` で、止まりうる操作を「良い」と出した操作からも外さない**——settings の判定とは別の区分で全部出す +- **`--cruise` で、worktree の中からの起動を「良い」にしない**——判定できなければ「判定できない」 +- **`--present` で、許可の項目を落とさない**——落とすのは不在に限る項目だけ 守らない: @@ -160,6 +186,9 @@ - **macOS 以外のスリープ**——判定できないと出す - **2.1.310 以外の版でキー名が同じであること**——`PushNotification` の返り値の文字列も同じ - **iPhone に届いたこと**——`Mobile push requested` から先は見えない +- **分類器・ガードが実際にどの操作を止めるか**——試さない(副作用の無い試し方は別の Issue)。止まりうるとして全部並べるだけ +- **セッションが worktree 隔離にあること**——見ているのは cwd だけ。メインの作業ツリーにいても、後で `EnterWorktree` すれば隔離される +- **`.claude/dev-loop.md` の宣言の網羅**——宣言に無い操作は見えない - **iPhone からの操作が使用中になるか**——本体からは読み切れていない(利用者の実測。#407)。だから手順は両方から離れる形にしてある """ @@ -376,7 +405,7 @@ def rule_matches(rule, command): return command == body -def check_permissions(stack, commands): +def check_permissions(stack, commands, present=False): items = [] managed_only = managed_rules_only(stack) allow = collect_rules(stack, "allow", managed_only) @@ -386,6 +415,7 @@ def check_permissions(stack, commands): mode, mode_layer = perm_value(stack, "defaultMode") bypass_off, _ = perm_value(stack, "disableBypassPermissionsMode") bypass_ok = mode == "bypassPermissions" and bypass_off not in ("disable", True) + wait = "確認で周が止まる(利用者の操作を待つ)" if present else "確認されても答える人がいない" for command in commands: # 名指しのコマンドは引数つきで呼ばれる——引数の無い Bash(git push) は覆っていない call = command + " <引数>" @@ -398,7 +428,7 @@ def check_permissions(stack, commands): "deny の規則を外すか、この周の手順からそのコマンドを外す")) elif asked: # ask は allow より強い——確認を出して、答える人がいないまま止まる - items.append((ACTION, label, f"ask に当たる({', '.join(asked)})——確認されても答える人がいない", + items.append((ACTION, label, f"ask に当たる({', '.join(asked)})——" + wait, "ask の規則を外すか、この周の手順からそのコマンドを外す")) elif broken: # 読めない層に deny・ask があるかもしれない @@ -415,7 +445,7 @@ def check_permissions(stack, commands): if managed_only: shown += "。managed の allowManagedPermissionRulesOnly で、managed 以外の規則は効かない" items.append((ACTION, label, - f"allow に無い(defaultMode: {shown})——拒否・確認されても答える人がいない", + f"allow に無い(defaultMode: {shown})——拒否・" + wait, f'settings の permissions.allow に "Bash({command}:*)" を足す')) return items @@ -609,19 +639,126 @@ def exit_code(items): return 0 -def gather(project, commands, flag_path, managed_path, home, platform, flag_note=None, env=None): +DECLARE = re.compile(r"^cruise の操作:(.*)$") +# 宣言の形に届かなかった行(字下げ・箇条書き・引用・全角コロン)——黙って読み飛ばさずに要対処にする +NEAR = re.compile(r"^\s*(?:(?:[-*+>]|\d+[.)])\s*)*cruise\s*の\s*操作") +# フェンスは開いた記号と同じ種類・同じ長さ以上で、後ろに何も無い行でだけ閉じる(CommonMark)。 +# `` ` `` の印の後ろに `` ` `` があればインラインコードで、フェンスを開かない +FENCE = re.compile(r"^\s*(`{3,}|~{3,})(.*)$") + + +def uncommented(line, in_comment): + """HTML コメント(``)を除いた行の残りと、行末でコメントの中にいるかを返す。 + コメントの中の宣言は、無効にしたものとして読まない(複数行にまたがっても)。""" + out = "" + while True: + if in_comment: + close = line.find("-->") + if close < 0: + return out, True + line, in_comment = line[close + 3:], False + start = line.find("\n\n" + "cruise の操作: gh after-comment\n") + code, items, out = run_cruise(mod, env) + check("cruise: HTML コメントの中の宣言は読まず、外は読む", + "gh disabled" not in out and "gh inline" not in out + and status_of(items, "`gh after-comment`") == [mod.ACTION], bad) + # 情報文字列に ` を含む ``` の行はインラインコードで、フェンスを開かない(開くと後ろの宣言を全部落とす) + env.write(env.project / ".claude" / "dev-loop.md", "``` `x` ```\ncruise の操作: gh after-code\n") + code, items, out = run_cruise(mod, env) + check("cruise: ` を含む情報文字列の行はフェンスを開かない", + status_of(items, "`gh after-code`") == [mod.ACTION], bad) + # cruise でなければ足さない(不在の周の既存の点検を変えない) + code, items, out = run(mod, env) + check("cruise でなければ、止まりうる操作・起動場所・宣言を出さない", + not any(status_of(items, k) for k in ("止まりうる操作", "親の起動場所", "周で使う操作の宣言")), bad) + + # cruise の起動時(#466): 親の起動場所 + with scenario(script) as env: + env.all_good() + repo = env.project.parent / "repo" + repo.mkdir() + git(repo, "init", "-q") + git(repo, "commit", "-q", "--allow-empty", "-m", "x") + git(repo, "worktree", "add", "-q", str(repo / ".claude" / "worktrees" / "w")) + outside = env.project.parent / "outside" + outside.mkdir() + check("起動場所: メインの作業ツリーは良い", + status_of(run_cruise(mod, env, cwd=repo)[1], "親の起動場所") == [mod.GOOD], bad) + r = run_cruise(mod, env, cwd=repo / ".claude" / "worktrees" / "w") + check("起動場所: worktree の中は要対処で、案内がメインの作業ツリーを示す", + status_of(r[1], "親の起動場所") == [mod.ACTION] + and f"メインの作業ツリー({repo.resolve()})" in fix_of(r[1], "親の起動場所"), bad) + check("起動場所: git の外は判定できない", + status_of(run_cruise(mod, env, cwd=outside)[1], "親の起動場所") == [mod.UNKNOWN], bad) + return bad MUTATIONS = { + "宣言した操作を点検に足さない": ( + " commands = tuple(commands) + tuple(c for c in declared if c not in commands)\n", + " commands = tuple(commands)\n"), + "フェンスの中の行も宣言にする": ( + " if fence is not None:\n if f and", + " if False:\n if f and"), + "種類の違うフェンスの記号でも閉じる": ( + " if f and f.group(1)[0] == fence[0] and len(f.group(1)) >= len(fence) and not f.group(2).strip():\n", + " if f:\n"), + "BOM を行頭に残す": ( + ' text = path.read_text(encoding="utf-8-sig")', + ' text = path.read_text(encoding="utf-8")'), + "HTML コメントを除かない": ( + " line, in_comment = uncommented(line, in_comment)\n", + " pass\n"), + "` を含む情報文字列でもフェンスを開く": ( + ' if f and not (f.group(1)[0] == "`" and "`" in f.group(2)):\n', + " if f:\n"), + "崩れた宣言の行を見ない": ( + " if broken:\n", + " if False:\n"), + "起動場所の案内を .git にする": ( + "{common.parent if common.name == '.git' else common}", + "{common}"), + "値の無い宣言の行を見ない": ( + " if empty:\n", + " if False:\n"), + "止まりうる操作から宣言した操作を落とす": ( + " items += stoppable_items(commands)\n", + " items += stoppable_items(commands[:2])\n"), + "worktree の中からの起動を良いにする": ( + " if git_dir != common:\n", + " if False:\n"), + "起動場所を判定できないとき良いにする": ( + ' return [(UNKNOWN, label, "git で worktree の中かを判定できなかった", "")]', + ' return [(GOOD, label, "git で worktree の中かを判定できなかった", "")]'), + "--present で許可の項目を落とす": ( + " items += check_permissions(stack, commands, present)\n", + " items += [] if present else check_permissions(stack, commands, present)\n"), + "--present でも不在の項目を出す": ( + " if not present:\n items += check_auto_continue(stack)\n", + " if True:\n items += check_auto_continue(stack)\n"), "読めなかったものを空として扱う": ( " except ValueError as e:\n return Unreadable(p, str(e))", " except ValueError as e:\n return {}"), @@ -734,6 +890,9 @@ def suite(script: Path) -> list[str]: def main() -> int: + # 起動場所のテストが本物の git を走らせる——GIT_DIR などが他のリポジトリを指す環境で、そちらを読まない + for key in [k for k in os.environ if k.startswith("GIT_")]: + del os.environ[key] print("陽性対照(元の本体)") bad = suite(SCRIPT) for name in bad: diff --git a/scripts/test-ledger.py b/scripts/test-ledger.py index 283f52cd..3ec97ad1 100644 --- a/scripts/test-ledger.py +++ b/scripts/test-ledger.py @@ -278,6 +278,9 @@ def entry(n, deps): ("lshape", {"lines": {"values": {}, "source": "s"}}), ("ldec", {"issues": [{**entry(1, []), "decisions": "x"}]}), ("lorig", {"issues": [{**entry(1, []), "origin": 5}]}), + ("uops", {"user_ops": {"commands": "gh x", "source": "s", "updated": ""}}), + ("usrc", {"user_ops": {"commands": ["gh x"], "source": " ", "updated": ""}}), + ("udup", {"user_ops": {"commands": ["gh x", "gh x"], "source": "s", "updated": ""}}), ("lold", {})): data = {"format": 1, "name": name, "issues": [entry(1, [])], **extra} (plans / f"cruise-{name}.json").write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") @@ -311,6 +314,31 @@ def entry(n, deps): o["origin の show(済み)"] = "周の途中で足した行のうち済んでいないもの: なし" in r o["origin の無い show"] = "周の途中で足した行" not in str(run(mod, ["show", "ln"], root)[1]) + # L14(#466): 利用者の操作待ちと、利用者が打つ操作 + run(mod, ["init", "uo"], root) + for args in (["1"], ["2", "--deps", "1"], ["3"]): + run(mod, ["add", "uo", *args], root) + uf = plans / "cruise-uo.json" + o["await の set"] = run(mod, ["set", "uo", "1", "state=await", "note=利用者が打つ: gh workflow run x"], root)[0] + o["next(依存が操作待ち)"] = run(mod, ["next", "uo"], root)[1].split() + r = str(run(mod, ["show", "uo"], root)[1]) + o["await の show"] = ("利用者の操作待ち: #1(利用者が打つ: gh workflow run x)" in r, "#1(await)" in r, + "利用者が打つ操作" not in r) + o["user-ops 通る"] = run(mod, ["user-ops", "uo", "gh workflow run x", "aws ecs run-task", "--source", "s1"], root)[0] + o["user-ops の show"] = "利用者が打つ操作(出所: s1・" in str(run(mod, ["show", "uo"], root)[1]) + before = snapshot(uf) + o["user-ops 拒む"] = tuple(run(mod, ["user-ops", "uo", *cmds, "--source", src], root)[0] for cmds, src in ( + (["gh x"], " "), ([" "], "s"), (["gh x", "gh x"], "s"))) + o["user-ops 拒んだ後も同じ"] = snapshot(uf) == before + o["user-ops 置き換え"] = run(mod, ["user-ops", "uo", "--source", "s2"], root)[0] + try: + o["user-ops の値"] = {k: v for k, v in json.loads(uf.read_text(encoding="utf-8"))["user_ops"].items() + if k != "updated"} + except (OSError, ValueError, KeyError): + o["user-ops の値"] = None + run(mod, ["set", "uo", "1", "state=merged"], root) + o["next(操作が済んだ)"] = run(mod, ["next", "uo"], root)[1].split() + # L9: git の外。**無い台帳の `show` では判別しない**——台帳が無いことでも 4 になる。 # 場所の解決そのものを当てる `path` と `init` を使い、何も書いていないことも見る outside = tmp / f"outside-{tag}" @@ -426,6 +454,11 @@ def where2(cwd): "set で出所があって依存の無い行を作れる": [ (' if ("deps" in changes or "origin" in changes) and after.get("origin") and not after["deps"]:', " if False:")], + "user_ops の出所を見ない": [ + (' raise Refused("出所が空(--source に、誰がいつ決めたかを書く)")', + ' pass')], + "await に依存する行を next に出す": [ + ('all(state.get(d) == DONE for d in i["deps"])', 'all(state.get(d) in (DONE, "await") for d in i["deps"])')], "書いてから検査する": [ ("def save(path, data):\n validate(data)\n", "def save(path, data):\n"), (" os.replace(tmp, path)\n", " os.replace(tmp, path)\n validate(data)\n")], @@ -511,8 +544,8 @@ def main() -> int: check("線の無い台帳の show は「無い」と出す", o["線の無い show"]) check("decisions は番号のカンマ区切りだけ(番号でなければ 3)", o["decisions"] == (0, 3) and o["decisions の値"] == "12,13") - for name in ("lbad", "lval", "lsrc", "lshape", "ldec", "lorig"): - check(f"手で崩した線・decisions・origin({name})は 4(next は答えない)", o[f"線の形 {name}"] == (4, "")) + for name in ("lbad", "lval", "lsrc", "lshape", "ldec", "lorig", "uops", "usrc", "udup"): + check(f"手で崩した線・decisions・origin・user_ops({name})は 4(next は答えない)", o[f"線の形 {name}"] == (4, "")) check("線と decisions と origin を持たない台帳はこれまでどおり読む", o["線の形 lold"] == (0, "1\n")) absent = (REAL_REPO / "plugins" / "dev-loop" / "skills" / "dev-loop" / "references" / "absent-lines.md" ).read_text(encoding="utf-8").split("## 線と既定")[1].split("\n## ")[0] @@ -540,6 +573,18 @@ def main() -> int: check("足した行がすべて merged か dropped なら「なし」", o["origin の show(済み)"]) check("足した行の無い台帳の show は、その行を出さない", o["origin の無い show"]) + print("\n利用者の操作待ちと利用者が打つ操作(L14・#466)") + check("await を書ける", o["await の set"] == 0) + check("await に依存する #2 は next に出ず、依存しない #3 は出る", o["next(依存が操作待ち)"] == ["3"]) + check("show が操作待ちを note つきで出し、進行中にも数え、user_ops の無い台帳では出さない", + o["await の show"] == (True, True, True)) + check("user-ops は通り、show が出所つきで出す", o["user-ops 通る"] == 0 and o["user-ops の show"]) + check("user-ops の空の出所・空のコマンド・同じコマンド 2 度は 3", o["user-ops 拒む"] == (3, 3, 3)) + check("拒んだ user-ops のあとも台帳が 1 バイトも変わらない", o["user-ops 拒んだ後も同じ"]) + check("user-ops は置き換え(渡さなければ空)・出所を残す", + o["user-ops 置き換え"] == 0 and o["user-ops の値"] == {"commands": [], "source": "s2"}) + check("操作待ちが merged になれば依存する #2 が出る", o["next(操作が済んだ)"] == ["2", "3"]) + print("\n名前(L8)・git の外(L9)") check("../x・a/b・.x・x/../../../escaped は 2", (o["名前 ../x"], o["名前 a/b"], o["名前 .x"], o["名前 x/../../../escaped"]) == (2, 2, 2, 2))