diff --git a/.claude/settings.json b/.claude/settings.json index 8fba7162..2a81d02b 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -11,5 +11,95 @@ }, "env": { "CLAUDE_CODE_USE_POWERSHELL_TOOL": "1" + }, + "hooks": { + "SessionStart": [ + { + "matcher": "startup|resume", + "hooks": [ + { + "type": "command", + "command": "node scripts/cloud-hook-dispatch.mjs --setup", + "timeout": 600 + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "node scripts/cloud-hook-dispatch.mjs hooks-user-prompt-feedback-recovery", + "timeout": 10 + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash|Write|Edit|Replace", + "hooks": [ + { + "type": "command", + "command": "node scripts/cloud-hook-dispatch.mjs hooks-pre-tool-validate", + "timeout": 10 + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Write|Edit|Replace", + "hooks": [ + { + "type": "command", + "command": "node scripts/cloud-hook-dispatch.mjs hooks-post-tool-linter", + "timeout": 35 + } + ] + }, + { + "matcher": "Write|Edit|Replace", + "hooks": [ + { + "type": "command", + "command": "node scripts/cloud-hook-dispatch.mjs hooks-post-tool-comment-lint-rust", + "timeout": 15 + } + ] + }, + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "node scripts/cloud-hook-dispatch.mjs hooks-post-tool-jj-op-verify", + "timeout": 15 + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "node scripts/cloud-hook-dispatch.mjs hooks-stop-tool-call-leak", + "timeout": 10 + }, + { + "type": "command", + "command": "node scripts/cloud-hook-dispatch.mjs hooks-stop-quality", + "timeout": 300 + }, + { + "type": "command", + "command": "node scripts/cloud-hook-dispatch.mjs hooks-stop-feedback-dispatch", + "timeout": 15 + } + ] + } + ] } } diff --git a/CLAUDE.md b/CLAUDE.md index e881f237..31debe8c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,6 +60,7 @@ - [ADR-057: docs-only / 空 diff の決定論 routing — instruction 規約から決定論機構への昇格](docs/adr/adr-057-docs-only-deterministic-routing.md) *(試験運用)* - [ADR-058: fix 後の決定論再ゲート (post-takt re-gate) — pre-push 経路への機械的 backstop 拡張](docs/adr/adr-058-post-takt-regate.md) *(試験運用)* - [ADR-059: hook 通知の可視化チャネル分離 (systemMessage = ユーザー向け / additionalContext = モデル向け)](docs/adr/adr-059-hook-system-message-visibility.md) *(試験運用)* +- [ADR-060: Cloud ハーネス有効化 — tracked dispatcher 登録 + SessionStart 実体確保の 2 層分離](docs/adr/adr-060-cloud-harness-sessionstart-dispatcher.md) *(試験運用)* ## 開発 convention / チェックリスト diff --git a/docs/adr/adr-060-cloud-harness-sessionstart-dispatcher.md b/docs/adr/adr-060-cloud-harness-sessionstart-dispatcher.md new file mode 100644 index 00000000..5c0d8a64 --- /dev/null +++ b/docs/adr/adr-060-cloud-harness-sessionstart-dispatcher.md @@ -0,0 +1,165 @@ +# ADR-060: Cloud ハーネス有効化 — tracked dispatcher 登録 + SessionStart 実体確保の 2 層分離 + +## ステータス + +試験運用 (2026-07-25) + +> 本 ADR は [ADR-039 (試験運用標準パターン)](adr-039-experimental-feature-standard-pattern.md) に従う。 +> Config opt-in / kill-switch / bounded lifetime の 3 点を満たす (§ 決定 4)。 + +## コンテキスト + +### Claude Code Web で hooks が「半整備」になる構造 + +WP-15 (`scripts/cloud-setup.sh`) は Linux プリビルドバイナリの配置 + `settings.local.json` +生成でクラウドセッションのハーネス有効化を狙ったが、実運用セッション (2026-07-25) で +**hooks が 1 つも発火しない**ことが確認された。調査で以下の 3 つのプラットフォーム制約が +確定した (いずれも公式ドキュメント記載 + セッション内実測): + +1. **セットアップスクリプトは環境キャッシュ構築時に 1 回だけ走る**。完了後にファイル + システムが snapshot され、以降のセッションはスクリプトを skip して snapshot から始まる。 +2. **セッションは毎回リポジトリを fresh clone する**。git 追跡外の生成物 + (`.claude/hooks-*` バイナリ / `settings.local.json` / `.jj` / `node_modules` / `target/`) + は clone に含まれず、**snapshot にあっても毎セッション消える**。cloud-setup.sh ヘッダの + 注意 (B) が警告した「環境半整備」はこの構造の帰結であり、UI に「session フェーズへ登録」 + する設定は存在しない。 +3. **hooks は Claude Code 起動時に snapshot され、セッション中の settings 変更は反映されない** + (セキュリティ仕様)。SessionStart hook で `settings.local.json` を生成しても、その + セッションの登録には**原理的に間に合わず**、次セッションでは (2) により消える。 + → 生成物ベースの hook 登録はクラウドでは永遠に 1 歩遅れる。 + +補助的な実測事実: + +- `CLAUDE_CODE_REMOTE=true` がクラウドセッションで設定される (ガード変数として利用可)。 +- `CLAUDE_PROJECT_DIR` は Bash 環境で unset ([ADR-005](adr-005-hooks-path-resolution-with-template.md) + が記録した不安定性はクラウドでも継続)。 +- GitHub release asset の取得は、**セットアップスクリプトフェーズでは attach 済みリポジトリ + に限定** (非 attach は 403、公式ドキュメント記載)。**セッション内からは非 attach リポジトリ + (jj-vcs/jj) も HTTP 200 で取得できる**ことを実測確認。 + +### 制約の含意 + +hook の**登録**は「clone に必ず含まれる = git 追跡ファイル」からしか成立しない。一方で +hook の**実体** (バイナリ) は毎セッション消えるので、毎セッション確保し直すしかない。 +つまり登録と実体確保は**別のライフサイクル**を持ち、単一機構 (従来のテンプレート → 生成) +では両立できない。 + +## 決定 + +**hook 登録を git 追跡の `.claude/settings.json` に置き、実体確保を SessionStart hook に +分離する。** 両者をつなぐのが cross-platform dispatcher (`scripts/cloud-hook-dispatch.mjs`)。 + +### 1. 登録層: `.claude/settings.json` + dispatcher + +`settings.json` (tracked) に SessionStart / UserPromptSubmit / PreToolUse / PostToolUse / +Stop を登録する。command は hook バイナリを直接指さず、すべて + +```text +node scripts/cloud-hook-dispatch.mjs +``` + +形式にする。dispatcher は: + +- `import.meta.url` から自己位置 → リポジトリルートを解決する (`CLAUDE_PROJECT_DIR` 非依存。 + ADR-005 追記「exe パスのみが安定した起点」の .mjs 版。[scripts/run-artifact.mjs](../../scripts/run-artifact.mjs) + と同じ手法だが、失敗セマンティクスが異なるため別スクリプト — + [ADR-044](adr-044-subprocess-utility-extraction-boundary.md) の分離判定) +- **クラウド以外 (`CLAUDE_CODE_REMOTE != true`) と opt-in 前 (§ 4) は無条件 exit 0**。 + Windows ローカルは従来どおり `settings.local.json` (テンプレート生成) が実働するため、 + settings.json 側の登録は node 起動 1 回分のオーバーヘッドを除き完全に不活性 +- `.claude/` バイナリを stdio 素通しで spawn し、exit code をそのまま伝播する + (hook プロトコル: exit 2 = block 等はバイナリ側の判断が透過する) + +`node scripts/...` の相対パス起動は「hook 実行時の cwd = プロジェクトルート」を前提とする +(クラウドでは常に成立。ローカルでルート外から起動する運用が生じたら再評価)。 + +### 2. 実体確保層: SessionStart hook → `cloud-setup.sh --session-phase` + +SessionStart (matcher `startup|resume`) で dispatcher の `--setup` モードを起動し、 +`bash scripts/cloud-setup.sh --session-phase` を実行する: + +- nightly release からバイナリ取得・配置 + fail-closed 検証 (従来と同じ) +- jj 導入 (**セッション内取得なら 403 制約を受けない**、上記実測) + colocated 初期化 + + identity + bookmark track (PR #318 で先行実装済みの休眠部品がここで本来の役割に就く) +- `pnpm install --frozen-lockfile` (Stop gate の lint/test/build step が node_modules 前提) +- `generate_settings` は **skip** する (クラウドの登録は settings.json が担う。 + settings.local.json を併産すると将来の二重登録リスクだけが残る) +- 完了後、buffer した stdin JSON で `hooks-session-start` バイナリを起動し、従来の + SessionStart 機能 (staleness 通知等) も同一エントリ内で**順序保証付き**で実行する + (別エントリに分けると並列実行され「バイナリ配置前に発火」する race がある) + +### 3. キャッシュ構築フェーズの役割縮小: `--cache-phase` + +Web UI のセットアップスクリプト欄は `bash scripts/cloud-setup.sh --cache-phase` に縮小する。 +snapshot に**載って意味があるもの**だけを暖める: + +- pnpm 確保 + `pnpm install` (pnpm store が snapshot に載り、セッション毎の install が高速化) +- `cargo clippy` warmup — **環境変数 `CARGO_TARGET_DIR=/opt/cargo-target` (Web UI で設定) + との組で初めて有効**。従来はリポ内 `target/` に書いて fresh clone で消えていた + (PR #318 warmup_cargo が休眠していた原因)。リポ外に出せば snapshot に載り、 + 全セッションの Stop gate `lint:rust` / `cargo test` が warm cache で始まる + +引数なしの `cloud-setup.sh` は従来 main() のまま残す (後方互換 + ローカル Linux 検証用)。 + +### 4. ADR-039 3 点セット + +**§ 1.b 判定**: 本 feature は PreToolUse block / Stop block という blocking 挙動を含むため +§ 1.b (non-blocking mechanical lint) に**該当しない** → § 1 適用、default OFF。 + +| 観点 | 内容 | +|---|---| +| **Config opt-in** | env `CLOUD_HARNESS=1` (または `true`) を Web UI の環境変数欄に設定した環境でのみ dispatcher が実働。未設定なら全 hook が exit 0 no-op (= merge しただけでは挙動が変わらない、制御されたロールアウト) | +| **Kill-switch** | 環境変数欄から `CLOUD_HARNESS` を削除 (次セッションから全停止)。コード変更・revert 不要。診断: dispatcher は有効時のみ動くため、無効時は無音 (ローカル常時発火のため無効時ログは出さない設計判断) | +| **Bounded lifetime** | decision trigger: **クラウドセッション 5 回の dogfood で「SessionStart 完走 + Pre/Post/Stop 発火 (ADR-055 テレメトリで確認) + Stop gate 完走」を確認したら、default-ON 化 (opt-in env 不要化) or 却下を判定**する。**2026-09-30 までに判定に至らなければ却下とみなす**。trigger の永続記録は本 ADR + dispatcher module doc の 2 箇所 | + +### 5. ADR-043 (fail-closed) からの意図的逸脱 1 点 + +dispatcher は「バイナリ不在」を **exit 0 + 毎イベント stderr 警告** で通す (fail-open)。 +ADR-043 の原則からの逸脱であり、理由を明記する: + +- 不在シナリオは「SessionStart の setup が失敗した後」に限られ、setup 失敗自体は + fail-closed (エラーが SessionStart 出力で明示される) — **無言ではない** +- PreToolUse を fail-closed にすると Bash が全 block され、復旧コマンド + (`bash scripts/cloud-setup.sh --session-phase`) 自体が実行不能になる**デッドロック**が生じる +- ADR-005 冒頭の事故 (hooks の無言無効化) の教訓は「無言」の禁止であり、毎イベント警告は + これを満たす + +昇格判定時に、この逸脱を維持するか (例: PreToolUse のみ fail-closed + 復旧コマンドの +allowlist 化) を再評価する。 + +## 帰結 + +### 利点 + +- クラウドで Pre/Post/Stop が**起動時から確実に登録**される (生成タイミング問題の根絶) +- ローカル Windows 経路 (テンプレート → settings.local.json) は無変更・無影響 +- opt-in env のみで有効化/停止でき、ロールバックに revert 不要 +- PR #318 の休眠部品 (jj init / identity / bookmark) が設計どおり機能し始める + +### 欠点 / 留意点 + +- SessionStart に毎セッション数十秒 (バイナリ ~10MB + jj + pnpm install) のコストが乗る。 + `--cache-phase` の store 暖機で pnpm 分は軽減 +- hook 登録が settings.json (クラウド) とテンプレート (ローカル) の 2 系統になる。 + hook 追加時は両方の更新が必要 (将来 dispatcher にローカルも統合してテンプレート機構を + retire する案は昇格判定後の検討事項 = ADR-005 v2 候補) +- E2E 検証は「merge + env 設定後の新規セッション」でしか原理的にできない。本セッションでは + リハーサル (バイナリ実取得 + 合成 stdin での単体駆動) までを検証済みとする + +### ユーザー側の環境設定 (コード外、Web UI) + +1. 環境変数欄: `CLOUD_HARNESS=1` と `CARGO_TARGET_DIR=/opt/cargo-target` を追加 +2. セットアップスクリプト欄: `bash scripts/cloud-setup.sh --cache-phase` へ変更 + (欄の変更がキャッシュ再構築のトリガーを兼ねる) + +## 関連 + +- [ADR-005](adr-005-hooks-path-resolution-with-template.md) — パス解決の不安定性と + exe-relative 原則 (dispatcher の自己位置解決はその .mjs 適用) +- [ADR-010](adr-010-hooks-layout-and-build-strategy-v2.md) — hook exe の `.claude/` 配置規則 +- [ADR-039](adr-039-experimental-feature-standard-pattern.md) — 試験運用 3 点セット +- [ADR-043](adr-043-security-gates-fail-closed.md) — fail-closed 原則 (§ 決定 5 で逸脱 1 点を明記) +- [ADR-044](adr-044-subprocess-utility-extraction-boundary.md) — run-artifact.mjs と別スクリプトにした分離判定 +- [ADR-055](adr-055-firing-telemetry-collection.md) — dogfood 時の発火確認に使うテレメトリ +- `scripts/cloud-setup.sh` — 実体確保の本体 (`--session-phase` / `--cache-phase`) +- `scripts/cloud-hook-dispatch.mjs` — dispatcher 本体 diff --git a/scripts/cloud-hook-dispatch.mjs b/scripts/cloud-hook-dispatch.mjs new file mode 100644 index 00000000..ac7b2a06 --- /dev/null +++ b/scripts/cloud-hook-dispatch.mjs @@ -0,0 +1,119 @@ +/** + * Claude Code Web (クラウドセッション) 用の hook dispatcher (ADR-060)。 + * + * .claude/settings.json (tracked) の hooks が全イベントで本スクリプトを起動し、 + * 本スクリプトが .claude/ 配下の hook バイナリへ委譲する。登録 (tracked settings) と + * 実体 (毎セッション消えるバイナリ) のライフサイクル分離が目的。設計根拠・実測事実は + * docs/adr/adr-060-cloud-harness-sessionstart-dispatcher.md 参照。 + * + * 使い方 (settings.json の hooks command から): + * node scripts/cloud-hook-dispatch.mjs --setup … SessionStart 専用 + * node scripts/cloud-hook-dispatch.mjs … その他の hook イベント + * + * ゲート (ADR-039 3 点セット、opt-in): + * - CLAUDE_CODE_REMOTE=true (クラウドセッションで自動設定) でなければ即 exit 0。 + * Windows ローカルでは settings.local.json (テンプレート生成) が実働するため、 + * 本スクリプトは常に不活性 (無音。ローカルの全イベントで発火するためログは出さない)。 + * - CLOUD_HARNESS が "1" または "true" (case-insensitive) でなければ即 exit 0。 + * 有効化は Web UI の環境変数欄で行う。kill-switch = この env を削除。 + * - bounded lifetime decision trigger: クラウドセッション 5 回の dogfood で + * 「SessionStart 完走 + Pre/Post/Stop 発火 + Stop gate 完走」を確認したら + * default-ON 昇格 or 却下を判定。2026-09-30 までに判定なしなら却下 (ADR-060 § 決定 4)。 + * + * バイナリ不在時は exit 0 + stderr 警告 (fail-open)。ADR-043 からの意図的逸脱で、 + * 理由 (復旧コマンド自体が block されるデッドロック回避) は ADR-060 § 決定 5 参照。 + * + * run-artifact.mjs と同じ自己位置解決 (import.meta.url) を使うが、失敗セマンティクスが + * 異なる (あちらは不在 = exit 1 の CLI ランチャー) ため別スクリプト (ADR-044)。 + */ + +import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const SCRIPTS_DIR = fileURLToPath(new URL(".", import.meta.url)); +const ROOT = resolve(SCRIPTS_DIR, ".."); +const CLAUDE_DIR = join(ROOT, ".claude"); + +// ─── ゲート (ADR-060 § 決定 4) ─── + +const enableValue = (process.env.CLOUD_HARNESS ?? "").toLowerCase(); +const enabled = enableValue === "1" || enableValue === "true"; +if (process.env.CLAUDE_CODE_REMOTE !== "true" || !enabled) { + process.exit(0); +} + +const [mode, ...forwarded] = process.argv.slice(2); +if (!mode) { + console.error("usage: node scripts/cloud-hook-dispatch.mjs <--setup | hook-exe-name> [args...]"); + process.exit(2); +} + +/** hook バイナリを stdin 指定つきで起動し exit code を正規化して返す。 */ +function runHookBinary(name, args, stdinData) { + const exePath = join(CLAUDE_DIR, name); + if (!existsSync(exePath)) { + // fail-open (exit 0)。無言ではなく毎イベント警告する (ADR-060 § 決定 5)。 + console.error( + `[cloud-hook-dispatch] WARNING: ${exePath} が未配置のため hook をスキップしました。` + + "SessionStart の setup が失敗しています。復旧: bash scripts/cloud-setup.sh --session-phase", + ); + return 0; + } + const result = spawnSync(exePath, args, { + stdio: [stdinData === undefined ? "inherit" : "pipe", "inherit", "inherit"], + input: stdinData, + }); + if (result.error) { + console.error(`[cloud-hook-dispatch] ${name} の起動に失敗: ${result.error.message}`); + return 1; + } + // シグナル終了は status = null になるため非ゼロへ正規化 (run-artifact.mjs と同じ規約)。 + return result.status === null ? 1 : result.status; +} + +// ─── --setup: SessionStart 専用モード ─── +// +// (1) cloud-setup.sh --session-phase でバイナリ配置・jj・pnpm install を実行し、 +// (2) 成功後に hooks-session-start バイナリへ SessionStart の stdin JSON を渡す。 +// 2 つを同一エントリで直列実行するのは、settings 側で別エントリに分けると並列発火し +// 「配置前にバイナリを呼ぶ」race があるため (ADR-060 § 決定 2)。 +if (mode === "--setup") { + // hooks-session-start 用に stdin を先に全量 buffer する (子 2 つで共有できないため)。 + let stdinJson = ""; + try { + const chunks = []; + for await (const chunk of process.stdin) chunks.push(chunk); + stdinJson = Buffer.concat(chunks).toString("utf8"); + } catch { + stdinJson = ""; + } + + const setup = spawnSync("bash", [join(ROOT, "scripts", "cloud-setup.sh"), "--session-phase"], { + stdio: ["ignore", "inherit", "inherit"], + }); + const setupStatus = setup.error ? 1 : (setup.status ?? 1); + if (setupStatus !== 0) { + // setup 失敗はここで明示的に落とす (fail-closed)。以降の hook イベントは + // バイナリ不在の warning (fail-open) として観測される。 + console.error( + `[cloud-hook-dispatch] cloud-setup.sh --session-phase が exit ${setupStatus} で失敗しました。` + + "ハーネスは無効のままです。復旧: bash scripts/cloud-setup.sh --session-phase", + ); + process.exit(2); + } + + process.exit(runHookBinary("hooks-session-start", [], stdinJson)); +} + +// ─── 通常モード: [args...] ─── + +// settings.json (tracked) 以外から名前が来ることは想定しないが、path traversal だけは +// 機械的に拒否する (テンプレートの exe 名文字種と同じ許容集合)。 +if (!/^[A-Za-z0-9._-]+$/.test(mode)) { + console.error(`[cloud-hook-dispatch] 不正な hook 名: ${mode}`); + process.exit(2); +} + +process.exit(runHookBinary(mode, forwarded, undefined)); diff --git a/scripts/cloud-setup.sh b/scripts/cloud-setup.sh index 3d929aba..1ab5d68c 100755 --- a/scripts/cloud-setup.sh +++ b/scripts/cloud-setup.sh @@ -6,12 +6,20 @@ # hooks / CLI を即時有効化する。release-binaries.yml が公開した rolling release # (タグ `nightly`) から Linux バイナリを取得し、.claude/ へ配置して settings を生成する。 # -# 使い方: Claude Code Web の環境設定で、セッションの作業ツリーに対して走る -# session-start フェーズに登録する (冪等なので毎セッション実行して安全)。 -# bash scripts/cloud-setup.sh -# 注意 (B): env-build フェーズだけに登録すると、生成物 (.claude/ バイナリ / -# settings.local.json / .jj / target/ 等はすべて git 追跡外) が session 起動時の -# fresh clone で消え、環境が半整備になる。必ず session の作業ツリーに対して走らせること。 +# 使い方 (ADR-060: 2 つのフェーズに分離。旧・注意 (B) の「session フェーズへ登録」は +# Web UI に対応する設定が存在しないことが判明したため、SessionStart hook 経由に確定): +# +# --session-phase … .claude/settings.json の SessionStart hook から毎セッション実行される +# (scripts/cloud-hook-dispatch.mjs --setup が起動)。バイナリ配置 / jj / +# pnpm install。生成物 (.claude/ バイナリ / .jj / node_modules) は git +# 追跡外で fresh clone のたびに消えるため、毎セッション再構築する。 +# 冪等なので resume 時の再実行も安全。 +# --cache-phase … Web UI のセットアップスクリプト欄に登録する (環境キャッシュ構築時に +# 1 回だけ走る)。snapshot に載って意味があるものだけを暖める: +# pnpm store / cargo clippy warmup (要 CARGO_TARGET_DIR=リポ外、 +# 例 /opt/cargo-target を Web UI の環境変数欄で設定)。 +# 引数なし … 旧来の全ステップ実行 (後方互換 + ローカル Linux 検証用)。 +# クラウドの実運用では上記 2 フェーズを使う。 # # 設計メモ: # - **認証を要求しない**: public リポジトリの Release asset は素の HTTPS で取得できる。 @@ -92,8 +100,11 @@ install_harness_binaries() { local base_url="https://github.com/${REPO_SLUG}/releases/download/${RELEASE_TAG}" local tmp_dir tmp_dir="$(mktemp -d)" - # 途中で失敗しても一時ディレクトリを残さない。 - trap 'rm -rf "${tmp_dir}"' RETURN + # 途中で失敗しても一時ディレクトリを残さない。RETURN trap は関数返却後もシェルに残留し、 + # 後続関数の return で消滅済み local tmp_dir を参照して set -u で落ちるため + # (--session-phase 再実行が exit 1 になる実測バグ)、発火時に自己解除する。 + # EXIT にも掛けるのは die() (exit 1) 経路では RETURN trap が発火しないため (PR #319 CR)。 + trap 'rm -rf "${tmp_dir}"; trap - RETURN EXIT' RETURN EXIT log "バイナリを取得中: ${base_url}/${archive}" # --fail: HTTP 404/5xx を silent な空ファイルではなく exit 非 0 にする @@ -193,7 +204,8 @@ install_jj() { local url="https://github.com/jj-vcs/jj/releases/download/v${JJ_VERSION}/${archive}" local tmp_dir tmp_dir="$(mktemp -d)" - trap 'rm -rf "${tmp_dir}"' RETURN + # 自己解除 + EXIT 併用の理由は install_harness_binaries の同 trap のコメント参照。 + trap 'rm -rf "${tmp_dir}"; trap - RETURN EXIT' RETURN EXIT log "jj ${JJ_VERSION} を取得中" if ! curl --fail --location --silent --show-error --output "${tmp_dir}/${archive}" "${url}"; then @@ -327,6 +339,27 @@ init_jj_repo() { fi fi + # A-4 (ADR-060 リハーサル実測): colocate init は git HEAD を detach し、環境によっては + # ローカルブランチ ref も残らない。jj 中心のローカル flow は影響しないが、クラウド + # セッションの Claude は git で commit/push するため、そのままだと push が + # "src refspec does not match any" で失敗する。init 前に取得した現ブランチへ + # HEAD を戻す (直後の working tree は不変なので checkout は安全)。ref が消えて + # いる場合は現 HEAD 位置で作り直す。 + if ! git -C "${REPO_ROOT}" symbolic-ref -q HEAD >/dev/null; then + if [ -n "${current_branch}" ]; then + if ( cd "${REPO_ROOT}" && { git checkout -q "${current_branch}" 2>/dev/null \ + || git checkout -q -B "${current_branch}" 2>/dev/null; } ); then + log "git HEAD を ${current_branch} へ再アタッチ" + else + warn "git HEAD の再アタッチに失敗 (detached のまま。git push 前に checkout -B が必要)" + fi + else + # 本 run の開始時点で既に detached だった場合は戻し先が分からない。無言 skip に + # すると push 失敗まで露見しないため、ここで明示する。 + warn "git HEAD が detached でブランチ名も不明のため再アタッチできません (git checkout -B で復旧)" + fi + fi + # A-3. push ワークフロー (ADR-011/015) 用に既定ブランチ + 現ブランチ (init 前に取得済み) の # remote bookmark を track。remote bookmark 未 import / 不在なら best-effort で無視。 ( cd "${REPO_ROOT}" && jj bookmark track "${DEFAULT_BRANCH}@origin" >/dev/null 2>&1 ) \ @@ -366,7 +399,38 @@ warmup_cargo() { fi } -main() { +# ─── フェーズ実行 (ADR-060) ─── + +# SessionStart hook (cloud-hook-dispatch.mjs --setup) から毎セッション実行される。 +# generate_settings は呼ばない: クラウドの hook 登録は tracked な .claude/settings.json + +# dispatcher が担う (ADR-060 § 決定 2)。settings.local.json を併産すると、起動時 snapshot +# 仕様により当該セッションには効かないまま二重登録リスクだけが残る。 +# warmup_cargo も呼ばない: SessionStart の latency 予算に収まらないため cache-phase + +# CARGO_TARGET_DIR (リポ外) の組に委ねる。 +run_session_phase() { + install_harness_binaries + verify_required_binaries + ensure_pnpm + install_jj # セッション内は非 attach リポの release も取得可 (ADR-060 実測) + init_jj_repo # A-1/A-2/A-3: colocated 初期化 + identity + bookmark track + install_node_dependencies + report_optional_features + log "セットアップ完了 (--session-phase)" +} + +# Web UI のセットアップスクリプト欄から環境キャッシュ構築時に 1 回だけ実行される。 +# fresh clone で消えるリポ内生成物 (バイナリ / settings / .jj) をここで作っても無意味なので +# 作らない。snapshot に載って次セッションを速くするものだけを暖める。 +run_cache_phase() { + ensure_pnpm + install_node_dependencies # pnpm store が snapshot に載り session-phase の install が高速化 + warmup_cargo # 要 CARGO_TARGET_DIR=リポ外。リポ内 target/ は clone で消える + report_optional_features + log "セットアップ完了 (--cache-phase)" +} + +# 旧来の全ステップ (後方互換 + ローカル Linux 検証用)。 +run_legacy_full() { install_harness_binaries verify_required_binaries generate_settings @@ -379,4 +443,13 @@ main() { log "セットアップ完了" } +main() { + case "${1:-}" in + --session-phase) run_session_phase ;; + --cache-phase) run_cache_phase ;; + "") run_legacy_full ;; + *) die "不明な引数: $1 (--session-phase / --cache-phase / 引数なし)" ;; + esac +} + main "$@" diff --git a/src/hooks-stop-quality/src/main.rs b/src/hooks-stop-quality/src/main.rs index c025670d..bdd4f862 100644 --- a/src/hooks-stop-quality/src/main.rs +++ b/src/hooks-stop-quality/src/main.rs @@ -350,12 +350,13 @@ fn expand_step_placeholders(cmd: &str) -> String { fn run_quality_steps(steps: &[QualityStepConfig], timeout: u64) -> Vec { let handles: Vec<(String, std::thread::JoinHandle<(bool, String)>)> = steps .iter() - .cloned() .map(|step| { let step_name = step.name.clone(); + let thread_name = step.name.clone(); + let raw_cmd = step.cmd.clone(); let handle = std::thread::spawn(move || { - let cmd = expand_step_placeholders(&step.cmd); - run_cmd_shell_capped(&step.name, &cmd, timeout, MAX_LINES) + let cmd = expand_step_placeholders(&raw_cmd); + run_cmd_shell_capped(&thread_name, &cmd, timeout, MAX_LINES) }); (step_name, handle) })