Skip to content

release: v0.46.1 - #3720

Merged
Kewton merged 1097 commits into
mainfrom
develop
Oct 15, 2026
Merged

Kewton merged 1097 commits into
mainfrom
develop

Conversation

@Kewton

@Kewton Kewton commented Oct 15, 2026

Copy link
Copy Markdown
Owner

リリース概要

Auto-Yes と応答 poller の「待ちの間に止まった・作り直された tick」が古い世代のまま書き込む問題をまとめて塞いだ修正リリース。あわせて Esc の層の閉じ方と、チャット検索の文字参照の強調を直した。

バージョン

0.46.0 → 0.46.1(patch)

DB マイグレーション

なし(CURRENT_SCHEMA_VERSION の変更なし、src/lib/db/migrations/v*.ts の差分なし)

実差分

git diff --stat origin/main..origin/develop: 82 files changed, 11614 insertions(+), 307 deletions(-)

※ squash 履歴のため、main..develop のコミット数は実態より多く表示される。

対応 Issue

#3663 #3670 #3671 #3672 #3674 #3681 #3682 #3683 #3684 #3691 #3692 #3700 #3701 #3705 #3706 #3707 #3713

主な変更

Fixed

品質チェック

develop の primary checkout で実行(build は CI に任せる):

  • npm run lint: 0
  • npx tsc --noEmit: 0
  • npm run test:unit: 0(2211 files / 38034 passed / 6 skipped)
  • release-notes-files.test.ts: 0

スラッシュコマンドカタログ

catalog:refresh --check で codex 0.163.0 の /config の追加を検出した(attestation は 0.161.0)。上流の enum(codex-rs/tui/src/slash_command.rs、rust-v0.161.0 → rust-v0.163.0)の差分で、Config の追加は確認済み。ただし attestation の記録先にする Issue が無いので、このリリースでは適用せず次回に持ち越す。

UAT

未実施

Vibe Metrics

Vibe Metrics (last 3 days)
Tasks:        3 total / 3 succeeded / 0 failed / 0 not-started  (success 100.0%)
Verification: 5 runs, pass 80.0%  (top fails: test-all-targets x1)
Intervention: 66 human responds / 514 auto answered
Retry loops:  avg 0.0 per failed task

🤖 Generated with Claude Code

Kewton and others added 30 commits October 5, 2026 04:35
…3216) (#3258)

auto-yes-suppression-reason.ts(import なし)へコメントごと移し、auto-yes-resolver.ts から
同じ名前で再 export。api-responses.ts は相対パスで import して再 export。
2 つの定義は 6 値(mode-off, deny-pattern, deny-pattern-unusable, type-not-allowed,
agent-launch-dialog, unclassified-frame)で完全一致を確認した。振る舞いは変えていない。

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
B-3: useSurfaceMode(src/hooks/useSurfaceMode.ts)。2 つの写しの違いは
storage key だけで、引数で受けた(split は useMemo の値、スマホは直接計算した値)。
B-4: buildPaneSessionLabels(pane-session-labels.ts)。モデルの出どころ
(split は agentModel、スマホは useCachedAgentModelLabel)だけが違い、引数で受けた。
B-5: splitOwnsKeyEvent(event, splitIndex) を TerminalSplitPaneContent.tsx 内に置き、
2 つのキーハンドラから呼ぶ。末尾の PanePromptState 再 export は src/ tests/ に
import が無いことを確認して削除した。

Issue の行番号は前の PR でずれていたため、名前で特定した。
本文に無い指摘: なし

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…pt 呼び出しをファイルごとの関数にする (#3219) (#3260)

K-3: codex/detect.ts の codexContentEnd の手書きは findCodexContentEnd と同じ処理。
後者は排他的な終端(end+1)を返すので、使う側で -1 して包含的な値に戻した。
K-4: codex 4 か所、copilot 2 か所の detectPrompt(stripBoxDrawing(...), buildDetectPromptOptions(tool)) を
各 detect.ts 内の非公開関数に置き換えた。copilot は一時変数 promptOptions を経由していたが、
他で使われておらず同一の呼び出しなので替えた。

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…3214) (#3261)

src/lib/auto-yes-poller.ts の detectAndRespondToPrompt を、同じファイルの中で
次の関数に分けた。どれも export しない。

- suppressAndWarnOnce: 4 か所にあった「recordPolicySuppression で抑止を記録し、
  warnOncePerFrame で 1 回だけ警告する」形。呼ぶ順番、抑止の理由の文字列、警告の
  ログ名、warnOncePerFrame に渡すキーの式、ログの欄の並びは元のまま。
- suppressIfNotOursToAnswer: 手順 3 / 3.2 / 3.5(codex の起動ダイアログ、/model
  ピッカー、ダイアログゲート)の判定と抑止。判定の順番は元のまま。コメントは
  書き換えずに一緒に移した。
- finishAnsweredPrompt: 送信後の後処理(手順 6・7 から端末スナップショットの配信
  まで)。コメントごと書き換えずに移した。元と同じ try の中で await するので、
  ここで投げた例外は今までどおり 'error' になる。
- JudgedPrompt: 上の 3 つに渡す値をまとめた型。

振る舞いは変えていない。tests/ は変えていない。

複雑度と行数:
- detectAndRespondToPrompt: 32 → 20、357 行 → 184 行。32 は Issue の値で、手で
  数えても 32。20 は ESLint の complexity 規則で測った値
  (npm run lint -- --rule 'complexity: ["warn", 5]')。
- suppressIfNotOursToAnswer は 9、finishAnsweredPrompt は 6。このファイルの最大は 20。

確認:
- npx tsc --noEmit と npm run lint は exit 0。
- このファイルに関係するテスト(auto-yes-poller / auto-yes-manager を参照する
  93 ファイル、1515 件)は変更前と同じ数だけ通った。
- 並行負荷が高いとき(load average 80 台)、tests/integration/
  auto-yes-persistence.test.ts と server-shutdown-remote-ingress-2489.test.ts が
  5000ms の timeout で落ちることがあった。変更前のコードでも同じ落ち方をし、
  負荷が下がると変更後のコードで続けて 3 回通った。

判断:
- Issue は「抑止して、1 回だけ警告して、戻る」が 4 回と書いているが、4 か所目
  (契約ポリシー)は警告の後で戻らず、続く answer === null の判定で戻る。この形は
  変えず、記録と警告の 2 つの呼び出しだけを suppressAndWarnOnce に置き換えた。
- 4 か所を suppressAndWarnOnce に寄せるだけでは、?? を含む引数の式が呼び出し側に
  残り、複雑度は手で数えて 27 までしか下がらない(受入基準は 25 未満)。そのため
  手順 3 / 3.2 / 3.5 は if ごと suppressIfNotOursToAnswer に移した。
- 受入基準の目安「約 40 行減」には届かない。ファイル全体は 1119 行 → 1192 行
  (+73 行)。重複の解消で減るのは約 30 行で、分けた関数の宣言・JSDoc と
  JudgedPrompt の分がそれを上回る。コメントは決まりどおり削っていない。
- foreign session の出口(warnOncePerFrame だけを呼び、recordPolicySuppression を
  呼ばない)は Issue の 4 か所に入っておらず形も違うので、触っていない。

本文に無い指摘: src/lib/auto-yes-poller.ts:264 warnOncePerFrame を説明する JSDoc が
promptFrameKey の JSDoc の上に置かれていて、warnOncePerFrame 本体(:296)には JSDoc
が付いていない。

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
gemini / antigravity / command-code / codex / copilot / opencode / opencode-v2 / vibe-local の
sendMessage 冒頭の「セッション確認→無ければ例外→(終了していれば)起動し直し」を
BaseCLITool.requireSession(label, worktreeId, instanceId, {relaunch}) に置き換えた。
copilot の sendModelCommand と opencode の sendMessageWithImage は relaunch: false。
エラー文は `${label} session ${sessionName} does not exist. Start the session first.` で従来と同一。
await の順番・例外の型は変えていない。差異のあったツールは無い。
base.ts は新しい import を足していない(hasSession は既存)。
copilot の sendModelCommand のコメント「Check if session exists」は呼び出しに置き換わり削除。

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…3263)

4 つの selectUnwritten*Turns を比べた結果、違いは依頼の id を取る関数と型の名前だけだった。
transcript-history.ts に selectUnwrittenTurns(target, turns, requestIdOf) を足し、
各 history.ts には同じ名前・同じ引数の薄い関数を残した。動的 import とログは変えていない。
Issue の行番号は 580/586/780/543 だったが、実コードと説明は一致していた。

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…3264)

kill-session と interrupt の対象一覧の組み立てを比べたところ、重複排除の
キー・順番・登録済みインスタンスの追加まで同一だった。違いは対象ツールの
一覧の作り方だけなので、それは各ルートに残し、引数で渡す。
kill-session の止める順番は変えていない。

