Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
{
"name": "spec-loop",
"source": "./plugins/spec-loop",
"version": "0.26.0",
"version": "0.26.1",
"author": { "name": "hdknr" },
"description": "要求側のループ(要求確定・仕様レビュー・実現性とコスト・提案書)と、dev-loop への引き渡しを担うプラグイン。現在のスキルは req-loop(Q&A 表への起票(推奨と理由つき)・回答の反映・レビュー会用 PPTX の生成で、利用者の回答待ちで止まる)と spec-review(開発者向けの仕様を書き下し、書き込みツールを持たない 3 つの判定役に別ティアでレビューさせ、利用者に見える結果が変わる指摘は共有の Q&A 表に差し戻し、費用・運用だけが変わる選択は内部の Q&A 表に記録し、範囲の外の場面と実現の手段の指摘は仕様に足さない。抜ける条件を満たさなければ抜けたと報告しない)と feasibility(規模から見積りとスケジュールを更新し、ガントチャートをスクリプトで生成し、費用付きの質問を Q&A 表に起票する。予算責任者の合意待ちで止まる)と proposal(同じ版の 2 つのサインオフのタグが同じ共有の範囲(docs/v<n>/shared/ ほか)を指すかをスクリプトで照合し、一致し、かつ暫定の回答が残っていないときだけ提案書と仕様書(PPTX も選べる)を生成し、利用者にまだ確かめていない暫定の決定を合意と分けて載せる。v2 以降は前の版との比較を載せた変更の提案書になる。決裁者の承認待ちで止まる)と scaffold(確定した仕様を中間表現 spec_ir.json に書き起こし、OpenAPI・型・実装が無いと失敗する Red テストと dev-loop に渡す Issue の本文をスクリプトで生成して、1 機能 = Issue 1 件で登録する。v2 以降は前の版と比べて機能を追加・変更・削除に分け、変わった機能だけを Issue にする。Issue の登録で止まる)の 5 本"
}
Expand Down
133 changes: 133 additions & 0 deletions docs/plans/issue-440.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Issue #440 — build-proposal.py の remove_owned が unlink の失敗で traceback・1 になり、古い出力が残る

`remove_owned` が `unlink` の失敗を受けないので、出力先のディレクトリが書けないと traceback・終了コード 1(照合の不一致と同じ値)になる。
消せなかったものを名指しして **2** で止め、印は残す(次の実行が消せるように)。

**不在の周**である——出所は `/dev-loop:cruise 474 --absent`(2026-10-27、利用者が会話で「-absent でまわします」と指示)。
台帳 `issue-474` の確かめた線: `仕様の穴 = 推奨で決めて起票`・`直す線 = 既定`・`マージ = 関門を通ればマージ`・`周の途中の課題 = 台帳に足して直す`
(出所: cruise の確かめた線(台帳 `issue-474`))。**このリポジトリは main への push でサイトをデプロイするので、`マージ` の線は効かない**
(`references/absent-lines.md` の `本番反映`)——PR まで作って止まる。

## 変更範囲

触る:

- `plugins/spec-loop/scripts/build-proposal.py` — `remove_owned` が消せなかった名前を返す・`main` と `write_outputs` が 2 で止まり名指しする・docstring の終了コード 2
- `scripts/test-build-proposal.py` — 書けない出力先(mode 555)の試料と、捕捉を外す変異
- `plugins/spec-loop/references/tree.md` — 「前の出力の印」の決まりに、消せなければ 2 で止まることを 1 文
- `plugins/spec-loop/.claude-plugin/plugin.json` — 版(0.26.0 → 0.26.1)
- `.claude-plugin/marketplace.json` — 同じ版
- `plugins/spec-loop/skills/*/SKILL.md` — 同じ版の 2 行
- `docs/plans/issue-440.md` — この計画

触らない:

