Skip to content

release: v0.11.1#1403

Merged
Kewton merged 180 commits into
mainfrom
develop
Jul 18, 2026
Merged

release: v0.11.1#1403
Kewton merged 180 commits into
mainfrom
develop

Conversation

@Kewton

@Kewton Kewton commented Jul 18, 2026

Copy link
Copy Markdown
Owner

release: v0.11.1

develop → main。サイドバー同期トーストの見切れ修正と Toast のグローバル1本化をまとめたパッチリリース。

含まれる変更

Issue 種別 内容 PR
#1399 fix サイドバー同期トーストの見切れ修正(ToastContainerdocument.body portal 化) #1401
#1400 refactor Toast をグローバル1本化(ToastProvider + 単一 Portal ホスト) #1402

ハイライト

サイドバー同期ボタンのトーストが、transform を持つ祖先(AppShell のサイドバー枠)の containing block に閉じ込められ、ビューポート右下ではなくサイドバー基準に配置されて左端が見切れていた問題を解消。#1399ToastContainerdocument.body への createPortal 化(mounted ガードで SSR 安全)して根本修正、#1400 で散在していた ToastContainer/useToast を単一 ToastProvider + 単一 Portal ホストに統合し、描画位置がツリー構造に依存しないようにして再発を構造的に防止。

差分

  • 実ファイル差分(origin/main...origin/develop): リリースメタ(package.json / package-lock.json / CHANGELOG.md)+ Toast 関連 15 ファイルのみ。ancestry は健全(merge-base = origin/main)でコンフリクトなし。
  • ※「Commits」タブには過去の squash マージ由来の履歴が多数並ぶが、これは当リポジトリの develop→main リリース PR の常態で、実ファイル差分は上記のみ。

品質

