版と更新¶
ccs が「自分はどの版か」をどう名乗り、どう配布・更新されるか。
なぜ導出するのか — 手書きの番号は実際に腐った¶
CCS_VERSION は長らく bin/ccs に手で書かれていた。履歴はこうなっている。
| コミット | 版 |
|---|---|
| 骨格を置いた | 0.1.0 |
| hub と設定層 | 0.2.0 |
| ADR-0003 を書いた(docs だけの PR) | 0.0.1 ← 後退している |
ccs gc の 4 分類 |
0.0.3 |
restore --last ほか 3 つの機能 PR |
0.0.3 のまま |
2 つ壊れている。番号が後退したこと(docs だけの PR で、誰も気づかなかった)と、 機能が入っても動かなかったこと。
後者の実害が 2026-08-29 に出た。PATH の ccs が古く ccs restore --last が
「知らないオプション」で落ちたが、ccs version は古い方も新しい方も
0.0.3 と答えた。
「前」と「後」を区別できない番号は、版として機能していない。 だから ccs の版は、人が打つ数字ではなく コミットから導出する。
何を名乗るか¶
解決の順は 3 段。上から順に、最初に答えられたものを使う。
| 段 | いつ効くか | 例 |
|---|---|---|
1. 焼き込み (CCS_BUILD) |
インストールされた実体。install が埋める | 0.0.3+3fed500 |
| 2. git から導出 | repo / worktree の bin/ccs を直に叩いたとき |
0.0.3+3fed500 (dev) |
| 3. どちらも無い | git の外に置かれ、焼き込みも無い | 0.0.3+unknown |
(dev)が付いていたら、それはインストールされた実体ではない。 手元の worktree を直に叩いている。一致しないものを見比べているとき、 真っ先に知りたいのがこれ。unknownは「分からない」と言っているだけで、嘘はついていない。 黙って錨の番号だけを答えると、また「前と後が同じ」に戻る。
tag を打つとどうなるか¶
tag があればそちらが勝つ。
| 状態 | ccs version |
--short |
|---|---|---|
| tag が無い | 0.0.3+3fed500 |
0.0.3 |
v1.2.3 ちょうど |
1.2.3 |
1.2.3 |
v1.2.3 から 3 コミット先 |
1.2.3-3-g3fed500 |
1.2.3 |
| 追跡されている変更がある | 末尾に -dirty |
同上 |
CCS_VERSION(bin/ccs の中の 0.0.3)は tag がまだ 1 つも無いときの錨。
tag を打ち始めれば出番は無くなる。
種別は綴りから当てていない
短い sha は数字で始まることがある(実測: 3f5673a)ので、
「数字で始まれば tag 由来」は誤判定する。--always を付けずに
git describe させ、成否そのもので分けている ── tag が 1 つも
無ければ describe は失敗するので、そこで sha に落とす。
--short は以前の契約そのまま¶
以前の ccs version は 0.0.3 のように X.Y.Z だけを返していた。
その形は --short に移した(機械可読の入口を消さないため)。
既定の出力は情報を足したので 1 行だが X.Y.Z ではない。
CHANGELOG は作らない¶
ROADMAP の完了ログが 既に日付つきの変更記録になっている。CHANGELOG を足すと同じことを 2 箇所に書くことになり、 片方が必ず腐る。GitHub Release を出すときは、Conventional Commits を守っているので auto-generated notes で足りる。
検査 — 版が嘘をつかないことを固定する¶
make lint が 2 つ見張る。
- コミットされた
bin/ccsのCCS_BUILDは必ず空。 焼き込み済みのファイルをコミットすると、そのファイルはどのコミットに 置かれても同じ版を名乗り続ける ── 手書きの番号が0.0.3で止まったのと まったく同じ壊れ方を、自動化した経路で再現することになる。 CCS_VERSIONはX.Y.Zの形。--shortの対外的な契約がここから出る。
test/unit/version.bats が 14 件で挙動を固定する。tag・dirty・symlink 経由・
git の外、といった状態は使い捨ての git リポジトリを作って試している
(本物のリポジトリに tag を打つのは副作用なので、テストからは触らない)。
配布と更新¶
なぜ ghq のチェックアウトを指してはいけないのか¶
以前の入れ方は symlink 1 本だった。
これだと 走るコードは「main checkout の HEAD がその瞬間に何であるか」そのものになる。
2026-08-29 に事故になった。PR は worktree のセッションから GitHub 上で self-merge される
── main checkout には何も起きない。誰かが手で git pull するまで、PATH の ccs は
取り残される。5 コミット遅れていて、ccs restore --last を知らなかった。
「古い」よりも「可変」であることのほうが問題だった。
| 性質 | 何が起きるか |
|---|---|
| 可変 | main checkout で branch を切っただけで、インストール操作なしに PATH の ccs が別物になる |
| 非原子的 | git pull は bin/ccs をその場で書き換える。hub は 300 秒ごとにそれを起動している |
| 戻せない | 直前の既知の良品がどこにも残らない |
いまの形 — 版ごとのディレクトリと symlink¶
同じマシンの claude / cursor-agent と同じ形にした
(~/.local/bin/claude -> ~/.local/share/claude/versions/2.1.251)。
ccs は単一ファイルの自己完結スクリプトなので、この形にきれいに嵌まる。
~/.local/share/ccs/versions/0.0.3+3fed500 実行可能な単一ファイル
~/.local/share/ccs/meta/0.0.3+3fed500 commit / installed / source
~/.local/share/ccs/current いま指している版
~/.local/share/ccs/source install 元のチェックアウト
~/.local/share/ccs/bin/ccs-install インストーラの複製
~/.local/share/ccs/install.log 切り替えの記録(JSONL)
~/.local/bin/ccs -> ~/.local/share/ccs/versions/0.0.3+3fed500
得られるもの:
- 原子的 ── symlink は rename で差し替える。「消えている瞬間」が無く、 走っているプロセスは自分が開いたファイルを持ち続ける
- 不変 ── 入った実体を git は二度と触らない。worktree の切り替えも dirty も届かない
- 巻き戻せる ── 前の版がそこにある。symlink を 1 回戻すだけ
- launchd を変えなくてよい ── plist は
~/.local/bin/ccsを指しているので、 symlink を差し替えれば次の起動から新しい実体を掴む
インストーラは自分の複製を ~/.local/share/ccs/bin/ に置く
ghq のチェックアウトが壊れていても・消えていても・別ブランチでも巻き戻せるように。 巻き戻しがチェックアウトに依存していたら、「別 path で管理する」が徹底できない。
入れる¶
ccs 自身に upgrade サブコマンドは作らない。作ると ccs が GitHub とネットワークを
知ることになり、4 つの責務の 5 つ目になる。
ccs-install いまの作業ツリーを入れて切り替える
ccs-install --auto origin/main が新しければ入れて切り替える
ccs-install --check [--force] 新しい版があるか見る(切り替えない)
ccs-install --list 入っている版
ccs-install --switch <版> その版へ切り替える(巻き戻し)
ccs-install --where いま何がどこに入っているか
更新 — 作業ツリーに触らずに取り出す¶
--auto は git show origin/main:bin/ccs でオブジェクトから直接読む。
これが「ghq は実装用、動作は別 path」を成立させている核心。 pull も branch の
切り替えも要らないので、他のセッションが使っている作業ツリーに一切手を出さない
(fetch は ref を更新するだけで作業ツリーを触らない)。
手で make install したときは逆に、作業ツリーの bin/ccs を入れる ── 手元のビルドを
実機で試すための入口で、dirty なら版に -dirty が付く。並べて置かれるので、
試したあと 1 手で戻せる。
古いまま気づかないことを防ぐ¶
4 層。上ほど安く、下ほど確実。
| 層 | 何が起きるか |
|---|---|
| 1. 見える | ccs version がコミットまで答える。ccs doctor が PATH の指す先まで出す |
| 2. 検知 | インストーラが origin/main と突き合わせる(終了コード 10 / 11) |
| 3. 自分で鳴る | ccs hub up の鼓動に相乗りして、古ければ切り替える |
| 4. 発生源 | マージ後に make install(規律。漏れた分を 3 が拾う) |
3 が要点 ── 新しいデーモンは作らない¶
ccs hub up は launchd / systemd から 5 分ごとに冪等に走っている。
検知はそこに相乗りする ── 既に在るものが気づくだけで、監視の仕組みは増えていない
(design.md §2.1 の「自前のデーモンやポーリングを作らない」に反しない)。
ネットワークを使うのは CCS_UPDATE_INTERVAL(既定 1 時間)に 1 回だけ。
検知したら自動で切り替える。 無人で入れ替わる以上、次を満たしている。
- 原子的に差し替える(rename)。走っている hub は自分が開いたファイルを持ち続ける
- 切り替える前に、その版が
versionを答えられるか試す。 人が見ていないので、起動もできないものを掴ませると hub ごと止まる - 記録が残る ──
install.logに「いつ / どの版から どの版へ / きっかけ」、hub.logにupdated。無人で変わる以上、記録が無いと「なぜ挙動が変わったか」を追えない - 1 手で戻せる ── 前の版は残る。巻き戻し方は切り替えのたびに出る
- 止められる ──
CCS_AUTO_UPDATE=offにすると、検知して報告するだけになる
切り替えを実行しているのは、切り替えられる当人(古い版)
launchd が起動した古い ccs が、自分を指している symlink を差し替える。
動作は安全(symlink の差し替えは開かれているファイルに影響しない)だが、
記録に自分の版を書くと「何から何へ」を取り違える。記録はインストーラが
「切り替え後の版」で書く。
何を根拠に「新しい」と言っているか¶
origin/main の HEAD。 tag ではない(tag は節目にしか打たないので遅れる)。
CI が緑であることは条件にしていない。 理由は 2 つ。launchd の経路から gh の認証を
要求することになるのが 1 つ、そして main は ruleset で required check が付いているので
赤いものはそもそもマージできない ── origin/main は構造上すでに CI 緑になる。
確認できないときは、そう言う¶
検知は fetch できて初めて意味を持つ。オフラインで ref を読めても、それが今のリモートと
同じとは限らない。そこで「最新です」と言うと、見ていないのに大丈夫だと言うことになる。
hub up の側では騒がない ── 5 分ごとに「分からない」と言われても人にできることは無く、
肝心の変化が埋もれる。見せるのは人が訊いたとき。
install 元のチェックアウトが消えたら¶
「分からない」と言って、切り替えない。
$ ccs doctor
更新
状態: **確認できていません**
理由: install 元のチェックアウトがありません: ~/ghq/github.com/ken-ty/ccs(消えた/移動した?)
入れ直す: チェックアウトで make install
巻き戻しは、チェックアウトが無くてもできます。
代わりのリポジトリを勝手に探さない
実装中に一度踏んだ。控えが辿れないときに「インストーラ自身が居るリポジトリ」へ
落ちる作りにしていたので、install 元を消したあと ccs の作業ツリーから --check を
打つと、そちらの origin/main を「新しい版」として報告した。
検知の根拠が黙って入れ替わるのは、古さを見逃すより悪い。
いまは控えてある install 元だけを見て、消えていれば場所を名指しして止まる
(test/unit/install.bats が回帰を見張る)。
巻き戻しはチェックアウトが無くてもできる。 インストーラの複製も過去の版も
~/.local/share/ccs/ に残っているため ── 複製を置いているのはまさにこのためで、
チェックアウトを要るのは入れ直すほうだけ。
設定¶
| キー | 既定 | 何を変えるか |
|---|---|---|
CCS_INSTALL_ROOT |
~/.local/share/ccs |
版の置き場所 |
CCS_BIN_DIR |
~/.local/bin |
symlink を置く場所(PATH が通っているところ) |
CCS_AUTO_UPDATE |
on |
off にすると自動では切り替えず、検知と報告だけ |
CCS_UPDATE_INTERVAL |
3600 |
検知でネットワークを使う間隔(秒) |
CCS_KEEP_VERSIONS |
5 |
残す版の数(2 未満にはならない ── 巻き戻し先が消えては困る) |
テスト¶
test/unit/install.bats が 28 件、test/integration/hub-update.bats が 11 件。
上流(bare リポジトリ)と、そこから引いたチェックアウトをテストごとに作って回す。
とくに次の 3 つは、わざと壊して落ちることを確かめてある(空振りしていないことの確認):
--autoが作業ツリーに触らない(別ブランチ・未コミットのままでも)- 起動できない版に切り替えない(smoke test を殺すと落ちる)
CCS_AUTO_UPDATE=offが効く(分岐を殺すと落ちる)- install 元が消えたときに別のリポジトリへすり替わらない(fallback を戻すと落ちる)