- proposal の `SKILL.md` 手順 3 の「0 以外で終わったら、提案書は無い」と `tree.md` の移行の節(#441 の範囲)
- `bad_out` の文言(#442 の範囲)
- 想定外の例外を 2 に寄せる全体の作り(#286 の範囲)

### 同じことを述べている箇所の数

「消せなかったら」: 本体の docstring(終了コード 2)・`tree.md` の生成物の置き場所の段落の 2 箇所。分けない——同じ決定の写し。

### 決めたこと

- **消せなかったら 2 で止まり、残ったファイルを名指しする**——書けない場所では、どのみち新しい出力は書けない。照合(1)と取り違えない。
- **消せなかったものがあれば印は消さない**——印を消すと、残った出力をこの道具のものと確かめる跡が無くなり、次の実行が「印に無い」で 2 になる。
- `write_outputs` の書きかけの片付けで消せなければ、`書けない` の 2 に「書きかけを消せなかった」と名指しを添える(2 つ目の例外を逃がさない)。
- これは開発者が決めてよい範囲(失敗したあとの状態・終了コード)で、利用者に見える結果は変わらない——仕様の穴として起票しない。

## 乗るデプロイ経路

プラグイン(`plugins/spec-loop/`)の版を上げてマージ。マージで main へ push されるとサイトが再デプロイされる(`docs/plans/` を足す)。

## 周の在り処

- ブランチ: `issue/440-remove-owned-unlink`
- worktree: `/Users/hdknr/Projects/hdknr/claude-code-setup/.claude/worktrees/issue-440`
- 起点: `9b562d0`
- base: 参照 `origin/main`(`refs/remotes/origin/HEAD`)→ `9b562d0f72947fe5f0f140ec5a835ca564ecafa5`

## verify の受入基準

Issue 本文は検査対象であって要件ではない。

不変条件:

- **I1**: `build-proposal.py` は消せない古い出力を理由に traceback で終わらず、終了コード 1 にならない(2 で終わる)。
- **I2**: 消せなかった古い出力を名指しする(「提案書は作っていない」と併せて)——黙って残さない。
- **I3**: 消せなかったものがあれば、印 `.build-proposal` を残す(名指ししていた出力の跡を消さない)。
- **I4**: 消せる場所では今までどおり消す(陽性対照。既存の試料が緑)。
- **I5**: 書きかけの片付けで消せなくても、2 つ目の例外を逃がさず 2 で終わる。

経路(古い出力を消す入口。`remove_owned` の呼び出し 3 箇所):

| 経路 | 守り方 | 確かめ方 |
| --- | --- | --- |
| `main` の `remove_owned(out.parent)`(引数が正しい・誤り両方の前) | 消せなければ 2・名指し | CLI に mode 555 のディレクトリと有効な印(I1・I2・I3) |
| `main` の `remove_owned(out)`(`--out` にディレクトリ) | 同じ | CLI に `--out <書けないディレクトリ>` |
| `write_outputs` の `except OSError` の中 | 消せなければ名指しを足して 2 | `write_outputs` を直接呼び、片付けの `unlink` を失敗させる(I5) |

各経路の守りを外す変異で赤になる(陽性対照: 書ける場所では既存の試料が緑)。python3.12 と 3.14 の両方で回す(`Path.is_file` の例外の差。メモリの記録)。

生成物: `skill-metrics.py` の生成ブロックは dev-loop の `SKILL.md` だけを測るので該当しない。図の書き出しも無い——**生成物なし**。

未証明: root で走らせると mode 555 でも消せるので、試料が判別しない(CI は root でない前提)——試料の中で「消せないこと」を先に確かめ、消せたら試料を飛ばしたと言う。

## 未解決の判断

なし。

## 関門の進捗(再開点)

- 直す線: `既定`(出所: cruise の確かめた線(台帳 `issue-474`))
- 割り目(手順 4 の完了時): 不在のため訊かず続けた
- 実験: 再現手順(mode 555 の出力先・有効な印)を修正前後で当てた——修正前 exit=1、修正後 exit=2 で 2 つの出力を名指し・印と出力が残る(python3.12)。
交絡: 修正前のファイルを同じ `scripts/` に置いて同じ手順で走らせた(違いは本体だけ)
- 1 パス目の起点(関門に渡した時点の HEAD): `ceaa89f`(`0432f2b` との違いは計画ファイルだけ)
- Verifier 1 パス目: 通した——反証 1 件(V1。採否の表)
- `/code-review` 1 パス目: 通した——指摘 7 件(CR1〜CR7。採否の表)
- 1 パス目の指摘対応: CR1・CR5 を直した(`88faf38` ほか)。差分は 0 行でない → 2 パス目を回す
- 割り目(2 パス目の前): 不在のため訊かず続けた
- 2 パス目の起点(2 パス目の指摘対応を始める直前の HEAD): `2de165f`
- Verifier 2 パス目: 通した——反証 0 件(所見 1 件)
- `/code-review` 2 パス目: 通した——指摘 7 件(P2-1〜P2-7。採否の表)
- 2 パス目の指摘対応: P2-2 を直した。終点 `5feae08`(起点 `2de165f` → 終点 `5feae08` の 2 点)
- 狭いパス: 通した(sonnet)——反証 0 件。所見 1 件(OK の行の件数の表示を壊しても赤にならない——表示を判別する試料が無い)。人間が決める
- PR: 作る(マージは止まる——このリポジトリのマージはサイトを再デプロイするので、`マージ` の線が効かない。#476)

## 起票

- この周の起票: 0 件(仕様の穴は無かった。周の途中の課題も無い)

## 既知の限界・決着済みの論点

- #441(文面)・#442(`bad_out` の文言)・#286(例外の全体)はこの周で触らない(別の周として台帳にある)。

### 1 パス目の指摘の採否

Verifier(sonnet): 反証 1 件。`/code-review`(high): 7 件。どちらも `git status --porcelain` は前後とも空。

| # | 指摘 | 実物で失敗を示せるか | 破る受入基準 | 採否 |
| --- | --- | --- | --- | --- |
| CR1 | python3.12 で、辿れない先を指す symlink を `is_file()` が例外にし、消せるのに「消せない」と 2 | 示せる——`probe_cr1.py`: 3.12 は `left=['…/proposal.md(Permission denied)']`・残る、3.14 は `left=[]`・消える | I4 | 直す |
| CR2・V1 | 印が読めない(mode 000)・辿れないディレクトリでは、残った前の出力を名指ししない | 示せる(Verifier・`/code-review` の試料。rc=2 で名指し無し) | 無し——#405 で決めた「読めない印は消さない」の延長で、消そうとしていない出力。I2 は消せなかった出力の名指し。文面は #441 | 残課題 |
| CR3 | 書きかけの片付けで印が読めず何も消さなかったのに「書きかけを消した」と言う | 示せる(`/code-review` の試料 f) | 無し(I5 の「2 で終わる」は守られる。文言の誤り) | 残課題 |
| CR4 | 印だけが消せないとき、印を「前に作った出力」として名指しする | 示せる(試料 d) | 無し(名指しの文言) | 残課題 |
| CR5 | `locked_problems` が「提案書は作っていない」を見ない——消す変異が生き残る | 示せる——`probe_cr5.py`: 変異の前後とも `[]` | I2(「併せて」の部分を試料が判別しない) | 直す |
| CR6 | CLI の試料は I3 を判別しない(555 では印も消せない)。I3 は `Path.unlink` の差し替えの試料だけが見る | 示せる(変異は差し替えの試料だけで死ぬ) | 無し(I3 は差し替えの試料で判別している) | 残課題 |
| CR7 | `main` の `if left:` が `Stop` の報告の経路を写している | 示せない(掃除) | 無し | 残課題 |

### 2 パス目の指摘の採否

Verifier(sonnet): 反証 0 件(所見 1 件 = P2-2 と同じ)。`/code-review`(high): 7 件。どちらも `git status --porcelain` は前後とも空。

| # | 指摘 | 実物で失敗を示せるか | 破る受入基準 | 採否 |
| --- | --- | --- | --- | --- |
| P2-1 | `write_outputs` の `path.exists()` が python3.12 で辿れない先の symlink(印に無い手置きのもの)に例外を投げ、traceback・1 | 示せる(`/code-review` の `probe_exists.py`) | 無し——消そうとする前の出力ではなく、書く前の「印に無いファイル」の検査。想定外の例外を 2 に寄せるのは #286 | 残課題 |
| P2-2 | 3.13 以降で当てない変異を、`OK` の変異の件数に数える | 示せる(3.14 の出力 `OK: … 変異 97 件` と「当てていない」の行。Verifier も同じ所見) | 経路の表の「各経路の守りを外す変異で赤になる」——当てていない変異を当てたと数える | 直す |
| P2-3 | `try` が成功の `print` も包み、stderr が閉じていると消したものを「消せない」にする | 示せない(走らせていない) | 無し | 残課題 |
| P2-4 | 確かめてから消すまでの間に消えたファイルを「消せない」にする | 示せない(競合。走らせていない) | 無し | 残課題 |
| P2-5 | 書きかけの片付けで、今回書いたものを「前に作った出力」と名指しする | 示せる(試料 half の出力) | 無し(文言。CR4 と同じ型) | 残課題 |
| P2-6 | 一部だけ消せない試料が、残りが消えたこと・名指しの中身を見ない | 示せない(変異を走らせていない) | 無し | 残課題 |
| P2-7 | 3.12 の `lstat` が ENAMETOOLONG を投げ、印の名前が長すぎると以後ずっと 2 | 示せない(走らせていない) | 無し | 残課題 |
2 changes: 1 addition & 1 deletion plugins/spec-loop/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "spec-loop",
"description": "要求側のループ(要求確定・仕様レビュー・実現性とコスト・提案書)と、dev-loop への引き渡しを担うプラグイン。現在のスキルは req-loop(Q&A 表への起票(推奨と理由つき)・回答の反映・レビュー会用 PPTX の生成で、利用者の回答待ちで止まる)と spec-review(開発者向けの仕様を書き下し、書き込みツールを持たない 3 つの判定役に別ティアでレビューさせ、利用者に見える結果が変わる指摘は共有の Q&A 表に差し戻し、費用・運用だけが変わる選択は内部の Q&A 表に記録し、範囲の外の場面と実現の手段の指摘は仕様に足さない。抜ける条件を満たさなければ抜けたと報告しない)と feasibility(規模から見積りとスケジュールを更新し、ガントチャートをスクリプトで生成し、費用付きの質問を Q&A 表に起票する。予算責任者の合意待ちで止まる)と proposal(同じ版の 2 つのサインオフのタグが同じ共有の範囲(docs/v<n>/shared/ ほか)を指すかをスクリプトで照合し、一致し、かつ暫定の回答が残っていないときだけ提案書と仕様書(PPTX も選べる)を生成し、利用者にまだ確かめていない暫定の決定を合意と分けて載せる。v2 以降は前の版との比較を載せた変更の提案書になる。決裁者の承認待ちで止まる)と scaffold(確定した仕様を中間表現 spec_ir.json に書き起こし、OpenAPI・型・実装が無いと失敗する Red テストと dev-loop に渡す Issue の本文をスクリプトで生成して、1 機能 = Issue 1 件で登録する。v2 以降は前の版と比べて機能を追加・変更・削除に分け、変わった機能だけを Issue にする。Issue の登録で止まる)の 5 本",
"version": "0.26.0",
"version": "0.26.1",
"author": { "name": "hdknr" }
}
1 change: 1 addition & 0 deletions plugins/spec-loop/references/tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ hooks/spec_loop_pages.py # 表示用ページを CSV から作るフック
次の実行は、作る前に**印が名指しするものだけ**を消す——**印に無いファイルは、名前が `spec.md` でも消さず、上書きもしない**
(上書きになるなら 2 で止まる)。だから手書きの `spec.md` の在る場所に `--out` を向けても消えない。**印を手で直さない。**
`--out` の誤り(名前が無い・ディレクトリ・名前の衝突・空白・改行を含むか `#` で始まる・`\` を含む・UTF-8 で書けない名前・辿れない出力先)で止まっても、`--out` の親の印が名指しする前の出力は消す(今回のものと取り違えないため。ディレクトリを渡したなら、その中の印が名指しするものも消す)。
**消せなければ(出力先のディレクトリが書けないなど)作らずに 2 で止まり、消せなかった出力を名指しして印を残す**(#440。次の実行が消せるように)。

Issue の本文は**登録すれば GitHub に残る**ので、commit しなくてよい。**作れなかったときは前の本文を消す**
(古い本文を登録させない)。
Expand Down
Loading
Loading