本文に無い指摘: なし
…code/selection-shape.ts へ移す (#3218) (#3265)

J-2。selection-shape.ts は移したもののうち export されていた名前を再 export する。
新ファイルの import は ansi と selection-shape-patterns のみ。

判断: OPTION_ROW_PATTERN(Claude 側と共有)に依存する scanCommandCodeQuestionScreen
・CommandCodeQuestionScreen・readCommandCodeQuestionRegion・hasCommandCodeQuestionChrome・
extractCommandCodeSelectionListFrame は、循環 import を避けるため selection-shape.ts に残した
(selection-shape-patterns.ts は scope 外で、そこへ OPTION_ROW_PATTERN を移せない)。
これらが使う COMMAND_CODE_CURSOR_GLYPH / COMMAND_CODE_RULE_ROW_PATTERN /
COMMAND_CODE_RULE_MIN_COLUMNS / COMMAND_CODE_QUESTION_REGION_REJECT_PATTERN /
isCommandCodeQuestionTabRow は export を付けて新ファイルから import する(中身は不変)。
selection-shape.ts は 1151 → 668 行。残りの移動は OPTION_ROW_PATTERN を patterns へ
移す別 PR が要る。

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…I-3〜I-8) (#3267)

* refactor(detection): opencode と opencode-v2 の規則を tools/ 配下へ移す (#3217)

cli-patterns.ts の opencode ブロック(OPENCODE_PROMPT_PATTERN〜OPENCODE_SELECTION_LIST_PATTERN、
OPENCODE_SEPARATOR_PATTERN、OPENCODE_SKIP_PATTERNS)をコメントごと書き換えずに
tools/opencode/patterns.ts へ、OPENCODE_V2_* と isOpencodeV2ComposerVisible を
tools/opencode-v2/patterns.ts へ移し、cli-patterns.ts が同じ名前で再 export する。
新ファイルは cli-patterns.ts を import しない。依存は shared/pasted-text の
PASTED_TEXT_PATTERN と、v2 から opencode/patterns の OPENCODE_SEPARATOR_PATTERN のみ。
cli-patterns.ts に残した依存は無い。
opencode の範囲の途中にある CLAUDE_MODEL_OVERLAY_FOOTER_PATTERN / CLAUDE_SELECTION_LIST_FOOTER は
claude の規則なので移さず残した。opencode-v2/dialog-title の再 export も既存のまま残した。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* refactor(detection): copilot・antigravity・command-code・claude・gemini・vibe-local の規則を tools/ 配下へ移す (#3217)

cli-patterns.ts のツール別ブロックを、コメントごと書き換えずに移し、cli-patterns.ts が同じ名前で再 export する。
- copilot → tools/copilot/patterns.ts(CopilotStatusBarState 型も)
- antigravity → tools/antigravity/patterns.ts(AntigravityDialogRegion 型も)
- command-code → tools/command-code/patterns.ts
- claude(3 か所: SPINNER〜trust dialog、MODEL_OVERLAY_FOOTER/SELECTION_LIST_FOOTER、SESSION_ERROR_*)
  → 既存の tools/claude/patterns.ts に追記。同ファイルは cli-patterns を import していないので足した。
  findClaudeInputBox(composer-text)の import だけ足した。
- gemini → tools/gemini/patterns.ts、vibe-local → tools/vibe-local/patterns.ts
新ファイルは cli-patterns.ts を import しない。依存は ansi / shared/pasted-text / shared/strip-box-drawing / composer-text のみ。
detectThinking・getCliToolPatterns・buildDetectPromptOptions と、ツールに依らない再 export(isShellPaneCommand、
PASTED_TEXT_*、stripAnsi、stripBoxDrawing、opencode-v2/dialog-title)は cli-patterns.ts に残した。
cli-patterns.ts は 1891 行 → 505 行。

export 名の確認: ts の getExportsOfModule で移す前(HEAD)と後の名前を取り出して sort し diff した。
131 件ずつで差分なし。
確認: tsc / lint / tests/unit/detection・tests/unit/lib/detection・guards(prompt-detector-single-entry 等)・
prompt-response-verification・claude-session が通る。

本文に無い指摘: なし。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…3) (#3268)

* refactor(cli): relay の登録とメッセージの送信を message-dispatch.ts にまとめる (#3224)

P-2。send.ts / ask.ts の写し 2 組を src/cli/commands/message-dispatch.ts に移した。
- registerRelayForMessage: --reply-to の既定値(ask は 'self')は呼び出し側から渡す。
  `Relay registered:` の stderr 出力は send 側に残した(ask は出さない)。
- postMessage: タスクの failed 報告は onFailure で渡し、PROMPT_WAITING の代わりの文は引数で渡す。
  失敗時の順序(task failed → relay cancel → PROMPT_WAITING 判定)は元のまま。
- PROMPT_WAITING_CODE は両者とも値が 'PROMPT_WAITING' で同じなので 1 つにした。
  元は非 export の const だったため、再 export は無い。
- ask 側のコメントは、移した先のコメントと内容が重なるため、PROMPT_WAITING の説明を呼び出し側に残して
  relay 取り消しの 1 行コメントは移した先の文に統合した。

本文に無い指摘: なし

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* refactor(cli): send と ask の action を非 export 関数へ分けて complexity を下げる (#3224)

send は検査・--model 検査・リクエスト本文の組み立てを、ask は検査・--async の応答・
待機失敗の報告・返答の読み取り・返答の出力を、同じファイルの関数へ移した。
振る舞い(文言・順序・exit code・モック exit 後の続行)は同じ。
ask の `process.exit` の後の `return` は action 側に残した。

complexity(npx eslint --rule '{"complexity":["warn",20]}' src/cli/commands/send.ts src/cli/commands/ask.ts):
  before: send 247:13 complexity 39 / ask 254:13 complexity 35
  after: 警告なし(全関数 20 未満)

Issue の説明は実コードと一致していた(複雑度の数字は P-2 後の値で、Issue の 47/42 より低い)。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* refactor(ui): 答えの状態と送信を usePromptAnswerState にまとめる (#3209)

PromptPanel と MobilePromptSheet の状態・質問変更時の初期化・文字入力の規則・送信を
src/hooks/usePromptAnswerState.ts に移した。違いは引数で残した。
- send: PC は onRespond(answer, decisionId)、スマホは onRespond そのまま
- onError: PC は非 production で console.error('[PromptPanel] Failed to respond:')、スマホは省略(握りつぶし)
- canRespondDecision: PC は !!decisionId(決定の送信は id 必須)、スマホは省略(常に可)

そのほかの違い(挙動は同じで、書き方だけの差):
- 文字入力の選択肢の数: PC は番号の配列の長さ、スマホは件数。フックは配列に統一(結果は同じ)
- PC の send は useCallback で包む(元の依存配列 onRespond, decisionId と同じ条件で変わる)

useState / useMemo / useCallback の数と順番は各ハンドラ・状態とも元のまま。
isBusy / isDisabled は isSubmitting に依存するためフック内で計算して返す。

* refactor(ui): PromptStuckHint を 1 つの部品にし、PromptQuestionChoices を QuestionChoices の別名にする (#3209)

A-3: PromptStuckHint を PromptStuckHint.tsx へ移し、PromptPanel / MobilePromptSheet が import する。リンクの class は linkClassName で渡す。
A-4: PromptQuestionChoices と QuestionChoices の欄(question, labels, questionCount, custom?)が同一なことを確認し、別名にした。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* refactor(ui): PromptQuestionChoices の欄の説明を別名の JSDoc に残す (#3209)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* chore(cleanup): knip の設定を入れ、未使用のファイルを消す (#3226)

R-2: knip.json を追加。entry に scripts/ の直接実行スクリプト、.agents/.claude の skills 配下の
scripts、public/sw.js、src/i18n.ts を、ignore に website/** を書いた。
R-3: knip が未使用と報告し、src/・tests/・scripts/・server.ts のどこからも import されていない
次の 15 ファイルを消した。
- src/components/error/TerminalErrorFallback.tsx, src/components/error/index.ts
- src/components/layout/MainLayout.tsx, src/components/layout/index.ts
- src/components/worktree/TreeContextMenu.tsx, src/components/worktree/index.ts
- src/lib/cli-tools/index.ts, src/lib/detection/index.ts
- src/lib/external-apps/index.ts, src/lib/external-apps/interfaces.ts
- src/lib/git/index.ts, src/lib/polling/index.ts, src/lib/proxy/index.ts
- src/lib/security/index.ts, src/lib/tmux/index.ts

消さなかったファイル:
- src/lib/session/index.ts: tests/unit/guards/tmux-import-allowlist.test.ts が
  readFileSync で読む(import ではないが、消すとテストが落ちる)。

消した後の knip の files: tests/helpers/logger-mock.ts,
tests/fixtures/remote/short-lived-parent.cjs, tests/fixtures/remote/stderr-writer.cjs,
src/lib/session/index.ts(上記の理由で残した)。src/ で残るのはこの 1 本だけ。
scope.allow に無い未使用ファイルは消していない(tests/ の 3 本は上記のとおり)。

本文に無い指摘: src/lib/external-apps/index.ts:16 interfaces.ts の型の再 export は誰も使っていなかった。

* chore(cleanup): 設計文書が名指しする 2 ファイルは消さずに残す (#3226)

PR の CI で tests/unit/docs/design-doc-identifier-audit.test.ts が落ちたため、
設計文書が実在するものとして挙げている次の 2 ファイルを元に戻した(origin/develop の内容)。
- src/lib/tmux/index.ts: docs/design/multi-agent-state-architecture.md:1324 が名指し
- src/components/error/TerminalErrorFallback.tsx: docs/design-system.md:115,165,313 が
  常時ダークの面の実例として名指し

ほかに消した 13 ファイルは、docs/ の下(module-reference.md・implementation-history.md を除く)に
バッククォートつきのパスで名指しされていないことを grep で確かめた。
文書を直すかどうかは別の Issue で決める。
S-1: `/respond` の本文の組み立てを buildDecisionRespondBody、断られたかの
読み取りを isPromptRefused として prompt-response-body-builder.ts に置いた。
S-2: isMultiSelectPrompt を prompt-answer.ts に 1 つだけ置いた。
3 つの応答の処理そのもの(通知・例外の扱い)はまとめていない。
送る本文のキー順、リクエスト回数、通知の文言は変えていない。

実コードとの違い: 行番号は動いていたので名前で特定した。
WorktreeDetailRefactored 側は json 読み取りの後に `!response.ok ||` を
見る形(先に ok を見て例外を投げる他の 2 か所と順序が違う)。読み取りだけ
共通にし、判定の順序は元のまま残した。

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…3275)

K-5〜K-7 を 1 コミットで行う。8 ファイル(claude/codex/copilot/command-code/
antigravity/opencode/opencode-v2 の detect.ts と run-detection.ts)にあった
手書きの判定結果 46 か所のうち 44 か所を置き換えた。

置き換え(44):
- positiveVerdict(status, reason): 29 か所(promptDetection の欄なし)
- positiveVerdictWithPrompt: 7 か所(promptDetection の欄あり、hasActivePrompt false)
- activePromptVerdict: 6 か所(waiting / hasActivePrompt true)
- unreadVerdictWithPrompt: 2 か所(run-detection の low / none)

残し(2):
- run-detection.ts の INPUT_PROMPT: evidence が resolveIdleEvidence の値で、ほかと形が違う
- antigravity/detect.ts の UNKNOWN_FRAME: low / none で promptDetection の欄が無く、1 回だけ

欄の有無と値は元と同じ。dialog.ts は判定結果の手書きが無く、変更なし。
関連テスト 119 ファイルが通る。

本文に無い指摘: なし

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…階ごとの関数に分ける (#3215 G-3〜G-6, G-8) (#3277)

* refactor(session): 状態の合成と履歴の行を書く処理を別ファイルへ移す (#3215)

整理 Epic #3207 の列 G、手順 G-3 と G-4。契約の指示で 2 つを 1 つの
コミットにした。どちらも節を別のファイルへ移すだけで、振る舞いは
変えていない。

G-3: src/lib/session/structured-status-merge.ts(新規)
- current-output-builder.ts の ScraperVerdict から readNewestPromptAt まで
  (HEAD 28fc8ec で 388-750 行、363 行)を、コメントごと移した。
  ScraperVerdict / MergedStatusVerdict / summarizeAskUserQuestion /
  mergeStructuredStatus / hookClosedTurn / staleReadyCandidate /
  structuredReadyPredatesPrompt / readNewestPromptAt。
- 移した 363 行の差分は export の 3 語だけ。buildPayload が呼ぶ
  summarizeAskUserQuestion / staleReadyCandidate / readNewestPromptAt に
  付けた。current-output-builder.ts は import して呼ぶだけで、再 export は
  しない(外から見える名前は増えていない)。
- current-output-builder.ts は、export されていた 3 つを同じ名前で
  再 export する: mergeStructuredStatus と、型 ScraperVerdict /
  MergedStatusVerdict。
- readNewestPromptAt が使う @/lib/db/chat-db の import は、上に付いていた
  コメント(Issue #2429)ごと新しいファイルへ移した。

G-4: src/lib/session/current-output-history-writers.ts(新規)
- current-output-builder.ts の describePaneObstruction から
  recordStructuredPrompt まで(HEAD 28fc8ec で 150-375 行、226 行)を、
  コメントごと移した。describePaneObstruction / broadcastRecordedRow /
  recordUnclassifiedFrame / recordStructuredPrompt。
- 移した 226 行の差分は export の 2 語だけ。buildPayload が呼ぶ
  recordUnclassifiedFrame / recordStructuredPrompt に付けた。どちらも
  もともと export していないので、current-output-builder.ts からの
  再 export は無い。
- ws-server の import は動的なままである。

両方に共通
- ロガーは新しい 2 つのファイルでも createLogger('current-output-builder')
  を使う。ログに出るモジュール名と 6 つのメッセージ名は変わらない。
  current-output-builder.ts のロガーも残る(detection-divergence が使う)。
- プロセス全体の状態(globalThis.__*)は、移した節には無い。
- 新しいファイルは @/lib/tmux/** を import しない。tmux のキャッシュの
  import と resolvePromptWaiting( の呼び出しは current-output-builder.ts に
  残っている。
- 新しいファイルは current-output-builder.ts を import しない。循環 import に
  なる部分は無かったので、残したものは下の 1 行だけである。
- 節を移したことで current-output-builder.ts で使われなくなった import を
  外した。残すと lint が通らない: createMessage /
  getLastUserMessageForInstance / OPENCODE_SIDEBAR_RECOVERY_CHORD /
  UNCLASSIFIED_PROMPT_TYPE / buildStructuredPromptHistoryRecord /
  structuredWaitingReason と、型 ChatMessage / UnclassifiedFrameRecord /
  SessionStatus / AskUserQuestionEpisode / StructuredPromptWaitingState /
  StructuredSessionState / PublishedTurn / StructuredAskUserQuestionSummary。
- current-output-builder.ts は 1,567 行から 967 行になった。

Issue の説明と実コードの照合
- 「B:847-1074」「B:1085-1449」は G-1 と G-2 の前の行番号。今は
  148-375 行と 386-750 行で、行数と関数の名前は一致した。
- G-4 の範囲の先頭(B:847)は const logger = createLogger(...) の行。
  current-output-builder.ts に残る detection-divergence が使うので、
  移さずに残した。
- G-3 の範囲の先頭(B:1085)は export type { StatusEvidence } の行。
  これは current-output-builder.ts 自身の再 export で、すぐ上のコメントも
  「this module から export し続ける」と書いている。移すと 2 つの
  ファイルを経由する再 export になるので、コメントごと残した。消すのは
  G-8 で、この契約では触っていない。
- 「テストの扱い」の「G-3 で 5 本の import 行の付け替え」は行っていない。
  再 export を残したので不要で、契約の指示もそうなっている。tests/ の
  差分は 0 行。
- 「触らないもの」の「B:1100-1103」(statusEvidence は
  isUnclassifiedActive の否定ではない、というコメント)は、書き換えずに
  structured-status-merge.ts:54 へ移った。

本文に無い指摘: src/lib/session/structured-status-merge.ts:86
summarizeAskUserQuestion は mergeStructuredStatus の補助ではない。
buildPayload が、画面を読めないダイアログの説明を作るのに使う。Issue の
範囲(B:1085-1449)に入っているので、一緒に移した。
本文に無い指摘: src/lib/session/structured-status-merge.ts:383
readNewestPromptAt は DB を読んでログを出す。mergeStructuredStatus の
コメントは「Pure」と書いているが、それは関数についての記述で、
ファイル全体は副作用の無いモジュールではない。
本文に無い指摘: src/lib/session/current-output-history-writers.ts:36
recordUnclassifiedFrame の説明のコメントが、describePaneObstruction の
コメントの上にあり、関数(:129)から離れている。移す前からこの並び
だった。
本文に無い指摘: src/lib/session/current-output-history-writers.ts:90、
:98、:111 と src/lib/session/structured-status-merge.ts:41 の
「this module」は、current-output-builder.ts のことを書いたコメントである。
コメントは書き換えない決まりなのでそのまま移し、ファイル冒頭にその旨を
書いた。
本文に無い指摘: src/lib/session/structured-status-merge.ts:376-377
{@link recordUnclassifiedFrame} と {@link recordStructuredPrompt} は、
移す前は同じファイルの中で名前が見えていたが、今は見えない。
src/lib/session/current-output-history-writers.ts:98
{@link emitChatTurnProgress} は G-2 の時点から見えていない(エディタで
飛べなくなるだけで、コンパイルには影響しない)。

確認
- npx tsc --noEmit: exit 0
- npm run lint: exit 0
- npx vitest run(current-output-builder と移した名前・ログ名を名指しする
  テスト、current-output-builder を import する 3 つのモジュール
  (current-output の route / terminal-broadcast / opencode の history)と
  unclassified-frame-tracker / prompt-waiting-composition /
  status-evidence を名指しするテスト、tests/unit/guards。
  unit 189 + integration 20): 209 files / 3,679 tests passed

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(session): hook のイベントを読む部分を readAgentEvents に分ける (#3215)

整理 Epic #3207 の列 G、手順 G-5。振る舞いは変えていない。

- buildPayload の先頭にあった、エージェント自身のイベントを読む部分
  (HEAD ce9a800 で 216-330 行、115 行。stopEventAt から
  structuredEvents の組み立てまで)を、同じファイルの中の関数
  readAgentEvents に分けた。export はしていない。
- 移した 115 行は 1 文字も変えていない(字下げも同じ)。差分に出るのは、
  関数の頭と、末尾の return、buildPayload の頭の位置だけである。
- 呼び出す位置は元と同じで、getSessionName の後、
  await cliTool.isRunning の前。中に await は無いので、await の順番と
  回数は変わらない。読む順番(getLastStopEventAt → getLastAgentEvent →
  getAgentEventSource → Date.now → getPendingDecisions →
  getAskUserQuestion → getAgentSessionTelemetry → structuredEvents の
  各キー)も、structuredEvents のキーの並びも同じ。
- 返すのは、後ろで使う 4 つだけ: stopEventAt / eventSource /
  askUserQuestion / structuredEvents。lastEvent / now /
  pendingDecisions / agentSession は、この部分の中でしか使われて
  いなかった。structuredEvents は同じオブジェクトを返すので、後ろで
  promptWaitingSince / promptWaitingSource を書き込む処理はそのまま働く。
- 戻り値の型 AgentEventsRead を書くために、型だけの import を 2 つ足した
  (AgentEventSource / AskUserQuestionEpisode)。実行時の import は
  増えていない。
- current-output-builder.ts は 967 行から 999 行になった(関数の頭、
  型、引数の受け渡しの分)。

Issue の説明と実コードの照合
- 「B:1515-1629」は G-1〜G-4 の前の行番号。今は 216-330 行で、行数
  (115 行)は一致した。

本文に無い指摘: src/lib/session/current-output-builder.ts:230、:242、
:283、:314 のコメントにある「the degraded prompt below」「before the
`isRunning` branch」「both return paths」は、buildPayload の後ろの処理を
指している。今は別の関数の中だが、コメントは書き換えない決まりなので
そのままにした。

確認
- npx tsc --noEmit: exit 0
- npm run lint: exit 0
- npx vitest run(current-output-builder / worktree-status-helper /
  prompt-waiting-composition / session-starting-state / status-evidence を
  名指しするテスト。unit 115 + integration 6): 121 files / 2,098 tests
  passed。サーバを立てる integration 3 本(ws-publisher-bridge-2220 /
  wait-idle-composer-2011 / ws-broadcast-module-instance-2214)は、
  契約の「サーバを起動しない」に従って実行していない。
- tests/ の差分は 0 行。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(session): 承認の組み立てを composePromptData に分ける (#3215)

整理 Epic #3207 の列 G、手順 G-6。振る舞いは変えていない。

- buildPayload の中の、公開するプロンプトと承認の返答を組み立てる部分
  (HEAD 185be80 で 619-734 行、116 行。Issue #1726 のコメントから
  promptAnswerable まで)を、同じファイルの中の関数 composePromptData に
  分けた。export はしていない。
- 移した 116 行は 1 文字も変えていない(字下げも同じ)。消した 116 行と
  足した行を突き合わせ、116 行すべてが同じ文字列で足されていることを
  確かめた。新しく書いたのは、関数の頭、引数と戻り値の型、return、
  呼び出しの 54 行である。
- 呼び出す位置は元と同じで、isPromptWaiting の後、
  detection-divergence のログの前。中に await は無いので、await の順番と
  回数は変わらない。中で呼ぶ順番(applyAskUserQuestion →
  isAddressableDecision → pendingDecisionKind →
  structuredDecisionOptionsFor → summarizeAskUserQuestion →
  buildStructuredPromptData → assessPromptAnswerability)も同じ。
- 引数は 9 つで、1 つのオブジェクトで渡す: worktreeId / cliToolId /
  output / statusResult / scraperPromptWaiting / startingSince /
  promptWaiting / askUserQuestion / eventSource。中の名前を元と同じに
  したので、移した行を書き換えずに済んだ。
- 返すのは、後ろで使う 3 つだけ: structuredFacts / promptData /
  promptAnswerable。scraperPromptData / correctedPromptData /
  addressableDecisionId / addressesQuestion / decisionOptions は、この
  部分の中でしか使われていなかった。応答のキーの並びは変わらない
  (return の組み立ては buildPayload に残っている)。
- 引数の型を書くために、型だけの import を 2 つ足した
  (StatusDetectionResult / StructuredPromptWaitingState)。実行時の
  import は増えていない。
- current-output-builder.ts は 999 行から 1,053 行になった。

Issue の説明と実コードの照合
- 「B:1895-2001」は G-1〜G-5 の前の行番号。今は 628-734 行で、行数
  (107 行)は一致した。範囲の先頭(const scraperPromptData)のすぐ上に
  付いているコメント 9 行(Issue #1726 / #2040)は、その行の説明なので
  一緒に移した。
- 「副作用が無い」は、この部分が自分の const 以外に何も代入しない、
  という範囲で確かめた。呼んでいる 7 つの関数の中までは追っていない。
  呼ぶ位置と順番が同じなので、どちらでも振る舞いは変わらない。

本文に無い指摘: src/lib/session/current-output-builder.ts:384 のコメント
「Issue #2040 moved the read to the top of this function」の
this function は buildPayload のことである(G-5 の後は readAgentEvents が
読んでいる)。コメントは書き換えない決まりなのでそのままにした。
本文に無い指摘: src/lib/session/current-output-builder.ts:342
composePromptData は statusResult と scraperPromptWaiting の両方を
受け取るが、後者は前者の hasActivePrompt と同じ値である。移した行が
両方の名前を使っているので、書き換えずに両方を渡した。

確認
- npx tsc --noEmit: exit 0
- npm run lint: exit 0
- npx vitest run(G-5 と同じ 121 本。unit 115 + integration 6):
  121 files / 2,098 tests passed
- tests/ の差分は 0 行。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(session): 承認の判定を isApiAnswerableStructuredWait の呼び出しにする (#3215)

整理 Epic #3207 の列 G、手順 G-8。振る舞いは変えていない。
この契約で行った手順は G-5 / G-6 / G-8 の 3 つで、G-7 は行っていない
(理由は下の「行わなかった手順」)。

G-8 で変えたこと(src/lib/session/current-output-builder.ts だけ)
- composePromptData の addressableDecisionId(:406)の条件のうち 3 つを、
  isApiAnswerableStructuredWait(prompt-waiting-composition.ts:62)の
  呼び出しに置き換えた。
  - 元の条件: promptWaiting !== null &&
    promptWaiting.source === 'notification' &&
    eventSource.capabilities.eventIdentity === 'permission-id' &&
    isAddressableDecision(promptWaiting.decisionId)
  - 関数の中身: structured !== null &&
    structured.source === 'notification' &&
    eventIdentity === 'permission-id' &&
    isAddressableDecision(structured.decisionId)
  - 4 つの条件も並びも同じで、isAddressableDecision の import 元
    (@/lib/session/structured-prompt)も同じであることを確かめた。
  - 先頭の promptWaiting !== null && は残した。関数は boolean を返すだけ
    なので、残さないと TypeScript が次の行の promptWaiting.decisionId を
    null かもしれないと判定する。同じ判定が 2 回走るだけで、結果は
    変わらない。型の言い切り(!)は使っていない。
  - addressableDecisionId の型は string | null のままである。
- 使わなくなった isAddressableDecision の import を外した(残すと lint が
  通らない)。isApiAnswerableStructuredWait の import を足した。
  prompt-waiting-composition を vi.mock で差し替えているテストは無い。
- export type { StatusEvidence } と、その説明のコメント 9 行を消した。
  src/ と tests/(scripts/ と bin/ も)の import / export / import() を
  複数行のものも含めて調べ、current-output-builder 経由で読んでいる名前は
  buildCurrentOutput / CurrentOutputPayload / ScraperVerdict /
  mergeStructuredStatus / buildChatTurnProgress / emitChatTurnProgress /
  resetChatTurnProgressState / ChatTurnProgressDraft の 8 つだけで、
  StatusEvidence は 0 件だった。export * も無い。tsconfig は tests/ も
  含むので、npx tsc --noEmit でも確かめた。
  型 StatusEvidence の import は残している(buildPayload が使う)。
- current-output-builder.ts は 1,053 行から 1,042 行になった。

行わなかった手順: G-7(画面の判定の前半を共通の関数にする)
契約の決まり「2 つの並びを 1 行ずつ見比べ、引数や間に挟まる処理に違いが
あれば、違いを引数にせず、G-7 を行わずに違いを書く」に当たったので、
コードは変えていない。新しいファイルも作っていない。

見比べた並び(このコミットの時点の行番号)
  current-output-builder.ts      worktree-status-helper.ts
  :607 buildCompositeKey          :521
  :608 getLastServerResponseTimestamp  :522
  :609 lastOutputTimestamp        :523
  :611 detectSessionStatus        :524
  :618 observeSessionStartingFrame     :527
  :624 startingStatusResult       :533
  :680 isUnclassifiedFrame        :557

同じだったもの
- 7 つの式の引数は、変数の名前(lastServerResponseTimestamp と
  lastServerResponseTs)を除いて同じである。
- import 元も同じである(@/lib/polling/auto-yes-manager /
  @/lib/detection/status-detector / @/lib/session/session-starting-state /
  @/lib/session/status-evidence)。
- buildCompositeKey から startingStatusResult までの 6 つの式の間には、
  どちらのファイルでもコメントしか無い。

違い
1. startingStatusResult と isUnclassifiedFrame の間に挟まる処理が違う。
   current-output-builder.ts(:633-669)は statusResult から 6 つの値を
   導くだけで、状態は書かない(thinking / scraperPromptWaiting /
   isSelectionListActive / isPagerActive / isDismissablePanelActive /
   evidence)。worktree-status-helper.ts(:543-548)は
   recordCapturedModelInfo(extractModelInfo(...)) を呼んで、モデルの
   保持状態を書く。4 つを 1 つの関数にすると、worktree-status-helper では
   isUnclassifiedFrame が recordCapturedModelInfo より前に動くことになり、
   呼び出す順番が変わる。
2. worktree-status-helper.ts の並びは try(:507-642)の中にあり、途中で
   throw すると isProcessing = true になる。startingSince(:478)と
   isUnclassified(:505)は try の外の let への代入である。
   current-output-builder.ts は try が無く、どちらも const である。
3. worktree-status-helper.ts は、並びの前に getSessionStartingSince で
   startingSince を 1 回読み(:478)、生存確認をするかどうかに使う。
   current-output-builder.ts の、セッションが動いている側の経路には
   この読みが無い。
4. instanceId の型が違う(current-output-builder.ts は
   string | undefined、worktree-status-helper.ts は string)。
5. lastServerResponseTimestamp を、current-output-builder.ts は応答に
   載せる(:1001)。worktree-status-helper.ts は並びの中でしか使わない。
6. 契約とIssueが呼び出し側に残すとしているもの: 画面を取る行数
   (STATUS_CAPTURE_LINES :573 と resolveCaptureSpec(...).statusLines
   :513)、並びの後の observeStatusEvidence に渡す値(合成した後の
   published :951 と、画面の statusResult :573)。

もう 1 つの理由
- 並びは式 7 つである。関数にすると、受け取るものが 4 つ(worktreeId /
  cliToolId / instanceId / output)、返すものが 4〜5 つ(compositeKey /
  lastServerResponseTimestamp / startingSince / statusResult /
  isUnclassified)になる。契約の「行が増えて重複が引数の受け渡しになる
  だけ」にも当たると判断した。試作はしていない。

#3232 への申し送り
- 違い 1 を避けて順番を変えずに共通にできるのは、buildCompositeKey から
  startingStatusResult までの 6 つの式である。isUnclassifiedFrame は
  呼び出し側に残すことになる。行うかどうかは決めていない。

本文に無い指摘: src/lib/session/prompt-waiting-composition.ts:49-50 の
コメント「The same three conjuncts `current-output-builder` requires」は、
2 つのファイルが同じ条件を別々に持っていた時の説明である。今は
current-output-builder.ts がこの関数を呼ぶ。コメントは書き換えない
決まりなのでそのままにした。
本文に無い指摘: src/lib/session/current-output-builder.ts:397 のコメント
「Issue #2031 adds the fourth conjunct」の 4 つの条件は、今は
promptWaiting !== null と、関数の中の 3 つである。コメントはそのままに
した。
本文に無い指摘: 消した export type { StatusEvidence } のコメントは
「the type is imported by name elsewhere」と書いていたが、
current-output-builder 経由の import は無かった。

確認
- npx tsc --noEmit: exit 0
- npm run lint: exit 0
- npx vitest run(G-5 / G-6 で実行した 121 本に、structured-prompt /
  isAddressableDecision / isApiAnswerable を名指しするテスト 14 本を
  足したもの。unit 128 + integration 7): 135 files / 2,314 tests passed。
  サーバを立てる integration 3 本(ws-publisher-bridge-2220 /
  wait-idle-composer-2011 / ws-broadcast-module-instance-2214)は、
  契約の「サーバを起動しない」に従って実行していない。
- tests/ の差分は 0 行。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
… O-4, O-6) (#3279)

* refactor(cli): pollWorktree の承認・選択画面・読めない画面・ready の段階を、終了・待つ・次へを返す関数にする (#3223)

整理 Epic #3207 の列 O、手順 O-3 と O-4(この契約では 2 つを 1 つのコミットで行う)。
pollWorktree の中の 4 つの段階を、wait.ts の中の export しない関数にした。関数は
PollContext(pollWorktree が書き換えない値)、PollState、その回の応答 data を受け取り、
StageOutcome を返す: exit(この結果で終了)、wait(待ってから次の回へ)、next(この段階は
当てはまらないので次の段階へ)。pollWorktree は関数を元の順番のまま呼び、exit なら
return、wait なら sleep(POLL_INTERVAL_MS) して continue、next なら先へ進む。

分けた関数:
- O-3: promptStage(承認)、selectionListStage(選択画面)、unclassifiedStage(読めない画面)
- O-4: readyStage(ready の枝。条件式ごと中へ移した)。中で次の 5 つを元の順に呼び、
  next でない結果をそのまま返す: upstreamFaultStage(上流の不具合)、unsettledTurnStage
  (ターンの確定)、selfResumeHoldStage(自分で再開する場合の保留)、promptLedgerStage
  (依頼の台帳)、completionStage(根拠)
- 型と補助: PollContext、PollResult、StageOutcome、exitWith

振る舞いの保持:
- 移した 8 つの範囲(281 行)は、字下げを除いて元の行と一致する。変えたのは 2 種類だけ。
  `await sleep(POLL_INTERVAL_MS); continue;` の 7 組は `return { kind: 'wait' };` に、
  終了の `return {…};` 5 か所は `return exitWith({…});` にした。機械的に突き合わせた
  (各範囲が、新しいファイルの中に連続した 1 かたまりとして 1 か所だけ見つかる)。
  コメントは書き換えずに一緒に移した。
- sleep を呼ぶのは pollWorktree だけ。段階の関数は待たない。sleep・リクエスト・
  console.error の回数と順番、条件式、state を書き換える位置は元のまま。
- answeredNewestPrompt は promptLedgerStage のローカル変数のまま。next の結果に載せて
  返し、completionStage が引数で受け取る。ポーリングをまたいで持ち越さない。
- selfResumeStopAt と selfResumeSuffix は pollWorktree に残し、引数で渡した。呼ぶ位置は
  元と同じ。
- 違いが 1 つある。readyStage と promptLedgerStage は async 関数なので、readyStage まで
  進んだ回は await が 1 段(台帳の段階まで進んだ回は 2 段)増え、その分だけマイクロ
  タスクが遅れる。下の突き合わせで、順番と時刻が変わらないことを確かめた。

確認:
- npx tsc --noEmit、npm run lint。
- wait.ts / ask.ts を import するテスト 23 本(532 件)。ソースを走査するガードと、
  wait.ts を名指しするテスト 27 本(551 件)。テスト全体(npm run test:unit)は実行して
  いない。
- 元の pollWorktree と新しい pollWorktree に、乱数で作った同じ応答の列を 6000 通り渡し、
  リクエストのパス、console.error の文言、sleep の長さ、それぞれの時刻、戻り値または
  例外を、順番ごと比べた。82,095 件がすべて一致した。移した段階の console.error は
  すべての種類が出ている。この突き合わせは作業用の場所で vitest で行い、リポジトリには
  入れていない。

複雑度(npx eslint --no-eslintrc --parser @typescript-eslint/parser --rule
'complexity: [warn, N]' src/cli/commands/wait.ts、ESLint 8.57.1):
- pollWorktree: 64 → 28
- promptStage 15、promptLedgerStage 9、readyStage 7、upstreamFaultStage 6、
  selectionListStage 5、unclassifiedStage 5、selfResumeHoldStage 4、unsettledTurnStage 3、
  completionStage 3、exitWith 1
- 行数: 1764 → 1933(+169。型と関数の宣言、その説明、呼び出し側の分岐)

Issue の説明と実コードの照合:
- 受入基準の「pollWorktree の複雑度が 25 未満」には、O-3 と O-4 では届かない(28)。
  残りの内訳: 関数本体 1、`??`(autoYesGrace)1、while 1、タイムアウト 2 つ 4、URL の
  三項 1、content / isRunning / startingSince 3、自己再開の保留の終了 2、段階の呼び出し
  4 つ × 2 = 8、Auto-Yes の保留の解除 1、Path A(未起動・セッション消滅)3、catch 3。
  O-5 は pollWorktree に触らない。25 未満にするには、O-1〜O-5 に無い場所を関数にする
  必要がある(例: タイムアウト 2 つと Path A を、それぞれ同じ形の関数にすると 23)。
  この契約の範囲の外なので、行っていない。
- 「今は 82」は 5d5592b の値で、O-1 と O-2 のあとは 64 だった。
- 承認の範囲のすぐ下にある「Auto-Yes の保留の解除」(Issue #2463 のコメントが付いた
  if)は、Issue の範囲(:909-1017)の外なので、pollWorktree に残した。
- promptStage は 15 のまま。中をさらに分けることは、この契約に無いので行っていない。

本文に無い指摘: src/cli/commands/wait.ts:1537 「Issue #1708 narrowed Path B」のコメントは
ready の条件(isUnclassifiedActive !== true)の説明だが、セッション消滅(Path A)の if の
上に付いている。ready の枝と隣り合っていないので、移さずに元の位置に残した。
本文に無い指摘: tests/unit/cli/commands/wait.test.ts:527 (ほか :628、:1524)
`--on-prompt human` の待ち(承認・選択画面・読めない画面)を next に変えても、また
pollWorktree が選択画面・読めない画面・ready の wait を無視しても、関連テスト 532 件は
通る(6 通りの書き換えで確かめて、元に戻した)。待ったあとに進捗の行
(Waiting: … (status=…))が 1 行増えるだけの違いを、テストが見ていない。上の 6000 通りの
突き合わせは、この 6 通りを含む 15 通りの書き換えを、すべて検出する。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(cli): pollWorktree のタイムアウトと、未起動・セッション消滅の判定を、終了・次へを返す関数にする (#3223)

整理 Epic #3207 の列 O、手順 O-6。O-3・O-4 のコミット(63ff9d58)の上に積む。
pollWorktree に残っていた 2 つの範囲を、wait.ts の中の export しない関数にした。形は
O-3・O-4 の段階の関数と同じで、PollContext・PollState(・その回の応答 data)を受け取り、
終了(exit)か次へ(next)を返す。pollWorktree は関数を元の位置で呼び、exit なら return、
そうでなければ先へ進む。

分けた関数:
- deadlineStage: タイムアウトの判定 2 つ(--timeout と --stall-timeout)。ループの先頭、
  try の外、リクエストの前で呼ぶ(元と同じ位置)。
- notRunningStage: 未起動(終了コード 21)とセッション消滅(終了コード 0、
  basis=session_gone)の判定(Path A)。読めない画面の段階の後、ready の段階の前で呼ぶ
  (元と同じ位置)。selfResumeSuffix は readyStage と同じく引数で渡す。
- 型 ExitOrNextOutcome(StageOutcome から wait を除いたもの)。上の 2 つの戻り値の型。

判断:
- タイムアウトの判定 2 つは、1 つの関数にまとめた。2 つの関数に分けると、呼び出し側の
  分岐が 1 つ増えて 24 になる。同じ 2 つの判定を 1 つの関数で行う先例が、同じファイルに
  ある(nextPollWithinDeadlines)。
- この 2 つの段階は待たない(wait を返さない)ので、呼び出し側は exit だけを見る。ほかの
  段階と同じように wait の分岐も書くと、pollWorktree は 25 になり、目安に届かない。
  wait を返すように書き換えたときに、呼び出し側が黙って next として扱うことがないよう、
  戻り値の型から wait を除いた(書き換えると型エラーになる)。
- そのために、exitWith(O-3 で足した補助)の戻り値の型を、StageOutcome から、その exit の
  枝(Extract<StageOutcome, { kind: 'exit' }>)に狭めた。型の注釈だけの変更で、実行される
  コードは変わらない。既存の呼び出し 5 か所は、そのまま型が通る。

振る舞いの保持:
- 移した 2 つの範囲(空行を除いて 16 行と 24 行)は、字下げを除いて元の行と一致する。
  変えたのは 1 種類だけで、終了の `return { exitCode: … };` 4 か所を
  `return exitWith({ exitCode: … });` にした。機械的に突き合わせた(各範囲が、新しい
  ファイルの中に連続した 1 かたまりとして 1 か所だけ見つかる)。コメントは書き換えずに
  一緒に移した。
- 2 つの関数は同期の関数で、await を足していない。sleep・リクエスト・console.error・
  Date.now() の回数と順番、条件式、標準エラーの文言、終了コードは元のまま。
- sleep を呼ぶのは pollWorktree だけ、という O-3・O-4 の形も変えていない。

確認:
- npx tsc --noEmit、npm run lint。
- wait.ts / ask.ts を import するテストと、wait.ts を名指しするテスト、合わせて 30 本
  (738 件)。テスト全体(npm run test:unit)は実行していない。
- O-3・O-4 と同じ確かめ方。元の pollWorktree(63ff9d58)と新しい pollWorktree に、乱数で
  作った同じ応答の列を 6000 通り渡し、リクエストのパス、console.error の文言、sleep の
  長さ、それぞれの時刻、戻り値または例外を、順番ごと比べた。51,397 件がすべて一致した。
  この中で、Timeout は 1,449 回、Stall timeout は 1,080 回、Not started は 828 回、
  basis=session_gone は 291 回(うち heldForSelfResume 付きが 49 回)出ている。
- 同じ突き合わせが、新しい 2 つの関数と呼び出し側への 16 通りの書き換え(>= を > に
  する、2 つの判定の順を入れ替える、判定をリクエストの後へ移す、exit を無視する、
  everRunning を見ない、終了コードを変える、selfResumeSuffix を落とす、Path A を ready の
  後や読めない画面の前へ移す、など)を、すべて検出することを確かめた。既存のテストが
  この 16 通りを検出するかは、確かめていない。
- 突き合わせは作業用のファイル(元の wait.ts の写しと、vitest のテスト 1 本)で行い、
  終わってから消した。コミットには入っていない。

複雑度(npx eslint --no-eslintrc --parser @typescript-eslint/parser --rule
'complexity: [warn, N]' src/cli/commands/wait.ts、ESLint 8.57.1。O-3・O-4 と同じ数え方):
- pollWorktree: 28 → 23(目安の 25 未満)
- deadlineStage 5、notRunningStage 4。ほかの関数は変わらない。
- 内訳: タイムアウト 2 つの 4 と Path A の 3 が pollWorktree から出て、呼び出しの分岐が
  1 つずつ(計 2)入った。
- 行数: 1933 → 1971(+38。型と関数の宣言、その説明、呼び出し側の分岐)

Issue の説明と実コードの照合:
- 「23 になる見込み」は、実測と一致した。
- 「Completion check [DR1-04]」のコメントは Path A と Path B の両方を説明しているが、
  未起動の if の上に付いているので、書き換えずに、分けずに、notRunningStage へ移した。
  Path B は、pollWorktree がその直後に呼ぶ readyStage である。

本文に無い指摘: src/cli/commands/wait.ts:1203 「Issue #1708 narrowed Path B」のコメントは
ready の条件(isUnclassifiedActive !== true)の説明だが、セッション消滅(Path A)の if の
上に付いている(63ff9d58 でも指摘。そのときは :1537)。付いている if と一緒に、
書き換えずに notRunningStage へ移した。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(ui): 互換の関数 3 つと重複した型 2 つを消し、読み取りを prompt-view に一本化 (#3228)

prompt-decision-id.ts の readPromptDecisionId / readPromptQuestionChoices /
readStructuredDecisionHeading と、型 PromptQuestionChoices / StructuredDecisionHeading を削除。
PromptPanel.tsx と MobilePromptSheet.tsx の `export { optionTakesTypedText }` も削除。
削除前に src/ を grep し、コメント以外の呼び出しが無いことを確認した。
消した関数・型の説明コメントは prompt-decision-id.ts のファイル先頭へ書き換えずに移した。
テストは import 行だけを変え、@/lib/session/prompt-view から別名で取る。
O-5(UNCLASSIFIED_PROMPT_TYPE の一本化)は行っていない。isAnswerablePromptData は触っていない。

prompt-view-3184.test.ts の 2 か所は題名・assert を変えず、import 行だけ変えた。比べる相手が変わる(PR で利用者に判断を訊く):
- `agrees with the panel's own text-field predicate`: 以前は prompt-view の optionTakesTypedText と
  PromptPanel の再 export を比べていた。今は同じ prompt-view の同じ関数どうしの比較(自己比較)になる。
- describe `prompt-decision-id compatibility wrappers keep their answers`: 以前は互換の関数と
  prompt-view の derivePromptView の結果を比べていた。今は readDecisionId / readQuestionChoices /
  readDecisionHeading(prompt-view)と derivePromptView の比較になり、互換の関数は検証対象から外れる。

判断: Issue の記述は実コードと一致していた。テスト 11 本のうち tests/unit/hooks/sources/opencode-question-decision-2100.test.ts は
コメントでの言及のみで import が無いため変えていない。PromptPanel.test.tsx と MobilePromptSheet.test.tsx は
optionTakesTypedText の import 先を prompt-view に変えた。

本文に無い指摘: なし

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(session): 互換の関数を消した後のテスト 2 か所を整える (#3228)

利用者の判断により、tests/unit/session/prompt-view-3184.test.ts の 2 か所を直した。

- 消したテスト: `agrees with the panel's own text-field predicate (until A2 removes the copy)`
  同じ関数どうしの比較になり、確かめる相手が無いため。専用だった import
  (optionTakesTypedText の 2 行)も消した。
- 題名の修正: describe の題名のみ。中の assert と呼び出しは変えていない。
  前: `prompt-decision-id compatibility wrappers keep their answers`
  後: `the field readers agree with derivePromptView`

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* docs(orchestrate): 整理の契約の型と、直す前に整える判断を手順に入れる (#3208)

整理 Epic #3207 の決まりを、ふだんの手順書に入れた。文書だけの変更。

.claude/commands/orchestrate.md
- 1-2b: 逸脱時の退避路の説明の後に、「退避路で報告された重複は、整理の Issue の
  候補として残す」を足した(run の一覧 refactor-candidates.md に残し、8-4 で訊く)。
- 2-4-3(新しい小節。2-4-2 の後): 整理(refactor)の Issue の契約の型。
  scope.allow に tests/** を入れない/goal に転記する「整理の決まり」/整理と
  振る舞いの変更を同じ PR に入れない。Epic #3207 の run の実測を、決まり → 実測
  の形で書いた(採用の基準と採用しなかった 9 手順、新しいモジュールに置く理由、
  await を足さない、ファイルを消す手順、コメント、同じ列の手順を 1 本の PR に
  まとめる、テストが自分どうしの比較になる変更、範囲外の食い違い、検証ゲート)。
- 2.5-4(新しい小節): 「直す前に整える」の判断。
- 8-4(新しい小節): 整理の Issue の候補を summary に写し、起票するかを利用者に訊く。

.claude/commands/bug-fix.md
- Phase 2 の先頭に「2-1. 直す前に整える」を足した。既存の文の前には、つなぎの
  見出し「2-2. 対策案の提示」だけを足した。

.claude/prompts/refactoring-core.md
- 冒頭に「整理は振る舞いを変えない。既存のテストを書き換えずに通す」と、
  「整理の決まり」の節を置いた。
- カバレッジ 80% を整理の目標と読める記述を直した: improvement_goals の例、
  Phase 4(テストの追加は別の PR にする)、Phase 5 の注記、コミットメッセージと
  出力の例の値(75% → 82% を 75% → 75% に)、原則 2、完了条件。

判断:
- git diff --shortstat の 1 行は「写しをまとめる手順の契約にだけ書く」とした。
  長い関数を分ける手順は行が増えうるので、採用の基準(長い関数が短くなるなら
  採用する)と両立させるため。Epic の契約でも、この行があるのは M-5・Q-3・W-1 で、
  E-4・E-5 には無い。
- 検証ゲートが lint と typecheck だけの契約は、ローカルの裁定がテストを含まない
  ので、「マージは CI の Unit Tests が pass になってから(6-2 の例外と同じ扱い)」
  と書いた。
- refactoring-core.md の原則 2(テストを先に書く/新しい関数にはテストを追加)は、
  冒頭の決まりと食い違うので「既存のテストで確かめる」に直した。
- Issue 本文が挙げるコメント(src/components/mobile/MobilePromptSheet.tsx:51-57)は、
  いまの develop には無いので、手順書には引用していない。

確認:
- .agents/ の下に、orchestrate・bug-fix・refactoring-core のミラーは無い
  (.agents/skills/ は cmate-verify・demo-video・video-to-gif だけ)。
- 3 つの手順書を読むテスト 10 本(223 件)、npm run lint、npx tsc --noEmit、
  node scripts/changelog-fragments.mjs check は、すべて通った。

本文に無い指摘: .claude/commands/pm-auto-dev.md:175 refactor-context.json の例の improvement_goals が「カバレッジを85%以上に向上」のまま
本文に無い指摘: .claude/commands/refactoring.md:78 完了条件が「品質メトリクスが改善(カバレッジ、複雑度)」のまま
本文に無い指摘: .claude/agents/refactoring-agent.md:64 完了条件が「Quality metrics improved (coverage, complexity)」のまま

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(orchestrate): 2-4-3 の実測の記述を事実に合わせる (#3208)

.claude/commands/orchestrate.md の 2-4-3 だけを直した。

- 「新しいモジュールに置く理由」: P-1 と Q-1 の止まった理由を分けて書いた。
  P-1 は、部分的な vi.mock(resolveInstanceTarget だけを差し替えるテスト)に当たった。
  Q-1 は、共通の関数を src/lib/session/resolve-session-target.ts の中に置くと、
  そのモジュールが next/server に依存し、API ルート以外(src/lib/push と
  src/lib/relay)からも import されているので置けなかった。置き直した先は
  src/lib/session/session-target-conflict-response.ts。
  決まりの文に「NextResponse を返す関数のように、依存が増える関数は、その依存を
  持ってよい層だけが import する新しいファイルに置く」を 1 文足した。
- 「範囲外の食い違い」: 実測を「#3232 に 48 件(2026-10-05 07:20 時点)」に直した。

確認: 3 つの手順書を読むテスト 10 本(223 件)と npm run lint は通った。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…3213 E-3〜E-5) (#3281)

* refactor(polling): checkForResponse の 4 か所を同じファイルの関数に分ける (#3213)

整理 Epic #3207 列 E の手順 E-3。checkForResponse の中の 4 か所を、
response-checker.ts の中の関数 4 つに分けた。振る舞いは変えていない。
tests/ は 1 行も変えていない。新しいファイルは足していない。

- 承認の保存 → savePromptMessage
- 整形 → cleanCompletedResponse
- 重複の再確認 → recheckDuplicateResponse
- 記録と保留 → recordOrHoldScrapedResponse(3 つに分かれる枝だけ。
  前半は下の「分けずに残した部分」)

移し方:

- 行の範囲をスクリプトで切り出して移した。手で打ち直していない。範囲は
  行番号ではなく、行の文面で特定した。移した 302 行は、字下げを除いて元と
  同じ。字下げを無視して数えると、元のファイルにあって今のファイルに無い
  行は 0 行である。逆に、分けた 4 つを呼び出しの位置へ戻すと、元のファイル
  と完全に一致することも確かめた
- 4 つとも、元の行があった位置で 1 回ずつ呼ぶ。DB への書き込み、WebSocket
  への配信、ログ、状態の書き換えは、どれも元と同じ行が同じ順番で行う
- return は書き換えていない。savePromptMessage と recheckDuplicateResponse
  の return は、そのまま checkForResponse の戻り値になる。足した return は
  cleanCompletedResponse の末尾の `return cleanedResponse;` だけ
- 外側の変数を書き換える関数は無い。cleanedResponse は戻り値で返し、
  呼び出し側が const で受ける
- ログ名、正規表現、定数は変えていない

判断したこと:

- 条件は checkForResponse に残した。`if (promptIsLive)` と
  `if (!lineCountIsCursor) { if (isDuplicateResponse(...))` はそのままで、
  中身だけを移した。E-2 は `if` ごと移して null を返す形だったが、ここでは
  早い return の位置と条件を checkForResponse の上で読めるほうを取った。
  null という「続ける」の合図も要らなくなる
- tick ごとに決まる 7 つの値(db, worktree, worktreeId, cliToolId,
  instanceId, resolvedInstanceId, pollerKey)は、ResponseCheckContext という
  1 つのオブジェクトで渡す。E-2 は引数を並べたが、同じやり方だとここでは
  10〜11 個になり、うち 4 つが文字列である。instanceId と
  resolvedInstanceId を取り違えても型エラーにならない。分けた関数は先頭で
  同じ名前に分解するので、移した行は元のまま読める。記録と保留の 3 つの
  真偽値も、同じ理由で ScrapedHistoryDecision にまとめた
- 整形の範囲の先頭 4 行(Claude のメタデータの読み取り)は残した。整形では
  なく、後ろの 2 つの関数も使う値で、一緒に移すと戻り値が 2 つになる
- recheckDuplicateResponse は中に await があるので async にした。呼び出しは
  `return await` で、reject は元と同じく checkForResponse の catch に入る。
  マイクロタスクの切れ目が 1 つ増えるが、位置は return の直前で、その tick
  の書き込みはすべて終わっている

分けずに残した部分(振る舞いを変えずには分けられない):

- 記録と保留の範囲の前半(:2004-2081)。構造化された履歴のゲート
  (`isStructuredHistoryWriterLive(...) || await captureStructuredHistoryTurn(...)`)、
  3 つのフラグ、`await recordClaudeConversation(...)`、承認の回答済みへの
  更新、競合の再確認である
- 理由: await が 2 つ入っている。これを含む関数は呼び出し側が await する
  ことになり、関数の最後の書き込みと、呼び出しの後ろの書き込み(待ちの
  終了、push、カーソルの更新、ポーリングの停止)の間に、元には無い切れ目が
  入る。元のコードは、createMessage / broadcastMessage /
  onRelayTurnCompleted から末尾の stopPolling まで、切れ目なしで走る。
  onRelayTurnCompleted は待たない async 関数を始める
  (src/lib/relay/relay-triggers.ts:70)ので、切れ目が入ると、その続きが
  待ちの終了とカーソルの更新より先に走りうる。構造化された履歴の書き手が
  生きている経路(opencode)では、元は画面の取得から return まで await が
  1 つも無い
- 残した部分は、await を元の位置のまま checkForResponse に置いてある。
  その下の 3 つに分かれる枝は await を含まないので、同期の関数として
  移した。理由は recordOrHoldScrapedResponse のコメントにも書いた

Issue の説明と実コードの食い違い(実コードを正とした):

- 行番号は 4 か所とも −53 行ずれていた(承認の保存 :1493-1599、
  整形 :1607-1647、重複の再確認 :1666-1753、記録と保留 :1778-1933)。
  範囲の中身は Issue の説明と合っていた
- Issue の「重複の再確認」の範囲は `if (!lineCountIsCursor) {` から始まり、
  直前の説明コメント(#1268 / #1670)を含まない。このコメントは `if` に
  付いているので、checkForResponse に残した

測った結果:

- checkForResponse: 645 行 → 363 行(うちコメント 165 行、空行 38 行)。
  複雑度 70 → 48
- extractResponse: 321 行、複雑度 70。この commit では変えていない
- 分けた関数: savePromptMessage 114 行 / 複雑度 8、
  cleanCompletedResponse 44 行 / 8、recheckDuplicateResponse 92 行 / 4、
  recordOrHoldScrapedResponse 87 行 / 6
- 複雑度は `npx eslint --no-eslintrc --parser @typescript-eslint/parser
  --rule 'complexity: [warn, 10]'` で測った(ESLint 8.57.1)
- Issue の受入基準には届いていない。最長の関数は checkForResponse の
  363 行(基準は約 150 行)。複雑度 25 以上の関数は extractResponse(70)
  と checkForResponse(48)の 2 つが残っている

残っていること:

- 記録と保留の前半は、末尾の 28 行(待ちの終了、push、カーソルの更新、
  停止、`return true`。:2089-2116)まで一緒に 1 つの関数にすれば、増える
  切れ目が return の直前だけになり、振る舞いを変えずに移せる。この契約が
  名指しした範囲の外なので、していない
- checkForResponse の前半(セッションの確認から重複の判定まで、約 150 行)
  は、この契約の対象ではない。ここだけで複雑度は 23 ある(手で数えた)

本文に無い指摘: src/lib/polling/response-checker.ts:1865 と :2063 承認を回答済みにする同じ 10 行(markPendingPromptsAsAnswered の呼び出しと `marked-answeredcount-pending` のログ)が、checkForResponse の中に 2 か所ある。まとめていない
本文に無い指摘: src/lib/polling/response-checker.ts:2058 `if (cleanedResponse && ...)` の `cleanedResponse &&` は常に真である。空のときは :1958 で return している。直していない

確認: npx tsc --noEmit は exit 0、npm run lint は exit 0。
npx vitest run は、response-checker / response-poller / checkForResponse /
extractResponse を参照するテスト 109 本と tests/unit/guards を合わせた
127 本(2208 件)がすべて通った。分けた 4 つの関数は、それぞれ先頭で
return するように壊すと、同じ 109 本のうち 10 / 2 / 3 / 15 本が落ちる
(壊したものは戻した)。テスト全体(npm run test:unit)は実行していない。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(polling): extractResponse の残りを同じファイルの関数に分ける (#3213)

整理 Epic #3207 列 E の手順 E-4。extractResponse に残っていた部分を、
response-checker.ts の中の関数 4 つに分けた。振る舞いは変えていない。
tests/ は 1 行も変えていない。新しいファイルも export も足していない。

- 完了の判定 → isTurnComplete
- 返答の行を集める繰り返し → collectCompletedResponse
- 完了の枝の残り(返答の末尾の thinking の判定、起動画面への防御の
  呼び出し、結果の組み立て) → extractCompletedResponse
- 途中経過の切り出し → extractPartialResponse

移し方:

- 行の範囲をスクリプトで切り出して移した。手で打ち直していない。範囲は
  行番号ではなく、行の文面で特定した。移した 187 行は、字下げを除いて元と
  同じ。字下げを無視して数えると、元のファイルにあって今のファイルに無い
  行は 1 行だけである:
  `if (isPromptBasedComplete || isClaudeComplete || isCommandCodeComplete || isOpenCodeDone) {`。
  この条件は、isTurnComplete の末尾の `return ... ;` になった。逆に、分けた
  4 つを呼び出しの位置へ戻すと、元のファイルと完全に一致することも確かめた
- 呼び出す順番は元と同じ。isTurnComplete、extractCompletedResponse、
  extractPartialResponse は、元の行があった位置で extractResponse が呼ぶ。
  collectCompletedResponse は、extractCompletedResponse が先頭で呼ぶ。
  getCliToolPatterns の呼び出しは extractResponse に残した 1 回のままで、
  同じ RegExp を、同じ順番で、同じ回数だけ test する
- return は書き換えていない。extractCompletedResponse と
  extractPartialResponse の return は、そのまま extractResponse の戻り値に
  なる。足した return は 2 つ: isTurnComplete の末尾(上の `if` の条件)と、
  collectCompletedResponse の末尾の `return { response, endIndex };`
- 外側の変数を書き換える関数は無い(E-3 と同じ形)。繰り返しの中で
  書き換える endIndex と responseLines は、collectCompletedResponse の中の
  変数になった。response と endIndex は戻り値で返し、呼び出し側が const で
  受ける
- この関数に await は無い。分けた 4 つも同期の関数である
- ログ名、正規表現、定数は変えていない

判断したこと:

- Issue が挙げた 3 か所だけを分けると、extractResponse の複雑度は 31 で
  止まる。完了の枝(`if` の中)の残りも extractCompletedResponse に移して
  23 にした。`if` は extractResponse に残し、中身だけを移した(E-3 と同じ)
- 完了の判定の 4 つの真偽値は、その `if` の条件でしか使っていない。
  isTurnComplete は 4 つの「または」を返す
- `const cleanOutputToCheck = stripAnsi(outputToCheck);` は extractResponse
  に残した。完了の判定と、完了の枝の suppressOpenCodeBanner の両方が使う
- 途中経過の切り出しは、最後の `return incompleteResult(totalLines);` まで
  一緒に移した。残すと、呼び出し側に「null なら続ける」の `if` が 1 つ増える
- 1 回の呼び出しで決まる 16 個の値は、ExtractionContext という 1 つの
  オブジェクトで渡す(E-3 の ResponseCheckContext と同じ形)。完了の枝は
  このうち 13 個を読む。数値が 5 つ、RegExp が 3 つあり、引数を並べると
  取り違えても型エラーにならない。分けた関数は先頭で同じ名前に分解する
  ので、移した行は元のまま読める。作った後に書き換える値は無い
- 起動画面への防御 5 つ(E-2)は、extractCompletedResponse が元と同じ順番で
  呼ぶようになった。5 つの前にある E-2 のコメントは「extractResponse が
  呼ぶ順に並べた」と書いている。このコメントは書き換えず、新しい 4 つの
  前のコメントに、呼び出し元が変わったことを書いた
- 移したコメントの「above」「below」は書き換えていない。元の
  extractResponse の中での位置を指す。その旨を 4 つの前のコメントに書いた

分けずに残した部分:

- 早い段階の承認の判定と、完了していないときの承認の判定(下の指摘の
  2 か所)、画面の縮みの判定、末尾の空行の切り落とし。Issue が挙げた範囲の
  外で、分けなくても目標(25 未満)に届いたので、触っていない

Issue の説明と実コードの食い違い(実コードを正とした):

- 「321 行、複雑度 70」は、測った値と合っていた
- 「残っているのは、完了の判定、返答の行を集める繰り返し、途中経過の
  切り出し」とあるが、完了の枝には、繰り返しの後ろに 39 行(上の
  「完了の枝の残り」)がある。上のとおり一緒に移した

測った結果:

- extractResponse: 321 行 → 142 行。複雑度 70 → 23
- checkForResponse: 363 行、複雑度 48。この commit では変えていない
  (E-5 は次の commit)
- 分けた関数: isTurnComplete 55 行 / 複雑度 19、
  collectCompletedResponse 75 行 / 14、extractCompletedResponse 48 行 / 9、
  extractPartialResponse 43 行 / 9
- 複雑度は `npx eslint --no-eslintrc --parser @typescript-eslint/parser
  --parser-options 'ecmaVersion: 2022, sourceType: module'
  --rule 'complexity: [warn, 1]'` で測った(ESLint 8.57.1)。行数は同じ
  やり方で `max-lines-per-function` を指定して測った。設定ファイルは
  変えていない
- この commit の後、複雑度 25 以上の関数は checkForResponse(48)だけである

本文に無い指摘: src/lib/polling/response-checker.ts:1365 と :1399 承認の判定(`lines.join` → `detectPromptWithOptions` → `isNumberedDialogVouched` → `buildPromptExtractionResult`)の同じ中身が、extractResponse の中に 2 か所ある。違いは外側の `if` の条件だけ。まとめていない
本文に無い指摘: src/lib/polling/response-checker.ts:1271 extractResponse の `@returns` は「null if incomplete」だが、null を返すのは新しい出力が無いときだけで、完了していないときは incompleteResult を返す。直していない

確認: npx tsc --noEmit は exit 0、npm run lint は exit 0。
npx vitest run は、response-checker / response-poller / checkForResponse /
extractResponse を参照するテスト 109 本と tests/unit/guards を合わせた
127 本(2208 件)がすべて通った。分けた 4 つの関数は、それぞれ先頭で
return するように壊すと、同じ 127 本のうち 23 / 23 / 23 / 1 本が落ちる
(壊したものは戻した)。テスト全体(npm run test:unit)は実行していない。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(polling): checkForResponse の画面の読み取りと、記録から末尾までを関数に分ける (#3213)

整理 Epic #3207 列 E の手順 E-5。checkForResponse の中の 2 か所を、
response-checker.ts の中の関数 2 つに分けた。振る舞いは変えていない。
tests/ は 1 行も変えていない。新しいファイルも export も足していない。

- 前半のうち、画面の取得より後ろ(障害の通知、Layer 2 への蓄積、
  extractResponse、完了していないときの承認の更新、カーソルによる重複の
  判定) → extractCompletedTurn
- 記録と保留の前半から、末尾の return まで(構造化された履歴のゲート、
  3 つのフラグ、会話ログ、承認の回答済みへの更新、競合の再確認、
  recordOrHoldScrapedResponse の呼び出し、待ちの終了、push、カーソルの
  更新、停止) → recordCompletedResponse

移し方:

- 行の範囲をスクリプトで切り出して移した。手で打ち直していない。範囲は
  行番号ではなく、行の文面で特定した。移した 204 行(68 行と 136 行)は、
  字下げを除いて元と同じ。字下げを無視して数えると、元のファイルにあって
  今のファイルに無い行は 4 行で、どれも下の「書き換えたコメント」の行で
  ある。逆に、分けた 2 つを呼び出しの位置へ戻し、ctx とコメントを元に
  戻すと、元のファイルと完全に一致することも確かめた
- extractCompletedTurn は同期の関数である。画面の取得の await の次の行から
  始まり、中に await が無いので、呼び出しで切れ目は増えない。中の 3 つの
  `return false;` は元の行のまま。戻り値の型は `CompletedTurn | false` で、
  checkForResponse は false を受けたらそのまま `return false` する。3 つの
  早い return の条件と順番は変わらない。足した行は、呼び出し側の
  `if (!turn) { return false; }` と、関数の末尾の
  `return { result, isFullScreenTui, lineCountIsCursor };`
- recordCompletedResponse は async である。中の await 2 つ
  (captureStructuredHistoryTurn と recordClaudeConversation)は元の位置の
  まま。呼び出しは `return await` で、reject は元と同じく checkForResponse の
  catch に入る。増える切れ目は 1 つで、位置は return の直前である(競合の
  `return false` でも、末尾の `return true` でも)。その tick の書き込みは
  すべて終わっている。Issue の見立てのとおりだった
- await は、ファイル全体で 8 個から 9 個になった。増えたのは上の
  `return await` だけ
- 外側の変数を書き換える関数は無い(E-3 と同じ形)。result、
  isFullScreenTui、lineCountIsCursor の 3 つは CompletedTurn として戻り値で
  返し、呼び出し側が const で受ける。recordCompletedResponse には同じ
  オブジェクトを渡す。真偽値 2 つを引数に並べない
- ctx(ResponseCheckContext)を作る 10 行を、`if (promptIsLive)` の前から、
  pollerKey の直後へ動かした。extractCompletedTurn に渡すためである。値を
  束ねるだけの行で、関数の呼び出しは含まない
- ログ名、正規表現、定数は変えていない

分けずに残した部分(振る舞いを変えずには分けられない):

- 前半のうち、セッションの確認から画面の取得まで(:2184-2263、80 行、
  分岐 5 つ)
- 理由: await が 4 つ入っている(isSessionRunning、
  settleExpiredPendingScrapedResponse、probeGeometryDelegation、
  captureSessionOutput)。これを含む関数は呼び出し側が await することに
  なり、画面を取得した直後、その画面を使う処理の前に、元には無い切れ目が
  入る。位置が return の直前ではないので、Issue が後半について認めた形
  (増える切れ目が return の直前だけ)に当たらない。3 つ目の await は
  条件つきで(isLiveAttachEligibleSession が偽なら待たない)、その手前で
  切っても同じことになる
- 複雑度の目標には、この部分を分けなくても届いた

判断したこと:

- extractCompletedTurn が「ここで終わり」を返す値は、null ではなく false に
  した。null にすると、移した 3 行の `return false;` を書き換えることに
  なる。E-2 の null は「続ける」の合図なので、逆の意味で使うことも避けた
- 中ほど(承認の判定から、内容による重複の判定まで)は、Issue が挙げた
  範囲の外なので、触っていない

書き換えたコメント(2 か所。どちらも E-3 が足した説明で、計測の記録では
ない):

- ScrapedHistoryDecision の説明: 3 つのフラグを決める場所を
  `{@link checkForResponse}` から `{@link recordCompletedResponse}` にした
- recordOrHoldScrapedResponse の説明: 「決めるものは checkForResponse に
  残した」を、「呼び出し元の recordCompletedResponse に、後ろの書き込みと
  一緒にある」にした
- 理由: 行を移した結果、書いてあることが事実でなくなる。E-3 が「残した」
  理由(await を含む関数にすると、後ろの書き込みとの間に切れ目が入る)は
  そのまま残し、末尾まで一緒に移した今の形に合わせた
- 移した行の中のコメントは 1 行も書き換えていない。「above」「below」は、
  元の checkForResponse の中での位置を指す(E-3 が関数の並びの前に書いた
  コメントのとおり)

Issue の説明と実コードの食い違い(実コードを正とした):

- 「363 行、複雑度 48」は、測った値と合っていた
- 「前半は約 150 行、そこだけで複雑度が 23」は、138 行で、1 つの関数に
  したときの複雑度は 22 になる(取得までの分岐 5、取得より後ろ 16、関数
  そのものの 1)。移したのは後ろの 68 行だけ

測った結果:

- checkForResponse: 363 行 → 167 行。複雑度 48 → 21
- extractResponse: 142 行、複雑度 23。この commit では変えていない
  (E-4 の commit で 321 行 / 70 から変えた)
- 分けた関数: extractCompletedTurn 79 行 / 複雑度 17、
  recordCompletedResponse 147 行 / 13
- 複雑度 25 以上の関数は、このファイルから無くなった
  (`complexity: [warn, 24]` で警告 0 件)。大きい順に、extractResponse 23、
  checkForResponse 21、isTurnComplete 19、extractCompletedTurn 17
- 最長の関数は checkForResponse の 167 行で、Issue の受入基準「約 150 行」
  より 17 行多い。次は recordCompletedResponse 147 行、extractResponse
  142 行。振る舞いを変えずにあと減らせるのは、`if (delegation.released)` の
  中身(:2237-2253、17 行、await なし)を関数に出すことで、約 150 行に
  なる。複雑度の目標には要らず、Issue が挙げた範囲でもないので、
  していない
- 測り方は E-4 の commit と同じ(ESLint 8.57.1 の `complexity` と
  `max-lines-per-function` を、コマンドの引数で指定した。設定ファイルは
  変えていない)

本文に無い指摘: src/lib/polling/response-checker.ts:2198 コメントの「this return is ~300 lines above the save path」は、E-3 と E-5 で保存の経路が別の関数に移ったので、行数としては合わなくなっている。書き換えていない
本文に無い指摘: src/lib/polling/response-checker.ts:1570 1 つ目の重複の判定の `!sessionState?.inProgressMessageId` は、ログを出すかどうかにしか効いていない。この条件で 1 つ目を通り抜けても、すぐ下の 2 つ目の判定(`<=`)が同じ false を返す。直していない

確認: npx tsc --noEmit は exit 0、npm run lint は exit 0。
npx vitest run は、response-checker / response-poller / checkForResponse /
extractResponse を参照するテスト 109 本と tests/unit/guards を合わせた
127 本(2208 件)がすべて通った。分けた 2 つの関数は、それぞれ先頭で
`return false` するように壊すと、同じ 127 本のうち 23 / 17 本が落ちる
(壊したものは戻した)。テスト全体(npm run test:unit)は実行していない。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(hooks): 使われていない関数と小さな重複を消す (#3222)

N-5。git grep -w で src/ tests/ scripts/ に宣言以外の言及が無いことを再確認して削除:
decisionRecheckTarget(と専用だった CLIToolType import)、readCopilotSettings、
getOpencodeV2SubscribedPort。
define-source.ts の normalizeEvent 2 つは中身が同一だったため normalizeSpecEvent に統合。
ApiClient.post/patch は同一で、sendWithBody に共通化(get は response.json() のため据え置き)。
getHeaders() は try の外で評価するようになるが、例外を投げないので挙動は同じ。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* refactor(hooks): 設計文書が名指しする readCopilotSettings は消さずに残す (#3222)

docs/design/multi-agent-state-architecture.md が readCopilotSettings を名指ししており、
design-doc-identifier-audit が落ちたため、copilot/hook-settings.ts を origin/develop に戻した。
N-5 でこのファイルに行った変更は readCopilotSettings の削除だけだった。
decisionRecheckTarget と getOpencodeV2SubscribedPort は、docs/(module-reference.md・
implementation-history.md を除く)に言及が無いため、削除のまま。
文書を直すかどうかは別の Issue で決める。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…4) (#3282)

* refactor(cli-tools): 起動の前処理を BaseCLITool の関数にまとめる (#3220)

L-3: launchSession 冒頭の写しを、BaseCLITool の 3 つの関数の呼び出しに置き換えた。
- requireInstalled(): isInstalled() が false なら missingToolError(this) を投げる
- resolveLaunchPane(worktreeId, worktreePath, instanceId, {logger, liveAction, relaunchAction}):
  getSessionName → hasSession →(在れば)reconcileExistingSession →
  isToolLive({confirm: true}) → ログ、までを行い {sessionName, exists, live} を返す
- createLaunchPane(sessionName, worktreePath): createSession →
  TUI_SESSION_CREATE_WAIT_MS の待ち

共通にできたツール:
- requireInstalled: gemini / antigravity / command-code / vibe-local / codex
- resolveLaunchPane: 上の 5 つ + copilot
- createLaunchPane: 8 ツールすべて(opencode / opencode-v2 を含む)

共通にしなかった部分(理由):
- copilot の導入の確認: resolveCopilotExecutable() の結果を起動行に使うので、今のまま。
- opencode / opencode-v2 の導入の確認と再利用の分岐: 実行ファイルの解決結果を起動行に使う。
  reconcileExistingSession に画面の大きさを渡し、生きていれば resume…EventStream、
  終了していれば release… を挟む。opencode の幅は resolveOpencodePaneWidthChecked() が
  警告ログを出すので、引数にして先に評価すると、新規作成の経路でログが 1 行増える。
- beginAgentSession: 各ツールに残した。直前のコメントが 6 ツールで別々の経緯
  (#1762 / #2251 / #1759・#2444 / #1760 / #1761)を持ち、「上の再利用の分岐は既に
  return 済み」という位置の説明になっている。1 行を base に移すと、コメントを書き換えるか
  捨てることになる。command-code は直後に composerPendingSince.set が入る。
  呼ぶ位置は変えていない。このため base.ts は agent-session-lifecycle を import していない。

変えていないもの: await の順番、ログ名とログを出す logger(各ツールの logger を引数で渡す)、
待ち時間、例外の型と文言。createLaunchPane は各ツールの try の中で呼ぶので、createSession の
失敗は従来どおり「Failed to start <tool> session: …」になる。

base.ts が新しく import した名前: createSession(tmux/tmux)、TUI_SESSION_CREATE_WAIT_MS
(config/cli-tool-timing-config)、missingToolError(./install-hints)、Logger(型のみ)。
前の 2 つは、起動を通すテストの差し替えが既に持っている。install-hints を差し替えている
テストは無い。tests/ は変えていない。

コメント: 文面が同じだったもの(#2070 の再利用の分岐、Create tmux session、Wait a moment、
Check if session already exists)は base に移した。codex と command-code の #2070 の
コメント、antigravity / codex / command-code の createSession のコメントは文面が違うので、
呼び出しの上にそのまま残した。

Issue との食い違い: Issue は copilot を「並びが同じ 6 ツール」に入れているが、導入の確認は
別の形だった(上記)。実コードを正とした。差分は 216 行減、160 行増(うち base.ts が +96)。

本文に無い指摘: src/lib/cli-tools/gemini.ts:83 src/lib/cli-tools/codex.ts:250
  src/lib/cli-tools/vibe-local.ts:62 src/lib/cli-tools/opencode.ts:375 生きている場合の
  ログ名が *-session-sessionname / vibe-local-session で、他の 4 ツールの
  *-session-exists と揃っていない。
本文に無い指摘: src/lib/cli-tools/opencode.ts:350 と :428 起動し直しの経路で
  resolveOpencodePaneWidthChecked() が 2 回評価され、幅の警告ログが 2 回出る。
本文に無い指摘: src/lib/cli-tools/opencode-v2.ts:255 警告を出さない
  resolveOpencodePaneWidth() を使っていて、CM_OPENCODE_PANE_WIDTH が退けられても何も出ない。

確認: npx tsc --noEmit、npm run lint、変えたモジュールか manager を import するテストと
guards の計 204 ファイル(2755 件)が通過。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(cli-tools): 停止の枠を BaseCLITool の requestExitAndKill にまとめる (#3220)

L-4: 6 ツール(gemini / vibe-local / codex / antigravity / command-code / copilot)の
killSession に同じ形で書かれていた並びを、BaseCLITool の 1 つの関数にした。

- requestExitAndKill(worktreeId, instanceId, {logger, stoppedAction, requestExit, afterKill?}):
  getSessionName → try → hasSession →(在れば)requestExit(sessionName) →
  tmux の killSession → afterKill?.(sessionName) → killed が真なら
  logger.info(stoppedAction) → catch で logger.error('session:stop-failed') して投げ直す
- 各ツールの killSession は、この関数に requestExit(キーの並びと待ち時間)を渡して呼ぶ。
  antigravity は afterKill で invalidateCache、command-code は afterKill で
  composerPendingSince.delete → invalidateCache(順番はそのまま)。

変えていないもの: キーの並び、待ち時間、tmux の呼び出しと await の順番、ログ名
(stopped-<tool>-session…。ツールごとの文字列のまま引数で渡す)、ログを出す logger
(各ツールの logger を引数で渡す。session:stop-failed も同じ logger)、例外
(投げ直すだけ)。afterKill は同期の関数として await せずに呼ぶので、killSession の
後・ログの前・try の中という位置は前と同じ。getSessionName は前と同じく try の外。
関数に分けたぶん、async 関数の境界が 2 つ増えている(killSession → requestExitAndKill →
requestExit)。tmux の呼び出しとタイマーの並びは同じ。

共通にしなかったツール(形が違う):
- claude: killSession は stopClaudeSession(worktreeId, instanceId) を呼ぶだけ。
  停止の並びは session-key-sender 側にある。
- opencode: try の前に、割り当てポートの取得、セッションの記憶の採取、
  releaseOpencodeEventStream がある。/exit の後に verifyGracefulExit で
  「セッションが消えた・ポートが応答しない」を確かめ、満たさないときだけ tmux の
  killSession を呼ぶ(セッションが無いときは呼ぶ)。戻り値の killed を見ず、
  停止のログを常に出す。
- opencode-v2: opencode と同じく verifyGracefulExit の結果で killSession を呼び分け、
  finally で releaseOpencodeV2Server を呼ぶ。停止のログを常に出す。

base.ts が新しく import した名前: killSession(tmux/tmux)、getErrorMessage(../errors)。
関係するテストのうち @/lib/tmux/tmux を差し替えているのは 94 本。importOriginal で
実物を広げているものが 7 本、killSession を持つものが 53 本、持たないものが 34 本。
持たない 34 本はどれもツールの killSession を呼ばないので、そのまま通る
(差し替えに無い名前は、読んだときにだけ失敗する)。lib/errors を差し替えている
テストは無い。tests/ は変えていない。

コメント: 「Kill the tmux session」(command-code 以外の 5 ツールで同じ文面)は base に
移した。キーと待ち時間のコメント、antigravity と command-code の invalidateCache の
コメントは、文面を変えずに requestExit / afterKill の中へ移した。codex の
「Send Ctrl+D to exit Codex gracefully」は hasSession の上にあったので、requestExit の
上に置いた。各 killSession の docblock はそのまま。

Issue との食い違い: Issue は「ツールごとの requestExit() を呼ぶ形」と書いているが、
メソッドにすると claude / opencode / opencode-v2 にも実装か既定が要る。L-3 と同じく、
呼び出し側が引数(requestExit)で渡す形にした。行番号は L-1〜L-3 でずれていたので、
名前で特定した。差分は 119 行減、105 行増(うち base.ts が +60)。

本文に無い指摘: src/lib/cli-tools/base.ts:859 この 6 ツールは tmux の killSession が
  真を返したときだけ停止のログを出す。src/lib/cli-tools/opencode.ts:990 と
  src/lib/cli-tools/opencode-v2.ts:423 は常に出す。

確認: npx tsc --noEmit、npm run lint、変えたモジュール・manager を import するテストと
killSession / kill-session / cli-tools に触れるテスト、guards の計 406 ファイル
(7184 件、skip 1)が通過。枠の requestExit と afterKill の呼び出しを外すと、
graceful-exit-conformance-1933 / antigravity / command-code / copilot の 12 件が
落ちることを確かめ、元に戻した。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…#3221) (#3283)

M-3: resolve*SessionId の本体を resolveSessionIdFromEvents に、
accept*Path の本体を acceptPathUnderRoot に寄せた。ログ接頭辞(イベント名)、
latch の Map、ルートと拡張子は呼び出し側から渡す。
M-4: claude と command-code の growTurnRow / refreshTurnRows を
growTurnRowTo / refreshTurnRowsTo に寄せた(antigravity は含めない)。
各 history.ts には同名・同引数の薄い関数を残した。ロガーは引数で渡し、
モジュール名・イベント名・欄・動的 import の位置は変えていない。
差異: 本文に無い食い違いは無し。antigravity の resolve は名前が
resolveAntigravityConversationId、変数名のみ conversationId で、処理は同じ。

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…に寄せる (#3218) (#3284)

正規表現の一覧と条件が prompt-view.ts と完全に同一であることを確認。
isTypedTextFieldOption の export・型は維持。detection→session を禁じるガードは無し。
計測の記録コメントは isTypedTextFieldOption の JSDoc へ書き換えずに移した。

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* refactor(detection): ツール別の検出パターンを Record<CLIToolType, …> の表 1 つにまとめる (#3230)

detectThinking と getCliToolPatterns の switch を、cli-patterns.ts 内の表から読む形にした。
引数・戻り値・ログ・正規表現・skipPatterns の並びは変えていない。skipPatterns は呼び出しごとにコピーを返す。
知らない cliToolId は従来の default と同じく claude の行を使う。表の正規表現に g/y フラグは無く、
detectThinking の正規表現は getCliToolPatterns の thinkingPattern と全ツールで同じ定数だった。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* refactor(detection): buildDetectPromptOptions と FULL_FRAME_PROMPT_TOOLS を同じ表から読む (#3230)

V-2。CLI_TOOL_PATTERN_TABLE に promptOptions / fullFramePrompt の欄を足し、
buildDetectPromptOptions は表から読んで毎回浅いコピーを返す。
run-detection.ts の FULL_FRAME_PROMPT_TOOLS は、cli-patterns.ts が export する
usesFullFramePrompt() の呼び出しに置き換えた。分岐・集合に付いていた計測コメントは
書き換えず、該当ツールの行へ移した。

確認方法: 置き換え後に、9 ツール全部で buildDetectPromptOptions の戻り値
(claude/copilot/antigravity は { requireDefaultIndicator: false }、opencode は
{ requireDefaultIndicator: false, hasNumberedDialogs: false }、他は undefined)と
集合の所属(opencode/codex/claude/copilot のみ true)が置き換え前と一致すること、
呼ぶたびに別オブジェクトであること、未知の id('constructor' 含む)で
undefined / false になることを使い捨てテストで確認した(テストは commit しない)。
FULL_FRAME_PROMPT_TOOLS を名指すテスト・ガードは無く、run-detection.ts から
cli-patterns.ts への依存を禁じるガードも無い(既存の import が元からある)。
lint / tsc / 検出系 118 ファイルのテストは通過。

本文に無い指摘: src/lib/detection/cli-patterns.ts buildDetectPromptOptions の JSDoc の
「currently 6」は実際の 9 ツールと食い違う(直していない)。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* refactor(detection): 表にない id は own-property の確認で弾く (#3230)

表はふつうのオブジェクトなので、'constructor' や '__proto__' のような名前が
実行時に渡されると Object.prototype のものが返り、patternRowFor の
thinkingPattern.test が例外になっていた(元の switch は default で claude の規則)。
表の行を取り出す ownRowFor() 1 つに own-property の確認をまとめ、
patternRowFor(表になければ claude の行)・buildDetectPromptOptions(undefined)・
usesFullFramePrompt(false)の 3 つがそれを使う形にした。

確認(使い捨てテスト、commit しない): 'constructor' / 'toString' /
'hasOwnProperty' / '__proto__' / 'no-such-tool' を渡すと、getCliToolPatterns と
detectThinking は claude を渡したときと同じ結果で例外を投げず、
buildDetectPromptOptions は undefined、usesFullFramePrompt は false。
9 ツールの promptOptions・集合の所属・thinkingPattern は手直し前と同じ。
lint / tsc / 検出系 118 ファイルのテストは通過。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* refactor(session): current-output の応答の形に名前を付け、フックの型を Pick<> にする (#3229)

U-1: ルートが返す形を CurrentOutputResponseBody(CurrentOutputPayload + agentMode + 省略可の detector)
として current-output-types.ts に定義。ルートは satisfies で確かめるだけで、式は変えていない。
U-2: useTerminalPanePolling.ts の手書き CurrentOutputResponse を Partial<Pick<>> & {...} に置換。
欄の比較(名前 / 省略可 / 型、置換前後で同一):
 isRunning, cliToolId, sessionStatus, isPromptWaiting, promptAnswerable, fullOutput,
 realtimeSnippet, thinking, isSelectionListActive, isPagerActive, isDismissablePanelActive,
 isUnclassifiedActive, startingSince: すべて省略可、型同一(payload 側で必須のものは Partial で省略可に)。
手書きのまま残した欄:
 promptData(LivePromptData は payload の PromptData | StructuredPromptWaitingData | null と型が違う)、
 structuredEvents(この画面が読む部分だけの狭い形で、payload の型と違う)。
isGenerating? は同ファイル内で読まれていないことを grep で確認して削除。
TerminalSnapshotEvent(realtime/types.ts)は触っていない。
useWorktreeDetailController.ts は #3227 が変更中のため触っていない。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* refactor(session): useWorktreeDetailController の手書きの応答型を Pick<> にする (#3229)

CurrentOutputResponse を CurrentOutputResponseBody からの Pick<> に置き換えた。型だけの変更で、
実行されるコードは変えていない。

欄の比較(名前 / 省略可 / 型): 置き換え前後で同一。
- isRunning: 必須 boolean のまま(Pick)
- cliToolId, isPromptWaiting, promptAnswerable, fullOutput, realtimeSnippet, thinking,
  isSelectionListActive, isPagerActive, isDismissablePanelActive, isUnclassifiedActive,
  sessionStatus, startingSince: 省略可、型は同じ(Partial<Pick<>>)。
  sessionStatus は本体では必須 string なので Partial で省略可にして手書きに合わせた。

手書きのまま残した欄:
- promptData: LivePromptData で読むため(本体の型と別)
- content: 本体では必須、手書きは省略可
- agentMode: 本体は AgentMode、手書きは string
- autoYes: stopReason の型と lastSuppression の必須が違う
- lastServerResponseTimestamp, serverPollerActive: 本体の autoYes 側の欄ではなく
  トップレベルの欄で、手書きの形を保つため
各欄の説明コメントは書き換えずに残した。

isGenerating? は同ファイル内を grep して読まれていないことを確かめて消した。
changelog.d/3229.md は両方のフックを表す文にした。

本文に無い指摘: なし

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
… W-1〜W-5) (#3287)

* refactor(state): globalThis.__* の初期化を getOrInitGlobal にまとめ、skills で試す (#3231)

src/lib/global-state.ts を新設(import なし)。src/lib/skills/ の 7 ファイルの
`x ?? (x = init)` / `x ??= init` を getOrInitGlobal('__name', () => <元の式>) に置換。
declare global は各ファイルに残し、初期化は読み込み時のまま。

型の確認(作業用、コードは残さない): 存在しない名前 '__noSuchName' は TS2345、
型が合わない初期値 { timer: 5 } は TS2322 になることを確認した。
git diff --shortstat origin/develop -- src/lib/skills は 15 insertions / 16 deletions(増えていない)。

本文に無い指摘: なし

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* refactor(state): globalThis.__* の初期化を残りのディレクトリで getOrInitGlobal に置換 (#3231)

src/ の 53 ファイル・85 か所の `x ?? (x = init)` / `x ??= init` / `if (!x) { x = init }` を
getOrInitGlobal('__name', () => <元の式>) に置換。declare global は残し、初期化は読み込み時のまま
(遅延の取得関数は関数内で呼ぶ形のまま)。初期値がオブジェクトリテラルの 5 か所は `() => ({...})` とした。
`if (!x)` 形は schedule-manager / timer-manager(オブジェクト)、claude-executor(Map)のみで、偽になるのは null/undefined のみ。

残した箇所(別 PR が変更中のため後の手順で行う):
- src/lib/hooks/sources/antigravity/history.ts:170, :217
- src/lib/hooks/sources/claude/history.ts:182
- src/lib/hooks/sources/codex/history.ts:141, :152
- src/lib/hooks/sources/command-code/history.ts:165, :175
- src/lib/hooks/sources/opencode-v2/subscription.ts:101, :107
置換しなかった「触らないもの」: 読み取り・代入・undefined 初期化し直し(tmux-capture-cache.ts:264 等)、
タイマー・購読解除・旗(resource-cleanup / relay-delivery:203 / waiting-push-notifier の timer・unsubscribe 等)。
`git grep` の `??=` / `?? (` 検索の残りは上の除外 7 か所と一致。

本文に無い指摘: なし

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs(changelog): 断片を列全体の内容にする (#3231)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* refactor(state): hooks/sources の初期化 9 か所を getOrInitGlobal に置き換える (#3231)

W-5。antigravity/history.ts(2)、claude/history.ts(1)、codex/history.ts(2)、
command-code/history.ts(2)、opencode-v2/subscription.ts(2) の
`globalThis.__X ??= new Map<...>()` を `getOrInitGlobal('__X', () => new Map<...>())` にした。
declare global・プロパティ名・初期化のタイミングは変えていない。

`git grep -nE "globalThis\.__[A-Za-z0-9]+ *(\?\?=|\?\? *\()" -- src` の結果: 0 件。

本文に無い指摘: なし

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
)

* docs(comments): 整理で事実と合わなくなったコメントを直す (#3207)

整理 Epic の PR は「コメントを書き換えない」決まりだったので、移した先を
指していないコメントが残っていた。合わなくなった文だけを直した。
コードの行は 1 行も変えていない。

- prompt-waiting-composition.ts: isApiAnswerableStructuredWait は、今は
  current-output-builder の composePromptData が呼んでいる (#3215)
- current-output-builder.ts: #2031 の 4 つの条件は、今は promptWaiting !== null と
  isApiAnswerableStructuredWait の中の 3 つ
- current-output-builder.ts: #2040 の「this function」は buildPayload のこと。
  今は readAgentEvents が読む
- response-checker.ts: 保存の経路は、今は別の関数 recordCompletedResponse
- prompt-view.ts: TYPED_TEXT_FIELD_LABEL_PATTERNS は写しではなく唯一の定義 (#3218)
- prompt-detect-multiple-choice.ts: {@link TYPED_TEXT_FIELD_LABEL_PATTERNS} の
  指す先を prompt-view にした。同じコメントの「PromptPanel.tsx に再掲」も
  #3218 で事実と合わなくなっていたので直した(依頼の 8 か所に無い 1 文)
- tools/claude/patterns.ts: Claude の規則は #3217 でこのファイルへ移った
- cli-patterns.ts: ツールは 9 つで、表への移行は #3230 で済んだ

確認: git diff -U0 origin/develop -- src の変更行をスクリプトで調べた。
7 ファイル・53 行のすべてが、コメントの行(//、/**、 *、*/ で始まる行)だった
(コメントでない行は 0)。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(module-reference): モジュール一覧を、整理 Epic の後の実物に合わせる (#3207)

1. 新しいモジュールの行を 36 本足した。
   git diff --diff-filter=A 5d5592b origin/develop -- src の 35 本と、
   列 W(#3287、未マージ)の src/lib/global-state.ts。1 ファイル 1 行で、
   近いモジュールの行の隣に置いた。どの行も、実物のファイルを読んで書いた。
   追記案(dev-reports/)にあった useSessionKill.ts / useFileTreeOperations.ts /
   hook-settings-shared.ts は develop に無いので足していない。
2. 消したファイルの行を 4 本消した(TreeContextMenu.tsx、external-apps/index.ts、
   external-apps/interfaces.ts、proxy/index.ts)。ほかの行に、消した 13 ファイルの
   言及は残っていない。
3. 中身が移ったモジュールの行を直した。
   - cli-patterns.ts: ツール別の規則は tools/<tool>/patterns.ts、ツールに依らない
     ものは shared/ へ。残るのは表 CLI_TOOL_PATTERN_TABLE と 4 つの関数
     (依頼文は 3 つ。#3230 が usesFullFramePrompt を足したので 4 つ)
   - current-output-builder.ts: 型・進み具合・2 層の合成・履歴の行を 4 ファイルへ。
     StatusEvidence の再 export は #3215 で消えていた
   - response-checker.ts: 保留中の画面読み取りは pending-scraped-response.ts へ
   - wait.ts: AutoYesSuppressionReason は「ミラー」ではなく同じ宣言 (#3216)。
     pollWorktree は段階ごとの関数に分けた (#3223)
   - selection-shape.ts: Command Code の規則と共有の正規表現を 2 ファイルへ (#3218)
   - 移した名前を「ここにある」「再掲」と書いていた行も直した: PromptPanel.tsx、
     MobilePromptSheet.tsx、WorktreeDetailRefactored.tsx、claude/{patterns,detect}.ts、
     claude/history.ts、prompt-detector.ts、auto-yes-resolver.ts、status-evidence.ts、
     send.ts、useWorktreeDetailController.ts(#3211 が消した displayedAgents)
4. 冒頭の globalThis の規約: 見本を getOrInitGlobal(src/lib/global-state.ts、
   #3231)にした。declare global は今までどおり要る、と書いた。

表の形: 足した行・直した行は、すべて行末のパイプがあり、説明に生のパイプを
足していない。同じパスの行は無い(スクリプトで確認)。

確認: npx vitest run tests/unit/docs tests/unit/guards(37 ファイル・576 件)、
module-reference.md と orchestrate.md を読むテストと changelog-fragments のテスト
(21 ファイル・622 件)、node scripts/changelog-fragments.mjs check、npm run lint、
npx tsc --noEmit。すべて成功。

前提: 4 と global-state.ts の行は、#3287 がマージされてから正しくなる。

未対応: docs/en/module-reference.md 契約の scope.allow の外なので変えていない
(日本語版とのずれが、この変更の分だけ広がる)。
未対応: docs/module-reference.md の cli-patterns.ts の行 同じ Issue の記述が
2 回ずつ入っている箇所がある(#379、#988、#1671、#1829、#1883、#1885、#1886、
#1893、#1897、#1911)。整理 Epic より前からのもので、どちらを残すかを決められ
ないので、そのままにした。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(orchestrate): 名前を消す手順にも、設計文書の確認を広げる (#3207)

2-4-3 の「ファイルを消す手順」の決まりを、関数・定数・型などの名前を消すときにも
使う、と足した。PR を出す前に
npx vitest run tests/unit/docs/design-doc-identifier-audit.test.ts を実行する。

実測: N-5(#3222)は、消した未使用の関数 readCopilotSettings を
docs/design/multi-agent-state-architecture.md が名指ししていて、同じテストで落ちた。
その関数は元に戻した。

この節のほかの文は変えていない。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* fix(hooks): ターンの開始を受けたら、そのセッションの stop を重複の窓から外す (#3289)

自分で再開した短いターンの `stop` が、前のターンの `stop` の写しとして捨てられ、
2 つ目のターンが開いたままになって `commandmate wait` が完了を返さなかった
(実測 2026-10-05 / claude 2.1.289: 1 つ目の stop の 540 ms 後に
user_prompt_submit、1473 ms 後に 2 つ目の stop)。

原因は本文のとおり。`isDuplicateAgentEvent` のキーは「インスタンス・イベント・
細目・セッション id」で、ターンを表すものが無い。セッション id はターンを
またいで同じなので、同じセッションの stop は 3 秒以内なら別のターンのものでも
同じキーになる。

## 直し方(案 2 を採った)

`isDuplicateAgentEvent` が、ターンの開始のイベントを「重複でない」と判定したとき、
同じインスタンス・セッションの `stop` のキーを、細目を問わずに消す
(`releaseStopClaims`)。次の `stop` は 1 回目の配送として通り、その写しは
今までどおり捨てられる。新しい状態は足していない。

案 1(`stop` のキーにターンを含める)を採らなかった理由:

- ターンを表す印(セッションごとの番号)を持つ Map がもう 1 つ要る。その印は、
  それを使ったキーより長く残さなければならない(窓の中で印が先に消えると、
  写しが別のキーになって適用される)。上限・掃除・テスト用の口が 2 組になる。
- 判定の結果は案 2 と同じになる。下の 2 の並び(遅れて届いた写し)は、
  案 1 でも区別できない。
- 印を時刻で持って「stop を見た時刻」と比べる形は、同じミリ秒に届いた
  イベントの順序を決められない。案 2 は消すだけなので、届いた順で決まる。

## 確かめたこと

1. 二重配送(#1722)。`stop, stop, user_prompt_submit, user_prompt_submit,
   stop, stop` で、適用は 1・3・5 番目、捨てるのは 2・4・6 番目になる。
   消すのは「重複でない」と判定したターンの開始だけなので、4 番目は何も
   消さない。単体テストと受信ルートのテストの両方で固定した。

2. `stop(A) → user_prompt_submit(B) → stop(A の写し)`。写しを B の終了と読む
   (B が早く閉じ、`wait` が 1 ターン早く返る)。本物の stop(B) が窓の中に
   届けば、そちらが写しとして捨てられる。
   - 注入した設定と、手順書どおりの手動設定では、この並びは届かない。
     Claude Code は Stop の hook の応答を待ってから次のターンへ進む(実測の
     ログで、user_prompt_submit の行は stop を適用した行の 29 ms 後、stop の
     到着の 540 ms 後)。注入する hook は `type: "http"` で、http に async は
     無い(docs/design/agent-hooks-live-verification.md §5.3.6)。手動設定
     (docs/user-guide/agent-event-hooks.md §3)は async なしの command で、
     中継の scripts/hooks/cmate-agent-event.sh は curl をフォアグラウンドで
     実行し、応答か --max-time を待って終わる。受信ルートは、キーの確保を
     `await applyAgentStopEvent` より前に同期で行う。
   - 届きうるのは、利用者が command の hook に `"async": true` を書いた場合
     (同 §5.3.6 で「待たない」と実測済み)。
   - 比べた結果。早すぎる完了は、既定でない設定のときだけ起き、1 ターン早く
     返るだけで済む。今回の不具合は、CommandMate が注入する設定で起き、
     `wait` が 30 分(`TURN_STALE_AFTER_MS`)か --timeout まで返らない。
     後者を直すほうを選んだ。
   - 写しには区別の材料が無い。遅れうる写しは中継のもので、中継は
     tool / event / cwd / sessionId だけを送る。

3. ターンの開始は `TURN_ACTIVITY_EVENTS`(user_prompt_submit / pre_tool_use /
   post_tool_use)で見る。サーバーがターンを開くのがこの 3 つなので、
   「サーバーが開いたターンが、写しとして捨てた stop のせいで開いたままに
   なることは無い」が成り立つ。user_prompt_submit だけにすると、antigravity
   (post_tool_use で開く)と Command Code(pre_tool_use で開く)に同じ
   不具合が残る。
   - ツールのイベントを含めたことで今より悪くなりうるのは、二重配送の環境で、
     1 つの stop の 2 つの写しの間に、同じセッションのツールのイベントが
     割り込んだときだけ(写しが適用され、agent_idle が 1 行増える。#1722 より
     前の動き)。hook は同期なので、割り込めるのは並行して動くもの
     (バックグラウンドのサブエージェントなど)に限られると考える。
     実測はしていない。
   - 重複として捨てたターンの開始は、何も消さない(サーバーはそのターンを
     開いていないので、開始と終了を一緒に適用するか、一緒に捨てる)。
   - セッション id の無いターンの開始は、何も消さない(今までどおり)。
   - 別のセッション・別のインスタンスのターンの開始は、消さない。

4. opencode 系。`classifyAgentEventDelivery` のルール 1・2 は変えていない。
   id を宣言するソースの stop は窓のキーを確保しないので、消す対象が無い。
   opencode-event-dedup-1899.test.ts と opencode-push-2045.test.ts は、
   書き換えずに通る。

5. 上限。状態を足していない。ターンの開始は、自分のキーを 1 つ足して stop の
   キーを消すだけなので、`MAX_RECENT_EVENT_KEYS` と `pruneRecentEventKeys` が
   そのまま効く。`clearAgentStopEvents` も変更不要。stop のキーを探すために
   Map を 1 回走査する(最大 512 件)。

6. stop 以外のイベントの判定は変えていない(テストで固定した)。

## 本文と実コードの食い違い

- 「`stop` には細目が無い」。antigravity の stop は `self_resume_pending` の
  細目を持つことがある(#2614、
  src/lib/hooks/sources/antigravity/hooks-config.ts:292)。そのため、細目を
  問わずに消している。
- 「呼び出しは 2 つ」。もう 1 つある(scripts/canary/hook-receiver.ts:361)。
  同じ関数を通るので、同じ扱いになる。変更していない。

## 本文に無い指摘

- 本文に無い指摘: src/lib/session/agent-event-state.ts:2754
  (`isDuplicateAgentEvent`)user_prompt_submit 自身も 3 秒の窓にかかる。
  前のターンの開始から 3 秒以内に次のターンが始まると(前のターンが 3 秒
  未満で、すぐ自分で再開した場合)、その開始は重複として捨てられ、サーバーは
  ターンを開かない。この場合は stop も今までどおり捨てられる。開いたターンが
  無いので `wait` は固まらないが、2 つ目のターンは記録されない。
  「確かめること」6 に従って変えていない。今の動きをテストで固定した。
- 本文に無い指摘: docs/user-guide/agent-event-hooks.md:117
  「別ターン(別 `session_id`)は畳まれない」は、ターンとセッション id を
  同じものとして書いている。セッション id はターンをまたいで同じなので、
  この説明は今回の修正の前から合っていない。scope 外なので変えていない。
- 本文に無い指摘: tests/unit/api/hooks-agent-event-instance.test.ts:456
  テスト名「two genuine turns, which carry different session ids」も同じ前提で
  書かれている。名前は変えていない。
- 本文に無い指摘: tests/fixtures/hooks/claude/stop.json:5
  Claude の payload には `prompt_id` があり、Stop にも UserPromptSubmit にも
  載る。http の hook はそのまま送るので、ターンを名指しする材料になりうる。
  ただし、自分で再開したターンで値が変わるかは実測していない。中継
  スクリプトは送らない。読むのは source の仕事
  (src/lib/hooks/sources/claude/source.ts、scope 外)。今回は使っていない。

## その他

- docs/design/multi-agent-state-architecture.md:86 は、窓の説明が事実と
  合わなくなるので、`stop` の例外と #3289 を足した。識別子は消していない。
- docs/module-reference.md:288 に足す注記は
  dev-reports/module-reference/issue-3289.md に書いた(コミットには無い)。
- 確認。関係するテスト 51 ファイル 1044 件、`npm run lint`、`npx tsc --noEmit`
  が通る。陽性対照のテストは、直す前(ツールのイベントの分は、
  user_prompt_submit だけで消す変異)で赤になることを確かめた。陰性対照の
  テストは、判定を広げる変異(どのイベントでも消す/捨てたターンの開始でも
  消す/セッションを見ない/細目の無い stop だけ消す)で、それぞれ赤になる
  ことを確かめた。
- 実機の確認はしていない(オーケストレーターが行う)。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(hooks): 重複の判定の説明を #3289 の動きに合わせる (#3289)

docs/user-guide/agent-event-hooks.md §0.4 と、英語版の同じ節。
「別ターン(別 `session_id`)は畳まれない」は、ターンとセッション id を同じ
ものとして書いていた。次の 3 点に書き換えた。日本語版と英語版は同じ内容。

- セッション id はターンをまたいで同じなので、セッション id だけでは別の
  ターンを区別できない。
- `stop` は、前の `stop` との間に同じセッションのターンの開始
  (`user_prompt_submit` / `pre_tool_use` / `post_tool_use`)を受けていれば、
  3 秒以内でも別のターンの終了として適用する。受けていなければ、今までどおり
  2 回目を畳む。
- `stop` 以外のイベントは、今までどおり 3 秒以内は 1 回として扱う。

見出しは変えていない。確認は `npx vitest run tests/unit/docs`(16 ファイル
215 件)が通ること。

依頼に無い指摘(変えていない): 同じ段落のキーの説明
`(worktreeId, cliTool, instance, event, sessionId)` には細目(`detail`)が
入っていない。実際のキーは #1726 から細目を含む
(src/lib/session/agent-event-state.ts の `isDuplicateAgentEvent`)。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(module-reference): isDuplicateAgentEvent の注記を #3289 の動きに合わせる (#3289)

ワーカーの断片(dev-reports/module-reference/issue-3289.md)を、オーケストレーターが本体へ写した。
既存の行(src/lib/session/agent-event-state.ts)の括弧の中を書き換えただけで、行は増やしていない。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…に理由を足す (#3298) (#3302)

A(古い説明)
- A-1 直した: current-output-types.ts / current-output-builder.ts / wait.test.ts のコメント、cli-operations-guide(日英)の表・式・前提(no_recent_output は running で出る、2 つは別の事実)。同じ前提の REASON 列の説明・statusEvidence の表も同じ向きに直した
- A-2 直した: api-responses.ts の Mirrors: 10 か所すべて current-output-types.ts に宣言があるので先を変えた
- A-3 直した: wait.ts の Path B のコメントを readyStage の条件の上へ移し、notRunningStage に 1 行足した
- A-4 直した: api-responses.ts 冒頭の存在しない api-contracts.ts への言及を、相対 import で共有する方法の説明に替えた
- A-5 直した: extractResponse の @returns
- A-6 直した: live-region-specs.ts のテスト名。設計書 :239 は 2 つの主張が 1 行にあるため、宣言の同一性は live-region-markers.test.ts と明記した
- A-7 直した: 計測の記録を prompt-view.ts の定数の上へ移した(書き換えず)

B(理由のコメント)
- 4 / 5 / 7 / 21 / 29 / 34 / 37: 足した(5 は WorktreeDetailRefactored と TerminalSplitPaneContent の両方、29 は 2 か所、34 は opencode と opencode-v2)

本文に無い指摘: docs/user-guide/cli-operations-guide.md:121 と docs/en/user-guide/cli-operations-guide.md:108 の ls 出力例は ready/no_recent_output のまま(tests/unit/docs/ls-columns-2586.test.ts が固定しているため直していない。現行サーバーは running で出す)
本文に無い指摘: docs/module-reference.md:142,171,994 に「同じ事実」の語が残る(#2011 の経緯を述べる文。scope 外のため触っていない。受入基準の grep はここで 0 件にならない)
本文に無い指摘: tests/unit/guards/no-procfs-env-fixtures.test.ts は run-related-unit-tests の並列実行中、一時 fixture(tests/fixtures/test-codex-oversized)の ENOENT で 1 回落ちた(単独実行は通る)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
- response-checker: /[░███]/g を /[░█]/g に(U+2591/U+2588 のみと確認)、重複防止の inProgressMessageId 条件を削除
- current-output-builder: composePromptData の scraperPromptWaiting 引数を statusResult から導出
- useWorktreeDetailController: 読まれていない content 欄を削除
- 本文に無い指摘: src/lib/polling/response-checker.ts:1505 extractCompletedTurn の sessionState 引数は他で使われていない

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Kewton and others added 28 commits October 14, 2026 00:18
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sidebar-settings-modal-2709 の「PC (1024px) > opens the modal instead of navigating」は、
モーダルが現れた直後に document.activeElement を同期で比べていた。フォーカスは
メニューが閉じて一度トリガーへ戻ってからモーダルへ移るため、負荷が高いと間に合わず落ちる。
製品の不具合ではなくテストの待ち方の欠陥(Issue の記述は実コードと一致)。

決めたこと:
- 同期 expect を await waitFor(() => expect(document.activeElement).toBe(modal-panel)) に変更。
  timeout 延長や期待の弱体化はしていない(it 名・期待内容は不変)。
- 同じ形の SidebarRail-settings-modal-2709.test.tsx の「opens the modal without navigating,
  and focus lands on the modal panel」も同様に修正。
- 直さないもの: ui/Modal.test.tsx:213 と ui/ConfirmDialog.test.tsx は menu 経由ではなく
  isOpen で直接描画する形で、フォーカスは同一 act 内で確定するため。
  AppShell-mobile-drawer-focus-3515 / Sidebar-top-controls-3512 / ContextMenu 等も、
  メニュー→モーダルのフォーカス受け渡しを待つ形ではないため対象外。
  ほかの menu→modal 経路(SettingsMenu-3510 等)は既に waitFor 済み。

確かめたこと:
- 負荷: CPU コア数(28)ぶんの `yes > /dev/null &` を起動し、2 ファイルを
  `npx vitest run ... --reporter=dot` で連続実行、終了後に kill して残存 0 を確認。
  修正後: 24/24 通過、続けて 40/40 通過。その前の 1 回目の 24 回連走では 23/24 で
  1 回失敗したが、失敗内容を取り損ね、再現しなかった(2 回目 24/24、3 回目 40/40)。
- npx vitest run <2 ファイル> tests/unit/guards tests/unit/docs 通過(757 tests)、
  npm run lint、npx tsc --noEmit 通過。

本文に無い指摘: 負荷下で 1 回だけ原因不明の失敗(修正後 1/88 回)。同ファイル群の
別の it の 5000ms timeout の可能性があるが、未確認。
探した対象: tests/unit/components 配下の `activeElement` 全件。同形で未対応のものは上記以外に無し。
* fix(ui): 「…」パネルを閉じた Esc で最大化まで解除しない (#3670)

## 決めたこと
- escape-key.ts に「層が閉じたが claim はしていない Esc」の印を集約した。
  `markEscapeClosedLayer(event)`(イベントを WeakSet に入れる)と、それを読む
  `isEscapeNoLayerClosed(event)`(= isUnhandledEscape かつ印なし)。
  ActionBarItemsOverflow は閉じた押下に印を付け、FilePanelContent の最大化解除
  (window listener)は isEscapeNoLayerClosed で判定する。
- 理由: preventDefault は札の理由の document listener(パネルより後に登録)まで
  止めてしまう(#3665 で 3592 のテストが落ちた)。印は isUnhandledEscape が無視
  するので、札の理由・アクティビティペイン等の既存の受け手は一切変わらず、
  読むと決めた受け手(最大化)だけが立ち止まる。WeakSet なのでイベントと同じ寿命で
  後始末が要らない。
- 捨てた案:
  - preventDefault を足す: 札の理由が閉じなくなる(上記)。
  - isUnhandledEscape 自体が印を読む: パネルより後の document listener である
    札の理由が止まり、同じ理由で 3592 が落ちる。他の 10 の受け手の意味も変わる。
  - 最大化の解除側で `[data-floating-layer]` の有無を読む: window に届く時点で
    React の setOpen(false) が DOM に反映済みか否かは flush のタイミング次第で
    不安定。また他の data-floating-layer(header tools の「…」等)は既に
    preventDefault しており、DOM 走査を足す理由がない。
- 実コードは Issue の説明どおりだった(ActionBarItemsOverflow の onKeyDown は
  isUnhandledEscape → setOpen(false) のみ、FilePanelContent は window で
  isUnhandledEscape のみ)。

## 確かめたこと
- 新テスト escape-chip-overflow-maximize-3670.test.tsx: 陽性(1 回目は「…」だけ、
  2 回目で最大化解除)は修正前の FilePanelContent で落ち、修正後に通る。陰性
  (「…」が閉じていれば 1 回目で解除)と、閉じた押下が後続の document listener に
  未 claim のまま届くことも確認。escape-key-3670.test.ts でヘルパー単体を確認。
  UI の対照なので一時ディレクトリは使っていない(実ファイルは書き換えていない)。
- npx vitest run(新テスト・escape-key-3611・escape-chip-overflow-3660・
  TerminalSplitContainer-header-in-bar-3592・FilePanelContent 系・
  escape-stacked-layers-3630・tests/unit/guards・tests/unit/docs): 57 files /
  866 tests 合格。npm run lint / npx tsc --noEmit 合格。
  changelog-fragments check exit 0。

## 対になる場所(探した結果)
- isUnhandledEscape の全受け手を grep。最大化/全画面の同種の解除は次の 2 つで、
  scope 外なので直していない。
- 本文に無い指摘: src/components/worktree/MarkdownEditor.tsx:816 最大化中の
  document Esc 解除も isUnhandledEscape だけを見るため、「…」パネル(document
  listener、こちらが後に登録されれば先に走る順序次第)と同じ押下で解除されうる。
  同 :662 の textarea onKeyDown も同様。isEscapeNoLayerClosed への置換が候補。
- 本文に無い指摘: src/components/worktree/FileViewer.tsx:505 全画面の window Esc
  解除も isUnhandledEscape だけを見る(統合画面以外の FileViewer)。同じく置換候補。
- スマホ側: ActionBarItemsOverflow は PC の 2 段目専用で、スマホに写しは無かった。
  src/cli/types/ に関係する型は無い。docs/ の ja/en に該当する説明は無かった。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): Esc を前面の層から閉じる — 最大化したファイルの背面の「…」は押下に触れない (#3670)

整合性レビュー(Codex)P2「Esc の優先順位と画面の重なり順が逆」への対応。

## 確かめたこと(実コード)
- 「…」パネル(ActionBarItemsOverflow): createPortal(..., document.body)、
  `fixed z-50`(= Z_INDEX.MODAL)。閉じるのは document の mousedown(パネル・
  ボタン・[data-floating-layer] の外)と document の Esc だけ。
- 最大化したファイル(FilePanelContent の MaximizableWrapper):
  createPortal(..., document.body)、`fixed inset-0`、zIndex = Z_INDEX.MAXIMIZED_EDITOR
  (55)。focus trap も inert も無い。
- 両方とも body 直下でルートの重なり文脈を共有するので、両方開いていれば常に
  最大化したファイルが前面(55 > 50、DOM 順も関係ない)。パネルは画面全体を覆う
  overlay の下に隠れて見えない。
- 両方が開く状態の作り方は 2 通りあり、どちらも「…」は背面:
  - 「…」を開いた後に最大化: ポインタで最大化ボタンを押すと mousedown で先に
    パネルが閉じる。キーボード(Tab で最大化ボタンへ移って Enter)なら閉じずに
    残る(レビューの例)。
  - 最大化の後に「…」を開く: overlay が全面を覆うのでポインタでは押せないが、
    focus trap が無いので Tab で overlay の下の「…」ボタンへ届き、Enter で開ける。
    開いたパネルは z 50 で overlay の背面に描かれる。
- つまり「最大化したファイルの上で『…』が前面に見えている」状態は、今の z-index
  では作れない。Issue の受入基準の前提(1 回目は「…」だけ)と実際の重なりは食い
  違っている。採った解釈: 受入基準の趣旨は「1 回の Esc で 1 層、前面から」。実際の
  重なり(「…」が背面)では 1 回目で最大化が解除され、見えるようになった「…」が
  2 回目で閉じる。「…」が前面に来る重なり(z-index が変わった場合)では受入基準の
  文言どおり、1 回目は「…」だけ、2 回目で最大化を解除する。

## 決めたこと
- 「前面にあるときだけ印を付ける」案を採った。escape-key.ts に
  `VIEWPORT_LAYER_ATTRIBUTE`(data-viewport-layer)と
  `isCoveredByViewportLayer(element)` を足し、最大化の overlay に印を付けた。
  判定は両方の要素が実際に持つ z-index(getComputedStyle)を比べ、同値なら文書順
  で後ろを前面とする。パネルは Esc のとき、背面なら押下に触れずに return(閉じない・
  印も付けない・フォーカスも動かさない)。前面なら前回どおり印を付けて閉じる。
- そのため、パネルの z-index を class の `z-50` から同じ値(Z_INDEX.MODAL)の
  inline style に移した(見た目は不変。jsdom を含め実際の値を読めるようにする)。
- 理由: 重なり順そのものから決めるので、作り方の 2 通り(開いてから最大化/最大化
  してから開く)のどちらでも正しい。z-index が将来変わってパネルが前面に来ても、
  前面の層から閉じる順がそのまま追従する。前面のファイルの解除が止まるのは、
  パネルが本当に前面にあるときだけになる。
- 捨てた案:
  - 最大化に入るときに「…」を閉じる: FilePanelContent から TerminalSplitContainer
    の局所 state へ届く経路が無く(共通の context かイベントを新設することになる)、
    「最大化してから Tab で『…』を開く」経路は塞げない。
  - 背面のパネルも Esc で同時に閉じる(印だけ付けない): 前面の解除は直るが、
    見えない層が同じ押下で閉じ、「1 回の Esc で 1 層」に反する。
  - 印(markEscapeClosedLayer / isEscapeNoLayerClosed)を外す: 現状の重なりでは
    前面の状態は作れないが、z-index が変わったときの受入基準(1 回目は「…」だけ)
    を守るために残した。
  - パネルの z-index を最大化より上げる: 「…」ボタンは overlay の下にあり、その
    下から浮いたパネルだけが最大化の上に出るのは見た目として不自然。
  - document.elementFromPoint で覆われているか調べる: jsdom で動かず、パネルの
    位置計算前(visibility: hidden)にも不安定。
- 前回のコミットで足したこのファイルのテスト(escape-chip-overflow-maximize-3670)
  は、背面のボタンへ直接イベントを送り「…」が前面である前提で書いていたので、
  この PR の中で書き直した(develop に既存のテストではない)。

## 確かめたこと(テスト)
- escape-chip-overflow-maximize-3670.test.tsx:
  - 実際の重なり: パネルと overlay が body 直下で、style.zIndex が 50 < 55 である
    ことを確かめたうえで、開いてから最大化(mousedown なしのクリック = キーボード
    操作相当)/最大化してから開く、の両方で 1 回目は最大化だけ解除・パネルは残る、
    2 回目でパネルが閉じる(陽性)。背面判定を無効にすると 2 件とも落ちる(変異で
    確認)。
  - 前面: overlay の z を 49 にして重なりが逆であることを確かめたうえで、1 回目は
    パネルだけ、2 回目で最大化を解除(陽性)。閉じた押下が後続の document listener
    に未 claim で届くこと。
  - パネルを開いていない Esc は 1 回目で最大化を解除(陰性)。
- escape-key-3670.test.ts: isCoveredByViewportLayer の上下・同値の文書順・z 未指定・
  印の無い層。
- npx vitest run(escape-key*, escape-*, TerminalSplitContainer*, FilePanelContent*,
  action-bar-overflow*, HeaderToolBar-3591, tests/unit/guards, tests/unit/docs):
  66 files / 970 tests 合格。npm run lint / npx tsc --noEmit 合格。
  changelog-fragments check exit 0。

## 本文に無い指摘
- 背面のパネルに畳まれた札の理由(document listener。重なりは未確認)は、最大化が前面でも同じ押下で
  閉じうる(isUnhandledEscape だけを見るため)。見えない層なら実害は小さいが、同じ
  「前面から」の考え方なら直す候補。
- MarkdownEditor.tsx:816 / :662(エディター独自の最大化)は印を読まない。レビューの
  P3 として追跡 Issue に回すため、この PR では変更していない。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(verification): env-clean が組み込みの除外を設定の除外として表示しない (#3672)

決めたこと:
- zsh の補完のキャッシュ(isZcompdumpEntry)は EnvProbeDiff.ignoredByConfig ではなく新設の ignoredBuiltIn に入れ、
  レポートでは `ignored (built-in): <名前, ...>` の別行に出す(見出し文言)。設定に書いた項目は従来どおり
  `ignored (options.envCleanIgnoreHomeEntries): ...`。判定(違反になるか)は変えない。
- 実コードの確認: DEMO_VITEST_HOME_ENTRY による除外は ignoredByConfig には入っておらず、attributeHomeEntry による
  帰属(ignoredAdded、`ignored: belongs to another worktree` の行)と deadDemoVitestEntries(removedStaleDemoVitest)で
  既に別の理由の行に出ているため変更なし(Issue の「確かめる」への答え)。

変えたテストの期待値: env-clean-gate.test.ts の `passes when only zsh compinit cache files were added...` のうち
ignoredByConfig に zcompdump が入る固定を ignoredBuiltIn に変更(it の名前は不変)。陽性/陰性の it を 1 本追加。
docs: docs/design/task-contract.md と verification-config.md に built-in 行の説明を追記。

探した対象: ignoredByConfig の参照(src/tests 全体)、`ignored (options` を含む docs/tests。他に写し・型の写しは無かった。

本文に無い指摘: docs/module-reference.md:259 「`ignoredByConfig` に残す」の記述は新実装と食い違う(scope 外、断片に記載)。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(verification): 組み込みの除外の説明を実装の範囲(.zcompdump と .zcompdump.<host>.<pid>)にそろえる (#3672)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(search): 2 文字を描く文字参照の全体へのヒットで描いた文字すべてを強調する (#3671)

## 決めたこと
- `mapMarkdownPositionsToDom` に、ヒットの終端用の写像 `rawEndToDom`(原文の位置
  → そこで終わるヒットの DOM 終端、排他)を足した。開始は従来の `rawToDom`(原文の
  位置ごとに最初の描画単位)のまま。1 つの `&` に 2 単位以上が置かれた並び(参照が
  描いた `fj` やサロゲートペア)を見つけたら、`referenceAt` で参照の終わりを引き、
  `;` の位置の終端を「描いた最後の単位 + 1」にする。
- 選んだ理由: 開始側は 1 か所も変えずに済み、#3661 の直し(後ろの語)・`&` だけの
  ヒット(`f` のみ)・名前の途中で終わるヒット(null)が構造上そのまま残る。
- 捨てた案:
  - 「範囲の終端側を使う」(`rawToDom` の最後の単位を書き換える): `&` だけのヒット
    が `fj` に広がり、決定の「参照の途中のヒットの今の動きは変えない」に反する。
  - 参照の原文 `&`〜`;` 全体を同じ単位に写す: 名前の途中(`&fj`)で終わるヒットが
    置かれるようになり、同じく今の動きが変わる。
  - 単位が 1 つの参照(`&lt;`)も `;` で終われるようにする: 既存テスト
    `terminal-highlight-3661.test.ts` の「a reference that draws a character is
    placed on its `&`」が全体ヒットを null と期待しているので、並び(2 単位以上)
    のときだけに限った。
- `terminal-highlight-3661.test.ts` の `&fjlig;` のテストのコメント(「参照全体の
  2 文字を覆う」)を、実際に確かめている範囲(原文の `&` 0..1 → 描画の `f` 0..1)
  に書き直した。it の名前と期待値は変えていない。#3661 のコミットの説明(同じ食い違い)
  はマージ済みで書き換えられないので、ここに訂正として残す。

## 確かめたこと
- 新規 `tests/unit/lib/terminal-highlight-reference-3671.test.ts`:
  陽性(`&fjlig;` 全体 → `fj`、先頭の `&fjlig;` と後ろの語、`&Afr;` 全体 → サロゲート
  ペア)は直す前の実装で 3 件とも落ち、直した後に通る。陰性(後ろの語・`&` だけ・
  名前の途中・`&lt;` 全体は null・インラインコード内・参照から後ろの語までのヒット)
  は前後とも通る。
- 新規 `tests/unit/components/worktree/chat-search-highlight-3671.test.tsx`:
  Chat(ChatTranscript)と History(HistoryPane)の両方で、`&fjlig;` 検索の現在で
  ないヒットが `fj` として強調される(直す前は 2 面とも落ちる)。陰性
  `&fjlig; sentinel and sentinel` の `sentinel` は前後とも強調される。
- 実リポジトリのファイルは書き換えずに確かめた(DOM のみで完結し、一時ファイルは不要)。
- `npx vitest run`(上 2 本+ terminal-highlight-3661 / chat-search-highlight-3661 +
  tests/unit/guards tests/unit/docs)、`npm run lint`、`npx tsc --noEmit` が通る。

## 対になる場所を探した結果
- 原文→DOM の写像を持つのは `mapMarkdownPositionsToDom` だけ。
  `terminal-highlight-mermaid.ts` の `mapRawPositionsToDom`、`terminal-highlight-offsets.ts`、
  `terminal-highlight.ts`(呼び出し元 1 か所。Chat・History は同じ経路)に、位置ごとに
  最初の単位だけを持つ写しは無かった。`src/cli/types/` に該当する型は無い。
  `docs/` に `fjlig` / `rawToDom` の記述は無かった。

## 本文に無い指摘
- src/lib/terminal-highlight-markdown.ts(`mapMarkdownPositionsToDom`): 1 文字を描く
  参照(`&lt;` → `<`、`&amp;` → `&`)の全体にヒットする検索語は今も強調されない。
  `tests/unit/lib/terminal-highlight-3661.test.ts` の「a reference that draws a
  character is placed on its `&`」が null を期待して固定しているため、期待値を変え
  ずに残した。`&fjlig;` と揃えるかは別途判断が要る。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(search): Mermaid を含むメッセージでも図の外の文字参照の全体ヒットを強調する (#3671)

整合性レビュー(Codex)の P2 への対応。

## 指摘の経路の確認(実コード)
- 指摘どおりだった。`applyHighlightsInternal`(src/lib/terminal-highlight.ts)は
  `sourceText` があると先に `mapRawPositionsToDom`(terminal-highlight-mermaid.ts)を
  呼び、`??` で次へ進むのは、それが配列ごと `null` を返すとき(コンテナに
  `[data-mermaid-source]` が 1 つも無いとき)だけだった。
- 図が 1 つでもあると、すべてのヒットが mermaid 側で決まる。図の外(フェンスの間の
  `gaps`)のヒットは、原文の文字列 `lowerRaw.slice(start, end)` を区間内で数え、同じ
  序数の出現を DOM の文字列から探す(mermaid.ts の `positions.map` の末尾)。
  `&fjlig;` は画面では `fj` なので見つからず `null`。返る配列は `[null, ...]` で
  非 null なので、`??` は Markdown 側(直前のコミットで直した
  `mapMarkdownPositionsToDom`)へ進まない。
- 新しいテストで、直す前は Chat・History の両方で図の前後の `&fjlig;` が強調されない
  (marked が空・現在のヒットのスクロール先が無い)ことを確かめた。

## 決めたこと
- `applyHighlightsInternal` の写像の選び方を `placeRawPositions` に切り出し、mermaid 側の
  結果に `null` があるときだけ Markdown 側の写像も求め、`null` の位置をその結果で埋める。
  ただし Markdown 側の範囲が図のソース(`[data-mermaid-source]` の DOM 区間)に重なる
  ものは埋めない(`null` のまま)。mermaid 側が置いたヒットはそのまま使う。
- 選んだ理由:
  - 図の中のヒットは今までどおり mermaid 側だけが決める(置けたものも、置けない
    `null` も変わらない)。図の外で mermaid 側が置けなかったヒットだけが、図の無い
    メッセージと同じ写像で置かれる=今回の修正と同じ範囲で強調される。
  - 変更が scope 内(terminal-highlight.ts)で完結し、mermaid 側の照合を触らない。
  - 図の無いメッセージ、mermaid 側が全部置けたメッセージは、呼ぶ写像も結果も従来と同じ
    (後者は Markdown 側を呼ばない)。
- 捨てた案:
  - mermaid 側が図の外を Markdown の写像へ任せる: terminal-highlight-mermaid.ts は
    契約の scope 外。加えて mermaid 側が markdown 側に依存する向きになり、#3517 の
    「本体 → mermaid → dom・fences」の一方向の依存が崩れる。
  - `[null]` のとき配列ごと次の写像へ進む: 図の中のヒット(Markdown 側では置けない)
    まで落ち、図の中の既存の挙動が変わる。
  - 図の外は常に Markdown 側を優先する: mermaid 側で今置けている図の外のヒットの
    置き場所まで変わりうる。今回の指摘(置けないもの)を超えるので採らなかった。

## 確かめたこと
- 新規 `tests/unit/components/worktree/chat-search-mermaid-3671.test.tsx`(Chat と History):
  - 陽性: 図の前後の `&fjlig;` を検索すると、後ろのヒットが `after fj` の段落で `fj` と
    して強調され、前の(現在の)ヒットの段落へスクロールする。図の前の `&fjlig;` と図の
    中の `&fjlig;` のときは、前の(現在の)ヒットが置かれ、図の中のヒットは従来どおり
    ソース上の `&fjlig;` に置かれる。直す前は 2 面とも落ち、直した後に通る。
  - 陰性: 図の中だけの `&fjlig;` 2 件はソースの原文どおり強調され `<details>` が開く。
    図の中だけの `sentinel` は図の描画(ラベル)に置かれず、ソースへスクロールする。
    直す前後とも通る。
- `npx vitest run tests/unit/lib/terminal-highlight* tests/unit/components/worktree/chat-search*
  tests/unit/components/worktree/chat-mermaid* MermaidDiagram-real-3503 HistorySearchBar
  tests/unit/guards tests/unit/docs`(69 files / 1025 tests)、`npm run lint`、
  `npx tsc --noEmit` が通る。テスト全体は走らせていない。
- 実リポジトリのファイルは書き換えずに確かめた(DOM のみで完結し、一時ファイルは不要)。

## 対になる場所を探した結果
- 写像を選ぶのは `applyHighlightsInternal` の 1 か所だけ(Chat・History・ターミナルは同じ
  経路)。`mapRawPositionsToDom` / `mapMarkdownPositionsToDom` の他の呼び出し元は無い。
  `src/cli/types/` に該当する型は無く、`docs/` の記述は module-reference の
  terminal-highlight.ts 行(断片に追記を書いた)だけ。

## 本文に無い指摘
- src/lib/terminal-highlight-mermaid.ts(`mapRawPositionsToDom` の図の外の照合): 原文と
  画面で出現数が違う語(例 `&fjlig; a & b` を `&` で検索)は、序数がずれて原文の先の
  ヒットが画面の別の出現(`a & b` の `&`)に置かれる(コードから読める。未実測)。
  置けているので今回の埋め方の対象外。scope 外のため直していない。
- この PR で直さないもの(レビューの P3、追跡 Issue へ): 1 単位の参照(`&lt;`/`&amp;`)
  の全体ヒットは null のまま。空白 2 単位を描く参照(`&ThickSpace;`)は空白の枝で対象外。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(search): Mermaid を含むメッセージの図の外のヒットは Markdown 側の写像だけで置く (#3671)

整合性レビュー(Codex)の再レビュー(e4c749c)で残った P2 への対応。

## 再現(直す前、e4c749c の実装)
- `` &fjlig; and `&fjlig;` `` の段落+Mermaid の図のメッセージで `&fjlig;` を検索する
  テストを Chat(ChatTranscript)と History(HistoryPane)に置き、2 面とも指摘どおり再現した:
  - 1 件目(通常文、現在のヒット): overlay を測った範囲は `fj` ではなく、インライン
    コード内の `&fjlig;` だった(mermaid 側の序数の照合が、原文 1 件目を画面に 1 回だけ
    原文どおり出るコード内の `&fjlig;` に置いた)。
  - 2 件目(コード内): mermaid 側は null、e4c749c の補完が Markdown 側の結果で埋め、
    同じコード内の `&fjlig;` に置いた(強調の範囲は `['&fjlig;']`)。
  - 通常文の `fj` はどちらにも置かれず、2 件が同じ範囲に重なる。

## 採った案
- 図のある(画面に mermaid のソースがある)メッセージでも、図のソースの外のヒットは
  `mapMarkdownPositionsToDom` の結果だけで置く(置けなければ null=強調しない。図の無い
  メッセージと同じ規則、#3661)。`mapRawPositionsToDom` の結果は、図のソースの DOM 区間に
  重なるものだけ使う。Markdown 側の結果がソースに重なるものは置かない(前回どおり)。
  Markdown の印が 1 つも無いコンテナ(`mapMarkdownPositionsToDom` が null)は従来どおり
  mermaid 側の結果を丸ごと使う。
- 理由: mermaid 側の図の外の照合は「原文の文字列の n 回目=画面の文字列の n 回目」で、
  原文と画面で文字列が違う(参照・Markdown の記号)場所では、非 null の結果でも原文と
  対応していないことがこの再現で分かった。どれが正しいかを mermaid 側の結果から確かめる
  手段は無く、原文の位置を要素ごとに持つ Markdown 側の写像がその確かめ役になる。図の外を
  Markdown 側に統一すれば、図の有無で挙動が変わらない。
- 前回捨てた「図の外は常に Markdown 側を優先する」の見直し: 捨てた理由は「今置けている
  ヒットの置き場所まで変わりうる」だったが、変わるのは mermaid 側が原文と違う出現に
  置いていた場合で、今回の再現がその例だった。置き場所が変わるのは図の無いメッセージと
  揃う方向なので、理由として成り立たないと判断して採った。

## 捨てた案
- mermaid 側の非 null の結果を残し、Markdown 側と食い違うときだけ Markdown 側に替える/
  重なりを取り除く: 結局 Markdown 側を正とするので上と同じになり、食い違いの判定が増える
  だけ。Markdown 側が null で mermaid 側が非 null のヒットを残すと、図の無いメッセージ
  では強調されないもの(原文と違う出現への誤配置を含む)が図のあるときだけ強調される。
- mermaid 側の序数の照合を直す: terminal-highlight-mermaid.ts は契約の scope 外。

## 確かめたこと
- `tests/unit/components/worktree/chat-search-mermaid-3671.test.tsx` に陽性を追加(Chat・
  History): 現在のヒットは通常文の `fj`(コードの外)、もう 1 件はコード内の `&fjlig;`
  (overlay を測った範囲を記録して確かめる)。直す前は 2 面とも落ち、直した後に通る。
  前回の陽性 2 件・陰性 2 件(図の中の `&fjlig;`・図の中だけの語)は引き続き通る。
- `npx vitest run tests/unit/lib/terminal-highlight* tests/unit/components/worktree/chat-search*
  tests/unit/components/worktree/chat-mermaid* MermaidDiagram-real-3503 HistorySearchBar
  tests/unit/guards tests/unit/docs`(69 files / 1027 tests)、
  `tests/unit/app/file-viewer-mermaid-3522.test.tsx`、`npm run lint`、`npx tsc --noEmit`
  が通る。テスト全体は走らせていない。
- `sourceText` を渡すのは ChatTranscript と HistoryPane だけ(FileViewer の `sourceText` は
  別物で、この経路を通らない)。

## 本文に無い指摘
- 前回の「`&fjlig; a & b` を `&` で検索すると序数がずれる」は、図の外が Markdown 側に
  なったので Chat・History では起きなくなった(コードから読める。未実測)。
  terminal-highlight-mermaid.ts の図の外の照合自体は残っており、ここからは使われない。
- レビューの P3(`&lt;` など 1 単位の参照の全体ヒット、`&ThickSpace;`)は追跡 Issue へ。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Orchestrator-Fold: module-reference
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

#3670・#3671・#3672 の断片を既存の行の注記に追記した(行は増やしていない)。
#3672 の断片の指示どおり、env-clean-gate.ts の行の #3664 の記述(ignoredByConfig に残す)を ignoredBuiltIn に置き換えた。
#3674 はテストのみの変更で追記なし。#3663 はマージしていない(設計からやり直す)。
* fix(polling): worktree ID の一括移行で保留中の画面読み取りを移し、退役した tick に触らせない (#3663)

## 決めたこと
- 判定は「呼び手がまだそのキーの持ち主か」。保留の倉庫(pending-scraped-response.ts)は書き込みの前に
  ownerStorage.getStore() の owner を見て、tick の外(route・移行)か、同じキーで stopped でない鎖だけを通す
  (mayActForPollerKey)。superseded だけの tick(待機つき再起動)は従来どおり hold でき、再起動時の停止で確定される。
  世代の Map のような列挙の仕組みは足していない。
- import 循環を増やさないため、PollerOwner と AsyncLocalStorage を新しい src/lib/polling/poller-owner.ts へ移した
  (pending-scraped-response → response-poller-core の import は core → response-checker → pending の新しい循環になる。
  #3482 のガードで確認)。core は coordinator.ownerStorage をやめ、共有の pollerOwnerStorage を使う。
- 経路ごと: (a) hold は退役 tick を拒否し boolean を返す(response-checker は held のときだけ held を log)
  (b)(c) 2 つの discard も退役 tick を拒否 (d)(e) settle は await 前と後で owner を確かめ、後では「同じキーの同じエントリー」
  も確かめてから discard / flush (f) stopPollingByKey の flush は従来どおり同期(flush 自体も退役 tick を拒否)
  (g) 移行: 全移送元(running・paused・鎖の無い保留)の保留を先に退避し、worktreeId だけ新 ID にして置く。
- 条件 5/6: 移送元の旧 owner は従来どおり stopped。移送元が空けない移送先に鎖・pause・保留があれば stopPollingByKey で止める
  (保留は今の ID で確定、owner は stopped、キャッシュは移送の前に消える)。B:y・状態の無い移送元・同じ ID の rename は触らない。
  移送先に既に保留がある(多対一など)ときも、先に今の ID で確定してから置く。abandoned は retargetPendingScrapedResponse で
  新 ID に向けてから停止の flush で確定する(古い ID は外部キーで失敗して消える)。
- 条件 7 の直し方: hold を拒否したとき、その tick が登録した本文のハッシュを後継の鎖のキーから外す(一致するときだけ、
  response-dedup の forgetResponseHash)。後継は移行時に retired.movedTo = 新 owner で指し、successorPollerKey で辿る。
  捨てた案: 「移行でハッシュを移すときに未保存の分を移さない」— 移行の時点では in-flight tick が保存するか保留するか
  分からず、未保存を知るには tick ごとの印(別の map か owner への書き込み)を response-checker に足す必要があり、
  保存済みのハッシュまで落とすと後継が同じ画面を二重保存する。「ハッシュの登録を保存・保留の決定の後へずらす」—
  isDuplicateResponse の check-and-set を全経路で変えることになり範囲が広い。
  残る誤差: 後継が独立に同じ本文を登録・保存した直後に旧 tick の hold が拒否された場合は、そのハッシュも外れて
  同じ本文が再保存されうる(移行で移したハッシュは後継が自分で登録し直さないので、通常は起きない)。

## 確かめたこと
- 新しいテスト tests/unit/lib/polling/response-poller-migrate-pending-scrape-3663.test.ts(26 件、タイマー→runPollTick→
  ownerStorage.run を通す)。陽性 16 件は直す前の実装で落ちることを確認してから直した(③の「primary・alias が正常に移る」
  も移送が無いので直す前は落ちる)。陰性は直す前も後も通る。
- npx vitest run tests/unit/lib/polling tests/unit/polling tests/unit/guards tests/unit/docs: 109 files / 1716 passed
  (#3553 と #2436・#2223・#2230・#1695 の既存テストを含む)。関係する tests/unit(session-cleanup・cli-tools・lib/session・
  response-poller*・detection など 160 files)と tests/integration の response-poller 系 9 files も通過。
- npm run lint / npx tsc --noEmit / node scripts/import-cycles.mjs(新しい循環なし)/ node scripts/changelog-fragments.mjs check

## 対になる場所を探した結果
- 保留の倉庫の呼び手: response-checker(hold・2 つの discard・settle)と response-poller-core(stop の flush・移行)だけ。
  stop-history-capture・auto-yes 系・worktree-session-reconcile(移行の呼び手、変更不要)は倉庫に触れない。
- src/cli/types/ に該当する型の写しは無し。docs/ の ja/en に保留の移行を説明する箇所は無し(module-reference は断片で追記)。

## 本文に無い指摘
- src/lib/polling/response-checker.ts:1130 退役した tick が保留ではなく即時保存の分岐に入ると、createMessage と
  onRelayTurnCompleted は旧 ID のまま走る(この Issue は保留の倉庫の条件だけを守る範囲)。
- src/lib/polling/response-poller-core.ts:706 2 つの旧 ID が同じ新 ID へ移る rename(A→C, B→C)では、同じキーに 2 本の鎖を
  順に takeOwnership し、先の鎖は superseded だけで timer が activePollers 上で上書きされる(移行前からの挙動。保留は先に
  確定してから置くので失われない)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(polling): 整合性レビューの 3 件 — 別キーの確定を通す・後継の保存済みハッシュを外さない・保留だけの移送でも移送先を退役 (#3663)

## 1. 正当な別セッションの再起動で flush が拒否される
- 確かめた事実: 経路は実コードどおり。recordOrHoldScrapedResponse(tick の中)→ onRelayTurnCompleted
  (relay-triggers.ts の void (async) で dynamic import → notifyRelayTurnCompleted)→ relay-delivery.ts:575 sendUserMessage
  → send-user-message.ts:503 startPolling(B)。async の連鎖なので A の tick の AsyncLocalStorage を継承し、
  mayActForPollerKey は「caller.pollerKey === key のときだけ通す」だったため、B の再起動の stop の flush を拒否していた
  (stopPollingByKey の stale-stop は同じキーだけ見るので B の停止は進み、保留だけが次のターンへ残る)。
- 直し方: mayActForPollerKey を「停止した鎖の tick が自分のキーへ作用するときだけ false」に変えた(stale-stop と同じ線)。
- 捨てた案: relay の起動を ALS の外(AsyncLocalStorage.exit / snapshot)で走らせる — relay は scope 外で、
  同じ形の連鎖(tick → 別セッションの操作)をほかにも塞ぐことにならない。退役 tick の後継キーまで拒否する —
  後継キーへの stop は stale-stop を通って進むので、flush だけ拒否すると保留が孤立する。
- テスト: 陽性=A の tick の relay から B を再起動すると B の保留が今の ID で 1 回確定(直す前は 0 件で落ちる)。
  陰性=A↔B の後、退役した A の tick の中から A のキーへ hold・discard・flush しても拒否(既存の ①〜④ も通る)。

## 2. 後継が保存した同じ本文のハッシュまで外す
- 確かめた事実: 経路は実コードどおり。forgetResponseHash は successor のキーの本文ハッシュの一致だけで外しており、
  移行→後継の再起動(stop でハッシュ消去)→後継が同じ本文を保存(再登録)→旧 hold 拒否、で保存済みのハッシュを外し、
  次の tick が isDuplicateResponse を通って再保存していた(再起動なしで後継が別本文→同じ本文と保存し直した場合も同じ)。
- 直し方: 移行の Phase 2 で後継の owner に移したハッシュを inheritedResponseHash として持たせ、isDuplicateResponse が
  呼び手の鎖の自分のキーへ新しいハッシュを登録したら印を消す。外すのは forgetInheritedResponseHash で、
  movedTo を辿った最後の後継が稼働中(stopped でない=再起動されていない)かつ印とキャッシュの両方が本文のハッシュと
  一致するときだけ。successorPollerKey と forgetResponseHash は削除。
- 捨てた案: response-dedup にキー → 継承ハッシュの Map を足す — rename・clear のたびに Map も動かす必要があり、
  契約の「キーを列挙して印を付ける仕組みは足さない」に寄る。owner の印なら停止・再起動で owner ごと無効になる。
- テスト: 陽性 2 件(後継の再起動後に保存/再起動なしで保存し直し、のあと旧 hold が拒否されても二重保存しない。
  直す前は 2 行になって落ちる)。陰性=既存の ⑦(未保存なら後継が保留して最後に保存)。

## 3. 保留だけを移すとき、移送先の owner が退役しない
- 確かめた事実: 経路は実コードどおり。occupied は moves と pausedMoves の移送先だけで、pendingMoves(鎖も pause も無い保留)の
  移送先は入らず、稼働中の B:x の鎖は止まらない(movePendingScrapedResponses は既存の保留を確定してから置くが、
  B の旧 tick は live owner のまま discard・hold できた)。
- 直し方: occupied に pendingMoves の移送先も入れ、鎖・pause・保留のどれが移っても同じく stopPollingByKey する
  (既存の保留を今の ID で確定、owner を退役)。移送元に何も無ければ pendingMoves にも入らないので移送先に触らない。
- 捨てた案: 保留だけの移送では鎖を止めず、移ってきた保留に「持ち主」を記録して旧 tick を弾く — 倉庫のエントリーに
  owner を持たせる別の仕組みが要り、条件「移すものが何でも同じ扱い」と一致しない。
- テスト: 陽性=保留だけの A:x→B:x(B:x 稼働中)で、旧 B の tick の再読 discard と hold が移ってきた保留に作用しない
  (直す前は消える・上書きされて落ちる)。陰性=既存の ⑥「移送元に何も無い rename は B の鎖と保留をそのまま」。

## 確かめたこと
- 新規 6 件を含む 3663 のテスト 32 件。陽性 5 件は直す前に落ちることを確認してから直した。
- npx vitest run tests/unit/lib/polling tests/unit/polling tests/unit/guards tests/unit/docs: 109 files / 1722 passed。
  tests/integration の response-poller 系と response-poller*・session-cleanup・lib/session・cli-tools: 106 files / 1581 passed。
- npm run lint / npx tsc --noEmit / node scripts/import-cycles.mjs(新しい循環なし。response-dedup → poller-owner は循環しない)
  / node scripts/changelog-fragments.mjs check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(polling): 条件 7 を「移行の時点で tick が走る鎖は応答ハッシュを移さない」に置き換える (#3663)

## 確かめた事実(再レビューの P2 2 件)
- 再移行: 旧 A が本文 X の hold の前で待機 → A→B → B が Y、続いて X を自分で保存 → B→C の順で、Phase 2 が C に X の
  継承の印を付け直し、旧 A の拒否が movedTo を C まで辿って保存済みの X のハッシュを外し、C が X を二重保存した。
  直す前の実装で再現(新しいテストが 2 行で落ちる)。
- paused の再開: A→B で未保存の X のハッシュだけ移った後、B が prompt で止まり再開すると、新しい owner に印も movedTo も
  無く、旧 A の拒否は停止済みの旧 B で終わって X のハッシュが残り、後継が保留を作れなかった。直す前の実装で再現
  (hasPending が false で落ちる)。

## 直し方(オーケストレーターの方針どおり)
- 継承の印・movedTo・拒否した hold のハッシュを後から外す仕組みを取り除いた(PollerOwner.movedTo /
  inheritedResponseHash、response-dedup の forgetInheritedResponseHash / peekResponseHash と isDuplicateResponse の印の消去、
  holdScrapedResponse の拒否時のハッシュ解放。移行の Phase 2 は元の scheduleNextResponsePoll に戻した)。
- migrateResponsePollerWorktreeIds は、移行の時点で coordinator.running にある移送元の鎖(tick が走っている鎖)の応答の
  dedup 状態(ハッシュ・recheck・skip 数)を移さずに捨てる(移送元を一括移送の前に、移送先を後に clearResponseHashCache)。
  走っていない鎖・paused の鎖は今どおり移す。prompt のハッシュと prompt-skip の移送は変えない。
- 指摘 1・3 の修正と、退役した tick が自分のキーの保留に作用するのを拒否する判定(前の run の P2 4 件の経路)は変えていない。

## 狭めた条件と、受け入れた重複
- 契約の陰性「保存済みの応答は後継でも重複保存しない」は「移行の時点で tick が走っていなかった鎖では」に狭めた。
- 受け入れた重複: 移行の瞬間に tick が走っていて、その tick がすでに返答を保存し終えていた(ハッシュ登録→保存の後、
  tick の残りの処理中だった)場合、後継が同じ画面をもう 1 行保存しうる。走っている tick はハッシュの登録後に保存か保留かが
  未確定で、移せば欠落、捨てれば重複になるので、#2436 と同じく欠落より重複を取った。本番の移行は起動時の 1 回だけで、
  tick が走っている窓は小さい。

## 捨てた案
- 印の寿命を再移行・再開・再起動ごとに引き継ぐ — 経路が増えるたびに同じ種類の P2 が出る(今回の 2 件がそれ)。
- 走っている tick の移行を tick の終了まで遅らせる — 移行は同期の一括処理で、呼び手(worktree-session-reconcile)の
  契約が変わる。

## テスト
- 削除: 印の仕組みを前提にした「後継の保存の後で旧 hold が拒否される」2 件(新しい再移行のテストに置き換え)。
- 追加(⑦ の続き): B が Y→X と自分で保存した後の B→C で旧 hold が拒否されても C は二重保存しない(陽性、直す前は落ちる)/
  旧 hold の拒否の前に B→C しても C が未保存の返答を保留して 1 回保存する/B が paused で止まって再開しても
  保留して 1 回保存する(陽性、直す前は落ちる)。
- 陰性: 既存の ⑦「移行の時点で tick が終わっていた鎖の保存済みの返答は後継で二重保存しない」と #3553 の既存テスト。
- 3663 のテスト 33 件、tests/unit/lib/polling・tests/unit/polling・tests/unit/guards・tests/unit/docs(109 files / 1723 passed)、
  integration の response-poller 系と response-poller*・session-cleanup・lib/session・cli-tools(106 files / 1581 passed)、
  npm run lint、npx tsc --noEmit、node scripts/import-cycles.mjs(新しい循環なし)、changelog-fragments check が通過。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(polling): 再レビューの 3 件 — 走る鎖のハッシュは移送元でだけ捨てる・running は今の owner で判定・停止した tick はハッシュを登録しない (#3663)

方針(移行の時点で走っている鎖は応答ハッシュを移さない・欠落より重複)は変えていない。

1. 多対一の移行で、走っていない別の移送元のハッシュも消していた
- 確かめた事実: A(tick が reader 待ち)と B(X を保存済み・待機中)を同じバッチで
  A→C・B→C すると、一括移送で B の X が C に入った後、midTick の移送先 clear
  (clearResponseHashCache(move.newKey))が消し、C が X を再保存した(新テストで再現)。
- 直し方: 移送先の後 clear を削除。走る鎖の応答 dedup 状態は、移送元のキーの上で
  一括移送の前にだけ捨てる。移送先には一括移送の結果だけが残る。
- 捨てた案: 移送先を一括移送の「前」に消す案。入れ替え(A↔B)では移送先が別の
  移送元でもあり、持ち上げる前の状態を消してしまう。

2. 退役した別 owner の running の記録で「実行中」と判定していた
- 確かめた事実: X を保存済みの A を、tick が reader 待ちの B へ移すと旧 B は退役するが、
  running の B の記録は旧 tick の後始末(owner の同一性で削除)まで残る。その間の
  B→C で、まだ tick を始めていない今の B の鎖が midTick と判定され、X が捨てられて
  C が再保存した(新テストで再現)。
- 直し方: midTick は running.get(oldKey).owner === owners.get(oldKey) のときだけ真。
- 捨てた案: 移行で退役させた owner の running を消す案。running の記録は stall 検出と
  待機つき再起動の判断にも使われ、旧 tick の後始末の前提も崩れる。

3. 追跡から外れた旧 tick の未保存ハッシュを移していた
- 確かめた事実: 旧 tick が capture 待ちのまま 60 秒超で再起動され running から外れ、
  戻って X のハッシュを登録(isDuplicateResponse は停止済み owner を拒否しない)→
  reader 待ちの間に A→C → X のハッシュが C へ移り、旧 hold は拒否され、C は X を
  重複と見て保留を作れなかった(返答の欠落、新テストで再現)。
- 直し方: isDuplicateResponse は mayActForPollerKey(pollerKey) が偽(停止した鎖の
  tick が自分のキーに作用)なら登録せず false を返す。保留の倉庫と同じ判定で、
  別キーへの作用・superseded だけの tick・tick 外の呼び手は今どおり。
  response-dedup は poller-owner だけを import するので新しい循環は無い。
- 捨てた案: 移行時に running に無い鎖も含め tick の有無を別に追跡する案。登録その
  ものを止めれば、移行以外(再起動後の継承)でも同じ未保存ハッシュが残らない。
  true(重複扱い)を返す案は、旧 tick の recheck 経路に入り後継の判定と無関係な
  分岐を作るので採らない。

テスト(response-poller-migrate-pending-scrape-3663.test.ts に 7 件追加)
- 陽性 3 件はいずれも直す前の実装で落ちることを確かめた。
- 陰性: A だけを C へ移すと C が自分で保留する/今の B の鎖の tick が走っていれば
  B→C でハッシュを捨て C が保留する/60 秒未満の待機つき再起動の tick は登録・保留し
  再起動で 1 回保存/今の鎖はハッシュを登録し 1 回だけ保存する。

確かめたこと
- 3663 のテスト 40 件、tests/unit/lib/polling・tests/unit/polling・guards・docs
  109 files / 1730 passed、integration の poller 系+lib/session+session-cleanup+guards
  65 files / 972 passed、npm run lint、npx tsc --noEmit、import-cycles(5、新規なし)、
  changelog-fragments check。

範囲外(本文に無い指摘、前回までと同じ)
- response-checker.ts の即時保存の分岐では、退役した tick がまだ旧 ID で
  createMessage と onRelayTurnCompleted を呼ぶ(今回の 3 でも登録しないだけで、
  その分岐は変えていない)。
- 多対一の移行(A→C・B→C)で同じキーに takeOwnership が 2 回走り、先の鎖は
  superseded のみで timer が activePollers で上書きされる(既存の挙動)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Orchestrator-Fold: module-reference

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#3683) (#3689)

clean のレポートに `ignored (attributed to another worktree): <名前, ...>` の 1 行を足し、
#3672 の `ignored (built-in): …` と同じ形にそろえた。判定・exit code・VIOLATED の列挙は不変。

確かめたこと: 実コードは Issue の説明と一致(formatEnvCleanReport の clean 側は件数のみ)。
陽性テストは修正前に落ちることを確認。verification / guards / docs のテスト、lint、tsc は通過。
既存のヘッダ行(件数)はそのまま残した(既存テストが固定しているため)。

対になる場所の探索: removedBy* / ignoredByConfig / ignoredBuiltIn の各 clean 表示は既に名前付き。
VIOLATED 側の ignoredAdded 列挙は既存。型の写し(src/cli/types)・docs に該当表示の記述は無かった。
本文に無い指摘: なし

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
## 決めたこと
- 実コードは Issue の説明どおりだった(MarkdownEditor の textarea onKeyDown と
  document listener は isUnhandledEscape だけ、CSS fallback の overlay に
  data-viewport-layer なし。札の理由の document listener は未 claim なら無条件に
  閉じる)。escape-key.ts は変更不要だったので変えていない。
- 札の理由(popover="floating"、PC): document の Esc で popoverRef が
  isCoveredByViewportLayer なら押下に触れない。前面で閉じたときは
  markEscapeClosedLayer で印を付ける(#3677 の「…」パネルと同じ規則)。判定のため
  z-index を class の z-50 から同じ値(Z_INDEX.MODAL)の inline style へ移した
  (見た目は不変)。inline の理由(スマホ)は札の箱の中で重なるため実際の z を
  比べられず、従来どおり(判定も印も無し)。
- MarkdownEditor の最大化解除(textarea / document の 2 経路)は
  isEscapeNoLayerClosed に置き換えた。浮遊層が無いときは今どおり 1 回で解除。
- エディターの最大化を前面の viewport layer として扱う(採用)。最大化中は
  CSS fallback(fixed inset-0 の body portal、z 55)とネイティブ Fullscreen API
  (要素は元の位置のまま、z 55 の inline style)の両方で data-viewport-layer を
  付ける。理由: どちらも画面全体を覆い、ネイティブは browser の top layer に
  載るので z-index に関係なく外の層すべての前面にある。属性を付ければ、背面の
  札の理由・「…」パネル(z 50)が最初の Esc をエディターに譲る(ファイルの最大化
  と同じ振る舞い)。ネイティブ経路は「両方がルートの重なり文脈にある」という
  isCoveredByViewportLayer の前提から外れるが、top layer は常に最前面なので
  z 55 との比較は z ≤ 55 の層を背面と判定し、結果は正しい(z > 55 の body
  portal で isCoveredByViewportLayer を読む層は今は無い)。
- 捨てた案:
  - 属性を CSS fallback だけに付ける: ネイティブ経路で背面の理由・「…」が押下を
    受けて閉じ、「1 回の Esc で 1 層」が経路によって変わる。
  - エディターを viewport layer にしない: 背面の「…」パネルは、エディターの
    document listener が先に登録されて preventDefault するかどうか(=開いた順)
    で振る舞いが変わる。理由も同じ押下で閉じる(Issue の 2 と同じ形)。
  - 札の理由で印を付けない: 理由が前面のとき(z が変わった場合)、閉じた同じ
    押下で最大化まで解除され、方針の「前面で閉じたら印」に反する。印は
    isUnhandledEscape が無視するので、他の受け手の挙動は変わらない。

## 確かめたこと
- 新テスト tests/unit/components/worktree/escape-layers-3681.test.tsx(11 件):
  - 最大化したファイルの背面の理由: 1 回目は最大化だけ解除・理由は残る、2 回目で
    理由が閉じる(陽性)。最大化なしなら 1 回目で閉じる/理由が前面(overlay を
    z 49)なら理由だけ閉じて最大化は 2 回目(陰性)。
  - エディターの最大化: 前面の層が印を付けた押下では解除しない(document と
    textarea の 2 経路、陽性)。浮遊層が無ければ 1 回目で解除(陰性)。
  - エディターが viewport layer: CSS fallback とネイティブの両方で属性が付く。
    背面の札の理由・「…」パネルは 1 回目をエディターに譲り、2 回目で閉じる。
  - 修正前の実装で 11 件中 8 件が落ちることを確認してから直した(「…」パネルの
    ケースは修正前も、登録順によりエディターの listener が先に claim して通る。
    今は登録順に依らない)。UI の対照なので一時ディレクトリは使っていない
    (実ファイルは書き換えていない)。
- npx vitest run(新テスト、escape-chip-overflow-3660 / -maximize-3670、
  VerificationStatusChip 系、MarkdownEditor*、FilePanelContent*、
  escape-stacked-layers-3630、escape-key*、TerminalSplitContainer*
  (header-in-bar-3592 を含む)、tests/unit/guards、tests/unit/docs):
  73 files / 1135 tests 合格。札の理由・MarkdownEditor・data-viewport-layer を
  参照する全テスト 26 files / 571 tests 合格。npm run lint / npx tsc --noEmit 合格。
  changelog-fragments check exit 0。

## 対になる場所(探した結果)
- isUnhandledEscape の全受け手を grep。escape-key.ts は変えていないので、他の
  受け手(RepositoryTabBar / FileMetadataToggle / ContextMenu / WorktreeBreadcrumb /
  FileViewer / Tooltip / useActivityPaneOverlay / useContextMenu /
  ActionBarItemsOverflow / FilePanelContent)の動きは不変。
- スマホの写し: 札の理由の inline(MobileHeader 直下)は上記のとおり判定できず
  従来どおり。src/cli/types/ に関係する型は無い。docs/ の ja/en に Esc の層の説明は
  無かった(docs/module-reference.md は断片で追記)。
- 本文に無い指摘: src/components/worktree/FileViewer.tsx:505 全画面の window Esc
  解除は isUnhandledEscape だけを見る(#3677 でも報告済み)。isEscapeNoLayerClosed
  と data-viewport-layer の候補。scope 外のため未変更。
- 本文に無い指摘: src/components/worktree/VerificationStatusChip.tsx の
  handleRootKeyDown(札の中にフォーカスがあるときの React の Esc)は背面判定を
  見ない。最大化の背面の札へ Tab で移って Esc を押すと、理由が閉じて押下を
  claim し、最大化は残る(逆順)。契約の決定 (1) が document listener に限るため
  変えていない。
- 本文に無い指摘: スマホの inline の理由は、エディターの CSS fallback 最大化の
  背面でも document の Esc で一緒に閉じうる(札の箱の中の absolute z-50 で、
  isCoveredByViewportLayer では比べられない)。

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Issue 本文の事象 1・2 だけを直す。事象 3(terminal-highlight-mermaid.ts の
図の外の照合の整理)は契約どおり触らない。

## 実コードとの照合
- Issue の説明どおりだった。`mapMarkdownPositionsToDom` は 1 つの `&` に 2 単位
  以上が並んだときだけ `rawEndToDom[;]` を書く(`if (last === k) return;`)。
  `alignSubsequence` の空白は `placePending` が原文の空白だけを探し、
  `&ThickSpace;` の `&` は踏み越すだけ(無ければ -1)。

## 決めたこと
- `mapMarkdownPositionsToDom`: `&` に置かれた単位の並びが、そこの参照の描いた
  もの(`referenceAt(...).drawn`)と長さ・中身とも一致し、次の単位が `&` の次の
  原文に置かれていない(=インラインコードのように書かれたとおりではない)とき、
  単位数にかかわらず `rawEndToDom[;]` を「描いた最後の単位 + 1」にする。
  開始の写像(`rawToDom`)は変えない。
- `alignSubsequence`: 待っている空白を置くとき、原文の空白より前に、待っている
  単位と描いたものが一致する空白の参照(`&ThickSpace;`、`&NewLine;` など)が
  あれば、描いた単位すべてをその `&` に置く(ほかの参照と同じ置き場所)。
  一致しなければ従来どおり原文の空白だけを探す(無ければ -1)。
  これで `&ThickSpace;` の 2 単位は `&` の上の並びになり、上の終端の処理に届く。
- 理由: 終端の処理を「並び(2 単位以上)」から「参照の描いたものと一致する並び」
  に一般化するだけで、1 単位・2 単位・空白が同じ規則になる。開始側は変えないので、
  `&` だけのヒット・名前の途中で終わるヒット(`rawEndToDom` は `;` にしか書かない
  ので null のまま)・#3661 の後ろの語は構造上そのまま。
- 捨てた案:
  - 1 単位のときも無条件に `referenceAt` の `;` へ終端を書く(`last === k` を外す
    だけ): インラインコードの `&amp;` は `&` 1 単位が参照の描いたもの(`&`)と
    同じに見え、`;` の終端が一時的に `&` の後ろに書かれる。同じ forEach の後で
    `;` 自身が上書きするので結果は同じだが、順序に頼るので「次の単位が `&` の次の
    原文か」で書かれたとおりの参照を先に外した。
  - 空白の参照の全体を `rawToDom` の範囲(`&`〜`;` 全部)に写す: 名前の途中で
    終わるヒットまで置かれ、陰性(今どおり)に反する。
  - 空白の枝で参照を見つけたら、その場で(`placePending` を待たずに)置く: 空白が
    原文の空白と参照の両方から来るとき(`a &ThickSpace;b`)の順序の扱いが二重に
    なる。`placePending` の中で原文の空白と比べて前にある方を取る形に寄せた。
- 変わる動き(意図したもの): 空白の参照の `&` だけのヒットは、`&fjlig;` の `&`
  だけのヒット(`f`)と同じく、描いた最初の単位に置かれるようになる(以前は null)。
  `&NewLine;` の描いた改行も、原文の改行ではなく `&` に置かれる(以前は -1)。
  #3661 の `&NewLine;` のテスト(後ろの語の置き場所)は前後とも通る。

## 既存テストの期待値を変えた 2 件(決定で許された 2 件のみ。it の名前は不変)
- `terminal-highlight-3661.test.ts`「a reference that draws a character is placed
  on its `&`」の `&lt;` 全体 → null、`terminal-highlight-reference-3671.test.ts`
  「a reference drawing one character is still not placed by a hit on its whole」
  の `&lt;` 全体 → `[null]`。どちらも「`;` に描いた単位が結びつかない」という
  #3671 時点の実装の制約を固定していたもので、仕様としての null ではない。
  この Issue の受入基準(`&lt;` の全体で `<` を強調する)で制約そのものを外す
  ので、null は正しくなくなる。期待値を `<` に変え、コメントを書き直した。
  3671 の it 名("still not placed")は名前を変えない決定に従い残した。

## 確かめたこと
- 新規 `tests/unit/lib/terminal-highlight-reference-3682.test.ts`: 陽性 4 件
  (`&lt;` → `<`、`&amp;` → `&`、`&ThickSpace;` → U+205F U+200A、原文の空白と
  並ぶ `a &ThickSpace;b`)は直す前の実装で 4 件とも落ち、直した後に通る。陰性 6 件
  (名前の途中で終わるヒット・名前の文字だけのヒット・`&lt;` の `&` だけ → `<`・
  参照から後ろの語まで・前後の語・インラインコード内は書かれたとおり)は前後とも通る。
- 新規 `tests/unit/components/worktree/chat-search-highlight-3682.test.tsx`
  (chat-search-highlight-3671 に倣う): Chat(ChatTranscript)と History
  (HistoryPane)の両方で、`&lt;` と `&ThickSpace;` の現在でないヒットが描いた範囲で
  強調され、現在のヒットに overlay が出る。直す前は 2 面×2 件とも落ちる。陰性
  (参照の後ろの `sentinel`)は前後とも通る。
- DOM だけで完結するので一時ファイルは作っていない。実リポジトリのファイルは
  書き換えていない。
- `npx vitest run tests/unit/lib/terminal-highlight tests/unit/components/worktree/chat-search
  tests/unit/components/worktree/chat-mermaid tests/unit/components/worktree/HistorySearchBar
  MermaidDiagram-real-3503 tests/unit/app/file-viewer-mermaid-3522.test.tsx tests/unit/guards
  tests/unit/docs`(72 files / 1045 tests、chat-search-mermaid-3671 と Mermaid の既存
  テストを含む)、`npm run lint`、`npx tsc --noEmit`、
  `node scripts/changelog-fragments.mjs check` が通る。テスト全体は走らせていない。

## 対になる場所を探した結果
- 原文→DOM の写像と参照の置き方は `terminal-highlight-markdown.ts` の
  `alignSubsequence`/`mapMarkdownPositionsToDom` だけ。呼び出し元は
  `terminal-highlight.ts` の 1 か所で、Chat・History・ターミナルは同じ経路
  (terminal-highlight.ts は変更不要だった)。`terminal-highlight-mermaid.ts`・
  `terminal-highlight-offsets.ts` に参照の単位数で分ける写しは無かった。
  `src/cli/types/` に該当する型は無い。`docs/` には module-reference の
  terminal-highlight-markdown.ts 行だけ(断片に追記を書いた)。en の docs に
  記述は無かった。

## 本文に無い指摘
- src/lib/terminal-highlight-mermaid.ts(`mapRawPositionsToDom` の図の外の序数の
  照合): Issue の事象 3。どこからも結果が使われていない。契約により直していない
  (整理は別 Issue)。

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Orchestrator-Fold: module-reference

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
段 1 の結論: (b) — 図の外を null にする案 (a) は #3503・#3525・#3544 の既存テスト 5 本の期待を変えざるを得ないため捨てた。コードは変えず、今の振る舞いを固定するテストだけを足す。

## 確かめたこと
- `placeRawPositions`(src/lib/terminal-highlight.ts)の `if (!fromMarkdown) return mermaid;` は Issue 本文どおり。
  `mapMarkdownPositionsToDom` が null になるのは、容器に `data-search-raw` の要素が 1 つも無いときだけ
  (要素はあるが揃わないときは null ではなく、全件 null の配列)。
- 描画の経路(読むだけ):
  - Chat(ChatMessageBubble の ChatMarkdownBody): 本文・思考・ツールログの 3 つとも `rehypeSearchRawText` を通す。
    p/li/td/th/strong/em/リンク/画像は `searchRawProps` で印を渡す。印が付かないのは
    (1) 本文が Mermaid の図だけの返答(図のソースの `<code>` は MERMAID_SOURCE_ATTR だけで印が無い)。
        この場合、図の外の本文のテキストは無く、畳んだ思考・ツールログは閉じている間 DOM に無いので、
        図の外のヒットは (a) でも (b) でも置かれない。開けば印が付き fromMarkdown は null でなくなる。
    (2) `chatMarkdownRawOffsets` の `mapDerivedLines` が null(行の連結が派生文字列と合わない)を返す防御的な経路。
        このとき本文に印が付かず、MermaidRawOffsetContext も null になり、図の外は序数の照合で置かれる。
  - History(ConversationPairCard): 常に `rehypeSearchRawText`(恒等写像)を通すので、図の外の要素には印がある。
  - ファイルのプレビュー(MarkdownPreview)は applyHistoryHighlights を呼ばない。
  したがって本番で null の経路に図の外のヒットが乗るのは (2) の防御的な経路だけ。一方、印の無い DOM で
  図の外の素の語を序数の照合で正しく置く例は既存テストに実在する(#3503 の 3 本・#3525・#3544 の各 1 本)。
- (a)(fromMarkdown が null のとき図の外を null)を試しに入れると、次の 5 本が落ちた(期待値の変更が要る):
  - terminal-highlight-mermaid-3503: marks every hit on its own word… / scrolls the current hit inside a diagram… /
    a hit in an indented diagram opens and marks that diagram's source
  - terminal-highlight-mermaid-3525: the paragraph's hit lands in the paragraph, the diagram's in the source
  - terminal-highlight-mermaid-3544: the paragraph's hit stays in the paragraph; the diagram's hit is marked in its source
  契約により既存の期待は変えず、(b) を採った。

## 足したテスト(tests/unit/lib/terminal-highlight-mermaid-outside-3692.test.ts)
- 印が無いとき: 図のソースの中のヒットは図の中に置かれる/図の外の素の語は自分の段落の語に置かれる(陽性)、
  画面に無い原文(`**alpha**` の `*alpha`)は置かれず図の中のヒットだけ置かれる(陰性)。
- 印があるとき(対照): `&#97;lpha alpha` の前では Markdown が 2 語目に置く。印を外すと序数の照合が 1 語目に置く
  ((b) が残す限界として固定)。
- 陽性対照: (a) を入れた実装では新しいテストのうち 3 本が落ちることを確かめた(実装は元に戻した)。

## 対になる場所
- applyHistoryHighlights を呼ぶのは ChatTranscript と HistoryPane の 2 か所で、どちらも同じ placeRawPositions を通る。
  写しは無い。src/cli/types/ に該当する型は無い。docs/ で placeRawPositions・mapRawPositionsToDom に触れるのは
  docs/module-reference.md だけで、振る舞いが変わらないので更新不要。

## 本文に無い指摘
- src/lib/terminal-highlight-mermaid.ts:213-263 付近 gaps の削除(Issue の受入基準 1)は、(b) のままでは
  上の 5 本の既存テスト(印の無い手組みの DOM で図の外を序数で置くことを固定)と両立しない。段 2 で削るには、
  先にそれらのフィクスチャに `data-search-raw` の印を付ける(または期待を変える)決定が要る。
- src/lib/terminal-highlight.ts の (2) の経路(mapDerivedLines が null)では、文字参照・Markdown の記号・
  インラインコードの前で序数の照合が別の語に置く(#3671 と同じ誤り)。新しいテストの最後の 1 本がこの限界を示す。

## #3555 との関係
- (b) では序数の照合(gap の計算)が残るので、#3555 が source の無い領域で予備の照合が要る理由に挙げた
  「後ろの同じ語の何番目の出現か」の前提は変わらない。

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): FileViewer の全画面・札の中の Esc・inline の理由も、Esc を前面の層から 1 層ずつ閉じる (#3691)

## 決めたこと
- 実コードは Issue の説明どおりだった(FileViewer の全画面解除と統合画面の段は
  isUnhandledEscape のみ・data-viewport-layer なし、handleRootKeyDown は背面判定なし、
  inline の理由は判定も印も無し)。escape-key.ts は変更不要だったので変えていない。
- (1) FileViewer: 読み取り専用の全画面(window listener)と統合画面の Escape の段は
  isEscapeNoLayerClosed を読む。段の分岐の順(検索→最大化→閉じる)は不変。
  全画面の要素に VIEWPORT_LAYER_ATTRIBUTE を付ける(採用)。読み取り専用の全画面
  overlay(data-testid="file-viewer-fullscreen" を追加)と、統合画面
  (markdown-file-screen)の両方。統合画面は最大化していなくても fixed inset-0 の
  z 55 で画面全体を覆うので、常に付ける。理由: 背面の理由・「…」パネル(z 50)は
  見えないまま最初の Esc を受けて閉じ、画面側は何も変わらない(利用者には Esc が
  効かなかったように見える)。付ければ最初の Esc は画面が取り、背面の層は残る
  (#3670 のファイル最大化・#3681 のエディター最大化と同じ)。両方ともルートの
  重なり文脈(スマホの shell の中、z 付きの祖先なし)にある。
- 同じ場所の別の条件: 全画面の検索欄の Esc は検索を閉じるだけで印を付けておらず、
  読み取り専用の全画面では検索欄と全画面が 1 回で同時に閉じていた。検索欄の
  onKeyDown で markEscapeClosedLayer を付けた(統合画面の段は検索分岐が同じ押下で
  closeSearch を重ねて呼ぶだけだったので、印で早期 return しても結果は同じ)。
- (2) handleRootKeyDown は document の経路と同じ背面判定(isReasonCovered)を通し、
  背面なら閉じず preventDefault もしない。
- (3) inline の理由は判定に「inline の理由の要素そのもの」を使う(採用)。z-index を
  class の z-50 から同じ値(Z_INDEX.MODAL)の inline style へ移した(見た目は不変)。
  札のルートや inline の理由に z 付きの祖先は無く(スマホの札の帯は z なし)、
  理由は FileViewer・CSS 最大化のエディターと同じルートの重なり文脈で z 50 として
  重なるので、要素自身の z を比べるのが実際の重なり順と一致する。浮遊・inline の
  どちらの理由も同じ isReasonCovered で読み、閉じたときは両方とも印を付ける。
  捨てた案:
  - 札のルートで判定: ルートは z を持たず stackOrder が null(常に「覆われていない」)。
    ルートに z を付けると重なり文脈ができ、inline の理由の重なり順が札の z に
    閉じ込められて見た目が変わる。
  - z に依らず「viewport layer があれば背面」とみなす: 前面の理由(z を上げた場合)
    でも閉じなくなり、#3681 の陰性(理由が前面なら理由だけ閉じる)と規則がずれる。
- 定数 FLOATING_POPOVER_Z_INDEX は inline でも使うので POPOVER_Z_INDEX に改名。

## 確かめたこと
- 新テスト tests/unit/components/worktree/escape-layers-3691.test.tsx(18 件):
  - 1: 読み取り専用の全画面・統合画面とも、前面の層が印を付けた押下では残る
    (陽性)/前面に何も無ければ解除・段が進む(陰性)。前面の inline の理由は理由だけ
    閉じる、背面の inline の理由・「…」パネルは最初の Esc を全画面に譲る。検索欄の
    Esc は検索だけ閉じる。両画面に data-viewport-layer が付く。
  - 2: 最大化したエディター/全画面の背面の理由で、札の中の Esc は理由を閉じず
    押下も claim しない(陽性)。前面に層が無ければ理由が閉じ claim する(陰性)。
  - 3: inline の理由+CSS 最大化のエディターで 1 回の Esc は最大化だけ解除、2 回目で
    理由(陽性)。最大化なしなら 1 回で閉じる(陰性)。
  - 直す前の実装で陽性 13 件(検索欄の 1 件は別に)が落ち、陰性 4 件が通ることを
    確認してから直した。UI の対照なので一時ディレクトリは使っていない(実ファイルは
    書き換えていない)。
- npx vitest run(新テスト、escape-layers-3681、escape-chip-overflow-3660 /
  -maximize-3670、escape-stacked-layers-3630、escape-file-metadata-toggle-3650、
  VerificationStatusChip / verification-chip-*、FileViewer*、HeaderToolBar-3591、
  DesktopHeader*、TerminalSplitContainer*(header-in-bar-3592 を含む)、
  escape-key*、WorktreeDetail*、tests/unit/guards、tests/unit/docs):
  101 files / 1410 tests 合格。npm run lint / npx tsc --noEmit 合格。
  changelog-fragments check exit 0。既存の it / describe と期待は変えていない。

## 対になる場所(探した結果)
- isUnhandledEscape の全受け手(RepositoryTabBar / TerminalSplitContainer /
  FileMetadataToggle / ContextMenu / WorktreeBreadcrumb / Tooltip /
  useActivityPaneOverlay / useContextMenu)は escape-key.ts を変えていないので不変。
  FileViewer からは isUnhandledEscape が無くなった。
- PC とスマホ: FileViewer はスマホだけで使われる(WorktreeDetailRefactored)。PC の
  ファイル最大化は FilePanelContent(#3670 で対応済み)。札は PC=floating、
  スマホ=inline で、両方を同じ判定にそろえた。src/cli/types/ に関係する型は無い。
  docs/ の ja/en に Esc の層の説明は無かった(docs/module-reference.md は断片で追記)。
- 本文に無い指摘: src/components/worktree/FileSearchBar.tsx:65 の検索欄の Esc は印を
  付けず、FilePanelContent の最大化(:899 の window listener)と同じ押下で両方閉じる
  (FileViewer の検索欄と同じ形)。scope 外のため未変更。
- 本文に無い指摘: src/components/ui/Modal.tsx:93 の Esc は e.key だけを見て
  isUnhandledEscape も印も読まない。FileViewer の Modal 表示(全画面でないとき)で
  検索欄の Esc は検索と Modal を同時に閉じる。scope 外のため未変更。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): 背面の検索欄は Esc を閉じず印も付けず、前面のエディターの最大化に譲る (#3691)

整合性レビュー(Codex)の P2 への対応。

## 決めたこと
- FileViewer の検索欄の Esc は、検索欄が viewport layer の背面にあるときは閉じず、
  markEscapeClosedLayer の印も付けず、preventDefault もしない(もともとしない)。
  前面のときは今どおり、検索だけ閉じて印を付ける。
- 背面判定(isBehindViewportLayer、FileViewer 内の関数): 検索欄の入力は z を持たない
  ので、要素自身では isCoveredByViewportLayer を使えない。代わりに、検索欄を収める
  FileViewer の画面(data-viewport-layer を持つ最も近い祖先)を札の理由と同じ
  isCoveredByViewportLayer で判定する。埋め込みエディターの CSS 最大化は z 55 の
  body portal で、同じ z・文書順で後ろなので画面を覆う。加えて、画面の中に入れ子の
  viewport layer があり検索欄の祖先でなければ背面とみなす(ネイティブ Fullscreen の
  エディターは元の位置のまま top layer に載るので、z の比較では拾えない)。画面の外
  (Modal 表示)では覆うものが無いので前面のまま。
- 捨てた案: escape-key.ts に汎用の「祖先の層で判定する」関数を足す — 受け手は今
  FileViewer の検索欄だけで、他の受け手(札・「…」パネル)は自分の z を持つ。

## 確かめたこと
- escape-layers-3691.test.tsx に 2 件追加: 統合画面で検索を開いたまま編集に切り替え、
  Ctrl+Shift+F で埋め込みエディターを CSS 最大化して検索入力で Esc → 最大化が解除され
  検索は残る(陽性、直す前の実装で落ちることを確認)/最大化なしなら検索だけ閉じ、
  画面は閉じない(陰性)。既存の it / describe と期待は変えていない。
- npx vitest run(escape-layers-3691 / -3681、escape-chip-overflow-*、
  escape-stacked-layers-3630、escape-file-metadata-toggle-3650、
  VerificationStatusChip / verification-chip-*、FileViewer*、MarkdownEditor*、
  TerminalSplitContainer*、escape-key*、tests/unit/guards、tests/unit/docs):
  75 files / 1165 tests 合格。npm run lint / npx tsc --noEmit 合格。
  changelog-fragments check exit 0。

## 対になる場所(探した結果)
- FileViewer の検索欄は FileViewerSearchBar 1 つで、読み取り専用の全画面・統合画面・
  Modal の 3 経路が共有する。範囲外の FileSearchBar.tsx・Modal.tsx は報告済みのため
  再報告しない。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(polling): 退役した tick が即時保存の分岐で古いキーのまま保存・配信・完了通知しない (#3684)

## 決めたこと
- 案 C(項目 3 だけ)。recordOrHoldScrapedResponse の保留しない分岐の直前に、hold と同じ
  mayActForPollerKey(pollerKey) を判定する分岐を足した(else if (!suppressScrapedHistory && !mayAct…))。
  拒否したときは createMessage・broadcastMessage・onRelayTurnCompleted のどれも行わない。
  判定と 3 つは同期の一続きで、間に await は無い(関数自体が同期)。
- 見送りの扱いは hold の拒否に合わせた: info ログ 'scraped-response-save-refused'
  (worktreeId・cliToolId・instanceId・generation。hold の 'pending-scrape-hold-refused' と同じ項目)、
  関数の戻り値は void のまま、呼び手(recordCompletedResponse)の以降の処理と戻り値も変えない。
- 保証は DB 行・broadcast・relay 通知だけで、その前の Markdown のログ保存(recordClaudeConversation)と
  呼び手の状態更新は止めない旨を判定の横のコメントに書いた。poller-owner.ts は mayActForPollerKey の
  doc に #3684 の用途を 1 行足しただけ(判定の中身は不変)。
- response-poller-core.ts と worktree-session-reconcile.ts は変えていない。

## 確かめたこと
- 実コードは Issue の説明どおり(分岐は response-checker.ts の recordOrHoldScrapedResponse、
  保存の直前の最後の await は recordClaudeConversation)。
- 新しいテスト tests/unit/lib/polling/response-checker-retired-immediate-save-3684.test.ts(11 件、
  #3663 の suite と同じく timer → runPollTick → ownerStorage.run → checkForResponse を通す)。
  陽性 5 件(停止→移行・移行のみ・60 秒超の再起動・停止した A の自キー保存・最後の await の間の停止)は
  直す前の実装で落ちることを確認してから直した。陰性 6 件(現 owner・移行先の後継・60 秒未満の
  superseded と再起動の完了・paused の再開・A の ALS からの B の再起動による B の保存・退役しない保存・
  多対一で上書きされた timer が checker を呼ばず後継の登録を消さない(項目 2 の見立ての固定))は前後とも通る。
- npx vitest run tests/unit/lib/polling tests/unit/polling tests/unit/guards tests/unit/docs: 110 files /
  1741 passed(response-poller-migrate-pending-scrape-3663 と response-checker-* を含む。1 回目は負荷で
  2230・2223 の既存テストが 5000ms timeout、単独・再実行で通過)。
- npm run lint / npx tsc --noEmit / node scripts/changelog-fragments.mjs check: exit 0。

## 対になる場所を探した結果
- src/lib/polling/ の createMessage / onRelayTurnCompleted の呼び手: response-checker の prompt 行
  (saveAndNotifyPrompt 付近)・即時保存・hold の分岐、pending-scraped-response の flush(#3663 で判定済み)、
  structured-history-gate の転写行。src/cli/types/ に該当する型の写しは無し。docs/ の ja/en に
  この分岐を説明する箇所は無し(module-reference は断片で追記)。

## 本文に無い指摘
- src/lib/polling/response-checker.ts:790 prompt 行の createMessage と broadcast は owner を見ない。
  リーダー等の await の後で退役した tick が prompt を検出した場合に旧キーで行を書きうる(未確認・未変更)。
- src/lib/polling/response-checker.ts:1130 hold の分岐は、hold が拒否されても onRelayTurnCompleted を
  旧 ID で出す(#3663 の既存テストがこの relay 経路を使っているため変えていない)。
- src/lib/polling/structured-history-gate.ts:611 転写経路の onRelayTurnCompleted(…, true) と転写行の書き込みも
  呼び手の owner を見ない(scope 外)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(polling): 保留を拒否された退役 tick は relay の完了通知も出さない (#3684)

## 決めたこと
- 整合性レビュー P2: recordOrHoldScrapedResponse の hold の分岐で、onRelayTurnCompleted を
  `if (held)` の中へ移した。保留の倉庫が拒否する(停止した鎖の tick が自分のキーへ hold する)ときは
  通知を出さない。即時保存の分岐(前のコミット 2b5427d)と同じ扱い。拒否のログは従来どおり倉庫の
  'pending-scrape-hold-refused'。superseded だけの tick・paused の再開・別キーの操作は holdScrapedResponse が
  従来どおり受理するので、通知も従来どおり出る。
- response-poller-migrate-pending-scrape-3663.test.ts の「negative: a retired tick still cannot hold, drop or
  write under its own key」は、旧 A tick の async 文脈へ入る足場を relay.hook から、倉庫の拒否ログ
  (mockLogger.info の 'pending-scrape-hold-refused'、worktreeId=A。その tick が必ず通る)へ移した。
  1 回だけ侵入し(seedHold が同じログを再度出すため再入を防ぐ)、finally で実装を戻す。侵入が実際に
  走ったことを expect(intrusions).toBe(1) で示す。expect の期待値・it/describe の名前は不変。
  同じファイルの relay.hook のもう 1 か所(:848「writes that session's held scrape once」)は、現 owner の
  A が hold を受理する経路で通知は今も出るので変えていない。

## 確かめたこと
- 3684 の suite に ⑥ を追加(3 件)。陽性「reader 待ちの間に停止し not_yet_closed で戻った tick は hold を
  拒否され、通知を出さない」は直す前の実装で落ちることを確認してから直した。陰性「現 owner の hold は
  通知 1 回」「60 秒未満の superseded だけの tick も hold して通知 1 回、再起動で行が書かれる」は前後とも通る。
- npx vitest run tests/unit/lib/polling tests/unit/polling tests/unit/guards tests/unit/docs: 1744 passed。
- npm run lint / npx tsc --noEmit / node scripts/changelog-fragments.mjs check: exit 0。

## 対になる場所を探した結果
- テストで relay.hook / onRelayTurnCompleted の mock を「拒否された hold の後の文脈」の足場に使っているのは
  3663 の suite の 1 か所だけ(tests/unit/lib/polling・tests/unit/polling を grep、ほかの suite は現 owner の
  hold の通知を数えるだけで、全件通過)。
- レビューの P3 2 件(response-checker.ts の prompt 行の保存、structured-history-gate.ts の転写経路)は
  契約どおり直していない(別 Issue)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#3692 はコードの変更が無く、断片は「追記なし」。

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
決めたこと:
- 項目 1: savePromptMessage の先頭(isDuplicatePrompt より前)で
  mayActForPollerKey(pollerKey) を見る。拒否なら dedup を更新せず、行・
  message 配信・task event・Push・待ち episode を出さずに false。info
  `prompt-save-refused`(#3684 のログと同じ形: worktreeId/cliToolId/
  instanceId/generation)。関数内に await は無い。
- 項目 2(a): captureStructuredHistoryTurn の reader の await の後・
  claimCompletionAnnouncement の前で `${worktreeId}:${resolvedInstanceId}`
  (getPollerKey と同じ形。gate キューの NUL 区切りキーは使わない)で判定。
  拒否なら通知も claim もしない。info `structured-history-completion-refused`。
  戻り値・report は reader の結果のまま。
- 項目 2(b)(reader 内の書き込み)は直さない。判定の近くのコメントに残る旨を記載。
- reader・poller-owner.ts・prompt-dedup.ts は変更なし。

確かめたこと:
- 新規テスト response-checker-retired-prompt-transcript-3701.test.ts は実際の
  timer → runPollTick → pollerOwnerStorage.run → checkForResponse と、実際の
  structured-history-gate(per-instance キュー込み)を通す。codex の transcript
  reader と pane capture だけを止められる fake にした。
- 陽性 4 件(prompt を保存/配信しない、後継が同じ prompt を保存できる、
  完了を通知しない、後継が同じ completion を通知できる)は修正前の実装で
  落ちることを確認してから直した。陰性(owner のままの tick、superseded だけの
  tick、別キーの停止、Stop hook がキュー上で停止 tick の後ろに並ぶ場合と
  tick の外単独)は修正前後とも通る。
- 実行: tests/unit/lib/polling tests/unit/polling tests/unit/guards tests/unit/docs
  (111 files / 1756 passed)、tests/unit/hooks ほか gate/checker を参照する unit
  (280 files passed)、関係する integration 10 本(67 passed)、npm run lint、
  npx tsc --noEmit、changelog-fragments check。#3663・#3684 の既存テストは
  名前・期待値を変えずに通る。既存テストに退役 tick の prompt 保存・gate 通知を
  足場にしたものは見つからなかった(修正後も全て緑)。

対になる場所の探索:
- messageType 'prompt' の createMessage: response-checker.ts のほかは
  hooks/permission-decision-service.ts・session/current-output-history-writers.ts・
  db/chat-db.ts。いずれも poller の tick の外(route/hook)なので対象外。
- onRelayTurnCompleted: response-checker.ts の 2 か所は #3684 で対応済み、
  gate の 1 か所を今回直した。recheckDuplicateResponse 経由の gate 呼び出しも
  gate 内の判定で覆われる。
- docs の ja/en・src/cli/types に該当する記述・型の写しは無し。

本文に無い指摘:
- src/lib/polling/response-checker.ts extractCompletedTurn / recordCompletedResponse
  の markPendingPromptsAsAnswered(prompt の sweep と message_updated 配信)と
  updateSessionState も capture / gate の await の後に owner を見ずに走る。
  退役した tick が pending の prompt 行を answered にしうる。
- src/lib/hooks/sources/codex/history.ts ほか reader 内の行の追加・更新・
  カーソル更新(項目 2(b))は残る。

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): ファイルパネルの検索欄と共通の Modal も Esc を前面の層から 1 層ずつ閉じる (#3700)

## 決めたこと
- isBehindViewportLayer は FileViewer.tsx から src/lib/escape-key.ts へ移して export
  した。理由: 中身は DOM と VIEWPORT_LAYER_ATTRIBUTE / isCoveredByViewportLayer だけに
  依存する層の判定で、escape-key.ts の既存の判定群と同じ層にある。FileSearchBar が
  FileViewer(重い画面コンポーネント)を import する依存の向きを作らずに済む。
  FileViewer 側は import 先が変わっただけで本体は同一(振る舞い不変、#3691 のテスト通過)。
- FileSearchBar の Esc は FileViewerSearchBar と同じ形: 背面なら閉じず印も付けない/
  前面なら markEscapeClosedLayer して閉じる。最大化していないパネルでは host が無く
  背面判定は false なので、従来どおり閉じる。
- Modal の Esc を isEscapeNoLayerClosed に変更。disableClose の分岐は不変。

## Modal の呼び手(<Modal は 25 か所。ui/Modal.tsx 自身を除く)
ConfirmDialog, SettingsDialog, DirectoryPickerModal, NewTaskDialog, MoveDialog,
OpencodeTurnDiffPanel, WorktreeDetailSubComponents(:1392), AgentAddMenu,
AutoYesConfirmDialog, FilePanelTabs, WorktreeDetailDesktop(:1193),
WorktreeDetailRefactored(:1283), InstanceCliCommandsModal, NewFileDialog,
VerificationPane, TodoPane, TimerDetailModal, TimerEditDialog, FileViewer(:1173),
ScheduleEditDialog, AppUpdateButton, WhatsNewDialog, KeyboardShortcutsOverlay,
ExternalAppForm, SkillUpdateDialog
- 中身に Escape を印付けする要素があるのは FileViewer のモーダル表示(:1153 以降の
  return)の検索欄(FileViewerSearchBar、markEscapeClosedLayer)だけ。前面の検索欄が
  閉じる押下で Modal も閉じていたのが止まるだけで、意図どおり(次の Esc で Modal は
  閉じる)。FileViewer の MarkdownEditor(最大化の Esc で preventDefault)と
  MobileFileActionsSheet(stopPropagation)は統合画面・全画面側だけで描かれ、Modal の
  中には出ない。
- ほかの 24 か所の中身(NewTaskDialog / NewFileDialog の onKeyDown を含む)に Escape
  を扱う要素は無い。Radix のメニュー類(DismissableLayer は閉じるとき preventDefault
  する)も Modal の中には無い(AgentAddMenu の Tooltip は Modal の外)。
- よって変更で Modal が閉じなくなる意図に反する呼び手は無い。

## 確かめたこと
- 新規テスト escape-layers-3700 / modal-escape-3700 の陽性 5 件は直す前の実装で落ち、
  陰性・disableClose は直す前から通ることを確認してから直した。
- #3592・#3660・#3670・#3681・#3691・Modal・escape-key の既存テスト、FileViewer /
  FileSearchBar / Modal を参照する unit テスト 25 ファイル、tests/unit/guards・docs が通る。
- npm run lint・npx tsc --noEmit・changelog-fragments check が通る。

## 対になる場所を探した結果
- FileSearchBar の呼び手は FilePanelContent の MarkdownWithSearch と
  CodeViewerWithSearch の 2 つ(どちらも同じコンポーネントなので一緒に直った)。
  スマホ側は FileViewer の FileViewerSearchBar で #3691 で対応済み。
- 型の写し(src/cli/types/)と docs/ の ja/en に該当の説明は無い(module-reference は
  断片で渡す)。古い動きを固定しているテストは無かった。

## 本文に無い指摘
- src/components/common/FullScreenModal.tsx:48 も `e.key === 'Escape' && isOpen` だけで
  閉じ、印も defaultPrevented も見ない(Modal と同じ形の写し。scope 外のため未変更)。
- src/components/worktree/MarkdownEditor.tsx:818 など document / window で Esc を受ける
  層どうしは listener の登録順で先後が決まる。Modal(document、bubble)より後に登録
  された document listener が preventDefault しても Modal は先に閉じる。今の Modal の
  呼び手の中身にそうした層は無いため実害は確認していない(記録のみ)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): 最大化していないパネルの検索欄も、前面のエディターの CSS 最大化に Esc を譲る (#3700)

#3700 の整合性レビュー(Codex、P2)への対応。

## 事象
最大化していないファイルパネルには viewport layer の祖先が無いので、FileSearchBar の
背面判定(isBehindViewportLayer)が常に false だった。検索欄を開いたまま同じパネルの
MarkdownEditor を CSS 最大化(body portal、data-viewport-layer)し、背面の検索入力へ
フォーカスして Esc を押すと、検索欄が閉じて印を付け、前面のエディター(document の
処理が isEscapeNoLayerClosed を読む)は最大化を解除しなかった。

## 決めたこと
- isBehindViewportLayer は広げず、escape-key.ts に isUnderViewportLayer を足して
  FileSearchBar だけが使う。理由: isBehindViewportLayer は FileViewer の検索欄(#3691)
  も使っており、祖先が無い場合(FileViewer のモーダル表示)の判定を変えると #3691 の
  振る舞いに触れる。指示どおり FileViewer の検索欄は変えないため、追加の判定は
  FileSearchBar の経路に限った。
- isUnderViewportLayer: viewport layer の祖先があれば isBehindViewportLayer と同じ
  (最大化したパネルの検索欄の振る舞いは不変)。無ければ要素が入っているルート直下の
  箱を見て、z-index を持てば isCoveredByViewportLayer で比べ、持たなければ(ページ
  そのもの)表示中の viewport layer が 1 つでもあれば背面とする。viewport layer は
  画面全体を覆う層で、表示中だけ属性を持つので、z-index の無いページはその下にある。

## 確かめたこと
- escape-layers-3700 に陽性(最大化していないパネルの検索欄+CSS 最大化のエディター
  → Esc でエディターだけ解除、検索欄は残る)と陰性(エディターを最大化していなければ
  検索欄の Esc で検索が閉じる)を追加。陽性は直す前に落ちる(エディターの最大化が
  残る)ことを確認してから直した。既存の it / describe の名前・期待は不変。
- FileViewer / FileSearchBar / FilePanelContent / MarkdownEditor / escape-key / Modal を
  参照する unit テスト 38 ファイルと tests/unit/guards・docs(計 87 ファイル)、
  npm run lint、npx tsc --noEmit、changelog-fragments check が通る。

## 本文に無い指摘
- 無し(MarkdownEditor.tsx・FilePanelContent.tsx は変更不要だった)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
executeTimer は isRunning の await の間にキャンセルが成功しても、戻った後の
updateTimerStatus(sending) が cancelled を上書きして送信していた。

決めたこと:
- 待ちの後に getTimerById で状態を同期的に読み直し、pending でなければ return。
  await を挟まず読み直しと更新が続くので better-sqlite3 上で原子的。
- timer-db の updateTimerStatus に条件を持たせる案は採らなかった。既存テストが
  timer-db を丸ごとモックしており、戻り値の変更で既存の期待が崩れるため。
- no-session の更新も同じ読み直しの後ろに置いた。理由: キャンセル済みを
  no_session に書き換えると、利用者が止めた記録が失われるため。
- 送信開始後のキャンセルを拒む既存の動き(cancelTimer の WHERE status='pending')は不変。

確かめたこと: 新テスト timer-manager-cancel-race-3707.test.ts の陽性 2 本は修正前に
落ち、修正後に通る。陰性 2 本(通常送信・sending 後のキャンセル拒否)は両方で通る。
既存 timer-manager.test.ts・guards・docs・lint・tsc は通過。

探した対象: timer-manager.ts 内の他の updateTimerStatus 呼び出し(sending 後の
sent/failed は sending 状態からの遷移でキャンセル不可のため問題なし)。
本文に無い指摘: なし。

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(polling): 退役した tick は待ちの後に共有の状態・DB・配信・Push を書き換えない (#3705)

応答 poller の tick は await の後、自分の鎖が止まっていても掃き出し・waiting edge・
完了 Push・カーソル・障害 Push・TUI 蓄積・skip streak・recheck・リーダーの行・
Markdown ログ・snapshot を古いキーのまま書いていた。本文の「直し方」1〜5 に沿って直す。

## 決めたこと
- checker: セッション確認・保留の期限切れ・ジオメトリ委譲の await の後は
  mayStillActAfter() で問い、拒否なら info 1 行(retired-tick-refused、tick ごとに 1 行)
  で抜ける。キャプチャの後は「抜ける」ではなく、その場の答え `owns` を同期の
  ステップ(extractCompletedTurn / cleanCompletedResponse / checkForResponse の残り)
  へ渡して書き込みごとに判定し、読みは続ける。理由: 既存テストが退役 tick の流れの
  継続を固定している — 3663 の「a tick restarted past it registers nothing…」は
  キャプチャ後の退役 tick がリーダーに入るのを待ち、3684 ③ は退役 tick が Markdown
  ログを呼ぶ回数(1)を、3663 の「a retired tick still cannot hold…」は保留の拒否ログ
  を、3701 は prompt-save-refused を待つ。名前・期待は変えていない。
  リーダーの後は markOrSettle を判定し、ログ(キー付き)の後で再び問い、拒否なら
  保留/即時保存の分岐だけを通して(各分岐が自分で拒否しログを出す)false を返す。
  再問い合わせはリーダーの後に問い、settle・discard をしない(戻り値はリーダーの答え)。
- self-stop の結果を返す `return await recordCompletedResponse` には判定を足していない。
- リーダー: 4 リーダーの返答行、共通の growTurnRowTo、agy の grow・追跡リスト
  (forget / unsettledStepsFor / note)、recordUserTurn(新しい outcome 'refused')、
  advanceCapturedLineForTranscriptTurn で、最後の await の後・書き込みの直前に
  mayActForAgentInstance()(poller-owner の pollerKeyOf でキーを綴る。getPollerKey も
  これに委譲)。既存行がある場合の grow 経路は grow 側が問い、戻り値 true(行はある)は保つ。
- Markdown ログ: recordClaudeConversation(…, { pollerKey }) / createLog(…, { mayWrite })。
  poller の即時保存経路だけがキーを渡す。停止時・displaced の保留の確定
  (pending-scraped-response)はキー無しで従来どおり書く。
- core: pendingRestart は { target, behind }。引き受けた tick だけが消費する。
  takeOwnership は owners に無い停止済みの前の鎖(latestOwners)も superseded にする
  (displaced・停止後の再開始で旧 tick の最終 snapshot が後継の上に出ていた)。
- snapshot: owner を snapshotOwnerStorage で運ぶ(core は 3 引数の呼び出しのまま —
  2223 のテストが toHaveBeenCalledWith(WT, 'claude', undefined) で固定)。
  broadcastTerminalSnapshot・buildCurrentOutput / buildPayload・chat-turn progress の
  各 await の後に maySnapshotPublish()。builder は RetiredSnapshotError を投げ、
  broadcastTerminalSnapshot が info で受ける。route(store 無し)では問わない。
- 本文が「触れない」とした response-checker の clearInProgressMessageId(旧 :1539)は
  判定の外に残した。旧 :1464 の updateSessionState(…, 0) は本文の直し方 1 どおり
  ジオメトリ委譲の await の後の判定の下に入る(行自体は変えていない)。

## 確かめたこと
- 新しいテスト(*3705*、5 ファイル 85 件)の陽性は、src を develop に戻した状態で
  落ちることを確認してから直した(src の差分を退避 → git checkout -- src → 実行 →
  git apply で戻す)。
- npx vitest run: tests/unit/lib/polling tests/unit/polling tests/unit/hooks
  tests/unit/realtime tests/unit/lib/session tests/unit/guards tests/unit/docs ほか、
  変更モジュールを import する unit 173 ファイル、integration(poller / history /
  current-output / ws-publisher / logs の 10 ファイル)がすべて通る。
  #2223・#2230・#2399・#2436・#3663・#3684・#3701 の既存テストは無変更で通る。
- npm run lint / npx tsc --noEmit / node scripts/changelog-fragments.mjs check が通る。

## 対になる場所(探した対象と結果)
- 4 リーダーの写し: claude / codex / antigravity / command-code を同じ形で直した。
  opencode/history.ts は SSE の writer で tick の外(poller の ALS を持たない)ので対象外。
- structured-history-gate の完了通知の判定(既存): キーを pollerKeyOf で綴るように
  し、「リーダー自身の書き込みは対象外」のコメントを更新。
- src/cli/types の型の写し: 該当なし。docs/ ja・en: mayActForPollerKey を説明する
  ページは docs/module-reference.md だけ(断片で扱う)。

## 本文に無い指摘
- src/lib/hooks/sources/transcript-history.ts の resolveSessionIdFromEvents
  (pointers.set)、codex/history.ts の rolloutPaths、command-code/history.ts の
  transcriptPaths、antigravity/history.ts の conversationPointers: 退役 tick も await の
  後に書くが、イベント・ファイルシステムの事実のキャッシュで後継の誤動作にならないため
  変えていない。
- Auto-Yes poller(src/lib/auto-yes-poller.ts ほか)の同じ問題は本文どおり別 Issue。
- 退役 tick が refused になってもリーダーの読み取り(転写ファイルの読み込み)は走る。
  AbortSignal で早く止める補助は入れていない。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(polling): 重複分岐と progress の記憶にも退役 tick の判定を入れる (#3705)

#3705 の整合性レビュー(Codex)の P2 2 件への対応。461097f の上に足す。

## P2-1 停止済み tick が重複分岐から後継を更新する
- isDuplicateResponse はハッシュ一致なら所有を問う前に true を返す(判定は登録だけを
  断る)。キャプチャ待ちの間に退役し、後継が同じ返答を保存してハッシュを登録した後に
  旧 tick が戻ると、owns=false でも recheckDuplicateResponse に入り、skip ログの
  claim・カーソル(updateSessionState)・再問い合わせの claim とリーダーを後継のキーに
  書いていた。recheckDuplicateResponse の先頭で mayStillActAfter(ctx, 'capture') を
  問い、拒否なら false で抜ける(書き込みまで await 無し)。response-dedup.ts は変えて
  いない(#3663 の判定のまま)。呼び手の「退役 tick は来ない」コメントを訂正。
- 構造のガード: recheckDuplicateResponse を scope 'all' にし、最初の await より前の
  書き込み(claimDuplicateResponseSkipLog・updateSessionState・
  claimStructuredHistoryRecheck)も対象にした。検出器の対照に、この形(判定の無い
  まま await の前に書く)の違反例と、先頭で判定する許可例を足した。

## P2-2 配信を拒否した progress が後継の同本文を抑止する
- buildChatTurnProgress を、await を含む prepare(読み・境界・no-change の判定、記憶
  しない)と同期の commit(本文・turnKey・version の記憶)に分けた。
  emitChatTurnProgress は prepare の後に maySnapshotPublish() を問い、通ったときだけ
  commit して配信する(commit から broadcast まで await 無し)。拒否した本文は記憶に
  残らず、後継の同じ本文は配信される。buildChatTurnProgress(エクスポート)は
  prepare→commit を続けて行い、振る舞いは変わらない。snapshot の外(HTTP の
  current-output から publishChatTurnProgress、opencode の SSE)は判定が常に true なので
  従来どおり。ガードの emitChatTurnProgress の対象に commitChatTurnProgress を足した。

## 確かめたこと
- 陽性(退役 tick が後継のカーソル・skip streak・recheck を変えない/build 完了から
  配信までの間に退役した progress が後継の同本文を抑止しない/ガードの
  recheckDuplicateResponse)は、直す前の実装で落ちることを確認してから直した
  (カーソル 42 → 9997、後継の emit が false、ガードの違反 3 件)。陰性: 後継自身の
  次の tick は重複としてカーソルを進めリーダーを 1 回呼ぶ/配信した本文は記憶され同じ
  本文は再送しない/snapshot の外では配信され version が続く。
- 既存の it / describe の名前・期待は変えていない。npm run lint・npx tsc --noEmit・
  changelog-fragments check と、polling / hooks / realtime / session / guards / docs と
  変更モジュールを import する unit(542 ファイル)が通る。

## 本文に無い指摘
- なし(探した対象: isDuplicatePrompt 側の同じ形 — savePromptMessage は dedup の前に
  #3701 の判定があるので該当しない。progress の記憶を持つ他の経路 —
  publishChatTurnProgress / opencode の SSE は emitChatTurnProgress を通るので今回の
  修正に含まれる)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(auto-yes): 待ちの後に tick の世代を照合し、旧い tick が回答・再予約しないようにする (#3706)

## 決めたこと
- 世代の token は「state そのものの同一性」で持つ。AutoYesPollerState の型は
  src/lib/polling/auto-yes-poller-state.ts(scope 外)にあり field を足せないため、
  startAutoYesPolling が作る poller state オブジェクトを poller の token
  (PollerOwner と同じ考え方。restart・disable→enable で別オブジェクト、stop で削除)、
  auto-yes-state の AutoYesState オブジェクトを許可の token(enable・disable のたびに
  新しいオブジェクトが入る。期限切れは getAutoYesState が disabled を入れ直すので
  同一性で外れる)とした。auto-yes-state.ts は変更不要だった。
- CLI セッションの epoch は既存の getEnterFallbackSessionEpoch(beginAgentSession が
  進める)をそのまま使った(新設なし)。
- tick 冒頭で AutoYesTickGeneration を取り、capture の後・所有権確認の後・Enter
  fallback の fresh capture の後・送信関数の中の各待ちの後・detect の後・
  isSessionMissing の後に isTickCurrent で照合。scheduleNextPoll(generation) は
  キーで引き直さず、自分の poller state が現役のときだけそこに timer を張る。
  timer の closure も自分の poller state を渡し、置き換わった poller の timer は何もしない。
- 世代が外れた tick は何も変えずに終わる。ただし自分の poller が現役(許可の更新・
  セッションの作り直しだけ)なら再予約して鎖を絶やさない。
- 送信関数: SendPromptAnswerParams.stillCurrent と sendSpecialKeys の
  options.shouldContinue(どちらも省略可能)。ガードの画面読み・キーの間・Enter 前の
  待ち・多肢選択の再読の後に照合し、偽なら PromptAnswerAbortedError /
  SpecialKeysAbortedError で止める。省略時は sendSpecialKeys を従来の 2 引数で呼ぶので、
  手動の送信・他の呼び手は不変。detectAndRespondToPrompt の generation も省略可能で、
  省略時(直接の呼び手・既存テスト)は照合しない。
- 送信済みと中断の区別: 途中で止めた送信は回答としても error としても数えない
  (poller:auto-yes-answer-abandoned を keysSent つきで info)。送り終えた回答は
  task event と監査行を残し、error count・最終応答時刻は自分の世代の poller state に
  だけ書く。応答 poller の再開と画面配信は、セッションの epoch が変わっていれば
  しない(disable だけなら送った回答の返事を拾うために従来どおり再開する)。
- 許可の更新(enable 中の再 enable)で旧 tick の送信も止まる。1 tick 分の回答が
  次の tick に遅れるだけで、鎖は続く。

## 確かめたこと
- tests/unit/polling/auto-yes-poller-generation-3706.test.ts(17 件。capture・所有権・
  sender のガード読み・Enter 前の待ち・fallback の fresh capture を deferred / fake
  timer で止める): 陽性 12 件は直す前の実装(HEAD の 3 ファイル)で 12 件とも落ち、
  陰性 5 件は直す前・後とも通ることを確認。
- tests/unit/lib/tmux-special-keys-abort-3706.test.ts(実 tmux.ts、execFile のみ stub)4 件。
- 関係するテスト 252 ファイル(auto-yes-poller / auto-yes-manager / prompt-answer-sender /
  tmux / auto-yes-state を参照するもの。auto-yes-poller-split-3411・
  auto-yes-enter-fallback-3397 を含む)+ tests/unit/guards + tests/unit/docs:
  296 files / 4900 tests 通過。既存の it/describe の名前・期待値は変えていない。
- npm run lint・npx tsc --noEmit 通過。node scripts/changelog-fragments.mjs check exit 0。

## 対になる場所を探した結果
- 応答 poller(polling/response-poller*, poller-owner.ts)は既に PollerOwner で照合済み。
- 手動の回答(/prompt-response route)は stillCurrent を渡さないので不変(意図どおり)。
- hooks の permission-adjudication.ts は resolvePermissionRequest(同期)から
  answerPendingDecisionWithReceipt の呼び出しまでに await が無く、同じ形の欠陥は無かった。
- docs(module-reference / implementation-history ja・en)に pollAutoYes の待ちの
  振る舞いを固定する記述は無かった。古い動きを固定する既存テストも無かった。

## 本文に無い指摘
- src/lib/polling/auto-yes-poller-state.ts:16 AutoYesPollerState に明示の世代 field が
  無い(今回はオブジェクトの同一性で代用)。将来 poller state を作り直さずに再利用する
  変更が入ると token が壊れるので、型に generation を足すなら scope を広げて行う。
- src/lib/auto-yes-state.ts:200 getAutoYesState は読むだけで期限切れを disable に
  書き換える(副作用つきの読み)。今回の照合も期限切れ時にこれを踏む(自分の世代の
  許可に対してのみ)。

* fix(auto-yes): 送信中の再起動・停止・途中中断の後始末を今の世代と区別する (#3706)

再指示 1(Codex 整合性レビューの P2 × 3)。7357e10 の上に足す。

## 決めたこと
### P2-1 送信中に CLI セッションが作り直された回答
- 区別は呼び手(finishAnsweredPrompt)で行い、監査の札の選び方には時刻の条件だけを
  足した。chat-db は session epoch を知らないので epoch で選ばせるのは層が合わない。
  代わりに recordAnsweredPrompt に省略可能な pendingSavedAtOrBefore(epoch ms)を足し、
  Auto-Yes はセッションが変わったときだけ tick 開始時刻(generation.startedAt)を渡す。
  キーが届いたのは旧プロセスで、新プロセスの prompt から保存された pending 札は
  必ず tick 開始より後なので採られない。旧セッションの札(tick 前に保存)は従来どおり
  回答済みになり、無ければ回答済みの行を新しく作る(監査は残る)。
  残る端: 旧セッションの札が tick の途中に保存された場合は採られず、新しい行が
  作られてその札は pending のまま残る(旧セッションの札なので新セッションには無害)。
- セッションが変わったときは dedup キー(lastAnsweredPromptKey/At)・最終応答時刻・
  error count のリセット・task の prompt_answered_auto を書かない(今の task を引き直すと
  新セッションの task に通知しうるため)。応答 poller の再開・画面配信は前回どおりしない。
### P2-2 Enter fallback の記録
- 記録に epoch を持たせた。recordEnterFallbackSent は sent.epoch(省略時は現在の epoch)が
  現在の epoch と違えば書かず false を返す。getLastEnterFallback も別 epoch の記録を
  返さない。poller は Enter の帰りで、自分の poller が現役かつ同じセッションのときだけ
  enterFallbackSentKey と記録を書く(停止=forgetEnterFallback の後に復活させない)。
  監査行(finishAnsweredPrompt)はどちらでも残す。
### P2-3 キーを送ってから止めた回答
- 中断(PromptAnswerAbortedError で keysSent=true)のとき、composite key ごとに
  {promptKey, session epoch} を覚える(auto-yes-poller-state.ts、globalThis)。
  poller state に持たせないのは、典型の停止が disable→enable で poller が作り直されるため。
  次以降の tick は同じ epoch・同じ promptKey の prompt に答えず no_answer
  (poller:auto-yes-skipped-partial-answer を 1 画面 1 回 WARN)。
- 半端な入力(`y` だけ、カーソル移動だけ)は画面から読み分けられないので、Auto-Yes は
  消しも補完もせず人に任せる。prompt が画面から消える・別の prompt に変わる・
  セッションが作り直される(epoch が変わる)と忘れる。stopAllAutoYesPolling と
  stopAutoYesPollingByWorktree でも消す。キーを 1 つも送っていない中断は覚えない。
- AutoYesPollerState に明示の世代 field は足さなかった(同一性の照合で足りている)。

## 確かめたこと
- auto-yes-poller-generation-3706.test.ts に 10 件追加(計 27 件)。P2-1 陽性 1・陰性 1
  (陽性は次の tick で新セッションの同じ質問に答えることまで確認)、P2-2 陽性 2
  (relaunch・stop)・陰性 1、P2-3 陽性 2(renewal・disable→enable、次の 3 tick まで)・
  陰性 3(prompt が消えて戻る・relaunch・キー送信前の中断)。陽性 5 件は直す前
  (7357e10 の 4 ファイル)で落ち、陰性は前後とも通ることを確認。
- 関係するテスト 393 ファイル(auto-yes-*、prompt-answer-sender、tmux、chat-db、
  current-output、enter-fallback ほか)+ tests/unit/guards + tests/unit/docs:
  429 files / 7357 tests 通過。既存の it/describe の名前・期待値は変えていない。
- npm run lint・npx tsc --noEmit 通過。changelog-fragments check exit 0。

## 対になる場所を探した結果
- recordAnsweredPrompt の他の呼び手(/prompt-response route)は新引数を渡さず不変。
- recordEnterFallbackSent の本番の呼び手は poller だけ。getLastEnterFallback の読み手
  (current-output、端末配信)は別 epoch の記録を受け取らなくなる(beginEnterFallbackSession
  が既に削除しているので通常は差が出ない)。
- 手動回答の経路には途中中断が無い(stillCurrent を渡さない)ので P2-3 の対は無い。
- docs に該当の振る舞いを固定する記述・古い動きを固定するテストは無かった。

## 本文に無い指摘
- src/lib/polling/auto-yes-suppression-reason.ts(scope 外): 途中中断で人に任せた
  prompt を lastSuppression として公開する理由が無いため、`cmate wait` はこの prompt を
  Auto-Yes 待ちとして保留し続けうる。理由語の追加は別 Issue で。
- src/lib/prompt-answer-sender.ts: 世代と無関係な transport 失敗(`y` の後の Enter の
  send-keys が失敗)でも半端な入力が残るが、error として数えるだけで P2-3 の記憶には
  入れていない(今回は中断のみ)。

* fix(auto-yes): 再開前の session 再照合と、途中送信の記憶の画面識別・移送・公開 (#3706)

再指示 2(Codex 再レビューの P2 × 4)。7357e10・d58d691 の上に足す。

## 決めたこと
### P2-1 後段の await 後の再開
- finishAnsweredPrompt は応答 poller と画面配信を始める直前(動的 import の await の後)に
  isSameSession(generation) を読み直す。前に取った sameSession は使い回さない。照合は
  session epoch(自分の世代の epoch)で、poller の置き換えは見ない — disable だけなら
  送った回答の返事を拾うために再開する、という d58d691 の判断を保つ。
### P2-2 途中送信の記憶のキー
- promptKey(type+question)をやめ、partialAnswerScreenKey(type・question・選択肢の
  番号と label・approvalTarget)にした。promptFrameKey が区別するもののうち、カーソル
  (option の isDefault)だけを除く。実測で、generic parser の approvalTarget は選択肢の
  パネルを `❯` ごと写すため、approvalTarget からも行頭のカーソル記号と字下げを除いた
  (除かないと、カーソルだけ動いた同じ画面が別 prompt に読まれ、陰性テストが落ちた)。
  promptFrameKey 自体は変えていない。期限は付けない(prompt が消える・別 prompt・
  session の作り直しで忘れる、は従来どおり)。
### P2-3 worktree ID の移行
- auto-yes-poller-state.ts に migratePartialAnswerWorktreeIds(renames) を置き、
  reconcile の migrateRuntimeState が Auto-Yes state の移送直後(poller 再開の前)に呼ぶ。
  記憶の Map が poller-state にあるので移送関数も同じ場所に置いた(auto-yes-state.ts から
  呼ぶと import が循環する)。移送は state と同じ 2 パス(ID の swap に耐える)。
  移送先に既に記憶があれば、移ってきた記憶で置き換える:rename が成功したなら新しい
  名前の session は無かったので、移送先の記憶は既に居ないプロセスについてのもの。
  epoch は移送先キーの今の値にする(epoch の counter はキーごとで、rename されたプロセスが
  新キーの今の session だから)。
### P2-4 人に任せた prompt の公開
- suppressAndWarnOnce で lastSuppression に理由語 'partial-answer' を記録する(毎 tick
  更新、WARN は 1 画面 1 回)。prompt Push の判定は policy-withheld になり通知され、
  current-output の autoYes.lastSuppression にも載るので cmate wait は「回答中」として
  待たない。
- 理由語は AutoYesSuppressionReason の union に入れず、auto-yes-suppression-reason.ts に
  PARTIAL_ANSWER_SUPPRESSION_REASON / AutoYesServerOnlySuppressionReason として別に置き、
  記録の 1 か所で cast した。union に足すと、CLI の写し(api-responses.ts が同じ union を
  re-export)を通して src/cli/commands/wait.ts の Record<AutoYesSuppressionReason,string>
  (SUPPRESSION_CAUSE)と tests/unit/cli/config/cross-validation.test.ts の双方向の型照合が
  tsc で落ちる。どちらも scope 外。サーバ側の読み手(suppression-state・prompt-push-gate・
  current-output-builder)は理由語を素通しするだけで、wait は知らない理由語を
  「for a reason this CLI does not recognise (partial-answer)」とそのまま出す(文書化済みの
  挙動)ので、実行時の扱いは正しい。

## 確かめたこと
- auto-yes-poller-generation-3706.test.ts に 8 件追加(計 35 件)。P2-1 陽性(audit の後の
  await 中に relaunch → 応答 poller を再開しない)・陰性、P2-2 陽性(gemini、同じ質問文の
  別の選択肢には答える)・陰性(カーソルだけ動いた同じ一覧は保留のまま)、P2-3 陽性
  (reconcileWorktreeSessions で移した後の新 poller が 3 tick 再入力しない)・陰性(記憶が
  無ければ移した poller は従来どおり答える)、P2-4 陽性(lastSuppression=partial-answer、
  decidePromptPush が policy-withheld)・陰性(通常の回答は suppression 無し・
  auto-yes-answering)。陽性 4 件は直す前(d58d691)で落ち、陰性は前後とも通る。
- 関係するテスト 427 ファイル(auto-yes-*、prompt-answer-sender、tmux、chat-db、
  current-output、worktree-session-reconcile、prompt-push-gate、suppression、
  cross-validation、cli wait ほか)+ tests/unit/guards + tests/unit/docs:
  462 files / 8019 tests 通過。既存の it/describe の名前・期待値は変えていない。
- npm run lint・npx tsc --noEmit 通過。changelog-fragments check exit 0。

## 対になる場所を探した結果
- 応答 poller の再開を行うのは finishAnsweredPrompt だけ(Enter fallback も同じ関数を通る)。
- 途中送信の記憶を読む・書く場所は detectAndRespondToPrompt だけ。worktree 削除
  (stopAutoYesPollingByWorktree)・全停止は d58d691 で消している。
- 理由語を列挙するもの: src/cli/commands/wait.ts:289 の SUPPRESSION_CAUSE、
  src/cli/types/api-responses.ts:199 の re-export、tests/unit/cli/config/cross-validation.test.ts:91
  の列挙。いずれも scope 外のため未変更(下の指摘)。docs に理由語の一覧は
  docs/module-reference.md(scope 外、断片で追記を依頼)。

## 本文に無い指摘
- src/cli/commands/wait.ts:283 SUPPRESSION_CAUSE に 'partial-answer' の文言(例:
  "because an earlier answer was stopped part-way and its keys are still in the input")を足し、
  src/lib/polling/auto-yes-suppression-reason.ts の union へ合流させ、
  tests/unit/cli/config/cross-validation.test.ts:91 の列挙にも足す(3 つ同時でないと tsc が落ちる)。
  合流後は auto-yes-poller.ts の cast を外せる。
- src/lib/session/worktree-session-reconcile.ts:613 移行は Enter fallback の記録と session
  epoch(auto-yes-enter-fallback-state.ts の Map、キーは worktree ID 込み)を移さない。
  移行後は旧キーに残り、新キーの epoch は 0 から始まる。

* fix(auto-yes): 途中送信の記憶の worktree ID 移送をこの PR から外す (#3706)

再指示 3(Codex 3 回目の再レビュー、review-3706c.md の P2 × 3)。3 件とも b6bfac6 で足した
migratePartialAnswerWorktreeIds とその reconcile からの呼び出しの中の指摘で、利用者の判断で
この処理をこの PR から外し、別 Issue で設計し直す。

## 決めたこと
- auto-yes-poller-state.ts から migratePartialAnswerWorktreeIds を取り除き、それだけが使って
  いた import(buildCompositeKey・extractCliToolId・extractInstanceId・extractWorktreeId・
  getEnterFallbackSessionEpoch)も d58d691 の時点の形へ戻した。
- src/lib/session/worktree-session-reconcile.ts は develop と同じ内容に戻した(差分 0)。
- auto-yes-poller-generation-3706.test.ts から、この PR で足した P2-3 移送のテスト(describe
  「re-review P2-3: a worktree ID move carries the stopped answer」の陽性・陰性 2 件)と、
  そのためだけに足した import(reconcileWorktreeSessions)・mock の項目
  (migrateResponsePollerWorktreeIds・migrateWorktreeRooms)を取り除いた。
- changelog.d/3706.md から移送の一文を除いた。
- P2-1 の再照合、P2-2 の画面識別のキー、P2-4 の partial-answer の公開、7357e10・d58d691 の
  変更は変えていない。

## 確かめたこと
- 生成テスト 33 件・tmux の中断テストを含む関係するテスト(tests/unit/polling、
  tests/unit/lib/auto-yes*、worktree-session-reconcile.test.ts)+ tests/unit/guards +
  tests/unit/docs: 89 files / 1476 tests 通過。既存の it/describe の名前・期待値は変えていない
  (取り除いたのはこの PR で足したテストだけ)。
- npm run lint・npx tsc --noEmit 通過。changelog-fragments check exit 0。
- git grep で migratePartialAnswer の参照が src・tests に残っていないこと。

## 本文に無い指摘
- 既知の残り(前回の P2-3): worktree ID の移行(reconcileWorktreeSessions)の後、途中送信の
  記憶は旧キーに残り、新キーで再開した poller はそれを見ないため、キーを途中まで送って止めた
  同じ prompt に再入力しうる(例: `y` の後に Enter を保留したまま移行 → 新 poller が `y` を
  もう一度打ち `yy` になる)。別 Issue で移送の設計(Enter fallback の記録・session epoch の
  移送と合わせて)をやり直す。
- 前回までの指摘は変わらず残る: wait.ts の SUPPRESSION_CAUSE・union・cross-validation への
  'partial-answer' の同時追加、reconcile が Enter fallback の記録と session epoch を移さない
  こと、transport 失敗による半端な入力は記憶の対象外、AutoYesPollerState に明示の世代 field が
  無いこと、getAutoYesState の副作用つきの読み。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(auto-yes): worktree ID の移行で途中送信の記憶を移し、理由語 partial-answer を型に入れる (#3713)

## 決めたこと(契約の決定どおり。事前レビュー prereview-3713.md の「採る案」)
- 送信の試行 registry(globalThis `__autoYesSendAttempts`、auto-yes-poller-state.ts)。
  poller は sendPromptAnswer の await の前に beginSendAttempt(key, screenKey, epoch)、
  帰りは await を挟まず settleSendAttempt(attempt, keysSent)。keysSent=true の中断は
  試行の「今の」記録先へ途中送信を記録(記録先の今の epoch が試行のものと違えば記録しない)、
  keysSent=false・完了・失敗は自分の試行だけ外す。rememberPartialAnswer は廃止(呼び手は poller だけ)。
- 新しいキーの poller は、移行で移ってきた未完了の試行と同じ画面(同じ epoch)には送らない
  (hasMovedSendInFlight。移っていない試行は数えないので、移行のない経路は従来どおり)。
  確定した partial-answer とは区別し、suppression は記録せず warnOncePerFrame だけ。
- reconcile: plan に cliToolId・instanceId(roster はその対、live は attributed.cliToolId と
  buildInstanceId の結果)。listSessions の後・最初の rename の await の前に移送元キーの epoch を控え、
  renameSessionsTwoStage の直後(await なし)に movePartialAnswers。二段とも true の plan だけ。
  stage 2 の false は完了に入れず skippedSessions('session no longer exists')に出す。
  movePartialAnswers は全移送元を先に読み・退避し、成功した移送先の古い記憶は削除(移ってこない
  未完了の試行も失効)してから一度だけ置く。epoch・Enter の記録は移さない(移送先の今の epoch を付ける)。
  許可・応答 poller・WS の移送は今のまま全要求に。送信の保留・rename と作り直しの排他は作っていない。
- 前提の修正: migrateRuntimeState の Auto-Yes poller 再開を「全旧 poller を停止 → 全後継を開始」に。
- 失効: forgetPartialAnswersByWorktree(worktree 単位の停止)と clearPartialAnswers(全体停止)は
  未完了の試行も expired にし、遅い旧 tick が記憶を復活させない。作り直しは epoch の照合で弾く。
  単一キーの disable→enable(stopAutoYesPolling)は試行に触れないので保護を保つ。
- 理由語: 'partial-answer' を AutoYesSuppressionReason に合流、AutoYesServerOnlySuppressionReason と
  cast を撤去。PARTIAL_ANSWER_SUPPRESSION_REASON は `satisfies` で union のメンバー。wait.ts の
  SUPPRESSION_CAUSE に "because an earlier answer left partial input; Auto-Yes is waiting for a human"。
  api-responses.ts と cross-validation.test.ts の「CLI の独立した写し」という古い説明を実態(純粋な
  モジュールの再 export)に直した。resolver・push・hooks は変更なし。
- docs/user-guide(ja/en)の lastSuppression 節に partial-answer の説明を追記。

## 確かめたこと
- 新規 tests/unit/polling/auto-yes-partial-answer-migration-3713.test.ts(46 件)。実 poller・実
  sendPromptAnswer・実 registry・実 reconcileWorktreeSessions、fake tmux は session ごとの pane と
  キー列を持ち二段 rename で session ごと移す。事前レビューの組 1〜15 を陽性・陰性で。組 2 は
  100ms の定数を変えず、y を pane に出した後の sendKeys の Promise を保留し、新キーの最初の tick が
  capture まで進んだ(capture 回数で確認)後に返す。組 3 は claude の AskUserQuestion 実画面
  (fixtures/claude-live-2486/preview-q1.txt、Down→Up→Enter の Down の後で保留)と gemini の数字入力。
  組 9 の入れ替えは両側の後継が capture まで進むことを確認。
  直す前の 3 ファイル(poller-state / poller / reconcile を HEAD に戻して)では陽性 23 件が落ちる
  ことを確認(registry の公開関数を直接呼ぶ一部の陰性も、関数が無いため落ちる)。
- wait.test.ts に partial-answer の文言・exit 10・JSON の reason と、未知理由の従来表示の対を追加。
- #3706 の既存テスト(auto-yes-poller-generation-3706.test.ts)は名前・期待値を変えずに通過。
- 関係するテスト(auto-yes-poller / auto-yes-manager / reconcile / poller-state / suppression-reason /
  commands/wait を参照する 129 ファイル)+ tests/unit/guards + tests/unit/docs + cross-validation:
  173 files / 3092 tests 通過。npm run lint・npx tsc --noEmit 通過(npm run build:cli も一度通した)。
  node scripts/changelog-fragments.mjs check exit 0。

## 対になる場所を探した結果
- 理由語の写し: src/cli/types/api-responses.ts は再 export のみ(説明だけ直した)。wait.ts の Record が
  唯一の網羅表。push/prompt-push-gate.ts・permission-decision-service.ts・auto-yes-suppression-state.ts は
  型の受け渡しのみで変更不要、tool-input-normalization.ts はコメントの例示だけで変更不要。
- 途中送信の記録先は poller の catch 1 か所だけ(Enter fallback の記録・送信済み監査は別で、変えていない)。
- docs: ja/en の cli-operations-guide の lastSuppression 節。agent-event-hooks.md は hook の判断だけで
  partial-answer を生まないので変更なし。

## 本文に無い指摘
- src/app/api/worktrees/[id]/auto-yes/route.ts:447 の「全 agent の disable」は worktree 削除と同じ
  stopAutoYesPollingByWorktree を呼ぶ。#3706 から確定済みの記憶はそこで忘れていたが、本 PR で未完了の
  試行もそこで失効するため、全 agent の disable → enable の最中に帰った旧 tick は記憶を残さない
  (単一 agent の disable → enable は保護を保つ)。削除と disable を分けるには route.ts / session-cleanup.ts
  (scope 外)の変更が要る。
- docs/user-guide/cli-operations-guide.md:1901 と en の同じ表の reason 欄はポリシーの 4 語だけで、
  agent-launch-dialog・unclassified-frame も載っていない(本 PR は partial-answer の説明だけ足した)。
- epoch は論理世代で、tmux の発行済み command の実配送先の保証ではない(事前レビューの未確認事項のまま)。

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(auto-yes): 全 agent の disable で途中送信の保護を保ち、遅い旧試行と移送の除外で記憶を消さない (#3713)

再指示 1(Codex 整合性レビューの P2 × 3)。5b0b5e7 の上に足す。

## 決めたこと
- P2-1: stopAutoYesPollingByWorktree(worktreeId, options?: { keepPartialAnswers?: boolean }) に引数を足して分けた。
  省略時(既定)は今のまま確定済みの記憶も未完了の試行も失効させる。全呼び手:
  - src/app/api/worktrees/[id]/auto-yes/route.ts(agent 無指定の disable)→ keepPartialAnswers: true(保つ。
    単一 agent の disable = stopAutoYesPolling と同じ扱い)。Enter の記録は従来どおり忘れる。
  - src/lib/auto-yes-lifecycle.ts の stopAutoYesPollersFor(scope 'worktree')→ 省略(失効)。
    この経路の呼び手は src/lib/session-cleanup.ts の 'worktree-deleted' だけ(ほかの releaseAutoYes /
    stopAutoYesPollersFor の呼び手は scope 'instance' / 'key' で、この関数を通らない)。
  - src/lib/polling/auto-yes-manager.ts は再 export のみ。session-cleanup.ts・auto-yes-lifecycle.ts は変更不要。
- P2-2: 試行に registry の通し番号 seq(globalThis `__autoYesSendAttemptSeq`、登録順)を持たせ、記憶にも
  記録した試行の seq を残す。確定時、記録先に同じ epoch でより大きい seq の記憶があれば上書きしない。
  時刻ではなく通し番号にしたのは、同じ ms に二つの送信が始まりうる(fake timer でも実時間でも)ため。
  移送では記憶の seq をそのまま運ぶ。先に記録された記憶(小さい seq)は従来どおり後の中断で置き換わる。
- P2-3: movePartialAnswers の退避は、運ぶと決めた値(epoch 照合を通ったもの)が今も移送元にある
  ときだけ消す。照合で外れた移送元の記憶(rename の待ちの間に作り直された新世代のもの)は残す。

## 確かめたこと
- auto-yes-partial-answer-migration-3713.test.ts に 8 件追加(既存 46 件の名前・期待は不変)。
  P2-1 は実 route(POST /auto-yes { enabled: false })を通し、y の後の Enter 待ち中に disable →
  旧送信の帰還 → enable → 3 tick で再入力なし(capture 回数も確認)、確定済みの記憶も disable で残る。
  陰性は session-cleanup と同じ stopAutoYesPollersFor('worktree-deleted', scope 'worktree') で失効。
  P2-2 は実 poller・移行で「A の X 送信待ち → A→B → B が Y の答えを中断 → 旧 A 帰還」で B の Y が残り
  再入力しない、と registry 直の順序の陽性。陰性は先に記録された記憶が後の中断で置き換わる。
  P2-3 は stage 1 の rename 中に A を作り直して新世代が途中送信を記録 → 移行後も A に残り B へ移らない。
  陰性は運んだ記憶が移送元から消える。
  直す前の実装で陽性 5 件が落ち、陰性 3 件は通ることを確認してから直した。
- 関係するテスト(auto-yes-poller / manager / reconcile / poller-state / auto-yes route / lifecycle /
  session-cleanup を参照する 159 ファイル。guards・docs を含む)2995 件通過。npm run lint・
  npx tsc --noEmit 通過。node scripts/changelog-fragments.mjs check exit 0。

## 対になる場所を探した結果
- stopAutoYesPollingByWorktree の呼び手は上の 2 つだけ(src 全体を grep)。tests/unit/session-cleanup-*.test.ts の
  toHaveBeenCalledWith('wt-1') は引数省略のままなので不変。
- 記憶を書く場所は settleSendAttempt と movePartialAnswers の 2 つで、どちらにも順序・除外を入れた。
  movePartialAnswers の移送先の古い記憶の削除は、移送先の session が移ってきた側に置き換わった場合だけで、
  P2-3 の「外れた移送元」には当たらない。
- docs/user-guide(ja/en)の partial-answer の説明は disable の種類に触れていないので変更なし。

## 本文に無い指摘
- なし(新しい状態は seq の通し番号 1 つだけで、上の 3 件の範囲に収まった)。

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(auto-yes): prompt の消失で忘れた後に、遅い旧試行が途中送信の記憶を復活させないようにする (#3713)

再指示 2(Codex 再レビューの P2)。5b0b5e7・ee97bc6 の上に足す。

## 決めたこと
- forgetPartialAnswer(compositeKey)(poller の no_prompt 分岐と、記憶と別画面の分岐の 2 か所が呼ぶ)で、
  記録先がそのキーの未完了の試行(移行で移ってきたもの・同じキーのもの)を expired にする。
  expired の試行は settleSendAttempt が何も書かない(既存の判定)。忘れた後に beginSendAttempt された
  試行は registry に新しく入るので、従来どおり記録する。
- 形は「試行に印を付ける」側を選んだ。既存の expired 状態をそのまま印に使い、registry から外すので、
  記録先ごとの「この seq 以下は無効」の Map のように試行より長生きする状態を新しく持たずに済む
  (キーごとの値の寿命・削除・移送を別に決める必要が無い)。忘れた時点で registry にある試行=
  忘れた時点の通し番号以下の試行なので、契約の条件と同じになる。
- 副作用として、失効した移送済みの試行は hasMovedSendInFlight にも数えられなくなる。prompt が消えた後に
  同じ確認が出たら、それは次の確認なので後継が答える(Issue の期待どおり)。

## 確かめたこと
- auto-yes-partial-answer-migration-3713.test.ts に 4 件追加(既存 54 件の名前・期待は不変)。
  陽性: 移行あり(A の X 送信待ち → A→B → B が X の消失を観測 → 旧 A 帰還 → B に記憶なし → 次の X に
  B が y・Enter で答える)、移行なしの同じキー(disable → enable の新 tick が消失を観測 → 旧送信の帰還 →
  記憶なし → 次の X に答える)。直す前の実装で 2 件とも落ちることを確認してから直した。
  陰性: 消失の後に始まった試行の中断は記憶される/消失を見ていなければ移行後に帰った旧試行の中断は
  記憶され、3 tick 再入力しない(直す前・後とも通過)。
- 関係するテスト 159 ファイル(guards・docs を含む)2999 件通過。npm run lint・npx tsc --noEmit 通過。
  node scripts/changelog-fragments.mjs check exit 0。

## 対になる場所を探した結果
- forgetPartialAnswer の呼び手は src/lib/auto-yes-poller.ts の 2 か所だけ(src 全体を grep)。どちらも
  この関数を通るので同時に直った。forgetPartialAnswersByWorktree・clearPartialAnswers は既に試行を
  失効させている。getPartialAnswer の epoch 不一致による遅延削除は「別 session の記憶」の削除で、
  その session の試行は settle 時の epoch 照合で弾かれるので変更不要。
- docs/user-guide(ja/en)は忘れる条件を書いていないので変更なし。

## 本文に無い指摘
- なし。

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(auto-yes): 別の画面で古い記憶を忘れるとき、今の画面へ送信中の試行は失効させない (#3713)

再指示 3(Codex 再レビューの P2、本人了承済みの追加の 1 回)。前の 3 コミットの上に足す。

## 決めたこと
- forgetPartialAnswer(compositeKey, screenKey?) に引数を足して分けた(関数は 1 つのまま)。
  - no_prompt の分岐(src/lib/auto-yes-poller.ts、prompt の消失)は引数なしのまま: そのキーの未完了の
    試行をすべて失効(再指示 2 のとおり)。
  - 「記憶と今の画面が違う」分岐は forgetPartialAnswer(compositeKey, partial.screenKey): 忘れた記憶と同じ
    screenKey の試行だけを失効させ、別の screenKey(今の画面 Y へ送信中で、移行で移ってきたもの)は残す。
    残った試行はその後の hasMovedSendInFlight で後継を Y から遠ざけ、帰ってきた中断は記録される。
- 引数で選ぶ形にしたのは、呼び手が 2 か所だけで、どちらも「記憶を消す+試行を失効」という同じ処理の
  範囲違いだから(関数を分けると Map の削除を 2 か所に書くことになる)。

## 確かめたこと
- auto-yes-partial-answer-migration-3713.test.ts に 2 件追加(既存 58 件の名前・期待は不変)。
  陽性: 指摘の順(A の X 送信待ち → A→B → B が Y の y を送って待機 → 旧 A 帰還で X を B に記録 → B→C →
  C の最初の tick が Y の帰りより先)で C が Y に再入力せず、後から帰った Y の中断が C に記憶され、
  その後 3 tick も再入力しない。直す前の実装で落ちる(y が 3 回)ことを確認してから直した。
  registry の対照(screenKey 指定は同じ画面だけ失効、省略は全部失効)も追加。これは新しい引数を使うので
  直す前は落ちる。陰性の「prompt の消失では全部失効」は再指示 2 のテスト 4 件がそのまま通ることで確認。
- 関係するテスト 159 ファイル(guards・docs を含む)3001 件通過。npm run lint・npx tsc --noEmit 通過。
  node scripts/changelog-fragments.mjs check exit 0。

## 対になる場所を探した結果
- forgetPartialAnswer の呼び手は src/lib/auto-yes-poller.ts の 2 か所だけ(src 全体を grep)。
  forgetPartialAnswersByWorktree・clearPartialAnswers は worktree 削除・全体停止で全部失効が正しいので変更なし。
  movePartialAnswers の移送先の古い試行の失効は、移送先の session が入れ替わる場合だけなので対象外。
- docs/user-guide(ja/en)は忘れる条件を書いていないので変更なし。

## 本文に無い指摘
- なし。

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Kewton
Kewton merged commit c524c89 into main Oct 15, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant