Repository navigation
v0.34.1
[0.34.1] - 2026-09-12
Highlight: 並列オーケストレーション 1 回分(7 Issue)をまとめたパッチ。
/rebuildが本番サーバを落としたまま残す競合(#2488)と、unit テストが本番の opencode ポート台帳を書き換える汚染(#2490)という運用を直接壊していた 2 件を潰し、remote実行中に PC からログインできず併用できなかった制約に--auth remote-onlyという出口を用意した(#2489)。公開面(public-messaging / README / LP)は「orchestrate」を看板にした軸へ揃えた(#2493 / #2494 / #2495)。
Added
- feat(remote):
commandmate remote --auth <all|remote-only>を追加し、remote 実行中も PC ローカルからはログイン無しで使えるようにした (#2489): 既定のallは現行どおり全経路で認証する。remote-onlyは サーバに 127.0.0.1 の 2 本目の listener(CM_REMOTE_INGRESS_PORT)を立てて Provider をそちらだけに向け、ユーザ自身が使う既存ポートの listener だけを認証免除する。判定は「どの listener に着いたか」だけで行い、送信元 IP・Host・X-Forwarded-*では行わない — Tailscale Serve も Cloudflare Quick Tunnel も upstream がhttp://127.0.0.1:<port>なので、tunnel 経由のリクエストもreq.socket.remoteAddressは 127.0.0.1 で、loopback を免除すると公開 URL が丸ごと無認証になる(Hostは Provider が書き換える実測あり:docs/qa/1937-remote-uat-record.mdD-2)。server.tsは両 listener で内部ヘッダx-cm-ingressをクライアント値を捨てて上書きし、src/middleware.ts(Edge / HTTP)とsrc/lib/ws-server.ts(Node / WS upgrade)はその値だけを読む。免除の判定は全段 fail-closed(scope がremote-only以外・stamp がlocal以外・未 stamp・2 本目の listener が開けなかった場合はすべて認証する)で、CM_ALLOWED_IPSの IP 制限は免除に含めない。ガードは 2 重で、CLI は--authの不正値とCM_BINDが loopback 以外(.env適用後の実効値をloadEffectiveEnv()で読む)を exit 2 で拒否し、サーバ側も同条件を再検査してprocess.env.CM_AUTH_SCOPEをallに書き戻す(局所変数に留めると middleware/ws-server が毎リクエストremote-onlyを読み続ける)。remote statusはテキストにAuth scope:行(免除されるポートと認証されるポートの両方を名指す)、--jsonに.remote.authScopeと.remote.server.remoteIngressPortを出す。gracefulShutdownは両 listener を閉じ、process.exit(0)は両方が drain し切るまで待つ(従来server.close()だけだったため、local の接続が捌けた瞬間のprocess.exit(0)が Provider 経由で処理中のリクエストを切っていた。local だけが 3 秒の猶予を持つ非対称だった)。--auth allでは listener が 1 本なので挙動は不変。remote.jsonの新フィールドは optional なので #2489 以前に記録されたセッションも従来どおり読めて(allとして扱われ)、生きた tunnel が孤児にならない。macOS 実機の隔離サーバで実測(2026-09-12、dist/serverビルド / 私設 HOME・DB・tmux socket):remote-onlyで local listener は画面 200 / API 200 / WS 101、Provider listener は画面 307 /Authorizationつき API 401 / WS 401 で、x-cm-ingress: local+X-Real-IP+X-Forwarded-For+Hostを偽装しても 307 / 401 のまま、正しい token では 200 / 101。--auth all・scope 未設定・CM_BIND=0.0.0.0・ingress port 未設定 / 重複・scope の綴り違いはいずれも local listener が 307(=現行挙動)で 2 本目の listener を開かない。Provider 経由の実機(スマホ / Tailscale Serve)は未確認。
Changed
-
docs(readme): README en/ja を orchestrate 軸で再構成し、hero を orchestrate run の収録へ差し替えた (#2494):
docs/design/public-messaging.md(#2493 で改訂済み)の §1 / §1b / §2 / §3 / §4b / §11b から逐語でコピーして、README.mdとdocs/ja/README.mdを Problem → Product(run) → Proof → Philosophy → Details の 15 節構成へ組み替えた。H1 はFrom vibe coding to Vibe Engineering./「vibe coding から、Vibe Engineering へ。」を廃止せず §13 Philosophy 節の##見出しへ降ろし、hero は §1 の新 H1One agent leads. Gates decide what's done./「指揮するのは、いつもの Agent。判定するのは、ゲート。」+ lede 2 文 + 事実行に差し替えた。新設は## More agents, same you(#2493 §1b の 4 行+締め 1 文を逐語)/## One agent leads(run の 1 段落+cmate-task-contract§3.1 最小形の契約例version/title/goal/scope.allow/verify.gates+ Measured 表)/## Review by another agent(commandmate askとcmate-delegate、cross-model review はランナーの外だと明記)/## Know which one needs you(通知・スマホ・Conversation view を集約)/## What it does not do(#2493 §4b の 5 行を逐語)の 5 節で、Key Features は## What it gives youの下に One agent leads / Verified, not vibe-checked / Know which one needs you / Method as a system / Also の 5 群(###)へ並べ替え、既存 13 行の文は 1 文字も変えずに新規 2 行(cmate-orchestrate Skill= plan → dispatch → merge → uat・--approveなしに何も書き換わらない、Delegation between sessions=ask/ask --async+relays/peers/cmate-delegate)だけを足した。### Supported agentsは##へ昇格し、## Use casesは #2493 §11b の実測範囲に合わせて 5 行を入れ替えた。Measured 表の 3 行は収録記録と一致する(workspace/market/posts/は .gitignore 対象なのでコミットされないが、05qa.md: 送信 15:43:54 → 報告 8 分 39 秒・waves [[7,8,9],[10]]・PR #13〜#16 merged・uat-reportverdicts.go = 4、06brief.md/qa.md: 17:25:25 送信 → 17:33:11 報告の 7 分 46 秒・作業者 4 体とも Command Code・PR #25〜#28・integration_verify.outcome: pass・UAT go 4/4・収録後に人が develop 8849f29 を pull して npm test 47/47、04brief.md: 司令塔 Command Code / Codex 実装 / Claude Code テスト / Antigravity レビューで REJECT 344s → 修正 → APPROVE 457s →verify --gates unitで RESULT passed)。表の下にas observedを明記した。Issue 本文からの意図的な逸脱は 1 つだけ: Measured 3 行目の見出しを1 issue, review loopではなく1 issue, with a review stepにした —loopは #2493 §11b の「書かない表現」かつ本 Issue の受入条件でもあるため。ただし §4b 1 行目のNothing loops foreverは §11b がその禁止の理由として引いている文なので逐語で残してあり、README 中のloopはこの 1 箇所だけである(grep -in loop README.md= 1 行、ja は 0 行)。hero メディアは Issue の推奨 A を採った: 06 のclip.mp4(1,007,180 バイト / 1080² / 15 秒)を.claude/skills/video-to-gif/scripts/to-gif.sh --width 560 --max-bytes 1400000で GIF 化してdocs/images/demo-hero.en.gif=demo-hero.ja.gif(1,369,261 バイト、560² / 10fps / 256 色、wc -c実測で予算 1.5MB 内。06 の収録は英語版のみなので 2 パスは byte-identical = git blob 1 つ)へ置き、現行の UX 30 秒 GIF はgit mvでdocs/images/demo-ux.{en,ja}.gif(1,406,883 / 1,552,144 バイト、再エンコードなし)へ改名して §8 へ移した。§8 にはさらに 07 のclip.mp4からdocs/images/demo-sessions.gif(1,131,939 バイト、560² / 8fps)を起こした。altは 3 本とも映っているコマだけから書いた(GIF から抜いたフレームのコンタクトシートを目視して確認。07 の「4/4 unit gate」は映像に無いので alt に書いていない)。Security の 1 文は en / ja ともClaude CLI's own API calls→the agent CLI's own API calls/「Claude CLI 自体の」→「エージェント CLI 自体の」に直し、How it works の mermaid にL["Lead session\n(any agent)"] -->|"send --contract / ask / wait --verify"| Bを 1 本足した。Supported agentsのrun the same loop against a model you host yourselfは禁止語loopを含むためthe same worktree session, contract and gates, against a model you host yourselfに置き換えた(ja も同様)。ガード(tests/unit/docs/public-messaging.test.ts)はHERO_H1_EN/JAを新 H1 に差し替え、旧 H1 をAXIS_EN/JAとして追加し、concept ja/en はAXIS_*のみ(思想の正本なので pitch は要求しない)、README はHERO_H1_*とAXIS_*の両方を要求するよう付け替えた。public-messaging.md側の照合も hero / axis / 定義文の 6 本へ広げてある。変異注入で実効性を確認済み: README の hero を別文へ差し替えるとmust open with the en hero verbatimが 1 件赤、§13 の見出しを別語へ差し替えるとmust name the axis verbatimが 1 件赤、対照は 23/23 緑。##の数は en / ja とも 11 → 18 で揃っている。npm run test:unit1545 ファイル 27,827 件、npm run test:integration115 ファイル 1,509 件(skip 2)、lint /tsc --noEmit/ build:cli / build:server / build すべて緑。別 Issue が要る積み残し:docs/features/product-highlights.md:226-227とdocs/en/features/product-highlights.md:237-238がdemo-hero.en.gif(1,406,883 バイト)/demo-hero.ja.gif(1,552,144 バイト)という旧ファイルのバイト数を地の文で持っており、この改名で古くなったが、どちらも本 Issue の scope 外(画像参照ではなく散文なのでリンク切れは無い)。#2493 §12 の脚注が指摘しているとおり product-highlights を縛るテストは 1 つも無いので、追随用の Issue を別に立てること。 -
docs(website): LP を orchestrate 軸へ改訂(h1 / title / og、Problem 節、See it running を先頭へ、4 カードの付け替え) (#2495):
website/index.htmlの H1 をFrom vibe coding to Vibe Engineering.からOne agent leads. Gates decide what's done.へ差し替え、lede ・<title>・og:title・description/og:descriptionを docs/design/public-messaging.md §1(#2493 改訂版)から逐語でコピーした。hero の直下に Problem 節「More agents, same you」(§1b)を新設し、See it runningを The loop より上へ移動してその先頭に Command Code を lead にした実ラン 1 本(website/assets/media/orchestrate-run.mp4、1080x1080 / 15 秒 / 0.96MB、キャプションOne message to Command Code. Four issues, four workers, 4/4 gates, then UAT.)を置き、既存デモ 4 本は §5 の対応表どおりに並べ替えた。4 カードは §3 の順と題(One agent leads / Verified, not vibe-checked / Know which one needs you / Method as a system)へ、With / Withoutの直後に §4b の 5 行を並べたWhat it does not do節を、footer の上にFrom vibe coding to Vibe Engineering.を見出しにした Philosophy 節(定義文 ・ 一節 ・ Willison 脚注)を新設した。定義文のdef:en逐語一致と Willison 脚注は hero から Philosophy 節へ移っただけで、文字列は変えていない。tests/unit/website/landing-page.test.tsは期待値を public-messaging.md から読む形に変え(§1 hero 行 ・ §3 カード表 ・ §5 キャプション表 ・ §11b「言えないこと」)、#2493 のような文言差し替えがテストを素通りしないようにした(hero 予算HERO_BUDGET_BYTES=100KB は据え置き)。README(#2494)・ チュートリアル ・docs/concept.mdは対象外。 -
docs(design): 公開面の単一ソース
public-messaging.mdを orchestrate 軸へ改訂し、禁止語にcontrol layerを追加 (#2493): hero の H1 をFrom vibe coding to Vibe Engineering.からOne agent leads. Gates decide what's done.(ja:指揮するのは、いつもの Agent。判定するのは、ゲート。)へ差し替え、lede を「lead エージェントに他を走らせる仕組みを渡す — タスクごとに worktree 1 つと契約 1 つ・完了を決めるゲート・毎ランの記録」+ 8 ツール列挙 +You approve; you don't relay.の 1 セルに置き換え、hero 直下に事実行Open source (MIT) · runs on your machine · macOS / Linux / Windows (WSL2) · no app to install(比較ではなく事実として書く旨の注記つき)を新設した。旧 H1 は廃止していない — §2 を「思想の名と定義文」に広げ、旧 H1 を en / ja の表で「思想の名(README / LP では Philosophy 節の##見出し)」として降ろした。これはtests/unit/docs/public-messaging.test.tsのHERO_H1_EN/JAが文書に対するtoContainである性質を利用した段取りで、定数そのものの差し替え(HERO_H1_*→ 新 H1、旧 H1 をAXIS_*化)は README を同じ PR で直せる #2494 へ後送りした(本 PR 単独では README 側のピンopens with the hero…が赤になるため)。新設は 3 節: §1bMore agents, same you(読者の困りごとを読者の言葉で。en / ja をコピペ可能な fenced block で併記)、§4bWhat it does not do(常駐ではなく 1 回のラン・コードは読まない・別エージェントのレビューは利用者が足すステップ・承認は人に来る・tmux / worktree / ターミナル / CLI を置き換えない、の 5 行)、§11b言えること / 言えないこと(実測の範囲)(言える: lead は Claude Code / Command Code で 4 Issue → 4/4・8m39s / 7m46s、worker は Codex / Claude Code / Antigravity / Command Code、別 CLI レビューで REJECT → 修正 → APPROVE、plan は dry-run、mutation は--approveの invocation だけ、gate 不合格で停止、修正ループは上限つき / 言えない:your orchestratorruns 24/7self-managing backloglooporchestrate with any agent as the leadreview by a fresh agent is built inthe only …no supervisionとモデルの序列・未計測の数字)。節番号は 1〜12 を 1 つも動かしていない — 新設を1b/4b/11bの英字サフィックスで挿入したのは、.claude/skills/demo-video/storyboard/readme-hero.yaml:39,42とdocs/images/features/storyboards/{01,03,11}-*.yamlがpublic-messaging.md §5/§6を番号で指しており、振り直すと絵コンテ側の由来コメントが一斉に嘘になるため。§3 の 4 カードは順序と題を入れ替え(1One agent leads(旧Any agent, in parallelを吸収)/ 2Verified, not vibe-checked(「証拠は要約ではなくラン」の 1 文を追加)/ 3Know which one needs you(旧Stay in control, anywhereを改題し、入力待ちが hooks 由来の状態であることを明示)/ 4Method as a system)、§5 のデモ 4 本の「対応カード」列を新カード番号へ付け替えた。§6 のテロップ表(16 行)は 1 文字も触っていない — デモ 3 のacross seven agent CLIs/7 種の CLI から選べるは撮影済み映像に焼かれtests/unit/skills/demo-video/storyboard.test.tsが逐語で固定しているので、「数の権威は §11 のCLI_TOOL_IDS= 8 id であり、直すには映像と絵コンテを撮り直す(別 Issue)」という注記を §6 に置いた(#2300 が残した「§3 / §5 が seven のまま §11 と自己矛盾」のうち §3 は本 Issue のカード差し替えで数を述べない文言になり解消、残るのは §6 のテロップ 1 箇所のみ)。禁止語は表とBANNED_TERMS配列へcontrol layer/コントロールレイヤーを同じ commit で追加し、片方だけ落とす変異注入 2 本(表の行を削る / 配列の要素を削る)でいずれもpublishes the banned-term list this test enforcesが 1 件赤になることを実測してから戻した。追加前にREADME.md/docs/ja/README.md/docs/concept.md/docs/en/concept.md/website/index.htmlを大文字小文字無視で grep し全 0 件であることを確認済み(実在すればuses none of the banned termsが即赤になる)。§12 の後続 Issue 表に #2494(README)/ #2495(LP)を追加。README / LP 本体・docs/concept.mdの Mission / Vision・§4 の With / Without 表は本 Issue では触っていない。検証:public-messaging.test.ts22/22 緑、landing-page.test.ts+storyboard.test.ts282/282 緑(def:enマーカーと hero テロップ 8 行の照合を含む)
Fixed
-
fix(test): unit テストが本物の
~/.commandmate/opencode-ports.jsonを書き換える問題を修正 (#2490):tests/setup.tsにCM_OPENCODE_PORT_FILEの隔離既定($TMPDIR/commandmate-test-opencode-ports/<worker pid>.json)を追加し、スイートが opencode のポート台帳へ到達できないようにした。既定値~/.commandmate/opencode-ports.jsonは本番サーバの生きた状態で、rememberOpencodePort()/forgetOpencodePort()は read-modify-write なので、テストは「行を足す」のではなく読んだ内容でファイル全体を書き戻す — その間にサーバが作った割り当ては消え、pane は生きたままポートが二度と見つからず、そのセッションは最後まで scraper に落ちる(reattach.ts:220/slash-commands/opencode-live.ts:113が読む先)。#1942 のCOPILOT_HOMEと違い将来への柵ではなく実害の後始末で、2026-09-12 実測では実ファイルの 7 エントリが全て fixture(wt-alpha/wt-beta/wt-1898/wt-respond/wt-recheck、いずれも/tmp/wt*)=利用者自身の割り当ては既に消えていた。opencode テスト 19 本は自分でCM_OPENCODE_PORT_FILEを向けており問題ではなく、漏れていたのは module を import するだけで既定に落ちる 10 本前後(respond-decision-id-1932/respond-question-2039/respond-sole-decision-2040ほか)なので、修正は気づいたファイルではなく既定側に置いた。verifyの env-clean が見逃していたのは構造的な理由による — 同ゲートは~/.commandmate直下のエントリ増減しか見ず、既存ファイルの中身が書き換わっても数は動かない(#2487 の run 750 はcommandmate-entries cleanを返しながらこれをやっていた)。COPILOT_HOMEと並ぶ 2 本目の worker pid 配下にしたのは、この既定にも実在の書き手がいて書き込みが単一 JSON 文書への read-modify-write だからで、固定名だと並列 checkout 同士が互いのキーを落とす。空文字は未設定扱い(resolveSafeDirectoryが''を未設定と読んで homedir へ戻すため)、vi.stubEnvと明示代入は従来どおり既定に勝つ。ガードはtests/unit/lib/hooks/sources/opencode/port-file-isolation-2490.test.ts(#1873 / #1942 と同型: 既定が外れたら赤になり、tautology でないことを「変数を消すと実ファイルを指す」で裏取りし、実書き込みを 1 本流して pinned 側に載り実台帳には載らないことまで見る(実台帳とのバイト列一致は敢えて見ない — 同居する本番サーバの正当な書き込みで赤くなるガードは他人の正しい挙動を報告するだけなので))。実測: 通常のnpm run test:unit全体で実台帳は mtime・バイト列とも不変、HOMEを空の番兵ディレクトリへ向けた unit 全体でも.commandmate/opencode-ports.jsonは作られない。 -
fix(api):
POST /sendだけが旧い解決順のままで、登録行の無い--instance <tool>と別 tool の要求が食い違いにならなかった問題を修正 (#2491):POST /api/worktrees/:id/sendの tool / instance 解決を、#1925 で一本化したresolveSessionTargetStrict()(src/lib/session/resolve-session-target.ts)へ付け替えた。この route だけが最後までresolveInstanceCliTool()(src/lib/db/agent-instances-db.ts)を使っており、その解決順は「roster の登録行 → 要求された tool → tool 名の instance id(#868 の primary anchor)」なので、登録行が無いと要求された tool をそのまま採る — #2487 が入れた規則(登録行が無くても tool 名の instance id は自分の tool を宣言しており、別 tool の要求は食い違い)がこの door には届いていなかった。結果、登録行の無い worktree で{"instanceId": "antigravity", "cliToolId": "command-code"}を投げると 400 にならずmcbd-command-code-<wt>-antigravityを立ててそこへ送る(instance 名だけを名乗る側からは誰も読まないセッション)。今後は ほかの副作用 route(kill-session/terminal/respond/auto-yesPOST、DR3-015)と同じく 400instance_tool_conflict+describeSessionTargetConflict()の文言+ conflict の各フィールド(instanceId/rosterCliTool/requestedCliTool/ primary anchor のときだけprimaryAnchor: true)を返し、tmux には一切触れない(CLIToolManager.getTool()より手前で return する)。CLI は挙動不変 —send/ask/respondは送る前に/resolve-target(strict)で解決するので #2487 のマージ後は exit 2 で止まり、この route には届かない。Web UI も挙動不変(roster の行から両方を取るので食い違わない)。影響を受けるのは API を直接叩く経路(スクリプト・Skill 等)だけで、そこでも従来どおり送れるものは全部送れる: ad-hoc の id(codex-3)+cliToolIdの--register経路、tool 名の id 単独、instance も tool も無い要求(worktree 既定へ)、roster と一致する明示指定。roster との食い違いは #1629 から一貫して 400 で、本件でcodeが付いた。あわせてresolveRelaySession()(src/lib/relay/relay-session-ref.ts)も同じ resolver へ揃えた — relay は tool を名指ししないので 4 段の解決結果は完全に同一(配送の挙動は変えていない)で、揃えたのは「/sendと同じ権威を使う」という #2377 の docblock の主張を真に戻すため。route 自前のDEFAULT_CLI_TOOL = 'claude'は削除した(設計 §4 D5 決定 4 が literal を許すのはresolve-session-target.tsだけで、worktree.cliToolId || 'claude'の意味は resolver の worktree-default / fallback 段そのもの)。resolveInstanceCliTool()は本件で本番の呼び出し元がゼロになったため@deprecatedとし、新規の呼び出しを禁じる旨を docblock に明記した(#1629 の unit テストが残っているので削除は別件)。 -
fix(scripts):
stop.sh→build-and-start.sh --daemonが旧サーバーの終了処理と競合して「Server is already running」でビルド前に中断し、サーバーが落ちたまま残る問題を修正 (#2488): 停止の判定をポートの静けさからプロセスの生死へ移した。server.tsのgracefulShutdownは待ち受けソケットを先に閉じ、残っている接続(ダッシュボードを開いたタブの keep-alive)を最大 3 秒待ってから強制終了するので、SIGTERM と exit の間には「誰も LISTEN していないのに旧プロセスは生きていてlogs/server.pidも握ったまま」という窓ができる。stop.shはその窓をsleep 2+ LISTEN 再検索で「停止済み」と読んで戻り、&&で続くbuild-and-start.sh --daemonが生きている PID を見てビルド前に exit 1していた(2026-09-11 22:44〜22:47 JST、本番ポート 3000 に LISTEN 無し・.next/BUILD_ID据え置き・GET /が 000 のまま 3 分間)。stop.sh/stop-server.shは自分が SIGTERM を送った PID そのものをkill -0で 100ms 間隔にポーリングし、猶予(CM_STOP_GRACE_SECONDS、既定 10 秒 = server.ts の 3 秒強制終了より長い)を超えた分だけ SIGKILL へ上げる。stop.shにはstop-server.shにしか無かった PID ファイル段(nohup npm start= node の親。LISTEN しないのでポート検索には決して映らず、まさにServer is already runningとして読み返される PID)を入れ、2 つの停止スクリプトの挙動を揃えた。build-and-start.sh/start.shは PID ファイルの PID が生きていても即断せず同じ猶予だけ待ち、待っても生きていれば従来どおり exit 1(本当に動いているサーバーは猶予を越えるので拒否は変わらない)。server.ts側はserver.close()の後にserver.closeIdleConnections()を足して、リクエストが飛んでいない keep-alive ソケットだけを落とす(処理中のリクエストは idle ではないので従来どおり 3 秒もらえる)——窓そのものが通常ミリ秒に縮む。#2473 の要件は維持: 待つのも殺すのも LISTEN 限定の検索が返した PID だけなので、ポートに接続しているだけのプロセス(ブラウザの network service、他セッションの CLI)は依然として一切触らない。検証はtests/unit/scripts/stop-waits-for-exit-2488.test.ts— 終了に 3 秒かかるサーバーを模した孤児プロセス+接続中クライアントで、stop.shが✓ Application stoppedを出した時点のkill -0が ESRCH であること、stop.sh && start.sh --daemonが中断せず起動しきること、変異対照(wait_for_exitを修正前のsleep 2+ LISTEN 再検索に戻すと旧プロセスが生きたまま「停止済み」と出る)まで含む。