コンテンツにスキップ

ADR-0002: 同一性は workspaceId と sessionId で表し、名前は表示用のラベルに落とす

  • 状態: Accepted
  • 日付: 2026-08-21
  • 関連: ADR-0001、ADR-0003(決定 5 の根拠 3 を差し替え)、docs/design.md §2.1 / §4.2 / §4.4 / §9.3 / §9.6、 bin/ccs の resolve_as_scratch / dir_is_empty / path_is_scratch_slot / registry_file_for_slug / pane_session_id_for / orphan_slots
  • 覆すもの: 無し。ADR-0001 の続きで、あちらが決めた「ディレクトリの同一性とセッションの 名前を分離する」を、どう表現するかまで具体化する。ADR-0001 の根拠にある 「空であること」という言い回しだけは、意味を変えずに精密化する(→ 決定 4)
  • 実装: 本 ADR では行わない。bin/ccs は触らない

文脈

セッションの名前は人が変える。 ccs で立てたあと、Remote Control 上で分かりやすい名前に 書き換えて長時間使う。ところが ccs は同一性を名前と枠番号から推論している。

いま、同一性はどこで推論されているか

# 箇所 何を同一性の根拠にしているか 名前が変わると
1 resolve_as_scratch cc/tmp-N という tmux があるか 稼働中の枠が「空き」に見え、横取りされる
2 resolve_as_scratch dir_is_empty(枠が空か) 中身の残った枠は二度と配られない(ADR-0001 の (c))
3 orphan_slots(ccs gc) 1 と 2 の組み合わせ 稼働中の枠を「孤児」として報告する
4 list_slugs tmux 名の cc/ 接頭辞以降 slug が変わる。cc/ を外されると一覧から消える
5 registry_file_for_slug レジストリの tmux を cc/<slug>: で前方一致 元の slug で引けなくなる
6 collect_session_row 同上 引けなければ status が stopped になる
7 pane_session_id_for pane_start_command の中の --session-id <uuid> — (名前ではなく起動コマンドに依存)
8 path_is_scratch_slot scratch root の直下 + 空 — (後述の印と正面から衝突する)
9 path_is_ccs_worktree worktree root 配下 + 解決時の変数 CCS_RESOLVED_WORKTREE_REPO — (実パスで指すと判定できない)
10 resolve_as_worktree パスの組み立て <root>/<repo>/<branch> だけ — (ディレクトリ側に印が無い)
11 cmd_new の冪等性 同一 slug の tmux があるか 同じ作業ツリーに 2 本目の claude が立つ

5 と 6 には未検証が残る。 レジストリの tmux フィールドが tmux 側の改名に追従するのか、 claude の起動時に書かれたきりなのかを確かめていない(手元の全セッションで両者は一致していたが、 枠 4/6/8 は改名の後に手で立て直されているので、どちらの説明でも同じ観測になる)。 どちらであっても「名前で照合するのは当てにならない」という結論は変わらないので、この ADR の 判断には効かない。確かめるなら tmux rename-session の前後でレジストリの tmux を読む。

11 は枠の話ではない。ccs new x01 で立てたセッションを cc/x01-refactor に改名すると、 次の ccs new x01 は「無い」と判断して同じリポジトリにもう 1 本立てる。冪等性 (design.md §4.3)が名前の付け替えで崩れる。

実測: 名前は実際に変わっている(2026-08-21、手元の 18 セッション)

tmux セッション名 claude の name formerNames cwd
cc/equipment equipment — ~/.cc-scratch/4
cc/wifi-diag wifi-diag — ~/.cc-scratch/6
cc/contacts contacts — ~/.cc-scratch/8
cc/tmp-2 [ccs] 朝会夕会 tmp-2 ~/.cc-scratch/2
cc/cost-management [ccs] コスト分析改善 [ccs] cost-management, cost-management ~/ghq/…/cost-management
cc/ccs [ccs] ccb [ccs] ccd, ccs ~/ghq/…/ccs

tmux の名前と claude の名前は、別々に、何度でも変わる。 前者が変わったのが枠 4/6/8、 後者が変わったのが tmp-2 と cost-management と ccs。ccs は前者を同一性に使い、 人は後者を「分かりやすい名前」として使っている。