各 PR(#1401 / #1402)で CI 全 9 チェック(Lint / Type Check / Unit / Build / Integration / E2E / Security / Token discipline / CLAUDE.md size)が pass 済み。#1402 の CI は #1399 統合後の develop を検証済み。

マージ後の手順(リリース手順)

  1. squash マージ後、gh release create v0.11.1(→ publish.yml が npm publish をトリガー)
  2. maindevelop へ backmerge(git merge -s ours origin/main)して ancestry を維持

Kewton and others added 30 commits July 5, 2026 19:10
…#1025)

スマホ版のファイルビューア(FileViewer, Issue #438 でモバイル専用化)から
任意種別のファイルをダウンロードできるようにする。

- B(サーバ): files/[...path] GET に download 分岐を追加し、生バイトを
  Content-Type: application/octet-stream + Content-Disposition: attachment で返却。
  filename は src/lib/http の content-disposition ヘルパーで RFC 準拠にサニタイズ
  (ASCII フォールバック + filename*=UTF-8'' パーセントエンコード、ヘッダ
  インジェクション回避)。既存 getWorktreeAndValidatePath でパス検証を再利用。
  base64 JSON を経由しないためプレビュー上限を超える大容量にも対応
- A(UI): FileViewer ツールバーにダウンロードボタンを追加し、同一オリジンの
  download URL へ遷移(iOS Safari 含めネイティブDLに乗せる)
- テスト新規29本(download API のヘッダ/パス検証/filename、content-disposition
  ヘルパー、FileViewer ボタン)
- C(独立ページのPDF分岐/DLリンク)は範囲外(モバイル動線が独立ページを使わないため)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Timer 機能(Issue #534)から送った指示が History(chat_messages)に残らない
問題を修正。executeTimer が cliTool.sendMessage だけを呼び createMessage/
startPolling をスキップしていたのが原因(#947 の委譲変更が起点)。

- send route L262-385 のインライン記録ロジックを src/lib/session/
  send-user-message.ts の共通関数 sendUserMessage() として抽出
  (savePendingAssistantResponse → send(画像/Copilot分岐含む)→ createMessage
  → updateLastUserMessage/orphan処理 → startPolling)。HTTP 層非依存
- send route POST を sendUserMessage() 利用へリファクタ(挙動不変)
- timer-manager.ts executeTimer を sendUserMessage() 経由に変更。
  timer_messages の status 遷移(sending/sent/failed)は維持
- テスト: sendUserMessage / executeTimer のユニット新規9件 + timer 既存更新(計26件)

これにより Timer 送信も user メッセージと assistant 応答の両方が History に残る。

スコープ外(別Issue推奨):
- tests/integration/api-send-cli-tool.test.ts の1件は base でも失敗する既存不具合
  (2引数アサート vs 3引数呼び出し)で本修正と無関係(CI の test:unit 対象外)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- migration v38: worktree_todos に status カラム追加(done=1→'done' backfill、CURRENT_SCHEMA_VERSION 37→38)
- DB層: status を真実源に done := status==='done' を派生(後方互換維持、writer整合)
- API/クライアント: PATCH で status ∈ {todo,doing,done} を検証・適用、done後方互換
- UI: 3状態巡回チップ+色バッジ+状態別件数、TodoPane全体を next-intl 化
- i18n: locales/{en,ja}/worktree.json に todo namespace 追加
- テスト: 既存5更新+新規 migration-v38(backfill検証)

Closes #1032

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- migration v39: worktree_todos に detail カラム追加(TEXT NOT NULL DEFAULT ''、既存行は空詳細で移行)
- DB層: WorktreeTodo に detail 追加、create/update で detail 受理
- API: POST/PATCH で detail を検証・適用(MAX_TODO_DETAIL_LENGTH 新設)
- クライアント: WorktreeTodoItem.detail、create/update の detail 対応
- UI: 項目クリックで編集モーダル(Modal.tsx再利用)を開き件名+詳細を編集、詳細ありインジケータ
- i18n: locales/{en,ja}/worktree.json に詳細/編集の文言追加
- テスト: 既存6更新+新規 migration-v39、docs/module-reference.md 更新

Closes #1034

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- UI: 編集モーダルの詳細欄に CopyButton を配置し詳細本文をコピー(詳細が空のときは非表示)
- 既存 src/components/common/CopyButton.tsx / clipboard-utils.ts を再利用(スマホHTTPは execCommand フォールバック)
- i18n: locales/{en,ja}/worktree.json に todo.copyDetail 追加
- テスト: TodoPane.test.tsx にコピー導線テスト3件追加
- migration/DB/API 変更なし(UIのみ)

Closes #1036

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
globals.css に :root(light)/.dark(dark) の CSS 変数
(background/foreground/surface/muted/border/input/ring/accent-50..700/
success/warning/danger/info) を定義し、tailwind.config.js の primary(cyan)・
cmd-bg-dark を semantic トークンへ置換。layout.tsx / MainLayout.tsx の
body 背景を bg-background に統一。既存の実効値を写し取り視覚は不変(no-op)。
docs/design-system.md を新設。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
clsx + tailwind-merge の cn() ヘルパーと class-variance-authority を導入し、
Button/Card/Badge/Modal を cva ベースへ移行。Button ghost バリアントの
dark: 指定漏れ(ダークモードで視認性劣化)を修正。既存 API は完全互換。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
絵文字アイコン(🤖⚡✦💻)を廃止し lucide アイコンへ置換。共通レイアウト・ナビ
(Header/GlobalMobileNav/MobileTabBar/ActivityBar/terminal page)および worktree
各所のインライン SVG を lucide に統一。docs/design-system.md にアイコンサイズ規約を追記。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
geist パッケージ(セルフホスト)で GeistSans/GeistMono の CSS 変数を <html> に適用し、
tailwind fontFamily の sans/mono を「Geist → 日本語(Hiragino/Noto Sans JP) →
システム」フォールバックで構成。Terminal.tsx(xterm)の等幅も Geist Mono を先頭指定。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Input/Textarea/Select/Tabs/Tooltip/DropdownMenu/Switch/Skeleton を追加
(cva + cn, セマンティックトークン着色, ライト/ダーク対応)。Portal 系は
'use client' + Z_INDEX(POPOVER)で Modal と階層整合。Sessions ページの
フィルタ/ソートを新プリミティブへ移行(適用サンプル)。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cyan/blue 直書き(~600箇所 / 131ファイル)を #1041 のセマンティックトークンへ置換。
用途判定: インタラクティブ=accent / フォーカス=ring / 情報=info。gray 系は
surface/background/border/muted へ実効値を保って置換。例外(Terminal.tsx の
xterm ANSI 配色 / CLI ブランド色 / .scrollbar-thin 等の固定装飾色)は維持。
globals.css の .btn-primary・検索ハイライト等もトークン化。
敵対的レビュー指摘(focus:ring-accent→focus:ring-ring 統一、design-system.md
の例外記述修正)を反映。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
layout(Header/Sidebar の検索・トグル・言語)/ home / repositories / review /
more / chat / login の生button・手組みフォーム・コピペカードを @/components/ui
プリミティブへ移行。機能・挙動(onClick/フォーム送信/バリデーション/キーボード)は維持。
敵対的レビュー指摘(chat系ボタン移行・ghost の dark hover)を反映。ネイティブ select・
複合 textarea 入力バー・極小アイコントグルは意図的に残置(PR の残置リスト参照)。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
.btn/.btn-*/.card/.card-hover/.badge*/.input を globals.css から削除し、
Button/Card/Badge の cva 定義へ実クラスを内挿(旧 @apply 定義と同値、視覚不変)。
src/ 全体で削除クラスへの参照が 0 であることを確認。worktree の一部
(WorktreeDetailRefactored/LogViewer/WorktreeList/ExternalAppForm)をプリミティブへ移行し、
マジックナンバー入りインライン style を定数化。worktree/mobile/sidebar/common の
一括プリミティブ移行は規模の都合により #1061 へ分割。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
globals.css の :root/.dark トークン値を刷新(深い base + 微リフト surface +
ヘアライン border)。Header/GlobalMobileNav に backdrop-blur(supports-[backdrop-filter]
フォールバック付)。Card に elevated/interactive variant、h1-h4 見出しタイポを調整。
敵対的レビュー指摘を反映: 主要閲覧画面のコンテンツカード(sessions/home/review)を
bg-surface/border-border へ移行し背景反転による溶け込みを解消、ダーク入力を surface-2 で
凹み表現、Card interactive にキーボード focus 対応を追加。コントラストは WCAG AA 全 pass。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tailwindcss-animate を導入し、Modal/MobilePromptSheet の enter/exit、Radix data-state
連動(DropdownMenu/Select/Tooltip)、一覧の stagger、hover lift を統一適用。
prefers-reduced-motion 対応。ターミナル/仮想スクロールには非適用。敵対レビュー指摘
(reduced-motionでのtranslate漏れ→motion-safe化、sessionsエラー時のstagger再発火→list
マウント維持、Tooltip instant-open enter、home focus parity)を反映。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Home を縦積みから bento グリッドへ再構成(統計/直近セッション/ToDo/クイックアクション)。
PC は CSS Grid(grid-cols-12 + col-span)、モバイルは1カラム縦積み(useIsMobile の JS 分岐を
増やさず Tailwind breakpoint)。既存 sessions 取得フック/API を再利用し新規 API なし。既存機能
(ToDo 追加/チェック/コピー、セッションカウント)を維持。空状態対応。敵対的レビュー指摘
(ToDo タイルの h-full・見出しパリティ)を反映。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cmdk で ⌘K(mac)/Ctrl+K(win) のコマンドパレットを追加(Navigation / Worktrees 検索 / Actions)。
input/textarea/ターミナル入力中は非発火、SSR 安全('use client')、i18n(en/ja)、Modal 相当の z-index。
モバイルは GlobalMobileNav に起動導線。敵対的レビュー指摘を反映: open 時に入力へフォーカスし
キーボード操作(type/矢印/Enter)を復旧[HIGH]、worktree 一覧は共有キャッシュ context を再利用
(最新化+失敗時 Navigation fallback)、Escape の preventDefault/stopPropagation と close 時の
フォーカス復帰。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
StatusDot(running/waiting/idle/error)を新設。running はグロー+パルス+静的リング halo、
waiting は amber の弱点滅。サイドバー(BranchStatusIndicator)/ Home / Sessions を StatusDot 化。
prefers-reduced-motion でパルス停止(静的リングで running≠ready を維持)、ポーリングで再発火しない
CSS infinite アニメ。敵対的レビュー指摘(reduced-motion での running/ready 識別性、docs の実態訂正)を
反映。worktree 詳細/MobileHeader は従来スピナー表示のまま(docs に明記・今後の追随対象)。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
showGlobalNav 時に固定サイドバーを top-16 h-[calc(100vh-4rem)] にオフセットし、
半透明ヘッダー(z-50, backdrop-blur)がサイドバー先頭64px(Branches 見出し+
view/sort/sync ツールバー行)を遮蔽してクリック不能+灰色の滲みを生む問題を解消。
あわせて border-gray-200/dark:border-gray-600 を border-border にトークン化。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
next-themes を defaultTheme="system" enableSystem に変更し、ライト環境ユーザーへの
強制ダークを解消。Header 右クラスタに ThemeToggle を追加(発見可能性向上)。
globals.css に color-scheme(:root=light / .dark=dark)を宣言し、残存ネイティブ
コントロールの OS クローム事故(dark で白いドロップダウン等)を防止。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
window.innerWidth 依存を window.matchMedia(`(max-width: ${breakpoint-1}px)`)に置換。
innerWidth と CSS ビューポートが乖離する環境(実測 innerWidth=846 vs
documentElement.clientWidth=390)では、Tailwind の md:(min-width:768px)と JS 分岐が
食い違い「CSS はモバイル・JS はデスクトップ」の混成レイアウトになっていた。
max-width:breakpoint-1 は md: の正確な補集合。change イベント購読+cleanup、
SSR 初期値 false、MOBILE_BREAKPOINT export と breakpoint オプションは維持。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
陳腐化した Welcome バナー(#600 のナビ再編告知)を削除。h1「CommandMate」を機能的見出し
(HomeHeading: Overview + ライブサブライン)へ降格しヘッダーのワードマークとの同語反復を解消。
Home の数値・相対時刻・サイドバーのブランチ数バッジに tabular-nums を導入。HomeQuickActions の
inline SVG を lucide へ置換。相対時刻の短縮形は新設 date-utils、見出し文言は home i18n
namespace(en/ja)。機能(ToDo/カウント/遷移)は不変、サブラインは既存キャッシュから算出。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
light を「白ページ+凹む灰カード」から Vercel/Linear 型の「グレー地ページ+
白カード+2層影」へ反転し、dark(#1049)と対のエレベーションモデルにする。
surface-2(740) < background(751) < surface(765) の「白カードが浮き、well が沈む」階梯。

- globals.css :root: --background 255→250 250 251 / --surface 248 250 252→255 255 255
  / --surface-2 241 245 249→244 246 250。.dark は不変(回帰なし)
- tailwind.config.js: 2層 boxShadow.sm を新規追加(md/lg は Tailwind 既定のまま)
- design-tokens/surface-depth テスト更新 + WCAG コントラスト検証を追加
  (foreground≥4.5 全面 / muted-fg≥4.5 page,card / ≥3 recessed well)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bg-gray-800 text-white 固定でテーマ孤島だったサイドバーを、新設の
--sidebar / --sidebar-foreground / --sidebar-border / --sidebar-hover /
--sidebar-muted セマンティックトークンへ移行。light=slate-50系の明るいパネル、
dark=#141821(#1049階梯と整合)でテーマに追従する。standalone なリテラルRGB値
としており、#1074 の --surface 反転がサイドバー色へ滲まない。

- globals.css :root/.dark に --sidebar-* を定義、tailwind.config.js に登録
- Sidebar.tsx / sidebar/*(BranchListItem, SortSelectorBase)/ 検索Input
  / LocaleSwitcher / ThemeToggle の直書きを全てトークンへ置換
- 敵対的レビュー由来のライト回帰修正2件:
  - LogoutButton.tsx: 漏れていた text-gray-300 直書きを ThemeToggle と同じ
    テーマ中立トークン(text-muted-foreground/hover:bg-muted)へ(モバイル共用)
  - SortSelectorBase: 選択中ソートを text-accent-700 dark:text-accent-400 に
    (ライトのslate-50上で 1.73→約5.1:1 でAA達成、dark時は据え置き)
- docs/design-system.md に --sidebar-* を追記、既存テスト/期待値を更新

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) (#1089)

- src/components/ui/Kbd.tsx を新設(semantic-token ベース、#1080 で再利用)し ui/index.ts に export 追加
- NAV 行に lucide アイコン、worktree 行に GitBranch + StatusDot(isProcessing/isWaitingForResponse/isSessionRunning から算出)
- バックドロップ/パネル/選択状態/入力/フッター文字色を semantic トークン化(bg-surface/backdrop-blur/ring-border/accent-500)
- キーボードガイドのフッター(↑↓/↵/esc、i18n)
- Recent グループ(localStorage cm.palette.recents、MRU<=8、空クエリ時のみ、dead worktree は無視)
- worktree を isSessionRunning 優先→updatedAt 降順ソート+空クエリ時 slice(0,8)
- Actions 拡充: Sync repositories(repositoryApi.sync + Toast)/ 言語切替 / Open GitHub
- Header に ⌘K 検索ピル(mount 後に platform 判定して ⌘K/Ctrl K を出し分け)
- #1053 の挙動契約(フォーカス/入力ガード/Escape/backdrop/data-testid)を維持
- 文言を locales/{en,ja}/commandPalette.json に追加、単体テスト更新

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
worktree ターミナルの全周枠を廃し surface-2 カード+ヘアラインのカード化に統一。
CLI インスタンスセレクタを native <select> から Radix DropdownMenu(StatusDot+alias
+ChevronDown)へ置換し、value/onChange の単一選択セマンティクスと testid を保持。

- Terminal / ActivityBar / TerminalDisplay / TerminalSplitContainer /
  TerminalSplitPane / TerminalSplitPaneContent を surface/border トークンへ
- TerminalSplitPane: <select> → DropdownMenu(RadioGroup)。cli-selector testid 保持
- xterm の theme は意図的に固定ダーク(--surface-2 dark 相当)を維持
- StatusDot/status-colors.ts は不変(#1078 の領域)。locales worktree.json 追記
- 既存 terminal テスト(cli-tab-switching 含む)更新・全パス

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kewton and others added 29 commits July 17, 2026 22:09
POST /api/app/update は acquireUpdateLock() の成功後、spawn 失敗時に
releaseUpdateLock() する try の外で ensureConfigDir()(ログパス解決)を
実行していた。設定ディレクトリが書き込めない等でここが throw すると、
ロックが解放されないまま例外がハンドラを抜け、以降の update 要求は
UPDATE_LOCK_TIMEOUT_MS(10分)の stale 再取得まで理由不明の 409
in_progress で拒否される。

ロック取得後の処理(willUpdateRestartServer / ensureConfigDir /
spawnUpdate)を既存の try に入れ、いずれの失敗でもロックを解放して
500 spawn_failed を返す。成功時にロックを保持したままにする既存の設計
(起動した update がこのプロセスを置き換えるため解放者が存在しない)は
不変のまま。

回帰テスト: ensureConfigDir() が throw するケースで
releaseUpdateLock() が呼ばれ 500 spawn_failed を返すことを検証。修正前の
route.ts に対して実際に赤くなる(例外が POST を素通りする)ことを確認済み。
既存テストの beforeEach に ensureConfigDir のモック再設定を追加した:
clearAllMocks() は実装を消さないため、throw させるテストが後続へ漏れる。

品質ゲート(exit code で判定): lint=0 / tsc=0 / build=0 /
CI=true test:unit=0(585 files, 9934 tests, Errors 行なし)。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`DELETE /api/repositories` は worktree 件数チェックの前に `disableRepository()` を
呼んでおり(#190 SF-C01)、`disableRepository()` は未登録パスに対して行を新規作成する。
このため worktree を持たない未登録パスの削除は、404「Repository not found」を返しながら
`enabled=0` / worktree 0 件の行だけを `repositories` テーブルに残し、リポジトリ管理画面に
「Disabled」の幽霊行として残置していた。#1339 と同型の行をランタイムで再生産する経路。

新規作成を行わない `disableExistingRepository()` を追加し、404 経路では登録済みの行が
ある場合のみ無効化する。

#190(SF-C01)の意図は保たれる:
- Sync All が復活させうる環境変数由来のリポジトリは、サーバー起動時の
  `initializeWorktrees()`(server.ts:216)と Sync All 自身(sync/route.ts:43)が
  `registerAndFilterRepositories()` -> `ensureEnvRepositoriesRegistered()` で
  `repositories` に登録済みである。したがって「未 sync(worktree 0 件)の環境変数
  リポジトリを削除 -> 除外登録される」という SF-C01 の受入条件は、行の更新として成立する。
- 行が無く worktree も無いパスはどの経路からも復活しないため、除外登録の対象になりえない。
  この場合のみ行を作らない、という線引き。
- worktree を持つ未登録パスは復活の対象になりうるため、従来どおり新規作成する。

テスト:
- 既存の SF-C01 テストは削除せず、環境変数リポジトリの起動時登録
  (`ensureEnvRepositoriesRegistered`)を前提に置いたうえで「404 でも除外登録される」
  ことと `filterExcludedPaths()` で除外されることを検証する形へ更新
- 回帰テスト(未登録パスの 404 で行が作られない / 繰り返しても増えない /
  worktree があれば従来どおり作られる)を追加。修正前のコードでは前2件が red になることを確認済み
- `disableExistingRepository()` の unit テストを追加

補足(Issue 本文の記載について):
- Issue 本文の「#190 の意図=除外登録は、実在リポジトリに限定しても満たせるか要確認」は
  上記のとおり満たせる(環境変数リポジトリは起動時に登録済みのため)
- 本修正はランタイム経路のみ。既存の幽霊行の掃除(マイグレーション)は #1339 の担当範囲

スコープ外:
- #1347(`validation.resolvedPath` が使われていないパス正規化不一致)には触れていない

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#1328 以前の getRepositoryPaths() は CM_ROOT_DIR を単一のリポジトリパスとして
返しており、ensureEnvRepositoriesRegistered() がそれを is_env_managed=1 の行と
して registered していた。CM_ROOT_DIR はリポジトリを格納するコンテナであって
リポジトリではないため git worktree list は 0 件を返し、worktree 行が作られない
まま リポジトリ行だけが残る。#1328 は自動発見自体は削除したが既存行は残す判断を
したため、既存利用者の DB に worktreeCount:0 の幽霊行が残置されている。
マイグレーション v43 で削除する。

削除条件は次のすべてを満たす行のみとし、判定はいずれも「残す」側へ倒す:
  1. CM_ROOT_DIR (legacy MCBD_ROOT_DIR) が明示的に設定されている
  2. 解決済みパスが WORKTREE_REPOS のエントリに含まれない
  3. その path を持つ worktrees 行が 1 件も無い
  4. is_env_managed = 1

実機同等の再現で検証済み: v42 DB に実際の ensureEnvRepositoriesRegistered() で
幽霊を作り、getAllRepositoriesWithWorktreeCount()(GET /api/repositories の実装)
が Issue 記載どおり name:"repos"/worktreeCount:0 を返すことを確認したうえで、
v43 適用後に幽霊のみが消え、実リポジトリと利用者が無効化した行が enabled=false の
まま残ることを確認した。各ガードは個別に破壊して対応テストが落ちることも確認済み。

--- Issue 本文の記載に対する補足(本文は orchestrator 管理のため未編集)---

Issue の案A(「is_env_managed=1 かつ worktree 行を持たない行を削除」)は採用して
いない。誤爆が無害ではないため:
ensureEnvRepositoriesRegistered() は行が無ければ enabled:true で再作成するので
(db-repository.ts:469)、利用者が意図的に無効化した WORKTREE_REPOS リポジトリを
消すと無効化が解除されて復活し、visible / display_name も失われる。案A は
「一時的に worktree が 0 件のリポジトリ」でこれを踏む。Issue 本文が併記していた
安全側の案(CM_ROOT_DIR 完全一致に限定)を採った。

環境変数を getEnv() 経由で読まないのも同じ理由による。getEnv() は CM_ROOT_DIR
未設定時に process.cwd() へフォールバックする (env.ts:218) ため、DELETE の狙いが
サーバーの起動ディレクトリ(実在のリポジトリでありうる)に向いてしまう。未設定は
「対象なし」として skip する。この退行はテストで固定した。

--- 委任時の事前調査に対する補足 ---

「マイグレーション追加時に更新が必要な 3 箇所」に加えて、4 箇所目があった:
tests/unit/lib/db-migrations.test.ts:37 が CURRENT_SCHEMA_VERSION を 42 と
ハードコードしており、43 へ上げると落ちる(実際に full suite で 1 件失敗を確認)。
併せて更新した。事前調査で安全と確認されていた tests/unit/db/future-schema-guard.test.ts
は import して使っているため指摘どおり無変更で通る。

また CHANGELOG の #1353 エントリ内 "CURRENT_SCHEMA_VERSION(現在 42)" が本変更で
不正確になるため 43 に更新した(同一 Unreleased 節のため未リリース)。

なお disableRepository() が作る is_env_managed=0 の幽霊行は別個体であり本修正の
対象外(#1346)。

品質ゲート(exit code で判定): lint=0 / tsc=0 / build=0 /
CI=true test:unit=0(587 files, 9960 tests passed, Errors 行なし)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
common/Tooltip はバブルを absolute 配置の子要素として描画し、水平方向は
left-1/2 -translate-x-1/2 で中央寄せするだけだった。この方式には位置補正の
余地がなく、トリガー中央から左右へバブル幅の半分がはみ出すことを構造的に
避けられない。サイドバー(既定幅 224px・最小 160px)のヘッダーアクションは
狭い祖先の中にあるため、placement="bottom" のツールチップが祖先の境界と
ビューポート端の双方で切れていた(最悪ケースは Sidebar の ViewModeToggle で
既定幅でも約 74px はみ出す)。#1364 で追加報告された WorktreeDetailSubComponents
のインスタンス切替タブは idle 時に 24px の丸ボタンへ縮退し全ラベルを
ツールチップに委ねるため、切れると情報が失われる。

既にポータル + 水平 clamp を実装済みの common/TruncationTooltip に倣い、
バブルを document.body へ createPortal し、トリガー矩形から算出した
position: fixed 座標で配置したうえで上下左右をビューポート内へ clamp する。
位置計算は純関数 computeTooltipPosition() に切り出して export した(jsdom は
矩形を常にゼロサイズで返すため、この分離がないと clamp をテストできない)。

衝突判定ライブラリ(Radix 等)は導入していない。それらはトリガーへ ref を
注入するため子要素の clone が必要になり、「子を clone せず wrapper が
マウスイベントを受ける」「バブルは role="tooltip" + aria-hidden="true" とし、
aria-label との二重読み上げを避けるため aria-describedby を子に張らない」と
いう #730 の a11y 設計と両立しない。この設計は不変とし、ActivityBar
(placement="right"・最左列かつ垂直センタリングのため元から切れていない)を
含め回帰なしを回帰テストで固定した。

absolute 子から position: fixed へ移ったことでバブルがトリガーの移動に追従
しなくなるため、表示中のみ resize と(祖先のスクロールコンテナを拾うため
capture フェーズの)scroll を購読して再計算する。

テストは削除ではなくポータル後の DOM 契約へ書き直した。トリガーは wrapper 内に
残りバブルだけが body 直下へ出ることを固定し、clamp を無効化 / ポータルを撤去
する変異でそれぞれ 5 件 / 3 件が落ちることを確認済み。


Claude-Session: https://claude.ai/code/session_013ayPE1oBaDAB4idFRzUEAD

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ブランチ checkout メニューは `absolute left-0 top-full z-20` でコンポーネント
自身の subtree 内に描画されており、3つの問題を同時に抱えていた。

1. 祖先による切り取り: Git ペインは `overflow-y-auto` のスクロールコンテナで
   あり、その外へ張り出すメニューは z-index に関係なくクリップされる
2. 下端はみ出し: `top-full` から常に下方向へ開きフリップが無いため、最大 256px
   (`max-h-64`) のメニューがトリガー位置次第でビューポート下端を突き抜け、
   末尾のブランチが選択できない
3. 低い stacking: `z-20` はペイン内の他のオーバーレイに負けうる

メニューを `createPortal(..., document.body)` で body 直下の `fixed` 要素として
描画し、表示時に `getBoundingClientRect()` でトリガーとメニューの実寸を測って、
下に入りきらず かつ 上の余白の方が大きいときのみ上方向へフリップする(通常の
下開きは維持)。上下・左右とも 8px マージンでビューポート内へ clamp し、
メニューがビューポートより大きい場合は開始辺 (top / left) 側を優先して到達可能に
する。z-index はオーバーレイ相当 (`z-50`) へ引き上げた。

ポータル化によりメニューは Git ペインのスクロールに追従しなくなるため、
`scroll` (capture: true) と `resize` で再測位してトリガーへのアンカーを保つ。
座標確定までは画面外へ退避させ (0,0) への一瞬の描画を防ぐ。ハンドラ・
data-testid・onCheckout 契約は不変。

回帰テストは jsdom に レイアウトエンジンが無いため getBoundingClientRect を
data-testid で判別してスタブし、ポータル先・下開き・上フリップ・両軸 clamp・
スクロール追従・z-index を pin する(修正前のコードでは 9 件中 7 件が失敗し、
残る 2 件は挙動非変更を担保するもの)。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ContextMenu は useContextMenu が拾ったポインタ座標をそのまま
position: fixed の left/top に流していたため、ビューポート右端・
下端付近で開くとメニューが画面外へはみ出していた。fixed 配置は
スクロールで画面内へ戻せないため、はみ出した項目(下端では Delete
を含む)は操作不能になる。375px 幅のモバイルではロングプレスでも
同じ経路を通るため右下で顕著だった。

useLayoutEffect で getBoundingClientRect() によりメニュー実寸を測り、
各軸をビューポート内(端から 8px マージン)へ clamp する。実測は
アンカー位置ではなく原点で行う: fixed 要素の幅はビューポート端まで
の残り幅に shrink-to-fit するため、端の近くで測ると本来必要な幅より
狭い値が返り clamp 後もはみ出しうる。測定パスは paint 前に完了する
ため利用者には見えない。メニューがビューポートより大きい場合は近い
側の端に寄せ、先頭項目が画面外へ押し出されるのを防ぐ。座標は測定
対象(アンカー座標+targetType)をキーに保持し、閉じるアニメーション
中に原点へ戻らないようにしている。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
サイドバーのブランチ項目のツールチップは createPortal + position: fixed まで
は済んでいたが、座標が `setCoords({ top: rect.top, left: rect.right + 8 })` の
固定計算で clamp・フリップ・衝突判定が一切無く、以下で見切れていた。

1. サイドバーを広げた場合(幅 160〜480px 可変): right≈488 + max-w-sm(384px)
   の展開が右端を突き抜ける
2. 一覧下端の項目: top: rect.top 固定・下方向フリップ無しのため、高さ上限の
   無い description が下端で切れる
3. モバイルドロワー(w-72=288px / 375px 幅): left≈296 からの 384px 展開が
   画面をほぼ完全に突き抜ける。ホバーの無い端末でも :focus-visible で発火する

表示時に bubble の実寸を getBoundingClientRect() で測り、水平は右側優先+左側に
余地がある場合のみフリップ、それ以外は両軸とも clampAxis() でビューポート内
(8px マージン)に収める。clamp だけでは 384px の bubble を 375px の画面に入れ
られないため maxWidth も min(384, innerWidth - 16) で頭打ちにした(広い画面では
従来どおり max-w-sm)。bubble がビューポートより大きい場合は開始側の端に寄せる。

#1341 が common/Tooltip.tsx に入れた clamp と同じ考え方だが、並行修正の衝突を
避けるため共通ユーティリティ化はせずコンポーネント内にローカル実装している。

回帰テストは 3 条件それぞれ+フリップ+通常配置+clampAxis 単体をカバーし、
修正前のコードに対して 9 件が失敗することを確認済み。


Claude-Session: https://claude.ai/code/session_01TyWLgAxaZBDrhyg1sd33RX

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#1341 が common/Tooltip をポータル化+ clamp した後も、ビューポート端で
見切れうるフローティング要素が5箇所残っていた。いずれもトリガーからの
相対位置だけで配置し、実際に画面へ収まるかを見ていない。

- SlashCommandSelector / FilePanelTabs / SortSelectorBase /
  FileMetadataToggle: 開いた直後に getBoundingClientRect() で実測し、
  はみ出す分だけ transform で引き戻す1軸 clamp をローカル実装した。
  外側クリック判定がコンテナ ref の内包を見ているため、ポータル化はしない
  (body 直下へ出すとメニュー自身へのクリックで即閉じる)。
- TruncationTooltip: 水平 clamp は不変のまま、表示後に実測して下方向へ
  溢れる場合はトリガーの上へ flip する(上にも入らなければ下端にピン留め)。

モバイルのボトムシートは fixed で常に画面内のため対象外。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
リポジトリ登録の3経路のうち scan(POST /api/repositories/scan)だけが
repositories 行を作らず、scanWorktrees → syncWorktreesAndCleanup で
worktrees テーブルだけを更新していた。管理画面は repositories 駆動
(getAllRepositoriesWithWorktreeCount)のため行が無いと現れず、scan 登録
リポジトリは①管理画面に出ず無効化・表示切替できない、②repository_todos が
repositories.id を参照するため Home ToDo も使えない、③次回 sync の対象集合
(env ∪ DB 登録済み enabled パス)に入らず以降 sync で更新も剪定もされない、
という状態に陥っていた。

scan 成功時(worktree を検出したリポジトリごと)に repositories 行を upsert
するようにし、clone 経路(CloneManager.onCloneSuccess → createRepository)と
整合させた(cloneSource: 'local', enabled/visible 既定 true)。
getRepositoryByPath() で既存行を確認し存在する場合は上書きしない
(利用者の enabled/visible/disabled 設定を保持。ensureEnvRepositoriesRegistered
の冪等な挙動に倣う)。複数 worktree を持つ単一リポジトリでも repositoryPath で
de-dup して1行のみ作成する。

db-repository.ts の関数定義は変更せず、既存の getRepositoryByPath /
createRepository をルートから呼び出す(#1350/#1352 が並列で db-repository.ts を
触るため定義には手を入れない)。回帰テストを tests/unit/api/repositories-scan.test.ts
に追加(新規登録・冪等・de-dup・basename フォールバック)。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getDbInstance() の接続 pragma は foreign_keys = ON のみで、busy_timeout
(既定 0 = ロック競合で即エラー)も journal_mode(既定のロールバック
ジャーナルは書き込み時に排他ロックを取る)も未設定だった。設定ミスで
2 つのサーバープロセスが同一 DB ファイルを指すと、書き込み競合が散発的な
500(SQLITE_BUSY)として現れ原因特定が難しかった。

接続直後・マイグレーション前(migration の書き込みにも効かせるため)に
journal_mode = WAL と busy_timeout = 5000 を追加。foreign_keys = ON
(#294、マイグレーション前必須)と #1353 のスキーマ検証後 singleton 代入の
前後関係は不変。多重オープンの検知・拒否は Issue 優先度が低いため見送り。

回帰テスト tests/unit/db/db-instance-pragmas.test.ts を追加(WAL は :memory:
では無視されるため実ファイル DB で検証)。

Issue 本文の記載について: 「対策コストが極小(pragma 2行)」とあるが、
db.pragma() は journal_mode と busy_timeout で 2 行、加えて既存の
foreign_keys で計 3 行になる(本修正では 2 行追加)。


Claude-Session: https://claude.ai/code/session_01BiJ2AKS69dJHVJvjExvG8b

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
)

update-check の currentVersion / hasUpdate 判定は next.config.js が
NEXT_PUBLIC_APP_VERSION として next build 時に焼き込んだ値を参照しており、
部分ビルド運用(next build のみ / build:all を通さない)では .next の焼き込み版と
dist/server の実コード版が乖離しうる。#1337/#1338 の版照合はこの値を信頼の起点に
するため、値自体の信頼性が前提になる。

- getServerVersion() を追加し、サーバー実行時版は process.cwd()/package.json
  (start.ts が cwd: packageRoot で spawn するインストールディレクトリの実 manifest)
  を実行時に読む。解決順は ①ランタイム package.json(name === "commandmate" を検証)
  → ②焼き込み NEXT_PUBLIC_APP_VERSION → ③"0.0.0"。name 検証で想定外の cwd が
  無関係な版を漏らすのを防ぎ、読み取り失敗・不正 JSON・版欠落・別パッケージ・
  環境変数未設定はすべて安全側へフォールバックする。
- getClientVersion() を分離。クライアントの焼き込み版は「このページのバンドルの版」
  として正しいため NEXT_PUBLIC_APP_VERSION 参照のまま維持(サーバー実行時版と
  クライアント焼き込み版の分離は #1338/#1356 の版不一致検知の土台)。
- getCurrentVersion() は後方互換のため残し getServerVersion() へ委譲。
- 回帰テスト: 実 temp package.json + cwd モックで getServerVersion/getClientVersion/
  getCurrentVersion の全分岐(ランタイム読取・name 不一致・欠落・不正 JSON・版欠落・
  env フォールバック・0.0.0)を追加。既存 checkForUpdate テストは cwd を非 commandmate
  ディレクトリへ固定し焼き込み版フォールバックを維持。

Issue 記載の補足: update.ts:74 のコメント(CLI 文脈で getCurrentVersion() が 0.0.0 に
なり使えない旨)は本修正後も有効。平常の CLI では cwd が commandmate のパッケージルート
でなく name 検証が外れて 0.0.0 にフォールバックするため、CLI は従来どおり
readInstalledVersion()(__dirname 基準)を使う。update.ts は本 Issue 対象外のため未変更。


Claude-Session: https://claude.ai/code/session_01XVwHkadHm5uwDwAbyorgDW

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DELETE /api/repositories は validateRepositoryPath() で resolvedPath を
算出しながら、生の repositoryPath を getWorktreeIdsByRepository() /
deleteRepositoryWorktrees() の WHERE キーに使っていた。worktrees.repository_path
は scanWorktrees() が path.resolve() して格納するため、末尾スラッシュ・
相対パス・".." を含む非正規パスで削除要求が来ると worktree の照合が空振りし、
404 を返しつつ worktree 行が DB に残置される(除外登録は正規化側で成立し
「消せないのに Disabled」の不整合も生む)。

ハンドラ冒頭で `const repositoryPath = validation.resolvedPath!` として、
以降の全 DB 操作・ログ・broadcast を正規化パスに統一した。#1346 が追加した
disableExistingRepository の 404 経路構造は変更していない。worktrees.ts /
DB 層の関数シグネチャは変えず(#1349 が並行修正のため)、呼び出し側の引数
正規化のみで対応。回帰テスト3件(末尾スラッシュ / ".." / 非正規パスでの
除外登録)を追加。

補足: Issue 本文は getWorktreeIdsByRepository / deleteRepositoryWorktrees を
src/lib/git/worktrees.ts と記載していたが、実体は src/lib/db/worktree-db.ts。


Claude-Session: https://claude.ai/code/session_01AJiWDuWLV9i4DBF4hqC8ct

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (#1385)

2つの POST 経路が DB の UNIQUE 制約を事前チェックせず生 INSERT していたため、
制約違反が原因不明の 500 として表面化していた。

- schedules: scheduled_executions は UNIQUE(worktree_id, name)。
  POST /api/worktrees/:id/schedules は同名重複を検査せず INSERT していたため、
  catch の一律 500 に落ちていた。INSERT 前に重複を検証し 409 DUPLICATE_NAME を返す。
- memos: worktree_memos は UNIQUE(worktree_id, position)。
  POST /api/worktrees/:id/memos は position 明示指定の分岐だけ空きチェックを
  通っておらず(自動採番分岐のみ usedPositions を参照)、UNIQUE 違反が 500 に
  なっていた。明示 position を事前検証し 409 DUPLICATE_POSITION を返す。

あわせて INSERT 側でも UNIQUE 違反を捕捉してエラーコードへ変換する多重防御を
追加した(createMemo は src/lib/external-apps/db.ts の ExternalAppDbError('DUPLICATE')
に倣い MemoDbError('DUPLICATE_POSITION') を throw、schedules 経路は catch 内で
UNIQUE constraint を検出して 409 化)。事前チェックと INSERT の間の競合でも
500 に戻らない。制約は worktree スコープのため、別 worktree での同名・同 position は
従来どおり許可される。FOREIGN KEY 等その他の DB エラーは素通しで 500 のまま。

回帰テスト:
- tests/integration/api/schedules.test.ts(新規): 同名で 409 DUPLICATE_NAME、
  happy path 201、別 worktree での同名許可
- tests/integration/api/memos.test.ts: 明示 position 重複で 409 DUPLICATE_POSITION、
  未指定時の自動採番回帰
- src/lib/__tests__/db-memo.test.ts: createMemo が MemoDbError(DUPLICATE_POSITION) を throw

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1352) (#1386)

#1350: worktree 全滅後にディスク上の実体が消えた repositories 行(enabled=1 /
worktree 0)が幽霊化し、物理削除経路が無いため同一 URL の再 clone を恒久的に封鎖して
いた。startCloneJob() の重複判定(正規化 URL 一致 checkDuplicateRepository、および
#1340 の path 一致 checkRepositoryAtPath)に removeIfGhostRepository() を挟み、
「ディレクトリ不存在 かつ worktrees 行 0 件」の行を物理削除して clone を許可する。
①ディレクトリ実在(通常の重複)②worktrees 行が残存(ディレクトリが一時不在なだけ
かもしれず記録を黙って落とさない)の場合は live とみなし従来どおり拒否する。この
「worktree を持つ行は幽霊ではない」判定は v43(#1339)と同じ考え方。物理削除用に
deleteRepository()、幽霊判定用に countWorktreesByRepositoryPath() を db-repository.ts
に追加(worktrees は FK でなく repository_path の TEXT 一致リンクのため cascade しない)。

#1352: createRepository() の INSERT が try/catch 未装備で、並行 clone の TOCTOU で
UNIQUE 違反時に raw SqliteError を throw し、背景 clone 経路(#1340)でログのみに握られ
「成功したはずの clone が黙って失敗」となっていた。external-apps/db.ts に倣い UNIQUE 違反
を RepositoryDbError('DUPLICATE') に、他 DB エラーを 'DB_ERROR' に変換(元エラーは cause
に保持)。DB の UNIQUE 制約が登録の最終直列化を担い、本修正は衝突防止でなく raw throw の
型付き化。あわせて is_env_managed が #1328 以降デッドフラグ(読み手は v43 のみ)である旨を
型定義に @deprecated として明記(カラム削除は #1347/#1348 の周辺参照を避け見送り)。

関数の削除・改名はせず内部実装の強化に留め、#1340 の checkRepositoryAtPath /
DUPLICATE_REPOSITORY_PATH は保持(worktrees 残存時に到達可能)。既存の重複判定テストは
「実体を伴う live 行」を表すよう existsSync=true / worktree 登録で更新し、幽霊掃除の
新規回帰テストを clone-manager / db-repository / api-clone(integration) に追加。

品質ゲート: lint(0 error) / tsc / build / CI=true test:unit(10033 passed) いずれも exit 0。

注: 両 Issue 本文中の行番号参照(例 #1350 の clone-manager.ts:344-353、#1352 の :356)は
現行コードから軽微にドリフトしているが、根本原因の分析自体は正確。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1387)

PID ファイル(`src/cli/utils/pid-manager.ts`)が `String(pid)` しか記録しておらず、
稼働プロセスの実体を照会する手段が無かったことに起因する3件を統合修正する。

- #1354: `status` が稼働デーモンの版を表示せず、新 CLI + 旧デーモンでも「Running」
  としか出ないため版ずれを検知できなかった。
- #1355: `--port`/`--cert`/`--auth` の実効設定が永続化されず、`getStatus()` が URL を
  現在の .env から都度再計算するため status/quickstart の URL が実サーバーと乖離していた。
- #1358: `isProcessRunning()` が `process.kill(pid,0)` のみで同一プロセス検証をせず、
  PID 再利用時に status 誤陽性・無関係プロセスの kill・start 恒久拒否を招いた。
  さらに EPERM を throw してコマンド自体が予期せぬエラー終了していた。

## 変更内容

- PID ファイルを `{ pid, version, port, bind, protocol, auth, startedAt, startTime }` の
  JSON state ファイルへ拡張(`PidManager.writeState()`/`readState()`、O_EXCL atomic write は不変)。
  旧デーモンが書いた bare-integer 形式は `readState()` が `{ pid }` として読むため後方互換。
- `daemon.start()` が起動時に version(`readPackageVersion()`:モジュールキャッシュに版を
  固定させないためディスクから読む)・実効 port/bind/protocol/auth・`ps -o lstart=` で採取した
  プロセス開始時刻(`process-inspector.ts`、macOS/Linux 両対応・取得失敗は null で best-effort)
  を state に記録する。
- `getStatus()` は state の実効設定を優先して port/URL/version/protocol/auth を返す(旧形式は
  #1266 の .env 解決へフォールバック)。`status` は版を表示し、インストール済み CLI と食い違えば警告する。
- `isProcessRunning()` は `process.kill(pid,0)` 成功後に記録済み開始時刻と生プロセスの開始時刻を
  照合し、不一致(PID 再利用)を stale として弾く。EPERM も throw せず stale 扱いにする。
  これにより stop の誤 kill と start の「already running」固着が解消する。開始時刻を取得できない
  環境では従来どおり best-effort で running を返す。

## Issue 記載に関する補足

- quickstart.ts(#1355 が原因箇所に挙げる `ensureServerRunning`)は `getStatus()?.url` を
  返すだけのため、本修正で getStatus が state ベースの正しい URL を返すことで自動的に是正される。
  quickstart 本体の版照合は #1337 Wave2 の担当のため、本コミットでは変更しない。
- #1355 の「getStatus が .env から再計算」は #1266 で .env 優先へ整理済みの前提を含む記述で、
  本修正はそれを「起動時に記録した実効設定を優先」へ置き換える形になる。

回帰テスト: pid-manager(state/identity/EPERM)・daemon(state 記録・state 優先 getStatus)・
status(版表示・版ずれ警告)・process-inspector・package-info を追加。
lint / tsc / build / CI=true test:unit いずれも exit 0。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
npx commandmate@latest を叩いてもデーモン稼働中なら ensureServerRunning は
再起動せず既存 URL を返すだけで、公開済みの修正が黙って利用者に届かなかった。
しかも npx …@latest --version は取得したての CLI 版を表示するため、利用者は
「最新で動いている」と誤認する(別のものを指しているだけで嘘ではない)。

#1354 でデーモンが状態ファイルに記録するようになった版(getStatus().version)と
readPackageVersion() が返す CLI 版を照合し、食い違う場合に「稼働中サーバーは更新
されない」旨と commandmate update への導線を warn 表示する(status コマンドの
printVersionInfo と同じ仕組みを踏襲)。自動再起動は稼働中セッションを巻き込むため
行わず、既定は警告に留める(Issue 対策①)。版を記録しない旧デーモンや CLI 版が
解決できない場合は従来どおり無警告で URL を返す。

担当は quickstart.ts のみ。api-client.ts は触らない(#1357 が並列で対応)。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CLI の `ApiClient.get<T>/post<T>/patch<T>` は受信 JSON を `as T` で無検証
キャストしており、形の違うレスポンスを返す旧デーモンを黙って受け入れて
いた。特に `fetchAgentInstances()` の `worktree.agentInstances ?? []` は、
roster API を持たない旧デーモンでのフィールド欠落を無音の空配列に丸め、
ユーザーには「インスタンスが無い」ように見えてしまっていた。

対応:
- `assertResponseShape()` を追加(必須トップレベルフィールドの存在チェック)。
  欠落時は「サーバーが古い可能性」を示す `ApiError` を投げる。
- `fetchAgentInstances()` は `?? []` をやめ `agentInstances` の存在を検証。
  欠落(=旧デーモン)はエラーとして表面化させ、現行デーモンが常に含める
  空配列は「インスタンス無し」として従来どおり正しく通す。
- #1359 で信頼できる実行時版になった `GET /api/app/update-check` の
  `currentVersion` を読む `fetchDaemonVersion()`(非 throw・不明時 undefined)と、
  CLI 自身の版(`readPackageVersion()`)と食い違えば stderr に警告する
  `warnIfVersionSkew()`(advisory・非 throw)を追加。接続経路への配線は
  #1337 側(quickstart.ts)が担うため本コミットでは触らない。

新規エンドポイントは作らず既存の update-check を利用(#1338/#1356 と競合回避)。
変更は担当ファイル `src/cli/utils/api-client.ts` と
`src/cli/utils/agent-instances.ts`(+各テスト)のみ。


Claude-Session: https://claude.ai/code/session_01CziE7EzmyUNrbyFcz4HXAy

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
リポジトリのディレクトリ削除・非git化で scanWorktrees() が git exit 128 に
より [] を返すと、そのリポジトリはスキャン結果から消え、syncWorktreesToDB()
の per-repo 剪定にも到達しない。結果として worktree 行が DB に残置し、サイド
バーに幽霊行として残る。syncWorktreesToDB() のグローバル early-return は「全
リポジトリが空のとき」しか効かないため、健全なリポジトリに混じった単一の消滅
リポジトリは永久に回収されなかった。

対応:
- worktrees.ts に pruneStaleRepositoryWorktrees(db, liveWorktrees) を追加。
  DB に worktree 行を持つ全 repository_path を getRepositories() で列挙し、
  当該スキャンで worktree を産まなかったもののうち、ディレクトリ(または
  .git)が実際に消えている場合のみ行を CASCADE 削除する。
- repositoryExistsOnDisk() を追加。dir と .git の双方が存在するときのみ
  「実在」と判定し、それ以外(EACCES 等の不確定な読み取りも含む)は剪定
  しない側に倒す。ディレクトリが実在するのに git がエラーを返しただけの
  ケース(ロック中・ネットワークドライブの一時不可視等)で幽霊行と誤判定
  して履歴を破壊しないため。
- 呼び出しは全リポジトリを対象とする POST /api/repositories/sync のみ。

設計判断(Issue 本文との差異): Issue は syncWorktreesToDB() 自体の改修を
想定していたが、同関数は scan/restore 経路(scanWorktrees の単一リポジトリ
結果を渡す)からも呼ばれる。ここに全 DB リポジトリを走査する剪定を組み込むと、
単一リポジトリのスキャンが対象外リポジトリの行まで剪定してしまい、「同一
リポジトリ内のみ削除」という既存不変条件(および回帰テスト)を破る。そのため
剪定は独立関数として切り出し、全体を掌握する全体スキャン経路のみが呼ぶ構成に
した。行キーは #1347 の正規化前提を踏襲し、getRepositories() が返す
path.resolve() 済みの repository_path を DB・ファイルシステム双方で一貫使用。

回帰テスト: pruneStaleRepositoryWorktrees / repositoryExistsOnDisk に対し、
実 temp ディレクトリ+実 SQLite(FK ON)で、ディレクトリ削除→剪定・非git化
→剪定・実在時は空スキャンでも非剪定・消滅リポジトリのみ剪定して健全リポジトリ
は不変・スキャンに現れたリポジトリはFS状態に依らず生存、を検証。

品質ゲート: lint / tsc / build / CI=true npm run test:unit いずれも exit 0
(unit 10079 passed)。


Claude-Session: https://claude.ai/code/session_013uPu2nRJSA3fWZhYEYYYEY

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
サーバーを新版に入れ替えた後も開いたままの旧タブは、参照する
`/_next/static/chunks/*` のハッシュが新サーバーに存在せず黙って壊れる
(サイドバーが空・client-side exception 等、毎回「別のバグ」に化ける)。
加えて WebSocket には版ネゴシエーションが無く、版ずれ時に未知イベントを
双方が黙殺してリアルタイム更新が無言で止まる(#1356)。両者は
「版不一致検知+リロード促し」という同一機構で解決する。

- realtime/types.ts: 共有定数 CLIENT_VERSION_MESSAGE_TYPE /
  VERSION_MISMATCH_EVENT_TYPE、VersionMismatchEvent 型、純粋関数
  isVersionMismatch(0.0.0・空などの未知版は誤検知しないよう mismatch 扱い
  しない)を追加。parseRealtimeEvent は既存の非エンベロープ経路で
  version_mismatch を素通しする。
- useWebSocket.ts: onopen でクライアントのバンドル版
  (NEXT_PUBLIC_APP_VERSION)を client_version hello として送信。再接続ごとに
  再送する(サーバー入替直後の再接続こそ検知が要る場面)。version-checker は
  fs/path を module scope で import するため client bundle に持ち込まず、
  焼き込み env を直接読む。
- ws-server.ts: handleClientVersion を追加。getServerVersion()(実行時
  package.json、#1359)と突き合わせ、不一致かつ既知版のときのみ
  version_mismatch を返す。OPEN でないソケットには送らない。
- VersionMismatchBanner(新規)を AppShell に配置。アプリ全体でマウントされる
  トップレベルに置き(#1337 の教訓に従い worktree 詳細内には載せない)、
  このイベントを検知トリガーに明示的なリロードボタン付きバナーを永続表示する。
  自動リロードはしない(入力中の内容を破壊しない)。閉じた版は再接続で
  再提示せず、より新しいサーバー版が現れたときのみ再表示する。

新エンドポイントは追加せず既存の update-check + WS 経路のみを使う。
version-checker.ts は読み取りのみ(#1359 の関数は不変)。

品質ゲート(いずれも exit code 0): npm run lint / npx tsc --noEmit /
npm run build / CI=true npm run test:unit(594 files / 10090 tests passed、
Unhandled Rejection なし)。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`.claude/skills/<name>`(Claude, cliTools undefined)と `.agents/skills/<name>`
(Codex, cliTools:['codex'])に同名スキルがあると、Claude Code 選択時に候補へ
出ない不具合を修正する。

原因: `deduplicateByName()` は `name + cliTools` をキーに両版を共存させる(#800)
のに対し、`mergeCommandGroups()` は `cmd.name` のみをキーにしていたため、マージ層で
配列後方の Codex 版が Claude 版を同名で上書きし、後段 `filterCommandsByCliTool(...,
'claude')` で Claude 版が除外され候補が空になっていた。

対策(案A): 重複排除キー生成 `keyOf`(name + 正規化 cliTools スコープ、undefined/
空は claude sentinel)を `command-merger.ts` に切り出し、`deduplicateByName()` と
`mergeCommandGroups()` の両方で共有(DRY)。循環 import を避けるため keyOf は
command-merger.ts 側に置き、slash-commands.ts から import する(既存の依存方向を維持)。
これにより Claude 版と Codex 版が共存し、SF-1(worktree コマンドが同一 CLI tool scope
の同名 standard を override)も従来どおり保たれる。

回帰テスト追加: mergeCommandGroups() で name 衝突する Claude 版と Codex 版が両方残る
こと、同一スコープでは worktree が standard を override すること(SF-1 非退行)、および
keyOf の正規化挙動を検証する。


Claude-Session: https://claude.ai/code/session_016KUNKsdsvJnP3bf7c89vU7

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
npx で起動したサーバは isGlobalInstall() が true を返す(Issue #1195 の
意図的挙動)ため、GUI の update-check / update route が npx を global と
誤認していた。結果「今すぐアップデート」ボタンが表示され、押下すると 202
started を返すが実体は no-op(spawn した commandmate update が #1319 の npx
ゲートで即終了)で、バナーは 5 分 timeout に陥っていた。CLI (#1319) の
ガードが GUI 経路に未反映だった。

- GET /api/app/update-check: isNpxExecution() を isGlobalInstall() より先に
  判定し installType: 'npx' を新設。updateCommand は 'global' ゲート済みで
  npx では null を維持。
- POST /api/app/update: lock/spawn 前に npx ゲートを追加し 400 code:'npx' で
  拒否。検知失敗時は fail-safe で既存 not_global ゲートにフォールバック。
- UpdateNotificationBanner: installType 'npx' で更新ボタンを出さず、正しい
  コマンド `npx commandmate@latest` の案内ブロックを表示。
- i18n: update.npxTitle / update.npxDescription を locales/{en,ja} に追加。
- InstallType union を 'global' | 'local' | 'npx' | 'unknown' に拡張
  (route / api-client / banner props)。

npx でのその場更新の実装は本 Issue のスコープ外(#1395 で別途対応)。


Claude-Session: https://claude.ai/code/session_01AqhdEu33W6t3ing8ksA6J3

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes #1395. 設計 Alt A(新npxキャッシュ取得→旧デーモン停止→新デーモン再起動)。#1198 セキュリティ不変条件維持。詳細は PR #1397 参照。
npx 起動サーバの GUI アップデート対応(#1394 fix + #1395 feat)をまとめた
minor リリース。#1394 で npx を正しく検知して GUI アップデートの誤動作を
解消し、#1395 で GUI ボタンからの npx その場更新を実装。

- package.json / package-lock.json: 0.10.4 -> 0.11.0
- CHANGELOG: [Unreleased] を [0.11.0] に確定(#1394 の Fixed エントリを追記)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqhdEu33W6t3ing8ksA6J3
ToastContainer が transform を持つ祖先 <aside data-testid="sidebar-container">
(AppShell) の subtree 内で描画されるため、position:fixed の containing block が
サイドバー枠となり、右下のはずのトーストがサイドバー基準に配置され左端が見切れて
いた。ToastContainer を createPortal(..., document.body) でラップして containing
block を viewport に戻す。SSR/hydration 対策として client-mount 後のみ portal を
描画する(CommandPalette / Modal と同じパターン)。


Claude-Session: https://claude.ai/code/session_01QRiYX98sodhZtCyHtbswoR

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
各コンポーネントがローカルに useToast() + <ToastContainer> を生成していたのを、
アプリ全体で共有する ToastProvider (React Context) へ集約。

- Toast.tsx: 状態機械を useToastState() に抽出し、ToastProvider が
  showToast/removeToast/clearToasts を Context 提供、単一の
  <ToastContainer>(#1399 の document.body への createPortal を踏襲)を1箇所だけ描画。
  useToast() は Provider 配下で共有インスタンス、非配下ではローカルへフォールバック。
- AppProviders: NextIntlClientProvider 直下に ToastProvider を配置(AppShell の transform 外)。
- Sidebar / CommandPalette / MarkdownEditor / LogViewer / NotificationsSettings /
  WorktreeDetailRefactored / WorktreeDetailDesktop / useWorktreeDetailController:
  ローカルの <ToastContainer> と toasts/removeToast の受け渡しを撤去し showToast のみ共有 Context 経由に。
- 種類・自動消滅・スタッキング・アニメーション等の既存挙動は不変。

grep -rn "ToastContainer" src/ の描画箇所は Provider 内の単一ホストのみ。
lint / tsc / test:unit (10169) すべてグリーン。


Claude-Session: https://claude.ai/code/session_01GtG2MtTqXXgKz28HAVx9K7

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
サイドバー同期トーストの見切れ修正 (#1399) と Toast のグローバル1本化 (#1400)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012QWFvhLNBEfP1yK86PWUAn
@Kewton
Kewton merged commit e74c4a0 into main Jul 18, 2026
18 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