枠 4/6/8 は resolve_as_scratch の占有判定(#1)から外れる。ccs new --tmp に横取り されるのを防ぐため、人が手でダミーファイルを置いた(ADR-0001 の運用メモ)。その中身は こうだった。

# 枠 4: 備品管理(Notion 📒 DB_備品管理台帳)

tmux: `cc/equipment` / 会話: 45299e77-9a36-4c18-8e99-fe17915ae5d9

戻すには: `cd ~/.cc-scratch/4 && claude --resume 45299e77-9a36-4c18-8e99-fe17915ae5d9`

単に非空にするためのダミーではない。 置かれていたのは「このディレクトリは何で、どの 会話のもので、どう戻すか」という印そのものだった。運用が自然発生的にこの形を選んでいる。

逆方向(ディレクトリ → 素性)が引けない

~/.cc-scratch/6 を見ても、それが ccs の発行したものか、人が手で作ったものか、いつ誰が 発行したのかが分からない。ccs gc が「中身の残った枠」として報告できるのはパスだけで、 そこから先は人が中を読むしかない。ADR-0001 で枠が ~/.cc-scratch/<一意な名前> になると、 数字ですらなくなるので、この問題は今より効いてくる。

sessionId は改名にも再起動にも耐える(実測)

  • 上の表のとおり、name が 2 回変わっても sessionId は同じままで、旧名は formerNames に残る
  • 枠 4/6/8 の pane_start_command は claude -n <名前> --resume <uuid> だった。手で 立て直されている。 それでもレジストリの sessionId は --resume に渡した uuid と一致する
  • ccs を claude セッションの中から呼ぶと、環境に CLAUDE_CODE_SESSION_ID があり、 呼び出し元のレジストリの sessionId と一致する(このセッションで確認: 47f73f31-76cd-4ca3-9cd1-e76112500b51)

再現:

tmux list-panes -t cc/equipment -F '#{pane_start_command}'
jq -r '[.sessionId,.name,(.formerNames//[]|map(.name)|join(",")),.cwd]|@tsv' ~/.claude/sessions/*.json
printf '%s\n' "$CLAUDE_CODE_SESSION_ID"   # claude の中から実行したとき

同時に、sessionId は「場所の同一性」ではない。 枠 4/6/8 は一度死んで別プロセスとして 立ち直っている。手で claude(--resume なし)を打てば、同じディレクトリに別の sessionId が生まれる。1 つの作業ディレクトリに対し、会話は時間軸上で複数あり得る。

印を置くと「空であること」と衝突する

ADR-0001 は自動承認の根拠をこう書いた。

枠は ccs 自身が作ったディレクトリで、渡すのは空だと確かめたときだけ。つまり信頼確認が 守ろうとしている「知らないコード」がそこには存在せず、確認は情報を増やさない。

path_is_scratch_slot() は実際に dir_is_empty を見ている。印を置いた瞬間に、その ディレクトリは自動承認されなくなる。 印と空判定の関係を決めない限り、この ADR は ADR-0001 と正面から衝突する。

決定

1. 同一性は 2 層。workspaceId(場所)と sessionId(会話)に分ける

層 発行者 変わるか 何を指すか
workspaceId ccs 発行から削除まで不変 その作業ディレクトリ
sessionId Claude 会話ごとに不変(改名・--resume に耐える) その会話
slug / tmux 名 / claude の name ccs が初期値を置き、人が変える いつでも 表示用のラベル

名前は同一性の根拠にしない。 どの判定も、名前ではなく上の 2 つ(または作業ディレクトリの 実パス)で行う。

sessionId だけを同一性の正にはしない。1 つの作業ディレクトリに会話は複数ありうる (実測: 枠 4/6/8 は手で立て直されており、--resume を使わなければ別 uuid になっていた)。 会話 id を場所の id に使うと、2 本目の会話が始まった瞬間に嘘になる。

workspaceId は ccs が発行する不透明な識別子で、ディレクトリ名そのものに使う (~/.cc-scratch/<workspaceId>)。長さは小文字 16 進 8 桁とする。衝突は mkdir の失敗で 検出して引き直す。中身を解釈してはならない — 意味を持たせると、また名前から推論する 道が開く。

2. 発行したディレクトリには、ccs が印を刻む

ccs が作った使い捨て作業ディレクトリの直下に .ccs.json を 1 つ置く。

{
  "schema": 1,
  "workspaceId": "3f9a2c1b",
  "kind": "scratch",
  "issuedAt": "2026-08-21T09:12:34Z",
  "issuedBy": {
    "ccs": "0.3.0",
    "host": "asakusa-t",
    "sessionId": "47f73f31-76cd-4ca3-9cd1-e76112500b51"
  },
  "issuedSlug": "tmp-3f9a2c1b",
  "issuedSessionId": "c56885d3-6808-4eb1-bd7b-5d39817ac670"
}

印には不変な事実だけを書く。 現在の名前・現在の sessionId・状態(running / idle)は 書かない。書けば更新が要り、更新が要るものは必ず腐る — それがいま名前で起きていること そのものである。issuedSessionId は「発行時にこの uuid で起動した」という履歴であって、 「いまこの会話のものだ」ではない。

  • issuedBy.sessionId は、ccs を呼んだのが claude セッションのときだけ入る (CLAUDE_CODE_SESSION_ID。実測で取れることを確認済み)。人が端末から打ったときは null
  • 形式は JSON。jq は既に必須依存なので、依存は増えない

3. 照合は名前ではなく作業ディレクトリで行う

占有も冪等性も、そのディレクトリを cwd にしている生きたセッションがあるかで判定する。

  • レジストリの cwd を使う(live_cwd_for が既に読んでいる)。registry_is_live で pid の生存も見る(ADR-0001 決定 3 の具体化)
  • 比較は abs_dir を通した実パスどうしで行う。/tmp → /private/tmp の正規化を 忘れると当たらない(design.md §6 と C1 で 2 度踏んでいる)
  • 削除・再発行の可否を決めるときは、cwd がそのディレクトリ配下にある場合も占有と見なす (安全側に倒す)
  • 冪等性(#11)も同じ判定に移す。同じ作業ツリーに 2 本目を立てないことが目的であって、 同じ名前を返すことが目的ではない。既存が見つかったら、改名後の tmux 名をそのまま返す

4. 自動承認の条件を「空」から「ccs が作り、印以外に何も無い」に置き換える

path_is_scratch_slot() の判定を次に変える。

  1. scratch root の直下である
  2. .ccs.json が読めて、kind が scratch で、workspaceId がディレクトリ名と一致する
  3. その印以外に何も無い

これは ADR-0001 の決定を覆さない。ADR-0001 が守ろうとしたのは「信頼確認が守るべき 『知らないコード』がそこに無い」ことで、「空であること」はその代理指標だった。 ccs 自身が書いた印は「知らないコード」ではないので、代理指標を直接の条件に置き換える。

判定は今より厳しくなる。 2 が加わるので、scratch root の直下に人が手で作った空 ディレクトリは自動承認されなくなる。安全側への変化なので受け入れる。

書く順番は「mkdir → 印を書く → ensure_trust → 起動」。順番で辻褄を合わせる案は採らない (→ 捨てた案)。

5. worktree と ghq 配下には印を置かない

  • git の作業ツリーを汚さない。 .ccs.json は git status に出て、コミットされ得て、 clean 判定を壊す
  • worktree は git 自身が素性を持っている。 git rev-parse --git-common-dir で元の リポジトリが引け、git worktree list で一覧が引ける。~~パスも <CCS_WORKTREE_ROOT>/<repo-slug>/<branch-slug> で素性を語る(design.md §9.6)~~
  • したがって worktree に必要なのは決定 3(cwd で照合)だけで、印は要らない

根拠 3 を差し替えた(2026-08-26、ADR-0003)。結論は変わらない。 worktree の置き場所が <repo>/.worktrees/<branch-slug> になり、CCS_WORKTREE_ROOT は 消えるので、「パスが素性を語る」は成立しなくなる。代わりに素性は git rev-parse から 直接引く(ADR-0003 決定 3)── 規約を読むのをやめて実体に訊くので、根拠 1 と 2 は むしろ強くなる。印を置かないという結論はそのまま維持する。

path_is_ccs_worktree は今のまま(元リポジトリが ghq 配下なら信頼)。印の有無は関係しない。

6. ccs は自前のレジストリを持たない

「このディレクトリはどのセッションのものか」は、Claude のレジストリが cwd として既に 持っている(実測で全セッションに入っている)。ccs 側に索引ファイルを作ると、 ディレクトリと索引の 2 箇所を同期する必要が生まれ、必ずずれる。design.md §2.1 の委譲方針を 維持する。

印は「状態」ではないので、この方針と衝突しない。 design.md §9.3 が devas.life 版の @claude_state を退けたのは「状態を hook で書く」ことに対してで、そこで置いてよいとしたのは 「ccs 自身が作った出自情報」だった。印はまさに後者で、しかもセッションではなく ディレクトリと同時に消えるので、後片付けの対象が増えない。

追記(2026-09-02、R6 / #97): ccs restore --pick が「選ばれなかった会話」を CCS_DISMISSED_FILE に記録する。 これもこの決定に反しない。ここが禁じたのは存在を二重に持つこと ── レジストリが 既に持っている事実を写して、ずれることだった。記録が持つのは他のどこにも無い人の判断 (この会話はもう終わっている)で、会話ログにもレジストリにも印にも書かれていない。 写しではないので、ずれる先が無い。

代わりに、写しではないものが残り続ける問題は引き受ける。だから (a) 書き込みのたびに、会話ログが消えた行を落とす、(b) 戻したら記録を消す、 (c) 消せば全部やり直せる(rm してよい)。

7. 孤児の扱い

「印はあるが、そのディレクトリを cwd にする生きたセッションが無い」状態を孤児と呼ぶ。

ディレクトリの状態 扱い
印あり + 印以外に何も無い + 生きたセッション無し ccs gc -y が消す
印あり + 中身あり + 生きたセッション無し 報告だけ。消さない(ADR-0001 の「利用者のファイル」の原則を維持)。報告には issuedAt / issuedSlug / issuedSessionId を添える
印あり + 生きたセッションあり 触らない
印が無い / 壊れている / workspaceId がディレクトリ名と食い違う + 空 ccs のものと見なさないが、空なら消す(2026-09-05 に改定。下記)
印が無い / 壊れている / workspaceId がディレクトリ名と食い違う + 中身あり 触らない。「素性の分からないディレクトリ」として報告する

改定(2026-09-05、#91): 元の表は決定 8 と食い違っていた。 ここは「印が無いものは触らない」と書き、決定 8 は レガシー枠({1..8})について「今までどおり扱う(空なら rmdir)」と書いていた。 実装は安全側(この表)を採っていたが、その結果レガシー枠は空になっても永久に残る ── I2b 以降は番号の枠を発行しないので、畳んだあとは誰も触らない。

決定 8 側へ倒した。 判断は「ゴミが溜まり続けるほうが害が大きい」(#91、Ken)。

代償は明示して受け入れる: 人が手で作った空ディレクトリも消える。 ただし消えるのは空のときだけなので、失うのは「そこに何かを置くつもりだった」という 意図だけで、置いたものは失わない。

名前で素性を推定する案(^[0-9]+$ だけ消す)は採らない。 この ADR が一貫して 否定してきた形(名前を素性の根拠にしない)に戻ってしまうため。

印を失っても安全側に倒れる。 消えたら「ccs のものではない」と見えるだけで、勝手に 消されることも、勝手に信頼されることもない(決定 4 の条件 2 が効く)。

補助として、~/.claude/projects/<エンコードした cwd>/ にディレクトリ名が残る (実測: ~/.cc-scratch/2 → -Users-apple--cc-scratch-2)。印を手で消されても、 transcript 側から workspaceId を逆引きできる。

8. 既存の ~/.cc-scratch/{1..8} には、遡って印を打たない

  • 発行時刻も発行元も分からない。 印に書けば捏造になる。素性が分からないこと自体が 記録すべき事実なので、印の無いディレクトリとして扱う(決定 7 の最終行)
  • 数字名のディレクトリは「レガシー枠」として扱う。新規発行はしない (resolve_as_scratch は印つきの新しいディレクトリしか作らず、返さない)
  • 移動しない。 ~/.cc-scratch/legacy/4 のような退避は、そこを cwd にしている稼働中の セッションを壊す(枠 4/6/8 は今も動いている)
  • レガシー枠の生死は決定 3 で見る。これだけで枠 4/6/8 の誤判定は消える
  • ccs gc はレガシー枠を今までどおり扱う(空なら rmdir、非空なら報告だけ)。人が 片付ければ自然に消える ── 決定 7 と食い違っていたが、2026-09-05 にこちら側へ揃えた(#91)。 ただし「レガシー枠だから」ではなく「印が無くて空だから」消す ── 名前で素性を推定しないため、結果として人が手で作った空ディレクトリも消える

ダミーファイル(~/.cc-scratch/{4,8}/SESSION.md)は、決定 3 が実装に入るまで消さない。 先に消すと、いまの名前ベースの判定では枠が空と見えて横取りされる。

根拠

なぜディレクトリの中に印を置くのか(索引ファイルではなく)

3 つある。

  1. 寿命が一致する。 印はディレクトリと同時に生まれ、同時に消える。索引は消えた ディレクトリの残骸を持ち続け、手で作られたディレクトリを知らないままになる。 孤児を掃除する仕組みが、孤児を作る仕組みになる
  2. 持ち運べる。 ディレクトリを見れば素性が分かる、が要求そのもの。索引は「どの索引を 見ればいいか」を先に知っている人にしか答えない
  3. 運用が既にそれを選んでいた。 人が手で置いた SESSION.md は、まさに印だった。 ccs がやるべきなのは、それを機械可読にして、置き忘れをなくすことである

なぜ workspaceId をディレクトリ名に使うのか

ディレクトリ名に意味のある文字列(equipment など)を置くと、名前から推論したくなる。 それがこの ADR で潰している問題そのものなので、名前は不透明にして「読んでも何も分からない」 状態にする。分かりやすい名前が欲しい人は、いままでどおり tmux と claude の名前を変える。

8 桁にするのは、パスが ~/.claude/projects/ のエンコード名や transcript のパスにも現れる ため。uuid の 36 文字は長すぎ、衝突は mkdir で確実に検出できる。

なぜ「空」ではなく「印以外に何も無い」なのか

ADR-0001 の根拠を読み直すと、条件は 2 つ書かれている。

枠は ccs 自身が作ったディレクトリで、渡すのは空だと確かめたときだけ

守っているのは前半(ccs が作ったこと)で、後半(空)はそれを外から確かめる手段だった。 印は前半を直接確かめられるようにする。 代理指標を捨てて直接の条件を見るので、 論拠はむしろ強くなる。

捨てた案

案 捨てた理由
sessionId を唯一の同一性にする(当初案) 1 ディレクトリに会話は複数ありうる(実測: 枠 4/6/8 は手で立て直されている)。会話 id を場所の id に使うと、2 本目の会話が始まった時点で嘘になる。発行時の sessionId は印に履歴として残す
ccs 側の索引ファイルだけで完結させる(~/.cc-scratch/index.json など) ディレクトリと索引の同期が要り、必ずずれる。孤児の掃除役が孤児を作る。design.md §2.1 の委譲方針にも反する。ただし将来の併用は否定しない(→ 未決)
tmux のユーザオプションに持つ(§9.3 のラベルと同じ場所) セッションと同時に消えるのが利点だが、孤児の素性を答えるという要求がまさに「セッションが死んだ後」の話なので、根本的に足りない。生きている間の補助としては有効(→ 未決)
印を起動の後に書き、空判定の順番で辻褄を合わせる 順番に依存した暗黙の不変条件になる。誰かが順番を変えた瞬間に壊れ、しかも壊れ方が「勝手に信頼される」なので危ない。条件そのものを直す
worktree にも .ccs.json を置く git の作業ツリーが汚れる(git status に出る・コミットされ得る・clean 判定が壊れる)。worktree の素性は git が既に持っている
名前の付け替えを禁じる / ccs が名前を書き戻す 改名は分かりやすさのために行われていて、それ自体は正しい運用。道具が人の運用を制約する側に回っているのが今の問題なので、逆向きに直す
既存の 8 枠に遡って印を打つ 発行時刻も発行元も分からず、書けば捏造になる。素性が不明であること自体が事実

影響

コード(別 PR。本 ADR では触らない)

  • resolve_as_scratch — workspaceId を発行し、ディレクトリを作り、印を書く。空き枠探索は消える
  • path_is_scratch_slot — 決定 4 の 3 条件に置き換える。名前は path_is_fresh_scratch などへ
  • dir_is_empty — 占有の推論に使うのをやめ、「印以外に何も無いか」を見る関数に寄せる
  • cwd で照合する関数を足す(live_session_for_dir 相当)。cmd_new の冪等性判定(#11)、 resolve_as_scratch の占有判定(#1)、orphan_slots(#3)がこれに乗り換える
  • orphan_slots — 枠番号のループをやめ、scratch root を走査して決定 7 の 4 分類で返す
  • cmd_gc — 分類が 3 種から 4 種に増える(印なしの「素性不明」が加わる)。報告に issuedAt / issuedSlug / issuedSessionId を出す
  • ls --json — workspaceId を足す候補(ROADMAP の C3 と同じ枠)。既存フィールドは変えない
  • pane_session_id_for(#7)— 手で --resume した場合に uuid を拾えない問題はこの ADR では 直さない。印の issuedSessionId があれば「発行時の会話」は引けるが、手で別の会話に 差し替えられた場合は追えない。決定 3 とは独立の話
  • コメントに ADR-0002 を参照させる(README の規律)

ドキュメント

いま直すもの(古い理由が残ると、次に読む人が同じ前提から議論を始めるため)。

  • docs/adr/README.md の一覧表に ADR-0002 を足す
  • mkdocs.yml の nav に ADR-0002 を足す
  • docs/design.md §4.2 — 「新しい名前の形は実装時に決める」と書いてある箇所から、 同一性の表現は ADR-0002 で決まったことへ導線を張る
  • docs/design.md §4.4 — 自動承認の条件が「空」から変わることを注記し、ADR-0002 へ送る
  • docs/adr/0001-scratch-workspace-identity.md — 関連に ADR-0002 を足す(決定は覆っていない ので状態は Accepted のまま)

実装が変わってから直すもの。 先に書き換えると、動くものと文書が食い違う。

  • docs/tutorial.md / docs/hands-on.md / docs/why.md / docs/agent-tools.md の tmp-N
  • bin/ccs の usage() と docs/img/help.svg
  • docs/configuration.md の CCS_SCRATCH_SLOTS の説明

運用

  • ~/.cc-scratch/{4,8}/SESSION.md は、決定 3 が実装に入るまで消さない(先に消すと横取りされる)
  • 実装後、印のあるディレクトリは cat ~/.cc-scratch/<id>/.ccs.json で素性が引ける
  • レガシー枠(1〜8)は人が片付けるまで残る。ccs は空のものだけ rmdir する

未決

以下は本 ADR では決めない。実装時に決めて、必要なら別 ADR にする。

  • 印のファイル名。.ccs.json の 1 ファイルか、.ccs/workspace.json のディレクトリか (後者は将来の追加物を入れられるが、いま入れるものが無い)
  • ccs が自分の素性を答えるコマンド(ccs whoami / ccs id)を足すか。セッションの中から 「自分はどの workspace か」を引けると便利だが、cat .ccs.json で足りる可能性がある
  • 生きている間の補助として tmux のユーザオプション(@ccs_workspace_id)にも持つか。 C2 のラベルと同じ場所だが役割が違う(あちらは ccb 用の不透明な文字列)
  • hub home(~/.cc-hub)に印を打つか。 場所が固定で 1 つしか無いので不要と考えているが、 害も無い
  • worktree の印を .git/worktrees/<name>/ に置くか。 git の管理領域なので git status に 出ず、git worktree remove で一緒に消える。決定 5 を保ったまま素性を持たせられるが、 印の置き場所が 2 種類になる
  • CCS_SCRATCH_SLOTS の去就(ADR-0001 から持ち越し)。同時に立てられる本数の上限として 残すか、廃止するか
  • workspaceId の衝突時の再試行回数と、scratch root が別マシンと共有されていた場合の扱い (印の issuedBy.host を見て拒むか、無視するか)